Skip to content

fix(api): consolidate admin endpoints below the Vercel Hobby function cap (Task 8.6) - #12

Merged
carlosjosemm merged 2 commits into
mainfrom
fix/task-8.6-api-consolidation
Sep 23, 2026
Merged

carlosjosemm merged 2 commits into
mainfrom
fix/task-8.6-api-consolidation

Conversation

@carlosjosemm

Copy link
Copy Markdown
Owner

🎯 Context & Motivation

Reference: PRODUCTION_READINESS_TODO.md → 8.6. Consolidate api/ Endpoints Below the Vercel Hobby Function Cap 🔴 blocks every deployment.

Every deployment — preview and production — was refused at the output stage after a successful build:

Error: No more than 12 Serverless Functions can be added to a Deployment
on the Hobby plan. Create a team (Pro plan) to deploy more.

The TODO counted 15 endpoints; the real count was 22, because Vercel counts every file under api/ unless its path contains a _-prefixed segment. Collapsing the admin endpoints and relocating shared code behind api/_lib/ takes api/ to 6 functions against the ceiling of 12.

Lifting the cap then exposed two further pre-existing runtime blockers that had never been reachable (no deploy had succeeded since 2026-09-17, and production still serves a much older build). Both are fixed here, because a deployment whose functions all return HTTP 500 does not count as "unblocked".


🛠️ Summary of Changes

API / Services — the cap (8.6 core)

  • Renamed api/lib/ → api/_lib/ (6 shared modules) and api/admin/*.ts → api/_lib/admin/*.ts (11 handlers, moved verbatim — git diff -M confirmed import-lines-only, 54 insertions / 54 deletions).
  • New api/admin/[action].ts — a single routed entry point. A plain Record<string, handler> table maps the 11 action names; unknown/missing/empty/non-string actions return 404 { success: false, error: 'Endpoint de administración no encontrado' } and log an [Admin Router] warning. Inherited prototype keys (constructor, __proto__, …) are rejected by an own-property check.
  • Public URLs are unchanged. Vercel emits ^/api/admin/([^/]+)$ → /api/admin/[action]?action=$1, so src/admin/services/adminApi.ts and vercel.json needed zero changes.
  • No framework was introduced — no Express/NestJS/Koa/Fastify, no middleware pipeline. Each delegated handler keeps its own CORS headers, OPTIONS preflight, method gate, verifyAdminToken call, and error handling.

Runtime blocker A — extensionless ESM imports (pre-existing)

Vercel does not bundle api/; it transpiles each file in place and ships the tree, so Node's ESM resolver runs at request time. With "type": "module" in package.json, every extensionless relative import ('./_lib/firebaseAdmin') threw ERR_MODULE_NOT_FOUND — every api/ function returned FUNCTION_INVOCATION_FAILED, including the untouched public endpoints.

  • Appended .js to relative value imports across api/ plus src/utils/schemaValidation.ts's ./rut.
  • Type-only imports targeting a directory ('../../../src/types') were deliberately left extensionless: they are erased at transpile time, and appending .js would break TypeScript's directory resolution.
  • Proven pre-existing: building main (e4206bc) in a throwaway worktree produced a create-preference.js with identical extensionless specifiers.

Runtime blocker B — jwks-rsa CJS requiring ESM-only jose (pre-existing)

firebase-admin → jwks-rsa@4 is CommonJS and calls require('jose') at module load, while jose@6 is ESM-only — so merely importing firebase-admin/auth crashed with ERR_REQUIRE_ESM, taking down every admin route.

  • Upstream: auth0/node-jwks-rsa#507, firebase/firebase-admin-node#3181. The fix (PR #508) is merged but unreleased (jwks-rsa latest is still 4.1.0).
  • Worked around with pnpm.overrides pinning jose to ^5.10.0 — the last dual CJS/ESM major. jwks-rsa only uses jose.importJWK/exportSPKI, which the upstream issue confirms are API-identical across jose 4/5/6.
  • Accepted trade-off: jose v5 is EOL per its own SECURITY.md. The CVE that motivated the v6 bump (CVE-2025-45767) is disputed by the maintainer and specific to v6.0.10.
  • Removal condition (documented in api/AGENTS.md §1.3): drop the override once jwks-rsa > 4.1.0 ships, then re-verify firebase-admin/auth on a preview deploy.

Tests

  • New src/tests/api/admin/admin-router.test.ts (11 tests): happy path, unknown/missing/empty/non-string action → 404, array normalization, whitespace trim, OPTIONS passthrough, all 11 actions mapped, and rejection of inherited prototype keys.
  • 16 existing suites updated for the new import paths only — assertions untouched.

Docs / tooling

  • api/AGENTS.md — new §1.2 (function layout, router, _lib exclusion) and §1.3 (ESM resolution invariants + the jose override removal condition).
  • root AGENTS.md — §2.2 records the routed-entry-point exception; §7 gains "a green build does NOT mean the functions run".
  • .vercel/** added to eslint.config.js ignores — a local vercel build otherwise surfaced 2800 artifact errors (my code contributed zero; verified by parsing the JSON report). Consistent with the existing dist/** / coverage/** / public/** entries.
  • PRODUCTION_READINESS_TODO.md — 8.6 marked resolved, both runtime blockers recorded, acceptance criterion 1 checked.

🇨🇱 Chilean Localization Compliance

  • No changes to currency, tax, or identity logic. Integer CLP handling, Math.round() 19% IVA math, and Modulo 11 RUT validation are untouched.
  • Handler bodies moved verbatim, so all Chilean business behaviour is preserved: transfer approval decrements dev_*/production stock inside a Firestore transaction, America/Santiago timezone bucketing in dashboard-stats, and Boleta/Factura field handling in the admin inspector.
  • Error copy stays in Spanish ('Endpoint de administración no encontrado', 'Método no permitido').

🧪 Verification & Quality Assurance

Automated:

Gate Result
pnpm test 429 passed / 59 suites (baseline 418/58, +11 new router tests)
pnpm build ✅ clean
pnpm lint ✅ 0 errors
pnpm format:check ✅ all files conform
vercel build ✅ Build Completed — 6 functions, no new TS diagnostics (same 8 pre-existing TS7006/TS2503)

Live preview deploy (end-to-end): ✓ Ready in 23s — verified with vercel curl (which bypasses preview SSO):

Route Status Interpretation
GET /api/admin/{dashboard-stats,orders,order-history,products} 403 Handler reached → verifyAdminToken rejects absent Bearer
GET /api/admin/{approve-transfer,dispatch-order,mark-delivered,update-stock,update-product,create-product,toggle-visibility} 405 POST-only method gates intact
GET /api/admin/nonexistent 404 Dispatcher's own 404 branch
OPTIONS /api/admin/orders 200 Delegated preflight (access-control-allow-methods: GET, OPTIONS)
GET /api/{create-preference,order-confirmation,track-order,upload-voucher} 405 Public POST-only endpoints load
GET /api/webhooks/mercadopago 200 Intentional pre-existing GET ping ({status:"ok"})

Function logs recorded zero errors across all requests. The 403/OPTIONS responses carry the delegated handlers' CORS headers, proving the handler modules executed rather than the dispatcher short-circuiting.

Not proven (stated honestly): an authenticated end-to-end operation (e.g. approving a transfer and observing stock decrement). That requires a real Firebase admin ID token, which was not handled. Note that preview deployments resolve to dev_* collections via getCollectionName(), so that manual step is safe to perform without touching production.


🛡️ Strict Guardrails Verification

  • ❌ No heavy state library, CSS framework, ORM, or backend framework added — the dispatcher is a plain lookup table.
  • ✅ Single-purpose functions preserved; the one routed entry point is a documented exception in root AGENTS.md §2.2 and api/AGENTS.md §1.2, forced by the Hobby cap.
  • ✅ No secrets, no client-side payment/stock authority changes, firestore.rules untouched, verifyAdminToken + admin custom-claim gate unchanged.
  • ✅ Integer CLP / Modulo 11 / SII logic untouched.
  • ✅ pnpm.overrides was added only after explicit human sign-off (it is a dependency pin, not a security-policy relaxation — no .npmrc settings were modified).
  • ⚠️ vercel --prod remains gated by the human-produced public/og-preview.png asset (root AGENTS.md §7). Production has not been touched.

carlosjosemm and others added 2 commits September 23, 2026 17:46
… cap (Task 8.6)

Restores deployability and makes the serverless layer actually run.

Layout (the cap):
- api/lib/ -> api/_lib/ and api/admin/*.ts -> api/_lib/admin/*.ts (paths
  containing a `_`-prefixed segment are excluded from Vercel's function
  count); the 11 handlers moved verbatim.
- New api/admin/[action].ts dispatches /api/admin/<action> through a plain
  Record<string, handler> table. Public URLs and
  src/admin/services/adminApi.ts are unchanged.
- api/ now counts 6 functions against the Hobby ceiling of 12 (was 22).

Runtime blocker A - extensionless ESM imports:
- Vercel transpiles api/ in place rather than bundling, so Node's ESM
  resolver runs at request time. With `"type": "module"` in package.json,
  every extensionless relative import threw ERR_MODULE_NOT_FOUND and every
  endpoint returned FUNCTION_INVOCATION_FAILED despite a green build.
- Appended .js to relative value imports across api/ plus
  src/utils/schemaValidation.ts's ./rut. Type-only imports targeting a
  directory stay extensionless (they are erased at transpile).
- Proven pre-existing by building main (e4206bc) in a throwaway worktree:
  its emitted create-preference.js carried identical extensionless
  specifiers.

Runtime blocker B - jwks-rsa CJS requiring ESM-only jose:
- firebase-admin -> jwks-rsa@4 is CommonJS and calls require('jose') at
  module load, but jose@6 is ESM-only, so merely importing
  firebase-admin/auth crashed with ERR_REQUIRE_ESM and took down every
  admin route. Upstream: auth0/node-jwks-rsa#507 and
  firebase/firebase-admin-node#3181; the fix (PR #508) is merged but
  unreleased (jwks-rsa latest is still 4.1.0).
- Worked around with pnpm.overrides pinning jose to ^5.10.0 - the last dual
  CJS/ESM major; jwks-rsa only uses importJWK/exportSPKI, which are
  API-identical across jose 4/5/6. Remove once jwks-rsa > 4.1.0 ships.

Verification:
- New src/tests/api/admin/admin-router.test.ts (11 tests): happy path,
  unknown/missing/empty/non-string action -> 404, array normalization,
  whitespace trim, OPTIONS passthrough, all 11 actions mapped, and
  rejection of inherited prototype keys.
- 429 tests across 59 suites pass; pnpm build, pnpm lint and
  pnpm format:check are clean.
- Live preview verified: 6 functions, all 11 admin actions route
  (GET -> 403 auth, POST-only -> 405, unknown -> 404, OPTIONS -> 200),
  zero runtime errors in the function logs.

Docs:
- api/AGENTS.md new 1.2 (function layout + router) and 1.3 (ESM resolution
  invariants, jose override removal condition).
- root AGENTS.md 2.2 (routed-entry-point exception) and 7 ("a green build
  does not mean the functions run").
- .vercel/** added to eslint ignores - a local `vercel build` otherwise
  surfaced 2800 artifact errors; test counts refreshed to 429/59.
- PRODUCTION_READINESS_TODO 8.6 resolved, recording both runtime blockers.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…uilt state

7.3 asked for `favicon.ico` / `apple-touch-icon.png` and an OG image
"featuring company logo and Melipilla delivery badge". All three are
superseded by the as-built repo:

- `public/` already exists (it holds `favicon.svg` and `assets/`).
- `favicon.svg` is Appendix B.2's turnkey SVG, already committed and wired
  via `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />`; no
  .ico and no apple-touch-icon are used by the markup.
- There is no delivery badge: Appendix B.1's approved composition carries a
  "Deposito dental - Melipilla y San Antonio" text line instead.

The entry now points at UI_UX_EVALUATION_AND_REDESIGN_PROPOSAL.md
Appendix B as the asset spec of record (as root AGENTS.md 7 and
src/components/AGENTS.md 2.2 already do), records the favicon as delivered,
and lists the three assets still owed - with `public/og-preview.png`
flagged as the hard `vercel --prod` gate.

The audit-table row at line 38 is deliberately left untouched: it is a
historical snapshot and remains accurate ("meta tags implemented; missing
actual image file").

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@carlosjosemm
carlosjosemm merged commit 0d7f616 into main Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant