create-ailk 0.2.0 → 0.4.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/cli.js +19 -9
- 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 +51 -3
- package/dist/lib/extract-module-bundle.js +327 -28
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/fetch-template.d.ts +20 -0
- package/dist/lib/fetch-template.js +129 -0
- package/dist/lib/module-license-gate.d.ts +10 -0
- package/dist/lib/module-license-gate.js +13 -4
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +197 -23
- package/dist/module-architecture.js +820 -25
- package/dist/parse-args.d.ts +9 -2
- package/dist/parse-args.js +46 -1
- package/dist/programmatic.d.ts +28 -2
- package/dist/programmatic.js +115 -8
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +77 -5
- package/dist/sync-routes.js +31 -1
- package/dist/template-manifest.d.ts +92 -0
- package/dist/template-manifest.js +168 -0
- package/dist/template-names.d.ts +41 -0
- package/dist/template-names.js +44 -0
- package/package.json +3 -2
- package/site-templates/marketing-site-free/ailk-template.json +8 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
- package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
- package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
- package/site-templates/marketing-site-free/provenance.json +4 -0
- 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/__tests__/workspace-module-registration.test.ts +166 -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 +504 -137
- 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/[locale]/pricing/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +19 -13
- package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
- package/templates/apps/web/lib/server-api.ts +28 -12
- package/templates/apps/web/messages/en.json +58 -0
- 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 +47 -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
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
33
33
|
import Fastify from 'fastify';
|
|
34
34
|
|
|
35
|
+
jest.mock('@working-theory/observability', () => ({
|
|
36
|
+
logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn() },
|
|
37
|
+
}));
|
|
38
|
+
|
|
35
39
|
// ── Typed request/response stubs ────────────────────────────────────────────
|
|
36
40
|
//
|
|
37
41
|
// Minimal stubs that satisfy only the VercelRequest/VercelResponse surface
|
|
@@ -209,3 +213,158 @@ describe('vercel-handler — response side: binary round-trip (#5168)', () => {
|
|
|
209
213
|
expect(Buffer.compare(res._sent as Buffer, rawPayload)).toBe(0);
|
|
210
214
|
});
|
|
211
215
|
});
|
|
216
|
+
|
|
217
|
+
// ── #5332: one build() per process ──────────────────────────────────────────
|
|
218
|
+
//
|
|
219
|
+
// getServer() used to cache the RESOLVED instance and test it before two
|
|
220
|
+
// awaits, so two invocations overlapping a cold start on one warm container
|
|
221
|
+
// both saw null and both built a complete Fastify server. The fix caches the
|
|
222
|
+
// in-flight promise instead. These tests drive getServer() through the default
|
|
223
|
+
// handler — the handler's exported shape is byte-synced into consuming sites,
|
|
224
|
+
// so getServer() stays module-private rather than being exported for the test.
|
|
225
|
+
|
|
226
|
+
const COLD_START_REQUEST: MockVercelRequest = {
|
|
227
|
+
method: 'GET',
|
|
228
|
+
url: '/health',
|
|
229
|
+
headers: {},
|
|
230
|
+
body: undefined,
|
|
231
|
+
query: {},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
function sentMessage(_res: MockRes): string {
|
|
235
|
+
// Re-required, not imported: jest.resetModules() gives each test a fresh
|
|
236
|
+
// mock instance, and the handler under test holds that fresh one.
|
|
237
|
+
const { logger } = require('@working-theory/observability') as {
|
|
238
|
+
logger: { error: jest.Mock };
|
|
239
|
+
};
|
|
240
|
+
const calls = logger.error.mock.calls.filter(
|
|
241
|
+
([, msg]) => msg === '[Vercel Handler] Error handling request',
|
|
242
|
+
);
|
|
243
|
+
const last = calls[calls.length - 1];
|
|
244
|
+
return last ? String((last[0] as { err?: Error }).err?.message ?? '') : '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
describe('vercel-handler — one build per process (#5332)', () => {
|
|
248
|
+
beforeEach(() => {
|
|
249
|
+
jest.resetModules();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
afterEach(() => {
|
|
253
|
+
jest.useRealTimers();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('builds one server for two invocations that overlap a cold start', async () => {
|
|
257
|
+
// One shared deferred for both calls, so whichever build() calls happen
|
|
258
|
+
// all settle together — the assertion is on the CALL COUNT, and a test
|
|
259
|
+
// that hung when the second call won the cache would obscure it.
|
|
260
|
+
let resolveServer!: (instance: unknown) => void;
|
|
261
|
+
const serverReady = new Promise<unknown>((resolve) => {
|
|
262
|
+
resolveServer = resolve;
|
|
263
|
+
});
|
|
264
|
+
const inject = jest.fn().mockResolvedValue({
|
|
265
|
+
statusCode: 200,
|
|
266
|
+
headers: {},
|
|
267
|
+
body: 'ok',
|
|
268
|
+
rawPayload: Buffer.from('ok'),
|
|
269
|
+
});
|
|
270
|
+
const build = jest.fn(() => serverReady);
|
|
271
|
+
jest.doMock('../server', () => ({ build }));
|
|
272
|
+
const handler = loadHandler();
|
|
273
|
+
|
|
274
|
+
const resA = makeRes();
|
|
275
|
+
const resB = makeRes();
|
|
276
|
+
// Both started before either resolves: neither has a server yet.
|
|
277
|
+
const a = handler(asRequest(COLD_START_REQUEST), asResponse(resA));
|
|
278
|
+
const b = handler(asRequest(COLD_START_REQUEST), asResponse(resB));
|
|
279
|
+
|
|
280
|
+
// Drain the microtask queue so both calls have reached getServer() and
|
|
281
|
+
// awaited build() before the build is allowed to finish.
|
|
282
|
+
for (let i = 0; i < 10; i += 1) {
|
|
283
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
resolveServer({ inject });
|
|
287
|
+
await Promise.all([a, b]);
|
|
288
|
+
|
|
289
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
290
|
+
expect(resA._status).toBe(200);
|
|
291
|
+
expect(resB._status).toBe(200);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('retries the build on the next invocation after a failed cold start', async () => {
|
|
295
|
+
const inject = jest.fn().mockResolvedValue({
|
|
296
|
+
statusCode: 200,
|
|
297
|
+
headers: {},
|
|
298
|
+
body: 'ok',
|
|
299
|
+
rawPayload: Buffer.from('ok'),
|
|
300
|
+
});
|
|
301
|
+
const build = jest
|
|
302
|
+
.fn()
|
|
303
|
+
.mockRejectedValueOnce(new Error('prisma connect refused'))
|
|
304
|
+
.mockResolvedValue({ inject });
|
|
305
|
+
jest.doMock('../server', () => ({ build }));
|
|
306
|
+
const handler = loadHandler();
|
|
307
|
+
|
|
308
|
+
const failed = makeRes();
|
|
309
|
+
await handler(asRequest(COLD_START_REQUEST), asResponse(failed));
|
|
310
|
+
expect(failed._status).toBe(500);
|
|
311
|
+
expect(sentMessage(failed)).toBe('Server build failed: prisma connect refused');
|
|
312
|
+
|
|
313
|
+
// A rejected promise left in the cache would serve this request the same
|
|
314
|
+
// failure forever without calling build() again.
|
|
315
|
+
const retried = makeRes();
|
|
316
|
+
await handler(asRequest(COLD_START_REQUEST), asResponse(retried));
|
|
317
|
+
|
|
318
|
+
expect(build).toHaveBeenCalledTimes(2);
|
|
319
|
+
expect(retried._status).toBe(200);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('does not leak an absolute filesystem path in the 500 body (#5351)', async () => {
|
|
323
|
+
const leakyPath = '/home/deploy/apps/api/dist/list.js';
|
|
324
|
+
const build = jest.fn().mockRejectedValue(
|
|
325
|
+
new Error(`ERR_MODULE_NOT_FOUND file://${leakyPath}`)
|
|
326
|
+
);
|
|
327
|
+
jest.doMock('../server', () => ({ build }));
|
|
328
|
+
const handler = loadHandler();
|
|
329
|
+
|
|
330
|
+
const res = makeRes();
|
|
331
|
+
await handler(asRequest(COLD_START_REQUEST), asResponse(res));
|
|
332
|
+
|
|
333
|
+
expect(res._status).toBe(500);
|
|
334
|
+
expect(JSON.stringify(res._sent)).not.toContain(leakyPath);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('still fires the build timeout with its original message', async () => {
|
|
338
|
+
jest.useFakeTimers();
|
|
339
|
+
// A module object carrying its own `then` is assimilated when the
|
|
340
|
+
// handler's `await import('./server.js')` resolves with it, so the
|
|
341
|
+
// dynamic import never settles — the production condition the 10s build
|
|
342
|
+
// timeout guards. `__esModule` keeps ts-jest's __importStar from copying
|
|
343
|
+
// the object and dropping `then`.
|
|
344
|
+
jest.doMock('../server', () => ({ __esModule: true, then: () => {} }));
|
|
345
|
+
const handler = loadHandler();
|
|
346
|
+
|
|
347
|
+
const res = makeRes();
|
|
348
|
+
const pending = handler(asRequest(COLD_START_REQUEST), asResponse(res));
|
|
349
|
+
await jest.advanceTimersByTimeAsync(10_000);
|
|
350
|
+
await pending;
|
|
351
|
+
|
|
352
|
+
expect(res._status).toBe(500);
|
|
353
|
+
expect(sentMessage(res)).toBe('Server build failed: Server build timeout after 10s');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('still fires the init timeout with its original message', async () => {
|
|
357
|
+
jest.useFakeTimers();
|
|
358
|
+
const build = jest.fn(() => new Promise(() => {}));
|
|
359
|
+
jest.doMock('../server', () => ({ build }));
|
|
360
|
+
const handler = loadHandler();
|
|
361
|
+
|
|
362
|
+
const res = makeRes();
|
|
363
|
+
const pending = handler(asRequest(COLD_START_REQUEST), asResponse(res));
|
|
364
|
+
await jest.advanceTimersByTimeAsync(10_000);
|
|
365
|
+
await pending;
|
|
366
|
+
|
|
367
|
+
expect(res._status).toBe(500);
|
|
368
|
+
expect(sentMessage(res)).toBe('Server build failed: Server initialization timeout after 10s');
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file workspace-module-registration.test.ts
|
|
3
|
+
* @description The `workspace || multi-tenant` registration matrix (#5371 D2).
|
|
4
|
+
*
|
|
5
|
+
* `workspace` cannot be folded into the sibling `module-exclusion.test.ts`:
|
|
6
|
+
* that suite's shape is "excluding module X 404s every route X owns", and the
|
|
7
|
+
* three routes here register under EITHER module, so excluding `workspace`
|
|
8
|
+
* while `multi-tenant` is on leaves them registered by design. That is the
|
|
9
|
+
* whole point of D2's gate, so it gets its own matrix.
|
|
10
|
+
*
|
|
11
|
+
* The three cases §5.1 names:
|
|
12
|
+
*
|
|
13
|
+
* 1. workspace only — the three workspace-settings routes resolve for a
|
|
14
|
+
* single-tenant session; billing / workspace-session
|
|
15
|
+
* / org routes stay unregistered.
|
|
16
|
+
* 2. multi-tenant only — everything resolves exactly as it did before this
|
|
17
|
+
* change (the regression floor).
|
|
18
|
+
* 3. both enabled — each of workspaceConfigRoutes /
|
|
19
|
+
* workspaceGeneralRoutes / workspaceMembersRoutes
|
|
20
|
+
* registers exactly ONCE; Fastify throws on a
|
|
21
|
+
* duplicate route at build() time, so a successful
|
|
22
|
+
* build IS the assertion, backed by an explicit
|
|
23
|
+
* count over the router tree.
|
|
24
|
+
*
|
|
25
|
+
* "Resolves" is measured as "not Fastify's own 404". An unauthenticated
|
|
26
|
+
* request to a registered tenant route answers 401, never 404, so the two
|
|
27
|
+
* states are unambiguous — the same signal `module-exclusion.test.ts` uses.
|
|
28
|
+
*
|
|
29
|
+
* Mirrors `src/__tests__/server.test.ts`'s external-boundary mocks (CORS,
|
|
30
|
+
* rate-limit, email) — see that file's header for why each is mocked.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
jest.mock('@fastify/cors', () => {
|
|
34
|
+
return jest.fn().mockResolvedValue(undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
jest.mock('@fastify/rate-limit', () => {
|
|
38
|
+
return jest.fn().mockResolvedValue(undefined);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
jest.mock('@working-theory/email', () => ({
|
|
42
|
+
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
import { build } from '../server';
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- server is typed via Fastify generics; explicit any avoids pulling in the full FastifyInstance type here
|
|
48
|
+
type App = any;
|
|
49
|
+
|
|
50
|
+
const WS = 'wksp_00000000-0000-7000-8000-0000000000aa';
|
|
51
|
+
|
|
52
|
+
/** The routes the widened gate must serve under EITHER module. */
|
|
53
|
+
const SHARED_TENANT_ROUTES = [
|
|
54
|
+
{ method: 'GET' as const, url: `/v1/workspaces/${WS}/theme` },
|
|
55
|
+
{ method: 'GET' as const, url: `/v1/workspaces/${WS}/identity` },
|
|
56
|
+
{ method: 'GET' as const, url: `/v1/workspaces/${WS}/seo` },
|
|
57
|
+
{ method: 'GET' as const, url: `/v1/workspaces/${WS}/general` },
|
|
58
|
+
{ method: 'GET' as const, url: `/v1/workspaces/${WS}/members` },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** Routes that must stay multi-tenant-only — the gate's other half. */
|
|
62
|
+
const MULTI_TENANT_ONLY_ROUTES = [
|
|
63
|
+
{ method: 'GET' as const, url: '/v1/workspaces' },
|
|
64
|
+
{ method: 'GET' as const, url: '/v1/billing' },
|
|
65
|
+
{ method: 'GET' as const, url: '/v1/waitlist-experiments' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
async function withEnabledModules<T>(
|
|
69
|
+
enabled: string,
|
|
70
|
+
fn: () => Promise<T>,
|
|
71
|
+
): Promise<T> {
|
|
72
|
+
const prior = process.env.AILK_ENABLED_MODULES;
|
|
73
|
+
process.env.AILK_ENABLED_MODULES = enabled;
|
|
74
|
+
try {
|
|
75
|
+
return await fn();
|
|
76
|
+
} finally {
|
|
77
|
+
if (prior === undefined) {
|
|
78
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
79
|
+
} else {
|
|
80
|
+
process.env.AILK_ENABLED_MODULES = prior;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every (method, path) pair Fastify's router holds, one per line. */
|
|
86
|
+
function routeLines(app: App): string[] {
|
|
87
|
+
return (app.printRoutes({ commonPrefix: false }) as string)
|
|
88
|
+
.split('\n')
|
|
89
|
+
.map((line: string) => line.trim())
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe('workspace-settings registration matrix (#5371 D2)', () => {
|
|
94
|
+
let app: App;
|
|
95
|
+
|
|
96
|
+
afterEach(async () => {
|
|
97
|
+
if (app) {
|
|
98
|
+
await app.close();
|
|
99
|
+
app = undefined;
|
|
100
|
+
}
|
|
101
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("with 'workspace' enabled and 'multi-tenant' disabled, the five settings routes resolve", async () => {
|
|
105
|
+
app = await withEnabledModules('workspace', () => build());
|
|
106
|
+
|
|
107
|
+
for (const { method, url } of SHARED_TENANT_ROUTES) {
|
|
108
|
+
const res = await app.inject({ method, url });
|
|
109
|
+
expect({ url, status: res.statusCode }).toEqual({ url, status: 401 });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("with 'workspace' alone, billing / workspace-session / org routes stay unregistered", async () => {
|
|
114
|
+
app = await withEnabledModules('workspace', () => build());
|
|
115
|
+
|
|
116
|
+
for (const { method, url } of MULTI_TENANT_ONLY_ROUTES) {
|
|
117
|
+
const res = await app.inject({ method, url });
|
|
118
|
+
expect({ url, status: res.statusCode }).toEqual({ url, status: 404 });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("with 'multi-tenant' enabled and 'workspace' disabled, every route resolves as before", async () => {
|
|
123
|
+
app = await withEnabledModules('multi-tenant', () => build());
|
|
124
|
+
|
|
125
|
+
for (const { method, url } of [
|
|
126
|
+
...SHARED_TENANT_ROUTES,
|
|
127
|
+
...MULTI_TENANT_ONLY_ROUTES,
|
|
128
|
+
]) {
|
|
129
|
+
const res = await app.inject({ method, url });
|
|
130
|
+
expect({ url, status: res.statusCode }).not.toEqual({ url, status: 404 });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('with BOTH enabled, each shared route registers exactly once', async () => {
|
|
135
|
+
// build() itself is the first assertion: Fastify throws
|
|
136
|
+
// FST_ERR_DUPLICATED_ROUTE at registration time, so a double-registration
|
|
137
|
+
// never reaches the router tree to be counted.
|
|
138
|
+
app = await withEnabledModules('workspace,multi-tenant', () => build());
|
|
139
|
+
|
|
140
|
+
const lines = routeLines(app);
|
|
141
|
+
for (const segment of ['theme', 'identity', 'seo', 'general', 'members']) {
|
|
142
|
+
const matches = lines.filter((line) => line.includes(segment));
|
|
143
|
+
expect({ segment, count: matches.length }).toEqual({
|
|
144
|
+
segment,
|
|
145
|
+
count: 1,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const { method, url } of [
|
|
150
|
+
...SHARED_TENANT_ROUTES,
|
|
151
|
+
...MULTI_TENANT_ONLY_ROUTES,
|
|
152
|
+
]) {
|
|
153
|
+
const res = await app.inject({ method, url });
|
|
154
|
+
expect({ url, status: res.statusCode }).not.toEqual({ url, status: 404 });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('with neither enabled, none of the five settings routes registers', async () => {
|
|
159
|
+
app = await withEnabledModules('', () => build());
|
|
160
|
+
|
|
161
|
+
for (const { method, url } of SHARED_TENANT_ROUTES) {
|
|
162
|
+
const res = await app.inject({ method, url });
|
|
163
|
+
expect({ url, status: res.statusCode }).toEqual({ url, status: 404 });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `isModuleEnabled()` default-flip regression test (D-44, #5368).
|
|
3
|
+
*
|
|
4
|
+
* Proves the mechanical claim #5368 exists to make true: an UNSET
|
|
5
|
+
* `AILK_ENABLED_MODULES` now gates every paid module OFF, the same as an
|
|
6
|
+
* explicit empty string — only an explicit allowlist turns modules on, and
|
|
7
|
+
* turns on exactly the modules it names.
|
|
8
|
+
*
|
|
9
|
+
* Reverting `enabledModules()`'s unset branch back to the `undefined`
|
|
10
|
+
* sentinel (S1, #5088's original shape) makes the "unset" cases below fail
|
|
11
|
+
* RED — see #5368 verification notes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isModuleEnabled } from '../modules';
|
|
15
|
+
|
|
16
|
+
const RECOGNIZED_MODULES = [
|
|
17
|
+
'docs',
|
|
18
|
+
'multi-tenant',
|
|
19
|
+
'scheduling',
|
|
20
|
+
'content-write',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
describe('isModuleEnabled', () => {
|
|
24
|
+
const originalEnv = process.env.AILK_ENABLED_MODULES;
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (originalEnv === undefined) {
|
|
28
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
29
|
+
} else {
|
|
30
|
+
process.env.AILK_ENABLED_MODULES = originalEnv;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('AILK_ENABLED_MODULES unset', () => {
|
|
35
|
+
it.each(RECOGNIZED_MODULES)('returns false for %s', (moduleName) => {
|
|
36
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
37
|
+
expect(isModuleEnabled(moduleName)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('AILK_ENABLED_MODULES=""', () => {
|
|
42
|
+
it.each(RECOGNIZED_MODULES)('returns false for %s', (moduleName) => {
|
|
43
|
+
process.env.AILK_ENABLED_MODULES = '';
|
|
44
|
+
expect(isModuleEnabled(moduleName)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('AILK_ENABLED_MODULES as an explicit allowlist', () => {
|
|
49
|
+
it.each(RECOGNIZED_MODULES)(
|
|
50
|
+
'enables exactly the listed modules — %s on when named',
|
|
51
|
+
(moduleName) => {
|
|
52
|
+
process.env.AILK_ENABLED_MODULES = moduleName;
|
|
53
|
+
expect(isModuleEnabled(moduleName)).toBe(true);
|
|
54
|
+
for (const other of RECOGNIZED_MODULES) {
|
|
55
|
+
if (other === moduleName) continue;
|
|
56
|
+
expect(isModuleEnabled(other)).toBe(false);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
it('enables every module named in a comma list, and no others', () => {
|
|
62
|
+
process.env.AILK_ENABLED_MODULES = 'multi-tenant,scheduling';
|
|
63
|
+
expect(isModuleEnabled('multi-tenant')).toBe(true);
|
|
64
|
+
expect(isModuleEnabled('scheduling')).toBe(true);
|
|
65
|
+
expect(isModuleEnabled('docs')).toBe(false);
|
|
66
|
+
expect(isModuleEnabled('content-write')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('trims whitespace and drops empty entries, same as before the flip', () => {
|
|
70
|
+
process.env.AILK_ENABLED_MODULES = ' multi-tenant , , scheduling ';
|
|
71
|
+
expect(isModuleEnabled('multi-tenant')).toBe(true);
|
|
72
|
+
expect(isModuleEnabled('scheduling')).toBe(true);
|
|
73
|
+
expect(isModuleEnabled('docs')).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -39,6 +39,21 @@ export interface Config {
|
|
|
39
39
|
// row's issueUrl stays null for a later retry via the PATCH route.
|
|
40
40
|
AILK_GITHUB_REPO?: string;
|
|
41
41
|
AILK_GITHUB_TOKEN?: string;
|
|
42
|
+
|
|
43
|
+
// The public origin this API is reachable at, e.g. https://api.example.com.
|
|
44
|
+
// Already consumed by the deliverable download link (#3961 D5); the OpenAPI
|
|
45
|
+
// `servers` block reads it too (#5306), since the spec is byte-synced into
|
|
46
|
+
// every consuming site and cannot name a host as a literal.
|
|
47
|
+
//
|
|
48
|
+
// This is the API's OWN origin, distinct from FRONTEND_URL (the site the
|
|
49
|
+
// browser loads) and from `_site.mdx`'s `baseUrl` (the site's public web
|
|
50
|
+
// origin). A deployment typically serves the site from example.com and this
|
|
51
|
+
// API from api.example.com, so neither of the other two can stand in.
|
|
52
|
+
//
|
|
53
|
+
// Optional: unset degrades the `servers` block to a document-relative
|
|
54
|
+
// origin, which stays correct for any agent that fetched the spec from this
|
|
55
|
+
// API. See resolveServers() in src/openapi/spec.ts.
|
|
56
|
+
AILK_PUBLIC_API_URL?: string;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
function validateConfig(): Config {
|
|
@@ -85,6 +100,10 @@ function validateConfig(): Config {
|
|
|
85
100
|
...(process.env.AILK_GITHUB_TOKEN !== undefined && {
|
|
86
101
|
AILK_GITHUB_TOKEN: process.env.AILK_GITHUB_TOKEN,
|
|
87
102
|
}),
|
|
103
|
+
|
|
104
|
+
...(process.env.AILK_PUBLIC_API_URL !== undefined && {
|
|
105
|
+
AILK_PUBLIC_API_URL: process.env.AILK_PUBLIC_API_URL,
|
|
106
|
+
}),
|
|
88
107
|
};
|
|
89
108
|
}
|
|
90
109
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Paid-module presence — the registration-grain signal used by `server.ts`
|
|
3
3
|
* to decide whether a paid module's routes (and their imports) are wired
|
|
4
|
-
* into this build (S1, #5088).
|
|
4
|
+
* into this build (S1, #5088; defaulted off by D-44, #5368).
|
|
5
5
|
*
|
|
6
6
|
* `AILK_ENABLED_MODULES` is a comma-separated allowlist (e.g.
|
|
7
|
-
* `"multi-tenant,scheduling"`). Unset means
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* shape elsewhere in this file's sibling config.
|
|
7
|
+
* `"multi-tenant,scheduling"`). Unset means **no paid modules** — installed
|
|
8
|
+
* modules enable themselves explicitly; an explicit empty string (`""`)
|
|
9
|
+
* means the same (the OSS-strip build target); a list enables exactly the
|
|
10
|
+
* modules it names.
|
|
12
11
|
*
|
|
13
|
-
* Recognized module keys: `docs`, `multi-tenant`, `scheduling
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
12
|
+
* Recognized module keys: `docs`, `multi-tenant`, `workspace`, `scheduling`,
|
|
13
|
+
* `signal-collectors`, `commerce` (the `ModuleName` vocabulary
|
|
14
|
+
* `packages/create-ailk/src/module-architecture.ts` already models via
|
|
15
|
+
* `MODULE_SLICES`) plus `content-write` — the Pro
|
|
16
16
|
* content-write surface (`create`/`update`/`delete` under
|
|
17
17
|
* `apps/api/src/routes/content/`). `content-write` is intentionally NOT a
|
|
18
18
|
* `MODULE_SLICES` entry: it is not (yet) a `create-ailk --modules`
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
* create-ailk's module types (outside its package allowlist anyway).
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
function enabledModules(): Set<string>
|
|
27
|
+
function enabledModules(): Set<string> {
|
|
28
28
|
const raw = process.env.AILK_ENABLED_MODULES;
|
|
29
|
-
if (raw === undefined) return
|
|
29
|
+
if (raw === undefined) return new Set(); // unset = no paid modules
|
|
30
30
|
return new Set(
|
|
31
31
|
raw
|
|
32
32
|
.split(',')
|
|
@@ -37,7 +37,5 @@ function enabledModules(): Set<string> | undefined {
|
|
|
37
37
|
|
|
38
38
|
/** Whether the named paid module's routes should register on this build/boot. */
|
|
39
39
|
export function isModuleEnabled(moduleName: string): boolean {
|
|
40
|
-
|
|
41
|
-
if (enabled === undefined) return true;
|
|
42
|
-
return enabled.has(moduleName);
|
|
40
|
+
return enabledModules().has(moduleName);
|
|
43
41
|
}
|
|
@@ -194,6 +194,9 @@ export const prisma = {
|
|
|
194
194
|
findMany: jest.fn(),
|
|
195
195
|
update: jest.fn(),
|
|
196
196
|
updateMany: jest.fn(),
|
|
197
|
+
// `count` — the resume email's per-recipient send cap (#5406): how many
|
|
198
|
+
// drafts this address has started on this site inside the rolling window.
|
|
199
|
+
count: jest.fn(),
|
|
197
200
|
},
|
|
198
201
|
// ── Inbox namespace (WaitlistExperiment — #4661 D5) ──────────────────────
|
|
199
202
|
// `upsert` — the ONLY write path into waitlist_experiments
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #5334 — `loadSlice()` is the slice-module load boundary.
|
|
3
|
+
*
|
|
4
|
+
* The regression that matters most here is the LAST block: a slice that throws
|
|
5
|
+
* while evaluating must keep its own error object, type and stack. Wrapping it
|
|
6
|
+
* would turn a real bug in a slice into a misleading "could not be resolved"
|
|
7
|
+
* report, which is the opposite of what this boundary exists to do.
|
|
8
|
+
*/
|
|
9
|
+
import { loadSlice } from "../slice-load.js";
|
|
10
|
+
|
|
11
|
+
function moduleNotFound(code: string, message: string): Error {
|
|
12
|
+
const error = new Error(message) as Error & { code: string };
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("loadSlice (#5334)", () => {
|
|
18
|
+
it("passes a resolving thunk's value straight through", async () => {
|
|
19
|
+
const mod = { workspaceContextRoutes: () => undefined };
|
|
20
|
+
await expect(loadSlice("./routes/workspaces/index.js", async () => mod))
|
|
21
|
+
.resolves.toBe(mod);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("awaits the thunk rather than the specifier — the specifier is never imported", async () => {
|
|
25
|
+
const load = jest.fn(async () => ({ ok: true }));
|
|
26
|
+
await loadSlice("./never-imported.js", load);
|
|
27
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe.each([
|
|
31
|
+
["ERR_MODULE_NOT_FOUND", "the ESM resolver's code"],
|
|
32
|
+
["MODULE_NOT_FOUND", "the CJS resolver's code"],
|
|
33
|
+
])("on %s (%s)", (code) => {
|
|
34
|
+
const original = moduleNotFound(
|
|
35
|
+
code,
|
|
36
|
+
"Cannot find module '/x/apps/api/dist/list.js'",
|
|
37
|
+
);
|
|
38
|
+
const failing = async () => {
|
|
39
|
+
throw original;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
it("names the specifier in the message", async () => {
|
|
43
|
+
await expect(loadSlice("./list.js", failing)).rejects.toThrow(
|
|
44
|
+
/AILK slice module '\.\/list\.js'/,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("names BOTH causes, so a reader can tell a tracer miss from a bundled build", async () => {
|
|
49
|
+
const error = await loadSlice("./list.js", failing).catch(
|
|
50
|
+
(e: unknown) => e as Error,
|
|
51
|
+
);
|
|
52
|
+
expect(error.message).toMatch(/not SHIPPED/);
|
|
53
|
+
expect(error.message).toMatch(/slice-reachability:check/);
|
|
54
|
+
expect(error.message).toMatch(/BUNDLED/);
|
|
55
|
+
expect(error.message).toMatch(/apps\/api\/CLAUDE\.md/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("preserves the original error as `cause`", async () => {
|
|
59
|
+
const error = await loadSlice("./list.js", failing).catch(
|
|
60
|
+
(e: unknown) => e as Error,
|
|
61
|
+
);
|
|
62
|
+
expect(error.cause).toBe(original);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rethrows a non-resolution error IDENTICALLY — same object, not a wrapper", async () => {
|
|
67
|
+
const boom = new Error("slice blew up while evaluating");
|
|
68
|
+
const caught = await loadSlice("./list.js", async () => {
|
|
69
|
+
throw boom;
|
|
70
|
+
}).catch((e: unknown) => e);
|
|
71
|
+
expect(caught).toBe(boom);
|
|
72
|
+
expect((caught as Error).message).toBe("slice blew up while evaluating");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rethrows an errno-shaped error that is not a module-resolution code", async () => {
|
|
76
|
+
const denied = moduleNotFound("EACCES", "permission denied");
|
|
77
|
+
const caught = await loadSlice("./list.js", async () => {
|
|
78
|
+
throw denied;
|
|
79
|
+
}).catch((e: unknown) => e);
|
|
80
|
+
expect(caught).toBe(denied);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("rethrows a non-Error throw untouched", async () => {
|
|
84
|
+
const caught = await loadSlice("./list.js", async () => {
|
|
85
|
+
throw "a string, because JavaScript";
|
|
86
|
+
}).catch((e: unknown) => e);
|
|
87
|
+
expect(caught).toBe("a string, because JavaScript");
|
|
88
|
+
});
|
|
89
|
+
});
|