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
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
*
|
|
12
|
+
* GET /v1/project-listings/me
|
|
13
|
+
*
|
|
14
|
+
* The FOUNDER'S OWN listings read (#5295) — the `/v1/flow-checkouts/me` and
|
|
15
|
+
* `/v1/waitlist-signups/me` posture, for `ProjectListing`.
|
|
16
|
+
*
|
|
17
|
+
* Registered INSIDE `server.ts`'s `authScope` (auth only — NOT the inner
|
|
18
|
+
* `tenantScope`): a founder is not a workspace member, and the tenant guard is
|
|
19
|
+
* default-deny, so a founder reaching the tenant-scoped list route gets a 403
|
|
20
|
+
* for their own submission. That is the problem this route exists to solve, and
|
|
21
|
+
* it is solved by ADDING this route rather than by relaxing
|
|
22
|
+
* `PROJECT_LISTING_ROLES` — the operator floor on
|
|
23
|
+
* `GET|PATCH /v1/project-listings[/:id]` is unchanged and still `owner|admin`.
|
|
24
|
+
*
|
|
25
|
+
* Identity is `request.userId` ONLY. The route declares no query schema and
|
|
26
|
+
* reads no query or body key — an `email` parameter is never parsed and never
|
|
27
|
+
* read — and the service joins on the owner link and nothing else
|
|
28
|
+
* (`services/project-listings.ts`'s `listMyProjectListings`: `ownerUserId ===
|
|
29
|
+
* userId`, never a `founderEmail` comparison at read time).
|
|
30
|
+
*
|
|
31
|
+
* Route precedence: this static path is registered alongside the parametric
|
|
32
|
+
* `/v1/project-listings/:id` in the tenant scope. Fastify's router prefers a
|
|
33
|
+
* static segment over a parametric one, so `/me` reaches this handler, exactly
|
|
34
|
+
* as the already-shipped static `/v1/project-listings/public` coexists with the
|
|
35
|
+
* same parametric route.
|
|
36
|
+
*
|
|
37
|
+
* Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
|
|
38
|
+
* `{ ok: false, error: { code, message } }`, real HTTP status codes:
|
|
39
|
+
* 401 unauthorized · 500 internal_error (the 200 body failed its own
|
|
40
|
+
* contract schema — a drift is never a leak). A signed-in caller who owns
|
|
41
|
+
* NOTHING is a 200 with an empty list, never a 403: they are legitimately
|
|
42
|
+
* signed in and simply own no listing yet.
|
|
43
|
+
*
|
|
44
|
+
* API↔MCP parity: identity-bound (the session IS the identity; the OSS MCP
|
|
45
|
+
* surface is identity-blind) — no MCP twin (#3591 exception, the same shape as
|
|
46
|
+
* `/v1/flow-checkouts/me` and `/v1/waitlist-signups/me`).
|
|
47
|
+
*/
|
|
48
|
+
import { projectListingMeResponseSchema } from "@working-theory/validation";
|
|
49
|
+
import type { FastifyPluginAsync, FastifyReply } from "fastify";
|
|
50
|
+
|
|
51
|
+
import { listMyProjectListings } from "../../services/project-listings.js";
|
|
52
|
+
|
|
53
|
+
import { sendValidated } from "./respond.js";
|
|
54
|
+
|
|
55
|
+
function unauthorized(reply: FastifyReply) {
|
|
56
|
+
return reply.status(401).send({
|
|
57
|
+
ok: false,
|
|
58
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const projectListingMeRoute: FastifyPluginAsync = async (fastify) => {
|
|
63
|
+
fastify.get("/v1/project-listings/me", async (request, reply) => {
|
|
64
|
+
const userId = request.userId;
|
|
65
|
+
if (!userId) return unauthorized(reply);
|
|
66
|
+
|
|
67
|
+
const listings = await listMyProjectListings(userId);
|
|
68
|
+
|
|
69
|
+
// The omission is explicit at the send site rather than hidden in a schema
|
|
70
|
+
// strip, the idiom `patch-draft.ts` uses: `projectListingDraftSchema` is
|
|
71
|
+
// `.strict()`, so a key that stops being dropped here becomes a 500 instead
|
|
72
|
+
// of quietly shipping an internal reference to the founder.
|
|
73
|
+
const data = listings.map(
|
|
74
|
+
({
|
|
75
|
+
siteId: _siteId,
|
|
76
|
+
leadId: _leadId,
|
|
77
|
+
decidedBy: _decidedBy,
|
|
78
|
+
issueUrl: _issueUrl,
|
|
79
|
+
experimentKey: _experimentKey,
|
|
80
|
+
...mine
|
|
81
|
+
}) => mine,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return sendValidated(reply, projectListingMeResponseSchema, data, "me");
|
|
85
|
+
});
|
|
86
|
+
};
|
|
@@ -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) => {
|
|
@@ -83,12 +107,26 @@ export const startProjectListingRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
const result = await startProjectListingDraft(siteId, parsed.data);
|
|
110
|
+
// #5275 — a `consents` map declining a REQUIRED purpose contradicts the
|
|
111
|
+
// body's own `consent: true`; no row was created.
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
return reply.status(400).send({
|
|
114
|
+
ok: false,
|
|
115
|
+
error: { code: result.error.kind, message: result.error.message },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
86
118
|
|
|
87
119
|
request.log.info(
|
|
88
120
|
{ siteId, listingId: result.id, ip: request.ip },
|
|
89
121
|
"project listing draft started",
|
|
90
122
|
);
|
|
91
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
|
+
|
|
92
130
|
const body = projectListingStartResponseSchema.parse({
|
|
93
131
|
id: result.id,
|
|
94
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
|
+
};
|