A Deno-powered backend service for Plants vs. Zombies: MODDED. [Read-only GitHub mirror] docs.pvzm.net
express typescript expressjs plant deno jspvz pvzm game online backend plants-vs-zombies zombie javascript plants modded vs plantsvszombies openapi pvz noads
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 790 lines 30 kB view raw
1openapi: 3.0.3 2info: 3 title: PVZM Backend API 4 description: "API for the Plants vs. Zombies: MODDED level sharing platform. Supports level uploading, downloading, browsing, favoriting, reporting, and admin management." 5 version: 0.6.5 6 contact: 7 url: https://pvzm.net 8 9servers: 10 - url: https://backend.pvzm.net 11 description: Production 12 13tags: 14 - name: Health 15 description: Health check endpoints 16 x-page-icon: heart-pulse 17 - name: Config 18 description: Server configuration 19 x-page-icon: gear 20 - name: Auth 21 description: GitHub OAuth authentication 22 x-page-icon: lock 23 - name: Admin 24 description: Admin level management 25 x-page-icon: shield 26 - name: I, Zombie 27 x-page-icon: skull 28 - name: Levels 29 description: Public level operations 30 x-parent: I, Zombie 31 x-page-icon: layer-group 32 33paths: 34 /api/health: 35 get: 36 tags: [Health] 37 summary: Health check 38 operationId: getHealth 39 responses: 40 "200": 41 description: Server is healthy 42 content: 43 application/json: 44 schema: 45 type: object 46 required: [status, timestamp, version] 47 properties: 48 status: 49 type: string 50 example: ok 51 timestamp: 52 type: string 53 format: date-time 54 version: 55 type: string 56 example: 0.6.5 57 58 /api/config: 59 get: 60 tags: [Config] 61 summary: Get server configuration 62 operationId: getConfig 63 responses: 64 "200": 65 description: Current server configuration 66 content: 67 application/json: 68 schema: 69 type: object 70 required: [turnstileEnabled, turnstileSiteKey, moderationEnabled] 71 properties: 72 turnstileEnabled: 73 type: boolean 74 turnstileSiteKey: 75 type: string 76 nullable: true 77 moderationEnabled: 78 type: boolean 79 80 /api/levels: 81 get: 82 tags: [Levels] 83 summary: List levels 84 description: Retrieve a paginated, filterable list of levels. 85 operationId: getLevels 86 parameters: 87 - name: page 88 in: query 89 schema: 90 type: integer 91 default: 1 92 minimum: 1 93 - name: limit 94 in: query 95 schema: 96 type: integer 97 default: 10 98 minimum: 1 99 - name: sort 100 in: query 101 schema: 102 type: string 103 enum: [plays, recent, favorites, featured] 104 default: plays 105 - name: reversed_order 106 in: query 107 schema: 108 type: string 109 enum: ["true", "false"] 110 default: "false" 111 - name: author 112 in: query 113 description: Filter by author name (partial match) 114 schema: 115 type: string 116 - name: is_water 117 in: query 118 schema: 119 type: string 120 enum: ["true", "false"] 121 - name: version 122 in: query 123 schema: 124 type: integer 125 - name: token 126 in: query 127 description: One-time token to fetch a specific level by ID. Ignores all other filters when provided. 128 schema: 129 type: string 130 responses: 131 "200": 132 description: Paginated list of levels 133 content: 134 application/json: 135 schema: 136 type: object 137 required: [levels, pagination] 138 properties: 139 levels: 140 type: array 141 items: 142 $ref: "#/components/schemas/LevelSummary" 143 pagination: 144 $ref: "#/components/schemas/Pagination" 145 "429": 146 $ref: "#/components/responses/RateLimited" 147 148 post: 149 tags: [Levels] 150 summary: Upload a level 151 description: | 152 Upload a new level in IZL3 binary format. Rate limited to 1 upload per 60 seconds per IP. 153 154 The request body must be sent as raw binary (Content-Type: application/octet-stream) containing the IZL3 level file. 155 156 Validation includes: IZL3 format check, name/author content moderation (OpenAI + bad-words filter), 157 plant/zombie placement rules, and optional Turnstile CAPTCHA verification. 158 operationId: createLevel 159 parameters: 160 - name: author 161 in: query 162 required: true 163 description: Author name (max 11 characters) 164 schema: 165 type: string 166 maxLength: 11 167 - name: turnstileResponse 168 in: query 169 description: Cloudflare Turnstile CAPTCHA token (required if Turnstile is enabled) 170 schema: 171 type: string 172 responses: 173 "201": 174 description: Level created successfully 175 content: 176 application/json: 177 schema: 178 type: object 179 required: [id, name, author, created_at, sun, is_water, version] 180 properties: 181 id: 182 type: integer 183 name: 184 type: string 185 author: 186 type: string 187 created_at: 188 type: integer 189 description: Unix timestamp 190 sun: 191 type: integer 192 is_water: 193 type: boolean 194 version: 195 type: integer 196 "400": 197 $ref: "#/components/responses/BadRequest" 198 "415": 199 description: Unsupported media type (Content-Type must be application/octet-stream) 200 content: 201 application/json: 202 schema: 203 $ref: "#/components/schemas/Error" 204 "429": 205 $ref: "#/components/responses/RateLimited" 206 207 /api/levels/{id}: 208 get: 209 tags: [Levels] 210 summary: Get a level by ID 211 operationId: getLevel 212 parameters: 213 - $ref: "#/components/parameters/LevelId" 214 responses: 215 "200": 216 description: Level details 217 content: 218 application/json: 219 schema: 220 $ref: "#/components/schemas/LevelSummary" 221 "400": 222 $ref: "#/components/responses/BadRequest" 223 "404": 224 $ref: "#/components/responses/NotFound" 225 226 /api/levels/{id}/download: 227 get: 228 tags: [Levels] 229 summary: Download a level file 230 description: Downloads the IZL3 level file. Increments the play count. Rate limited to 5 downloads per 5 seconds per IP. 231 operationId: downloadLevel 232 parameters: 233 - $ref: "#/components/parameters/LevelId" 234 responses: 235 "200": 236 description: Level file download 237 headers: 238 Content-Disposition: 239 schema: 240 type: string 241 example: attachment; filename="MyLevel.izl3" 242 content: 243 application/octet-stream: 244 schema: 245 type: string 246 format: binary 247 "400": 248 $ref: "#/components/responses/BadRequest" 249 "404": 250 $ref: "#/components/responses/NotFound" 251 "429": 252 $ref: "#/components/responses/RateLimited" 253 254 /api/levels/{id}/report: 255 post: 256 tags: [Levels] 257 summary: Report a level 258 operationId: reportLevel 259 parameters: 260 - $ref: "#/components/parameters/LevelId" 261 requestBody: 262 required: true 263 content: 264 application/json: 265 schema: 266 type: object 267 required: [reason] 268 properties: 269 reason: 270 type: string 271 description: Description of why the level is being reported 272 responses: 273 "200": 274 description: Report submitted 275 content: 276 application/json: 277 schema: 278 type: object 279 required: [success] 280 properties: 281 success: 282 type: boolean 283 example: true 284 "404": 285 $ref: "#/components/responses/NotFound" 286 287 /api/levels/{id}/favorite: 288 post: 289 tags: [Levels] 290 summary: Toggle favorite on a level 291 description: Toggles the favorite state for the requesting IP. Rate limited to 30 actions per 10 seconds per IP. 292 operationId: toggleFavorite 293 parameters: 294 - $ref: "#/components/parameters/LevelId" 295 responses: 296 "200": 297 description: Favorite toggled 298 content: 299 application/json: 300 schema: 301 type: object 302 required: [success, level] 303 properties: 304 success: 305 type: boolean 306 example: true 307 level: 308 type: object 309 required: [id, name, author, favorites] 310 properties: 311 id: 312 type: integer 313 name: 314 type: string 315 author: 316 type: string 317 favorites: 318 type: integer 319 "400": 320 $ref: "#/components/responses/BadRequest" 321 "404": 322 $ref: "#/components/responses/NotFound" 323 "429": 324 $ref: "#/components/responses/RateLimited" 325 326 # --- Auth --- 327 328 /api/auth/github: 329 get: 330 tags: [Auth] 331 summary: Initiate GitHub OAuth login 332 description: Redirects the user to GitHub for OAuth authentication. 333 operationId: githubLogin 334 responses: 335 "302": 336 description: Redirect to GitHub OAuth 337 338 /api/auth/github/callback: 339 get: 340 tags: [Auth] 341 summary: GitHub OAuth callback 342 description: Handles the OAuth callback from GitHub. Redirects to `/admin.html` on success. 343 operationId: githubCallback 344 parameters: 345 - name: code 346 in: query 347 schema: 348 type: string 349 responses: 350 "302": 351 description: Redirect to admin page on success 352 353 /api/auth/status: 354 get: 355 tags: [Auth] 356 summary: Check authentication status 357 operationId: getAuthStatus 358 responses: 359 "200": 360 description: Authentication status 361 content: 362 application/json: 363 schema: 364 oneOf: 365 - type: object 366 required: [authenticated, user] 367 properties: 368 authenticated: 369 type: boolean 370 enum: [true] 371 user: 372 type: object 373 required: [username, displayName, profileUrl, avatarUrl] 374 properties: 375 username: 376 type: string 377 displayName: 378 type: string 379 profileUrl: 380 type: string 381 avatarUrl: 382 type: string 383 - type: object 384 required: [authenticated] 385 properties: 386 authenticated: 387 type: boolean 388 enum: [false] 389 390 /api/auth/logout: 391 get: 392 tags: [Auth] 393 summary: Logout 394 operationId: logout 395 responses: 396 "200": 397 description: Logged out successfully 398 399 # --- Admin --- 400 401 /api/admin/levels: 402 get: 403 tags: [Admin] 404 summary: List levels (admin) 405 description: Paginated level list with search. Requires GitHub OAuth. 406 operationId: getAdminLevels 407 security: 408 - githubOAuth: [] 409 parameters: 410 - name: page 411 in: query 412 schema: 413 type: integer 414 default: 1 415 - name: limit 416 in: query 417 schema: 418 type: integer 419 default: 10 420 - name: q 421 in: query 422 description: Search query (searches name, author, ID) 423 schema: 424 type: string 425 responses: 426 "200": 427 description: Admin level listing 428 content: 429 application/json: 430 schema: 431 type: object 432 required: [levels, total, page, limit, totalPages] 433 properties: 434 levels: 435 type: array 436 items: 437 $ref: "#/components/schemas/LevelRecord" 438 total: 439 type: integer 440 page: 441 type: integer 442 limit: 443 type: integer 444 totalPages: 445 type: integer 446 "401": 447 $ref: "#/components/responses/Unauthorized" 448 449 /api/admin/levels/{id}: 450 put: 451 tags: [Admin] 452 summary: Update a level 453 description: Update level metadata. Requires GitHub OAuth or a valid one-time token. 454 operationId: updateLevel 455 security: 456 - githubOAuth: [] 457 - oneTimeToken: [] 458 parameters: 459 - $ref: "#/components/parameters/LevelId" 460 - name: token 461 in: query 462 description: One-time admin token (alternative to OAuth) 463 schema: 464 type: string 465 requestBody: 466 required: true 467 content: 468 application/json: 469 schema: 470 type: object 471 properties: 472 name: 473 type: string 474 author: 475 type: string 476 sun: 477 type: integer 478 is_water: 479 type: integer 480 enum: [0, 1] 481 difficulty: 482 type: integer 483 favorites: 484 type: integer 485 plays: 486 type: integer 487 featured: 488 type: integer 489 enum: [0, 1] 490 featured_at: 491 type: integer 492 nullable: true 493 responses: 494 "200": 495 description: Level updated 496 content: 497 application/json: 498 schema: 499 type: object 500 required: [success, level] 501 properties: 502 success: 503 type: boolean 504 example: true 505 level: 506 $ref: "#/components/schemas/LevelRecord" 507 "400": 508 $ref: "#/components/responses/BadRequest" 509 "401": 510 $ref: "#/components/responses/Unauthorized" 511 "404": 512 $ref: "#/components/responses/NotFound" 513 514 delete: 515 tags: [Admin] 516 summary: Delete a level 517 description: Permanently deletes a level, its file, and all associated data. Requires GitHub OAuth or a valid one-time token. 518 operationId: deleteLevel 519 security: 520 - githubOAuth: [] 521 - oneTimeToken: [] 522 parameters: 523 - $ref: "#/components/parameters/LevelId" 524 - name: token 525 in: query 526 description: One-time admin token (alternative to OAuth) 527 schema: 528 type: string 529 responses: 530 "200": 531 description: Level deleted 532 content: 533 application/json: 534 schema: 535 type: object 536 required: [success] 537 properties: 538 success: 539 type: boolean 540 example: true 541 "400": 542 $ref: "#/components/responses/BadRequest" 543 "401": 544 $ref: "#/components/responses/Unauthorized" 545 "404": 546 $ref: "#/components/responses/NotFound" 547 548 /api/admin/levels/{id}/token: 549 post: 550 tags: [Admin] 551 summary: Generate a one-time token for a level 552 description: Creates a single-use token scoped to a specific level, allowing unauthenticated edit/delete access. 553 operationId: generateToken 554 security: 555 - githubOAuth: [] 556 parameters: 557 - $ref: "#/components/parameters/LevelId" 558 responses: 559 "200": 560 description: Token generated 561 content: 562 application/json: 563 schema: 564 type: object 565 required: [token, level_id] 566 properties: 567 token: 568 type: string 569 example: token_abc123... 570 level_id: 571 type: integer 572 "400": 573 $ref: "#/components/responses/BadRequest" 574 "401": 575 $ref: "#/components/responses/Unauthorized" 576 "404": 577 $ref: "#/components/responses/NotFound" 578 579 /api/admin/levels/{id}/feature: 580 post: 581 tags: [Admin] 582 summary: Feature a level 583 description: Marks a level as featured. Requires GitHub OAuth. 584 operationId: featureLevel 585 security: 586 - githubOAuth: [] 587 parameters: 588 - $ref: "#/components/parameters/LevelId" 589 responses: 590 "200": 591 description: Level featured 592 content: 593 application/json: 594 schema: 595 type: object 596 required: [success, level] 597 properties: 598 success: 599 type: boolean 600 example: true 601 level: 602 $ref: "#/components/schemas/LevelRecord" 603 "400": 604 $ref: "#/components/responses/BadRequest" 605 "401": 606 $ref: "#/components/responses/Unauthorized" 607 "404": 608 $ref: "#/components/responses/NotFound" 609 610 delete: 611 tags: [Admin] 612 summary: Unfeature a level 613 description: Removes the featured status from a level. Requires GitHub OAuth. 614 operationId: unfeatureLevel 615 security: 616 - githubOAuth: [] 617 parameters: 618 - $ref: "#/components/parameters/LevelId" 619 responses: 620 "200": 621 description: Level unfeatured 622 content: 623 application/json: 624 schema: 625 type: object 626 required: [success, level] 627 properties: 628 success: 629 type: boolean 630 example: true 631 level: 632 $ref: "#/components/schemas/LevelRecord" 633 "400": 634 $ref: "#/components/responses/BadRequest" 635 "401": 636 $ref: "#/components/responses/Unauthorized" 637 "404": 638 $ref: "#/components/responses/NotFound" 639 640components: 641 securitySchemes: 642 githubOAuth: 643 type: http 644 scheme: bearer 645 description: GitHub OAuth2 session-based authentication (via browser cookies) 646 oneTimeToken: 647 type: apiKey 648 in: query 649 name: token 650 description: Single-use token scoped to a specific level ID 651 652 parameters: 653 LevelId: 654 name: id 655 in: path 656 required: true 657 schema: 658 type: integer 659 description: Level ID 660 661 schemas: 662 LevelSummary: 663 type: object 664 required: [id, name, author, created_at, sun, is_water, favorites, plays, difficulty, version, featured, featured_at] 665 properties: 666 id: 667 type: integer 668 name: 669 type: string 670 author: 671 type: string 672 created_at: 673 type: integer 674 description: Unix timestamp 675 sun: 676 type: integer 677 is_water: 678 type: boolean 679 favorites: 680 type: integer 681 plays: 682 type: integer 683 difficulty: 684 type: integer 685 version: 686 type: integer 687 featured: 688 type: integer 689 enum: [0, 1] 690 featured_at: 691 type: integer 692 nullable: true 693 description: Unix timestamp 694 thumbnail: 695 type: array 696 nullable: true 697 description: Array of plant placement tuples [plantIndex, eleLeft, eleTop, eleWidth, eleHeight, zIndex] 698 items: 699 type: array 700 items: 701 type: number 702 703 LevelRecord: 704 type: object 705 required: [id, name, author, sun, is_water, difficulty, favorites, plays, version, featured, featured_at, logging_data] 706 properties: 707 id: 708 type: integer 709 name: 710 type: string 711 author: 712 type: string 713 sun: 714 type: integer 715 is_water: 716 type: integer 717 enum: [0, 1] 718 difficulty: 719 type: integer 720 favorites: 721 type: integer 722 plays: 723 type: integer 724 version: 725 type: integer 726 featured: 727 type: integer 728 enum: [0, 1] 729 featured_at: 730 type: integer 731 nullable: true 732 logging_data: 733 type: string 734 nullable: true 735 description: JSON string containing Discord/Bluesky message IDs for logging management 736 737 Pagination: 738 type: object 739 required: [total, page, limit, pages] 740 properties: 741 total: 742 type: integer 743 page: 744 type: integer 745 limit: 746 type: integer 747 pages: 748 type: integer 749 750 Error: 751 type: object 752 required: [error, message] 753 properties: 754 error: 755 type: string 756 message: 757 type: string 758 retryAfterSeconds: 759 type: number 760 description: Present on 429 responses 761 762 responses: 763 BadRequest: 764 description: Invalid input or validation failure 765 content: 766 application/json: 767 schema: 768 $ref: "#/components/schemas/Error" 769 NotFound: 770 description: Resource not found 771 content: 772 application/json: 773 schema: 774 $ref: "#/components/schemas/Error" 775 Unauthorized: 776 description: Authentication required 777 content: 778 application/json: 779 schema: 780 $ref: "#/components/schemas/Error" 781 RateLimited: 782 description: Too many requests 783 headers: 784 Retry-After: 785 schema: 786 type: integer 787 content: 788 application/json: 789 schema: 790 $ref: "#/components/schemas/Error"