create-ailk 0.3.0 → 0.5.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.
Files changed (155) hide show
  1. package/component-catalog.json +11939 -0
  2. package/component-catalog.md +176 -64
  3. package/dist/cli.js +48 -10
  4. package/dist/derive-architecture.js +5 -2
  5. package/dist/lib/extract-module-bundle.d.ts +88 -1
  6. package/dist/lib/extract-module-bundle.js +375 -80
  7. package/dist/lib/fetch-guard.d.ts +28 -0
  8. package/dist/lib/fetch-guard.js +101 -0
  9. package/dist/lib/fetch-module.js +4 -0
  10. package/dist/lib/fetch-template.d.ts +20 -0
  11. package/dist/lib/fetch-template.js +133 -0
  12. package/dist/lib/module-license-gate.d.ts +8 -0
  13. package/dist/lib/module-license-gate.js +12 -4
  14. package/dist/module-architecture.d.ts +116 -7
  15. package/dist/module-architecture.js +465 -94
  16. package/dist/parse-args.d.ts +8 -1
  17. package/dist/parse-args.js +49 -2
  18. package/dist/programmatic.d.ts +36 -1
  19. package/dist/programmatic.js +216 -5
  20. package/dist/site-chrome.d.ts +80 -0
  21. package/dist/site-chrome.js +135 -0
  22. package/dist/surfaces.d.ts +21 -6
  23. package/dist/surfaces.js +140 -23
  24. package/dist/template-manifest.d.ts +144 -0
  25. package/dist/template-manifest.js +337 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +45 -0
  28. package/package.json +10 -2
  29. package/site-templates/_base/ailk-template.json +6 -0
  30. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  31. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
  35. package/site-templates/marketing-site/ailk-template.json +11 -0
  36. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  37. package/site-templates/marketing-site/provenance.json +4 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  39. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
  40. package/site-templates/marketing-site-free/ailk-template.json +9 -0
  41. package/site-templates/marketing-site-free/provenance.json +4 -0
  42. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
  43. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
  44. package/site-templates/minimalism-site/ailk-template.json +21 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  49. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  50. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  51. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  53. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  54. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  55. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  56. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  57. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  58. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  60. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  65. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  68. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  69. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  70. package/site-templates/minimalism-site/provenance.json +4 -0
  71. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  72. package/site-templates/saas-product/ailk-template.json +16 -0
  73. package/site-templates/saas-product/provenance.json +10 -0
  74. package/templates/.claude/skills/README.md +16 -25
  75. package/templates/.env.example +32 -7
  76. package/templates/.github/workflows/ci.yml +48 -0
  77. package/templates/README.md +3 -1
  78. package/templates/apps/api/CLAUDE.md +4 -1
  79. package/templates/apps/api/eslint.config.mjs +69 -0
  80. package/templates/apps/api/package.json +6 -2
  81. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
  82. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  83. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +173 -0
  84. package/templates/apps/api/src/config/modules.ts +1 -1
  85. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  86. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
  87. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
  88. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  89. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  90. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  91. package/templates/apps/api/src/server.ts +122 -3
  92. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  93. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  94. package/templates/apps/api/src/services/default-tenant.ts +261 -0
  95. package/templates/apps/api/src/services/lead-promotion.ts +161 -0
  96. package/templates/apps/api/src/services/leads-inbox.ts +263 -0
  97. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  98. package/templates/apps/mcp/CLAUDE.md +1 -1
  99. package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
  100. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  101. package/templates/apps/mcp/eslint.config.mjs +69 -0
  102. package/templates/apps/mcp/package.json +6 -2
  103. package/templates/apps/mcp/src/server.ts +2 -2
  104. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  105. package/templates/apps/mcp/src/tools/index.ts +2 -2
  106. package/templates/apps/web/.env.example +3 -3
  107. package/templates/apps/web/CLAUDE.md +2 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
  109. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
  110. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  111. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  112. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  113. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  114. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  115. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  116. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  117. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  118. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  119. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  120. package/templates/apps/web/app/[locale]/layout.tsx +214 -17
  121. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  122. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  123. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  124. package/templates/apps/web/components/google-analytics.tsx +18 -1
  125. package/templates/apps/web/eslint.config.mjs +69 -0
  126. package/templates/apps/web/jest.config.cjs +12 -15
  127. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  128. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
  129. package/templates/apps/web/lib/nav-config.ts +27 -7
  130. package/templates/apps/web/lib/server-api.ts +29 -13
  131. package/templates/apps/web/lib/site-chrome.ts +29 -0
  132. package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
  133. package/templates/apps/web/messages/en.json +86 -1
  134. package/templates/apps/web/next.config.mjs +20 -0
  135. package/templates/apps/web/package.json +6 -2
  136. package/templates/apps/web/sentry.client.config.ts +131 -0
  137. package/templates/apps/web/sentry.edge.config.ts +72 -0
  138. package/templates/apps/web/sentry.server.config.ts +72 -0
  139. package/templates/apps/web/src/lib/routes.ts +13 -18
  140. package/templates/brand_assets/README.md +24 -0
  141. package/templates/content/en/AboutPage/about.mdx +160 -15
  142. package/templates/content/en/ContactPage/contact.mdx +90 -13
  143. package/templates/content/en/FAQPage/faq.mdx +62 -22
  144. package/templates/content/en/HomePage/home.mdx +244 -20
  145. package/templates/content/en/LandingPage/blog.mdx +83 -0
  146. package/templates/content/en/LandingPage/features.mdx +103 -0
  147. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  148. package/templates/database/CHANGELOG.md +21 -0
  149. package/templates/database/__tests__/seed.test.ts +1 -1
  150. package/templates/database/eslint.config.mjs +69 -0
  151. package/templates/database/package.json +7 -3
  152. package/templates/database/seed.ts +1 -1
  153. package/templates/package.json +1 -14
  154. package/versions.json +52 -0
  155. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared network-safety guards for `fetch-template.ts` / `fetch-module.ts`
3
+ * (issue #5514, Lows 2 and 5 — a post-merge backstop on #5510's
4
+ * `create-ailk --template` feature).
5
+ *
6
+ * A plain internal helper module, not a shared package: both callers already
7
+ * state at length (see their file docs) why `create-ailk` — the one package
8
+ * in this monorepo published to public npm — cannot take a runtime
9
+ * dependency on anything `restricted`-tier. A sibling file inside
10
+ * `packages/create-ailk/src/lib/` carries no such dependency and is exactly
11
+ * the right place for logic the two fetchers would otherwise duplicate a
12
+ * second time.
13
+ *
14
+ * Three guards live here:
15
+ * - `REQUEST_TIMEOUT_MS` / `fetchTimeoutSignal()` — neither fetcher had a
16
+ * request timeout before this; a hung or slow-drip response could block
17
+ * `create-ailk` forever.
18
+ * - `assertHttpsRegistryUrl()` — neither fetcher validated the scheme of
19
+ * the registry URL (env-var-overridable) it was about to send a bearer
20
+ * JWT to. Rejects any non-`https:` origin, UNLESS the test-mode seam
21
+ * (`NODE_ENV === "test"`) is active — the same seam
22
+ * `template-scaffold.test.ts`'s `run()` helper already relies on when it
23
+ * spreads `process.env` into the CLI subprocess it spawns against the
24
+ * loopback mock registry (`http://127.0.0.1:<port>`). Confirmed
25
+ * empirically: Jest sets `NODE_ENV=test` for its own process (and thus
26
+ * every child it spawns, since `run()` inherits `process.env`) whenever
27
+ * the ambient environment does not already set it — there is no other
28
+ * test-mode flag anywhere in this codebase (`AILK_ALLOW*`, `insecure`,
29
+ * `__TEST__`, etc. do not exist).
30
+ * - `assertContentLengthWithinLimit()` — neither fetcher checked a
31
+ * response's declared size against `MAX_TOTAL_SIZE_BYTES` before buffering
32
+ * the WHOLE body via `.arrayBuffer()`. That cap was previously enforced
33
+ * only downstream, against tar entries, in
34
+ * `extract-module-bundle.ts`'s `safeExtractTarGz` — too late to stop an
35
+ * oversized response from being held in memory first.
36
+ */
37
+ import { MAX_TOTAL_SIZE_BYTES } from "./extract-module-bundle.js";
38
+ /**
39
+ * Per-request timeout applied to every template/module registry fetch.
40
+ * 30s is generous for a small tarball fetch over a real network while still
41
+ * bounding a hung connection or slow-drip response.
42
+ */
43
+ export const REQUEST_TIMEOUT_MS = 30_000;
44
+ /** `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` — one call site, one constant. */
45
+ export function fetchTimeoutSignal() {
46
+ return AbortSignal.timeout(REQUEST_TIMEOUT_MS);
47
+ }
48
+ /**
49
+ * `true` iff `hostname` is a loopback host — the ONLY hosts a plain `http:`
50
+ * registry URL is accepted for. The test suite spawns its mock registry on
51
+ * `127.0.0.1`; nothing else needs cleartext. (An earlier revision keyed this
52
+ * on `NODE_ENV=test`, an ambient variable any environment can export — a
53
+ * security control in a public npm package must not carry a kill switch.)
54
+ */
55
+ function isLoopbackHost(hostname) {
56
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
57
+ }
58
+ /**
59
+ * Reject a registry URL whose origin is not `https:`, unless its host is
60
+ * loopback (above) — the mock registry the test suite spawns is deliberately
61
+ * plain `http://127.0.0.1:<port>`. `ErrorCtor` lets each
62
+ * caller throw its own error class (`FetchTemplateError` / `FetchModuleError`)
63
+ * so this is a new FAILURE MODE, never a new error type from the caller's
64
+ * point of view — and never a licence error (`isLicenseError` stays false).
65
+ */
66
+ export function assertHttpsRegistryUrl(registryUrl, ErrorCtor) {
67
+ let scheme;
68
+ let hostname;
69
+ try {
70
+ const parsed = new URL(registryUrl);
71
+ scheme = parsed.protocol;
72
+ hostname = parsed.hostname;
73
+ }
74
+ catch {
75
+ throw new ErrorCtor(`refusing to use registry URL "${registryUrl}": not a valid URL`);
76
+ }
77
+ if (scheme !== "https:" && !isLoopbackHost(hostname)) {
78
+ throw new ErrorCtor(`refusing to use registry URL "${registryUrl}": scheme must be https:, got "${scheme}"`);
79
+ }
80
+ }
81
+ /**
82
+ * Reject a `Response` whose `Content-Length` header declares a size larger
83
+ * than `MAX_TOTAL_SIZE_BYTES` — called by the caller right after its
84
+ * existing status-code checks succeed, BEFORE `.arrayBuffer()` ever runs, so
85
+ * an oversized body is never buffered into memory. A missing or unparseable
86
+ * `Content-Length` is not itself a violation (a chunked/streamed response may
87
+ * omit it) — this is defense in depth against a declared size, not a
88
+ * replacement for the tar-entry cumulative-size cap `extract-module-bundle.ts`
89
+ * still enforces downstream.
90
+ */
91
+ export function assertContentLengthWithinLimit(response, ErrorCtor) {
92
+ const header = response.headers.get("content-length");
93
+ if (header === null)
94
+ return;
95
+ const declared = Number(header);
96
+ if (!Number.isFinite(declared))
97
+ return;
98
+ if (declared > MAX_TOTAL_SIZE_BYTES) {
99
+ throw new ErrorCtor(`refusing response: declared Content-Length ${declared} exceeds the ${MAX_TOTAL_SIZE_BYTES}-byte limit`);
100
+ }
101
+ }
@@ -36,6 +36,7 @@
36
36
  * exit uniformly.
37
37
  */
38
38
  import pkg from "../../package.json" with { type: "json" };
39
+ import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
39
40
  export const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/api/modules";
40
41
  /**
41
42
  * This package's own `package.json` `"version"`. Deliberately a static JSON
@@ -79,6 +80,7 @@ export async function fetchModule(options) {
79
80
  const registryUrl = options.registryUrl ??
80
81
  process.env["AILK_MODULE_REGISTRY_URL"] ??
81
82
  DEFAULT_AILK_MODULE_REGISTRY_URL;
83
+ assertHttpsRegistryUrl(registryUrl, FetchModuleError);
82
84
  const url = buildUrl(registryUrl, moduleName, version);
83
85
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
84
86
  let response;
@@ -90,6 +92,7 @@ export async function fetchModule(options) {
90
92
  Accept: "application/gzip, application/octet-stream",
91
93
  },
92
94
  redirect: "error",
95
+ signal: fetchTimeoutSignal(),
93
96
  });
94
97
  }
95
98
  catch (err) {
@@ -111,6 +114,7 @@ export async function fetchModule(options) {
111
114
  const body = await safeReadBody(response);
112
115
  throw new FetchModuleError(`module registry returned ${response.status} for "${moduleName}": ${body}`);
113
116
  }
117
+ assertContentLengthWithinLimit(response, FetchModuleError);
114
118
  const buf = await response.arrayBuffer();
115
119
  return new Uint8Array(buf);
116
120
  }
@@ -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>;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Authenticated site-template-bundle fetch (spec 5481 § 4; parent record
3
+ * D1, § 6).
4
+ *
5
+ * The client half of `GET /v1/templates/:name/:version` (#5480's route),
6
+ * reached publicly at `/api/templates/:path*`. Posture is
7
+ * `fetch-module.ts`'s, mirrored one-for-one rather than generalised into a
8
+ * shared helper: bearer-JWT auth, `redirect: "error"` so a redirected
9
+ * response cannot drop the bearer token onto an unintended host, an
10
+ * empty-token guard before any network call, and the same status mapping —
11
+ * 401 → licence invalid/revoked/lapsed, 403 → not included / term ended,
12
+ * 404 → content not yet available, other non-2xx → the upstream body.
13
+ *
14
+ * ONE word differs, and it is the whole reason this is a sibling rather
15
+ * than a second call site of `fetchModule` (spec 5481 § 3 DD5): the 404
16
+ * sentence names its subject, and the subject here is a TEMPLATE. Parent
17
+ * record D17 makes that sentence a specified outcome rather than an
18
+ * incidental one, so the fourth refusal reuses it — with the noun
19
+ * parameterised, since left alone it would call the template a module.
20
+ *
21
+ * Deliberately depends on neither `@working-theory/license` nor
22
+ * `@working-theory/cli`, for the reason `fetch-module.ts`'s file doc states
23
+ * at length: both are `publishConfig.access: "restricted"` and this is the
24
+ * one package published to PUBLIC npm, so a runtime dependency on either
25
+ * would break `pnpm install` for every Free consumer rather than merely
26
+ * gate the paid path. The registry's own 401 IS the licence verdict.
27
+ */
28
+ import pkg from "../../package.json" with { type: "json" };
29
+ import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
30
+ export const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://ailaunchkit.ai/api/templates";
31
+ /**
32
+ * This package's own `package.json` `"version"` — a static JSON import for
33
+ * the reason `fetch-module.ts`'s `CREATE_AILK_VERSION` doc gives: `import.meta`
34
+ * is a SyntaxError under ts-jest's CommonJS transform, and this file is
35
+ * reachable from jest. A template bundle overlays the scaffold this
36
+ * `create-ailk` just wrote, so the default version request is the running
37
+ * package's own.
38
+ */
39
+ const CREATE_AILK_VERSION = pkg.version;
40
+ export class FetchTemplateError extends Error {
41
+ isLicenseError;
42
+ constructor(message,
43
+ /** True iff the failure was a license-gating signal (HTTP 401 or 403). */
44
+ isLicenseError = false) {
45
+ super(message);
46
+ this.isLicenseError = isLicenseError;
47
+ this.name = "FetchTemplateError";
48
+ }
49
+ }
50
+ /**
51
+ * Join `registryUrl` with `templateName` and `version` as two trailing path
52
+ * segments. `encodeURIComponent` on each is defense in depth —
53
+ * `parse-args.ts` already restricts `templateName` to `TEMPLATE_MANIFESTS`'s
54
+ * keys, but a hostile value can never smuggle a path separator or query
55
+ * string here regardless.
56
+ */
57
+ function buildUrl(registryUrl, templateName, version) {
58
+ const trimmed = registryUrl.replace(/\/+$/, "");
59
+ return `${trimmed}/${encodeURIComponent(templateName)}/${encodeURIComponent(version)}`;
60
+ }
61
+ export async function fetchTemplate(options) {
62
+ const { templateName, token } = options;
63
+ if (token.length === 0) {
64
+ throw new FetchTemplateError("internal error: fetchTemplate called without a token");
65
+ }
66
+ const version = options.version ?? CREATE_AILK_VERSION;
67
+ const registryUrl = options.registryUrl ??
68
+ process.env["AILK_TEMPLATE_REGISTRY_URL"] ??
69
+ DEFAULT_AILK_TEMPLATE_REGISTRY_URL;
70
+ assertHttpsRegistryUrl(registryUrl, FetchTemplateError);
71
+ const url = buildUrl(registryUrl, templateName, version);
72
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
73
+ let response;
74
+ try {
75
+ response = await fetchImpl(url, {
76
+ method: "GET",
77
+ headers: {
78
+ Authorization: `Bearer ${token}`,
79
+ Accept: "application/gzip, application/octet-stream",
80
+ },
81
+ redirect: "error",
82
+ signal: fetchTimeoutSignal(),
83
+ });
84
+ }
85
+ catch (err) {
86
+ const detail = err instanceof Error ? err.message : String(err);
87
+ throw new FetchTemplateError(`template registry unreachable: ${detail}`);
88
+ }
89
+ if (response.status === 401) {
90
+ throw new FetchTemplateError(`your licence does not cover the "${templateName}" template (invalid, revoked, or lapsed)`,
91
+ /* isLicenseError */ true);
92
+ }
93
+ if (response.status === 403) {
94
+ throw new FetchTemplateError(`your licence does not include the "${templateName}" template, or its term ended before create-ailk ${version} was released — use a create-ailk version released during your term`,
95
+ /* isLicenseError */ true);
96
+ }
97
+ if (response.status === 404) {
98
+ throw new FetchTemplateError(`"${templateName}" template content not yet available — ping us at hello@ailaunchkit.ai`);
99
+ }
100
+ if (!response.ok) {
101
+ const body = await safeReadBody(response);
102
+ throw new FetchTemplateError(`template registry returned ${response.status} for "${templateName}": ${body}`);
103
+ }
104
+ assertContentLengthWithinLimit(response, FetchTemplateError);
105
+ const buf = await response.arrayBuffer();
106
+ return new Uint8Array(buf);
107
+ }
108
+ /**
109
+ * Best-effort error body extraction — the registry uses `{ error: "..." }`
110
+ * shapes, falls back to text, and swallows any read failure so the caller's
111
+ * message stays useful. `fetch-module.ts`'s helper of the same name.
112
+ */
113
+ async function safeReadBody(response) {
114
+ try {
115
+ const text = await response.text();
116
+ try {
117
+ const parsed = JSON.parse(text);
118
+ if (parsed !== null &&
119
+ typeof parsed === "object" &&
120
+ "error" in parsed &&
121
+ typeof parsed.error === "string") {
122
+ return parsed.error;
123
+ }
124
+ }
125
+ catch {
126
+ /* not JSON — fall through to raw text */
127
+ }
128
+ return text || response.statusText;
129
+ }
130
+ catch {
131
+ return response.statusText;
132
+ }
133
+ }
@@ -39,6 +39,14 @@ export interface ResolvePaidModulesOptions {
39
39
  version?: string;
40
40
  /** Forwarded to `fetchModule`; defaults to `globalThis.fetch`. Never a live call in tests. */
41
41
  fetchImpl?: typeof fetch;
42
+ /**
43
+ * The site template whose `requires` put these modules in `paidModules`
44
+ * (spec 5481 § 3 DD5 rows 1-2). Present ⟹ every refusal below names it
45
+ * alongside the specific module, because the buyer asked for a TEMPLATE and
46
+ * a message naming only a module they never typed does not tell them what
47
+ * to do. Absent ⟹ a plain `--modules` run, whose messages are unchanged.
48
+ */
49
+ templateName?: string;
42
50
  }
43
51
  /**
44
52
  * Resolve every requested paid module to its fetched bundle bytes, or throw
@@ -28,7 +28,7 @@ export async function resolvePaidModules(options) {
28
28
  }
29
29
  const token = options.licenseKey ?? process.env["AILK_LICENSE_KEY"];
30
30
  if (token === undefined || token.length === 0) {
31
- throw new LicenseRequiredError(conversionHint(paidModules));
31
+ throw new LicenseRequiredError(conversionHint(paidModules, options.templateName));
32
32
  }
33
33
  const bundles = new Map();
34
34
  for (const moduleName of paidModules) {
@@ -49,7 +49,12 @@ export async function resolvePaidModules(options) {
49
49
  catch (err) {
50
50
  if (err instanceof FetchModuleError) {
51
51
  if (err.isLicenseError) {
52
- throw new LicenseRequiredError(`${err.message} → ${CONVERSION_HINT_URL}`);
52
+ // DD5 row 2: the mapped 401/403 already names the module the key
53
+ // does not cover; the template is appended so the buyer can see
54
+ // WHICH of the things they asked for needs it.
55
+ throw new LicenseRequiredError(options.templateName !== undefined
56
+ ? `${err.message}, required by template "${options.templateName}" → ${CONVERSION_HINT_URL}`
57
+ : `${err.message} → ${CONVERSION_HINT_URL}`);
53
58
  }
54
59
  // Not a license-gating signal (e.g. 404 "not yet available", or the
55
60
  // registry unreachable) — a generic failure, not the honest-denial
@@ -62,7 +67,10 @@ export async function resolvePaidModules(options) {
62
67
  }
63
68
  return bundles;
64
69
  }
65
- function conversionHint(paidModules) {
70
+ function conversionHint(paidModules, templateName) {
66
71
  const list = paidModules.join(", ");
67
- return `Pro module(s) requested (${list}) — set AILK_LICENSE_KEY first → ${CONVERSION_HINT_URL}`;
72
+ const requested = templateName !== undefined
73
+ ? `template "${templateName}" requires Pro module(s) (${list})`
74
+ : `Pro module(s) requested (${list})`;
75
+ return `${requested} — set AILK_LICENSE_KEY first → ${CONVERSION_HINT_URL}`;
68
76
  }
@@ -29,6 +29,39 @@ export interface ArchNamespaces {
29
29
  members: Record<string, ArchNamespaceMember>;
30
30
  rules: string[];
31
31
  }
32
+ /**
33
+ * One page a site template's roster declares (spec 5478 § D1).
34
+ *
35
+ * Both producers emit this same row type: a template's BASE roster
36
+ * (`TemplateManifest.roster`, template-manifest.ts) and a module's
37
+ * `pageContributions` below. One vocabulary, so the merge has exactly one
38
+ * collision key to reconcile rather than two (§ D1/D2).
39
+ *
40
+ * Declared HERE rather than in `template-manifest.ts` (§ D3) because
41
+ * `ModuleSlice` carries it and `template-manifest.ts` already imports
42
+ * `MODULE_SLICES` + `normalizeModules` from this file — so the dependency
43
+ * stays a DAG (`parse-args` ← `module-architecture` ← `template-manifest`)
44
+ * instead of becoming a cycle in the value graph.
45
+ *
46
+ * A row carries no nav label: `GATEABLE_SURFACES` already carries
47
+ * `navLabelKey?` per surface (`surfaces.ts`) and the nav is derived from the
48
+ * roster (parent spec 5462 § D9), so a label here would be a second authority
49
+ * for one fact.
50
+ */
51
+ export interface PageRosterRow {
52
+ /**
53
+ * Site-relative route path, e.g. `/blog` or `/flows/[slug]/start`. This is
54
+ * the row's IDENTITY and the merge's collision key (§ D2): a path claimed by
55
+ * two sources is refused at build time, never resolved by a precedence rule
56
+ * (parent § D8).
57
+ */
58
+ route: string;
59
+ /**
60
+ * Repo-relative seed content this row renders. Absent for an API-backed row
61
+ * (`/meet` renders from the scheduling API, not from an MDX file).
62
+ */
63
+ content?: readonly string[];
64
+ }
32
65
  /**
33
66
  * The architecture fragment one module contributes.
34
67
  *
@@ -72,7 +105,7 @@ export interface ModuleSlice {
72
105
  * disappears just because one module's slice of it does).
73
106
  *
74
107
  * One entry in the arrays below is deliberately a GLOB
75
- * (`"apps-paid/workspace/**"`) rather than enumerated file-by-file — a
108
+ * (`"apps-paid/admin/**"`) rather than enumerated file-by-file — a
76
109
  * whole app with dozens of files, owned outright by one module; every other
77
110
  * entry is a literal file
78
111
  * path. A file that a module's own docs mention but that lives inside a
@@ -95,7 +128,7 @@ export interface ModuleSlice {
95
128
  * app, never a customer opt-in (see `apps/api/src/config/modules.ts`).
96
129
  * - Two are this-list-only and the manifest simply never recorded them:
97
130
  * `apps/web/app/[locale]/docs/[[...slug]]/page.tsx` and
98
- * `apps/web/app/[locale]/schedule/confirmed/page.tsx`.
131
+ * `apps/web/app/[locale]/meet/confirmed/page.tsx`.
99
132
  *
100
133
  * A file's TEST and STORY companions are never listed here (the
101
134
  * SOURCE-files-only rule above still holds) but DO carry the marker. They
@@ -104,18 +137,67 @@ export interface ModuleSlice {
104
137
  * which miss eleven and over-mark three.
105
138
  */
106
139
  ownedFiles?: string[];
140
+ /**
141
+ * The page rows and seed content this module contributes to any site
142
+ * template that takes it (spec 5478; parent spec 5462 § D5) — `ownedFiles`'s
143
+ * sibling, and a DIFFERENT kind of fact again from every field above.
144
+ * `ownedFiles` names which files in THIS monorepo physically belong to the
145
+ * module; `pageContributions` names which PAGES the module brings to a
146
+ * template's roster. `template-manifest.ts`'s `mergeRoster` reads it.
147
+ *
148
+ * A contribution never carries route CODE. How the route code reaches a
149
+ * scaffold is a property of the module, not of the contribution (parent
150
+ * § D6): `docs-blog` and `scheduling` list their route files in `ownedFiles`
151
+ * so the code rides their own licence-checked bundle, while
152
+ * `signal-collectors` deliberately does not — its `flows` routes reach a
153
+ * scaffold from the `templates/` snapshot, retained because
154
+ * `MODULE_SURFACES` enables the surface (see this file's
155
+ * `signal-collectors.ownedFiles` comment for why listing them there broke
156
+ * `sync-routes`, #5366). Both shapes declare their rows here identically.
157
+ *
158
+ * Unset for a module that brings no pages to any declared template:
159
+ * `marketing` (base, no `ownedFiles` and `MODULE_SURFACES.marketing` is
160
+ * `[]`), and — deferred, not absent — `multi-tenant` and `commerce`, whose
161
+ * rows are their own instances' to enumerate. An unset field contributes
162
+ * zero rows openly; it is not a partial merge.
163
+ */
164
+ pageContributions?: readonly PageRosterRow[];
107
165
  /**
108
166
  * Bundle-relative source → scaffold-relative destination (spec 5373 § D7).
109
167
  * Every source must be a file the validated tar extraction produced in the
110
168
  * bundle producer's staging directory — repo-relative, coinciding with the
111
169
  * `ownedFiles`/`installMap`-source paths this module's producer step
112
- * stages at, by construction. `extractModuleBundle` applies this map after
113
- * the tar entries are validated: an absolute path, a `..` segment, or a
114
- * path absent from staging rejects the whole bundle, as does a destination
115
- * that resolves outside the scaffold. A mapped source is copied to its
116
- * destination only, never also to its original path.
170
+ * stages at, by construction. ONE exception: `build-module-bundles.mjs`
171
+ * also stages the proprietary licence-client LICENSE at the archive root
172
+ * as `LICENSE` and adds its own `installMap["LICENSE"]` entry (never a
173
+ * slice field — the producer adds it uniformly, #5552 D1), so a slice's
174
+ * own `installMap` must not declare a `"LICENSE"` key. `extractModuleBundle`
175
+ * applies this map after the tar entries are validated: an absolute path, a
176
+ * `..` segment, or a path absent from staging rejects the whole bundle, as
177
+ * does a destination that resolves outside the scaffold. A mapped source is
178
+ * copied to its destination only, never also to its original path.
117
179
  */
118
180
  installMap?: Record<string, string>;
181
+ /**
182
+ * Rename root for this slice's whole-app glob `ownedFiles` entry(ies)
183
+ * (a `"<dir>/**"` entry — see `ownedFiles`'s own doc above) — the glob
184
+ * sibling of `installMap` (#5644 D1). Every file `expandEntry` yields
185
+ * under `<dir>/` installs at `installRoot + <rel-path-under-dir>` instead
186
+ * of its repo-relative path. `build-module-bundles.mjs`'s
187
+ * `deriveGlobInstallMap` derives one `installMap` entry per file from this
188
+ * rule at bundle-build time, and the mock registry fixture
189
+ * (`__tests__/fixtures/mock-module-registry-server.mjs`) calls the SAME
190
+ * function so its served manifest cannot drift from the producer's.
191
+ * Undefined for a slice with no glob `ownedFiles` entry, or one whose glob
192
+ * files install unchanged at their repo-relative path.
193
+ *
194
+ * `installMap` above stays the mechanism for hand-enumerated, non-glob
195
+ * remaps (see `commerce.installMap`) — a literal map is fine there because
196
+ * enumerating ten skill files costs ten lines; a whole app's worth of
197
+ * files committed one line per file into this OSS-shipped source is the
198
+ * leak class `scripts/check-oss-strip.sh` exists to catch (#5644).
199
+ */
200
+ installRoot?: string;
119
201
  /**
120
202
  * Plain text `extractModuleBundle` prints to stdout after a successful
121
203
  * merge, with C0/C1 control characters (other than newline) stripped
@@ -288,4 +370,31 @@ export declare function normalizeModules(modules: string[]): ModuleName[];
288
370
  * Passed in rather than derived from `import.meta.url`, which ts-jest cannot
289
371
  * load — the same reason `sync-routes.ts` documents for staying pure.
290
372
  */
373
+ /**
374
+ * Scaffold destinations that MORE THAN ONE module's `ownedFiles` declares —
375
+ * the co-ownership the `#5618 D4` install-time ownership registry must treat
376
+ * as intentional rather than as an overwrite (#5702 follow-up).
377
+ *
378
+ * Co-ownership is how a file reaches a scaffold under either of two modules
379
+ * without being transferred away from the other. #5677 needed it for the
380
+ * submissions review surface: the routes register under
381
+ * `multi-tenant || signal-collectors`, so BOTH bundles must be able to
382
+ * deliver them, or the module that lacks them throws
383
+ * `ERR_MODULE_NOT_FOUND` during `build()`.
384
+ *
385
+ * `validateLayerOwnership` (`lib/extract-module-bundle.ts`) refuses a
386
+ * destination an earlier layer already wrote, because a silent
387
+ * last-writer-wins between two modules that disagree is the bug `#5618 D4`
388
+ * exists to catch. Two modules that both DECLARE the same repo file are not
389
+ * that case: they stage the same bytes from the same source, so whichever
390
+ * lands second writes what is already there. Without this allowance a
391
+ * scaffold selecting both modules — `saas-product`, whose `requires` names
392
+ * `signal-collectors` AND `multi-tenant` — aborts mid-install, after the
393
+ * first module's files are on disk.
394
+ *
395
+ * Only a DECLARED overlap is allowed. A destination two layers collide on
396
+ * without both naming it in `ownedFiles` is still refused, which is the
397
+ * whole invariant.
398
+ */
399
+ export declare function coOwnedModuleDestinations(): ReadonlySet<string>;
291
400
  export declare function paidModuleTemplateExclusions(snapshotRoots: readonly string[], repoRoot: string): string[];