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.
- package/component-catalog.json +11939 -0
- package/component-catalog.md +176 -64
- package/dist/cli.js +48 -10
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/extract-module-bundle.d.ts +88 -1
- package/dist/lib/extract-module-bundle.js +375 -80
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.d.ts +20 -0
- package/dist/lib/fetch-template.js +133 -0
- package/dist/lib/module-license-gate.d.ts +8 -0
- package/dist/lib/module-license-gate.js +12 -4
- package/dist/module-architecture.d.ts +116 -7
- package/dist/module-architecture.js +465 -94
- package/dist/parse-args.d.ts +8 -1
- package/dist/parse-args.js +49 -2
- package/dist/programmatic.d.ts +36 -1
- package/dist/programmatic.js +216 -5
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +140 -23
- package/dist/template-manifest.d.ts +144 -0
- package/dist/template-manifest.js +337 -0
- package/dist/template-names.d.ts +41 -0
- package/dist/template-names.js +45 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
- package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
- package/site-templates/marketing-site-free/ailk-template.json +9 -0
- package/site-templates/marketing-site-free/provenance.json +4 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
- package/site-templates/minimalism-site/ailk-template.json +21 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +32 -7
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/CLAUDE.md +4 -1
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/package.json +6 -2
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +173 -0
- package/templates/apps/api/src/config/modules.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/server.ts +122 -3
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/default-tenant.ts +261 -0
- package/templates/apps/api/src/services/lead-promotion.ts +161 -0
- package/templates/apps/api/src/services/leads-inbox.ts +263 -0
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/mcp/CLAUDE.md +1 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/package.json +6 -2
- package/templates/apps/mcp/src/server.ts +2 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/index.ts +2 -2
- package/templates/apps/web/.env.example +3 -3
- package/templates/apps/web/CLAUDE.md +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +214 -17
- 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/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +12 -15
- package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +29 -13
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
- package/templates/apps/web/messages/en.json +86 -1
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +6 -2
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- /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
|
+
}
|
package/dist/lib/fetch-module.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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/
|
|
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]/
|
|
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. `
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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[];
|