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
|
@@ -9,52 +9,50 @@
|
|
|
9
9
|
* - Vercel serverless deployment (api/index.ts)
|
|
10
10
|
* - Local development (src/index.ts)
|
|
11
11
|
*/
|
|
12
|
-
import cors from
|
|
13
|
-
import rateLimit from
|
|
14
|
-
import Fastify from
|
|
15
|
-
import type { FastifyPluginAsync, FastifyPluginCallback } from
|
|
12
|
+
import cors from "@fastify/cors";
|
|
13
|
+
import rateLimit from "@fastify/rate-limit";
|
|
14
|
+
import Fastify from "fastify";
|
|
15
|
+
import type { FastifyPluginAsync, FastifyPluginCallback } from "fastify";
|
|
16
16
|
|
|
17
|
-
import { config } from
|
|
18
|
-
import { isModuleEnabled } from
|
|
19
|
-
import { registerErrorSerializer } from
|
|
20
|
-
import { validateLeadCaptureDir } from
|
|
21
|
-
import {
|
|
22
|
-
import { authPreHandlerPlugin } from
|
|
17
|
+
import { config } from "./config/index.js";
|
|
18
|
+
import { isModuleEnabled } from "./config/modules.js";
|
|
19
|
+
import { registerErrorSerializer } from "./lib/error-serializer.js";
|
|
20
|
+
import { validateLeadCaptureDir } from "./lib/lead-capture-store.js";
|
|
21
|
+
import { loadSlice } from "./lib/slice-load.js";
|
|
22
|
+
import { authPreHandlerPlugin } from "./middleware/auth.js";
|
|
23
23
|
import {
|
|
24
24
|
rateLimitMiddleware,
|
|
25
25
|
DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
26
|
-
} from
|
|
27
|
-
import { openapiRoutes } from
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
} from "./middleware/rate-limit.js";
|
|
27
|
+
import { openapiRoutes } from "./openapi/index.js";
|
|
28
|
+
import {
|
|
29
|
+
deriveRootEndpoints,
|
|
30
|
+
REGISTERED_ROUTES_DECORATOR,
|
|
31
|
+
API_VERSION,
|
|
32
|
+
apiDescription,
|
|
33
|
+
apiDisplayName,
|
|
34
|
+
resolveApiIdentity,
|
|
35
|
+
} from "./openapi/spec.js";
|
|
36
|
+
import type { RegisteredRoute } from "./openapi/surface-drift.js";
|
|
37
|
+
import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
|
|
38
|
+
import { tokenRoutes } from "./routes/auth/tokens.js";
|
|
39
|
+
import { userRoutes } from "./routes/auth/user.js";
|
|
40
|
+
import { getPageRoute } from "./routes/content/get-page.js";
|
|
41
|
+
import { listPagesRoute } from "./routes/content/list-pages.js";
|
|
42
|
+
import { listTypesRoute } from "./routes/content/list-types.js";
|
|
35
43
|
import {
|
|
36
44
|
CONTENT_RATE_LIMIT_UNAUTH,
|
|
37
45
|
CONTENT_RATE_LIMIT_WINDOW,
|
|
38
|
-
} from
|
|
39
|
-
import { deliverableDownloadRoute } from './routes/deliverables/index.js';
|
|
40
|
-
import { flowCheckoutRoutes } from './routes/flow-checkouts/index.js';
|
|
41
|
-
import { getFlowAggregateRoute, getFlowRoute, submitFlowStepRoute } from './routes/flows/index.js';
|
|
42
|
-
import { checkoutRecommendationRoute, saveRecommendationRoute } from './routes/flows/recommender.js';
|
|
46
|
+
} from "./routes/content/rate-limit.js";
|
|
43
47
|
import {
|
|
44
48
|
leadCallbackRoute,
|
|
45
49
|
leadSubmitRoute,
|
|
46
50
|
leadSubscribeRoute,
|
|
47
|
-
} from
|
|
51
|
+
} from "./routes/leads/index.js";
|
|
48
52
|
import {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
projectListingSubmitRoute,
|
|
53
|
-
} from './routes/project-listings/index.js';
|
|
54
|
-
import { slackActionsRoute } from './routes/slack/index.js';
|
|
55
|
-
import { registerTestAuthRoute, shouldMountTestAuth } from './routes/test/auth/session.js';
|
|
56
|
-
import { waitlistSignupRoutes } from './routes/waitlist-signups/index.js';
|
|
57
|
-
import { webhooksRoutes } from './routes/webhooks/index.js';
|
|
53
|
+
registerTestAuthRoute,
|
|
54
|
+
shouldMountTestAuth,
|
|
55
|
+
} from "./routes/test/auth/session.js";
|
|
58
56
|
|
|
59
57
|
/**
|
|
60
58
|
* Paid-module SLICE entry points, keyed by the export each branch needs
|
|
@@ -69,9 +67,15 @@ import { webhooksRoutes } from './routes/webhooks/index.js';
|
|
|
69
67
|
* `./middleware/tenant.js` import was an eleventh — measured on a stripped
|
|
70
68
|
* tree, not argued.
|
|
71
69
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* Keeping the specifier off the `import()` call site is what defeats that
|
|
71
|
+
* resolution, and the INDIRECTION — not the type of the value — is what does
|
|
72
|
+
* the work. WHICH FORM of indirection is not free, and #5304 is the correction:
|
|
73
|
+
* reading the specifier out of a TABLE, as this file did until then, hid it
|
|
74
|
+
* from the deploy-time file tracer as well as from `tsc`, so the files were
|
|
75
|
+
* compiled and never shipped. The form below — a module-level constant
|
|
76
|
+
* dereferenced into `import()` — is unresolvable to `tsc` and traceable to
|
|
77
|
+
* `@vercel/nft`, the only pair of properties that satisfies both the strip and
|
|
78
|
+
* the deployment. S1's `isModuleEnabled()` gate is unchanged and still decides
|
|
75
79
|
* whether the branch runs; `AILK_ENABLED_MODULES` unset still means every
|
|
76
80
|
* module on, so a default build registers exactly what it registered before.
|
|
77
81
|
*
|
|
@@ -81,27 +85,200 @@ import { webhooksRoutes } from './routes/webhooks/index.js';
|
|
|
81
85
|
* with no members, so the module silently fails to load on the tree that HAS
|
|
82
86
|
* it. See the note above the imports in `packages/ui/src/sections/SectionRenderer.tsx`.
|
|
83
87
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
+
* BUNDLING `apps/api` IS NOT A SUPPORTED DEPLOY PATH, and that is a property of
|
|
89
|
+
* the analysers rather than a posture (#5334). esbuild bundles a dynamic
|
|
90
|
+
* `import()` only when its argument is a string literal at the call site —
|
|
91
|
+
* exactly the predicate `tsc` resolves on, and exactly the one the strip
|
|
92
|
+
* forbids. So no expression shape can satisfy both, and in a bundled build
|
|
93
|
+
* these specifiers survive verbatim and resolve against the BUNDLE OUTPUT
|
|
94
|
+
* directory instead of their source module's: `"./list.js"` in
|
|
95
|
+
* `routes/project-listings/index.ts` becomes `dist/list.js` and is not there.
|
|
96
|
+
* esbuild does not warn. `build()` then throws during CONSTRUCTION, so every
|
|
97
|
+
* route 500s rather than one — measured in production, nine hours of total API
|
|
98
|
+
* outage on a consuming site. The supported path is what the scaffold ships:
|
|
99
|
+
* plain `tsc` output traced by `@vercel/node` (`apps/api/vercel.json`,
|
|
100
|
+
* `framework: null`). If you are syncing this file into a repo that bundles,
|
|
101
|
+
* that is the constraint you have just inherited; `apps/api/CLAUDE.md`
|
|
102
|
+
* § Deploy path says what to do instead. Every load site below routes through
|
|
103
|
+
* `loadSlice()` so a bundled build says so at boot rather than failing mute.
|
|
104
|
+
*
|
|
105
|
+
* `./routes/project-listings/index.js` carries two independent load paths on
|
|
106
|
+
* purpose: its five anonymous-surface exports are reached below through
|
|
107
|
+
* `PROJECT_LISTINGS_INDEX` / `SLICE_PLUGINS`, now that signal-collectors gates
|
|
108
|
+
* their registration (D4), while the tenant scope's own `import()` of the same
|
|
109
|
+
* specifier stays a literal — the file is a MIXED directory that always ships
|
|
110
|
+
* (multi-tenant's `TENANT_HANDLERS` table lives in the same file), so a literal
|
|
111
|
+
* there resolves fine on every tree regardless of module selection.
|
|
112
|
+
*/
|
|
113
|
+
/**
|
|
114
|
+
* One module-level constant per slice module. The call sites below read the
|
|
115
|
+
* loader out of `SLICE_PLUGINS`; the specifier itself is never written at an
|
|
116
|
+
* `import()` call site, which is what keeps `tsc` from resolving it (#5182 D1).
|
|
117
|
+
*
|
|
118
|
+
* The constant is not a style choice — it is the mechanism (#5304 D3).
|
|
119
|
+
* `@vercel/nft` constant-propagates a module-level binding into `import()` and
|
|
120
|
+
* emits the file; it cannot evaluate a table lookup or a for-of destructure,
|
|
121
|
+
* which is what the previous `Record<string, string>` shape forced it to do.
|
|
122
|
+
* So the old shape was invisible to the DEPLOY tracer as well as to `tsc`, the
|
|
123
|
+
* files went untraced and unshipped, and `build()` threw ERR_MODULE_NOT_FOUND
|
|
124
|
+
* on the first slice it reached — measured in production, nine hours of total
|
|
125
|
+
* API outage on a consuming site. Both analysers are re-measured on every run
|
|
126
|
+
* by `scripts/__tests__/enumerate-slice-specifiers.test.ts`, and
|
|
127
|
+
* `scripts/check-slice-reachability.ts` fails when any specifier here stops
|
|
128
|
+
* being traced.
|
|
88
129
|
*/
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
const WORKSPACES_INDEX = "./routes/workspaces/index.js";
|
|
131
|
+
const WORKSPACES_SESSIONS = "./routes/workspaces/sessions.js";
|
|
132
|
+
const WORKSPACES_CONFIG = "./routes/workspaces/config.js";
|
|
133
|
+
const BILLING_INDEX = "./routes/billing/index.js";
|
|
134
|
+
const WAITLIST_EXPERIMENTS_INDEX = "./routes/waitlist-experiments/index.js";
|
|
135
|
+
const CONTENT_CREATE = "./routes/content/create.js";
|
|
136
|
+
const CONTENT_UPDATE = "./routes/content/update.js";
|
|
137
|
+
const CONTENT_DELETE = "./routes/content/delete.js";
|
|
138
|
+
const SCHEDULE_INDEX = "./routes/schedule/index.js";
|
|
139
|
+
const FLOWS_INDEX = "./routes/flows/index.js";
|
|
140
|
+
const DELIVERABLES_INDEX = "./routes/deliverables/index.js";
|
|
141
|
+
const PROJECT_LISTINGS_INDEX = "./routes/project-listings/index.js";
|
|
142
|
+
const SLACK_INDEX = "./routes/slack/index.js";
|
|
143
|
+
const WAITLIST_SIGNUPS_INDEX = "./routes/waitlist-signups/index.js";
|
|
144
|
+
const CHECKOUT_INDEX = "./routes/checkout/index.js";
|
|
145
|
+
const WEBHOOKS_INDEX = "./routes/webhooks/index.js";
|
|
146
|
+
const FLOW_CHECKOUTS_INDEX = "./routes/flow-checkouts/index.js";
|
|
147
|
+
const RECOMMENDER_INDEX = "./routes/flows/recommender.js";
|
|
103
148
|
|
|
104
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Paid-module SLICE entry points, keyed by the export each branch needs.
|
|
151
|
+
*
|
|
152
|
+
* `specifier` is carried alongside `load` for the drift message below, and both
|
|
153
|
+
* read the SAME constant, so the two can never disagree. `load` is what runs:
|
|
154
|
+
* an arrow closing over that constant, which is the only shape both analysers
|
|
155
|
+
* treat as intended — unresolvable to `tsc`, traceable to `nft`.
|
|
156
|
+
*/
|
|
157
|
+
const SLICE_PLUGINS = {
|
|
158
|
+
workspaceContextRoutes: {
|
|
159
|
+
specifier: WORKSPACES_INDEX,
|
|
160
|
+
load: () => import(WORKSPACES_INDEX),
|
|
161
|
+
},
|
|
162
|
+
workspaceSessionRoutes: {
|
|
163
|
+
specifier: WORKSPACES_INDEX,
|
|
164
|
+
load: () => import(WORKSPACES_INDEX),
|
|
165
|
+
},
|
|
166
|
+
sessionRoutes: {
|
|
167
|
+
specifier: WORKSPACES_SESSIONS,
|
|
168
|
+
load: () => import(WORKSPACES_SESSIONS),
|
|
169
|
+
},
|
|
170
|
+
workspaceConfigRoutes: {
|
|
171
|
+
specifier: WORKSPACES_CONFIG,
|
|
172
|
+
load: () => import(WORKSPACES_CONFIG),
|
|
173
|
+
},
|
|
174
|
+
billingRoutes: {
|
|
175
|
+
specifier: BILLING_INDEX,
|
|
176
|
+
load: () => import(BILLING_INDEX),
|
|
177
|
+
},
|
|
178
|
+
waitlistExperimentRoutes: {
|
|
179
|
+
specifier: WAITLIST_EXPERIMENTS_INDEX,
|
|
180
|
+
load: () => import(WAITLIST_EXPERIMENTS_INDEX),
|
|
181
|
+
},
|
|
182
|
+
createPageRoute: {
|
|
183
|
+
specifier: CONTENT_CREATE,
|
|
184
|
+
load: () => import(CONTENT_CREATE),
|
|
185
|
+
},
|
|
186
|
+
updatePageRoute: {
|
|
187
|
+
specifier: CONTENT_UPDATE,
|
|
188
|
+
load: () => import(CONTENT_UPDATE),
|
|
189
|
+
},
|
|
190
|
+
deletePageRoute: {
|
|
191
|
+
specifier: CONTENT_DELETE,
|
|
192
|
+
load: () => import(CONTENT_DELETE),
|
|
193
|
+
},
|
|
194
|
+
listAvailabilityRoute: {
|
|
195
|
+
specifier: SCHEDULE_INDEX,
|
|
196
|
+
load: () => import(SCHEDULE_INDEX),
|
|
197
|
+
},
|
|
198
|
+
getEventMetaRoute: {
|
|
199
|
+
specifier: SCHEDULE_INDEX,
|
|
200
|
+
load: () => import(SCHEDULE_INDEX),
|
|
201
|
+
},
|
|
202
|
+
createBookingRoute: {
|
|
203
|
+
specifier: SCHEDULE_INDEX,
|
|
204
|
+
load: () => import(SCHEDULE_INDEX),
|
|
205
|
+
},
|
|
206
|
+
getFlowRoute: {
|
|
207
|
+
specifier: FLOWS_INDEX,
|
|
208
|
+
load: () => import(FLOWS_INDEX),
|
|
209
|
+
},
|
|
210
|
+
submitFlowStepRoute: {
|
|
211
|
+
specifier: FLOWS_INDEX,
|
|
212
|
+
load: () => import(FLOWS_INDEX),
|
|
213
|
+
},
|
|
214
|
+
getFlowAggregateRoute: {
|
|
215
|
+
specifier: FLOWS_INDEX,
|
|
216
|
+
load: () => import(FLOWS_INDEX),
|
|
217
|
+
},
|
|
218
|
+
deliverableDownloadRoute: {
|
|
219
|
+
specifier: DELIVERABLES_INDEX,
|
|
220
|
+
load: () => import(DELIVERABLES_INDEX),
|
|
221
|
+
},
|
|
222
|
+
projectListingStartRoute: {
|
|
223
|
+
specifier: PROJECT_LISTINGS_INDEX,
|
|
224
|
+
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
225
|
+
},
|
|
226
|
+
projectListingDraftPatchRoute: {
|
|
227
|
+
specifier: PROJECT_LISTINGS_INDEX,
|
|
228
|
+
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
229
|
+
},
|
|
230
|
+
projectListingSubmitRoute: {
|
|
231
|
+
specifier: PROJECT_LISTINGS_INDEX,
|
|
232
|
+
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
233
|
+
},
|
|
234
|
+
// #5407 — the resume link's verification write. Anonymous surface,
|
|
235
|
+
// module-conditional (signal-collectors, D-48) like its four siblings.
|
|
236
|
+
projectListingVerifyEmailRoute: {
|
|
237
|
+
specifier: PROJECT_LISTINGS_INDEX,
|
|
238
|
+
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
239
|
+
},
|
|
240
|
+
projectListingPublicRoute: {
|
|
241
|
+
specifier: PROJECT_LISTINGS_INDEX,
|
|
242
|
+
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
243
|
+
},
|
|
244
|
+
projectListingMeRoute: {
|
|
245
|
+
specifier: PROJECT_LISTINGS_INDEX,
|
|
246
|
+
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
247
|
+
},
|
|
248
|
+
slackActionsRoute: {
|
|
249
|
+
specifier: SLACK_INDEX,
|
|
250
|
+
load: () => import(SLACK_INDEX),
|
|
251
|
+
},
|
|
252
|
+
waitlistSignupRoutes: {
|
|
253
|
+
specifier: WAITLIST_SIGNUPS_INDEX,
|
|
254
|
+
load: () => import(WAITLIST_SIGNUPS_INDEX),
|
|
255
|
+
},
|
|
256
|
+
// Commerce (D-47, #5364) — checkout, the Stripe webhook receiver, flow
|
|
257
|
+
// checkouts and the recommender. Module-conditional (isModuleEnabled
|
|
258
|
+
// "commerce") like every other paid slice above.
|
|
259
|
+
checkoutRoutes: {
|
|
260
|
+
specifier: CHECKOUT_INDEX,
|
|
261
|
+
load: () => import(CHECKOUT_INDEX),
|
|
262
|
+
},
|
|
263
|
+
webhooksRoutes: {
|
|
264
|
+
specifier: WEBHOOKS_INDEX,
|
|
265
|
+
load: () => import(WEBHOOKS_INDEX),
|
|
266
|
+
},
|
|
267
|
+
flowCheckoutRoutes: {
|
|
268
|
+
specifier: FLOW_CHECKOUTS_INDEX,
|
|
269
|
+
load: () => import(FLOW_CHECKOUTS_INDEX),
|
|
270
|
+
},
|
|
271
|
+
saveRecommendationRoute: {
|
|
272
|
+
specifier: RECOMMENDER_INDEX,
|
|
273
|
+
load: () => import(RECOMMENDER_INDEX),
|
|
274
|
+
},
|
|
275
|
+
checkoutRecommendationRoute: {
|
|
276
|
+
specifier: RECOMMENDER_INDEX,
|
|
277
|
+
load: () => import(RECOMMENDER_INDEX),
|
|
278
|
+
},
|
|
279
|
+
} satisfies Record<string, { specifier: string; load: () => Promise<unknown> }>;
|
|
280
|
+
|
|
281
|
+
type SlicePluginName = keyof typeof SLICE_PLUGINS;
|
|
105
282
|
|
|
106
283
|
/**
|
|
107
284
|
* Load one slice route plugin. Every entry above is a `FastifyPluginAsync`
|
|
@@ -116,8 +293,8 @@ type SlicePluginName = keyof typeof SLICE_PLUGIN_SPECIFIERS;
|
|
|
116
293
|
async function sliceRoutePlugin(
|
|
117
294
|
name: SlicePluginName,
|
|
118
295
|
): Promise<FastifyPluginAsync> {
|
|
119
|
-
const specifier
|
|
120
|
-
const mod = (await
|
|
296
|
+
const { specifier, load } = SLICE_PLUGINS[name];
|
|
297
|
+
const mod = (await loadSlice(specifier, load)) as Record<
|
|
121
298
|
string,
|
|
122
299
|
FastifyPluginAsync | undefined
|
|
123
300
|
>;
|
|
@@ -134,17 +311,40 @@ async function sliceRoutePlugin(
|
|
|
134
311
|
* (apps/api/src/middleware/tenant.ts:83).
|
|
135
312
|
*/
|
|
136
313
|
async function tenantPreHandlerSlicePlugin(): Promise<FastifyPluginCallback> {
|
|
137
|
-
const specifier: string =
|
|
138
|
-
const mod = (await import(specifier)) as {
|
|
314
|
+
const specifier: string = "./middleware/tenant.js";
|
|
315
|
+
const mod = (await loadSlice(specifier, () => import(specifier))) as {
|
|
139
316
|
tenantPreHandlerPlugin?: FastifyPluginCallback;
|
|
140
317
|
};
|
|
141
318
|
const plugin = mod.tenantPreHandlerPlugin;
|
|
142
319
|
if (!plugin) {
|
|
143
|
-
throw new Error(
|
|
320
|
+
throw new Error(
|
|
321
|
+
"slice export tenantPreHandlerPlugin missing from " + specifier,
|
|
322
|
+
);
|
|
144
323
|
}
|
|
145
324
|
return plugin;
|
|
146
325
|
}
|
|
147
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The commerce boot-time sandbox guard — the one other slice entry that is
|
|
329
|
+
* not a route plugin, mirroring `tenantPreHandlerSlicePlugin()` above (D3).
|
|
330
|
+
* `lib/stripe.ts` is commerce-owned (D5) and its `assertSandboxKey` is the
|
|
331
|
+
* sole `apps/api` reach into the Stripe SDK wrapper package, so it can no
|
|
332
|
+
* longer be a static import (spec D1) once the module is stripped. The
|
|
333
|
+
* widened `const specifier: string` annotation is what defeats `tsc`
|
|
334
|
+
* resolution, exactly as it does above.
|
|
335
|
+
*/
|
|
336
|
+
async function assertCommerceSandboxKey(key: string): Promise<void> {
|
|
337
|
+
const specifier: string = "./lib/stripe.js";
|
|
338
|
+
const mod = (await loadSlice(specifier, () => import(specifier))) as {
|
|
339
|
+
assertSandboxKey?: (key: string) => void;
|
|
340
|
+
};
|
|
341
|
+
const assertSandboxKeyFn = mod.assertSandboxKey;
|
|
342
|
+
if (!assertSandboxKeyFn) {
|
|
343
|
+
throw new Error("slice export assertSandboxKey missing from " + specifier);
|
|
344
|
+
}
|
|
345
|
+
assertSandboxKeyFn(key);
|
|
346
|
+
}
|
|
347
|
+
|
|
148
348
|
/**
|
|
149
349
|
* Build Fastify server instance (for serverless or standalone)
|
|
150
350
|
* @returns Configured Fastify instance ready for requests
|
|
@@ -155,9 +355,11 @@ export async function build() {
|
|
|
155
355
|
|
|
156
356
|
// Runtime sandbox-mode guard: reject live-mode Stripe keys before any
|
|
157
357
|
// webhook or checkout call can exercise them. Idempotent — safe even if
|
|
158
|
-
// assertSandboxKey is called again inside a route handler.
|
|
159
|
-
|
|
160
|
-
|
|
358
|
+
// assertSandboxKey is called again inside a route handler. Module-
|
|
359
|
+
// conditional (commerce, D-47/D3): a build without commerce has no
|
|
360
|
+
// commerce request path to guard, and lib/stripe.ts is stripped with it.
|
|
361
|
+
if (isModuleEnabled("commerce") && config.STRIPE_SECRET_KEY) {
|
|
362
|
+
await assertCommerceSandboxKey(config.STRIPE_SECRET_KEY);
|
|
161
363
|
}
|
|
162
364
|
|
|
163
365
|
// trustProxy: true (#4730) — the Vercel serverless handler
|
|
@@ -187,6 +389,32 @@ export async function build() {
|
|
|
187
389
|
trustProxy: true,
|
|
188
390
|
});
|
|
189
391
|
|
|
392
|
+
// Collect the route table this server registers (#5314).
|
|
393
|
+
//
|
|
394
|
+
// Installed here — after `Fastify()` and before the first `register()` — so
|
|
395
|
+
// no route can precede it: a root-instance `onRoute` hook fires for every
|
|
396
|
+
// route added afterwards in ANY encapsulated child scope, which is what lets
|
|
397
|
+
// one hook see the auth, tenant, webhooks, leads, flows, aeo and schedule
|
|
398
|
+
// scopes without touching any of them.
|
|
399
|
+
//
|
|
400
|
+
// HEAD is dropped: Fastify synthesises it from every GET, so publishing it
|
|
401
|
+
// would describe routes no handler declared.
|
|
402
|
+
//
|
|
403
|
+
// The table lives on THIS instance, not in module state, so two servers
|
|
404
|
+
// built concurrently in one process cannot corrupt each other's — see the
|
|
405
|
+
// note on `readRegisteredRoutes` in openapi/spec.ts for the concrete race.
|
|
406
|
+
// The decorator holds the array by reference and the hook pushes into it, so
|
|
407
|
+
// every reader sees the current contents without re-publishing.
|
|
408
|
+
const registeredRoutes: RegisteredRoute[] = [];
|
|
409
|
+
server.decorate(REGISTERED_ROUTES_DECORATOR, registeredRoutes);
|
|
410
|
+
server.addHook("onRoute", (route) => {
|
|
411
|
+
const methods = Array.isArray(route.method) ? route.method : [route.method];
|
|
412
|
+
for (const method of methods) {
|
|
413
|
+
if (method === "HEAD") continue;
|
|
414
|
+
registeredRoutes.push({ method, url: route.url });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
190
418
|
// Register error serializer — maps internal exceptions to generic strings
|
|
191
419
|
// so connection strings, hostnames, stack traces, and SDK error details
|
|
192
420
|
// never reach the client. Full detail still goes to pino at error level.
|
|
@@ -198,9 +426,32 @@ export async function build() {
|
|
|
198
426
|
// `credentials: true` would let any website read credentialed responses
|
|
199
427
|
// (security review M1). `false` disables cross-origin access entirely;
|
|
200
428
|
// same-origin and non-browser callers are unaffected.
|
|
429
|
+
//
|
|
430
|
+
// `methods` is EXPLICIT because @fastify/cors defaults it to `GET,HEAD,POST`
|
|
431
|
+
// (its own `defaultOptions`), which is narrower than the surface this server
|
|
432
|
+
// registers (#5397). With only `origin` + `credentials` passed, the preflight
|
|
433
|
+
// for `PATCH /v1/project-listings/drafts/:id` answered
|
|
434
|
+
// `access-control-allow-methods: GET, HEAD, POST`, so a browser refused every
|
|
435
|
+
// draft save on a split-origin deploy — a site could create a listing draft
|
|
436
|
+
// and never save one. The list below is the union of the methods the route
|
|
437
|
+
// table actually serves (GET/POST/PATCH/PUT/DELETE — `/v1/auth/tokens/:id`,
|
|
438
|
+
// `/v1/content/:type/:slug`, `/v1/user/data/:key`, the workspace-config PUTs),
|
|
439
|
+
// plus HEAD (Fastify synthesises it from every GET) and OPTIONS (the plugin's
|
|
440
|
+
// own wildcard preflight route). `cors.test.ts` re-derives that union from the
|
|
441
|
+
// live route table on every run, so a route added in a NEW method fails there
|
|
442
|
+
// rather than silently becoming unreachable from a browser.
|
|
443
|
+
//
|
|
444
|
+
// This widens WHICH METHODS the allowed origin may use, never WHO is allowed:
|
|
445
|
+
// `origin` and `credentials` are untouched.
|
|
446
|
+
//
|
|
447
|
+
// `allowedHeaders` is deliberately left unset: null means @fastify/cors
|
|
448
|
+
// reflects the request's `Access-Control-Request-Headers`, so `authorization`
|
|
449
|
+
// and `content-type` pass without this file having to maintain a list that
|
|
450
|
+
// silently drops a header a future route needs.
|
|
201
451
|
await server.register(cors, {
|
|
202
|
-
origin: config.FRONTEND_URL || config.NODE_ENV !==
|
|
452
|
+
origin: config.FRONTEND_URL || config.NODE_ENV !== "production",
|
|
203
453
|
credentials: true,
|
|
454
|
+
methods: ["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
204
455
|
});
|
|
205
456
|
|
|
206
457
|
// Register rate limiting — applied globally with the unauth ceiling. The
|
|
@@ -213,32 +464,35 @@ export async function build() {
|
|
|
213
464
|
});
|
|
214
465
|
|
|
215
466
|
// Health check
|
|
216
|
-
server.get(
|
|
217
|
-
status:
|
|
467
|
+
server.get("/health", async () => ({
|
|
468
|
+
status: "healthy",
|
|
218
469
|
timestamp: new Date().toISOString(),
|
|
219
470
|
}));
|
|
220
471
|
|
|
221
|
-
// Root endpoint
|
|
222
|
-
|
|
472
|
+
// Root endpoint — introduces this deployment as the SITE it serves, not as
|
|
473
|
+
// the kit it was built with (#5307). `_site.mdx` is read once per process;
|
|
474
|
+
// a site without one degrades to a neutral name, never to the kit's.
|
|
475
|
+
server.get("/", async () => {
|
|
476
|
+
const identity = resolveApiIdentity();
|
|
223
477
|
return {
|
|
224
|
-
name:
|
|
225
|
-
version:
|
|
226
|
-
description:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
478
|
+
name: apiDisplayName(identity),
|
|
479
|
+
version: API_VERSION,
|
|
480
|
+
description: apiDescription(identity),
|
|
481
|
+
// Derived from the routes this process actually registered (#5314),
|
|
482
|
+
// not a hand-maintained literal. The literal listed 11 endpoints against
|
|
483
|
+
// 68 registered routes, omitted `/v1/project-listings/public` and
|
|
484
|
+
// `/v1/billing/usage`, advertised `DELETE /v1/auth/tokens` at a path
|
|
485
|
+
// that 404s (the route is `/v1/auth/tokens/:id`), and named two billing
|
|
486
|
+
// routes on builds where the multi-tenant module registers neither.
|
|
487
|
+
// Same mechanism now covers commerce (D-47, #5364): checkout, the
|
|
488
|
+
// Stripe webhook, the flow-checkouts read and the two recommender
|
|
489
|
+
// routes are derived exactly like the billing routes above and
|
|
490
|
+
// disappear from this list on a build where `commerce` is off, rather
|
|
491
|
+
// than advertising a route that 404s.
|
|
492
|
+
//
|
|
493
|
+
// Read at REQUEST time, so `ready()` has fired and the set is complete
|
|
494
|
+
// even though this handler is registered before most routes.
|
|
495
|
+
endpoints: deriveRootEndpoints(registeredRoutes),
|
|
242
496
|
};
|
|
243
497
|
});
|
|
244
498
|
|
|
@@ -257,20 +511,48 @@ export async function build() {
|
|
|
257
511
|
// apply to them; each enforces membership in the service layer.
|
|
258
512
|
// Module-conditional (S1, #5088): without the multi-tenant module there
|
|
259
513
|
// is no workspace concept to switch or inspect.
|
|
260
|
-
if (isModuleEnabled(
|
|
261
|
-
await authScope.register(
|
|
514
|
+
if (isModuleEnabled("multi-tenant")) {
|
|
515
|
+
await authScope.register(
|
|
516
|
+
await sliceRoutePlugin("workspaceContextRoutes"),
|
|
517
|
+
);
|
|
262
518
|
}
|
|
263
519
|
|
|
264
520
|
// Respondent signup read (#4731) — auth only, NOT tenant-guarded: a
|
|
265
521
|
// respondent has no workspace membership and the tenant guard is
|
|
266
522
|
// default-deny. Identity is request.userId; the service re-checks
|
|
267
|
-
// User.emailVerified before any Lead read.
|
|
268
|
-
|
|
523
|
+
// User.emailVerified before any Lead read. Module-conditional
|
|
524
|
+
// (signal-collectors, D-48): the waitlist instrument's respondent half.
|
|
525
|
+
if (isModuleEnabled("signal-collectors")) {
|
|
526
|
+
await authScope.register(await sliceRoutePlugin("waitlistSignupRoutes"));
|
|
527
|
+
}
|
|
269
528
|
|
|
270
529
|
// Respondent checkouts read (#4984) — the same posture as the signup
|
|
271
530
|
// read above: auth only, NOT tenant-guarded. Identity is request.userId;
|
|
272
531
|
// the only join is the lead-promotion link (never an email match).
|
|
273
|
-
|
|
532
|
+
// Module-conditional (commerce, D-47): the `/account` Purchases panel
|
|
533
|
+
// already degrades to its empty state with no code change when this is
|
|
534
|
+
// absent (`SKIPPED_STATUSES` covers 404 — spec §4.2).
|
|
535
|
+
if (isModuleEnabled("commerce")) {
|
|
536
|
+
await authScope.register(await sliceRoutePlugin("flowCheckoutRoutes"));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// The founder's OWN listings read (#5295) — the same posture again: auth
|
|
540
|
+
// only, NOT tenant-guarded, because a founder is not a workspace member
|
|
541
|
+
// and the tenant guard is default-deny. Identity is request.userId; the
|
|
542
|
+
// only join is the owner link (ProjectListing.ownerUserId, stamped by
|
|
543
|
+
// services/listing-promotion.ts), never an email match. The tenant-scoped
|
|
544
|
+
// listing routes below keep their owner|admin floor untouched — this route
|
|
545
|
+
// is additive, not a relaxation of that floor. Registered here rather than
|
|
546
|
+
// inside the multi-tenant scope so a founder can read their own rows on a
|
|
547
|
+
// build without the module; the static `/me` path takes router precedence
|
|
548
|
+
// over the tenant scope's parametric `/v1/project-listings/:id`, exactly as
|
|
549
|
+
// the already-shipped static `/v1/project-listings/public` does.
|
|
550
|
+
// Module-conditional (signal-collectors, D-48 / D4): the listing
|
|
551
|
+
// instrument's founder-read half; not severable from the four anonymous
|
|
552
|
+
// listing routes below (same static barrel, D4).
|
|
553
|
+
if (isModuleEnabled("signal-collectors")) {
|
|
554
|
+
await authScope.register(await sliceRoutePlugin("projectListingMeRoute"));
|
|
555
|
+
}
|
|
274
556
|
|
|
275
557
|
// Tenant-guarded domain routes (multi-tenant module) — the tenant
|
|
276
558
|
// preHandler (default-deny guard, see middleware/tenant.ts) runs after
|
|
@@ -279,24 +561,27 @@ export async function build() {
|
|
|
279
561
|
// no tenant concept at all — the whole scope is module-conditional
|
|
280
562
|
// (S1, #5088), so an OSS build that excludes multi-tenant registers
|
|
281
563
|
// none of it.
|
|
282
|
-
if (isModuleEnabled(
|
|
564
|
+
if (isModuleEnabled("multi-tenant")) {
|
|
283
565
|
await authScope.register(async (tenantScope) => {
|
|
284
566
|
await tenantScope.register(await tenantPreHandlerSlicePlugin());
|
|
285
|
-
await tenantScope.register(
|
|
286
|
-
|
|
287
|
-
|
|
567
|
+
await tenantScope.register(
|
|
568
|
+
await sliceRoutePlugin("workspaceSessionRoutes"),
|
|
569
|
+
);
|
|
570
|
+
await tenantScope.register(await sliceRoutePlugin("sessionRoutes"));
|
|
571
|
+
await tenantScope.register(
|
|
572
|
+
await sliceRoutePlugin("workspaceConfigRoutes"),
|
|
573
|
+
);
|
|
288
574
|
// Org-billing routes (#3701) — org-scoped read + Stripe customer-portal.
|
|
289
|
-
await tenantScope.register(await sliceRoutePlugin(
|
|
575
|
+
await tenantScope.register(await sliceRoutePlugin("billingRoutes"));
|
|
290
576
|
// Waitlist operator-dashboard read API (#4665 D7, B0) — tenant-scoped
|
|
291
577
|
// list/comparison/signups reads over A1's waitlist-experiment rows.
|
|
292
578
|
await tenantScope.register(
|
|
293
|
-
await sliceRoutePlugin(
|
|
579
|
+
await sliceRoutePlugin("waitlistExperimentRoutes"),
|
|
294
580
|
);
|
|
295
581
|
// Project-listings dashboard (#4998) — tenant-scoped list/get/patch
|
|
296
582
|
// over the founder's-application-row lifecycle.
|
|
297
|
-
const { projectListingTenantRoutes } =
|
|
298
|
-
|
|
299
|
-
);
|
|
583
|
+
const { projectListingTenantRoutes } =
|
|
584
|
+
await import("./routes/project-listings/index.js");
|
|
300
585
|
await tenantScope.register(projectListingTenantRoutes);
|
|
301
586
|
});
|
|
302
587
|
}
|
|
@@ -311,22 +596,28 @@ export async function build() {
|
|
|
311
596
|
// module-conditional at REGISTRATION too (S1, #5088) so an OSS build that
|
|
312
597
|
// excludes the content-write slice never wires the handler in at all. Not
|
|
313
598
|
// a MODULE_SLICES entry — see config/modules.ts.
|
|
314
|
-
if (isModuleEnabled(
|
|
315
|
-
await server.register(await sliceRoutePlugin(
|
|
316
|
-
await server.register(await sliceRoutePlugin(
|
|
317
|
-
await server.register(await sliceRoutePlugin(
|
|
599
|
+
if (isModuleEnabled("content-write")) {
|
|
600
|
+
await server.register(await sliceRoutePlugin("createPageRoute"));
|
|
601
|
+
await server.register(await sliceRoutePlugin("updatePageRoute"));
|
|
602
|
+
await server.register(await sliceRoutePlugin("deletePageRoute"));
|
|
603
|
+
}
|
|
604
|
+
// Module-conditional (commerce, D-47): checkout moved to its own module.
|
|
605
|
+
if (isModuleEnabled("commerce")) {
|
|
606
|
+
await server.register(await sliceRoutePlugin("checkoutRoutes"));
|
|
318
607
|
}
|
|
319
|
-
await server.register(checkoutRoutes);
|
|
320
608
|
// Deliverable download — a capability-token URL (#3961 D5); the token
|
|
321
|
-
// itself is the sole gate, so no auth preHandler applies.
|
|
322
|
-
|
|
609
|
+
// itself is the sole gate, so no auth preHandler applies. Module-conditional
|
|
610
|
+
// (signal-collectors, D-48): gated PDFs and reports.
|
|
611
|
+
if (isModuleEnabled("signal-collectors")) {
|
|
612
|
+
await server.register(await sliceRoutePlugin("deliverableDownloadRoute"));
|
|
613
|
+
}
|
|
323
614
|
|
|
324
615
|
// Webhooks routes — raw-body parser scoped to this sub-router only so the
|
|
325
616
|
// global JSON parser on all other routes is unaffected.
|
|
326
617
|
await server.register(async (webhooksScope) => {
|
|
327
618
|
webhooksScope.addContentTypeParser(
|
|
328
|
-
|
|
329
|
-
{ parseAs:
|
|
619
|
+
"application/json",
|
|
620
|
+
{ parseAs: "buffer" },
|
|
330
621
|
(_req, body, done) => {
|
|
331
622
|
done(null, body);
|
|
332
623
|
},
|
|
@@ -335,14 +626,24 @@ export async function build() {
|
|
|
335
626
|
// field (#4999) — raw buffer here too, so the signature is verified over
|
|
336
627
|
// the EXACT bytes Slack signed, never a re-serialized parse.
|
|
337
628
|
webhooksScope.addContentTypeParser(
|
|
338
|
-
|
|
339
|
-
{ parseAs:
|
|
629
|
+
"application/x-www-form-urlencoded",
|
|
630
|
+
{ parseAs: "buffer" },
|
|
340
631
|
(_req, body, done) => {
|
|
341
632
|
done(null, body);
|
|
342
633
|
},
|
|
343
634
|
);
|
|
344
|
-
|
|
345
|
-
|
|
635
|
+
// Stripe webhook receiver — module-conditional (commerce, D-47). The
|
|
636
|
+
// scope wrapper and both raw-body parsers above stay unconditional: the
|
|
637
|
+
// Slack parser below needs them too (D2).
|
|
638
|
+
if (isModuleEnabled("commerce")) {
|
|
639
|
+
await webhooksScope.register(await sliceRoutePlugin("webhooksRoutes"));
|
|
640
|
+
}
|
|
641
|
+
// Project-listing operator decision (Slack Approve/Reject) — the "hand an
|
|
642
|
+
// operator a decision" half of D-48's grouping sentence. Module-conditional
|
|
643
|
+
// (signal-collectors).
|
|
644
|
+
if (isModuleEnabled("signal-collectors")) {
|
|
645
|
+
await webhooksScope.register(await sliceRoutePlugin("slackActionsRoute"));
|
|
646
|
+
}
|
|
346
647
|
});
|
|
347
648
|
|
|
348
649
|
// Leads routes — rate-limit middleware scoped to this sub-router. Own
|
|
@@ -359,20 +660,43 @@ export async function build() {
|
|
|
359
660
|
// Project-listings anonymous surface (#4998, reshaped by #5036) — the
|
|
360
661
|
// three-operation draft lifecycle plus the public page-render read, all
|
|
361
662
|
// on the same rate-limited, identity-blind posture as the leads routes
|
|
362
|
-
// above.
|
|
663
|
+
// above. Module-conditional (signal-collectors, D-48): the listing
|
|
664
|
+
// instrument's application-lifecycle half.
|
|
363
665
|
//
|
|
364
666
|
// Do NOT read that rate limit as a bound on anonymous draft creation. It
|
|
365
667
|
// keys on `req.ip`, and `trustProxy` is on (see the note at the top of this
|
|
366
668
|
// file), so a caller reaching the API directly can vary
|
|
367
669
|
// `X-Forwarded-For` and get a fresh budget per request. That is a
|
|
368
670
|
// pre-existing property of the limiter, not of these routes; what these
|
|
369
|
-
// routes do is keep the cost of an unbounded call small —
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
671
|
+
// routes do is keep the cost of an unbounded call small — the Lead write
|
|
672
|
+
// with its operator notification waits for `submit` (PR #5042 security
|
|
673
|
+
// review, finding 4).
|
|
674
|
+
//
|
|
675
|
+
// `start` DOES now send one email (#5406, the founder's resume link), so
|
|
676
|
+
// the older form of this note — "`start` writes one row and sends
|
|
677
|
+
// nothing" — no longer holds. That send is opt-in per site, and where it
|
|
678
|
+
// is on it is bounded in the send path rather than per IP
|
|
679
|
+
// (`services/project-listing-resume-email.ts`), precisely because this IP
|
|
680
|
+
// limiter cannot carry the weight of an outbound-mail decision. Two bounds,
|
|
681
|
+
// both counted in the database so they hold across instances: PER RECIPIENT
|
|
682
|
+
// (PR #5411 security review, finding 1) and, since #5414, PER SITE across
|
|
683
|
+
// every recipient — the residual the first one left, and the one that
|
|
684
|
+
// decides whether a flood can cost the site its sending domain.
|
|
685
|
+
if (isModuleEnabled("signal-collectors")) {
|
|
686
|
+
await leadsScope.register(await sliceRoutePlugin("projectListingStartRoute"));
|
|
687
|
+
await leadsScope.register(
|
|
688
|
+
await sliceRoutePlugin("projectListingDraftPatchRoute"),
|
|
689
|
+
);
|
|
690
|
+
await leadsScope.register(await sliceRoutePlugin("projectListingSubmitRoute"));
|
|
691
|
+
// #5407 — the resume link's verification write. Anonymous, and gated
|
|
692
|
+
// on the resume token AND the nonce that only the resume email
|
|
693
|
+
// carries, so it belongs in the same rate-limited scope as the other
|
|
694
|
+
// three.
|
|
695
|
+
await leadsScope.register(
|
|
696
|
+
await sliceRoutePlugin("projectListingVerifyEmailRoute"),
|
|
697
|
+
);
|
|
698
|
+
await leadsScope.register(await sliceRoutePlugin("projectListingPublicRoute"));
|
|
699
|
+
}
|
|
376
700
|
});
|
|
377
701
|
|
|
378
702
|
// Flows routes — anonymous surface; rate-limit middleware scoped to this
|
|
@@ -383,13 +707,23 @@ export async function build() {
|
|
|
383
707
|
// scope #4730's bump was actually sized for; unchanged by #5111.
|
|
384
708
|
await server.register(async (flowsScope) => {
|
|
385
709
|
flowsScope.register(rateLimitMiddleware);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
710
|
+
// Flow engine reads (#3956) — module-conditional (signal-collectors,
|
|
711
|
+
// D-48). The scope itself and its rate limiter above stay unconditional
|
|
712
|
+
// (D2) — commerce's recommender routes below need them too.
|
|
713
|
+
if (isModuleEnabled("signal-collectors")) {
|
|
714
|
+
await flowsScope.register(await sliceRoutePlugin("getFlowRoute"));
|
|
715
|
+
await flowsScope.register(await sliceRoutePlugin("submitFlowStepRoute"));
|
|
716
|
+
await flowsScope.register(await sliceRoutePlugin("getFlowAggregateRoute"));
|
|
717
|
+
}
|
|
389
718
|
// The recommender's email-moment routes (#4841) — same anonymous,
|
|
390
|
-
// rate-limited posture as the three above.
|
|
391
|
-
|
|
392
|
-
|
|
719
|
+
// rate-limited posture as the three above. Module-conditional (commerce,
|
|
720
|
+
// D-47): the recommender leaves Pro flows and moves to commerce whole.
|
|
721
|
+
if (isModuleEnabled("commerce")) {
|
|
722
|
+
await flowsScope.register(await sliceRoutePlugin("saveRecommendationRoute"));
|
|
723
|
+
await flowsScope.register(
|
|
724
|
+
await sliceRoutePlugin("checkoutRecommendationRoute"),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
393
727
|
});
|
|
394
728
|
|
|
395
729
|
// AEO score routes (#4666) — anonymous surface, same posture as the flows
|
|
@@ -414,14 +748,18 @@ export async function build() {
|
|
|
414
748
|
// (#5111, DEF-004). Module-conditional (S1, #5088): the scheduling module
|
|
415
749
|
// owns this whole route set, so an OSS build that excludes it registers
|
|
416
750
|
// neither the routes nor their imports.
|
|
417
|
-
if (isModuleEnabled(
|
|
751
|
+
if (isModuleEnabled("scheduling")) {
|
|
418
752
|
await server.register(async (scheduleScope) => {
|
|
419
753
|
scheduleScope.register(rateLimitMiddleware, {
|
|
420
754
|
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
421
755
|
});
|
|
422
|
-
await scheduleScope.register(
|
|
423
|
-
|
|
424
|
-
|
|
756
|
+
await scheduleScope.register(
|
|
757
|
+
await sliceRoutePlugin("listAvailabilityRoute"),
|
|
758
|
+
);
|
|
759
|
+
await scheduleScope.register(await sliceRoutePlugin("getEventMetaRoute"));
|
|
760
|
+
await scheduleScope.register(
|
|
761
|
+
await sliceRoutePlugin("createBookingRoute"),
|
|
762
|
+
);
|
|
425
763
|
});
|
|
426
764
|
}
|
|
427
765
|
|
|
@@ -433,7 +771,7 @@ export async function build() {
|
|
|
433
771
|
if (shouldMountTestAuth()) {
|
|
434
772
|
await server.register(registerTestAuthRoute);
|
|
435
773
|
server.log.warn(
|
|
436
|
-
|
|
774
|
+
"test-auth route MOUNTED — only safe in dev/test environments",
|
|
437
775
|
);
|
|
438
776
|
}
|
|
439
777
|
|