create-ailk 0.11.0 → 0.11.2

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 (69) hide show
  1. package/dist/content-payloads.js +2 -2
  2. package/dist/lib/scaffold-render-sweep.d.ts +16 -0
  3. package/dist/lib/scaffold-render-sweep.js +52 -3
  4. package/dist/module-architecture.js +11 -0
  5. package/package.json +1 -1
  6. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +8 -8
  7. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +8 -8
  8. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +8 -8
  9. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +8 -8
  10. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +8 -8
  11. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +8 -8
  12. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +8 -8
  13. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +8 -8
  14. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +8 -8
  15. package/templates/.env.example +13 -7
  16. package/templates/apps/api/.env.example +5 -0
  17. package/templates/apps/api/package.json +1 -1
  18. package/templates/apps/api/src/__tests__/cors.test.ts +91 -10
  19. package/templates/apps/api/src/__tests__/server.test.ts +5 -1
  20. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +135 -0
  21. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +238 -0
  22. package/templates/apps/api/src/config/index.ts +136 -0
  23. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +1 -1
  24. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +1 -1
  25. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +1 -1
  26. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +2 -2
  27. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +142 -0
  28. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +1 -1
  29. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +1 -1
  30. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +1 -1
  31. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +1 -1
  32. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +1 -1
  33. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +1 -1
  34. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +1 -1
  35. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +1 -1
  36. package/templates/apps/api/src/middleware/auth.ts +19 -21
  37. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +1 -1
  38. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +1 -1
  39. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +168 -39
  40. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -34
  41. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +1 -1
  42. package/templates/apps/api/src/server.ts +21 -16
  43. package/templates/apps/mcp/package.json +1 -1
  44. package/templates/apps/web/CLAUDE.md +1 -1
  45. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +0 -10
  46. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +0 -10
  47. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +1 -14
  48. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +0 -10
  49. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +0 -10
  50. package/templates/apps/web/app/[locale]/for/[slug]/page.tsx +0 -10
  51. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +1 -14
  52. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +0 -10
  53. package/templates/apps/web/app/[locale]/layout.tsx +57 -56
  54. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +0 -10
  55. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +0 -10
  56. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +0 -10
  57. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +1 -15
  58. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +0 -10
  59. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +0 -10
  60. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +0 -10
  61. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +1 -14
  62. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +4 -16
  63. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +3 -15
  64. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +0 -10
  65. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +1 -14
  66. package/templates/apps/web/auth.ts +73 -13
  67. package/templates/apps/web/components/sign-out-trigger.tsx +3 -2
  68. package/templates/apps/web/components/signed-in-nav.tsx +58 -0
  69. package/versions.json +0 -1
@@ -29,8 +29,8 @@ export const CONTENT_PAYLOADS = Object.freeze([
29
29
  kind: "template-set",
30
30
  name: "scaffold-commerce",
31
31
  feature: "commerce",
32
- version: "1.0.0",
33
- digest: "baa4ac6e86ec7e3b367eb67e77c3407da90c7ff9ef51d5f4e1185615194543ee",
32
+ version: "1.0.1",
33
+ digest: "112704a7910dba3275defc0cb86b1b4f33fc8f262f4c09320e8f6b3a8d873916",
34
34
  sourceRoot: "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce",
35
35
  files: Object.freeze([
36
36
  "references/lookup-keys-template.md",
@@ -103,3 +103,19 @@ export declare function assessConfigGatedRoute(input: {
103
103
  * a socket) throws rather than being silently followed or skipped.
104
104
  */
105
105
  export declare function listPublicFiles(dir: string): string[];
106
+ /** The slug the sweep requests to prove a detail route 404s an unknown slug. */
107
+ export declare const UNKNOWN_SLUG = "ailk-unknown-slug-7379";
108
+ /** `/features/[slug]` → `/features`; null unless `[slug]` is the one dynamic segment and the last. */
109
+ export declare function detailRoutePrefix(route: string): string | null;
110
+ /** The sorted basenames (sans `.mdx`) of the `*.mdx` files directly in `dir`; [] when `dir` is absent. */
111
+ export declare function listSeedSlugs(dir: string): string[];
112
+ /** An unknown slug must answer exactly 404, with no redirect. A 500 is the #7379 failure; a 200 means no 404 branch ran. */
113
+ export declare function assessUnknownSlug(input: {
114
+ route: string;
115
+ hops: RedirectHop[];
116
+ status: number | null;
117
+ }): SweepVerdict;
118
+ /** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
119
+ export declare function assessPrerenderManifest(manifest: {
120
+ dynamicRoutes?: Record<string, unknown>;
121
+ }): SweepVerdict;
@@ -13,10 +13,17 @@
13
13
  * not cost a templates build, an install and a `next build`. Each is a pure
14
14
  * function of what the driver observed, so
15
15
  * `__tests__/verify-scaffold-render-sweep.test.ts` drives them directly. The
16
- * driver loads this module from `dist/`. Only `listPublicFiles` touches the
17
- * filesystem, and none of it touches the network.
16
+ * driver loads this module from `dist/`. Only `listPublicFiles` and
17
+ * `listSeedSlugs` touch the filesystem, and none of it touches the network.
18
+ *
19
+ * #7379 added two sweeps. A starter roster's detail row that names a seed
20
+ * directory (`/features/[slug]` → `content/en/Feature`) is probed: each seed
21
+ * must render, and `UNKNOWN_SLUG` must answer 404. And the build's
22
+ * `prerender-manifest.json` must register no `/[locale]/…` route as an
23
+ * on-demand prerender — under the cookie-reading locale layout, such a route
24
+ * answers 500 for every slug, seeded or not.
18
25
  */
19
- import { readdirSync } from "node:fs";
26
+ import { existsSync, readdirSync } from "node:fs";
20
27
  import { join } from "node:path";
21
28
  /** More hops than this is a redirect chain, not a render. */
22
29
  export const MAX_REDIRECTS = 5;
@@ -234,3 +241,45 @@ function listFilesUnder(dir, relPrefix) {
234
241
  }
235
242
  return out;
236
243
  }
244
+ /** The slug the sweep requests to prove a detail route 404s an unknown slug. */
245
+ export const UNKNOWN_SLUG = "ailk-unknown-slug-7379";
246
+ /** `/features/[slug]` → `/features`; null unless `[slug]` is the one dynamic segment and the last. */
247
+ export function detailRoutePrefix(route) {
248
+ const match = /^((?:\/[^/[\]]+)+)\/\[slug\]$/.exec(route);
249
+ return match?.[1] ?? null;
250
+ }
251
+ /** The sorted basenames (sans `.mdx`) of the `*.mdx` files directly in `dir`; [] when `dir` is absent. */
252
+ export function listSeedSlugs(dir) {
253
+ if (!existsSync(dir))
254
+ return [];
255
+ return readdirSync(dir, { withFileTypes: true })
256
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".mdx"))
257
+ .map((entry) => entry.name.slice(0, -".mdx".length))
258
+ .sort();
259
+ }
260
+ /** An unknown slug must answer exactly 404, with no redirect. A 500 is the #7379 failure; a 200 means no 404 branch ran. */
261
+ export function assessUnknownSlug(input) {
262
+ const { route, hops, status } = input;
263
+ const first = hops[0];
264
+ if (first !== undefined) {
265
+ return { ok: false, reason: `${route} -> ${first.status} redirect (an unknown slug must answer 404, not redirect)` };
266
+ }
267
+ if (status === 200)
268
+ return { ok: false, reason: `${route} -> 200 (no 404 branch ran for an unknown slug)` };
269
+ if (status !== 404)
270
+ return { ok: false, reason: `${route} -> status ${status} (expected 404 for an unknown slug)` };
271
+ return { ok: true, reason: "404" };
272
+ }
273
+ /** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
274
+ export function assessPrerenderManifest(manifest) {
275
+ const locale = Object.keys(manifest.dynamicRoutes ?? {})
276
+ .filter((route) => route.startsWith("/[locale]/"))
277
+ .sort();
278
+ if (locale.length > 0) {
279
+ return {
280
+ ok: false,
281
+ reason: `on-demand prerender under the cookie-reading locale layout (500s every slug, #7379): ${locale.join(", ")}`,
282
+ };
283
+ }
284
+ return { ok: true, reason: "no /[locale] on-demand prerender route" };
285
+ }
@@ -790,6 +790,15 @@ export const MODULE_SLICES = {
790
790
  // deliverable by BOTH bundles.
791
791
  "apps/api/src/services/inbox-messages.ts",
792
792
  "apps/api/src/routes/workspaces/messages.ts",
793
+ // #7399 — the two siblings #6216 left `workspace`-only. They register
794
+ // under that same `workspace || multi-tenant` gate, so a scaffold with
795
+ // `multi-tenant` and without `workspace` loaded them with no file behind
796
+ // the specifier and 500'd every request at server construction. Co-owned
797
+ // with `workspace` (see that slice's entry below); both import only
798
+ // `lib/tenant-db.ts` (owned here) and base files.
799
+ // `__tests__/tenancy-slice-resolution.test.ts` holds the whole gate set.
800
+ "apps/api/src/routes/workspaces/general.ts",
801
+ "apps/api/src/routes/workspaces/members.ts",
793
802
  "apps/api/src/lib/tenant-db.ts",
794
803
  "apps/api/src/lib/ws-token.ts",
795
804
  // #6157 D16 — the magnet Admin authoring surface, co-owned with
@@ -890,6 +899,8 @@ export const MODULE_SLICES = {
890
899
  "apps/web/app/[locale]/(authed)/admin/page.tsx",
891
900
  "apps/web/app/[locale]/(authed)/admin/site/page.tsx",
892
901
  "apps/web/app/[locale]/(authed)/admin/team/page.tsx",
902
+ // Co-owned with `multi-tenant` (#7399) — see that slice's entry: both
903
+ // register under the workspace||multi-tenant gate.
893
904
  "apps/api/src/routes/workspaces/general.ts",
894
905
  "apps/api/src/routes/workspaces/members.ts",
895
906
  // Co-owned with `multi-tenant` (#6216) — see that slice's entry: the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ailk",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,29 +11,29 @@ schema:
11
11
  name: "Your Company"
12
12
  ---
13
13
 
14
- # {First job}
14
+ # \{First job}
15
15
 
16
- {Open with the situation a reader is in when they come looking for this. Two or
16
+ \{Open with the situation a reader is in when they come looking for this. Two or
17
17
  three sentences, no more — the detail belongs under the headings below.}
18
18
 
19
19
  ## What it covers
20
20
 
21
- - {The first thing you actually do}
22
- - {The second thing you actually do}
23
- - {The third thing you actually do}
21
+ - \{The first thing you actually do}
22
+ - \{The second thing you actually do}
23
+ - \{The third thing you actually do}
24
24
 
25
25
  ## How it runs
26
26
 
27
- {One paragraph on the shape of the visit: how it is scheduled, how long it
27
+ \{One paragraph on the shape of the visit: how it is scheduled, how long it
28
28
  takes, who turns up, and what you need from the customer beforehand.}
29
29
 
30
30
  ## What you end up with
31
31
 
32
- {One paragraph on the result. Be concrete — describe the property when the
32
+ \{One paragraph on the result. Be concrete — describe the property when the
33
33
  crew drives away.}
34
34
 
35
35
  ## What it costs
36
36
 
37
- {One line on the pricing shape — flat rate, hourly, or by the job — and what it
37
+ \{One line on the pricing shape — flat rate, hourly, or by the job — and what it
38
38
  excludes. Replace with your real numbers, or delete this section and let a
39
39
  call do it.}
@@ -11,29 +11,29 @@ schema:
11
11
  name: "Your Company"
12
12
  ---
13
13
 
14
- # {Second job}
14
+ # \{Second job}
15
15
 
16
- {Open with the situation a reader is in when they come looking for this. Two or
16
+ \{Open with the situation a reader is in when they come looking for this. Two or
17
17
  three sentences, no more — the detail belongs under the headings below.}
18
18
 
19
19
  ## What it covers
20
20
 
21
- - {The first thing you actually do}
22
- - {The second thing you actually do}
23
- - {The third thing you actually do}
21
+ - \{The first thing you actually do}
22
+ - \{The second thing you actually do}
23
+ - \{The third thing you actually do}
24
24
 
25
25
  ## How it runs
26
26
 
27
- {One paragraph on the shape of the visit: how it is scheduled, how long it
27
+ \{One paragraph on the shape of the visit: how it is scheduled, how long it
28
28
  takes, who turns up, and what you need from the customer beforehand.}
29
29
 
30
30
  ## What you end up with
31
31
 
32
- {One paragraph on the result. Be concrete — describe the property when the
32
+ \{One paragraph on the result. Be concrete — describe the property when the
33
33
  crew drives away.}
34
34
 
35
35
  ## What it costs
36
36
 
37
- {One line on the pricing shape — flat rate, hourly, or by the job — and what it
37
+ \{One line on the pricing shape — flat rate, hourly, or by the job — and what it
38
38
  excludes. Replace with your real numbers, or delete this section and let a
39
39
  call do it.}
@@ -11,29 +11,29 @@ schema:
11
11
  name: "Your Company"
12
12
  ---
13
13
 
14
- # {Third job}
14
+ # \{Third job}
15
15
 
16
- {Open with the situation a reader is in when they come looking for this. Two or
16
+ \{Open with the situation a reader is in when they come looking for this. Two or
17
17
  three sentences, no more — the detail belongs under the headings below.}
18
18
 
19
19
  ## What it covers
20
20
 
21
- - {The first thing you actually do}
22
- - {The second thing you actually do}
23
- - {The third thing you actually do}
21
+ - \{The first thing you actually do}
22
+ - \{The second thing you actually do}
23
+ - \{The third thing you actually do}
24
24
 
25
25
  ## How it runs
26
26
 
27
- {One paragraph on the shape of the visit: how it is scheduled, how long it
27
+ \{One paragraph on the shape of the visit: how it is scheduled, how long it
28
28
  takes, who turns up, and what you need from the customer beforehand.}
29
29
 
30
30
  ## What you end up with
31
31
 
32
- {One paragraph on the result. Be concrete — describe the property when the
32
+ \{One paragraph on the result. Be concrete — describe the property when the
33
33
  crew drives away.}
34
34
 
35
35
  ## What it costs
36
36
 
37
- {One line on the pricing shape — flat rate, hourly, or by the job — and what it
37
+ \{One line on the pricing shape — flat rate, hourly, or by the job — and what it
38
38
  excludes. Replace with your real numbers, or delete this section and let a
39
39
  call do it.}
@@ -10,29 +10,29 @@ schema:
10
10
  name: "Your Company"
11
11
  ---
12
12
 
13
- # {First engagement}
13
+ # \{First engagement}
14
14
 
15
- {Open with the situation a reader is in when they come looking for this. Two or
15
+ \{Open with the situation a reader is in when they come looking for this. Two or
16
16
  three sentences, no more — the detail belongs under the headings below.}
17
17
 
18
18
  ## What it covers
19
19
 
20
- - {The first thing you actually do}
21
- - {The second thing you actually do}
22
- - {The third thing you actually do}
20
+ - \{The first thing you actually do}
21
+ - \{The second thing you actually do}
22
+ - \{The third thing you actually do}
23
23
 
24
24
  ## How it runs
25
25
 
26
- {One paragraph on the shape of the engagement: how it starts, how long it runs,
26
+ \{One paragraph on the shape of the engagement: how it starts, how long it runs,
27
27
  who is involved on both sides, and what a week looks like.}
28
28
 
29
29
  ## What you end up with
30
30
 
31
- {One paragraph on the deliverable. Be concrete — name the artefact, not the
31
+ \{One paragraph on the deliverable. Be concrete — name the artefact, not the
32
32
  feeling.}
33
33
 
34
34
  ## What it costs
35
35
 
36
- {One line on the pricing shape — fixed fee, retainer, day rate — and what it
36
+ \{One line on the pricing shape — fixed fee, retainer, day rate — and what it
37
37
  excludes. Replace with your real numbers, or delete this section and let the
38
38
  call do it.}
@@ -10,29 +10,29 @@ schema:
10
10
  name: "Your Company"
11
11
  ---
12
12
 
13
- # {Second engagement}
13
+ # \{Second engagement}
14
14
 
15
- {Open with the situation a reader is in when they come looking for this. Two or
15
+ \{Open with the situation a reader is in when they come looking for this. Two or
16
16
  three sentences, no more — the detail belongs under the headings below.}
17
17
 
18
18
  ## What it covers
19
19
 
20
- - {The first thing you actually do}
21
- - {The second thing you actually do}
22
- - {The third thing you actually do}
20
+ - \{The first thing you actually do}
21
+ - \{The second thing you actually do}
22
+ - \{The third thing you actually do}
23
23
 
24
24
  ## How it runs
25
25
 
26
- {One paragraph on the shape of the engagement: how it starts, how long it runs,
26
+ \{One paragraph on the shape of the engagement: how it starts, how long it runs,
27
27
  who is involved on both sides, and what a week looks like.}
28
28
 
29
29
  ## What you end up with
30
30
 
31
- {One paragraph on the deliverable. Be concrete — name the artefact, not the
31
+ \{One paragraph on the deliverable. Be concrete — name the artefact, not the
32
32
  feeling.}
33
33
 
34
34
  ## What it costs
35
35
 
36
- {One line on the pricing shape — fixed fee, retainer, day rate — and what it
36
+ \{One line on the pricing shape — fixed fee, retainer, day rate — and what it
37
37
  excludes. Replace with your real numbers, or delete this section and let the
38
38
  call do it.}
@@ -10,29 +10,29 @@ schema:
10
10
  name: "Your Company"
11
11
  ---
12
12
 
13
- # {Third engagement}
13
+ # \{Third engagement}
14
14
 
15
- {Open with the situation a reader is in when they come looking for this. Two or
15
+ \{Open with the situation a reader is in when they come looking for this. Two or
16
16
  three sentences, no more — the detail belongs under the headings below.}
17
17
 
18
18
  ## What it covers
19
19
 
20
- - {The first thing you actually do}
21
- - {The second thing you actually do}
22
- - {The third thing you actually do}
20
+ - \{The first thing you actually do}
21
+ - \{The second thing you actually do}
22
+ - \{The third thing you actually do}
23
23
 
24
24
  ## How it runs
25
25
 
26
- {One paragraph on the shape of the engagement: how it starts, how long it runs,
26
+ \{One paragraph on the shape of the engagement: how it starts, how long it runs,
27
27
  who is involved on both sides, and what a week looks like.}
28
28
 
29
29
  ## What you end up with
30
30
 
31
- {One paragraph on the deliverable. Be concrete — name the artefact, not the
31
+ \{One paragraph on the deliverable. Be concrete — name the artefact, not the
32
32
  feeling.}
33
33
 
34
34
  ## What it costs
35
35
 
36
- {One line on the pricing shape — fixed fee, retainer, day rate — and what it
36
+ \{One line on the pricing shape — fixed fee, retainer, day rate — and what it
37
37
  excludes. Replace with your real numbers, or delete this section and let the
38
38
  call do it.}
@@ -10,29 +10,29 @@ schema:
10
10
  name: "Your Name"
11
11
  ---
12
12
 
13
- # {First engagement}
13
+ # \{First engagement}
14
14
 
15
- {Open with the situation a reader is in when they come looking for this. Two or
15
+ \{Open with the situation a reader is in when they come looking for this. Two or
16
16
  three sentences, no more — the detail belongs under the headings below.}
17
17
 
18
18
  ## What it covers
19
19
 
20
- - {The first thing you actually do}
21
- - {The second thing you actually do}
22
- - {The third thing you actually do}
20
+ - \{The first thing you actually do}
21
+ - \{The second thing you actually do}
22
+ - \{The third thing you actually do}
23
23
 
24
24
  ## How it runs
25
25
 
26
- {One paragraph on the shape of the engagement: how it starts, how long it runs,
26
+ \{One paragraph on the shape of the engagement: how it starts, how long it runs,
27
27
  who is involved, and what a week looks like.}
28
28
 
29
29
  ## What you end up with
30
30
 
31
- {One paragraph on the deliverable. Be concrete — name the artefact, not the
31
+ \{One paragraph on the deliverable. Be concrete — name the artefact, not the
32
32
  feeling.}
33
33
 
34
34
  ## What it costs
35
35
 
36
- {One line on the pricing shape — fixed fee, retainer, day rate — and what it
36
+ \{One line on the pricing shape — fixed fee, retainer, day rate — and what it
37
37
  excludes. Replace with your real numbers, or delete this section and let the
38
38
  call do it.}
@@ -10,29 +10,29 @@ schema:
10
10
  name: "Your Name"
11
11
  ---
12
12
 
13
- # {Second engagement}
13
+ # \{Second engagement}
14
14
 
15
- {Open with the situation a reader is in when they come looking for this. Two or
15
+ \{Open with the situation a reader is in when they come looking for this. Two or
16
16
  three sentences, no more — the detail belongs under the headings below.}
17
17
 
18
18
  ## What it covers
19
19
 
20
- - {The first thing you actually do}
21
- - {The second thing you actually do}
22
- - {The third thing you actually do}
20
+ - \{The first thing you actually do}
21
+ - \{The second thing you actually do}
22
+ - \{The third thing you actually do}
23
23
 
24
24
  ## How it runs
25
25
 
26
- {One paragraph on the shape of the engagement: how it starts, how long it runs,
26
+ \{One paragraph on the shape of the engagement: how it starts, how long it runs,
27
27
  who is involved, and what a week looks like.}
28
28
 
29
29
  ## What you end up with
30
30
 
31
- {One paragraph on the deliverable. Be concrete — name the artefact, not the
31
+ \{One paragraph on the deliverable. Be concrete — name the artefact, not the
32
32
  feeling.}
33
33
 
34
34
  ## What it costs
35
35
 
36
- {One line on the pricing shape — fixed fee, retainer, day rate — and what it
36
+ \{One line on the pricing shape — fixed fee, retainer, day rate — and what it
37
37
  excludes. Replace with your real numbers, or delete this section and let the
38
38
  call do it.}
@@ -10,29 +10,29 @@ schema:
10
10
  name: "Your Name"
11
11
  ---
12
12
 
13
- # {Third engagement}
13
+ # \{Third engagement}
14
14
 
15
- {Open with the situation a reader is in when they come looking for this. Two or
15
+ \{Open with the situation a reader is in when they come looking for this. Two or
16
16
  three sentences, no more — the detail belongs under the headings below.}
17
17
 
18
18
  ## What it covers
19
19
 
20
- - {The first thing you actually do}
21
- - {The second thing you actually do}
22
- - {The third thing you actually do}
20
+ - \{The first thing you actually do}
21
+ - \{The second thing you actually do}
22
+ - \{The third thing you actually do}
23
23
 
24
24
  ## How it runs
25
25
 
26
- {One paragraph on the shape of the engagement: how it starts, how long it runs,
26
+ \{One paragraph on the shape of the engagement: how it starts, how long it runs,
27
27
  who is involved, and what a week looks like.}
28
28
 
29
29
  ## What you end up with
30
30
 
31
- {One paragraph on the deliverable. Be concrete — name the artefact, not the
31
+ \{One paragraph on the deliverable. Be concrete — name the artefact, not the
32
32
  feeling.}
33
33
 
34
34
  ## What it costs
35
35
 
36
- {One line on the pricing shape — fixed fee, retainer, day rate — and what it
36
+ \{One line on the pricing shape — fixed fee, retainer, day rate — and what it
37
37
  excludes. Replace with your real numbers, or delete this section and let the
38
38
  call do it.}
@@ -50,10 +50,6 @@ CONTENT_ADAPTER=mdx
50
50
  # Developers → Webhooks → <your endpoint> → Signing secret.
51
51
  # Format: whsec_*. Server-only — must NOT be prefixed with NEXT_PUBLIC_.
52
52
  # STRIPE_WEBHOOK_SECRET=
53
- # Internal URL of the billing service — used by marketing-api's webhook
54
- # proxy (POST /v1/webhooks/stripe) to forward verified deliveries. Format:
55
- # https://<vercel-deployment>.vercel.app (no trailing slash, no path).
56
- # BILLING_SERVICE_URL=
57
53
 
58
54
  # ── Auth (Better Auth — apps/web only) ───────────────────────────────────────
59
55
  # BETTER_AUTH_URL=http://localhost:3000
@@ -63,10 +59,20 @@ CONTENT_ADAPTER=mdx
63
59
  # Unset in production, either one makes the API log a startup warning; the
64
60
  # API keeps running.
65
61
  # FRONTEND_URL is the site the browser loads (no trailing slash). Set it on the
66
- # API project: unset in production, CORS, cookie origin checks and the
67
- # billing-portal return URLs fall back to http://localhost:3000, and checkout
68
- # answers 500 to a caller without X-Internal-Auth.
62
+ # API project: it is the first trusted origin for CORS, cookie-authenticated
63
+ # writes and checkout redirects. Unset in production, those three trust only
64
+ # AILK_TRUSTED_ORIGINS below (with neither set, they refuse every browser
65
+ # origin and checkout answers 500 to a caller without X-Internal-Auth), and the
66
+ # billing-portal return URLs fall back to http://localhost:3000.
69
67
  # FRONTEND_URL=https://your-domain.com
68
+ # AILK_TRUSTED_ORIGINS lists MORE browser origins the API trusts, beside
69
+ # FRONTEND_URL — the Admin app and spoke sites a hub API serves. Comma-separated
70
+ # exact origins, written as a browser sends them: scheme + host (+ port), no
71
+ # path, no trailing slash, lower-case. `*`, wildcard hosts and any other form
72
+ # are refused and the API does not start; production requires https. Each
73
+ # listed origin gets CORS reads, cookie writes and checkout-redirect trust, so
74
+ # list ONLY origins the deployment owner operates — never another party's site.
75
+ # AILK_TRUSTED_ORIGINS=https://admin.your-domain.com,https://spoke.example
70
76
  # OSS_API_INTERNAL_SECRET gates POST /v1/checkout/sessions. Unset, the route
71
77
  # admits every caller. Set, it answers 401 to any caller that does not send it
72
78
  # as X-Internal-Auth — including the MCP start_purchase tool and a browser
@@ -15,6 +15,11 @@ PORT=3002
15
15
  # Frontend URL (for CORS)
16
16
  # =============================================================================
17
17
  FRONTEND_URL=http://localhost:3000
18
+ # More trusted browser origins (Admin app, spoke sites): comma-separated exact
19
+ # origins, no path or trailing slash; `*` is refused at startup; https in production.
20
+ # Each listed origin gets credentialed CORS, cookie writes and checkout-redirect
21
+ # trust — list only origins the deployment owner operates.
22
+ # AILK_TRUSTED_ORIGINS=https://admin.your-domain.com,https://spoke.example
18
23
 
19
24
  # =============================================================================
20
25
  # Local Database (required in production)
@@ -49,7 +49,7 @@
49
49
  "@working-theory/stripe-client": "workspace:*",
50
50
  "@working-theory/validation": "workspace:*",
51
51
  "dotenv": "^17.4.2",
52
- "fastify": "^5.12.1",
52
+ "fastify": "^5.12.5",
53
53
  "pdf-lib": "^1.17.1",
54
54
  "pg": "^8.22.0",
55
55
  "yaml": "^2.9.0",