create-ailk 0.15.0 → 0.16.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 (127) hide show
  1. package/README.md +124 -0
  2. package/dist/add-module.js +52 -7
  3. package/dist/add-route.js +8 -0
  4. package/dist/adopt.d.ts +74 -0
  5. package/dist/adopt.js +314 -0
  6. package/dist/cli.js +82 -2
  7. package/dist/derive-architecture.d.ts +22 -0
  8. package/dist/derive-architecture.js +58 -1
  9. package/dist/lib/declared-env.d.ts +142 -0
  10. package/dist/lib/declared-env.js +985 -0
  11. package/dist/lib/env-catalog.d.ts +17 -0
  12. package/dist/lib/env-catalog.js +234 -0
  13. package/dist/lib/extract-module-bundle.d.ts +9 -0
  14. package/dist/lib/extract-module-bundle.js +18 -1
  15. package/dist/lib/module-tier.d.ts +2 -0
  16. package/dist/lib/module-tier.js +4 -0
  17. package/dist/module-architecture.js +76 -0
  18. package/dist/parse-args.d.ts +5 -0
  19. package/dist/parse-args.js +17 -1
  20. package/dist/programmatic.d.ts +8 -0
  21. package/dist/programmatic.js +78 -9
  22. package/dist/seed-flows.d.ts +65 -0
  23. package/dist/seed-flows.js +350 -0
  24. package/dist/site-manifest.d.ts +43 -0
  25. package/dist/site-manifest.js +98 -3
  26. package/dist/site-roster.d.ts +13 -0
  27. package/dist/site-roster.js +22 -8
  28. package/dist/spoke-checkout.d.ts +42 -0
  29. package/dist/spoke-checkout.js +359 -0
  30. package/dist/surfaces.js +7 -0
  31. package/dist/sync-routes.js +23 -8
  32. package/dist/topology.d.ts +124 -9
  33. package/dist/topology.js +449 -40
  34. package/package.json +3 -3
  35. package/templates/.env.example +10 -0
  36. package/templates/apps/api/CLAUDE.md +19 -4
  37. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
  38. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
  39. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
  40. package/templates/apps/api/src/bin/spokes.ts +62 -1
  41. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
  42. package/templates/apps/api/src/config/index.ts +35 -0
  43. package/templates/apps/api/src/lib/__mocks__/prisma.ts +89 -0
  44. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
  45. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
  46. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
  47. package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
  48. package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
  49. package/templates/apps/api/src/lib/spoke-store.ts +25 -3
  50. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
  51. package/templates/apps/api/src/middleware/auth.ts +126 -0
  52. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
  53. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
  54. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +7 -0
  55. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
  56. package/templates/apps/api/src/routes/webhooks/stripe.ts +72 -1
  57. package/templates/apps/api/src/server.ts +105 -0
  58. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
  59. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
  60. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +293 -0
  61. package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
  62. package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
  63. package/templates/apps/api/src/services/lead-routing.ts +131 -2
  64. package/templates/apps/api/src/services/person.ts +541 -0
  65. package/templates/apps/web/.env.example +58 -5
  66. package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
  67. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
  68. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
  69. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
  70. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
  71. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
  72. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
  73. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
  74. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
  75. package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
  76. package/templates/apps/web/app/[locale]/layout.tsx +28 -15
  77. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
  78. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
  79. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
  80. package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
  81. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
  82. package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
  83. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +15 -0
  84. package/templates/apps/web/app/openapi.json/route.ts +3 -6
  85. package/templates/apps/web/auth.ts +29 -2
  86. package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
  87. package/templates/apps/web/components/signed-in-nav.tsx +17 -3
  88. package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
  89. package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
  90. package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
  91. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
  92. package/templates/apps/web/lib/__tests__/relay.test.ts +27 -0
  93. package/templates/apps/web/lib/account-config.ts +130 -1
  94. package/templates/apps/web/lib/api-origin.generated.ts +11 -0
  95. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
  96. package/templates/apps/web/lib/hub-session/config.ts +104 -0
  97. package/templates/apps/web/lib/hub-session/index.ts +23 -0
  98. package/templates/apps/web/lib/hub-session/seal.ts +82 -0
  99. package/templates/apps/web/lib/hub-session/session.ts +180 -0
  100. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
  101. package/templates/apps/web/lib/listings-config.ts +31 -0
  102. package/templates/apps/web/lib/listings.tsx +7 -3
  103. package/templates/apps/web/lib/meeting-slugs.ts +11 -1
  104. package/templates/apps/web/lib/next-path.ts +35 -0
  105. package/templates/apps/web/lib/relay.ts +3 -1
  106. package/templates/apps/web/lib/server-api.ts +11 -1
  107. package/templates/apps/web/lib/topology.generated.ts +15 -5
  108. package/templates/apps/web/messages/en.json +32 -3
  109. package/templates/database/CHANGELOG.md +26 -0
  110. package/templates/database/__tests__/agents.db.test.ts +25 -5
  111. package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
  112. package/templates/database/__tests__/member-area.db.test.ts +245 -0
  113. package/templates/database/__tests__/person.db.test.ts +584 -0
  114. package/templates/database/auth/schema.prisma +3 -2
  115. package/templates/database/config/schema.prisma +13 -0
  116. package/templates/database/inbox/schema.prisma +98 -0
  117. package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
  118. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
  119. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
  120. package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
  121. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/person-backfill.sql +132 -0
  124. package/templates/database/tenancy.ts +53 -0
  125. package/templates/tools/check-declared-env.ts +37 -0
  126. package/templates/tools/declared-env.ts +1163 -0
  127. package/versions.json +35 -35
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The described row create-ailk writes for each env name a spoke-carried app
3
+ * reads (#7874 D10). `planEnvRows` (`declared-env.ts`) takes a name's
4
+ * `description` and `secret` from here; a name missing from it gets a
5
+ * placeholder row a reviewer sees.
6
+ *
7
+ * One plain sentence per name, drawn from the `.env.example` comments
8
+ * (`apps/web`, `apps/mcp`, the repo root) where they exist. The keys are
9
+ * exactly the non-platform names this monorepo's `apps/web` and `apps/mcp`
10
+ * read: `__tests__/check-declared-env.test.ts` scans both with the same
11
+ * scanner and fails on a name with no entry or an entry nothing reads, so a
12
+ * new env read in a spoke-carried app cannot ship undescribed. A
13
+ * `NEXT_PUBLIC_*` name is inlined into the browser bundle, so it is never a
14
+ * secret.
15
+ */
16
+ import type { EnvCatalogEntry } from "./declared-env.js";
17
+ export declare const ENV_CATALOG: Readonly<Record<string, EnvCatalogEntry>>;
@@ -0,0 +1,234 @@
1
+ export const ENV_CATALOG = {
2
+ AILK_API_URL: {
3
+ description: "The hub API origin this site's server calls (forms, flows, listings). Server-only; never NEXT_PUBLIC_.",
4
+ secret: false,
5
+ },
6
+ AILK_AUTH_PASSWORD_RESET: {
7
+ description: 'Set to "1" or "true" to offer password reset once the server-side reset flow is wired.',
8
+ secret: false,
9
+ },
10
+ AILK_AUTH_REQUIRED: {
11
+ description: 'Set to "1" or "true" to require sign-in; unset leaves every route public.',
12
+ secret: false,
13
+ },
14
+ AILK_AUTH_SIGNUP_OPEN: {
15
+ description: 'Set to "1" or "true" to open public sign-up on /sign-up.',
16
+ secret: false,
17
+ },
18
+ AILK_AUTH_TRUSTED_PROXIES: {
19
+ description: "Comma-separated CIDRs of this site's edge or proxy, used to key the sign-in rate limit on the real client address.",
20
+ secret: false,
21
+ },
22
+ AILK_BASE_URL: {
23
+ description: "This site's public origin as the MCP server sees it: the same-origin check for audits and the purchase return URLs.",
24
+ secret: false,
25
+ },
26
+ AILK_CONTENT_ROOT: {
27
+ description: "Overrides the on-disk content root; defaults to <repo-root>/content.",
28
+ secret: false,
29
+ },
30
+ AILK_ENABLED_MODULES: {
31
+ description: "Comma-separated paid modules the MCP server enables; must match the API's value. Unset means none.",
32
+ secret: false,
33
+ },
34
+ AILK_HELP_CENTER_URL: {
35
+ description: "When set, the account area's help menu links to this help center.",
36
+ secret: false,
37
+ },
38
+ AILK_HUB_CLIENT_ID: {
39
+ description: "The spoke's client id at its hub (spokes register --redirect-uri <origin>/api/hub-session/callback). Server-only; set with --hub-sign-in.",
40
+ secret: false,
41
+ },
42
+ AILK_HUB_CLIENT_SECRET: {
43
+ description: "The spoke's client secret at its hub, shown once at registration. Server-only; never NEXT_PUBLIC_, never committed.",
44
+ secret: true,
45
+ },
46
+ AILK_HUB_SESSION_SECRET: {
47
+ description: "Seals this spoke's own hub-session cookie (create-ailk writes one; `openssl rand -base64 32` makes another). Never the client secret; rotating it signs every visitor out.",
48
+ secret: true,
49
+ },
50
+ AILK_HUB_SIGN_IN_URL: {
51
+ description: "The hub's WEB origin where its /sign-in lives (https:, or http: on localhost). Server-only; create-ailk writes it to .env.local.",
52
+ secret: false,
53
+ },
54
+ AILK_LEAD_SITE: {
55
+ description: "Marks which site of a hub and its spokes captured an MCP lead; unset leaves the lead unmarked.",
56
+ secret: false,
57
+ },
58
+ AILK_LICENSE_STORAGE_PATH: {
59
+ description: "The file the daily-refreshed licence key is written to; defaults to .ailk-license.jwt in the working directory.",
60
+ secret: false,
61
+ },
62
+ AILK_LISTING_PUBLIC_KEY: {
63
+ description: "The site's public listing key, read on the server when NEXT_PUBLIC_AILK_LISTING_SITE_KEY is unset. Public by design: it names a site and grants nothing.",
64
+ secret: false,
65
+ },
66
+ AILK_ROBOTS_DISALLOW: {
67
+ description: 'Comma-separated paths robots.txt disallows, e.g. "/admin/,/private/".',
68
+ secret: false,
69
+ },
70
+ AILK_SITE_ID: {
71
+ description: "The scope captured leads are stamped with; on a single-tenant install, the default workspace id.",
72
+ secret: false,
73
+ },
74
+ AILK_VISITOR_FORWARD_KEY: {
75
+ description: "This spoke's key for forwarding each visitor's IP to the hub, so the hub rate-limits per visitor. Server-only.",
76
+ secret: true,
77
+ },
78
+ ANALYZE: {
79
+ description: 'Set to "true" to run the bundle analyzer during next build.',
80
+ secret: false,
81
+ },
82
+ AUTH_EMAIL_FROM: {
83
+ description: "The From address on sign-in and password-reset mail.",
84
+ secret: false,
85
+ },
86
+ AUTH_EMAIL_SERVER: {
87
+ description: "The mail server connection that enables the email sign-in method.",
88
+ secret: true,
89
+ },
90
+ AUTH_EMAIL_WEBHOOK_SECRET: {
91
+ description: "Signs the hop that asks the API to send auth mail; must match the API's value. Deployment-only.",
92
+ secret: true,
93
+ },
94
+ BETTER_AUTH_SECRET: {
95
+ description: "Signs session cookies and email-verification tokens. Required in production.",
96
+ secret: true,
97
+ },
98
+ BETTER_AUTH_URL: {
99
+ description: "The origin Better Auth builds OAuth callbacks and password-reset links on.",
100
+ secret: false,
101
+ },
102
+ BETTER_STACK_HEARTBEAT_URL: {
103
+ description: "Better Stack heartbeat URL pinged on cron and startup; unset turns the heartbeat off.",
104
+ secret: true,
105
+ },
106
+ CONTENT_REVALIDATE_SECRET: {
107
+ description: "The shared secret /api/revalidate checks the API's publish webhook against; the same value as the API's.",
108
+ secret: true,
109
+ },
110
+ DATABASE_URL: {
111
+ description: "The database connection string, for the database content adapter and the licence-key store.",
112
+ secret: true,
113
+ },
114
+ GITHUB_CLIENT_ID: {
115
+ description: "GitHub OAuth app client id; set with GITHUB_CLIENT_SECRET to turn on GitHub sign-in.",
116
+ secret: false,
117
+ },
118
+ GITHUB_CLIENT_SECRET: {
119
+ description: "GitHub OAuth app client secret; set with GITHUB_CLIENT_ID to turn on GitHub sign-in.",
120
+ secret: true,
121
+ },
122
+ GOOGLE_CLIENT_ID: {
123
+ description: "Google OAuth client id; set with GOOGLE_CLIENT_SECRET to turn on Google sign-in.",
124
+ secret: false,
125
+ },
126
+ GOOGLE_CLIENT_SECRET: {
127
+ description: "Google OAuth client secret; set with GOOGLE_CLIENT_ID to turn on Google sign-in.",
128
+ secret: true,
129
+ },
130
+ MCP_ABUSE_BLOCKED_USER_AGENTS: {
131
+ description: "Comma-separated User-Agent substrings the MCP server blocks, beyond its built-in list.",
132
+ secret: false,
133
+ },
134
+ MCP_CORS_WRITE_ALLOWED_ORIGINS: {
135
+ description: "Comma-separated origins allowed to call the MCP write tools; empty allows no cross-origin writes.",
136
+ secret: false,
137
+ },
138
+ MCP_HTTP_PATH: {
139
+ description: "The URL path of the MCP HTTP endpoint; defaults to /mcp.",
140
+ secret: false,
141
+ },
142
+ MCP_HTTP_PORT: {
143
+ description: "The port the MCP HTTP transport binds; defaults to 8080.",
144
+ secret: false,
145
+ },
146
+ MCP_PUBLIC_HTTP_URL: {
147
+ description: "The public URL of the MCP HTTP transport, advertised by /.well-known/mcp.json.",
148
+ secret: false,
149
+ },
150
+ MCP_RATE_LIMIT_READ_PER_MIN: {
151
+ description: "Maximum MCP read-tool requests per minute per IP; defaults to 60.",
152
+ secret: false,
153
+ },
154
+ MCP_RATE_LIMIT_WRITE_PER_MIN: {
155
+ description: "Maximum MCP write-tool requests per minute per IP; defaults to 10.",
156
+ secret: false,
157
+ },
158
+ MCP_TRANSPORT: {
159
+ description: 'Which MCP transport to boot: "stdio" (the default) or "http".',
160
+ secret: false,
161
+ },
162
+ NEXT_PUBLIC_AILK_API_URL: {
163
+ description: "The API origin the browser reads on a hub. A spoke never sets it: its hub origin is server-only (AILK_API_URL).",
164
+ secret: false,
165
+ },
166
+ NEXT_PUBLIC_AILK_LISTING_SITE_KEY: {
167
+ description: "The site's public listing key, inlined at build; it switches the listicle directory on. Public by design.",
168
+ secret: false,
169
+ },
170
+ NEXT_PUBLIC_API_URL: {
171
+ description: "Deprecated fallback for the API origin; do not set it in a new deployment.",
172
+ secret: false,
173
+ },
174
+ NEXT_PUBLIC_BASE_URL: {
175
+ description: "This site's public origin, scheme and host only: canonical URLs and Stripe return URLs are built on it.",
176
+ secret: false,
177
+ },
178
+ NEXT_PUBLIC_DEFAULT_LOCALE: {
179
+ description: "The site's default locale; defaults to en.",
180
+ secret: false,
181
+ },
182
+ NEXT_PUBLIC_ENABLED_LOCALES: {
183
+ description: "The site's locales, baked at build from project.yaml; do not set by hand.",
184
+ secret: false,
185
+ },
186
+ NEXT_PUBLIC_GOOGLE_TAG_ID: {
187
+ description: "Google Analytics 4 measurement ID (G-XXXXXXXXXX); unset sends no analytics.",
188
+ secret: false,
189
+ },
190
+ NEXT_PUBLIC_SENTRY_DSN: {
191
+ description: "Sentry DSN for browser error capture; unset turns it off. Public by design.",
192
+ secret: false,
193
+ },
194
+ NEXT_PUBLIC_SITE_NAME: {
195
+ description: "The site name shown where the site's content files cannot be read (the OG image, the admin pages).",
196
+ secret: false,
197
+ },
198
+ NEXT_PUBLIC_SITE_WORDMARK: {
199
+ description: "The wordmark the generated OG image shows.",
200
+ secret: false,
201
+ },
202
+ NEXT_PUBLIC_THEME_COOKIE_NAME: {
203
+ description: "The cookie the colour-mode choice is stored in; defaults to ailk-theme.",
204
+ secret: false,
205
+ },
206
+ OSS_API_INTERNAL_SECRET: {
207
+ description: "Shared secret the /dev/purchase action sends to the API's checkout route; must equal the API's value. Server-only.",
208
+ secret: true,
209
+ },
210
+ SCHEDULING_CAL_LINK: {
211
+ description: 'Which event /meet books, as "<owner>/<event-slug>"; unset makes /meet 404.',
212
+ secret: false,
213
+ },
214
+ SENTRY_AUTH_TOKEN: {
215
+ description: "Build-time Sentry token that uploads source maps; unset skips the upload.",
216
+ secret: true,
217
+ },
218
+ SENTRY_DSN: {
219
+ description: "Sentry DSN for server-side error capture; unset turns it off.",
220
+ secret: false,
221
+ },
222
+ SENTRY_ORG: {
223
+ description: "The Sentry organisation slug used for the source-map upload.",
224
+ secret: false,
225
+ },
226
+ SENTRY_PROJECT: {
227
+ description: "The Sentry project slug used for the source-map upload.",
228
+ secret: false,
229
+ },
230
+ STEALTH: {
231
+ description: 'Set to "true" to keep the site out of search indexes (noindex headers) until launch.',
232
+ secret: false,
233
+ },
234
+ };
@@ -51,6 +51,8 @@ export interface ExtractModuleBundleOptions {
51
51
  checkPatches?: (patches: readonly ModulePatch[]) => void;
52
52
  /** Destinations this scaffold does not carry (#7426 D4). See `InstallBundleTreeOptions`. */
53
53
  skip?: (destination: string) => boolean;
54
+ /** Where an installMap destination lands instead (#7876 D10). See `InstallBundleTreeOptions`. */
55
+ remapDestination?: (destination: string) => string;
54
56
  }
55
57
  /**
56
58
  * Extract a fetched module bundle into `targetDir`, merging its files onto
@@ -142,6 +144,13 @@ export interface InstallBundleTreeOptions {
142
144
  * entry is still validated as before. Absent ⟹ nothing is skipped.
143
145
  */
144
146
  skip?: (destination: string) => boolean;
147
+ /**
148
+ * Where an `installMap` destination lands instead (#7876 D10) — a spoke
149
+ * passes `topology.ts` `spokeDestination`. Applied after the installMap is
150
+ * validated and before `skip`; a remapped destination that leaves the
151
+ * target refuses the tree. Absent ⟹ every destination is unchanged.
152
+ */
153
+ remapDestination?: (destination: string) => string;
145
154
  }
146
155
  /** See `InstallBundleTreeOptions.existingFilePolicy`. */
147
156
  export type ExistingFilePolicy = "overwrite" | "skip-existing";
@@ -476,6 +476,9 @@ export async function extractModuleBundle(options) {
476
476
  ? { checkPatches: options.checkPatches }
477
477
  : {}),
478
478
  ...(options.skip !== undefined ? { skip: options.skip } : {}),
479
+ ...(options.remapDestination !== undefined
480
+ ? { remapDestination: options.remapDestination }
481
+ : {}),
479
482
  });
480
483
  }
481
484
  finally {
@@ -505,11 +508,25 @@ export function installBundleTree(options) {
505
508
  : readAilkModuleManifest(sourceRoot);
506
509
  const declaredInstallMap = manifest?.installMap ?? {};
507
510
  validateInstallMap(declaredInstallMap, sourceRoot, targetAbs, subject, isTemplate);
511
+ // #7876 D10 — a spoke lands some destinations elsewhere (its module flows
512
+ // become declared data). The remapped destination passes the same
513
+ // containment rule `validateInstallMap` applies, or the tree is refused.
514
+ const remap = options.remapDestination;
515
+ const remappedInstallMap = {};
516
+ for (const [source, declared] of Object.entries(declaredInstallMap)) {
517
+ const destination = remap !== undefined ? remap(declared) : declared;
518
+ if (destination !== declared &&
519
+ (hasUnsafeSegment(destination) ||
520
+ !isPathInside(targetAbs, resolve(targetAbs, destination)))) {
521
+ throw new Error(`${subject}: remapped installMap destination "${destination}" resolves outside target`);
522
+ }
523
+ remappedInstallMap[source] = destination;
524
+ }
508
525
  // #7426 D4 — a destination the scaffold does not carry is dropped here,
509
526
  // after validation, so a spoke never receives it and never records it.
510
527
  const skip = options.skip;
511
528
  const isSkipped = (destination) => skip !== undefined && skip(normalize(destination).split(sep).join("/"));
512
- const installMap = Object.fromEntries(Object.entries(declaredInstallMap).filter(([, destination]) => !isSkipped(destination)));
529
+ const installMap = Object.fromEntries(Object.entries(remappedInstallMap).filter(([, destination]) => !isSkipped(destination)));
513
530
  // patches validation (spec 5418 § 3 D6) runs at the same point, before
514
531
  // any copy — a bad target/kind/op rejects the whole tree with nothing
515
532
  // written to targetDir, same posture as installMap and the tar-entry
@@ -30,6 +30,8 @@ export declare const ADD_ONS: readonly ModuleName[];
30
30
  * adds it without trimming, exactly like an add-on.
31
31
  */
32
32
  export declare const OPT_IN_MODULES: readonly ModuleName[];
33
+ /** Whether naming `m` in `--modules` adds it without trimming the level default. */
34
+ export declare function isAddition(m: ModuleName): boolean;
33
35
  /**
34
36
  * What a key at each level switches on with no `--modules` (D2). Derived —
35
37
  * every list is `RECOGNIZED_MODULES.filter(...)`, so declaration order is
@@ -54,6 +54,10 @@ export const OPT_IN_MODULES = ["workspace"];
54
54
  const PRO_PLUS_MODULES = ["multi-tenant"];
55
55
  /** Named modules that never trim the level default when composing a set (D2). */
56
56
  const ADDITIONS = [...ADD_ONS, ...OPT_IN_MODULES];
57
+ /** Whether naming `m` in `--modules` adds it without trimming the level default. */
58
+ export function isAddition(m) {
59
+ return ADDITIONS.includes(m);
60
+ }
57
61
  const PRO_EXCLUDED = new Set([...ADDITIONS, ...PRO_PLUS_MODULES]);
58
62
  /**
59
63
  * What a key at each level switches on with no `--modules` (D2). Derived —
@@ -150,6 +150,23 @@ export const ARCHITECTURE_BASE = {
150
150
  },
151
151
  ],
152
152
  };
153
+ /**
154
+ * The member area's apps/api files (#7877 D9, D11), co-owned by `workspace`
155
+ * and `multi-tenant`: server.ts registers all five routes under the
156
+ * `workspace || multi-tenant` disjunction, so both bundles must deliver every
157
+ * file behind it — the three operator route files, the two member route
158
+ * files, the member resolution (`services/member-area.ts`) and the member
159
+ * data boundary (`lib/member-store.ts`).
160
+ */
161
+ const MEMBER_AREA_FILES = [
162
+ "apps/api/src/routes/workspaces/pieces.ts",
163
+ "apps/api/src/routes/workspaces/plan.ts",
164
+ "apps/api/src/routes/workspaces/messages-out.ts",
165
+ "apps/api/src/routes/member/pieces.ts",
166
+ "apps/api/src/routes/member/messages.ts",
167
+ "apps/api/src/services/member-area.ts",
168
+ "apps/api/src/lib/member-store.ts",
169
+ ];
153
170
  /**
154
171
  * The workspace asset store (#5854), CO-OWNED by the `workspace` and
155
172
  * `multi-tenant` slices.
@@ -799,6 +816,15 @@ export const MODULE_SLICES = {
799
816
  "apps/api/src/routes/workspaces/submissions.ts",
800
817
  "apps/api/src/routes/workspaces/subscribers.ts",
801
818
  "apps/api/src/services/inbox-submissions.ts",
819
+ // #852 D8 — the People reads: the same workspace||multi-tenant gate and
820
+ // the same tenant-db.ts import, co-owned with `workspace` for the same
821
+ // reason.
822
+ "apps/api/src/routes/workspaces/people.ts",
823
+ // #7877 D9, D11 — the member area: the operator's routes and the member
824
+ // reads register under the same workspace||multi-tenant gate, so each
825
+ // file behind it — the routes, the member resolution and the member
826
+ // data boundary — is co-owned with `workspace`, as people.ts is.
827
+ ...MEMBER_AREA_FILES,
802
828
  // #7399 — the two siblings #6216 left `workspace`-only. They register
803
829
  // under that same `workspace || multi-tenant` gate, so a scaffold with
804
830
  // `multi-tenant` and without `workspace` loaded them with no file behind
@@ -947,6 +973,10 @@ export const MODULE_SLICES = {
947
973
  "apps/api/src/routes/workspaces/submissions.ts",
948
974
  "apps/api/src/routes/workspaces/subscribers.ts",
949
975
  "apps/api/src/services/inbox-submissions.ts",
976
+ // Co-owned with `multi-tenant` (#852 D8), as the reads above.
977
+ "apps/api/src/routes/workspaces/people.ts",
978
+ // Co-owned with `multi-tenant` (#7877 D9, D11) — see that slice's entry.
979
+ ...MEMBER_AREA_FILES,
950
980
  // Co-owned with `multi-tenant` (#5853) — see that slice's entry.
951
981
  "apps/api/src/routes/workspaces/content.ts",
952
982
  ...ASSET_STORE_FILES,
@@ -1005,6 +1035,15 @@ export const MODULE_SLICES = {
1005
1035
  // scheduling-bearing scaffold's `apps/api` really does contain the billing,
1006
1036
  // checkout and email routes those imports are for.
1007
1037
  scheduling: {
1038
+ // #7872 — admitted on a spoke. Its bundle installs through the spoke
1039
+ // filter (`isSpokeExcluded`), which drops every destination under
1040
+ // apps/api or database — the routes, services/scheduling (the provider
1041
+ // client and the only reader of the credential), the Cal.com webhook —
1042
+ // and the (authed)/appointments page. What lands is the web tier: the
1043
+ // three apps/web/app/[locale]/meet pages and the @working-theory/ui-scheduling
1044
+ // dependency (`patches` below), plus the three MCP tools when apps/mcp is
1045
+ // kept. Every booking call then reaches the hub with the spoke key.
1046
+ spoke: true,
1008
1047
  apps: {
1009
1048
  api: {
1010
1049
  path: "apps/api",
@@ -1341,6 +1380,33 @@ export const MODULE_SLICES = {
1341
1380
  // cannot place an unreached file either) is unchanged and still governs
1342
1381
  // where all eleven live.
1343
1382
  "signal-collectors": {
1383
+ // #7876 D8 — admitted on a spoke, so a Pro spoke carries the flow
1384
+ // stepper and its flows reach the hub with the spoke key. Its bundle
1385
+ // installs through the spoke filter (`isSpokeExcluded`) and the D10
1386
+ // remap (`spokeDestination`); every part of the slice lands as follows:
1387
+ //
1388
+ // apps/web/app/[locale]/flows/** (its `flows` surface) lands
1389
+ // apps/web/app/[locale]/reports/**, the ReportPage + ReportsList seeds
1390
+ // dropped (`SPOKE_EXCLUDED_MODULE_PATHS`,
1391
+ // 2026-10-09): the reader links a report's
1392
+ // bytes on the hub through
1393
+ // NEXT_PUBLIC_AILK_API_URL, refused on a
1394
+ // spoke (#7874); deliverables are hub-side
1395
+ // ownedFiles under apps/api/** dropped (removed root)
1396
+ // ownedFiles under (authed)/waitlist/** dropped (removal seed)
1397
+ // ownedFiles projects/apply/ListingApplicationClient lands; no route mounts it
1398
+ // ownedFiles under apps/mcp/src/tools/** lands when apps/mcp is kept
1399
+ // installMap content/en/** (12 seeds) lands, but for the five report seeds
1400
+ // installMap database/flows/{referral,fit-check}.flow.json
1401
+ // remapped to flows/<name>.json — declared
1402
+ // data `create-ailk seed-flows` sends
1403
+ // installMap database/waitlist/**.experiment.json dropped (removed root):
1404
+ // a register is hub-side state
1405
+ // installMap apps/web/public/reports/**, reports/** lands
1406
+ // patches — @working-theory/ui-signal-collectors lands
1407
+ //
1408
+ // It stays a Pro module: a keyless spoke carries no flows.
1409
+ spoke: true,
1344
1410
  paidSectionsPackage: "@working-theory/ui-signal-collectors",
1345
1411
  ownedFiles: [
1346
1412
  "apps/api/src/routes/flows/index.ts",
@@ -1655,6 +1721,16 @@ export const MODULE_SLICES = {
1655
1721
  // `lib/stripe.ts` and the webhook receiver this module owns — declared
1656
1722
  // here, not engineered around.
1657
1723
  commerce: {
1724
+ // #7838 — admitted on a spoke. Its bundle installs through the spoke
1725
+ // filter (`isSpokeExcluded`), which drops every destination under
1726
+ // apps/api or database and the web files that call the hub's own
1727
+ // checkout (`SPOKE_HUB_CHECKOUT_SEEDS`: flows/[slug]/checkout.actions.ts,
1728
+ // the flow results pages, dev/purchase — spoke-intake Rule 3). What lands
1729
+ // is the scaffold-commerce skill, the commerce-boundary rule and the
1730
+ // @working-theory/ui-commerce dependency (`patches` below); the spoke's own
1731
+ // checkout — two routes, checkout-config.ts and two return pages — is
1732
+ // rendered by create-ailk (`spoke-checkout.ts`), not carried here.
1733
+ spoke: true,
1658
1734
  paidSectionsPackage: "@working-theory/ui-commerce",
1659
1735
  apps: {
1660
1736
  api: {
@@ -116,6 +116,11 @@ export interface CliOptions {
116
116
  hubUrl?: string;
117
117
  /** Spoke only: `false` with `--without-mcp` (#7426 D3), else `true`. */
118
118
  mcp?: boolean;
119
+ /**
120
+ * Spoke only: the raw `--hub-sign-in` value (#7894 D1) — the hub's web
121
+ * origin this spoke signs visitors in at. `scaffold()` validates it.
122
+ */
123
+ hubSignIn?: string;
119
124
  }
120
125
  export declare class ParseError extends Error {
121
126
  constructor(message: string);
@@ -178,6 +178,12 @@ Options:
178
178
  a spoke with neither is refused. Written to
179
179
  apps/web/.env.local — the spoke key never is.
180
180
  --without-mcp A spoke without apps/mcp.
181
+ --hub-sign-in <origin> A spoke whose /account signs visitors in through
182
+ its hub's own sign-in page (the hub's web origin,
183
+ https: or http: on localhost). No database: the
184
+ session is a sealed cookie. Writes the origin and
185
+ a session secret to apps/web/.env.local — never the
186
+ client secret, which the hub issues.
181
187
  --install After scaffolding, run \`pnpm install\` in the
182
188
  target directory. Off by default.
183
189
  --help, -h Print this message.
@@ -217,6 +223,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
217
223
  topology: { type: "string" },
218
224
  "hub-url": { type: "string" },
219
225
  "without-mcp": { type: "boolean", default: false },
226
+ "hub-sign-in": { type: "string" },
220
227
  surfaces: { type: "string" },
221
228
  "sync-architecture": { type: "string" },
222
229
  "sync-catalog": { type: "boolean", default: false },
@@ -251,6 +258,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
251
258
  const topology = parseTopology(rawTopology);
252
259
  const hubUrl = parsed.values["hub-url"];
253
260
  const withoutMcp = parsed.values["without-mcp"] === true;
261
+ const hubSignIn = parsed.values["hub-sign-in"];
254
262
  // The sync modes each exit after rewriting their one file, so asking for
255
263
  // more than one in one invocation has no coherent meaning.
256
264
  const syncFlags = [
@@ -271,6 +279,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
271
279
  const spokeOnly = [
272
280
  hubUrl !== undefined ? "--hub-url" : undefined,
273
281
  withoutMcp ? "--without-mcp" : undefined,
282
+ hubSignIn !== undefined ? "--hub-sign-in" : undefined,
274
283
  ].filter((f) => f !== undefined);
275
284
  if (spokeOnly.length > 0) {
276
285
  throw new ParseError(`${spokeOnly.join(" and ")} only apply${spokeOnly.length === 1 ? "s" : ""} to a spoke. Pass --topology spoke, or drop ${spokeOnly.length === 1 ? "it" : "them"}.`);
@@ -367,7 +376,11 @@ export function parseCliArgs(argv, env = { interactive: false }) {
367
376
  ...(license !== undefined ? { license } : {}),
368
377
  topology,
369
378
  ...(topology === "spoke"
370
- ? { ...(hubUrl !== undefined ? { hubUrl } : {}), mcp: !withoutMcp }
379
+ ? {
380
+ ...(hubUrl !== undefined ? { hubUrl } : {}),
381
+ mcp: !withoutMcp,
382
+ ...(hubSignIn !== undefined ? { hubSignIn } : {}),
383
+ }
371
384
  : {}),
372
385
  };
373
386
  }
@@ -538,6 +551,9 @@ export function toScaffoldOptions(opts) {
538
551
  topology: "spoke",
539
552
  ...(opts.hubUrl !== undefined ? { hubUrl: opts.hubUrl } : {}),
540
553
  mcp: opts.mcp !== false,
554
+ ...(opts.hubSignIn !== undefined
555
+ ? { hubSignInUrl: opts.hubSignIn }
556
+ : {}),
541
557
  }
542
558
  : {}),
543
559
  };
@@ -149,6 +149,12 @@ export interface ScaffoldOptions {
149
149
  hubUrl?: string;
150
150
  /** Spoke only: `false` drops `apps/mcp` (`--without-mcp`, #7426 D3). Default true. */
151
151
  mcp?: boolean;
152
+ /**
153
+ * Spoke only: the hub's web origin this spoke signs visitors in at
154
+ * (`--hub-sign-in`, #7894 D1). Set, the spoke is scaffolded in the hub
155
+ * sign-in mode (`SpokeShapeOptions.signIn: "hub"`).
156
+ */
157
+ hubSignInUrl?: string;
152
158
  }
153
159
  export interface ScaffoldResult {
154
160
  /** Resolved absolute path to the scaffolded directory. */
@@ -181,6 +187,8 @@ export interface ScaffoldResult {
181
187
  droppedModules?: ModuleName[];
182
188
  /** A spoke only (#7426 D2): the normalized hub origin written to apps/web/.env.local. */
183
189
  hubOrigin?: string;
190
+ /** A hub sign-in spoke only (#7894 D11): the hub web origin written to apps/web/.env.local. */
191
+ hubSignInOrigin?: string;
184
192
  }
185
193
  /**
186
194
  * Scaffold an AI Launch Kit starter project into targetDir (synchronous).