create-ailk 0.14.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 (184) hide show
  1. package/README.md +124 -0
  2. package/component-catalog.json +3 -3
  3. package/dist/add-module.d.ts +7 -0
  4. package/dist/add-module.js +80 -3
  5. package/dist/add-route.js +26 -2
  6. package/dist/adopt.d.ts +74 -0
  7. package/dist/adopt.js +314 -0
  8. package/dist/apply-design.d.ts +12 -1
  9. package/dist/apply-design.js +36 -4
  10. package/dist/cli.js +103 -3
  11. package/dist/derive-architecture.d.ts +37 -2
  12. package/dist/derive-architecture.js +92 -7
  13. package/dist/design-package.js +3 -2
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/lib/chrome-readers.d.ts +21 -0
  17. package/dist/lib/chrome-readers.js +100 -0
  18. package/dist/lib/declared-env.d.ts +142 -0
  19. package/dist/lib/declared-env.js +985 -0
  20. package/dist/lib/env-catalog.d.ts +17 -0
  21. package/dist/lib/env-catalog.js +234 -0
  22. package/dist/lib/extract-module-bundle.d.ts +20 -0
  23. package/dist/lib/extract-module-bundle.js +53 -6
  24. package/dist/lib/license-key-files.d.ts +6 -1
  25. package/dist/lib/license-key-files.js +9 -4
  26. package/dist/lib/module-tier.d.ts +2 -0
  27. package/dist/lib/module-tier.js +4 -0
  28. package/dist/module-architecture.d.ts +9 -0
  29. package/dist/module-architecture.js +82 -0
  30. package/dist/parse-args.d.ts +27 -0
  31. package/dist/parse-args.js +89 -0
  32. package/dist/programmatic.d.ts +28 -0
  33. package/dist/programmatic.js +181 -11
  34. package/dist/seed-flows.d.ts +65 -0
  35. package/dist/seed-flows.js +350 -0
  36. package/dist/site-chrome.d.ts +4 -0
  37. package/dist/site-chrome.js +34 -3
  38. package/dist/site-manifest.d.ts +55 -5
  39. package/dist/site-manifest.js +121 -12
  40. package/dist/site-roster.d.ts +13 -0
  41. package/dist/site-roster.js +22 -8
  42. package/dist/spoke-checkout.d.ts +42 -0
  43. package/dist/spoke-checkout.js +359 -0
  44. package/dist/surfaces.js +7 -0
  45. package/dist/sync-routes.js +29 -5
  46. package/dist/topology.d.ts +246 -0
  47. package/dist/topology.js +942 -0
  48. package/package.json +3 -3
  49. package/site-templates/designs/minimalism/DESIGN.md +3 -3
  50. package/templates/.env.example +10 -0
  51. package/templates/apps/api/CLAUDE.md +21 -4
  52. package/templates/apps/api/package.json +1 -0
  53. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
  54. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
  55. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
  56. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
  57. package/templates/apps/api/src/bin/spokes.ts +62 -1
  58. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
  59. package/templates/apps/api/src/config/index.ts +35 -0
  60. package/templates/apps/api/src/lib/__mocks__/prisma.ts +99 -0
  61. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
  62. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
  63. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
  64. package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
  65. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
  66. package/templates/apps/api/src/lib/agent-store.ts +95 -3
  67. package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
  68. package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
  69. package/templates/apps/api/src/lib/site.ts +37 -0
  70. package/templates/apps/api/src/lib/spoke-store.ts +41 -3
  71. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
  72. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
  73. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
  74. package/templates/apps/api/src/middleware/auth.ts +129 -0
  75. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
  76. package/templates/apps/api/src/openapi/spec.ts +38 -2
  77. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
  78. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
  79. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
  80. package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
  81. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +282 -0
  82. package/templates/apps/api/src/routes/leads/index.ts +69 -6
  83. package/templates/apps/api/src/routes/webhooks/README.md +39 -0
  84. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
  85. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
  86. package/templates/apps/api/src/routes/webhooks/stripe.ts +133 -2
  87. package/templates/apps/api/src/server.ts +105 -0
  88. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
  89. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
  90. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +492 -0
  91. package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
  92. package/templates/apps/api/src/services/default-tenant.ts +41 -7
  93. package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
  94. package/templates/apps/api/src/services/lead-routing.ts +278 -30
  95. package/templates/apps/api/src/services/person.ts +541 -0
  96. package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
  97. package/templates/apps/mcp/__tests__/index.test.ts +5 -5
  98. package/templates/apps/mcp/src/index.ts +6 -4
  99. package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
  100. package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
  101. package/templates/apps/web/.env.example +58 -5
  102. package/templates/apps/web/CLAUDE.md +1 -0
  103. package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
  106. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
  107. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
  110. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
  111. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
  112. package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
  113. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
  114. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
  115. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
  116. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +105 -11
  117. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +12 -8
  118. package/templates/apps/web/app/[locale]/layout.tsx +83 -18
  119. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
  120. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
  121. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
  122. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
  123. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
  124. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
  125. package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
  126. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
  127. package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
  128. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +104 -1
  129. package/templates/apps/web/app/openapi.json/route.ts +72 -7
  130. package/templates/apps/web/auth.ts +29 -2
  131. package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
  132. package/templates/apps/web/components/signed-in-nav.tsx +17 -3
  133. package/templates/apps/web/instrumentation.ts +7 -4
  134. package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
  135. package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
  136. package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
  137. package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
  138. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
  139. package/templates/apps/web/lib/__tests__/relay.test.ts +317 -0
  140. package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
  141. package/templates/apps/web/lib/account-config.ts +130 -1
  142. package/templates/apps/web/lib/api-origin.generated.ts +11 -0
  143. package/templates/apps/web/lib/closing-footer-path.ts +50 -0
  144. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
  145. package/templates/apps/web/lib/hub-session/config.ts +104 -0
  146. package/templates/apps/web/lib/hub-session/index.ts +23 -0
  147. package/templates/apps/web/lib/hub-session/seal.ts +82 -0
  148. package/templates/apps/web/lib/hub-session/session.ts +180 -0
  149. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
  150. package/templates/apps/web/lib/listings-config.ts +31 -0
  151. package/templates/apps/web/lib/listings.tsx +7 -3
  152. package/templates/apps/web/lib/meeting-slugs.ts +11 -1
  153. package/templates/apps/web/lib/next-path.ts +35 -0
  154. package/templates/apps/web/lib/relay.ts +209 -3
  155. package/templates/apps/web/lib/server-api.ts +11 -1
  156. package/templates/apps/web/lib/site-brand.tsx +22 -1
  157. package/templates/apps/web/lib/site-chrome.ts +3 -0
  158. package/templates/apps/web/lib/topology.generated.ts +33 -0
  159. package/templates/apps/web/messages/en.json +32 -3
  160. package/templates/apps/web/package.json +1 -1
  161. package/templates/database/CHANGELOG.md +51 -0
  162. package/templates/database/__tests__/agents.db.test.ts +85 -0
  163. package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
  164. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
  165. package/templates/database/__tests__/member-area.db.test.ts +245 -0
  166. package/templates/database/__tests__/person.db.test.ts +584 -0
  167. package/templates/database/auth/schema.prisma +24 -0
  168. package/templates/database/config/schema.prisma +13 -0
  169. package/templates/database/inbox/schema.prisma +111 -0
  170. package/templates/database/jest.config.cjs +7 -11
  171. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
  172. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
  173. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
  174. package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
  175. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
  176. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
  177. package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
  178. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
  179. package/templates/database/package.json +1 -1
  180. package/templates/database/scripts/person-backfill.sql +132 -0
  181. package/templates/database/tenancy.ts +53 -0
  182. package/templates/tools/check-declared-env.ts +37 -0
  183. package/templates/tools/declared-env.ts +1163 -0
  184. package/versions.json +35 -34
@@ -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
+ };
@@ -49,6 +49,10 @@ export interface ExtractModuleBundleOptions {
49
49
  existingFilePolicy?: ExistingFilePolicy;
50
50
  /** A caller check on the manifest's `patches` (#6653 D3). See `InstallBundleTreeOptions`. */
51
51
  checkPatches?: (patches: readonly ModulePatch[]) => void;
52
+ /** Destinations this scaffold does not carry (#7426 D4). See `InstallBundleTreeOptions`. */
53
+ skip?: (destination: string) => boolean;
54
+ /** Where an installMap destination lands instead (#7876 D10). See `InstallBundleTreeOptions`. */
55
+ remapDestination?: (destination: string) => string;
52
56
  }
53
57
  /**
54
58
  * Extract a fetched module bundle into `targetDir`, merging its files onto
@@ -131,6 +135,22 @@ export interface InstallBundleTreeOptions {
131
135
  * bundle's patches to the plus-config allowlist.
132
136
  */
133
137
  checkPatches?: (patches: readonly ModulePatch[]) => void;
138
+ /**
139
+ * Scaffold-relative destinations this scaffold does not carry (#7426 D4) —
140
+ * a spoke passes `topology.ts` `isSpokeExcluded`. A skipped destination is
141
+ * never written: a plain file or an `installMap` destination it names is
142
+ * dropped, and so is a `patches` entry whose target it names. It is NOT an
143
+ * ownership claim, and a skipped patch target is not a missing one. Every
144
+ * entry is still validated as before. Absent ⟹ nothing is skipped.
145
+ */
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;
134
154
  }
135
155
  /** See `InstallBundleTreeOptions.existingFilePolicy`. */
136
156
  export type ExistingFilePolicy = "overwrite" | "skip-existing";
@@ -385,6 +385,24 @@ function validateLayerOwnership(destinations, ownership, subject, isTemplate, pa
385
385
  }
386
386
  }
387
387
  }
388
+ /** Every regular file under `srcRoot`, as a `/`-separated relative path. */
389
+ function stagedFiles(srcRoot) {
390
+ const out = [];
391
+ const stack = [srcRoot];
392
+ while (stack.length > 0) {
393
+ const dir = stack.pop();
394
+ if (dir === undefined)
395
+ break;
396
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
397
+ const srcPath = join(dir, entry.name);
398
+ if (entry.isDirectory())
399
+ stack.push(srcPath);
400
+ else if (entry.isFile())
401
+ out.push(relative(srcRoot, srcPath).split(sep).join("/"));
402
+ }
403
+ }
404
+ return out;
405
+ }
388
406
  /**
389
407
  * Every scaffold-relative destination this tree will write: each unmapped
390
408
  * staged file at its own path (exactly the set `copyTree` lands, same walk,
@@ -457,6 +475,10 @@ export async function extractModuleBundle(options) {
457
475
  ...(options.checkPatches !== undefined
458
476
  ? { checkPatches: options.checkPatches }
459
477
  : {}),
478
+ ...(options.skip !== undefined ? { skip: options.skip } : {}),
479
+ ...(options.remapDestination !== undefined
480
+ ? { remapDestination: options.remapDestination }
481
+ : {}),
460
482
  });
461
483
  }
462
484
  finally {
@@ -484,13 +506,32 @@ export function installBundleTree(options) {
484
506
  const manifest = isTemplate
485
507
  ? readAilkTemplateManifest(sourceRoot, name, templateRequires, templateExtends)
486
508
  : readAilkModuleManifest(sourceRoot);
487
- const installMap = manifest?.installMap ?? {};
488
- validateInstallMap(installMap, sourceRoot, targetAbs, subject, isTemplate);
509
+ const declaredInstallMap = manifest?.installMap ?? {};
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
+ }
525
+ // #7426 D4 — a destination the scaffold does not carry is dropped here,
526
+ // after validation, so a spoke never receives it and never records it.
527
+ const skip = options.skip;
528
+ const isSkipped = (destination) => skip !== undefined && skip(normalize(destination).split(sep).join("/"));
529
+ const installMap = Object.fromEntries(Object.entries(remappedInstallMap).filter(([, destination]) => !isSkipped(destination)));
489
530
  // patches validation (spec 5418 § 3 D6) runs at the same point, before
490
531
  // any copy — a bad target/kind/op rejects the whole tree with nothing
491
532
  // written to targetDir, same posture as installMap and the tar-entry
492
533
  // validation above.
493
- const patches = manifest?.patches ?? [];
534
+ const declaredPatches = manifest?.patches ?? [];
494
535
  // #5514 (Medium) — content, never code (spec 5462 D9/D16): every `patches`
495
536
  // kind either edits a code file directly (`next-config`, `jest-config`)
496
537
  // or a manifest a template has no legitimate reason to touch
@@ -498,10 +539,13 @@ export function installBundleTree(options) {
498
539
  // content template) — no shipped template uses it, so the simplest
499
540
  // fail-closed reading refuses it outright rather than enumerating an
500
541
  // allowed subset that has no current user.
501
- if (isTemplate && patches.length > 0) {
542
+ if (isTemplate && declaredPatches.length > 0) {
502
543
  throw new Error(`${subject}: a template must not carry patches — content, never code (spec 5462 D9)`);
503
544
  }
504
- validatePatches(patches, targetAbs, subject);
545
+ // Every declared patch is validated, skipped or not (#7426 D4), exactly as
546
+ // the installMap is above; only then are the skipped ones dropped.
547
+ validatePatches(declaredPatches, targetAbs, subject);
548
+ const patches = declaredPatches.filter((patch) => !isSkipped(patch.target));
505
549
  options.checkPatches?.(patches);
506
550
  // The exclusion set copyTree/the content-only guard both use — built from
507
551
  // NORMALIZED installMap keys (#5514) so a "./x" key excludes the same
@@ -512,7 +556,10 @@ export function installBundleTree(options) {
512
556
  const excluded = new Set([
513
557
  AILK_TEMPLATE_JSON,
514
558
  AILK_MODULE_JSON,
515
- ...Object.keys(installMap).map((key) => normalize(key).split(sep).join("/")),
559
+ ...Object.keys(declaredInstallMap).map((key) => normalize(key).split(sep).join("/")),
560
+ // A plain file lands at its own staged path, so that path IS its
561
+ // destination: a skipped one is excluded from the copy like a mapped source.
562
+ ...(skip !== undefined ? stagedFiles(sourceRoot).filter(isSkipped) : []),
516
563
  ]);
517
564
  if (isTemplate) {
518
565
  // #6658 D7 — a template's roster of record lands as JSON, through its
@@ -20,5 +20,10 @@ export declare function appendLine(text: string, line: string): string;
20
20
  * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
21
21
  * `ailk.key` when no existing line covers it. Idempotent — a second call with
22
22
  * the same key changes no byte.
23
+ *
24
+ * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) has no
25
+ * `apps/api`, so it keeps the key in `ailk.key` alone.
23
26
  */
24
- export declare function writeLicenseKeyFiles(dir: string, key: string): void;
27
+ export declare function writeLicenseKeyFiles(dir: string, key: string, opts?: {
28
+ apiEnv?: boolean;
29
+ }): void;
@@ -85,13 +85,18 @@ function upsertEnvLine(existing, key) {
85
85
  * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
86
86
  * `ailk.key` when no existing line covers it. Idempotent — a second call with
87
87
  * the same key changes no byte.
88
+ *
89
+ * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) has no
90
+ * `apps/api`, so it keeps the key in `ailk.key` alone.
88
91
  */
89
- export function writeLicenseKeyFiles(dir, key) {
92
+ export function writeLicenseKeyFiles(dir, key, opts = {}) {
90
93
  assertLicenseKeyShape(key);
91
94
  writeOwnerOnly(join(dir, LICENSE_KEY_FILE), `${key}\n`);
92
- const envPath = join(dir, LICENSE_ENV_FILE);
93
- const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
94
- writeOwnerOnly(envPath, upsertEnvLine(env, key));
95
+ if (opts.apiEnv !== false) {
96
+ const envPath = join(dir, LICENSE_ENV_FILE);
97
+ const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
98
+ writeOwnerOnly(envPath, upsertEnvLine(env, key));
99
+ }
95
100
  const ignorePath = join(dir, ".gitignore");
96
101
  const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
97
102
  const covered = ignore
@@ -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 —
@@ -83,6 +83,15 @@ export interface PageRosterRow {
83
83
  * scaffold carries (#6940).
84
84
  */
85
85
  export interface ModuleSlice {
86
+ /**
87
+ * Admits the module on a spoke (`create-ailk --topology spoke`, #7426 D7).
88
+ * `true` → a spoke may carry it, and its bundle installs through the spoke
89
+ * filter (`topology.ts` `isSpokeExcluded`), so any `apps/api` or
90
+ * `database` part it carries is dropped. Unset → refused on a spoke.
91
+ * `topology.ts` `spokeModules()` reads it; a sibling issue admits its
92
+ * module by setting it here, never in a second list.
93
+ */
94
+ spoke?: true;
86
95
  /**
87
96
  * The paid UI package base-tree code loads through
88
97
  * `apps/web/lib/paid-sections.generated.ts` (#7536). Present → one row in the
@@ -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.
@@ -206,6 +223,7 @@ function payloadPaths(kind, name, destRoot) {
206
223
  export const MODULE_SLICES = {
207
224
  // ── marketing — thin presentation module (the default) ───────────────────
208
225
  marketing: {
226
+ spoke: true,
209
227
  webImports: [
210
228
  WEB_AUTH_ADAPTER_IMPORT,
211
229
  "packages/content-adapters",
@@ -287,6 +305,7 @@ export const MODULE_SLICES = {
287
305
  },
288
306
  // ── docs-blog — Pro module: both fumadocs surfaces (D-46) ────────────────
289
307
  "docs-blog": {
308
+ spoke: true,
290
309
  webImports: [WEB_AUTH_ADAPTER_IMPORT, "packages/fumadocs-schema"],
291
310
  packages: {
292
311
  "fumadocs-schema": {
@@ -797,6 +816,15 @@ export const MODULE_SLICES = {
797
816
  "apps/api/src/routes/workspaces/submissions.ts",
798
817
  "apps/api/src/routes/workspaces/subscribers.ts",
799
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,
800
828
  // #7399 — the two siblings #6216 left `workspace`-only. They register
801
829
  // under that same `workspace || multi-tenant` gate, so a scaffold with
802
830
  // `multi-tenant` and without `workspace` loaded them with no file behind
@@ -945,6 +973,10 @@ export const MODULE_SLICES = {
945
973
  "apps/api/src/routes/workspaces/submissions.ts",
946
974
  "apps/api/src/routes/workspaces/subscribers.ts",
947
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,
948
980
  // Co-owned with `multi-tenant` (#5853) — see that slice's entry.
949
981
  "apps/api/src/routes/workspaces/content.ts",
950
982
  ...ASSET_STORE_FILES,
@@ -1003,6 +1035,15 @@ export const MODULE_SLICES = {
1003
1035
  // scheduling-bearing scaffold's `apps/api` really does contain the billing,
1004
1036
  // checkout and email routes those imports are for.
1005
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,
1006
1047
  apps: {
1007
1048
  api: {
1008
1049
  path: "apps/api",
@@ -1339,6 +1380,33 @@ export const MODULE_SLICES = {
1339
1380
  // cannot place an unreached file either) is unchanged and still governs
1340
1381
  // where all eleven live.
1341
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,
1342
1410
  paidSectionsPackage: "@working-theory/ui-signal-collectors",
1343
1411
  ownedFiles: [
1344
1412
  "apps/api/src/routes/flows/index.ts",
@@ -1388,6 +1456,10 @@ export const MODULE_SLICES = {
1388
1456
  "apps/api/src/bin/deliverable-upload.ts",
1389
1457
  "apps/api/src/bin/session-retention-sweep.ts",
1390
1458
  "apps/api/src/bin/seed-presets.ts",
1459
+ // #7844 D7 — the operator flow-copy CLI and its service: flows code by
1460
+ // subject, and a Free scaffold carries no flows.
1461
+ "apps/api/src/bin/flows-copy.ts",
1462
+ "apps/api/src/services/flow-copy.ts",
1391
1463
  "apps/api/src/bin/listing-csv-sweep.ts",
1392
1464
  "apps/api/src/bin/followup-sweep.ts",
1393
1465
  "apps/api/src/bin/seed-waitlist-experiments.ts",
@@ -1649,6 +1721,16 @@ export const MODULE_SLICES = {
1649
1721
  // `lib/stripe.ts` and the webhook receiver this module owns — declared
1650
1722
  // here, not engineered around.
1651
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,
1652
1734
  paidSectionsPackage: "@working-theory/ui-commerce",
1653
1735
  apps: {
1654
1736
  api: {
@@ -26,6 +26,15 @@ export declare const RESERVED_SHAPES: readonly ["local-business-product"];
26
26
  /** The `--design` vocabulary (#6754 D1) — `STARTER_DESIGNS`, pinned by test. */
27
27
  export declare const RECOGNIZED_DESIGNS: readonly ["standard", "minimalism"];
28
28
  export type DesignName = (typeof RECOGNIZED_DESIGNS)[number];
29
+ /**
30
+ * The `--topology` vocabulary (#7426 D1) — orthogonal to `--shape`: any shape
31
+ * can be scaffolded as either. Declared here rather than imported from
32
+ * `topology.ts` for the reason `RECOGNIZED_MODULES` is: that module reads
33
+ * `MODULE_SLICES`, and importing it here closes a runtime cycle through
34
+ * `lib/module-tier.ts`. `spoke-topology.test.ts` pins the two lists equal.
35
+ */
36
+ export declare const RECOGNIZED_TOPOLOGIES: readonly ["hub", "spoke"];
37
+ export type TopologyName = (typeof RECOGNIZED_TOPOLOGIES)[number];
29
38
  export interface CliOptions {
30
39
  /** Positional target directory (the dir to scaffold into). Optional only
31
40
  * when `help` or `version` is set. */
@@ -94,6 +103,24 @@ export interface CliOptions {
94
103
  * flag is.
95
104
  */
96
105
  license?: string;
106
+ /**
107
+ * The scaffold's topology (`--topology`, #7426 D1). `hub` — the default,
108
+ * and the only value before #7426 — unless `--topology spoke`.
109
+ */
110
+ topology: TopologyName;
111
+ /**
112
+ * The raw `--hub-url` value (#7426 D2), present only on a spoke run that
113
+ * passed it. `scaffold()` validates and normalizes it, falling back to
114
+ * `AILK_API_URL`.
115
+ */
116
+ hubUrl?: string;
117
+ /** Spoke only: `false` with `--without-mcp` (#7426 D3), else `true`. */
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;
97
124
  }
98
125
  export declare class ParseError extends Error {
99
126
  constructor(message: string);