create-ailk 0.2.0 → 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 +50 -53
- 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 +2 -2
- 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 +33 -5
- 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 +3 -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 +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -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 +10 -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 +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +10 -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 +30 -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 +459 -135
- 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 +229 -4
- 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/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +14 -13
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +0 -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/database/CHANGELOG.md +33 -0
- package/templates/database/inbox/schema.prisma +67 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- 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__/me-route-precedence.test.ts +0 -221
- 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__/listing-promotion.test.ts +0 -684
- 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 -1296
- 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/listing-promotion.ts +0 -342
- 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 -510
- 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,3 +1,6 @@
|
|
|
1
|
+
// Not marked oss: false (D-48/#5366 escalation) — tests the
|
|
2
|
+
// always-shipping project-listings anonymous surface; see
|
|
3
|
+
// routes/project-listings/me.ts's header comment.
|
|
1
4
|
/**
|
|
2
5
|
* @file site-answers.test.ts
|
|
3
6
|
*
|
|
@@ -243,6 +246,13 @@ function installStore(): void {
|
|
|
243
246
|
addressCountry: null,
|
|
244
247
|
questions: null,
|
|
245
248
|
siteAnswers: null,
|
|
249
|
+
// #5407 — a row written before the columns existed, and a freshly
|
|
250
|
+
// started draft on a site that asks for one name line: parts null,
|
|
251
|
+
// no phone, no verification stamp.
|
|
252
|
+
founderFirstName: null,
|
|
253
|
+
founderLastName: null,
|
|
254
|
+
founderPhone: null,
|
|
255
|
+
emailVerifiedAt: null,
|
|
246
256
|
founderCompany: null,
|
|
247
257
|
leadId: null,
|
|
248
258
|
headline: null,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Not marked oss: false (D-48/#5366 escalation) — tests the
|
|
2
|
+
// always-shipping project-listings anonymous surface; see
|
|
3
|
+
// routes/project-listings/me.ts's header comment.
|
|
1
4
|
/**
|
|
2
5
|
* @file site-key.test.ts
|
|
3
6
|
*
|
|
@@ -28,6 +31,13 @@ jest.mock("../../../lib/prisma.js");
|
|
|
28
31
|
|
|
29
32
|
jest.mock("@working-theory/email", () => ({
|
|
30
33
|
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
|
|
34
|
+
// #5406 — `start` now reaches the resume email. Declared here for the reason
|
|
35
|
+
// docs/lessons/project-listings/04 records: a mock factory that omits an
|
|
36
|
+
// export the code under test calls leaves it `undefined` at the callsite.
|
|
37
|
+
// These suites configure no `resumeEmail`, so the send short-circuits before
|
|
38
|
+
// this is reached; it is declared so a future fixture that DOES configure
|
|
39
|
+
// one fails on its assertion rather than on a missing mock.
|
|
40
|
+
sendListingResumeEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-resume" }),
|
|
31
41
|
}));
|
|
32
42
|
|
|
33
43
|
jest.mock("@working-theory/license", () => ({
|
|
@@ -136,6 +146,13 @@ function installStore(): void {
|
|
|
136
146
|
anythingElse: null,
|
|
137
147
|
address: null,
|
|
138
148
|
questions: null,
|
|
149
|
+
// #5407 — a row written before the columns existed, and a freshly
|
|
150
|
+
// started draft on a site that asks for one name line: parts null,
|
|
151
|
+
// no phone, no verification stamp.
|
|
152
|
+
founderFirstName: null,
|
|
153
|
+
founderLastName: null,
|
|
154
|
+
founderPhone: null,
|
|
155
|
+
emailVerifiedAt: null,
|
|
139
156
|
founderCompany: null,
|
|
140
157
|
leadId: null,
|
|
141
158
|
headline: null,
|
|
@@ -186,6 +203,13 @@ function seedActiveListing(siteId: string, slug: string): void {
|
|
|
186
203
|
address: "1 Founder Way",
|
|
187
204
|
founderName: "Some Founder",
|
|
188
205
|
founderEmail: "founder@example.com",
|
|
206
|
+
// #5407 — a row written before the columns existed, and a freshly
|
|
207
|
+
// started draft on a site that asks for one name line: parts null,
|
|
208
|
+
// no phone, no verification stamp.
|
|
209
|
+
founderFirstName: null,
|
|
210
|
+
founderLastName: null,
|
|
211
|
+
founderPhone: null,
|
|
212
|
+
emailVerifiedAt: null,
|
|
189
213
|
founderCompany: null,
|
|
190
214
|
questions: null,
|
|
191
215
|
logoMarkUrl: null,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Not marked oss: false (D-48/#5366 escalation) — tests the
|
|
2
|
+
// always-shipping project-listings anonymous surface; see
|
|
3
|
+
// routes/project-listings/me.ts's header comment.
|
|
1
4
|
/**
|
|
2
5
|
* @file structured-address.test.ts
|
|
3
6
|
*
|
|
@@ -36,6 +39,13 @@ jest.mock("../../../lib/prisma.js");
|
|
|
36
39
|
|
|
37
40
|
jest.mock("@working-theory/email", () => ({
|
|
38
41
|
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
|
|
42
|
+
// #5406 — `start` now reaches the resume email. Declared here for the reason
|
|
43
|
+
// docs/lessons/project-listings/04 records: a mock factory that omits an
|
|
44
|
+
// export the code under test calls leaves it `undefined` at the callsite.
|
|
45
|
+
// These suites configure no `resumeEmail`, so the send short-circuits before
|
|
46
|
+
// this is reached; it is declared so a future fixture that DOES configure
|
|
47
|
+
// one fails on its assertion rather than on a missing mock.
|
|
48
|
+
sendListingResumeEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-resume" }),
|
|
39
49
|
}));
|
|
40
50
|
|
|
41
51
|
jest.mock("@working-theory/license", () => ({
|
|
@@ -221,6 +231,13 @@ function installStore(): void {
|
|
|
221
231
|
addressPostalCode: null,
|
|
222
232
|
addressCountry: null,
|
|
223
233
|
questions: null,
|
|
234
|
+
// #5407 — a row written before the columns existed, and a freshly
|
|
235
|
+
// started draft on a site that asks for one name line: parts null,
|
|
236
|
+
// no phone, no verification stamp.
|
|
237
|
+
founderFirstName: null,
|
|
238
|
+
founderLastName: null,
|
|
239
|
+
founderPhone: null,
|
|
240
|
+
emailVerifiedAt: null,
|
|
224
241
|
founderCompany: null,
|
|
225
242
|
leadId: null,
|
|
226
243
|
headline: null,
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* POST /v1/project-listings — anonymous start (leadsScope)
|
|
7
7
|
* PATCH /v1/project-listings/drafts/:id — anonymous save-as-you-go, token-gated
|
|
8
8
|
* POST /v1/project-listings/drafts/:id/submit — anonymous submit, token-gated
|
|
9
|
+
* POST /v1/project-listings/drafts/:id/verify-email — anonymous, token- AND
|
|
10
|
+
* nonce-gated (#5407)
|
|
9
11
|
* GET /v1/project-listings/public — anonymous page-render read (leadsScope)
|
|
10
12
|
* GET /v1/project-listings/me — the founder's OWN listings (authScope)
|
|
11
13
|
* GET /v1/project-listings — tenant-scoped dashboard list
|
|
@@ -35,16 +37,24 @@
|
|
|
35
37
|
*/
|
|
36
38
|
import type { FastifyPluginAsync } from "fastify";
|
|
37
39
|
|
|
40
|
+
import { loadSlice } from "../../lib/slice-load.js";
|
|
41
|
+
|
|
38
42
|
import { projectListingMeRoute as meRoute } from "./me.js";
|
|
39
43
|
import { patchProjectListingDraftRoute } from "./patch-draft.js";
|
|
40
44
|
import { publicProjectListingsRoute } from "./public.js";
|
|
41
45
|
import { startProjectListingRoute } from "./start.js";
|
|
42
46
|
import { submitProjectListingRoute } from "./submit.js";
|
|
47
|
+
import { verifyProjectListingDraftEmailRoute } from "./verify-email.js";
|
|
43
48
|
|
|
44
49
|
export const projectListingStartRoute = startProjectListingRoute;
|
|
45
50
|
export const projectListingDraftPatchRoute = patchProjectListingDraftRoute;
|
|
46
51
|
export const projectListingSubmitRoute = submitProjectListingRoute;
|
|
47
52
|
export const projectListingPublicRoute = publicProjectListingsRoute;
|
|
53
|
+
/**
|
|
54
|
+
* The resume link's verification write (#5407). Static, not a slice: it is
|
|
55
|
+
* part of the free anonymous surface, exactly like the three routes above.
|
|
56
|
+
*/
|
|
57
|
+
export const projectListingVerifyEmailRoute = verifyProjectListingDraftEmailRoute;
|
|
48
58
|
/**
|
|
49
59
|
* The founder's own listings read (#5295). Static, not a slice: it depends on
|
|
50
60
|
* the owner link alone, so it is registered — and works — on a build with no
|
|
@@ -62,50 +72,67 @@ export const projectListingMeRoute = meRoute;
|
|
|
62
72
|
* purpose, which is why the boundary is drawn here at the registration
|
|
63
73
|
* call-site rather than by splitting the directory.
|
|
64
74
|
*
|
|
65
|
-
* The specifier is
|
|
66
|
-
*
|
|
67
|
-
*
|
|
75
|
+
* The specifier is kept off the `import()` call site because `tsc` resolves a
|
|
76
|
+
* string-literal `import("...")` EXPRESSION whether or not the branch runs, and
|
|
77
|
+
* would report TS2307 on the stripped tree. It is held in a module-level
|
|
78
|
+
* constant rather than read out of the table per iteration (#5304): the table
|
|
79
|
+
* form was invisible to the deploy-time file tracer too, so these files were
|
|
80
|
+
* compiled and never shipped, and the constant form is the one shape `tsc`
|
|
81
|
+
* refuses to resolve and `@vercel/nft` still traces. `specifier` stays on each
|
|
82
|
+
* row for the drift message and reads the same constant as `load`. The plugin body
|
|
68
83
|
* only executes when `server.ts` registers it, which it does inside
|
|
69
84
|
* `isModuleEnabled('multi-tenant')` — so nothing here runs on an OSS build.
|
|
70
85
|
*
|
|
71
86
|
* The export name is unchanged so `server.ts` needs no edit: it still reaches
|
|
72
87
|
* `projectListingTenantRoutes` from this module.
|
|
73
88
|
*/
|
|
89
|
+
const LIST_HANDLER = "./list.js";
|
|
90
|
+
const GET_HANDLER = "./get.js";
|
|
91
|
+
const PATCH_HANDLER = "./patch.js";
|
|
92
|
+
const PATCH_COPY_HANDLER = "./patch-copy.js";
|
|
93
|
+
|
|
74
94
|
const TENANT_HANDLERS: ReadonlyArray<{
|
|
75
95
|
method: "get" | "patch";
|
|
76
96
|
url: string;
|
|
77
97
|
specifier: string;
|
|
78
98
|
exportName: string;
|
|
99
|
+
load: () => Promise<unknown>;
|
|
79
100
|
}> = [
|
|
80
101
|
{
|
|
81
102
|
method: "get",
|
|
82
103
|
url: "/v1/project-listings",
|
|
83
|
-
specifier:
|
|
104
|
+
specifier: LIST_HANDLER,
|
|
84
105
|
exportName: "listProjectListingsHandler",
|
|
106
|
+
load: () => import(LIST_HANDLER),
|
|
85
107
|
},
|
|
86
108
|
{
|
|
87
109
|
method: "get",
|
|
88
110
|
url: "/v1/project-listings/:id",
|
|
89
|
-
specifier:
|
|
111
|
+
specifier: GET_HANDLER,
|
|
90
112
|
exportName: "getProjectListingHandler",
|
|
113
|
+
load: () => import(GET_HANDLER),
|
|
91
114
|
},
|
|
92
115
|
{
|
|
93
116
|
method: "patch",
|
|
94
117
|
url: "/v1/project-listings/:id",
|
|
95
|
-
specifier:
|
|
118
|
+
specifier: PATCH_HANDLER,
|
|
96
119
|
exportName: "patchProjectListingHandler",
|
|
120
|
+
load: () => import(PATCH_HANDLER),
|
|
97
121
|
},
|
|
98
122
|
{
|
|
99
123
|
method: "patch",
|
|
100
124
|
url: "/v1/project-listings/:id/copy",
|
|
101
|
-
specifier:
|
|
125
|
+
specifier: PATCH_COPY_HANDLER,
|
|
102
126
|
exportName: "patchProjectListingCopyHandler",
|
|
127
|
+
load: () => import(PATCH_COPY_HANDLER),
|
|
103
128
|
},
|
|
104
129
|
];
|
|
105
130
|
|
|
106
|
-
export const projectListingTenantRoutes: FastifyPluginAsync = async (
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
export const projectListingTenantRoutes: FastifyPluginAsync = async (
|
|
132
|
+
fastify,
|
|
133
|
+
) => {
|
|
134
|
+
for (const { method, url, specifier, exportName, load } of TENANT_HANDLERS) {
|
|
135
|
+
const mod = (await loadSlice(specifier, load)) as Record<
|
|
109
136
|
string,
|
|
110
137
|
Parameters<typeof fastify.get>[1] | undefined
|
|
111
138
|
>;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* NOT marked `oss: false` (D-48/#5366 escalation, mirrors the original
|
|
3
|
+
* multi-tenant/#5088 card's own conclusion about this exact file, lines
|
|
4
|
+
* 556-571 of module-architecture.ts): `./index.ts` in this directory is a
|
|
5
|
+
* MIXED barrel that statically imports this file, and `server.ts` reaches
|
|
6
|
+
* `./index.ts` via a plain (non-slice) literal `import()` for multi-tenant's
|
|
7
|
+
* tenant-scoped routes — a specifier `tsc` always resolves, module selection
|
|
8
|
+
* or not. Stripping this file leaves that barrel with a dangling import on
|
|
9
|
+
* every tree. Registration stays gated (`isModuleEnabled("signal-collectors")`
|
|
10
|
+
* in server.ts); only the source file itself must ship unconditionally.
|
|
11
|
+
*
|
|
2
12
|
* GET /v1/project-listings/me
|
|
3
13
|
*
|
|
4
14
|
* The FOUNDER'S OWN listings read (#5295) — the `/v1/flow-checkouts/me` and
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
|
|
3
|
+
* escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
|
|
4
|
+
* reaches by a literal, always-resolved `import()`; this file ships
|
|
5
|
+
* unconditionally, registration stays gated).
|
|
6
|
+
*
|
|
2
7
|
* PATCH /v1/project-listings/drafts/:id
|
|
3
8
|
*
|
|
4
9
|
* `patch` — the save-as-you-go write (#5036 D1). Anonymous, token-gated,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
|
|
3
|
+
* escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
|
|
4
|
+
* reaches by a literal, always-resolved `import()`; this file ships
|
|
5
|
+
* unconditionally, registration stays gated).
|
|
6
|
+
*
|
|
2
7
|
* GET /v1/project-listings/public?siteKey=
|
|
3
8
|
*
|
|
4
9
|
* The anonymous page-render read (#4998 AC4, reshaped by #5033). Every
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* NOT marked `oss: false` (D-48/#5366 escalation, reversing this pass's
|
|
3
|
+
* own D6 read — see `./me.ts`'s header comment): `sendValidated` is imported
|
|
4
|
+
* by `./me.ts`, `./patch-draft.ts` and `./public.ts`, each of which must ship
|
|
5
|
+
* unconditionally because `./index.ts`'s multi-tenant literal `import()`
|
|
6
|
+
* forces it. A file imported by an always-shipping file cannot itself be
|
|
7
|
+
* excluded — this is the ORIGINAL multi-tenant/#5088 card's own conclusion
|
|
8
|
+
* about this exact file (module-architecture.ts lines 569-571), which D6's
|
|
9
|
+
* "every other consumer is Pro Plus" premise doesn't override: two of its
|
|
10
|
+
* three non-tenant consumers are these three always-shipping files, not a
|
|
11
|
+
* gated one.
|
|
12
|
+
*
|
|
2
13
|
* Response-schema validation for the project-listings routes (#4998),
|
|
3
14
|
* mirroring `waitlist-experiments/respond.ts`'s discipline: response schemas
|
|
4
15
|
* are contract-only unless re-parsed against their own `.strict()` schema
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
|
|
3
|
+
* escalation): imported only by `./patch-draft.ts` and `./submit.ts`, both
|
|
4
|
+
* of which ship unconditionally (`./index.ts`'s multi-tenant literal
|
|
5
|
+
* `import()`), so this file cannot be excluded either.
|
|
6
|
+
*
|
|
2
7
|
* The resume token's transport (#5036 D1).
|
|
3
8
|
*
|
|
4
9
|
* `start` hands the client an opaque token; `patch` and `submit` present it
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
|
|
3
|
+
* escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
|
|
4
|
+
* reaches by a literal, always-resolved `import()`; this file ships
|
|
5
|
+
* unconditionally, registration stays gated).
|
|
6
|
+
*
|
|
2
7
|
* POST /v1/project-listings
|
|
3
8
|
*
|
|
4
9
|
* `start` — step one of the application (#5036 D1). Anonymous, registered in
|
|
@@ -37,6 +42,24 @@
|
|
|
37
42
|
* PII-safe logging (mirrors `routes/leads/index.ts`): routing metadata only —
|
|
38
43
|
* never founderEmail, founderName, or the resume token.
|
|
39
44
|
*
|
|
45
|
+
* SENDS THE RESUME EMAIL (#5406), fire-and-forget, after the row exists. The
|
|
46
|
+
* 201 below is never delayed or failed by it: `sendListingResumeLink` returns
|
|
47
|
+
* `void` and swallows everything, exactly as `sendListingDecisionEmail`'s
|
|
48
|
+
* `fileIssueOnApprove` does on the approve path. The email is OPT-IN — a site
|
|
49
|
+
* whose `SiteListingConfig` carries no `resumeEmail.fromAddress` gets none, so
|
|
50
|
+
* a site sending its own today does not start sending two — and the request
|
|
51
|
+
* and response shapes of this hard-floor route are unchanged.
|
|
52
|
+
*
|
|
53
|
+
* THE EMAIL IS ALSO THE ONLY CARRIER OF THE VERIFICATION NONCE (#5407).
|
|
54
|
+
* `startProjectListingDraft` mints it, stores only its digest, and returns it
|
|
55
|
+
* as a SIBLING of the result — never inside the value parsed below, because
|
|
56
|
+
* `projectListingStartResponseSchema` is this route's hard-floor contract and
|
|
57
|
+
* carries `id` and `resumeToken` and nothing else. The nonce reaches
|
|
58
|
+
* `sendListingResumeLink` from a local variable and goes nowhere else: not
|
|
59
|
+
* into the response, not into the log line above it. That is precisely what
|
|
60
|
+
* makes `email_verified_at` mean something the resume token alone could not
|
|
61
|
+
* prove — see `verifyProjectListingDraftEmail`.
|
|
62
|
+
*
|
|
40
63
|
* The response uses the API's own `{ ok, data }` / `{ ok, error }` envelope
|
|
41
64
|
* (`apps/api/CLAUDE.md`) on BOTH the success and the failure path — the
|
|
42
65
|
* anonymous create it replaces answered bare on success and enveloped on
|
|
@@ -49,6 +72,7 @@ import {
|
|
|
49
72
|
import type { FastifyPluginAsync } from "fastify";
|
|
50
73
|
|
|
51
74
|
import { siteIdForPublicKey } from "../../services/listing-config.js";
|
|
75
|
+
import { sendListingResumeLink } from "../../services/project-listing-resume-email.js";
|
|
52
76
|
import { startProjectListingDraft } from "../../services/project-listings.js";
|
|
53
77
|
|
|
54
78
|
export const startProjectListingRoute: FastifyPluginAsync = async (fastify) => {
|
|
@@ -97,6 +121,12 @@ export const startProjectListingRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
97
121
|
"project listing draft started",
|
|
98
122
|
);
|
|
99
123
|
|
|
124
|
+
// #5406 — the founder's way back to this draft. NOT awaited: the 201 must
|
|
125
|
+
// not wait on Resend, and must not fail if Resend does. Both credentials
|
|
126
|
+
// are handed over here and neither is ever logged — they ride the link
|
|
127
|
+
// inside the email, and the #5407 nonce rides NOTHING ELSE anywhere.
|
|
128
|
+
sendListingResumeLink(result.id, result.resumeToken, result.verifyNonce);
|
|
129
|
+
|
|
100
130
|
const body = projectListingStartResponseSchema.parse({
|
|
101
131
|
id: result.id,
|
|
102
132
|
resumeToken: result.resumeToken,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
|
|
3
|
+
* escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
|
|
4
|
+
* reaches by a literal, always-resolved `import()`; this file ships
|
|
5
|
+
* unconditionally, registration stays gated).
|
|
6
|
+
*
|
|
2
7
|
* POST /v1/project-listings/drafts/:id/submit
|
|
3
8
|
*
|
|
4
9
|
* `submit` — the moment an application stops being a draft (#5036 D1).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /v1/project-listings/drafts/:id/verify-email
|
|
3
|
+
*
|
|
4
|
+
* Records that the founder proved control of `founderEmail`, by opening the
|
|
5
|
+
* resume link emailed at step one (#5407). Anonymous, credential-gated,
|
|
6
|
+
* registered in the same rate-limited `leadsScope` as the other three
|
|
7
|
+
* anonymous listing routes.
|
|
8
|
+
*
|
|
9
|
+
* WHY THE ROUTE EXISTS. The resume email (#5406) tells the founder to "verify
|
|
10
|
+
* your email and continue", and until this route nothing made that sentence
|
|
11
|
+
* true: `ListingApplicationFlow` rehydrated from `?draft=&token=` entirely in
|
|
12
|
+
* the browser and touched the server only on the NEXT advance — so a founder
|
|
13
|
+
* who opened the link and closed the tab left no record, and the operator had
|
|
14
|
+
* no way to tell a proved address from a typed one at review (samhenry spec
|
|
15
|
+
* 316 D6b).
|
|
16
|
+
*
|
|
17
|
+
* WHAT IT PROVES. That somebody who can read mail at `founderEmail` opened
|
|
18
|
+
* this link — evidence about the ADDRESS, not about who is driving the draft
|
|
19
|
+
* (`verifyProjectListingDraftEmail` carries the full note). It takes
|
|
20
|
+
* TWO credentials and only the second carries that: the `Authorization:
|
|
21
|
+
* Bearer` resume token says "you opened this draft" (it is in `start`'s own
|
|
22
|
+
* 201 body, so anyone who posted the form has it), and the `verifyNonce` in
|
|
23
|
+
* the body says "you read the email" — it is minted at `start`, digested onto
|
|
24
|
+
* the row, and handed to nothing but the resume-email builder, reaching the
|
|
25
|
+
* outside world only as the `?verify=` parameter inside the message. Keying
|
|
26
|
+
* this route on the resume token ALONE was the first design and was
|
|
27
|
+
* forgeable in two HTTP calls; `verifyProjectListingDraftEmail` carries the
|
|
28
|
+
* full note.
|
|
29
|
+
*
|
|
30
|
+
* It still says nothing about WHO the founder is. `ownerUserId` (#5295) is
|
|
31
|
+
* the column that carries an identity claim.
|
|
32
|
+
*
|
|
33
|
+
* A SITE THAT SENDS NO RESUME EMAIL CAN NEVER VERIFY, and that is correct:
|
|
34
|
+
* the nonce is minted on every draft but delivered only by the email, so
|
|
35
|
+
* where `SiteListingConfig` carries no `resumeEmail` the column stays null
|
|
36
|
+
* forever — nobody has read anything at that address.
|
|
37
|
+
*
|
|
38
|
+
* IDEMPOTENT, AND WRITE-ONCE. The service stamps by compare-and-swap on
|
|
39
|
+
* `emailVerifiedAt: null`, so a founder who opens the link five times is
|
|
40
|
+
* stamped once and every call answers with that FIRST stamp.
|
|
41
|
+
*
|
|
42
|
+
* THE ERROR SURFACE IS `patch`'s AND `submit`'s, deliberately unchanged: a
|
|
43
|
+
* malformed id or a body that is not `{ verifyNonce }` is `400
|
|
44
|
+
* validation_failed`, a missing or malformed `Authorization: Bearer` is `401
|
|
45
|
+
* unauthorized`, and a WRONG token, a WRONG nonce, a credential bound to
|
|
46
|
+
* another listing, an unknown id and an already-submitted row are all one
|
|
47
|
+
* `404 not_found`. The 400s describe the REQUEST's shape and reveal nothing
|
|
48
|
+
* about any row; everything that depends on a row is the single 404. An
|
|
49
|
+
* anonymous surface must not distinguish "exists but not yours" from "does
|
|
50
|
+
* not exist", and this route least of all — a distinguishable answer would
|
|
51
|
+
* make it both an oracle for which drafts exist and a guessing channel for
|
|
52
|
+
* the nonce.
|
|
53
|
+
*
|
|
54
|
+
* THE NONCE TRAVELS IN THE BODY, not the query string, for the reason
|
|
55
|
+
* `resume-token.ts` gives about the resume token: a query parameter lands in
|
|
56
|
+
* access logs, proxy logs and `Referer` headers. The claim is about THIS hop
|
|
57
|
+
* only, and it is worth being exact about that — the nonce's DELIVERY hop is
|
|
58
|
+
* itself a query parameter on the consuming site's URL, so it lands in that
|
|
59
|
+
* site's own access logs exactly as the resume token does. What the body
|
|
60
|
+
* placement buys is that the API's logs and any proxy between the browser and
|
|
61
|
+
* this server never see it. The resume token keeps its `Authorization:
|
|
62
|
+
* Bearer` header. Neither credential is ever logged and neither is ever
|
|
63
|
+
* returned.
|
|
64
|
+
*/
|
|
65
|
+
import { zProjectListingId } from "@working-theory/ids";
|
|
66
|
+
import { projectListingVerifyEmailResponseSchema } from "@working-theory/validation";
|
|
67
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
68
|
+
import { z } from "zod";
|
|
69
|
+
|
|
70
|
+
import { verifyProjectListingDraftEmail } from "../../services/project-listings.js";
|
|
71
|
+
|
|
72
|
+
import { sendValidated } from "./respond.js";
|
|
73
|
+
import { resumeTokenFrom } from "./resume-token.js";
|
|
74
|
+
|
|
75
|
+
const paramsSchema = z.object({ id: zProjectListingId });
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The nonce, as the emailed link carried it. Bounded so an oversized body is
|
|
79
|
+
* refused before any digest is computed; the value is 256 bits of CSPRNG
|
|
80
|
+
* output in base64url, which is 43 characters.
|
|
81
|
+
*/
|
|
82
|
+
const bodySchema = z.object({ verifyNonce: z.string().min(1).max(256) }).strict();
|
|
83
|
+
|
|
84
|
+
export const verifyProjectListingDraftEmailRoute: FastifyPluginAsync = async (
|
|
85
|
+
fastify,
|
|
86
|
+
) => {
|
|
87
|
+
fastify.post(
|
|
88
|
+
"/v1/project-listings/drafts/:id/verify-email",
|
|
89
|
+
async (request, reply) => {
|
|
90
|
+
const parsedParams = paramsSchema.safeParse(request.params);
|
|
91
|
+
if (!parsedParams.success) {
|
|
92
|
+
return reply.status(400).send({
|
|
93
|
+
ok: false,
|
|
94
|
+
error: { code: "validation_failed", message: "Invalid project listing id" },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const resumeToken = resumeTokenFrom(request);
|
|
99
|
+
if (resumeToken === null) {
|
|
100
|
+
return reply.status(401).send({
|
|
101
|
+
ok: false,
|
|
102
|
+
error: { code: "unauthorized", message: "A resume token is required" },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The message names the FIELD, never whether the value would have
|
|
107
|
+
// matched — that distinction is the 404's, below.
|
|
108
|
+
const parsedBody = bodySchema.safeParse(request.body);
|
|
109
|
+
if (!parsedBody.success) {
|
|
110
|
+
return reply.status(400).send({
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
code: "validation_failed",
|
|
114
|
+
message: "A verification nonce is required",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await verifyProjectListingDraftEmail(
|
|
120
|
+
parsedParams.data.id,
|
|
121
|
+
resumeToken,
|
|
122
|
+
parsedBody.data.verifyNonce,
|
|
123
|
+
);
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
return reply.status(404).send({
|
|
126
|
+
ok: false,
|
|
127
|
+
error: { code: "not_found", message: "Draft not found" },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Routing metadata only, the same PII boundary `start` documents: the
|
|
132
|
+
// listing id and the caller's ip, never the founder's email and never
|
|
133
|
+
// the token.
|
|
134
|
+
request.log.info(
|
|
135
|
+
{ listingId: result.value.id, ip: request.ip },
|
|
136
|
+
"project listing draft email verified",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return sendValidated(
|
|
140
|
+
reply,
|
|
141
|
+
projectListingVerifyEmailResponseSchema,
|
|
142
|
+
{
|
|
143
|
+
id: result.value.id,
|
|
144
|
+
emailVerifiedAt: result.value.emailVerifiedAt.toISOString(),
|
|
145
|
+
},
|
|
146
|
+
"verify-email",
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
};
|