create-ailk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file surface-drift.test.ts
|
|
3
|
+
* @description The self-description drift gate (#5314).
|
|
4
|
+
*
|
|
5
|
+
* Builds the REAL server, takes the route table Fastify actually registered,
|
|
6
|
+
* and diffs it against both surfaces that claim to describe it — the root
|
|
7
|
+
* index `endpoints` object and the served OpenAPI `paths` object.
|
|
8
|
+
*
|
|
9
|
+
* Both surfaces are read over `inject`, i.e. exactly what an agent would
|
|
10
|
+
* fetch, never the object the builder returned.
|
|
11
|
+
*
|
|
12
|
+
* This gate was RED before the derivation landed: 61 registered routes were
|
|
13
|
+
* missing from `paths` and 53 from `endpoints`. Two of its five concerns
|
|
14
|
+
* (AC-1 coverage on each surface) are unfailable now that both surfaces
|
|
15
|
+
* derive from the same registry they are compared against — that is the
|
|
16
|
+
* inherent consequence of AC-2's "derived" branch, and the spec's D5 says so.
|
|
17
|
+
* What stays live is the overlay join (a hand-authored entry whose route no
|
|
18
|
+
* longer registers — the concern that caught `DELETE /v1/auth/tokens`, whose
|
|
19
|
+
* real path is `/v1/auth/tokens/:id`), the anti-vacuity floor (an empty or
|
|
20
|
+
* stale registry would pass a coverage check trivially, so it is asserted
|
|
21
|
+
* against directly), and the module-agreement case's non-empty guard, which
|
|
22
|
+
* fails if module gating ever collapses so both configs register identically.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors `src/__tests__/module-exclusion.test.ts`'s external-boundary mocks
|
|
25
|
+
* (CORS, rate-limit, email) — see that file's header for why each is mocked.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
jest.mock("@fastify/cors", () => {
|
|
29
|
+
return jest.fn().mockResolvedValue(undefined);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
jest.mock("@fastify/rate-limit", () => {
|
|
33
|
+
return jest.fn().mockResolvedValue(undefined);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
jest.mock("@working-theory/email", () => ({
|
|
37
|
+
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import { build } from "../../server";
|
|
41
|
+
import { readRegisteredRoutes } from "../spec";
|
|
42
|
+
import {
|
|
43
|
+
compareSurfaces,
|
|
44
|
+
formatSurfaceDrift,
|
|
45
|
+
hasDrift,
|
|
46
|
+
openApiSurfaceKeys,
|
|
47
|
+
rootIndexSurfaceKeys,
|
|
48
|
+
routeKey,
|
|
49
|
+
toOpenApiPath,
|
|
50
|
+
type RegisteredRoute,
|
|
51
|
+
} from "../surface-drift";
|
|
52
|
+
|
|
53
|
+
/** A route every build registers, outside every module conditional. */
|
|
54
|
+
const FLOOR_ROUTE = routeKey("GET", "/v1/content/types");
|
|
55
|
+
|
|
56
|
+
interface Surfaces {
|
|
57
|
+
registered: RegisteredRoute[];
|
|
58
|
+
paths: Record<string, Record<string, unknown>>;
|
|
59
|
+
endpoints: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the server under one module configuration and read both surfaces back
|
|
64
|
+
* over the wire. `undefined` means "leave AILK_ENABLED_MODULES unset", which
|
|
65
|
+
* is now the default deployment shape (no paid module, D-44 / #5368).
|
|
66
|
+
*/
|
|
67
|
+
async function surfacesFor(enabled: string | undefined): Promise<Surfaces> {
|
|
68
|
+
const prior = process.env.AILK_ENABLED_MODULES;
|
|
69
|
+
if (enabled === undefined) {
|
|
70
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
71
|
+
} else {
|
|
72
|
+
process.env.AILK_ENABLED_MODULES = enabled;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const app = await build();
|
|
77
|
+
await app.ready();
|
|
78
|
+
|
|
79
|
+
const registered = [...readRegisteredRoutes(app)];
|
|
80
|
+
|
|
81
|
+
const rootRes = await app.inject({ method: "GET", url: "/" });
|
|
82
|
+
const specRes = await app.inject({
|
|
83
|
+
method: "GET",
|
|
84
|
+
url: "/v1/openapi.json",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await app.close();
|
|
88
|
+
|
|
89
|
+
expect(rootRes.statusCode).toBe(200);
|
|
90
|
+
expect(specRes.statusCode).toBe(200);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
registered,
|
|
94
|
+
paths: JSON.parse(specRes.body).paths,
|
|
95
|
+
endpoints: JSON.parse(rootRes.body).endpoints,
|
|
96
|
+
};
|
|
97
|
+
} finally {
|
|
98
|
+
if (prior === undefined) {
|
|
99
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
100
|
+
} else {
|
|
101
|
+
process.env.AILK_ENABLED_MODULES = prior;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The module configurations every claim below is asserted under. Unset is now
|
|
107
|
+
// the default deployment (no paid module, D-44 / #5368), the same as an
|
|
108
|
+
// explicit "" (the OSS-strip build target); a full allowlist is the
|
|
109
|
+
// every-module-on case. A slice route absent under "" must be absent from
|
|
110
|
+
// BOTH surfaces there, which is what makes its absence agreement rather than
|
|
111
|
+
// drift.
|
|
112
|
+
const ALL_PAID_MODULES =
|
|
113
|
+
"docs,multi-tenant,scheduling,content-write,signal-collectors";
|
|
114
|
+
const CONFIGS: Array<[string, string | undefined]> = [
|
|
115
|
+
["AILK_ENABLED_MODULES unset (no paid module, the default)", undefined],
|
|
116
|
+
['AILK_ENABLED_MODULES="" (no paid module)', ""],
|
|
117
|
+
[
|
|
118
|
+
`AILK_ENABLED_MODULES="${ALL_PAID_MODULES}" (every paid module on)`,
|
|
119
|
+
ALL_PAID_MODULES,
|
|
120
|
+
],
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
describe("API self-description drift (#5314)", () => {
|
|
124
|
+
describe.each(CONFIGS)("%s", (_label, enabled) => {
|
|
125
|
+
let surfaces: Surfaces;
|
|
126
|
+
|
|
127
|
+
beforeAll(async () => {
|
|
128
|
+
surfaces = await surfacesFor(enabled);
|
|
129
|
+
}, 60000);
|
|
130
|
+
|
|
131
|
+
// Anti-vacuity floor. A coverage assertion over an empty registry passes
|
|
132
|
+
// trivially, so an empty or stale registry is failed directly — the most
|
|
133
|
+
// likely way a derivation gate silently stops testing anything.
|
|
134
|
+
it("registers a non-empty route table including the floor route", () => {
|
|
135
|
+
expect(surfaces.registered.length).toBeGreaterThan(0);
|
|
136
|
+
const keys = surfaces.registered.map((r) => routeKey(r.method, r.url));
|
|
137
|
+
expect(keys).toContain(FLOOR_ROUTE);
|
|
138
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("publishes no surface entry for a route the server does not register", () => {
|
|
142
|
+
const diffs = compareSurfaces(
|
|
143
|
+
surfaces.registered,
|
|
144
|
+
surfaces.paths,
|
|
145
|
+
surfaces.endpoints,
|
|
146
|
+
);
|
|
147
|
+
const orphaned = diffs.filter((d) => d.orphanedInSurface.length > 0);
|
|
148
|
+
expect(formatSurfaceDrift(orphaned)).toBe("");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("describes every registered route on both surfaces", () => {
|
|
152
|
+
const diffs = compareSurfaces(
|
|
153
|
+
surfaces.registered,
|
|
154
|
+
surfaces.paths,
|
|
155
|
+
surfaces.endpoints,
|
|
156
|
+
);
|
|
157
|
+
expect(formatSurfaceDrift(diffs)).toBe("");
|
|
158
|
+
expect(hasDrift(diffs)).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// AC-1 names this endpoint specifically: it returns 200 and is what the
|
|
162
|
+
// consuming site depends on, and before #5314 both surfaces omitted it.
|
|
163
|
+
// #5366 (D-48) moved it to signal-collectors: the no-drift half
|
|
164
|
+
// (`missingFromSurface`) holds regardless of whether the route
|
|
165
|
+
// registers at all, but the route itself now only registers when
|
|
166
|
+
// signal-collectors is one of the enabled modules for this config.
|
|
167
|
+
it("describes GET /v1/project-listings/public on both surfaces when registered", () => {
|
|
168
|
+
const diffs = compareSurfaces(
|
|
169
|
+
surfaces.registered,
|
|
170
|
+
surfaces.paths,
|
|
171
|
+
surfaces.endpoints,
|
|
172
|
+
);
|
|
173
|
+
for (const d of diffs) {
|
|
174
|
+
expect(d.missingFromSurface).not.toContain(
|
|
175
|
+
routeKey("GET", "/v1/project-listings/public"),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const signalCollectorsEnabled = (enabled ?? "")
|
|
179
|
+
.split(",")
|
|
180
|
+
.map((s) => s.trim())
|
|
181
|
+
.includes("signal-collectors");
|
|
182
|
+
expect(
|
|
183
|
+
surfaces.registered.some(
|
|
184
|
+
(r) => r.url === "/v1/project-listings/public" && r.method === "GET",
|
|
185
|
+
),
|
|
186
|
+
).toBe(signalCollectorsEnabled);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Regression: the route table is per-instance, not module state.
|
|
191
|
+
//
|
|
192
|
+
// `vercel-handler.ts`'s `getServer()` is a check-then-set — it tests
|
|
193
|
+
// `!serverInstance`, then awaits twice before assigning — so two concurrent
|
|
194
|
+
// invocations can both call `build()`. With a module-level table, one
|
|
195
|
+
// build's reset would land mid-fill of the other and leave a truncated or
|
|
196
|
+
// doubled set, and both surfaces would silently misreport. Building two
|
|
197
|
+
// servers under DIFFERENT module configs and asserting each keeps its own
|
|
198
|
+
// table is what makes that unreachable rather than merely unlikely.
|
|
199
|
+
it("keeps each server's route table on its own instance", async () => {
|
|
200
|
+
const prior = process.env.AILK_ENABLED_MODULES;
|
|
201
|
+
try {
|
|
202
|
+
process.env.AILK_ENABLED_MODULES = ALL_PAID_MODULES;
|
|
203
|
+
const allOn = await build();
|
|
204
|
+
await allOn.ready();
|
|
205
|
+
|
|
206
|
+
process.env.AILK_ENABLED_MODULES = "";
|
|
207
|
+
const noneOn = await build();
|
|
208
|
+
await noneOn.ready();
|
|
209
|
+
|
|
210
|
+
const allKeys = readRegisteredRoutes(allOn).map((r) =>
|
|
211
|
+
routeKey(r.method, r.url),
|
|
212
|
+
);
|
|
213
|
+
const noneKeys = readRegisteredRoutes(noneOn).map((r) =>
|
|
214
|
+
routeKey(r.method, r.url),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// The second build must not have emptied, truncated or doubled the
|
|
218
|
+
// first — the exact corruption a shared table would produce.
|
|
219
|
+
expect(allKeys.length).toBeGreaterThan(noneKeys.length);
|
|
220
|
+
expect(new Set(allKeys).size).toBe(allKeys.length);
|
|
221
|
+
expect(new Set(noneKeys).size).toBe(noneKeys.length);
|
|
222
|
+
expect(allKeys).toContain(FLOOR_ROUTE);
|
|
223
|
+
expect(noneKeys).toContain(FLOOR_ROUTE);
|
|
224
|
+
|
|
225
|
+
// And each server's own OpenAPI document still matches its own table.
|
|
226
|
+
for (const app of [allOn, noneOn]) {
|
|
227
|
+
const paths = JSON.parse(
|
|
228
|
+
(await app.inject({ method: "GET", url: "/v1/openapi.json" })).body,
|
|
229
|
+
).paths;
|
|
230
|
+
const endpoints = JSON.parse(
|
|
231
|
+
(await app.inject({ method: "GET", url: "/" })).body,
|
|
232
|
+
).endpoints;
|
|
233
|
+
expect(
|
|
234
|
+
formatSurfaceDrift(
|
|
235
|
+
compareSurfaces(readRegisteredRoutes(app), paths, endpoints),
|
|
236
|
+
),
|
|
237
|
+
).toBe("");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await allOn.close();
|
|
241
|
+
await noneOn.close();
|
|
242
|
+
} finally {
|
|
243
|
+
if (prior === undefined) {
|
|
244
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
245
|
+
} else {
|
|
246
|
+
process.env.AILK_ENABLED_MODULES = prior;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}, 60000);
|
|
250
|
+
|
|
251
|
+
// AC-4. Asserted as agreement rather than as an exemption list: a disabled
|
|
252
|
+
// module's routes vanish from the registered set and from both surfaces at
|
|
253
|
+
// once, so there is nothing for the gate to forgive.
|
|
254
|
+
it("drops a disabled module's routes from the route table AND both surfaces together", async () => {
|
|
255
|
+
const allOn = await surfacesFor(ALL_PAID_MODULES);
|
|
256
|
+
const noneOn = await surfacesFor("");
|
|
257
|
+
|
|
258
|
+
const noneKeys = new Set(
|
|
259
|
+
noneOn.registered.map((r) => routeKey(r.method, r.url)),
|
|
260
|
+
);
|
|
261
|
+
const moduleOnly = allOn.registered.filter(
|
|
262
|
+
(r) => !noneKeys.has(routeKey(r.method, r.url)),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// The configurations must actually differ, or this asserts nothing.
|
|
266
|
+
expect(moduleOnly.length).toBeGreaterThan(0);
|
|
267
|
+
|
|
268
|
+
// Every module-only route is described when its module is on...
|
|
269
|
+
expect(
|
|
270
|
+
formatSurfaceDrift(
|
|
271
|
+
compareSurfaces(allOn.registered, allOn.paths, allOn.endpoints),
|
|
272
|
+
),
|
|
273
|
+
).toBe("");
|
|
274
|
+
|
|
275
|
+
// ...and named by neither surface when it is off, which is agreement.
|
|
276
|
+
//
|
|
277
|
+
// Each surface is parsed by the instrument's own reader, and each route is
|
|
278
|
+
// compared in that surface's vocabulary — OpenAPI-templated for `paths`,
|
|
279
|
+
// Fastify-shaped for `endpoints`. Comparing raw values instead would make
|
|
280
|
+
// this assertion quietly weaker than it reads: `paths` keys `{id}` where a
|
|
281
|
+
// registered route says `:id`, so every parametric route would "pass" on a
|
|
282
|
+
// vocabulary mismatch, and `endpoints` groups methods into one value
|
|
283
|
+
// (`"GET/PUT/DELETE /v1/user/data/:key"`), which never equals a
|
|
284
|
+
// single-method key. Both halves would then be vacuous for exactly the
|
|
285
|
+
// routes most likely to regress.
|
|
286
|
+
const offPathKeys = openApiSurfaceKeys(noneOn.paths);
|
|
287
|
+
const offEndpointKeys = rootIndexSurfaceKeys(noneOn.endpoints);
|
|
288
|
+
for (const { method, url } of moduleOnly) {
|
|
289
|
+
expect(offPathKeys).not.toContain(routeKey(method, toOpenApiPath(url)));
|
|
290
|
+
expect(offEndpointKeys).not.toContain(routeKey(method, url));
|
|
291
|
+
}
|
|
292
|
+
}, 60000);
|
|
293
|
+
});
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { FastifyPluginAsync } from "fastify";
|
|
5
5
|
|
|
6
|
-
import { buildOpenApiSpec } from "./spec.js";
|
|
6
|
+
import { buildOpenApiSpec, readRegisteredRoutes } from "./spec.js";
|
|
7
7
|
|
|
8
8
|
export const openapiRoutes: FastifyPluginAsync = async (fastify) => {
|
|
9
|
-
fastify.get("/v1/openapi.json", async (
|
|
10
|
-
|
|
9
|
+
fastify.get("/v1/openapi.json", async (request, reply) => {
|
|
10
|
+
// The route table comes off the instance that served this request (#5314),
|
|
11
|
+
// never from module state — two servers built concurrently in one process
|
|
12
|
+
// would otherwise share, and corrupt, one table. Absent (a bare Fastify
|
|
13
|
+
// instance with no `build()`) degrades to the hand-authored paths.
|
|
14
|
+
return reply.send(
|
|
15
|
+
buildOpenApiSpec(undefined, readRegisteredRoutes(request.server)),
|
|
16
|
+
);
|
|
11
17
|
});
|
|
12
18
|
};
|