create-ailk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.md +50 -53
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +2 -2
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +33 -5
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +459 -135
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +229 -4
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +14 -13
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +0 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/database/CHANGELOG.md +33 -0
- package/templates/database/inbox/schema.prisma +67 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1296
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/listing-promotion.ts +0 -342
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -9,53 +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
|
-
projectListingStartRoute,
|
|
53
|
-
projectListingSubmitRoute,
|
|
54
|
-
} from './routes/project-listings/index.js';
|
|
55
|
-
import { slackActionsRoute } from './routes/slack/index.js';
|
|
56
|
-
import { registerTestAuthRoute, shouldMountTestAuth } from './routes/test/auth/session.js';
|
|
57
|
-
import { waitlistSignupRoutes } from './routes/waitlist-signups/index.js';
|
|
58
|
-
import { webhooksRoutes } from './routes/webhooks/index.js';
|
|
53
|
+
registerTestAuthRoute,
|
|
54
|
+
shouldMountTestAuth,
|
|
55
|
+
} from "./routes/test/auth/session.js";
|
|
59
56
|
|
|
60
57
|
/**
|
|
61
58
|
* Paid-module SLICE entry points, keyed by the export each branch needs
|
|
@@ -70,9 +67,15 @@ import { webhooksRoutes } from './routes/webhooks/index.js';
|
|
|
70
67
|
* `./middleware/tenant.js` import was an eleventh — measured on a stripped
|
|
71
68
|
* tree, not argued.
|
|
72
69
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
|
76
79
|
* whether the branch runs; `AILK_ENABLED_MODULES` unset still means every
|
|
77
80
|
* module on, so a default build registers exactly what it registered before.
|
|
78
81
|
*
|
|
@@ -82,27 +85,200 @@ import { webhooksRoutes } from './routes/webhooks/index.js';
|
|
|
82
85
|
* with no members, so the module silently fails to load on the tree that HAS
|
|
83
86
|
* it. See the note above the imports in `packages/ui/src/sections/SectionRenderer.tsx`.
|
|
84
87
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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.
|
|
89
129
|
*/
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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";
|
|
104
148
|
|
|
105
|
-
|
|
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;
|
|
106
282
|
|
|
107
283
|
/**
|
|
108
284
|
* Load one slice route plugin. Every entry above is a `FastifyPluginAsync`
|
|
@@ -117,8 +293,8 @@ type SlicePluginName = keyof typeof SLICE_PLUGIN_SPECIFIERS;
|
|
|
117
293
|
async function sliceRoutePlugin(
|
|
118
294
|
name: SlicePluginName,
|
|
119
295
|
): Promise<FastifyPluginAsync> {
|
|
120
|
-
const specifier
|
|
121
|
-
const mod = (await
|
|
296
|
+
const { specifier, load } = SLICE_PLUGINS[name];
|
|
297
|
+
const mod = (await loadSlice(specifier, load)) as Record<
|
|
122
298
|
string,
|
|
123
299
|
FastifyPluginAsync | undefined
|
|
124
300
|
>;
|
|
@@ -135,17 +311,40 @@ async function sliceRoutePlugin(
|
|
|
135
311
|
* (apps/api/src/middleware/tenant.ts:83).
|
|
136
312
|
*/
|
|
137
313
|
async function tenantPreHandlerSlicePlugin(): Promise<FastifyPluginCallback> {
|
|
138
|
-
const specifier: string =
|
|
139
|
-
const mod = (await import(specifier)) as {
|
|
314
|
+
const specifier: string = "./middleware/tenant.js";
|
|
315
|
+
const mod = (await loadSlice(specifier, () => import(specifier))) as {
|
|
140
316
|
tenantPreHandlerPlugin?: FastifyPluginCallback;
|
|
141
317
|
};
|
|
142
318
|
const plugin = mod.tenantPreHandlerPlugin;
|
|
143
319
|
if (!plugin) {
|
|
144
|
-
throw new Error(
|
|
320
|
+
throw new Error(
|
|
321
|
+
"slice export tenantPreHandlerPlugin missing from " + specifier,
|
|
322
|
+
);
|
|
145
323
|
}
|
|
146
324
|
return plugin;
|
|
147
325
|
}
|
|
148
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
|
+
|
|
149
348
|
/**
|
|
150
349
|
* Build Fastify server instance (for serverless or standalone)
|
|
151
350
|
* @returns Configured Fastify instance ready for requests
|
|
@@ -156,9 +355,11 @@ export async function build() {
|
|
|
156
355
|
|
|
157
356
|
// Runtime sandbox-mode guard: reject live-mode Stripe keys before any
|
|
158
357
|
// webhook or checkout call can exercise them. Idempotent — safe even if
|
|
159
|
-
// assertSandboxKey is called again inside a route handler.
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
363
|
}
|
|
163
364
|
|
|
164
365
|
// trustProxy: true (#4730) — the Vercel serverless handler
|
|
@@ -188,6 +389,32 @@ export async function build() {
|
|
|
188
389
|
trustProxy: true,
|
|
189
390
|
});
|
|
190
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
|
+
|
|
191
418
|
// Register error serializer — maps internal exceptions to generic strings
|
|
192
419
|
// so connection strings, hostnames, stack traces, and SDK error details
|
|
193
420
|
// never reach the client. Full detail still goes to pino at error level.
|
|
@@ -199,9 +426,32 @@ export async function build() {
|
|
|
199
426
|
// `credentials: true` would let any website read credentialed responses
|
|
200
427
|
// (security review M1). `false` disables cross-origin access entirely;
|
|
201
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.
|
|
202
451
|
await server.register(cors, {
|
|
203
|
-
origin: config.FRONTEND_URL || config.NODE_ENV !==
|
|
452
|
+
origin: config.FRONTEND_URL || config.NODE_ENV !== "production",
|
|
204
453
|
credentials: true,
|
|
454
|
+
methods: ["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
205
455
|
});
|
|
206
456
|
|
|
207
457
|
// Register rate limiting — applied globally with the unauth ceiling. The
|
|
@@ -214,32 +464,35 @@ export async function build() {
|
|
|
214
464
|
});
|
|
215
465
|
|
|
216
466
|
// Health check
|
|
217
|
-
server.get(
|
|
218
|
-
status:
|
|
467
|
+
server.get("/health", async () => ({
|
|
468
|
+
status: "healthy",
|
|
219
469
|
timestamp: new Date().toISOString(),
|
|
220
470
|
}));
|
|
221
471
|
|
|
222
|
-
// Root endpoint
|
|
223
|
-
|
|
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();
|
|
224
477
|
return {
|
|
225
|
-
name:
|
|
226
|
-
version:
|
|
227
|
-
description:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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),
|
|
243
496
|
};
|
|
244
497
|
});
|
|
245
498
|
|
|
@@ -258,20 +511,30 @@ export async function build() {
|
|
|
258
511
|
// apply to them; each enforces membership in the service layer.
|
|
259
512
|
// Module-conditional (S1, #5088): without the multi-tenant module there
|
|
260
513
|
// is no workspace concept to switch or inspect.
|
|
261
|
-
if (isModuleEnabled(
|
|
262
|
-
await authScope.register(
|
|
514
|
+
if (isModuleEnabled("multi-tenant")) {
|
|
515
|
+
await authScope.register(
|
|
516
|
+
await sliceRoutePlugin("workspaceContextRoutes"),
|
|
517
|
+
);
|
|
263
518
|
}
|
|
264
519
|
|
|
265
520
|
// Respondent signup read (#4731) — auth only, NOT tenant-guarded: a
|
|
266
521
|
// respondent has no workspace membership and the tenant guard is
|
|
267
522
|
// default-deny. Identity is request.userId; the service re-checks
|
|
268
|
-
// User.emailVerified before any Lead read.
|
|
269
|
-
|
|
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
|
+
}
|
|
270
528
|
|
|
271
529
|
// Respondent checkouts read (#4984) — the same posture as the signup
|
|
272
530
|
// read above: auth only, NOT tenant-guarded. Identity is request.userId;
|
|
273
531
|
// the only join is the lead-promotion link (never an email match).
|
|
274
|
-
|
|
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
|
+
}
|
|
275
538
|
|
|
276
539
|
// The founder's OWN listings read (#5295) — the same posture again: auth
|
|
277
540
|
// only, NOT tenant-guarded, because a founder is not a workspace member
|
|
@@ -284,7 +547,12 @@ export async function build() {
|
|
|
284
547
|
// build without the module; the static `/me` path takes router precedence
|
|
285
548
|
// over the tenant scope's parametric `/v1/project-listings/:id`, exactly as
|
|
286
549
|
// the already-shipped static `/v1/project-listings/public` does.
|
|
287
|
-
|
|
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
|
+
}
|
|
288
556
|
|
|
289
557
|
// Tenant-guarded domain routes (multi-tenant module) — the tenant
|
|
290
558
|
// preHandler (default-deny guard, see middleware/tenant.ts) runs after
|
|
@@ -293,24 +561,27 @@ export async function build() {
|
|
|
293
561
|
// no tenant concept at all — the whole scope is module-conditional
|
|
294
562
|
// (S1, #5088), so an OSS build that excludes multi-tenant registers
|
|
295
563
|
// none of it.
|
|
296
|
-
if (isModuleEnabled(
|
|
564
|
+
if (isModuleEnabled("multi-tenant")) {
|
|
297
565
|
await authScope.register(async (tenantScope) => {
|
|
298
566
|
await tenantScope.register(await tenantPreHandlerSlicePlugin());
|
|
299
|
-
await tenantScope.register(
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
);
|
|
302
574
|
// Org-billing routes (#3701) — org-scoped read + Stripe customer-portal.
|
|
303
|
-
await tenantScope.register(await sliceRoutePlugin(
|
|
575
|
+
await tenantScope.register(await sliceRoutePlugin("billingRoutes"));
|
|
304
576
|
// Waitlist operator-dashboard read API (#4665 D7, B0) — tenant-scoped
|
|
305
577
|
// list/comparison/signups reads over A1's waitlist-experiment rows.
|
|
306
578
|
await tenantScope.register(
|
|
307
|
-
await sliceRoutePlugin(
|
|
579
|
+
await sliceRoutePlugin("waitlistExperimentRoutes"),
|
|
308
580
|
);
|
|
309
581
|
// Project-listings dashboard (#4998) — tenant-scoped list/get/patch
|
|
310
582
|
// over the founder's-application-row lifecycle.
|
|
311
|
-
const { projectListingTenantRoutes } =
|
|
312
|
-
|
|
313
|
-
);
|
|
583
|
+
const { projectListingTenantRoutes } =
|
|
584
|
+
await import("./routes/project-listings/index.js");
|
|
314
585
|
await tenantScope.register(projectListingTenantRoutes);
|
|
315
586
|
});
|
|
316
587
|
}
|
|
@@ -325,22 +596,28 @@ export async function build() {
|
|
|
325
596
|
// module-conditional at REGISTRATION too (S1, #5088) so an OSS build that
|
|
326
597
|
// excludes the content-write slice never wires the handler in at all. Not
|
|
327
598
|
// a MODULE_SLICES entry — see config/modules.ts.
|
|
328
|
-
if (isModuleEnabled(
|
|
329
|
-
await server.register(await sliceRoutePlugin(
|
|
330
|
-
await server.register(await sliceRoutePlugin(
|
|
331
|
-
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"));
|
|
332
607
|
}
|
|
333
|
-
await server.register(checkoutRoutes);
|
|
334
608
|
// Deliverable download — a capability-token URL (#3961 D5); the token
|
|
335
|
-
// itself is the sole gate, so no auth preHandler applies.
|
|
336
|
-
|
|
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
|
+
}
|
|
337
614
|
|
|
338
615
|
// Webhooks routes — raw-body parser scoped to this sub-router only so the
|
|
339
616
|
// global JSON parser on all other routes is unaffected.
|
|
340
617
|
await server.register(async (webhooksScope) => {
|
|
341
618
|
webhooksScope.addContentTypeParser(
|
|
342
|
-
|
|
343
|
-
{ parseAs:
|
|
619
|
+
"application/json",
|
|
620
|
+
{ parseAs: "buffer" },
|
|
344
621
|
(_req, body, done) => {
|
|
345
622
|
done(null, body);
|
|
346
623
|
},
|
|
@@ -349,14 +626,24 @@ export async function build() {
|
|
|
349
626
|
// field (#4999) — raw buffer here too, so the signature is verified over
|
|
350
627
|
// the EXACT bytes Slack signed, never a re-serialized parse.
|
|
351
628
|
webhooksScope.addContentTypeParser(
|
|
352
|
-
|
|
353
|
-
{ parseAs:
|
|
629
|
+
"application/x-www-form-urlencoded",
|
|
630
|
+
{ parseAs: "buffer" },
|
|
354
631
|
(_req, body, done) => {
|
|
355
632
|
done(null, body);
|
|
356
633
|
},
|
|
357
634
|
);
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
}
|
|
360
647
|
});
|
|
361
648
|
|
|
362
649
|
// Leads routes — rate-limit middleware scoped to this sub-router. Own
|
|
@@ -373,20 +660,43 @@ export async function build() {
|
|
|
373
660
|
// Project-listings anonymous surface (#4998, reshaped by #5036) — the
|
|
374
661
|
// three-operation draft lifecycle plus the public page-render read, all
|
|
375
662
|
// on the same rate-limited, identity-blind posture as the leads routes
|
|
376
|
-
// above.
|
|
663
|
+
// above. Module-conditional (signal-collectors, D-48): the listing
|
|
664
|
+
// instrument's application-lifecycle half.
|
|
377
665
|
//
|
|
378
666
|
// Do NOT read that rate limit as a bound on anonymous draft creation. It
|
|
379
667
|
// keys on `req.ip`, and `trustProxy` is on (see the note at the top of this
|
|
380
668
|
// file), so a caller reaching the API directly can vary
|
|
381
669
|
// `X-Forwarded-For` and get a fresh budget per request. That is a
|
|
382
670
|
// pre-existing property of the limiter, not of these routes; what these
|
|
383
|
-
// routes do is keep the cost of an unbounded call small —
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}
|
|
390
700
|
});
|
|
391
701
|
|
|
392
702
|
// Flows routes — anonymous surface; rate-limit middleware scoped to this
|
|
@@ -397,13 +707,23 @@ export async function build() {
|
|
|
397
707
|
// scope #4730's bump was actually sized for; unchanged by #5111.
|
|
398
708
|
await server.register(async (flowsScope) => {
|
|
399
709
|
flowsScope.register(rateLimitMiddleware);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
+
}
|
|
403
718
|
// The recommender's email-moment routes (#4841) — same anonymous,
|
|
404
|
-
// rate-limited posture as the three above.
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
}
|
|
407
727
|
});
|
|
408
728
|
|
|
409
729
|
// AEO score routes (#4666) — anonymous surface, same posture as the flows
|
|
@@ -428,14 +748,18 @@ export async function build() {
|
|
|
428
748
|
// (#5111, DEF-004). Module-conditional (S1, #5088): the scheduling module
|
|
429
749
|
// owns this whole route set, so an OSS build that excludes it registers
|
|
430
750
|
// neither the routes nor their imports.
|
|
431
|
-
if (isModuleEnabled(
|
|
751
|
+
if (isModuleEnabled("scheduling")) {
|
|
432
752
|
await server.register(async (scheduleScope) => {
|
|
433
753
|
scheduleScope.register(rateLimitMiddleware, {
|
|
434
754
|
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
435
755
|
});
|
|
436
|
-
await scheduleScope.register(
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
);
|
|
439
763
|
});
|
|
440
764
|
}
|
|
441
765
|
|
|
@@ -447,7 +771,7 @@ export async function build() {
|
|
|
447
771
|
if (shouldMountTestAuth()) {
|
|
448
772
|
await server.register(registerTestAuthRoute);
|
|
449
773
|
server.log.warn(
|
|
450
|
-
|
|
774
|
+
"test-auth route MOUNTED — only safe in dev/test environments",
|
|
451
775
|
);
|
|
452
776
|
}
|
|
453
777
|
|