From 0e7cb6d6a8be214e1ecb768b06e47f8f4a28da0d Mon Sep 17 00:00:00 2001 From: Pratiikpy Date: Sat, 13 Jun 2026 21:50:00 +0530 Subject: [PATCH] fix(share): use a static /api/share/proof dispatcher, not a dynamic route The dynamic api/share/[kind].ts route was shadowed by the catch-all rewrite (/(.*) -> /index.html) and never reached, which broke proof unfurls live (/v/:proofId returned the SPA shell). Static API files are matched in the filesystem phase, before rewrites, so the OG dispatcher moves back to the static api/share/proof.ts path. Resource-link rewrites now target /api/share/proof?kind=link; /v/:proofId stays kind=proof. --- packages/app/api/share/[kind].ts | 28 ---------------- .../{[kind].test.ts => dispatch.test.ts} | 27 ++++++++-------- packages/app/api/share/proof.ts | 32 +++++++++++++++++++ packages/app/vercel.json | 10 +++--- 4 files changed, 50 insertions(+), 47 deletions(-) delete mode 100644 packages/app/api/share/[kind].ts rename packages/app/api/share/{[kind].test.ts => dispatch.test.ts} (53%) create mode 100644 packages/app/api/share/proof.ts diff --git a/packages/app/api/share/[kind].ts b/packages/app/api/share/[kind].ts deleted file mode 100644 index 7c7c9a2..0000000 --- a/packages/app/api/share/[kind].ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * /api/share/:kind — single serverless function that fans out to the - * per-kind OG handlers in _lib. One function, two surfaces, so the - * resource-link previews don't push the deploy over the 12-function cap - * (see #380). - * - * kind=proof (default) -> /v/:proofId unfurl + bounce to /verify/:id - * kind=link -> /pay /claim /shop /fund /conditional-invoice - * unfurl + bounce to the SPA route - * - * vercel.json rewrites resolve /api/share/proof and /api/share/link to - * this dynamic route; Vercel injects the matched segment as req.query.kind. - * Fall back to parsing the path so the function is correct even if the - * query param is absent. - */ - -import proofHandler from "../_lib/share-proof.js"; -import linkHandler from "../_lib/share-link.js"; - -export default async function handler(req: any, res: any) { - let kind: string | undefined = req.query?.kind; - if (!kind) { - const path = new URL(req.url ?? "/", "http://x").pathname; - kind = path.split("/").filter(Boolean).pop(); - } - if (kind === "link") return linkHandler(req, res); - return proofHandler(req, res); -} diff --git a/packages/app/api/share/[kind].test.ts b/packages/app/api/share/dispatch.test.ts similarity index 53% rename from packages/app/api/share/[kind].test.ts rename to packages/app/api/share/dispatch.test.ts index d44a3a6..fa6ba62 100644 --- a/packages/app/api/share/[kind].test.ts +++ b/packages/app/api/share/dispatch.test.ts @@ -1,13 +1,12 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -// §15.x test for /api/share/[kind] — the single function that fans out -// to the per-kind OG handlers in _lib (one function, two surfaces, to -// stay under the 12-function deploy cap; see #380). The handlers -// themselves are covered by share-proof/share-link suites; this pins the -// dispatch: kind=link -> link card, default -> proof card, resolved from -// either req.query.kind (Vercel) or the request path (fallback). +// §15.x test for the api/share/proof dispatcher — one static function +// that fans out to the per-kind OG handlers in _lib (one function, two +// surfaces, to stay under the 12-function deploy cap; see #380). The +// handlers themselves are covered by the share-proof/share-link suites; +// this pins the dispatch: ?kind=link -> link card, default -> proof card. -import handler from "./[kind].js"; +import handler from "./proof.js"; interface MockRes { headers: Record; @@ -36,29 +35,29 @@ afterEach(() => { delete process.env.VERCEL_URL; }); -describe("share kind dispatch", () => { - it("kind=link via req.query routes to the link handler", async () => { +describe("share dispatch", () => { + it("?kind=link via the query string routes to the link handler", async () => { const res = makeRes(); await handler( - { url: "/api/share/link?type=conditional-invoice&chainId=421614&id=2", query: { kind: "link" }, headers: {} }, + { url: "/api/share/proof?kind=link&type=conditional-invoice&chainId=421614&id=2", headers: {} }, res, ); expect(res.body).toContain("Conditional invoice on Blank"); }); - it("kind=link inferred from the path when query.kind is absent", async () => { + it("?kind=link via req.query (Vercel-injected) routes to the link handler", async () => { const res = makeRes(); await handler( - { url: "/api/share/link?type=pay&id=alice", headers: {} }, + { url: "/api/share/proof?type=pay&id=alice", query: { kind: "link", type: "pay", id: "alice" }, headers: {} }, res, ); expect(res.body).toContain("Get paid privately on Blank"); }); - it("default (proof) handler when kind is not link", async () => { + it("no kind -> proof handler (the /v/:proofId default)", async () => { const res = makeRes(); // No proof id -> generic proof copy, no chain read needed. - await handler({ url: "/api/share/proof", query: { kind: "proof" }, headers: {} }, res); + await handler({ url: "/api/share/proof", headers: {} }, res); expect(res.body).toContain("Encrypted proof on Blank"); }); }); diff --git a/packages/app/api/share/proof.ts b/packages/app/api/share/proof.ts new file mode 100644 index 0000000..c7f035d --- /dev/null +++ b/packages/app/api/share/proof.ts @@ -0,0 +1,32 @@ +/** + * /api/share/proof — single static serverless function that fans out to + * the per-kind OG handlers in _lib. One function, two surfaces, so the + * resource-link previews don't push the deploy over the 12-function cap + * (#380). + * + * Why a static route (not api/share/[kind].ts): + * A dynamic [kind] route is resolved AFTER vercel.json `rewrites`, so + * the catch-all `/(.*) -> /index.html` shadows it and the function is + * never reached (verified live: /api/share/ returned the SPA + * shell). Static API files are matched in the filesystem phase, before + * rewrites — the same reason /api/health and the original proof route + * always worked. So both surfaces ride this one static path: + * + * /v/:proofId -> /api/share/proof (kind=proof) + * /pay /claim /shop -> /api/share/proof?kind=link&...(kind=link) + * /fund /conditional-invoice + * + * Vercel injects the destination query onto req.query; we read ?kind to + * pick the handler and fall back to the proof handler (the original + * behaviour) when it is absent. + */ + +import proofHandler from "../_lib/share-proof.js"; +import linkHandler from "../_lib/share-link.js"; + +export default async function handler(req: any, res: any) { + const url = new URL(req.url ?? "/", "http://x"); + const kind = req.query?.kind ?? url.searchParams.get("kind"); + if (kind === "link") return linkHandler(req, res); + return proofHandler(req, res); +} diff --git a/packages/app/vercel.json b/packages/app/vercel.json index 1ccde99..ee149d8 100644 --- a/packages/app/vercel.json +++ b/packages/app/vercel.json @@ -30,11 +30,11 @@ { "source": "/v/:proofId", "destination": "/api/share/proof?id=:proofId" }, { "source": "/api/relayer-health", "destination": "/api/health?kind=relayer" }, { "source": "/api/reconcile-user", "destination": "/api/cron/reconcile-user" }, - { "source": "/conditional-invoice/:chainId/:escrowId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/link?type=conditional-invoice&chainId=:chainId&id=:escrowId" }, - { "source": "/pay/:identifier", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/link?type=pay&id=:identifier" }, - { "source": "/claim/:chainId/:linkId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/link?type=claim&chainId=:chainId&id=:linkId" }, - { "source": "/shop/:chainId/:listingId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/link?type=shop&chainId=:chainId&id=:listingId" }, - { "source": "/fund/:chainId/:campaignId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/link?type=fund&chainId=:chainId&id=:campaignId" }, + { "source": "/conditional-invoice/:chainId/:escrowId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/proof?kind=link&type=conditional-invoice&chainId=:chainId&id=:escrowId" }, + { "source": "/pay/:identifier", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/proof?kind=link&type=pay&id=:identifier" }, + { "source": "/claim/:chainId/:linkId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/proof?kind=link&type=claim&chainId=:chainId&id=:linkId" }, + { "source": "/shop/:chainId/:listingId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/proof?kind=link&type=shop&chainId=:chainId&id=:listingId" }, + { "source": "/fund/:chainId/:campaignId", "missing": [{ "type": "query", "key": "app" }], "destination": "/api/share/proof?kind=link&type=fund&chainId=:chainId&id=:campaignId" }, { "source": "/(.*)", "destination": "/index.html" } ], "headers": [