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
|
@@ -28,26 +28,243 @@
|
|
|
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.
|
|
65
|
+
*
|
|
66
|
+
* SITE TEMPLATES (spec 5481; parent record D9, D10, DD4, DD7). A template's
|
|
67
|
+
* root manifest is `ailk-template.json` — `ailk-module.json`'s shape plus a
|
|
68
|
+
* mandatory `requires`, cross-checked against the client registry's list so a
|
|
69
|
+
* stale client cannot land a template built for a different module set. Two
|
|
70
|
+
* differences from the module path, both deliberate and both confined to
|
|
71
|
+
* reading the manifest: a template's manifest MUST be present and parseable
|
|
72
|
+
* (a module's absent-or-unparseable manifest legitimately degrades to a plain
|
|
73
|
+
* content merge; a template's `requires` is mandatory, so treat-as-absent
|
|
74
|
+
* would make the cross-check optional at the bundle's own discretion), and a
|
|
75
|
+
* `requires` disagreement rejects. Everything after that — traversal,
|
|
76
|
+
* containment, link rejection, the limits, the atomicity ordering — is the
|
|
77
|
+
* same code, reached through `installBundleTree`, which the keyless Free path
|
|
78
|
+
* calls directly with `site-templates/<name>/` as its source root.
|
|
32
79
|
*/
|
|
33
|
-
import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
80
|
+
import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
34
81
|
import { tmpdir } from "node:os";
|
|
35
82
|
import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
36
83
|
import { pipeline } from "node:stream/promises";
|
|
37
84
|
import { createGunzip } from "node:zlib";
|
|
38
85
|
import * as tar from "tar";
|
|
86
|
+
import { computeModulePatches, writeModulePatches } from "./apply-module-patches.js";
|
|
39
87
|
/** Soft limits to prevent decompression-bomb / zip-bomb attacks. Same values as extractBundle. */
|
|
40
88
|
const MAX_ENTRIES = 1000;
|
|
41
89
|
const MAX_TOTAL_SIZE_BYTES = 50 * 1024 * 1024; // 50 MiB
|
|
90
|
+
/** The bundle-root manifest file (spec § D7). Never copied into the scaffold. */
|
|
91
|
+
const AILK_MODULE_JSON = "ailk-module.json";
|
|
92
|
+
/**
|
|
93
|
+
* A SITE TEMPLATE's root manifest (spec 5481 § 4; parent record D10) — the
|
|
94
|
+
* same shape as `ailk-module.json` plus one mandatory field, `requires`.
|
|
95
|
+
* Never copied into the scaffold either.
|
|
96
|
+
*/
|
|
97
|
+
const AILK_TEMPLATE_JSON = "ailk-template.json";
|
|
98
|
+
/** The op names valid for each patch `kind` (spec 5418 § 3 D1's ModulePatchOp union, partitioned). */
|
|
99
|
+
const VALID_OPS_BY_KIND = {
|
|
100
|
+
json: new Set(["merge-keys", "append-unique"]),
|
|
101
|
+
"next-config": new Set(["add-transpile-package", "wrap-export-with"]),
|
|
102
|
+
"jest-config": new Set([
|
|
103
|
+
"add-module-name-mapper",
|
|
104
|
+
"add-transform-ignore-pattern",
|
|
105
|
+
"add-transform",
|
|
106
|
+
]),
|
|
107
|
+
};
|
|
108
|
+
/** True iff `child` is the same path as `parent` or a descendant of it. */
|
|
109
|
+
function isPathInside(parent, child) {
|
|
110
|
+
const rel = relative(parent, child);
|
|
111
|
+
if (rel === "")
|
|
112
|
+
return true;
|
|
113
|
+
if (rel.startsWith(".."))
|
|
114
|
+
return false;
|
|
115
|
+
if (isAbsolute(rel))
|
|
116
|
+
return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Reject an absolute path or any `..` segment — the same shape check
|
|
121
|
+
* `safeExtractTarGz` applies to tar entries, reused here for `installMap`
|
|
122
|
+
* keys/values, which come from bundle-controlled JSON rather than tar
|
|
123
|
+
* entries and so need the same fail-closed treatment.
|
|
124
|
+
*/
|
|
125
|
+
function hasUnsafeSegment(p) {
|
|
126
|
+
if (isAbsolute(p))
|
|
127
|
+
return true;
|
|
128
|
+
const normalized = normalize(p);
|
|
129
|
+
return (normalized === ".." ||
|
|
130
|
+
normalized.startsWith(`..${sep}`) ||
|
|
131
|
+
normalized.split(sep).includes(".."));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read `ailk-module.json` from the validated staging tree, if present.
|
|
135
|
+
* Malformed JSON is treated as absent — the manifest is written by the
|
|
136
|
+
* producer (D7), never hand-authored, so a parse failure is a producer bug,
|
|
137
|
+
* not a security boundary to fail closed on with a rejected install.
|
|
138
|
+
*/
|
|
139
|
+
function readAilkModuleManifest(extractedRoot) {
|
|
140
|
+
const manifestPath = join(extractedRoot, AILK_MODULE_JSON);
|
|
141
|
+
if (!existsSync(manifestPath))
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Read `ailk-template.json` from a template tree's root and cross-check its
|
|
152
|
+
* `requires` against the client registry's list for that template.
|
|
153
|
+
*
|
|
154
|
+
* REFUSES where `readAilkModuleManifest` degrades (spec 5481 § 3 DD4). A
|
|
155
|
+
* module's manifest fields are all optional, so an absent or unparseable one
|
|
156
|
+
* legitimately means "a plain content merge". A template's `requires` is
|
|
157
|
+
* mandatory (record D10) — inheriting treat-as-absent would make the
|
|
158
|
+
* cross-check optional at the bundle's own discretion, since a bundle
|
|
159
|
+
* carrying no manifest would never be checked at all.
|
|
160
|
+
*
|
|
161
|
+
* The disagreement check is a boundary check, and the boundary is the bundle:
|
|
162
|
+
* `expectedRequires` comes from the caller's own `TEMPLATE_MANIFESTS` entry
|
|
163
|
+
* (internal, compile-checked against `RECOGNIZED_MODULES`), while `requires`
|
|
164
|
+
* is whatever the archive says. Compared as SETS, not as sequences — the
|
|
165
|
+
* registry's declaration order is not part of the contract.
|
|
166
|
+
*/
|
|
167
|
+
function readAilkTemplateManifest(sourceRoot, name, expectedRequires) {
|
|
168
|
+
const manifestPath = join(sourceRoot, AILK_TEMPLATE_JSON);
|
|
169
|
+
if (!existsSync(manifestPath)) {
|
|
170
|
+
throw new Error(`template "${name}": bundle carries no ${AILK_TEMPLATE_JSON} — a template manifest is mandatory`);
|
|
171
|
+
}
|
|
172
|
+
let manifest;
|
|
173
|
+
try {
|
|
174
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new Error(`template "${name}": ${AILK_TEMPLATE_JSON} is not readable JSON`);
|
|
178
|
+
}
|
|
179
|
+
const declared = manifest.requires;
|
|
180
|
+
if (!Array.isArray(declared)) {
|
|
181
|
+
throw new Error(`template "${name}": ${AILK_TEMPLATE_JSON} declares no "requires" list`);
|
|
182
|
+
}
|
|
183
|
+
const expected = [...new Set(expectedRequires)].sort();
|
|
184
|
+
const actual = [...new Set(declared)].sort();
|
|
185
|
+
if (expected.join(",") !== actual.join(",")) {
|
|
186
|
+
throw new Error(`template "${name}": bundle requires [${actual.join(", ")}] but this create-ailk's registry requires [${expected.join(", ")}] — upgrade create-ailk`);
|
|
187
|
+
}
|
|
188
|
+
return manifest;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Validate every `installMap` entry against the staged tree and the target
|
|
192
|
+
* scaffold, throwing on the first violation — called BEFORE any copy, so a
|
|
193
|
+
* bad entry rejects the whole bundle with nothing written to `targetDir`.
|
|
194
|
+
* Returns the map unchanged (validation only) for the caller to apply.
|
|
195
|
+
*/
|
|
196
|
+
function validateInstallMap(installMap, extractedRoot, targetAbs, subject) {
|
|
197
|
+
for (const [source, destination] of Object.entries(installMap)) {
|
|
198
|
+
if (hasUnsafeSegment(source)) {
|
|
199
|
+
throw new Error(`${subject}: unsafe installMap source "${source}"`);
|
|
200
|
+
}
|
|
201
|
+
const sourceAbs = resolve(extractedRoot, source);
|
|
202
|
+
if (!isPathInside(extractedRoot, sourceAbs)) {
|
|
203
|
+
throw new Error(`${subject}: installMap source "${source}" resolves outside staging`);
|
|
204
|
+
}
|
|
205
|
+
let sourceStat;
|
|
206
|
+
try {
|
|
207
|
+
sourceStat = statSync(sourceAbs);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw new Error(`${subject}: installMap source "${source}" is not a file the bundle produced`);
|
|
211
|
+
}
|
|
212
|
+
if (!sourceStat.isFile()) {
|
|
213
|
+
throw new Error(`${subject}: installMap source "${source}" is not a file the bundle produced`);
|
|
214
|
+
}
|
|
215
|
+
if (hasUnsafeSegment(destination)) {
|
|
216
|
+
throw new Error(`${subject}: unsafe installMap destination "${destination}"`);
|
|
217
|
+
}
|
|
218
|
+
const destAbs = resolve(targetAbs, destination);
|
|
219
|
+
if (!isPathInside(targetAbs, destAbs)) {
|
|
220
|
+
throw new Error(`${subject}: installMap destination "${destination}" resolves outside target`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Validate every `patches` entry against `targetDir` — the same fail-closed
|
|
226
|
+
* point and containment guard `validateInstallMap` above uses, extended
|
|
227
|
+
* (spec 5418 § 3 D6): `target` must resolve inside `targetDir`; `kind` and
|
|
228
|
+
* every `op.op` must be a recognized vocabulary member. Any violation
|
|
229
|
+
* refuses the WHOLE bundle before `copyTree` runs — this is D4 class 4
|
|
230
|
+
* (unknown kind/op), resolved here at validation time, never inside
|
|
231
|
+
* `applyModulePatches` (which never sees an invalid kind/op).
|
|
232
|
+
*/
|
|
233
|
+
function validatePatches(patches, targetAbs, subject) {
|
|
234
|
+
for (const patch of patches) {
|
|
235
|
+
if (hasUnsafeSegment(patch.target)) {
|
|
236
|
+
throw new Error(`${subject}: unsafe patch target "${patch.target}"`);
|
|
237
|
+
}
|
|
238
|
+
const targetAbsPath = resolve(targetAbs, patch.target);
|
|
239
|
+
if (!isPathInside(targetAbs, targetAbsPath)) {
|
|
240
|
+
throw new Error(`${subject}: patch target "${patch.target}" resolves outside target`);
|
|
241
|
+
}
|
|
242
|
+
const validOps = VALID_OPS_BY_KIND[patch.kind];
|
|
243
|
+
if (!validOps) {
|
|
244
|
+
throw new Error(`invalid module manifest: unknown patch kind "${patch.kind}" for target ${patch.target}`);
|
|
245
|
+
}
|
|
246
|
+
for (const op of patch.ops) {
|
|
247
|
+
if (!validOps.has(op.op)) {
|
|
248
|
+
throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${patch.target}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** C0 (0x00-0x1F) and C1 (0x7F-0x9F) control characters, newline excepted. */
|
|
254
|
+
function stripControlCharacters(text) {
|
|
255
|
+
// eslint-disable-next-line no-control-regex -- deliberately stripping control chars
|
|
256
|
+
return text.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, "");
|
|
257
|
+
}
|
|
42
258
|
/**
|
|
43
259
|
* Extract a fetched module bundle into `targetDir`, merging its files onto
|
|
44
260
|
* the tree `copyTemplate` already wrote. Throws on any unsafe entry,
|
|
45
|
-
* decompress error,
|
|
46
|
-
* failure (
|
|
47
|
-
*
|
|
261
|
+
* decompress error, limit overrun, invalid `installMap`, or `patches`
|
|
262
|
+
* failure (D4) — `targetDir` is left untouched on failure in every one of
|
|
263
|
+
* those cases: validation AND patch computation both complete before any
|
|
264
|
+
* file is copied or written into place.
|
|
48
265
|
*/
|
|
49
266
|
export async function extractModuleBundle(options) {
|
|
50
|
-
const { bytes, targetDir, moduleName } = options;
|
|
267
|
+
const { bytes, targetDir, moduleName, templateRequires } = options;
|
|
51
268
|
const targetAbs = resolve(targetDir);
|
|
52
269
|
if (!existsSync(targetAbs)) {
|
|
53
270
|
throw new Error(`extractModuleBundle: target directory does not exist: ${targetAbs}`);
|
|
@@ -58,23 +275,114 @@ export async function extractModuleBundle(options) {
|
|
|
58
275
|
writeFileSync(stagingTar, bytes);
|
|
59
276
|
const extractedRoot = join(stagingDir, "extracted");
|
|
60
277
|
mkdirSync(extractedRoot, { recursive: true });
|
|
61
|
-
await safeExtractTarGz(stagingTar, extractedRoot,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
278
|
+
await safeExtractTarGz(stagingTar, extractedRoot, templateRequires !== undefined
|
|
279
|
+
? `template "${moduleName}"`
|
|
280
|
+
: `module "${moduleName}"`);
|
|
281
|
+
installBundleTree({
|
|
282
|
+
sourceRoot: extractedRoot,
|
|
283
|
+
targetDir: targetAbs,
|
|
284
|
+
name: moduleName,
|
|
285
|
+
...(templateRequires !== undefined ? { templateRequires } : {}),
|
|
286
|
+
...(options.versions !== undefined ? { versions: options.versions } : {}),
|
|
287
|
+
});
|
|
66
288
|
}
|
|
67
289
|
finally {
|
|
68
290
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
69
291
|
}
|
|
70
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Apply one resolved manifest's `installMap`, plain content and `patches`
|
|
295
|
+
* from `sourceRoot` into `targetDir`.
|
|
296
|
+
*
|
|
297
|
+
* Extracted from `extractModuleBundle` so the Free template path — which has
|
|
298
|
+
* no archive to unpack, only a local tree — runs the SAME step (DD7). The
|
|
299
|
+
* atomicity guarantee is this function's, not its callers': validation AND
|
|
300
|
+
* patch computation both complete before the first byte is written, so a
|
|
301
|
+
* rejected tree leaves `targetDir` exactly as it was.
|
|
302
|
+
*/
|
|
303
|
+
export function installBundleTree(options) {
|
|
304
|
+
const { sourceRoot, name, templateRequires } = options;
|
|
305
|
+
const targetAbs = resolve(options.targetDir);
|
|
306
|
+
const isTemplate = templateRequires !== undefined;
|
|
307
|
+
const subject = isTemplate ? `template "${name}"` : `module "${name}"`;
|
|
308
|
+
// installMap validation runs BEFORE any copy, so a bad entry rejects the
|
|
309
|
+
// whole tree with nothing written to targetDir (same posture as the
|
|
310
|
+
// tar-entry validation in safeExtractTarGz).
|
|
311
|
+
const manifest = isTemplate
|
|
312
|
+
? readAilkTemplateManifest(sourceRoot, name, templateRequires)
|
|
313
|
+
: readAilkModuleManifest(sourceRoot);
|
|
314
|
+
const installMap = manifest?.installMap ?? {};
|
|
315
|
+
validateInstallMap(installMap, sourceRoot, targetAbs, subject);
|
|
316
|
+
// patches validation (spec 5418 § 3 D6) runs at the same point, before
|
|
317
|
+
// any copy — a bad target/kind/op rejects the whole tree with nothing
|
|
318
|
+
// written to targetDir, same posture as installMap and the tar-entry
|
|
319
|
+
// validation above.
|
|
320
|
+
const patches = manifest?.patches ?? [];
|
|
321
|
+
validatePatches(patches, targetAbs, subject);
|
|
322
|
+
// Compute every patch target's new content BEFORE any write to
|
|
323
|
+
// targetDir — including before copyTree/installMap below, not only
|
|
324
|
+
// before the patch step's own write (spec 5418 § 3 D4/§ 6.2): a
|
|
325
|
+
// conflict, missing-target, or unparseable-target failure must leave
|
|
326
|
+
// the scaffold exactly as it was, which includes the bundle's own
|
|
327
|
+
// plain content and installMap destinations never landing either. A
|
|
328
|
+
// patch target the bundle itself delivers (as plain content or as an
|
|
329
|
+
// installMap destination) is read from the STAGED tree here, since
|
|
330
|
+
// targetDir would not have it yet at this point.
|
|
331
|
+
const installMapDestinationToSource = new Map(Object.entries(installMap).map(([source, destination]) => [destination, source]));
|
|
332
|
+
const readPatchTarget = (target) => {
|
|
333
|
+
const mappedSource = installMapDestinationToSource.get(target);
|
|
334
|
+
if (mappedSource !== undefined) {
|
|
335
|
+
const sourceAbs = resolve(sourceRoot, mappedSource);
|
|
336
|
+
return existsSync(sourceAbs) ? readFileSync(sourceAbs, "utf8") : undefined;
|
|
337
|
+
}
|
|
338
|
+
const stagedAbs = resolve(sourceRoot, target);
|
|
339
|
+
if (existsSync(stagedAbs) && statSync(stagedAbs).isFile()) {
|
|
340
|
+
return readFileSync(stagedAbs, "utf8");
|
|
341
|
+
}
|
|
342
|
+
const targetAbsPath = resolve(targetAbs, target);
|
|
343
|
+
return existsSync(targetAbsPath) ? readFileSync(targetAbsPath, "utf8") : undefined;
|
|
344
|
+
};
|
|
345
|
+
const computedPatches = computeModulePatches(patches, {
|
|
346
|
+
versions: options.versions ?? {},
|
|
347
|
+
readTarget: readPatchTarget,
|
|
348
|
+
});
|
|
349
|
+
// Full validation AND patch computation passed — only now does anything
|
|
350
|
+
// get written to targetDir. Copies only (never deletes anything): a
|
|
351
|
+
// fetched module's or template's files ADD to the free scaffold, they
|
|
352
|
+
// never remove what the free copy already wrote. The root manifest and
|
|
353
|
+
// every installMap source are excluded from the whole-tree copy — the
|
|
354
|
+
// manifest never lands in the scaffold, and a mapped source lands ONLY
|
|
355
|
+
// at its mapped destination, never also at its original path.
|
|
356
|
+
const excluded = new Set([
|
|
357
|
+
isTemplate ? AILK_TEMPLATE_JSON : AILK_MODULE_JSON,
|
|
358
|
+
...Object.keys(installMap),
|
|
359
|
+
]);
|
|
360
|
+
copyTree(sourceRoot, targetAbs, excluded);
|
|
361
|
+
for (const [source, destination] of Object.entries(installMap)) {
|
|
362
|
+
const destAbs = resolve(targetAbs, destination);
|
|
363
|
+
mkdirSync(join(destAbs, ".."), { recursive: true });
|
|
364
|
+
copyFileSync(resolve(sourceRoot, source), destAbs);
|
|
365
|
+
}
|
|
366
|
+
// patches write AFTER every file the tree brought is in place (D5) —
|
|
367
|
+
// after installMap, before postInstall — so a patch can never race a
|
|
368
|
+
// file its own ownedFiles/installMap just delivered, and (since it was
|
|
369
|
+
// COMPUTED above, before either ran) overwrites rather than conflicts
|
|
370
|
+
// with content copyTree/installMap just placed at the same path.
|
|
371
|
+
writeModulePatches(computedPatches, targetAbs);
|
|
372
|
+
if (manifest?.postInstall) {
|
|
373
|
+
process.stdout.write(`${stripControlCharacters(manifest.postInstall)}\n`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
71
376
|
/**
|
|
72
377
|
* Recursively copy every file from `srcRoot` into `destRoot`, creating parent
|
|
73
|
-
* directories as needed
|
|
74
|
-
*
|
|
75
|
-
*
|
|
378
|
+
* directories as needed, except any path in `excluded` (relative to
|
|
379
|
+
* `srcRoot`, POSIX-normalized) — the bundle manifest and any `installMap`
|
|
380
|
+
* source, which land only at their mapped destination. Runs ONLY after
|
|
381
|
+
* `safeExtractTarGz` has validated every entry, so no additional
|
|
382
|
+
* path-safety check is needed here — `srcRoot` is our own staging tree, not
|
|
383
|
+
* attacker-controlled input.
|
|
76
384
|
*/
|
|
77
|
-
function copyTree(srcRoot, destRoot) {
|
|
385
|
+
function copyTree(srcRoot, destRoot, excluded = new Set()) {
|
|
78
386
|
const stack = [srcRoot];
|
|
79
387
|
while (stack.length > 0) {
|
|
80
388
|
const dir = stack.pop();
|
|
@@ -89,6 +397,8 @@ function copyTree(srcRoot, destRoot) {
|
|
|
89
397
|
stack.push(srcPath);
|
|
90
398
|
}
|
|
91
399
|
else if (entry.isFile()) {
|
|
400
|
+
if (excluded.has(rel.split(sep).join("/")))
|
|
401
|
+
continue;
|
|
92
402
|
mkdirSync(join(destPath, ".."), { recursive: true });
|
|
93
403
|
copyFileSync(srcPath, destPath);
|
|
94
404
|
}
|
|
@@ -101,14 +411,14 @@ function copyTree(srcRoot, destRoot) {
|
|
|
101
411
|
* file doc for why this is a port and not an import). Extracts `tarGzPath`
|
|
102
412
|
* into `targetDir`, which the caller guarantees is empty.
|
|
103
413
|
*/
|
|
104
|
-
async function safeExtractTarGz(tarGzPath, targetDir,
|
|
414
|
+
async function safeExtractTarGz(tarGzPath, targetDir, subject) {
|
|
105
415
|
const resolvedTarget = resolve(targetDir);
|
|
106
416
|
let entryCount = 0;
|
|
107
417
|
let totalSize = 0;
|
|
108
418
|
const captured = { error: null };
|
|
109
419
|
const reject = (msg) => {
|
|
110
420
|
if (captured.error === null) {
|
|
111
|
-
captured.error = new Error(
|
|
421
|
+
captured.error = new Error(`${subject}: ${msg}`);
|
|
112
422
|
}
|
|
113
423
|
return false;
|
|
114
424
|
};
|
|
@@ -171,14 +481,3 @@ async function safeExtractTarGz(tarGzPath, targetDir, moduleName) {
|
|
|
171
481
|
throw captured.error;
|
|
172
482
|
}
|
|
173
483
|
}
|
|
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
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://ailaunchkit.ai/api/templates";
|
|
2
|
+
export interface FetchTemplateOptions {
|
|
3
|
+
templateName: string;
|
|
4
|
+
/** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
|
|
5
|
+
token: string;
|
|
6
|
+
/** Registry root; `<name>/<version>` is appended as two path segments. */
|
|
7
|
+
registryUrl?: string;
|
|
8
|
+
/** The template version to request. Defaults to this package's own version. */
|
|
9
|
+
version?: string;
|
|
10
|
+
/** Override for tests; defaults to `globalThis.fetch`. Never a live call in CI. */
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
export declare class FetchTemplateError extends Error {
|
|
14
|
+
/** True iff the failure was a license-gating signal (HTTP 401 or 403). */
|
|
15
|
+
readonly isLicenseError: boolean;
|
|
16
|
+
constructor(message: string,
|
|
17
|
+
/** True iff the failure was a license-gating signal (HTTP 401 or 403). */
|
|
18
|
+
isLicenseError?: boolean);
|
|
19
|
+
}
|
|
20
|
+
export declare function fetchTemplate(options: FetchTemplateOptions): Promise<Uint8Array>;
|