create-ailk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -1,471 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Payment-link MANAGEMENT route — emitted by `scaffold-commerce`'s
|
|
3
|
-
* surface-selection branch (SKILL.md § Surface-selection branch, Step 10e-ii,
|
|
4
|
-
* FEAT-023 U3). Three Fastify handlers: list, create (from a `lookupKey`),
|
|
5
|
-
* archive/restore (by id).
|
|
6
|
-
*
|
|
7
|
-
* Instantiated into the consumer's own `apps/api/src/routes/payment-links/`,
|
|
8
|
-
* alongside `stripe-config.ts` (Step 10d) — the SAME home as the checkout
|
|
9
|
-
* sibling, and for the same two reasons:
|
|
10
|
-
* 1. `architecture.yaml`: `apps/web` MAY NOT import `packages/stripe-client`
|
|
11
|
-
* ("billing is org-scoped and server-side via apps/api"). Stripe secrets
|
|
12
|
-
* never enter the Next.js app.
|
|
13
|
-
* 2. `apps/api` is where the auth middleware this route depends on LIVES.
|
|
14
|
-
* (An earlier revision of this template was a Next.js App Router handler.
|
|
15
|
-
* It shipped with NO authentication at all — AILK#3744, a P0. A management
|
|
16
|
-
* surface mounted in a runtime that has no auth preHandler in front of it
|
|
17
|
-
* is a hole waiting to happen; this one was.)
|
|
18
|
-
*
|
|
19
|
-
* ── Auth posture — AUTHENTICATED + SITE-ADMIN ONLY (the opposite of checkout) ─
|
|
20
|
-
* This is a MANAGEMENT surface: it LISTS, CREATES, and ARCHIVES payment
|
|
21
|
-
* objects. The checkout sibling (`checkout-route.template.ts`) is deliberately
|
|
22
|
-
* public — it is a storefront "buy" action that can only ever create one new
|
|
23
|
-
* Checkout Session for one allowlisted product. Its own header says it plainly:
|
|
24
|
-
* "A management surface (list/update/archive payment objects) is a DIFFERENT
|
|
25
|
-
* shape and MUST be authenticated". This is that surface. Do not copy the
|
|
26
|
-
* checkout route's public posture here.
|
|
27
|
-
*
|
|
28
|
-
* Without authentication, an anonymous caller could:
|
|
29
|
-
* - GET -> read every Payment Link on the site (ids, buy URLs, active flags)
|
|
30
|
-
* - PATCH -> archive them; iterating the GET output deactivates EVERY link:
|
|
31
|
-
* total denial of revenue, in two curl calls
|
|
32
|
-
* - POST -> mint Payment Links against the merchant's own Stripe account
|
|
33
|
-
*
|
|
34
|
-
* ── Entitlement is NOT authorization (the AILK#3744 reasoning error) ──────────
|
|
35
|
-
* `assertCommerceEntitled()` reads the SITE's license from env. It returns the
|
|
36
|
-
* IDENTICAL answer for every request, anonymous or not — it answers "does this
|
|
37
|
-
* DEPLOYMENT have commerce?", never "is this CALLER allowed to manage payment
|
|
38
|
-
* links?". It is a correct check and it fails closed, but it is the WRONG AXIS,
|
|
39
|
-
* so failing closed buys nothing on its own. It is kept below, LAYERED UNDER
|
|
40
|
-
* the identity gate — never as a substitute for it.
|
|
41
|
-
*
|
|
42
|
-
* ── Registration — LOAD-BEARING; the route is fail-closed if you get it wrong ─
|
|
43
|
-
* Register INSIDE the authenticated scope, so `authPreHandlerPlugin` decorates
|
|
44
|
-
* `request.userId` (and applies its same-origin CSRF check) before any handler
|
|
45
|
-
* here runs:
|
|
46
|
-
*
|
|
47
|
-
* await fastify.register(async (authed) => {
|
|
48
|
-
* authed.register(authPreHandlerPlugin, { frontendOrigin: config.frontendUrl });
|
|
49
|
-
* authed.get("/api/payment-links", listPaymentLinksHandler);
|
|
50
|
-
* authed.post("/api/payment-links", createPaymentLinkHandler);
|
|
51
|
-
* authed.patch("/api/payment-links", updatePaymentLinkHandler);
|
|
52
|
-
* });
|
|
53
|
-
*
|
|
54
|
-
* If you mis-register these OUTSIDE that scope, `request.userId` is `undefined`
|
|
55
|
-
* and every handler answers 401 — `requireSiteAdmin()` re-checks it rather than
|
|
56
|
-
* assuming the preHandler ran. That belt-and-suspenders re-check is the reason
|
|
57
|
-
* a mis-registration is a broken feature instead of an open door.
|
|
58
|
-
*
|
|
59
|
-
* ── CSRF ─────────────────────────────────────────────────────────────────────
|
|
60
|
-
* Inherited from the auth scope: `apps/api/src/middleware/auth.ts` requires a
|
|
61
|
-
* same-origin `Origin` header on every cookie-authenticated non-GET request
|
|
62
|
-
* (`isOriginAllowed`). No custom token scheme is needed — but that protection
|
|
63
|
-
* exists ONLY inside the auth scope, which is the second reason the
|
|
64
|
-
* registration above is load-bearing.
|
|
65
|
-
*
|
|
66
|
-
* ── Tenancy ──────────────────────────────────────────────────────────────────
|
|
67
|
-
* Deliberately NOT registered inside `tenantScope`, and the org is deliberately
|
|
68
|
-
* NOT read from `request.tenant`. This surface manages the SITE OWNER's own
|
|
69
|
-
* Stripe catalog: `packages/stripe-client` is single-tenant (one Stripe account
|
|
70
|
-
* per deployment), a Stripe Payment Link carries no organization binding, and
|
|
71
|
-
* no local table maps a payment link to an org. The multi-tenant module's
|
|
72
|
-
* org-scoped billing is a SEPARATE surface (`apps/api/src/routes/billing/`,
|
|
73
|
-
* which DOES derive its org from `request.tenant.organizationId`, and whose
|
|
74
|
-
* `apps/workspace` consumer may not import `stripe-client` at all).
|
|
75
|
-
*
|
|
76
|
-
* Cross-account access is therefore structurally impossible rather than
|
|
77
|
-
* checked: Stripe object ids are scoped to the account whose key made the call,
|
|
78
|
-
* so a payment-link id from any other Stripe account comes back
|
|
79
|
-
* `resource_missing` — mapped below to the SAME 404 shape as a simply-unknown
|
|
80
|
-
* id, so the response is never an enumeration oracle.
|
|
81
|
-
*
|
|
82
|
-
* The correct authorization axis for this surface is therefore the SITE role,
|
|
83
|
-
* not the org membership: `User.role` in {"admin","owner"}
|
|
84
|
-
* (`database/auth/schema.prisma` — a per-deployment String, distinct from the
|
|
85
|
-
* org-membership `Role` enum used by the multi-tenant module).
|
|
86
|
-
*
|
|
87
|
-
* ── Test-mode-only ───────────────────────────────────────────────────────────
|
|
88
|
-
* Carried in twice, belt-and-suspenders: `loadStripeSurfaceConfig()` guards
|
|
89
|
-
* this surface's entry point; `@working-theory/stripe-client`'s
|
|
90
|
-
* `getStripeClient()`/`assertSandboxKey()` guards the SDK call itself. This
|
|
91
|
-
* route deliberately does NOT hand-roll its own live-key prefix check — it
|
|
92
|
-
* defers ENTIRELY to the shared guard so it inherits that guard's coverage (and
|
|
93
|
-
* any future fix to it) rather than risking a second, divergent implementation.
|
|
94
|
-
*
|
|
95
|
-
* Error mapping:
|
|
96
|
-
* 401 unauthorized — no authenticated caller (anonymous, or the
|
|
97
|
-
* route mis-registered outside the auth scope)
|
|
98
|
-
* 403 forbidden — authenticated, but not a site admin/owner
|
|
99
|
-
* 403 not-entitled — the deployment lacks `feature:commerce`
|
|
100
|
-
* 400 invalid-input — malformed body (bad lookupKey / link id)
|
|
101
|
-
* 404 not-found — no such payment link IN THIS ACCOUNT
|
|
102
|
-
* 500 config-error — Stripe/site env missing or misconfigured
|
|
103
|
-
* 500 internal-error — the role lookup itself failed (fail-closed)
|
|
104
|
-
* 502 payment-link-unavailable — Stripe SDK threw an unexpected error
|
|
105
|
-
*/
|
|
106
|
-
import { checkLicense } from "@working-theory/license";
|
|
107
|
-
import {
|
|
108
|
-
createPaymentLink,
|
|
109
|
-
listPaymentLinks,
|
|
110
|
-
StripeConfigError,
|
|
111
|
-
UnknownProductCodeError,
|
|
112
|
-
updatePaymentLink,
|
|
113
|
-
} from "@working-theory/stripe-client";
|
|
114
|
-
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
115
|
-
|
|
116
|
-
// The consumer's own Prisma singleton (`apps/api/src/lib/prisma.ts`) — the same
|
|
117
|
-
// client the auth middleware uses to resolve sessions.
|
|
118
|
-
import { prisma } from "../../lib/prisma.js";
|
|
119
|
-
|
|
120
|
-
// Sibling file instantiated at Step 10d — same directory as this route.
|
|
121
|
-
import { loadStripeSurfaceConfig } from "./stripe-config.js";
|
|
122
|
-
|
|
123
|
-
// ── Authorization ─────────────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* The site roles allowed to manage the payment catalog.
|
|
127
|
-
*
|
|
128
|
-
* `User.role` is a per-deployment String ("user" | "admin" | "owner"). On a
|
|
129
|
-
* site with open signup, "authenticated" is NOT a meaningful bar — it is "the
|
|
130
|
-
* whole internet with an extra step" — so authentication alone would not close
|
|
131
|
-
* AILK#3744. The role check is what makes this an authorization control.
|
|
132
|
-
*/
|
|
133
|
-
const SITE_ADMIN_ROLES: ReadonlySet<string> = new Set(["admin", "owner"]);
|
|
134
|
-
|
|
135
|
-
const PAYMENT_LINK_ID_REGEX = /^plink_[A-Za-z0-9_]{1,100}$/;
|
|
136
|
-
const LOOKUP_KEY_REGEX = /^[A-Za-z0-9._-]{1,200}$/;
|
|
137
|
-
|
|
138
|
-
function errorResponse(
|
|
139
|
-
reply: FastifyReply,
|
|
140
|
-
status: number,
|
|
141
|
-
code: string,
|
|
142
|
-
message: string,
|
|
143
|
-
): FastifyReply {
|
|
144
|
-
return reply.status(status).send({ ok: false, error: { code, message } });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The gate. Returns `true` only for an authenticated caller whose CURRENT role
|
|
149
|
-
* — re-read from the database on every request — is a site admin or owner.
|
|
150
|
-
* Every other path replies and returns `false`; the caller MUST bail out.
|
|
151
|
-
*
|
|
152
|
-
* Call it as the FIRST statement of every handler:
|
|
153
|
-
*
|
|
154
|
-
* if (!(await requireSiteAdmin(request, reply))) return;
|
|
155
|
-
*
|
|
156
|
-
* Three properties are load-bearing:
|
|
157
|
-
*
|
|
158
|
-
* 1. The role is re-read from the DB by the caller's id — never trusted off
|
|
159
|
-
* the request. A stale session belonging to a user who has since been
|
|
160
|
-
* demoted loses access on their very next request.
|
|
161
|
-
* 2. It FAILS CLOSED. A throw while resolving the role DENIES (500). It never
|
|
162
|
-
* falls through to allow — the classic catch-and-continue that turns a
|
|
163
|
-
* transient DB blip into an open door.
|
|
164
|
-
* 3. It re-checks `request.userId` rather than assuming the auth preHandler
|
|
165
|
-
* ran, so a MIS-REGISTRATION (routes mounted outside the auth scope) is a
|
|
166
|
-
* 401 — a broken feature, not an open door.
|
|
167
|
-
*/
|
|
168
|
-
export async function requireSiteAdmin(
|
|
169
|
-
request: FastifyRequest,
|
|
170
|
-
reply: FastifyReply,
|
|
171
|
-
): Promise<boolean> {
|
|
172
|
-
const userId = request.userId;
|
|
173
|
-
if (!userId) {
|
|
174
|
-
// Anonymous — or mis-registered outside the auth scope. Same answer.
|
|
175
|
-
errorResponse(
|
|
176
|
-
reply,
|
|
177
|
-
401,
|
|
178
|
-
"unauthorized",
|
|
179
|
-
"Authentication is required.",
|
|
180
|
-
);
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let role: string | undefined;
|
|
185
|
-
try {
|
|
186
|
-
const user = await prisma.user.findUnique({
|
|
187
|
-
where: { id: userId },
|
|
188
|
-
select: { role: true },
|
|
189
|
-
});
|
|
190
|
-
role = user?.role;
|
|
191
|
-
} catch (err) {
|
|
192
|
-
// FAIL CLOSED. Never `catch {}` into the happy path.
|
|
193
|
-
request.log.error(
|
|
194
|
-
{
|
|
195
|
-
method: request.method,
|
|
196
|
-
path: request.url,
|
|
197
|
-
message: err instanceof Error ? err.message : "unknown",
|
|
198
|
-
},
|
|
199
|
-
"[payment-links] role resolution failed; DENYING",
|
|
200
|
-
);
|
|
201
|
-
errorResponse(
|
|
202
|
-
reply,
|
|
203
|
-
500,
|
|
204
|
-
"internal-error",
|
|
205
|
-
"Could not verify your permissions.",
|
|
206
|
-
);
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// A missing user row (deleted account, stale session) has no role -> denied.
|
|
211
|
-
if (!role || !SITE_ADMIN_ROLES.has(role)) {
|
|
212
|
-
request.log.warn(
|
|
213
|
-
{ method: request.method, path: request.url, status: 403 },
|
|
214
|
-
"[payment-links] caller is not a site admin; refused",
|
|
215
|
-
);
|
|
216
|
-
errorResponse(
|
|
217
|
-
reply,
|
|
218
|
-
403,
|
|
219
|
-
"forbidden",
|
|
220
|
-
"Managing payment links requires a site admin role.",
|
|
221
|
-
);
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ── Entitlement (layered UNDER authorization — never a substitute for it) ─────
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Resolves the deployment's license and enforces the `feature:commerce`
|
|
232
|
-
* entitlement. Fails closed: no license, an expired license, a revoked license,
|
|
233
|
-
* or a license without the `commerce` feature are all treated as NOT entitled.
|
|
234
|
-
*
|
|
235
|
-
* This answers "does this DEPLOYMENT have commerce?" — a DIFFERENT question
|
|
236
|
-
* from "may this CALLER manage payment links?", which is `requireSiteAdmin`'s
|
|
237
|
-
* job. Both are enforced, in that order.
|
|
238
|
-
*/
|
|
239
|
-
async function assertCommerceEntitled(): Promise<void> {
|
|
240
|
-
const result = await checkLicense();
|
|
241
|
-
|
|
242
|
-
// A revoked license is treated as feature-less — the feature check below then
|
|
243
|
-
// throws, same as an absent license.
|
|
244
|
-
const features: readonly string[] = result.revoked ? [] : result.features;
|
|
245
|
-
|
|
246
|
-
// Expiry first, then the feature claim — the same order, and the same two
|
|
247
|
-
// conditions, the paid entitlement helper applies. The third thing that
|
|
248
|
-
// helper does is assert the feature's verification MODE against the license
|
|
249
|
-
// manifest; `commerce` is declared `verification: static`, so that assert is
|
|
250
|
-
// a no-op pass for this call, and the manifest is a paid-tier package an
|
|
251
|
-
// emitted OSS route may not reach. Both throws below are caught by
|
|
252
|
-
// `preflight()` and mapped to the SAME 403 not-entitled response, so the
|
|
253
|
-
// error type is not observable at the route boundary.
|
|
254
|
-
//
|
|
255
|
-
// `LicenseResult.expiresAt` is `Date | null`; normalise anyway so a licence
|
|
256
|
-
// resolver that yields `undefined` reads as "no expiry" rather than throwing.
|
|
257
|
-
const expiresAt = result.expiresAt ?? null;
|
|
258
|
-
if (expiresAt !== null && expiresAt.getTime() <= Date.now()) {
|
|
259
|
-
throw new Error("This site's license has expired.");
|
|
260
|
-
}
|
|
261
|
-
if (!features.includes("commerce")) {
|
|
262
|
-
throw new Error("This site's license does not include the commerce feature.");
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Runs the entitlement + Stripe-config preflight shared by all three handlers.
|
|
268
|
-
* Returns `true` when the request may proceed to a Stripe call.
|
|
269
|
-
*
|
|
270
|
-
* Callers MUST have cleared `requireSiteAdmin` first — this function performs
|
|
271
|
-
* no identity check of its own.
|
|
272
|
-
*/
|
|
273
|
-
async function preflight(
|
|
274
|
-
request: FastifyRequest,
|
|
275
|
-
reply: FastifyReply,
|
|
276
|
-
): Promise<boolean> {
|
|
277
|
-
try {
|
|
278
|
-
await assertCommerceEntitled();
|
|
279
|
-
} catch {
|
|
280
|
-
errorResponse(
|
|
281
|
-
reply,
|
|
282
|
-
403,
|
|
283
|
-
"not-entitled",
|
|
284
|
-
"This site is not entitled to the commerce feature.",
|
|
285
|
-
);
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
loadStripeSurfaceConfig();
|
|
291
|
-
} catch (err) {
|
|
292
|
-
request.log.error(
|
|
293
|
-
{
|
|
294
|
-
method: request.method,
|
|
295
|
-
path: request.url,
|
|
296
|
-
message: err instanceof Error ? err.message : "unknown",
|
|
297
|
-
},
|
|
298
|
-
"[payment-links] config error",
|
|
299
|
-
);
|
|
300
|
-
errorResponse(
|
|
301
|
-
reply,
|
|
302
|
-
500,
|
|
303
|
-
"config-error",
|
|
304
|
-
"Stripe is misconfigured for this site.",
|
|
305
|
-
);
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Stripe raises `resource_missing` for an id that does not exist IN THIS
|
|
314
|
-
* ACCOUNT — which is also what a payment-link id belonging to a DIFFERENT
|
|
315
|
-
* Stripe account looks like, since object ids are account-scoped. Both map to
|
|
316
|
-
* the same 404, so the response cannot be used to enumerate ids.
|
|
317
|
-
*/
|
|
318
|
-
function isResourceMissing(err: unknown): boolean {
|
|
319
|
-
return (
|
|
320
|
-
typeof err === "object" &&
|
|
321
|
-
err !== null &&
|
|
322
|
-
(err as { code?: unknown }).code === "resource_missing"
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** Shared Stripe error mapping. Never echoes caller input back. */
|
|
327
|
-
function stripeErrorResponse(
|
|
328
|
-
request: FastifyRequest,
|
|
329
|
-
reply: FastifyReply,
|
|
330
|
-
err: unknown,
|
|
331
|
-
): FastifyReply {
|
|
332
|
-
if (isResourceMissing(err)) {
|
|
333
|
-
return errorResponse(reply, 404, "not-found", "No such payment link.");
|
|
334
|
-
}
|
|
335
|
-
if (err instanceof StripeConfigError) {
|
|
336
|
-
return errorResponse(
|
|
337
|
-
reply,
|
|
338
|
-
500,
|
|
339
|
-
"config-error",
|
|
340
|
-
"Payment links are misconfigured.",
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
request.log.error(
|
|
344
|
-
{
|
|
345
|
-
method: request.method,
|
|
346
|
-
path: request.url,
|
|
347
|
-
errName: err instanceof Error ? err.name : "unknown",
|
|
348
|
-
},
|
|
349
|
-
"[payment-links] stripe error; payment-link-unavailable",
|
|
350
|
-
);
|
|
351
|
-
return errorResponse(
|
|
352
|
-
reply,
|
|
353
|
-
502,
|
|
354
|
-
"payment-link-unavailable",
|
|
355
|
-
"Payment links are temporarily unavailable.",
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
360
|
-
|
|
361
|
-
/** GET /api/payment-links[?active=true|false] — list payment links. */
|
|
362
|
-
export async function listPaymentLinksHandler(
|
|
363
|
-
request: FastifyRequest,
|
|
364
|
-
reply: FastifyReply,
|
|
365
|
-
): Promise<void> {
|
|
366
|
-
if (!(await requireSiteAdmin(request, reply))) return;
|
|
367
|
-
if (!(await preflight(request, reply))) return;
|
|
368
|
-
|
|
369
|
-
const query = request.query as { active?: string } | undefined;
|
|
370
|
-
const activeParam = query?.active;
|
|
371
|
-
const active = activeParam === undefined ? undefined : activeParam === "true";
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
const { paymentLinks } = await listPaymentLinks({ active });
|
|
375
|
-
reply.send({ ok: true, data: { paymentLinks } });
|
|
376
|
-
} catch (err) {
|
|
377
|
-
stripeErrorResponse(request, reply, err);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** POST /api/payment-links — create a Payment Link from a lookupKey. */
|
|
382
|
-
export async function createPaymentLinkHandler(
|
|
383
|
-
request: FastifyRequest,
|
|
384
|
-
reply: FastifyReply,
|
|
385
|
-
): Promise<void> {
|
|
386
|
-
if (!(await requireSiteAdmin(request, reply))) return;
|
|
387
|
-
if (!(await preflight(request, reply))) return;
|
|
388
|
-
|
|
389
|
-
const body = request.body as
|
|
390
|
-
| { lookupKey?: unknown; quantity?: unknown }
|
|
391
|
-
| undefined;
|
|
392
|
-
if (
|
|
393
|
-
!body ||
|
|
394
|
-
typeof body.lookupKey !== "string" ||
|
|
395
|
-
!LOOKUP_KEY_REGEX.test(body.lookupKey)
|
|
396
|
-
) {
|
|
397
|
-
errorResponse(
|
|
398
|
-
reply,
|
|
399
|
-
400,
|
|
400
|
-
"invalid-input",
|
|
401
|
-
"lookupKey is required and must match /^[A-Za-z0-9._-]{1,200}$/.",
|
|
402
|
-
);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const quantity =
|
|
407
|
-
typeof body.quantity === "number" &&
|
|
408
|
-
Number.isInteger(body.quantity) &&
|
|
409
|
-
body.quantity >= 1 &&
|
|
410
|
-
body.quantity <= 999
|
|
411
|
-
? body.quantity
|
|
412
|
-
: undefined;
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
const link = await createPaymentLink({
|
|
416
|
-
lookupKey: body.lookupKey,
|
|
417
|
-
quantity,
|
|
418
|
-
});
|
|
419
|
-
reply.status(201).send({ ok: true, data: link });
|
|
420
|
-
} catch (err) {
|
|
421
|
-
if (err instanceof UnknownProductCodeError) {
|
|
422
|
-
// Do NOT echo the lookupKey back to the caller.
|
|
423
|
-
errorResponse(
|
|
424
|
-
reply,
|
|
425
|
-
404,
|
|
426
|
-
"unknown-lookup-key",
|
|
427
|
-
"No active price exists for that product.",
|
|
428
|
-
);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
stripeErrorResponse(request, reply, err);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** PATCH /api/payment-links — archive/restore a Payment Link by id. */
|
|
436
|
-
export async function updatePaymentLinkHandler(
|
|
437
|
-
request: FastifyRequest,
|
|
438
|
-
reply: FastifyReply,
|
|
439
|
-
): Promise<void> {
|
|
440
|
-
if (!(await requireSiteAdmin(request, reply))) return;
|
|
441
|
-
if (!(await preflight(request, reply))) return;
|
|
442
|
-
|
|
443
|
-
const body = request.body as
|
|
444
|
-
| { paymentLinkId?: unknown; active?: unknown }
|
|
445
|
-
| undefined;
|
|
446
|
-
if (
|
|
447
|
-
!body ||
|
|
448
|
-
typeof body.paymentLinkId !== "string" ||
|
|
449
|
-
!PAYMENT_LINK_ID_REGEX.test(body.paymentLinkId) ||
|
|
450
|
-
typeof body.active !== "boolean"
|
|
451
|
-
) {
|
|
452
|
-
// Shape-checked BEFORE any Stripe call. Does not echo the input back.
|
|
453
|
-
errorResponse(
|
|
454
|
-
reply,
|
|
455
|
-
400,
|
|
456
|
-
"invalid-input",
|
|
457
|
-
"paymentLinkId (plink_...) and active (boolean) are required.",
|
|
458
|
-
);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
const link = await updatePaymentLink({
|
|
464
|
-
paymentLinkId: body.paymentLinkId,
|
|
465
|
-
active: body.active,
|
|
466
|
-
});
|
|
467
|
-
reply.send({ ok: true, data: link });
|
|
468
|
-
} catch (err) {
|
|
469
|
-
stripeErrorResponse(request, reply, err);
|
|
470
|
-
}
|
|
471
|
-
}
|