create-ailk 0.18.0 → 0.19.1

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 (218) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +11 -3
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/lib/license-key-files.d.ts +6 -3
  11. package/dist/lib/license-key-files.js +8 -2
  12. package/dist/lib/scaffold-render-sweep.d.ts +32 -0
  13. package/dist/lib/scaffold-render-sweep.js +76 -3
  14. package/dist/license-set.d.ts +1 -1
  15. package/dist/license-set.js +13 -8
  16. package/dist/module-architecture.js +128 -3
  17. package/dist/programmatic.js +11 -1
  18. package/dist/site-roster.js +21 -4
  19. package/dist/spoke-checkout.d.ts +11 -3
  20. package/dist/spoke-checkout.js +62 -12
  21. package/dist/starter-grid.d.ts +1 -1
  22. package/dist/surfaces.d.ts +14 -0
  23. package/dist/surfaces.js +170 -1
  24. package/dist/template-manifest.d.ts +4 -3
  25. package/dist/topology.d.ts +7 -2
  26. package/dist/topology.js +88 -3
  27. package/package.json +5 -4
  28. package/templates/.env.example +3 -3
  29. package/templates/.github/workflows/ci.yml +5 -0
  30. package/templates/apps/api/.env.example +17 -1
  31. package/templates/apps/api/CLAUDE.md +31 -7
  32. package/templates/apps/api/LICENSE +6 -5
  33. package/templates/apps/api/NOTICE +2 -0
  34. package/templates/apps/api/jest.config.cjs +4 -29
  35. package/templates/apps/api/package.json +1 -0
  36. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
  37. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
  38. package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
  39. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
  40. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
  41. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  42. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
  43. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
  44. package/templates/apps/api/src/lib/consent-store.ts +335 -0
  45. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  46. package/templates/apps/api/src/lib/stripe.ts +176 -3
  47. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
  48. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
  49. package/templates/apps/api/src/middleware/auth.ts +214 -46
  50. package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
  51. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
  52. package/templates/apps/api/src/routes/checkout/session.ts +3 -3
  53. package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
  54. package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
  55. package/templates/apps/api/src/routes/consent/index.ts +18 -0
  56. package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
  57. package/templates/apps/api/src/routes/leads/index.ts +19 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
  59. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
  60. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
  61. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
  62. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
  63. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
  64. package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
  65. package/templates/apps/api/src/server.ts +218 -16
  66. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
  67. package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
  68. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
  69. package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
  70. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
  71. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
  72. package/templates/apps/api/src/services/consent.ts +488 -3
  73. package/templates/apps/api/src/services/lead-routing.ts +135 -14
  74. package/templates/apps/api/src/services/person.ts +501 -1
  75. package/templates/apps/api/src/services/project-listings.ts +7 -2
  76. package/templates/apps/api/src/services/request-confirm.ts +52 -0
  77. package/templates/apps/api/src/services/revalidation.ts +11 -1
  78. package/templates/apps/api/vercel.json +2 -2
  79. package/templates/apps/mcp/.env.example +10 -0
  80. package/templates/apps/mcp/CLAUDE.md +9 -4
  81. package/templates/apps/mcp/LICENSE +6 -5
  82. package/templates/apps/mcp/NOTICE +2 -0
  83. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  84. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  85. package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
  86. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  87. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  88. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  89. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  90. package/templates/apps/mcp/jest.config.cjs +3 -44
  91. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  92. package/templates/apps/mcp/src/boot.ts +5 -23
  93. package/templates/apps/mcp/src/http-transport.ts +93 -73
  94. package/templates/apps/mcp/src/index.ts +2 -0
  95. package/templates/apps/mcp/src/request-context.ts +52 -0
  96. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  97. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  98. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  99. package/templates/apps/web/LICENSE +6 -5
  100. package/templates/apps/web/NOTICE +2 -0
  101. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
  115. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
  116. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  117. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  118. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
  119. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  123. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  124. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  125. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
  129. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  132. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  133. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  134. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  135. package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
  136. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  137. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
  138. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  139. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  140. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  141. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  142. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
  143. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  144. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  145. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  146. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  147. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  148. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  149. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
  150. package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
  151. package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
  152. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  153. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
  154. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  155. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  156. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  157. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  158. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  159. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  160. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  161. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  162. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  163. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  164. package/templates/apps/web/auth.ts +25 -2
  165. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  166. package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
  167. package/templates/apps/web/lib/account-config.ts +188 -52
  168. package/templates/apps/web/lib/auth-email.ts +45 -10
  169. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  170. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  171. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  172. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  173. package/templates/apps/web/lib/topology.generated.ts +33 -0
  174. package/templates/apps/web/messages/en.json +194 -33
  175. package/templates/database/CHANGELOG.md +37 -0
  176. package/templates/database/LICENSE +6 -5
  177. package/templates/database/NOTICE +2 -0
  178. package/templates/database/README.md +1 -1
  179. package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
  180. package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
  181. package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
  182. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  183. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  184. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  185. package/templates/database/__tests__/person-contact.db.test.ts +563 -0
  186. package/templates/database/__tests__/person.db.test.ts +3 -1
  187. package/templates/database/__tests__/seed.test.ts +54 -2
  188. package/templates/database/inbox/schema.prisma +166 -25
  189. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  190. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  191. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
  192. package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
  193. package/templates/database/package.json +3 -2
  194. package/templates/database/seed.ts +34 -2
  195. package/versions.json +36 -36
  196. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  197. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  198. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  199. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  200. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  201. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  202. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  203. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  204. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  205. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  206. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  207. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  208. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  209. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  210. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  211. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  212. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  213. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  214. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  215. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  216. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  217. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  218. package/templates/database/__tests__/build.test.ts +0 -131
@@ -77,8 +77,8 @@ const PUBLIC_SCHEDULING_VAR_PATTERN = [
77
77
  * live-key-shaped token in this file's source would itself trip the repo's
78
78
  * own live-key hard block (`rules/environment-boundaries.md`). `apps/api` is
79
79
  * excluded because `STRIPE_SECRET_KEY` legitimately lives there, gated by
80
- * `assertSandboxKey`'s sandbox-only allow-list at request time and by
81
- * `assertCommerceSandboxKey` at boot (server.ts) — this pattern catches a
80
+ * lib/stripe.ts's admitted-key guard at request time and by
81
+ * `admitCommerceStripeKey` at boot (server.ts) — this pattern catches a
82
82
  * live-shaped key leaking anywhere ELSE (a fixture, a client component, a
83
83
  * `NEXT_PUBLIC_` var).
84
84
  */
@@ -157,15 +157,43 @@ export const ARCHITECTURE_BASE = {
157
157
  * file behind it — the three operator route files, the two member route
158
158
  * files, the member resolution (`services/member-area.ts`) and the member
159
159
  * data boundary (`lib/member-store.ts`).
160
+ *
161
+ * #8169 D9 adds the five Phase 1 member reads registered under that same
162
+ * disjunction. Its two other reads — `routes/member/meeting-types.ts` and
163
+ * `availability.ts` — reach `services/scheduling/`, so they are the
164
+ * `scheduling` slice's, not listed here.
165
+ *
166
+ * #8170 D11 adds the four member writes registered under that same
167
+ * disjunction (the message POST rides `messages.ts`). The fifth —
168
+ * `routes/member/bookings.ts` — reaches `services/scheduling/`, so it is the
169
+ * `scheduling` slice's.
160
170
  */
161
171
  const MEMBER_AREA_FILES = [
162
172
  "apps/api/src/routes/workspaces/pieces.ts",
163
173
  "apps/api/src/routes/workspaces/plan.ts",
164
174
  "apps/api/src/routes/workspaces/messages-out.ts",
175
+ // #8171 D8 — the workspace Requests read; imports only pieces.ts,
176
+ // lib/tenant-db.ts and @working-theory/validation.
177
+ "apps/api/src/routes/workspaces/requests.ts",
165
178
  "apps/api/src/routes/member/pieces.ts",
166
179
  "apps/api/src/routes/member/messages.ts",
180
+ "apps/api/src/routes/member/purchases.ts",
181
+ "apps/api/src/routes/member/appointments.ts",
182
+ "apps/api/src/routes/member/artifacts.ts",
183
+ "apps/api/src/routes/member/request-form.ts",
184
+ "apps/api/src/routes/member/billing.ts",
185
+ "apps/api/src/routes/member/requests.ts",
186
+ "apps/api/src/routes/member/artifact-review.ts",
187
+ "apps/api/src/routes/member/profile.ts",
188
+ "apps/api/src/routes/member/deletion-request.ts",
189
+ // #8438 D6 — the member's own consents; the same gate as the writes above.
190
+ "apps/api/src/routes/member/consents.ts",
167
191
  "apps/api/src/services/member-area.ts",
168
192
  "apps/api/src/lib/member-store.ts",
193
+ // #8226 D12 — the member-request match CLI; both import member-store.ts,
194
+ // which a build without the member slice does not carry.
195
+ "apps/api/src/lib/member-request-match.ts",
196
+ "apps/api/src/bin/member-request-match.ts",
169
197
  ];
170
198
  /**
171
199
  * The workspace asset store (#5854), CO-OWNED by the `workspace` and
@@ -759,6 +787,10 @@ export const MODULE_SLICES = {
759
787
  "apps/api/src/routes/workspaces/index.ts",
760
788
  "apps/api/src/routes/workspaces/sessions.ts",
761
789
  "apps/api/src/routes/workspaces/config.ts",
790
+ // #8438 D8 — the Admin owner's consents to Working Theory: registered
791
+ // under `isModuleEnabled("multi-tenant")` alone, the module that
792
+ // contributes the Admin app.
793
+ "apps/api/src/routes/consent/platform.ts",
762
794
  // Co-owned with `workspace` (#5853): the content-write scope is gated on
763
795
  // content-write AND (workspace || multi-tenant) and this file imports
764
796
  // lib/tenant-db.ts, so each tenancy slice must deliver it.
@@ -864,6 +896,18 @@ export const MODULE_SLICES = {
864
896
  "apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx",
865
897
  "apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx",
866
898
  "apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx",
899
+ // #7389 — the three api suites that enable `multi-tenant` ALONE and so
900
+ // reach files this slice delivers (`services/tenant-context.ts` and the
901
+ // `routes/workspaces/` index). Each is listed individually, never as a
902
+ // glob (the `scheduling` slice's § D12 property 1: a glob entry is a
903
+ // silent no-op), so it ships to a keyed scaffold with the slice and is
904
+ // stripped from a keyless one with it — instead of a keyless scaffold
905
+ // carrying a test that raises `createNoMappedModuleFoundError`. A suite
906
+ // that enables SEVERAL paid modules cannot be owned by any one of
907
+ // them; those are `EXCLUDE_TEMPLATE_DIRS` entries below.
908
+ "apps/api/src/services/__tests__/lead-promotion.test.ts",
909
+ "apps/api/src/services/__tests__/leads-inbox.test.ts",
910
+ "apps/api/src/__tests__/content-write-production-boot.test.ts",
867
911
  "apps-paid/admin/**",
868
912
  ],
869
913
  // The whole-app glob above lands its files at their REPO-relative path
@@ -1232,6 +1276,13 @@ export const MODULE_SLICES = {
1232
1276
  // records: "each tenancy slice must deliver it").
1233
1277
  "apps/api/src/lib/tenant-db.ts",
1234
1278
  "apps/api/src/services/tenant-context.ts",
1279
+ // #7389 — `routes/workspaces/__tests__/meetings.test.ts` and
1280
+ // `meetings-connection.test.ts` (listed below) register the real
1281
+ // `tenantPreHandlerPlugin` in front of `meetings.ts`, so a scheduling
1282
+ // scaffold without `multi-tenant` could not load them. It imports only
1283
+ // `services/tenant-context.ts` (owned above) and the base
1284
+ // `default-tenant.ts`, so co-owning it keeps the closure tight.
1285
+ "apps/api/src/middleware/tenant.ts",
1235
1286
  // #7533 — a scheduling connection per workspace. `connections.ts` (the
1236
1287
  // cipher + the one SchedulingConnection data-access boundary) and
1237
1288
  // `resolve.ts` (the workspace-aware provider resolution the meetings,
@@ -1248,6 +1299,23 @@ export const MODULE_SLICES = {
1248
1299
  "apps/api/src/routes/schedule/__tests__/public-connection.test.ts",
1249
1300
  "apps/api/src/routes/webhooks/__tests__/calcom-connection.test.ts",
1250
1301
  "apps/api/src/config/__tests__/scheduling-credential-key.test.ts",
1302
+ // #8169 D9 — the two member reads that reach `services/scheduling/`,
1303
+ // registered under `scheduling && (workspace || multi-tenant)`, plus
1304
+ // the three member files they import, which this slice then CO-OWNS
1305
+ // with the two tenancy slices (`MEMBER_AREA_FILES`) — exactly as it
1306
+ // co-owns `lib/tenant-db.ts` for `routes/workspaces/meetings.ts` above.
1307
+ "apps/api/src/routes/member/meeting-types.ts",
1308
+ "apps/api/src/routes/member/availability.ts",
1309
+ // #8170 D11 — the member booking, the same shape.
1310
+ "apps/api/src/routes/member/bookings.ts",
1311
+ "apps/api/src/routes/member/pieces.ts",
1312
+ "apps/api/src/services/member-area.ts",
1313
+ "apps/api/src/lib/member-store.ts",
1314
+ // #7389 — the two api suites that enable `scheduling` alone, so they
1315
+ // strip with `routes/schedule/index.ts`, which they load by a computed
1316
+ // specifier the import-graph closure cannot follow.
1317
+ "apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts",
1318
+ "apps/api/src/__tests__/openapi-keyless-hub.test.ts",
1251
1319
  ],
1252
1320
  // pageContributions (spec 5478; parent § D6 shape 1) — the booker, the
1253
1321
  // per-booking-type page and the confirmation page, whose route files
@@ -1463,6 +1531,10 @@ export const MODULE_SLICES = {
1463
1531
  "apps/api/src/bin/listing-csv-sweep.ts",
1464
1532
  "apps/api/src/bin/followup-sweep.ts",
1465
1533
  "apps/api/src/bin/seed-waitlist-experiments.ts",
1534
+ // #7389 — the listing-directory suite enables `signal-collectors` alone
1535
+ // and loads `routes/waitlist-signups/index.ts` through the server it
1536
+ // builds, so it strips with that route. Individually, never as a glob.
1537
+ "apps/api/src/routes/project-listings/__tests__/directory.test.ts",
1466
1538
  "apps/mcp/src/tools/get_flow.ts",
1467
1539
  "apps/mcp/src/tools/submit_flow_step.ts",
1468
1540
  // #3591 PR-E — the signed-in MCP tools that twin the tenant-scoped
@@ -1777,7 +1849,7 @@ export const MODULE_SLICES = {
1777
1849
  {
1778
1850
  pattern: STRIPE_LIVE_KEY_PATTERN,
1779
1851
  paths_excluded: ["apps/api/**"],
1780
- reason: "The commerce credential belongs to apps/api, behind assertSandboxKey's sandbox-only allow-list. A live-mode-shaped key anywhere else is a leak, not a config choice",
1852
+ reason: "The commerce credential belongs to apps/api, behind lib/stripe.ts's admitted-key guard. A live-mode-shaped key anywhere else is a leak, not a config choice",
1781
1853
  },
1782
1854
  ],
1783
1855
  // ownedFiles (S1, #5088). SOURCE files only (#5182 D6) — no __tests__/,
@@ -2013,6 +2085,44 @@ export function coOwnedModuleDestinations() {
2013
2085
  // docs/specs/7315-a-gated-pro-tool-re-spec.md D4) — same reason as the
2014
2086
  // three suites above: it imports `SITE_ARGS` from the monorepo's root
2015
2087
  // `tests/parity/` tree, which no scaffold carries.
2088
+ // - apps/mcp/__tests__/write-tool-guard.test.ts (#8071, spec
2089
+ // docs/specs/8071-mcp-write-guard-spec.md D4) — it reads every anonymous
2090
+ // catalog tool's handler file, and a scaffold carries only the handler
2091
+ // files of the modules it selected while the catalog lists every tool.
2092
+ // `WRITE_TOOLS` is edited in the monorepo, which is where it must run.
2093
+ // - the suites a keyless scaffold's own `pnpm test` cannot run (#7389) —
2094
+ // file entries, matched exactly. Two classes, neither of which a slice can
2095
+ // own (a slice's `ownedFiles` carries a suite that needs ONE paid module;
2096
+ // the six api suites that do are listed under `multi-tenant`, `scheduling`
2097
+ // and `signal-collectors` above, and a keyed scaffold runs them):
2098
+ // * monorepo fixtures — each reads a path only this repo has:
2099
+ // `services/__tests__/aeo-score.test.ts` (packages/aeo's result.json),
2100
+ // `routes/leads/__tests__/index.test.ts` (packages/validation's
2101
+ // newsletter-lead fixture), `routes/workspaces/__tests__/
2102
+ // inbox-wire-schemas.test.ts` (the root `tests/parity/` core),
2103
+ // mcp's `workspace-surface.test.ts` (the same core) and mcp's
2104
+ // `audit.test.ts` (the T1 fixture site's `project.yaml`).
2105
+ // * whole-composition suites — each builds the real server or reads
2106
+ // the whole tree under SEVERAL module sets, or asserts the
2107
+ // monorepo's own identity, so no single slice is its owner and a
2108
+ // scaffold that selected only some of the modules would fail it:
2109
+ // `server.test.ts` (asserts the monorepo's "AI Launch Kit API" name,
2110
+ // and a `signal-collectors` case), `cors.test.ts` (multi-tenant +
2111
+ // scheduling), `root-modules.test.ts`, `module-exclusion.test.ts`,
2112
+ // `openapi/__tests__/surface-drift.test.ts`,
2113
+ // `review-surface-registration.test.ts` and
2114
+ // `workspace-module-registration.test.ts` (each a matrix over module
2115
+ // sets), and mcp's `parity.test.ts` (counts the Server Actions and
2116
+ // tool handlers of the FULL tree, which a stripped scaffold lacks).
2117
+ // The exclusion is per file, not a `testPathIgnorePatterns` entry in the
2118
+ // scaffold's jest config: that would hide these suites from a keyed
2119
+ // scaffold too, and from the suites a slice owns.
2120
+ // - database/__tests__/build.test.ts (#7959) — the monorepo build probe
2121
+ // (#7565). Its `beforeAll` symlinks `REPO_ROOT/packages` and copies
2122
+ // `REPO_ROOT/tsconfig.base.json`, neither of which a scaffold has, so it
2123
+ // red the scaffold's database jest run. Matched exactly, not by basename
2124
+ // in `EXCLUDE_FILES`, which would drop a `build.test.ts` anywhere in the
2125
+ // snapshot; every other database suite still ships.
2016
2126
  export const EXCLUDE_TEMPLATE_DIRS = [
2017
2127
  "apps/web/__tests__",
2018
2128
  "apps/web/app/__tests__",
@@ -2023,6 +2133,21 @@ export const EXCLUDE_TEMPLATE_DIRS = [
2023
2133
  "apps/api/src/__tests__/api-parity-paid.test.ts",
2024
2134
  "apps/mcp/__tests__/api-parity.test.ts",
2025
2135
  "apps/mcp/__tests__/pro-tool-call-gate.test.ts",
2136
+ "apps/mcp/__tests__/write-tool-guard.test.ts",
2137
+ "apps/api/src/services/__tests__/aeo-score.test.ts",
2138
+ "apps/api/src/routes/leads/__tests__/index.test.ts",
2139
+ "apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts",
2140
+ "apps/api/src/__tests__/server.test.ts",
2141
+ "apps/api/src/__tests__/cors.test.ts",
2142
+ "apps/api/src/__tests__/root-modules.test.ts",
2143
+ "apps/api/src/__tests__/module-exclusion.test.ts",
2144
+ "apps/api/src/openapi/__tests__/surface-drift.test.ts",
2145
+ "apps/api/src/__tests__/review-surface-registration.test.ts",
2146
+ "apps/api/src/__tests__/workspace-module-registration.test.ts",
2147
+ "apps/mcp/__tests__/workspace-surface.test.ts",
2148
+ "apps/mcp/__tests__/parity.test.ts",
2149
+ "apps/mcp/__tests__/audit.test.ts",
2150
+ "database/__tests__/build.test.ts",
2026
2151
  ];
2027
2152
  export function paidModuleTemplateExclusions(snapshotRoots, repoRoot) {
2028
2153
  const roots = new Set(snapshotRoots);
@@ -45,7 +45,7 @@ import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
45
45
  import { planSiteManifest } from "./site-manifest.js";
46
46
  import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, withMeetPlaceholderSeeds, } from "./site-roster.js";
47
47
  import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
48
- import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
48
+ import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, isUnservedSurfaceContent, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
49
49
  import { syncRoutes } from "./sync-routes.js";
50
50
  import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, templateTreeDir, } from "./template-manifest.js";
51
51
  import { SPOKE_ONLY_FILES, TopologyError, applySpokeTransforms, isSpokeExcluded, normalizeHubSignInUrl, normalizeHubUrl, readSiteTopology, spokeExclusions, spokeDestination, spokeModuleRefusal, spokeModules, writeHubOriginEnv, writeHubSignInEnv, } from "./topology.js";
@@ -573,7 +573,14 @@ export async function scaffold(opts) {
573
573
  versions,
574
574
  markers,
575
575
  ownership,
576
+ // #8427 D4 — a module bundle never writes a file inside the content
577
+ // directory of a gateable surface the result does not enable (the
578
+ // signal-collectors `projects`-surface seeds, on a scaffold whose
579
+ // roster carries no /projects route). The spoke filter still drops
580
+ // whatever it already drops; remapDestination is unchanged.
576
581
  ...spokeSkip,
582
+ skip: (destination) => spokeSkip.skip?.(destination) === true ||
583
+ isUnservedSurfaceContent(destination, result.surfaces),
577
584
  });
578
585
  }
579
586
  // The template lands LAST — after `scaffoldSync` and after every module
@@ -747,11 +754,14 @@ export async function scaffold(opts) {
747
754
  // two return pages, once every layer has landed (so the spoke filter has
748
755
  // already dropped the hub-checkout files) and before the manifest, whose
749
756
  // code-route rows then record /checkout/thank-you and /checkout/unavailable.
757
+ // #8217 D8 — a spoke that signs in through its hub also gets the billing
758
+ // action and the web seam render that reaches it.
750
759
  const spokeCommerce = spoke !== undefined && result.modules.includes("commerce");
751
760
  if (spokeCommerce) {
752
761
  writeSpokeCheckoutSlice(result.targetDir, {
753
762
  site: spokeCheckoutSite(projectName),
754
763
  versions,
764
+ signIn: spoke.signIn,
755
765
  });
756
766
  }
757
767
  // #6944 — the manifest records what this scaffold IS, once every layer has
@@ -33,6 +33,7 @@ import { locatePageSections } from "./design-presets.js";
33
33
  import { diffTwoStrings } from "./lib/extract-module-bundle.js";
34
34
  import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
35
35
  import { substituteMarkers } from "./substitute.js";
36
+ import { isUnservedSurfaceContent, resolveSurfaces } from "./surfaces.js";
36
37
  import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, shippedManifestPath, templateTreeDir, } from "./template-manifest.js";
37
38
  /** The site-root file that is the roster of record (#6658 D7). */
38
39
  export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
@@ -613,7 +614,7 @@ function spliceRows(file, rows) {
613
614
  return text.slice(0, open + 1) + body + text.slice(close);
614
615
  }
615
616
  /** Every `content/en/**.mdx` some layer installs into a site of this template and module set. */
616
- function layerInstalledSeeds(template, modules, opts) {
617
+ function layerInstalledSeeds(template, modules, opts, surfaces) {
617
618
  const out = new Set();
618
619
  for (const root of [
619
620
  opts.templatesDir,
@@ -640,7 +641,15 @@ function layerInstalledSeeds(template, modules, opts) {
640
641
  for (const path of row.content ?? [])
641
642
  out.add(path);
642
643
  }
643
- destinations.push(...Object.values(slice.installMap ?? {}));
644
+ // #8427 D4 — a module installMap destination the scaffold's own
645
+ // surfaces do not serve is not "installed": the writer
646
+ // (programmatic.ts's module-bundle loop, add-module.ts) declines the
647
+ // write, so the reader must agree or report a seed that is not there.
648
+ for (const dest of Object.values(slice.installMap ?? {})) {
649
+ if (isUnservedSurfaceContent(dest, surfaces))
650
+ continue;
651
+ destinations.push(dest);
652
+ }
644
653
  }
645
654
  for (const to of destinations) {
646
655
  if (to.startsWith("content/en/") && to.endsWith(".mdx"))
@@ -666,7 +675,15 @@ export function siteRosterProblems(opts) {
666
675
  const roster = readSiteRoster(opts.targetDir);
667
676
  if (roster === undefined)
668
677
  return [];
669
- const { modules } = readArchitectureHeader(readFileSync(join(opts.targetDir, "architecture.yaml"), "utf8"));
678
+ const { modules, surfaces: headerSurfaces } = readArchitectureHeader(readFileSync(join(opts.targetDir, "architecture.yaml"), "utf8"));
679
+ // #8427 D4 — the FULL effective surface set (module-contributed surfaces
680
+ // unioned with the header's own explicit list), the same set the writer
681
+ // resolves. Throws SurfaceError on a header surface that is unknown or
682
+ // owned by a module the site lacks — intended: `add-module` already
683
+ // refuses such a header before its first write, so a direct caller
684
+ // reaches the same refusal instead of a report built on a header the
685
+ // scaffold would never actually write.
686
+ const surfaces = resolveSurfaces(modules, headerSurfaces);
670
687
  const problems = [];
671
688
  const routes = roster.rows.map((r) => r.value["route"]);
672
689
  const seen = new Set();
@@ -715,7 +732,7 @@ export function siteRosterProblems(opts) {
715
732
  });
716
733
  // #6942 — the Free /meet placeholder seeds the scheduling module's rows keep.
717
734
  const declared = withMeetPlaceholderSeeds(effective);
718
- const installed = [...layerInstalledSeeds(roster.template, modules, opts)]
735
+ const installed = [...layerInstalledSeeds(roster.template, modules, opts, surfaces)]
719
736
  .filter((path) => existsSync(join(opts.targetDir, path)))
720
737
  .sort();
721
738
  for (const path of installed) {
@@ -1,6 +1,7 @@
1
1
  import type { EnvCatalogEntry, PlanEnvRowsOptions } from "./lib/declared-env.js";
2
2
  import { type InstallReportRow } from "./lib/extract-module-bundle.js";
3
3
  import { type WorkspaceVersionMap } from "./substitute.js";
4
+ import { type SignInMode } from "./topology.js";
4
5
  /** The checkout package a spoke's two route files import. */
5
6
  export declare const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
6
7
  /** The site's checkout data — written once, the site's from then on. */
@@ -48,13 +49,19 @@ export declare const CHECKOUT_E2E_WORKFLOW: string;
48
49
  /**
49
50
  * The slice for `site`, in order: the two route files, the config, the two
50
51
  * pages (D3), then the checkout e2e workflow (#7839 D8), which does not
51
- * depend on `site`. Throws `TopologyError` for an invalid stamp (SEC-7838-10).
52
+ * depend on `site`. With `signIn: "hub"` the billing action is the third
53
+ * pair, so the first three paths are `SPOKE_CHECKOUT_ROUTE_FILES` (#8217 D8).
54
+ * Throws `TopologyError` for an invalid stamp (SEC-7838-10).
52
55
  */
53
- export declare function renderSpokeCheckoutFiles(site: string): [string, string][];
56
+ export declare function renderSpokeCheckoutFiles(site: string, opts?: {
57
+ signIn?: SignInMode | undefined;
58
+ }): [string, string][];
54
59
  /**
55
60
  * Write the slice into `targetDir` (D3). A file that already exists is left
56
61
  * byte-for-byte and reported `skipped` — the config is the site's once
57
- * written. Adds the package to `apps/web/package.json` when absent, and
62
+ * written. With `signIn: "hub"` (#8217 D8) the slice carries the billing
63
+ * action, and the web seam — generated, so always rewritten — becomes the
64
+ * billing render: reported `patched` when it changed, not at all when equal. Adds the package to `apps/web/package.json` when absent, and
58
65
  * raises a plain pin older than the one it writes (#7839): the generated
59
66
  * checkout e2e workflow runs a command a `^0.2.0` pin never reaches. Any
60
67
  * other range — newer, `workspace:`, a tag — is the site's and is kept.
@@ -64,4 +71,5 @@ export declare function renderSpokeCheckoutFiles(site: string): [string, string]
64
71
  export declare function writeSpokeCheckoutSlice(targetDir: string, opts: {
65
72
  site: string;
66
73
  versions: WorkspaceVersionMap;
74
+ signIn?: SignInMode | undefined;
67
75
  }): InstallReportRow[];
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * The spoke checkout slice (#7838) — what a spoke carrying `commerce` gets in
3
- * `apps/web`: the buy route and the Stripe webhook route (the only two files
4
- * allowed to import the checkout package, #7873), the site's own
5
- * `lib/checkout-config.ts`, and the thank-you and unavailable return pages.
3
+ * `apps/web`: the buy route and the Stripe webhook route (allowed to import
4
+ * the checkout package, #7873), the site's own `lib/checkout-config.ts`, and
5
+ * the thank-you and unavailable return pages. A spoke that signs in through
6
+ * its hub also gets the billing action, the third file allowed to import the
7
+ * package, and the web seam render that reaches it (#8217 D6–D8).
6
8
  *
7
9
  * Every guarantee lives in `@working-theory/spoke-checkout` and arrives by a
8
10
  * package bump; these files are thin and, apart from the config's one stamp
@@ -23,7 +25,7 @@ import { SPOKE_CHECKOUT_ROUTE_FILES } from "./derive-architecture.js";
23
25
  import { ENV_CATALOG } from "./lib/env-catalog.js";
24
26
  import { diffTwoStrings, } from "./lib/extract-module-bundle.js";
25
27
  import { substituteWorkspaceVersions, } from "./substitute.js";
26
- import { TopologyError } from "./topology.js";
28
+ import { TopologyError, WEB_SEAM_PATH, renderWebSeam, } from "./topology.js";
27
29
  /** The checkout package a spoke's two route files import. */
28
30
  export const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
29
31
  const API_CLIENT_PACKAGE = "@working-theory/api-client";
@@ -131,6 +133,33 @@ const WEBHOOK_ROUTE = renderRoute({
131
133
  waiver: "the endpoint URL registered in Stripe, not a versioned API contract",
132
134
  exportLine: "export const { POST } = createSpokeCheckout(checkoutConfig).webhook;",
133
135
  });
136
+ /**
137
+ * The spoke billing action (#8217 D6): wiring only, byte-identical across
138
+ * sites, so every guarantee arrives by a package bump. A plain server module
139
+ * (C2): its two exports take no argument, and the hub's read is its only
140
+ * source of customer ids.
141
+ */
142
+ const BILLING_ACTION = [
143
+ "// The spoke billing action — GENERATED by create-ailk (#8217). Do not edit:",
144
+ "// every guarantee lives in the spoke-checkout package and arrives by a",
145
+ "// package bump. A plain server module, not a Server Action file: neither",
146
+ "// export takes an argument, and the hub's GET /v1/member/billing is the only",
147
+ "// source of customer ids. billing-source.ts reaches it through",
148
+ "// lib/topology.generated.ts and never imports the package.",
149
+ `import { member } from "${API_CLIENT_PACKAGE}";`,
150
+ `import { createSpokeCheckout } from "${SPOKE_CHECKOUT_PACKAGE}";`,
151
+ "",
152
+ `import { checkoutConfig } from "../../../../../lib/checkout-config";`,
153
+ `import { forwardedCookieInit } from "../../../../../lib/server-api";`,
154
+ "",
155
+ "const billing = createSpokeCheckout(checkoutConfig).billing.forMember(",
156
+ " async () => member.billing(await forwardedCookieInit()),",
157
+ ");",
158
+ "",
159
+ "export const readSpokeBilling = billing.read;",
160
+ "export const spokeBillingPortalUrl = billing.portalUrl;",
161
+ "",
162
+ ].join("\n");
134
163
  /** The site's checkout data (D5). Only the `const SITE` line varies by site. */
135
164
  function renderConfig(site) {
136
165
  return [
@@ -138,9 +167,10 @@ function renderConfig(site) {
138
167
  "//",
139
168
  "// Written once by create-ailk and yours from then on: create-ailk never",
140
169
  "// rewrites it. The buy route and the Stripe webhook route build their",
141
- "// handlers from it. Those two are the only files allowed to import the",
142
- "// checkout package (architecture.yaml), so this file never imports it,",
143
- "// stripe-client or the stripe SDK. Keep it to data and the fulfil callback.",
170
+ "// handlers from it, and so does the billing action on a spoke that signs",
171
+ "// in through its hub. Only those files may import the checkout package",
172
+ "// (architecture.yaml), so this file never imports it, stripe-client or the",
173
+ "// stripe SDK. Keep it to data and the fulfil callback.",
144
174
  "//",
145
175
  "// site Stamped on every Checkout Session as metadata.site. This site's",
146
176
  "// webhook acts only on its own stamp, and the hub ignores any stamp",
@@ -462,15 +492,20 @@ jobs:
462
492
  /**
463
493
  * The slice for `site`, in order: the two route files, the config, the two
464
494
  * pages (D3), then the checkout e2e workflow (#7839 D8), which does not
465
- * depend on `site`. Throws `TopologyError` for an invalid stamp (SEC-7838-10).
495
+ * depend on `site`. With `signIn: "hub"` the billing action is the third
496
+ * pair, so the first three paths are `SPOKE_CHECKOUT_ROUTE_FILES` (#8217 D8).
497
+ * Throws `TopologyError` for an invalid stamp (SEC-7838-10).
466
498
  */
467
- export function renderSpokeCheckoutFiles(site) {
499
+ export function renderSpokeCheckoutFiles(site, opts = {}) {
468
500
  assertSite(site);
469
- const [buyPath, webhookPath] = SPOKE_CHECKOUT_ROUTE_FILES;
501
+ const [buyPath, webhookPath, billingActionPath] = SPOKE_CHECKOUT_ROUTE_FILES;
470
502
  const [thankYouPath, unavailablePath] = SPOKE_CHECKOUT_PAGE_FILES;
471
503
  return [
472
504
  [buyPath, BUY_ROUTE],
473
505
  [webhookPath, WEBHOOK_ROUTE],
506
+ ...(opts.signIn === "hub"
507
+ ? [[billingActionPath, BILLING_ACTION]]
508
+ : []),
474
509
  [SPOKE_CHECKOUT_CONFIG_PATH, renderConfig(site)],
475
510
  [thankYouPath, THANK_YOU_PAGE],
476
511
  [unavailablePath, UNAVAILABLE_PAGE],
@@ -516,7 +551,9 @@ function isOlderPin(current, wanted) {
516
551
  /**
517
552
  * Write the slice into `targetDir` (D3). A file that already exists is left
518
553
  * byte-for-byte and reported `skipped` — the config is the site's once
519
- * written. Adds the package to `apps/web/package.json` when absent, and
554
+ * written. With `signIn: "hub"` (#8217 D8) the slice carries the billing
555
+ * action, and the web seam — generated, so always rewritten — becomes the
556
+ * billing render: reported `patched` when it changed, not at all when equal. Adds the package to `apps/web/package.json` when absent, and
520
557
  * raises a plain pin older than the one it writes (#7839): the generated
521
558
  * checkout e2e workflow runs a command a `^0.2.0` pin never reaches. Any
522
559
  * other range — newer, `workspace:`, a tag — is the site's and is kept.
@@ -524,7 +561,7 @@ function isOlderPin(current, wanted) {
524
561
  * missing or malformed `apps/web/package.json`) throws before the first write.
525
562
  */
526
563
  export function writeSpokeCheckoutSlice(targetDir, opts) {
527
- const files = renderSpokeCheckoutFiles(opts.site);
564
+ const files = renderSpokeCheckoutFiles(opts.site, { signIn: opts.signIn });
528
565
  const range = JSON.parse(substituteWorkspaceVersions(`{"${SPOKE_CHECKOUT_PACKAGE}": "workspace:*"}`, opts.versions));
529
566
  const version = range[SPOKE_CHECKOUT_PACKAGE];
530
567
  const pkgPath = join(targetDir, WEB_PACKAGE_JSON);
@@ -541,6 +578,19 @@ export function writeSpokeCheckoutSlice(targetDir, opts) {
541
578
  writeFileSync(abs, text);
542
579
  rows.push({ path, action: "written" });
543
580
  }
581
+ if (opts.signIn === "hub") {
582
+ const seamPath = join(targetDir, WEB_SEAM_PATH);
583
+ const seamBefore = existsSync(seamPath) ? readFileSync(seamPath, "utf8") : "";
584
+ const seam = renderWebSeam("spoke", "hub", { spokeBilling: true });
585
+ if (seamBefore !== seam) {
586
+ writeFileSync(seamPath, seam);
587
+ rows.push({
588
+ path: WEB_SEAM_PATH,
589
+ action: "patched",
590
+ change: diffTwoStrings(WEB_SEAM_PATH, seamBefore, seam, true),
591
+ });
592
+ }
593
+ }
544
594
  const deps = pkg.dependencies ?? {};
545
595
  const current = Object.prototype.hasOwnProperty.call(deps, SPOKE_CHECKOUT_PACKAGE)
546
596
  ? deps[SPOKE_CHECKOUT_PACKAGE]
@@ -14,7 +14,7 @@
14
14
  * lives in `template-manifest.ts`, whose `resolveTemplate` is the one reader
15
15
  * that yields a template's roster. Nothing but those two files imports this one.
16
16
  */
17
- import type { CategorySlug } from "@working-theory/schema/categories";
17
+ import type { CategorySlug } from "./category-profile.js";
18
18
  import type { ModuleName } from "./parse-args.js";
19
19
  import type { TemplateLayout } from "./site-chrome.js";
20
20
  import type { StarterDesign, StarterShape, TemplateRosterRow } from "./template-manifest.js";
@@ -126,6 +126,20 @@ export declare function excludedRoutePaths(enabled: string[]): string[];
126
126
  * route has been removed would advertise URLs that 404.
127
127
  */
128
128
  export declare function excludedContentPaths(enabled: string[]): string[];
129
+ /**
130
+ * True when `path` (scaffold-relative, `/`-separated) lies inside one of the
131
+ * content directories `excludedContentPaths(enabled)` names, with `<locale>`
132
+ * matching any one path segment (#8427 D4).
133
+ *
134
+ * This is `excludedContentPaths`' first non-export reader — its own comment
135
+ * already named exactly this class ("orphan content makes `/sitemap.xml`
136
+ * advertise URLs that 404"). It is wired into the `skip` predicate
137
+ * `extractModuleBundle` already takes (`programmatic.ts`, `add-module.ts`),
138
+ * and into `layerInstalledSeeds`'s installed-seed count (`site-roster.ts`),
139
+ * so a module bundle never writes — and a roster check never counts — a
140
+ * seed under a gateable surface the scaffold does not enable.
141
+ */
142
+ export declare function isUnservedSurfaceContent(path: string, enabled: readonly string[]): boolean;
129
143
  /**
130
144
  * Route manifest (#4986): every template route directory, keyed relative to
131
145
  * ROUTE_ROOT (`apps/web/app/[locale]`), → the `@working-theory/*` fixed-group