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
|
@@ -28,23 +28,180 @@
|
|
|
28
28
|
* `extractModuleBundle` therefore extracts into a throwaway temp dir first
|
|
29
29
|
* (same validation, same guards), and ONLY on full success copies each
|
|
30
30
|
* validated file into its place under `targetDir` — so a rejected or
|
|
31
|
-
* partially-decompressed bundle never touches the scaffold at all.
|
|
31
|
+
* partially-decompressed bundle never touches the scaffold at all. This
|
|
32
|
+
* guarantee extends all the way through `patches` (below): NOTHING is
|
|
33
|
+
* written to `targetDir` — not the bundle's own plain content, not an
|
|
34
|
+
* `installMap` destination, not a patched file — until every patch target
|
|
35
|
+
* has been computed successfully in memory.
|
|
36
|
+
*
|
|
37
|
+
* `installMap` / `postInstall` (spec 5373 § D7): a bundle may carry an
|
|
38
|
+
* `ailk-module.json` at its root, `{name, version, installMap, postInstall}`.
|
|
39
|
+
* `installMap` remaps specific staged files to a different scaffold
|
|
40
|
+
* destination (e.g. the docs-blog `llms-route.ts` landing on
|
|
41
|
+
* `app/llms.txt/route.ts`) — every source must be a file the validated
|
|
42
|
+
* extraction produced in staging (an absolute path, a `..` segment, or a
|
|
43
|
+
* path absent from staging rejects the WHOLE bundle, nothing copied), and
|
|
44
|
+
* every destination gets the same fail-closed containment check as a tar
|
|
45
|
+
* entry. A mapped source is copied to its destination only, never also to
|
|
46
|
+
* its original path. `postInstall` is printed to stdout after a successful
|
|
47
|
+
* merge, with C0/C1 control characters (other than newline) stripped first
|
|
48
|
+
* so a bundle cannot inject terminal escape sequences. `ailk-module.json`
|
|
49
|
+
* itself is never copied into the scaffold.
|
|
50
|
+
*
|
|
51
|
+
* `patches` (spec 5418 § 3 D1-D6): a sibling manifest field naming
|
|
52
|
+
* declarative edits to files the buyer's scaffold already has (or that this
|
|
53
|
+
* SAME bundle is about to deliver — see below). Validated at the same
|
|
54
|
+
* fail-closed point as `installMap` (D6), then COMPUTED — every patch
|
|
55
|
+
* target's new content resolved in memory, throwing on the first conflict /
|
|
56
|
+
* missing-target / unparseable-target failure (D4) — before `copyTree` or
|
|
57
|
+
* the `installMap` copy loop runs at all. A patch target the bundle itself
|
|
58
|
+
* delivers (as plain content or as an `installMap` destination) is read
|
|
59
|
+
* from the STAGED tree for this computation, since `targetDir` would not
|
|
60
|
+
* have it yet; every other target is read from the already-scaffolded
|
|
61
|
+
* `targetDir` file. Only once every target computes successfully does
|
|
62
|
+
* `copyTree`/`installMap` run, followed by one write pass for the computed
|
|
63
|
+
* patches — so a patch failure leaves `targetDir` exactly as it was before
|
|
64
|
+
* this call, including files THIS bundle would otherwise have added.
|
|
32
65
|
*/
|
|
33
|
-
import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
66
|
+
import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
34
67
|
import { tmpdir } from "node:os";
|
|
35
68
|
import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
36
69
|
import { pipeline } from "node:stream/promises";
|
|
37
70
|
import { createGunzip } from "node:zlib";
|
|
38
71
|
import * as tar from "tar";
|
|
72
|
+
import { computeModulePatches, writeModulePatches } from "./apply-module-patches.js";
|
|
39
73
|
/** Soft limits to prevent decompression-bomb / zip-bomb attacks. Same values as extractBundle. */
|
|
40
74
|
const MAX_ENTRIES = 1000;
|
|
41
75
|
const MAX_TOTAL_SIZE_BYTES = 50 * 1024 * 1024; // 50 MiB
|
|
76
|
+
/** The bundle-root manifest file (spec § D7). Never copied into the scaffold. */
|
|
77
|
+
const AILK_MODULE_JSON = "ailk-module.json";
|
|
78
|
+
/** The op names valid for each patch `kind` (spec 5418 § 3 D1's ModulePatchOp union, partitioned). */
|
|
79
|
+
const VALID_OPS_BY_KIND = {
|
|
80
|
+
json: new Set(["merge-keys", "append-unique"]),
|
|
81
|
+
"next-config": new Set(["add-transpile-package", "wrap-export-with"]),
|
|
82
|
+
"jest-config": new Set([
|
|
83
|
+
"add-module-name-mapper",
|
|
84
|
+
"add-transform-ignore-pattern",
|
|
85
|
+
"add-transform",
|
|
86
|
+
]),
|
|
87
|
+
};
|
|
88
|
+
/** True iff `child` is the same path as `parent` or a descendant of it. */
|
|
89
|
+
function isPathInside(parent, child) {
|
|
90
|
+
const rel = relative(parent, child);
|
|
91
|
+
if (rel === "")
|
|
92
|
+
return true;
|
|
93
|
+
if (rel.startsWith(".."))
|
|
94
|
+
return false;
|
|
95
|
+
if (isAbsolute(rel))
|
|
96
|
+
return false;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Reject an absolute path or any `..` segment — the same shape check
|
|
101
|
+
* `safeExtractTarGz` applies to tar entries, reused here for `installMap`
|
|
102
|
+
* keys/values, which come from bundle-controlled JSON rather than tar
|
|
103
|
+
* entries and so need the same fail-closed treatment.
|
|
104
|
+
*/
|
|
105
|
+
function hasUnsafeSegment(p) {
|
|
106
|
+
if (isAbsolute(p))
|
|
107
|
+
return true;
|
|
108
|
+
const normalized = normalize(p);
|
|
109
|
+
return (normalized === ".." ||
|
|
110
|
+
normalized.startsWith(`..${sep}`) ||
|
|
111
|
+
normalized.split(sep).includes(".."));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read `ailk-module.json` from the validated staging tree, if present.
|
|
115
|
+
* Malformed JSON is treated as absent — the manifest is written by the
|
|
116
|
+
* producer (D7), never hand-authored, so a parse failure is a producer bug,
|
|
117
|
+
* not a security boundary to fail closed on with a rejected install.
|
|
118
|
+
*/
|
|
119
|
+
function readAilkModuleManifest(extractedRoot) {
|
|
120
|
+
const manifestPath = join(extractedRoot, AILK_MODULE_JSON);
|
|
121
|
+
if (!existsSync(manifestPath))
|
|
122
|
+
return null;
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validate every `installMap` entry against the staged tree and the target
|
|
132
|
+
* scaffold, throwing on the first violation — called BEFORE any copy, so a
|
|
133
|
+
* bad entry rejects the whole bundle with nothing written to `targetDir`.
|
|
134
|
+
* Returns the map unchanged (validation only) for the caller to apply.
|
|
135
|
+
*/
|
|
136
|
+
function validateInstallMap(installMap, extractedRoot, targetAbs, moduleName) {
|
|
137
|
+
for (const [source, destination] of Object.entries(installMap)) {
|
|
138
|
+
if (hasUnsafeSegment(source)) {
|
|
139
|
+
throw new Error(`module "${moduleName}": unsafe installMap source "${source}"`);
|
|
140
|
+
}
|
|
141
|
+
const sourceAbs = resolve(extractedRoot, source);
|
|
142
|
+
if (!isPathInside(extractedRoot, sourceAbs)) {
|
|
143
|
+
throw new Error(`module "${moduleName}": installMap source "${source}" resolves outside staging`);
|
|
144
|
+
}
|
|
145
|
+
let sourceStat;
|
|
146
|
+
try {
|
|
147
|
+
sourceStat = statSync(sourceAbs);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error(`module "${moduleName}": installMap source "${source}" is not a file the bundle produced`);
|
|
151
|
+
}
|
|
152
|
+
if (!sourceStat.isFile()) {
|
|
153
|
+
throw new Error(`module "${moduleName}": installMap source "${source}" is not a file the bundle produced`);
|
|
154
|
+
}
|
|
155
|
+
if (hasUnsafeSegment(destination)) {
|
|
156
|
+
throw new Error(`module "${moduleName}": unsafe installMap destination "${destination}"`);
|
|
157
|
+
}
|
|
158
|
+
const destAbs = resolve(targetAbs, destination);
|
|
159
|
+
if (!isPathInside(targetAbs, destAbs)) {
|
|
160
|
+
throw new Error(`module "${moduleName}": installMap destination "${destination}" resolves outside target`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validate every `patches` entry against `targetDir` — the same fail-closed
|
|
166
|
+
* point and containment guard `validateInstallMap` above uses, extended
|
|
167
|
+
* (spec 5418 § 3 D6): `target` must resolve inside `targetDir`; `kind` and
|
|
168
|
+
* every `op.op` must be a recognized vocabulary member. Any violation
|
|
169
|
+
* refuses the WHOLE bundle before `copyTree` runs — this is D4 class 4
|
|
170
|
+
* (unknown kind/op), resolved here at validation time, never inside
|
|
171
|
+
* `applyModulePatches` (which never sees an invalid kind/op).
|
|
172
|
+
*/
|
|
173
|
+
function validatePatches(patches, targetAbs, moduleName) {
|
|
174
|
+
for (const patch of patches) {
|
|
175
|
+
if (hasUnsafeSegment(patch.target)) {
|
|
176
|
+
throw new Error(`module "${moduleName}": unsafe patch target "${patch.target}"`);
|
|
177
|
+
}
|
|
178
|
+
const targetAbsPath = resolve(targetAbs, patch.target);
|
|
179
|
+
if (!isPathInside(targetAbs, targetAbsPath)) {
|
|
180
|
+
throw new Error(`module "${moduleName}": patch target "${patch.target}" resolves outside target`);
|
|
181
|
+
}
|
|
182
|
+
const validOps = VALID_OPS_BY_KIND[patch.kind];
|
|
183
|
+
if (!validOps) {
|
|
184
|
+
throw new Error(`invalid module manifest: unknown patch kind "${patch.kind}" for target ${patch.target}`);
|
|
185
|
+
}
|
|
186
|
+
for (const op of patch.ops) {
|
|
187
|
+
if (!validOps.has(op.op)) {
|
|
188
|
+
throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${patch.target}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** C0 (0x00-0x1F) and C1 (0x7F-0x9F) control characters, newline excepted. */
|
|
194
|
+
function stripControlCharacters(text) {
|
|
195
|
+
// eslint-disable-next-line no-control-regex -- deliberately stripping control chars
|
|
196
|
+
return text.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, "");
|
|
197
|
+
}
|
|
42
198
|
/**
|
|
43
199
|
* Extract a fetched module bundle into `targetDir`, merging its files onto
|
|
44
200
|
* the tree `copyTemplate` already wrote. Throws on any unsafe entry,
|
|
45
|
-
* decompress error,
|
|
46
|
-
* failure (
|
|
47
|
-
*
|
|
201
|
+
* decompress error, limit overrun, invalid `installMap`, or `patches`
|
|
202
|
+
* failure (D4) — `targetDir` is left untouched on failure in every one of
|
|
203
|
+
* those cases: validation AND patch computation both complete before any
|
|
204
|
+
* file is copied or written into place.
|
|
48
205
|
*/
|
|
49
206
|
export async function extractModuleBundle(options) {
|
|
50
207
|
const { bytes, targetDir, moduleName } = options;
|
|
@@ -59,10 +216,71 @@ export async function extractModuleBundle(options) {
|
|
|
59
216
|
const extractedRoot = join(stagingDir, "extracted");
|
|
60
217
|
mkdirSync(extractedRoot, { recursive: true });
|
|
61
218
|
await safeExtractTarGz(stagingTar, extractedRoot, moduleName);
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
219
|
+
// installMap validation runs BEFORE any copy, so a bad entry rejects the
|
|
220
|
+
// whole bundle with nothing written to targetDir (same posture as the
|
|
221
|
+
// tar-entry validation above).
|
|
222
|
+
const manifest = readAilkModuleManifest(extractedRoot);
|
|
223
|
+
const installMap = manifest?.installMap ?? {};
|
|
224
|
+
validateInstallMap(installMap, extractedRoot, targetAbs, moduleName);
|
|
225
|
+
// patches validation (spec 5418 § 3 D6) runs at the same point, before
|
|
226
|
+
// any copy — a bad target/kind/op rejects the whole bundle with nothing
|
|
227
|
+
// written to targetDir, same posture as installMap and the tar-entry
|
|
228
|
+
// validation above.
|
|
229
|
+
const patches = manifest?.patches ?? [];
|
|
230
|
+
validatePatches(patches, targetAbs, moduleName);
|
|
231
|
+
// Compute every patch target's new content BEFORE any write to
|
|
232
|
+
// targetDir — including before copyTree/installMap below, not only
|
|
233
|
+
// before the patch step's own write (spec 5418 § 3 D4/§ 6.2): a
|
|
234
|
+
// conflict, missing-target, or unparseable-target failure must leave
|
|
235
|
+
// the scaffold exactly as it was, which includes the bundle's own
|
|
236
|
+
// plain content and installMap destinations never landing either. A
|
|
237
|
+
// patch target the bundle itself delivers (as plain content or as an
|
|
238
|
+
// installMap destination) is read from the STAGED tree here, since
|
|
239
|
+
// targetDir would not have it yet at this point.
|
|
240
|
+
const installMapDestinationToSource = new Map(Object.entries(installMap).map(([source, destination]) => [destination, source]));
|
|
241
|
+
const readPatchTarget = (target) => {
|
|
242
|
+
const mappedSource = installMapDestinationToSource.get(target);
|
|
243
|
+
if (mappedSource !== undefined) {
|
|
244
|
+
const sourceAbs = resolve(extractedRoot, mappedSource);
|
|
245
|
+
return existsSync(sourceAbs) ? readFileSync(sourceAbs, "utf8") : undefined;
|
|
246
|
+
}
|
|
247
|
+
const stagedAbs = resolve(extractedRoot, target);
|
|
248
|
+
if (existsSync(stagedAbs) && statSync(stagedAbs).isFile()) {
|
|
249
|
+
return readFileSync(stagedAbs, "utf8");
|
|
250
|
+
}
|
|
251
|
+
const targetAbsPath = resolve(targetAbs, target);
|
|
252
|
+
return existsSync(targetAbsPath) ? readFileSync(targetAbsPath, "utf8") : undefined;
|
|
253
|
+
};
|
|
254
|
+
const computedPatches = computeModulePatches(patches, {
|
|
255
|
+
versions: options.versions ?? {},
|
|
256
|
+
readTarget: readPatchTarget,
|
|
257
|
+
});
|
|
258
|
+
// Full validation AND patch computation passed — only now does anything
|
|
259
|
+
// get written to targetDir. Copies only (never deletes anything): a
|
|
260
|
+
// fetched module's files ADD to the free scaffold, they never remove
|
|
261
|
+
// what the free copy already wrote. ailk-module.json and every
|
|
262
|
+
// installMap source are excluded from the whole-tree copy — the
|
|
263
|
+
// manifest never lands in the scaffold, and a mapped source lands ONLY
|
|
264
|
+
// at its mapped destination, never also at its original path.
|
|
265
|
+
const excluded = new Set([
|
|
266
|
+
AILK_MODULE_JSON,
|
|
267
|
+
...Object.keys(installMap),
|
|
268
|
+
]);
|
|
269
|
+
copyTree(extractedRoot, targetAbs, excluded);
|
|
270
|
+
for (const [source, destination] of Object.entries(installMap)) {
|
|
271
|
+
const destAbs = resolve(targetAbs, destination);
|
|
272
|
+
mkdirSync(join(destAbs, ".."), { recursive: true });
|
|
273
|
+
copyFileSync(resolve(extractedRoot, source), destAbs);
|
|
274
|
+
}
|
|
275
|
+
// patches write AFTER every file the bundle brought is in place (D5) —
|
|
276
|
+
// after installMap, before postInstall — so a patch can never race a
|
|
277
|
+
// file its own ownedFiles/installMap just delivered, and (since it was
|
|
278
|
+
// COMPUTED above, before either ran) overwrites rather than conflicts
|
|
279
|
+
// with content copyTree/installMap just placed at the same path.
|
|
280
|
+
writeModulePatches(computedPatches, targetAbs);
|
|
281
|
+
if (manifest?.postInstall) {
|
|
282
|
+
process.stdout.write(`${stripControlCharacters(manifest.postInstall)}\n`);
|
|
283
|
+
}
|
|
66
284
|
}
|
|
67
285
|
finally {
|
|
68
286
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -70,11 +288,14 @@ export async function extractModuleBundle(options) {
|
|
|
70
288
|
}
|
|
71
289
|
/**
|
|
72
290
|
* Recursively copy every file from `srcRoot` into `destRoot`, creating parent
|
|
73
|
-
* directories as needed
|
|
74
|
-
*
|
|
75
|
-
*
|
|
291
|
+
* directories as needed, except any path in `excluded` (relative to
|
|
292
|
+
* `srcRoot`, POSIX-normalized) — the bundle manifest and any `installMap`
|
|
293
|
+
* source, which land only at their mapped destination. Runs ONLY after
|
|
294
|
+
* `safeExtractTarGz` has validated every entry, so no additional
|
|
295
|
+
* path-safety check is needed here — `srcRoot` is our own staging tree, not
|
|
296
|
+
* attacker-controlled input.
|
|
76
297
|
*/
|
|
77
|
-
function copyTree(srcRoot, destRoot) {
|
|
298
|
+
function copyTree(srcRoot, destRoot, excluded = new Set()) {
|
|
78
299
|
const stack = [srcRoot];
|
|
79
300
|
while (stack.length > 0) {
|
|
80
301
|
const dir = stack.pop();
|
|
@@ -89,6 +310,8 @@ function copyTree(srcRoot, destRoot) {
|
|
|
89
310
|
stack.push(srcPath);
|
|
90
311
|
}
|
|
91
312
|
else if (entry.isFile()) {
|
|
313
|
+
if (excluded.has(rel.split(sep).join("/")))
|
|
314
|
+
continue;
|
|
92
315
|
mkdirSync(join(destPath, ".."), { recursive: true });
|
|
93
316
|
copyFileSync(srcPath, destPath);
|
|
94
317
|
}
|
|
@@ -171,14 +394,3 @@ async function safeExtractTarGz(tarGzPath, targetDir, moduleName) {
|
|
|
171
394
|
throw captured.error;
|
|
172
395
|
}
|
|
173
396
|
}
|
|
174
|
-
/** True iff `child` is the same path as `parent` or a descendant of it. */
|
|
175
|
-
function isPathInside(parent, child) {
|
|
176
|
-
const rel = relative(parent, child);
|
|
177
|
-
if (rel === "")
|
|
178
|
-
return true;
|
|
179
|
-
if (rel.startsWith(".."))
|
|
180
|
-
return false;
|
|
181
|
-
if (isAbsolute(rel))
|
|
182
|
-
return false;
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
@@ -1,47 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authenticated paid-module-bundle fetch (K1, #5089 — Flavor 3).
|
|
3
|
-
*
|
|
4
|
-
* Posture lifted verbatim from `packages/cli/src/lib/fetch-bundle.ts`
|
|
5
|
-
* (`fetchBundle`, the skill-registry twin of this function):
|
|
6
|
-
* - Bearer-JWT auth on every request.
|
|
7
|
-
* - `redirect: "error"` so a maliciously redirected response can't drop us
|
|
8
|
-
* onto an unintended host — the CLI must hit the configured module
|
|
9
|
-
* distribution only.
|
|
10
|
-
* - Empty-token guard before any network call (defense in depth — the
|
|
11
|
-
* caller should already have gated on an empty/absent key).
|
|
12
|
-
* - Status-code mapping: 401 → license invalid/revoked/lapsed, 404 →
|
|
13
|
-
* module content not yet available, other 4xx/5xx → upstream body.
|
|
14
|
-
*
|
|
15
|
-
* Deliberately DOES NOT depend on `@working-theory/license` or
|
|
16
|
-
* `@working-theory/cli` — both are `publishConfig.access: "restricted"` and
|
|
17
|
-
* `create-ailk` is the one package in this monorepo published to PUBLIC npm
|
|
18
|
-
* (Apache-2.0, A1 disposed it `oss`). A runtime dependency on a restricted
|
|
19
|
-
* package would break `pnpm install` for every free-tier consumer, not just
|
|
20
|
-
* gate the paid modules — so this file re-states fetchBundle's POSTURE
|
|
21
|
-
* rather than importing it. Named in the K1 PR body as the public-package
|
|
22
|
-
* boundary this card had to draw.
|
|
23
|
-
*
|
|
24
|
-
* Also deliberately skips fetchBundle's caller-side `checkLicense()` local
|
|
25
|
-
* JWT pre-check (install-skill.ts's step 2): that local verification lives
|
|
26
|
-
* in `@working-theory/license`, which for the same publish-boundary reason
|
|
27
|
-
* above cannot be a runtime dependency here. Instead the module distribution
|
|
28
|
-
* endpoint's own 401 response IS the license verdict — exactly the signal
|
|
29
|
-
* `fetchBundle` already surfaces via `isLicenseError`. This trades one local,
|
|
30
|
-
* no-network check for one authoritative, server-verified one; the caller-
|
|
31
|
-
* visible contract (present-but-invalid key ⟹ the honest "does not cover
|
|
32
|
-
* this" message) is unchanged.
|
|
33
|
-
*
|
|
34
|
-
* Returns the response body bytes on 200; throws a descriptive
|
|
35
|
-
* `FetchModuleError` on any failure path so the caller can map to an honest
|
|
36
|
-
* exit uniformly.
|
|
37
|
-
*/
|
|
38
1
|
export declare const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/api/modules";
|
|
39
2
|
export interface FetchModuleOptions {
|
|
40
3
|
moduleName: string;
|
|
41
4
|
/** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
|
|
42
5
|
token: string;
|
|
43
|
-
/** Registry root;
|
|
6
|
+
/** Registry root; `<name>/<version>` is appended as two path segments. */
|
|
44
7
|
registryUrl?: string;
|
|
8
|
+
/** The module version to request. Defaults to this package's own version. */
|
|
9
|
+
version?: string;
|
|
45
10
|
/** Override for tests; defaults to `globalThis.fetch`. Never a live call in CI. */
|
|
46
11
|
fetchImpl?: typeof fetch;
|
|
47
12
|
}
|
package/dist/lib/fetch-module.js
CHANGED
|
@@ -35,7 +35,20 @@
|
|
|
35
35
|
* `FetchModuleError` on any failure path so the caller can map to an honest
|
|
36
36
|
* exit uniformly.
|
|
37
37
|
*/
|
|
38
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
38
39
|
export const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/api/modules";
|
|
40
|
+
/**
|
|
41
|
+
* This package's own `package.json` `"version"`. Deliberately a static JSON
|
|
42
|
+
* import (`resolveJsonModule`), not `import.meta.url` + `fileURLToPath` (the
|
|
43
|
+
* technique `cli.ts:56-58`'s `readPackageVersion` uses) — `fetch-module.ts`
|
|
44
|
+
* is loaded directly by `fetch-module.test.ts`, and `import.meta` is a
|
|
45
|
+
* SyntaxError under ts-jest's CommonJS transform (the same reason
|
|
46
|
+
* `module-architecture.ts`'s `paidModuleTemplateExclusions` doc gives for
|
|
47
|
+
* avoiding it in any jest-reachable file). A bundle overlays the scaffold
|
|
48
|
+
* `create-ailk` just copied from its own `templates/`, so the default
|
|
49
|
+
* version request must be the running package's own.
|
|
50
|
+
*/
|
|
51
|
+
const CREATE_AILK_VERSION = pkg.version;
|
|
39
52
|
export class FetchModuleError extends Error {
|
|
40
53
|
isLicenseError;
|
|
41
54
|
constructor(message,
|
|
@@ -47,25 +60,26 @@ export class FetchModuleError extends Error {
|
|
|
47
60
|
}
|
|
48
61
|
}
|
|
49
62
|
/**
|
|
50
|
-
* Build the fetch URL by joining `registryUrl` with `moduleName`
|
|
51
|
-
* trailing path
|
|
52
|
-
* in depth — `parse-args.ts`'s `parseModules` already restricts
|
|
53
|
-
* `RECOGNIZED_MODULES`, but a hostile value can never smuggle
|
|
54
|
-
* separator or query string here regardless.
|
|
63
|
+
* Build the fetch URL by joining `registryUrl` with `moduleName` and
|
|
64
|
+
* `version` as two trailing path segments. `encodeURIComponent` on each is
|
|
65
|
+
* defense in depth — `parse-args.ts`'s `parseModules` already restricts
|
|
66
|
+
* `moduleName` to `RECOGNIZED_MODULES`, but a hostile value can never smuggle
|
|
67
|
+
* a path separator or query string here regardless.
|
|
55
68
|
*/
|
|
56
|
-
function buildUrl(registryUrl, moduleName) {
|
|
69
|
+
function buildUrl(registryUrl, moduleName, version) {
|
|
57
70
|
const trimmed = registryUrl.replace(/\/+$/, "");
|
|
58
|
-
return `${trimmed}/${encodeURIComponent(moduleName)}`;
|
|
71
|
+
return `${trimmed}/${encodeURIComponent(moduleName)}/${encodeURIComponent(version)}`;
|
|
59
72
|
}
|
|
60
73
|
export async function fetchModule(options) {
|
|
61
74
|
const { moduleName, token } = options;
|
|
62
75
|
if (token.length === 0) {
|
|
63
76
|
throw new FetchModuleError("internal error: fetchModule called without a token");
|
|
64
77
|
}
|
|
78
|
+
const version = options.version ?? CREATE_AILK_VERSION;
|
|
65
79
|
const registryUrl = options.registryUrl ??
|
|
66
80
|
process.env["AILK_MODULE_REGISTRY_URL"] ??
|
|
67
81
|
DEFAULT_AILK_MODULE_REGISTRY_URL;
|
|
68
|
-
const url = buildUrl(registryUrl, moduleName);
|
|
82
|
+
const url = buildUrl(registryUrl, moduleName, version);
|
|
69
83
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
70
84
|
let response;
|
|
71
85
|
try {
|
|
@@ -86,6 +100,10 @@ export async function fetchModule(options) {
|
|
|
86
100
|
throw new FetchModuleError(`your licence does not cover the "${moduleName}" module (invalid, revoked, or lapsed)`,
|
|
87
101
|
/* isLicenseError */ true);
|
|
88
102
|
}
|
|
103
|
+
if (response.status === 403) {
|
|
104
|
+
throw new FetchModuleError(`your licence does not include the "${moduleName}" module, or its term ended before create-ailk ${version} was released — use a create-ailk version released during your term`,
|
|
105
|
+
/* isLicenseError */ true);
|
|
106
|
+
}
|
|
89
107
|
if (response.status === 404) {
|
|
90
108
|
throw new FetchModuleError(`"${moduleName}" module content not yet available — ping us at hello@ailaunchkit.ai`);
|
|
91
109
|
}
|
|
@@ -35,6 +35,8 @@ export interface ResolvePaidModulesOptions {
|
|
|
35
35
|
licenseKey?: string | undefined;
|
|
36
36
|
/** Forwarded to `fetchModule`; defaults to `AILK_MODULE_REGISTRY_URL` / the built-in default. */
|
|
37
37
|
registryUrl?: string;
|
|
38
|
+
/** Forwarded to `fetchModule`; defaults to this package's own version. */
|
|
39
|
+
version?: string;
|
|
38
40
|
/** Forwarded to `fetchModule`; defaults to `globalThis.fetch`. Never a live call in tests. */
|
|
39
41
|
fetchImpl?: typeof fetch;
|
|
40
42
|
}
|
|
@@ -39,6 +39,7 @@ export async function resolvePaidModules(options) {
|
|
|
39
39
|
...(options.registryUrl !== undefined
|
|
40
40
|
? { registryUrl: options.registryUrl }
|
|
41
41
|
: {}),
|
|
42
|
+
...(options.version !== undefined ? { version: options.version } : {}),
|
|
42
43
|
...(options.fetchImpl !== undefined
|
|
43
44
|
? { fetchImpl: options.fetchImpl }
|
|
44
45
|
: {}),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The derived set of scaffold-relative paths a keyless scaffold must not
|
|
3
|
+
* contain — sorted, deduped.
|
|
4
|
+
*
|
|
5
|
+
* Throws if the result is empty: `PAID_MODULES` is never empty (it is
|
|
6
|
+
* `RECOGNIZED_MODULES` minus `FREE_MODULES`) and every paid module registered
|
|
7
|
+
* today owns at least one file under `apps/` or a whole app, so an empty
|
|
8
|
+
* result means the derivation itself broke — not that there is nothing to
|
|
9
|
+
* check (DEF-007: a silent `[]` must fail loudly, not pass the gate).
|
|
10
|
+
*/
|
|
11
|
+
export declare function derivePaidScaffoldPaths(): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Every derived paid path that exists under `scaffoldDir` — the leaked set,
|
|
14
|
+
* sorted. Empty means the scaffold is clean.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findLeakedPaidPaths(scaffoldDir: string): string[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived paid-path list (R2-5374 — the no-paid-code CI gate, D-44).
|
|
3
|
+
*
|
|
4
|
+
* The set of scaffold-relative paths a keyless (Free) `create-ailk` scaffold
|
|
5
|
+
* must never contain, computed from `MODULE_SLICES` / `PAID_MODULES` at call
|
|
6
|
+
* time — never hand-maintained, so it grows automatically once commerce
|
|
7
|
+
* (D-47) and signal-collectors (D-48) gain their own `MODULE_SLICES` entries.
|
|
8
|
+
*
|
|
9
|
+
* Only two of `ModuleSlice`'s shapes name a path that can actually appear in a
|
|
10
|
+
* scaffold's own output tree (`build-templates.mjs`'s `SNAPSHOT_DIRS` is
|
|
11
|
+
* `["apps", "database"]` — see its own header comment; `packages/*` is never
|
|
12
|
+
* copied into a scaffold, it is consumed as an npm dependency):
|
|
13
|
+
*
|
|
14
|
+
* - `ownedFiles` entries that already sit under `apps/` — the scaffold
|
|
15
|
+
* copies that tree verbatim, so the path is identical on both sides.
|
|
16
|
+
* - `apps[name].path` — a paid module's OWN app directory. Multi-tenant's
|
|
17
|
+
* `workspace` app has been extracted out of this repo's own `apps/` tree
|
|
18
|
+
* into a sibling paid-apps root (its `ownedFiles` entry is a
|
|
19
|
+
* whole-directory glob naming that monorepo-internal location, not a
|
|
20
|
+
* scaffold one), so the app it introduces into a SCAFFOLD is named here
|
|
21
|
+
* instead, via the slice's own declared path. `api` / `mcp` / `web` are
|
|
22
|
+
* excluded: they are base apps present in
|
|
23
|
+
* EVERY scaffold regardless of licence — a module only ever grants
|
|
24
|
+
* import permissions into them, never introduces the directory, and the
|
|
25
|
+
* paid FILES inside them are already covered by the `ownedFiles` branch
|
|
26
|
+
* above.
|
|
27
|
+
*
|
|
28
|
+
* `ownedFiles` entries under `packages/` are excluded for the reason above.
|
|
29
|
+
* `namespaces` (database/*) are excluded too: each slice's own doc comment
|
|
30
|
+
* (see multi-tenant's in `module-architecture.ts`) records that a namespace
|
|
31
|
+
* directory mixes base and paid Prisma models in one `schema.prisma` file, so
|
|
32
|
+
* the DIRECTORY itself is not paid-owned even though some models inside it
|
|
33
|
+
* are — asserting it absent would be wrong on every scaffold, paid or free.
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync } from "node:fs";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
import { MODULE_SLICES } from "../module-architecture.js";
|
|
38
|
+
import { PAID_MODULES } from "./module-tier.js";
|
|
39
|
+
/** Base app paths present in every scaffold regardless of the selected modules. */
|
|
40
|
+
const BASE_APP_PATHS = new Set([
|
|
41
|
+
"apps/web",
|
|
42
|
+
"apps/api",
|
|
43
|
+
"apps/mcp",
|
|
44
|
+
]);
|
|
45
|
+
/** A slice's `ownedFiles` entries that live under `apps/` — copied verbatim into a scaffold. */
|
|
46
|
+
function ownedFilesUnderApps(slice) {
|
|
47
|
+
return (slice.ownedFiles ?? [])
|
|
48
|
+
.filter((entry) => entry.startsWith("apps/"))
|
|
49
|
+
.map((entry) => entry.endsWith("/**") ? entry.slice(0, -"/**".length) : entry);
|
|
50
|
+
}
|
|
51
|
+
/** A slice's own app directories, excluding the base apps every scaffold ships. */
|
|
52
|
+
function introducedAppPaths(slice) {
|
|
53
|
+
return Object.values(slice.apps ?? {})
|
|
54
|
+
.map((app) => app.path)
|
|
55
|
+
.filter((path) => !BASE_APP_PATHS.has(path));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The derived set of scaffold-relative paths a keyless scaffold must not
|
|
59
|
+
* contain — sorted, deduped.
|
|
60
|
+
*
|
|
61
|
+
* Throws if the result is empty: `PAID_MODULES` is never empty (it is
|
|
62
|
+
* `RECOGNIZED_MODULES` minus `FREE_MODULES`) and every paid module registered
|
|
63
|
+
* today owns at least one file under `apps/` or a whole app, so an empty
|
|
64
|
+
* result means the derivation itself broke — not that there is nothing to
|
|
65
|
+
* check (DEF-007: a silent `[]` must fail loudly, not pass the gate).
|
|
66
|
+
*/
|
|
67
|
+
export function derivePaidScaffoldPaths() {
|
|
68
|
+
const paths = new Set();
|
|
69
|
+
for (const moduleName of PAID_MODULES) {
|
|
70
|
+
const slice = MODULE_SLICES[moduleName];
|
|
71
|
+
for (const p of ownedFilesUnderApps(slice))
|
|
72
|
+
paths.add(p);
|
|
73
|
+
for (const p of introducedAppPaths(slice))
|
|
74
|
+
paths.add(p);
|
|
75
|
+
}
|
|
76
|
+
const sorted = [...paths].sort();
|
|
77
|
+
if (sorted.length === 0) {
|
|
78
|
+
throw new Error("derivePaidScaffoldPaths() computed an empty list — MODULE_SLICES and " +
|
|
79
|
+
"PAID_MODULES have drifted out of sync (DEF-007: this must never " +
|
|
80
|
+
"silently pass the no-paid-code gate)");
|
|
81
|
+
}
|
|
82
|
+
return sorted;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Every derived paid path that exists under `scaffoldDir` — the leaked set,
|
|
86
|
+
* sorted. Empty means the scaffold is clean.
|
|
87
|
+
*/
|
|
88
|
+
export function findLeakedPaidPaths(scaffoldDir) {
|
|
89
|
+
return derivePaidScaffoldPaths().filter((relPath) => existsSync(join(scaffoldDir, relPath)));
|
|
90
|
+
}
|