create-ailk 0.19.1 → 0.21.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 (199) hide show
  1. package/README.md +84 -7
  2. package/component-catalog.json +184 -6
  3. package/component-catalog.md +20 -9
  4. package/dist/apply-design.d.ts +9 -0
  5. package/dist/apply-design.js +78 -31
  6. package/dist/cli.js +35 -0
  7. package/dist/copy.d.ts +41 -0
  8. package/dist/copy.js +87 -21
  9. package/dist/design-home.d.ts +54 -11
  10. package/dist/design-home.js +160 -22
  11. package/dist/lib/design-checks.d.ts +6 -1
  12. package/dist/lib/design-checks.js +23 -2
  13. package/dist/lib/fetch-template-snapshot.d.ts +21 -0
  14. package/dist/lib/fetch-template-snapshot.js +205 -0
  15. package/dist/lib/merge-three-way.d.ts +31 -0
  16. package/dist/lib/merge-three-way.js +65 -0
  17. package/dist/module-architecture.js +55 -2
  18. package/dist/programmatic.d.ts +7 -1
  19. package/dist/programmatic.js +76 -22
  20. package/dist/site-roster.d.ts +4 -2
  21. package/dist/site-roster.js +7 -5
  22. package/dist/spoke-checkout.d.ts +7 -0
  23. package/dist/spoke-checkout.js +8 -1
  24. package/dist/starter-grid.js +16 -0
  25. package/dist/surfaces.js +78 -2
  26. package/dist/sync-routes.d.ts +28 -0
  27. package/dist/sync-routes.js +123 -1
  28. package/dist/template-record.d.ts +59 -0
  29. package/dist/template-record.js +183 -0
  30. package/dist/update.d.ts +53 -0
  31. package/dist/update.js +606 -0
  32. package/package.json +3 -3
  33. package/site-templates/_base/ailk-template.json +2 -2
  34. package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
  35. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  36. package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
  37. package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
  38. package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
  39. package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
  41. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
  43. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
  46. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
  48. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
  49. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
  50. package/templates/.claude/cloudparity-repo-hooks.json +9 -0
  51. package/templates/.claude/hooks/architecture-check.sh +8 -0
  52. package/templates/.claude/rules/loop-contract.md +129 -0
  53. package/templates/.claude/settings.json +3 -2
  54. package/templates/.env.example +13 -0
  55. package/templates/CLAUDE.md +1 -1
  56. package/templates/CONVENTIONS.md +3 -2
  57. package/templates/README.md +26 -25
  58. package/templates/_gitignore +162 -0
  59. package/templates/apps/api/CLAUDE.md +18 -10
  60. package/templates/apps/api/jest.config.cjs +1 -0
  61. package/templates/apps/api/package.json +2 -1
  62. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
  63. package/templates/apps/api/src/__tests__/setup.ts +7 -0
  64. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
  65. package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
  66. package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
  67. package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
  68. package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
  69. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
  70. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
  71. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
  72. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
  73. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
  74. package/templates/apps/api/src/middleware/auth.ts +8 -4
  75. package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
  76. package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
  77. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
  78. package/templates/apps/api/src/routes/webhooks/README.md +33 -5
  79. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
  80. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
  81. package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
  82. package/templates/apps/api/src/server.ts +14 -2
  83. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
  84. package/templates/apps/api/src/services/audit-log.ts +72 -10
  85. package/templates/apps/api/src/services/default-tenant.ts +12 -3
  86. package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
  87. package/templates/apps/api/src/services/project-listings.ts +6 -1
  88. package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
  89. package/templates/apps/api/vercel.json +1 -1
  90. package/templates/apps/mcp/CLAUDE.md +19 -7
  91. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +139 -44
  92. package/templates/apps/mcp/jest.config.cjs +1 -0
  93. package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
  94. package/templates/apps/mcp/src/tools/proposals.ts +27 -0
  95. package/templates/apps/mcp/src/workspace-server.ts +16 -0
  96. package/templates/apps/web/CLAUDE.md +26 -1
  97. package/templates/apps/web/_gitignore +6 -0
  98. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
  99. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
  100. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
  101. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
  111. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
  115. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
  116. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
  117. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
  118. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
  119. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
  123. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
  124. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
  125. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
  129. package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
  132. package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
  133. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
  134. package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
  135. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  136. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  137. package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
  138. package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
  139. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
  140. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
  141. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
  143. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
  144. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
  145. package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
  146. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
  147. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +48 -4
  148. package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
  149. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  150. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
  151. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  152. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  153. package/templates/apps/web/app/[locale]/layout.tsx +46 -4
  154. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
  155. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  156. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  157. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  158. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  159. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  160. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
  161. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
  162. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
  163. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
  164. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
  165. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  166. package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
  167. package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
  168. package/templates/apps/web/app/robots.txt/route.ts +13 -10
  169. package/templates/apps/web/jest.config.cjs +4 -0
  170. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
  171. package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
  172. package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
  173. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +100 -9
  174. package/templates/apps/web/lib/account-config.ts +31 -16
  175. package/templates/apps/web/lib/consent-visitor.ts +103 -0
  176. package/templates/apps/web/lib/deck-page.ts +186 -0
  177. package/templates/apps/web/lib/relay.ts +20 -0
  178. package/templates/apps/web/messages/en.json +83 -9
  179. package/templates/apps/web/next.config.mjs +8 -2
  180. package/templates/database/CHANGELOG.md +48 -0
  181. package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
  182. package/templates/database/__tests__/person-notes.db.test.ts +249 -0
  183. package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
  184. package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
  185. package/templates/database/__tests__/seed.test.ts +39 -2
  186. package/templates/database/_gitignore +8 -0
  187. package/templates/database/inbox/schema.prisma +38 -12
  188. package/templates/database/jest.config.cjs +1 -0
  189. package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
  190. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
  191. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
  192. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
  193. package/templates/database/ops/schema.prisma +16 -0
  194. package/templates/database/package.json +1 -1
  195. package/templates/database/seed.ts +29 -0
  196. package/templates/project.yaml +6 -3
  197. package/templates/tools/jest/pin-timezone.cjs +34 -0
  198. package/versions.json +35 -35
  199. package/templates/.claude/rules/preserve-existing-style.md +0 -51
@@ -48,7 +48,9 @@
48
48
  // (`GET /v1/member/messages`) — rows of All. A message with a file has a
49
49
  // file action to this site's own member file read,
50
50
  // `account/inbox/message/[id]/file`, which re-serves the bytes — the
51
- // browser holds no hub credential.
51
+ // browser holds no hub credential. A message whose `action` is
52
+ // `onboarding` (#8515 D6) has an Open onboarding action to this site's
53
+ // own `/account/onboarding`.
52
54
  // - For-you source `piece-needs-client` — each piece waiting on the client,
53
55
  // with an Open action to its Inbox row. It shares the pieces read.
54
56
  //
@@ -283,23 +285,36 @@ const message: AccountInboxKind<MemberMessage> = {
283
285
  }));
284
286
  },
285
287
  renderDetail({ data }, { locale, t }) {
288
+ // An onboarding message (#8515 D6) links to this site's own onboarding
289
+ // page, before the file action when both apply; the body stays plain text.
290
+ const actions = [
291
+ ...(data.action === "onboarding"
292
+ ? [
293
+ {
294
+ key: "onboarding",
295
+ label: t("messages.openOnboarding"),
296
+ variant: "default" as const,
297
+ href: localeHref(locale, "/account/onboarding"),
298
+ },
299
+ ]
300
+ : []),
301
+ ...(data.file
302
+ ? [
303
+ {
304
+ key: "file",
305
+ label: t("messages.download", { filename: data.file.filename }),
306
+ variant: "default" as const,
307
+ href: localeHref(
308
+ locale,
309
+ `/account/inbox/message/${encodeURIComponent(data.id)}/file`,
310
+ ),
311
+ },
312
+ ]
313
+ : []),
314
+ ];
286
315
  return {
287
316
  lead: data.body,
288
- ...(data.file
289
- ? {
290
- actions: [
291
- {
292
- key: "file",
293
- label: t("messages.download", { filename: data.file.filename }),
294
- variant: "default" as const,
295
- href: localeHref(
296
- locale,
297
- `/account/inbox/message/${encodeURIComponent(data.id)}/file`,
298
- ),
299
- },
300
- ],
301
- }
302
- : {}),
317
+ ...(actions.length > 0 ? { actions } : {}),
303
318
  fields: {
304
319
  heading: t("messages.heading"),
305
320
  items: [
@@ -0,0 +1,103 @@
1
+ /**
2
+ * consent-visitor.ts — the banner's visitor id (#8438 D12, spec
3
+ * docs/specs/8438-consent-registry-and-ledger-step-spec.md).
4
+ *
5
+ * Owns `ailk-vid`, a random version-4 UUID the site's own server mints for a
6
+ * visitor who has not signed in — it is the subject a cookie-banner choice is
7
+ * logged against (`POST /v1/consent/cookie`), and it is NEVER the cookie that
8
+ * gates anything (`ailk-consent`, §1.4/C3 of the spec, stays the operative
9
+ * switch; this id only lets a later form or sign-in link the two).
10
+ *
11
+ * httpOnly, `Secure` in production, `SameSite=Lax`, path `/`, one year — a
12
+ * Server Action can read and mint it; client script never sees its value.
13
+ */
14
+
15
+ const CONSENT_VISITOR_COOKIE_NAME = "ailk-vid";
16
+ const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
17
+
18
+ const V4_UUID =
19
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
20
+
21
+ /** Whether `value` is a well-formed version-4 UUID. */
22
+ export function isValidConsentVisitorId(value: string): boolean {
23
+ return V4_UUID.test(value);
24
+ }
25
+
26
+ /** The subset of Next's `cookies()` jar a read needs. */
27
+ export interface ConsentVisitorCookieReader {
28
+ get(name: string): { value: string } | undefined;
29
+ }
30
+
31
+ /** The cookie attributes `ensureConsentVisitor` sets — a subset of Next's `ResponseCookie`. */
32
+ export interface ConsentVisitorCookieOptions {
33
+ httpOnly?: boolean;
34
+ secure?: boolean;
35
+ sameSite?: "lax" | "strict" | "none";
36
+ path?: string;
37
+ maxAge?: number;
38
+ }
39
+
40
+ /**
41
+ * The subset of Next's `cookies()` jar `ensureConsentVisitor` needs. The
42
+ * options type is narrow (not `Record<string, unknown>`) on purpose: Next's
43
+ * real `cookies().set` is itself a rest-tuple overload
44
+ * (`ResponseCookies.set`), and TypeScript checks a rest-tuple signature's
45
+ * parameters contravariantly with no method-bivariance leniency — a wider
46
+ * options type here fails that check even though every real call site passes
47
+ * only these five attributes.
48
+ */
49
+ export interface ConsentVisitorCookieWriter extends ConsentVisitorCookieReader {
50
+ set(name: string, value: string, options: ConsentVisitorCookieOptions): void;
51
+ }
52
+
53
+ /** The existing visitor id, or `undefined` with none set or an invalid value. */
54
+ export function readConsentVisitor(
55
+ cookies: ConsentVisitorCookieReader,
56
+ ): string | undefined {
57
+ const value = cookies.get(CONSENT_VISITOR_COOKIE_NAME)?.value;
58
+ return value !== undefined && isValidConsentVisitorId(value) ? value : undefined;
59
+ }
60
+
61
+ /**
62
+ * The visitor id, minting and setting a fresh one when absent or invalid.
63
+ * Server Actions only — Next refuses a cookie write outside one (the same
64
+ * posture `hub-session/reseal.ts` documents).
65
+ */
66
+ export function ensureConsentVisitor(cookies: ConsentVisitorCookieWriter): string {
67
+ const existing = readConsentVisitor(cookies);
68
+ if (existing !== undefined) return existing;
69
+ const id = crypto.randomUUID();
70
+ cookies.set(CONSENT_VISITOR_COOKIE_NAME, id, {
71
+ httpOnly: true,
72
+ secure: process.env.NODE_ENV === "production",
73
+ sameSite: "lax",
74
+ path: "/",
75
+ maxAge: ONE_YEAR_SECONDS,
76
+ });
77
+ return id;
78
+ }
79
+
80
+ /** The one header a visitor-id-bearing request carries (D11). */
81
+ export function consentVisitorHeader(id: string): Record<string, string> {
82
+ return { "x-ailk-consent-visitor": id };
83
+ }
84
+
85
+ /**
86
+ * The visitor id out of a raw `Cookie:` header string (the relay's own
87
+ * `Request`, which carries no Next `cookies()` jar). `undefined` with no
88
+ * header, no matching cookie, or a value that fails the UUID shape.
89
+ */
90
+ export function readConsentVisitorFromCookieHeader(
91
+ header: string | null | undefined,
92
+ ): string | undefined {
93
+ if (!header) return undefined;
94
+ for (const part of header.split(";")) {
95
+ const eq = part.indexOf("=");
96
+ if (eq === -1) continue;
97
+ const name = part.slice(0, eq).trim();
98
+ if (name !== CONSENT_VISITOR_COOKIE_NAME) continue;
99
+ const value = part.slice(eq + 1).trim();
100
+ return isValidConsentVisitorId(value) ? value : undefined;
101
+ }
102
+ return undefined;
103
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The deck-page resolver (#8687 D3) — the layout's question ("does the page
3
+ * at this path opt in?") and `renderPage`'s consult both ask this ONE
4
+ * resolver, so they always agree. Registers with `@working-theory/page-renderer`
5
+ * at module load, in `lib/site-identity.ts`'s precedent: `app/[locale]/layout.tsx`
6
+ * imports this module, and Next.js evaluates a layout module before any page
7
+ * under it renders.
8
+ */
9
+ import {
10
+ getMdxSource as defaultGetMdxSource,
11
+ optsIntoDeck,
12
+ setDeckPages,
13
+ type DeckPageContext,
14
+ type DeckPageInputs,
15
+ type SchemaOrgType,
16
+ } from "@working-theory/page-renderer";
17
+ import { ROUTES, type RouteRow } from "@working-theory/templates/routes";
18
+ import { readLocales } from "@working-theory/web-i18n";
19
+ import { headers } from "next/headers";
20
+ import { cache } from "react";
21
+
22
+ import { normalizeRoutePath } from "./closing-footer-path";
23
+ import { siteChrome, type SiteChrome } from "./site-chrome";
24
+
25
+ /**
26
+ * The routes besides `[slug]` whose page can present as a deck: every
27
+ * routes-map row with seed content whose route renders sections, minus `/`.
28
+ *
29
+ * This is a fixed superset across every shape/level a scaffold can ship,
30
+ * not a per-site exact list: a shape that doesn't carry one of these routes
31
+ * (e.g. a Free scaffold without `/alternatives`) has no `page.tsx` there, so
32
+ * `deckCandidates`/`resolveDeckPage` never get a request for that path —
33
+ * harmless. `apps/web/lib/__tests__/deck-page.test.ts`'s parity case checks
34
+ * the direction that would be a real bug: that this list names every
35
+ * sections route THIS build's own tree actually carries.
36
+ */
37
+ export const DECK_ROUTES = [
38
+ "/about",
39
+ "/alternatives",
40
+ "/contact",
41
+ "/faq",
42
+ "/launch-service",
43
+ "/pricing",
44
+ "/privacy",
45
+ "/projects",
46
+ "/projects/:slug",
47
+ "/purchase-policy",
48
+ "/reports",
49
+ "/terms",
50
+ "/thanks/:slug",
51
+ "/working-agreement",
52
+ ] as const;
53
+
54
+ /** The widened type (#7719 D3, #7740 D8): present only when the design sets it. */
55
+ type Chrome = SiteChrome & {
56
+ deckGrounds?: DeckPageInputs["deckGrounds"];
57
+ deckPresentation?: true;
58
+ };
59
+ const chrome: Chrome = siteChrome;
60
+
61
+ function routeSegments(route: string): string[] {
62
+ return route.split("/").filter((segment) => segment.length > 0);
63
+ }
64
+
65
+ function matchesRoute(routeDef: string, pathSegments: readonly string[]): boolean {
66
+ const defSegments = routeSegments(routeDef);
67
+ if (defSegments.length !== pathSegments.length) return false;
68
+ return defSegments.every(
69
+ (segment, i) => segment.startsWith(":") || segment === pathSegments[i],
70
+ );
71
+ }
72
+
73
+ /** The type and slug a matched routes-map row resolves to for this request. */
74
+ function resolveTypeAndSlug(
75
+ row: RouteRow,
76
+ routeDef: string,
77
+ pathSegments: readonly string[],
78
+ ): { type: SchemaOrgType; slug: string } | null {
79
+ if (row.contentPath === undefined) return null;
80
+ const parts = row.contentPath.split("/");
81
+ const last = parts[parts.length - 1];
82
+ if (last === undefined) return null;
83
+ const isFile = last.endsWith(".mdx");
84
+ const type = isFile ? parts[parts.length - 2] : last;
85
+ if (type === undefined) return null;
86
+ const slug = isFile
87
+ ? last.replace(/\.mdx$/, "")
88
+ : pathSegments[routeSegments(routeDef).findIndex((segment) => segment.startsWith(":"))];
89
+ if (slug === undefined) return null;
90
+ return { type: type as SchemaOrgType, slug };
91
+ }
92
+
93
+ /** Whether a routes-map row at this exact route is always scaffolded (never absent from a site). */
94
+ function isAlwaysScaffolded(path: string): boolean {
95
+ const row = ROUTES.find((candidate) => candidate.route === path);
96
+ return row !== undefined && row.gateable !== true && row.module === "marketing";
97
+ }
98
+
99
+ /**
100
+ * The content pages that could serve `pathname`, in the order Next.js tries
101
+ * them (D3). `/` gives none — the home's deck is its own `layout.home` knob,
102
+ * never this resolver's.
103
+ */
104
+ export function deckCandidates(
105
+ pathname: string,
106
+ locales: readonly string[],
107
+ ): Array<{ type: SchemaOrgType; slug: string }> {
108
+ const path = normalizeRoutePath(pathname, locales);
109
+ if (path === "/") return [];
110
+ const segments = routeSegments(path);
111
+ const candidates: Array<{ type: SchemaOrgType; slug: string }> = [];
112
+
113
+ for (const routeDef of DECK_ROUTES) {
114
+ if (!matchesRoute(routeDef, segments)) continue;
115
+ const row = ROUTES.find((candidate) => candidate.route === routeDef);
116
+ const resolved = row && resolveTypeAndSlug(row, routeDef, segments);
117
+ if (resolved) candidates.push(resolved);
118
+ break;
119
+ }
120
+
121
+ // A one-segment path not always scaffolded falls through to `[slug]`,
122
+ // which serves `LandingPage` then `BlogList` (D3).
123
+ if (segments.length === 1 && !isAlwaysScaffolded(path)) {
124
+ const slug = segments[0];
125
+ if (slug !== undefined) {
126
+ candidates.push({ type: "LandingPage", slug });
127
+ candidates.push({ type: "BlogList", slug });
128
+ }
129
+ }
130
+
131
+ return candidates;
132
+ }
133
+
134
+ /**
135
+ * The first candidate with a source, and its frontmatter, or `null`. A read
136
+ * that throws ends the walk as `null` (NFR 3) — the page's own render
137
+ * surfaces that error, and the layout keeps its plain shell. Per-request
138
+ * memoised (React `cache`) — the layout and `renderPage`'s consult ask the
139
+ * same question for the same request and must share one answer.
140
+ */
141
+ export const resolveDeckPage = cache(async function resolveDeckPage(
142
+ pathname: string,
143
+ locale: string,
144
+ ): Promise<{ type: SchemaOrgType; slug: string; frontmatter: Record<string, unknown> } | null> {
145
+ for (const candidate of deckCandidates(pathname, readLocales())) {
146
+ let source;
147
+ try {
148
+ source = await defaultGetMdxSource(candidate.type, candidate.slug, locale);
149
+ } catch {
150
+ return null;
151
+ }
152
+ if (source) return { ...candidate, frontmatter: source.frontmatter };
153
+ }
154
+ return null;
155
+ });
156
+
157
+ /** The layout's question: does the page at this path opt in? */
158
+ export async function pageOptsIntoDeck(input: {
159
+ pathname: string;
160
+ locale: string;
161
+ }): Promise<boolean> {
162
+ const resolved = await resolveDeckPage(input.pathname, input.locale);
163
+ return resolved !== null && optsIntoDeck(resolved.frontmatter);
164
+ }
165
+
166
+ /** `inputsFor`: only when this render IS the page the request path resolves to and it opts in. */
167
+ async function inputsFor(ctx: DeckPageContext): Promise<DeckPageInputs | null> {
168
+ const headerStore = await headers();
169
+ const pathname = headerStore.get("x-pathname") ?? null;
170
+ if (pathname === null) return null;
171
+ const resolved = await resolveDeckPage(pathname, ctx.locale);
172
+ if (
173
+ resolved === null ||
174
+ resolved.type !== ctx.type ||
175
+ resolved.slug !== ctx.slug ||
176
+ !optsIntoDeck(resolved.frontmatter)
177
+ ) {
178
+ return null;
179
+ }
180
+ return {
181
+ ...(chrome.deckGrounds !== undefined ? { deckGrounds: chrome.deckGrounds } : {}),
182
+ deckPresentation: chrome.deckPresentation === true,
183
+ };
184
+ }
185
+
186
+ setDeckPages({ inputsFor });
@@ -43,6 +43,7 @@ import { isIP } from "node:net";
43
43
  import { relayToApi, visitorForwardHeaders } from "@working-theory/api-client";
44
44
 
45
45
  import { browserApiOrigin } from "./api-origin.generated";
46
+ import { readConsentVisitorFromCookieHeader } from "./consent-visitor";
46
47
 
47
48
  type RelayMethod = "GET" | "POST";
48
49
 
@@ -77,6 +78,16 @@ export const RELAY_ROUTES: readonly RelayRoute[] = Object.freeze([
77
78
  SESSION_ROUTE,
78
79
  ]);
79
80
 
81
+ /**
82
+ * The three lead routes (#8438 D11, D12): a visitor's banner choice is
83
+ * linked to a Person only through these — a booking has no consent writer.
84
+ */
85
+ const LEAD_ROUTE_PATHS = new Set([
86
+ "/v1/leads/submit",
87
+ "/v1/leads/subscribe",
88
+ "/v1/leads/callback",
89
+ ]);
90
+
80
91
  /** Fastify's default `bodyLimit`; the hub sets none (NFR4). */
81
92
  const RELAY_BODY_LIMIT = 1_048_576;
82
93
 
@@ -395,6 +406,15 @@ function relayHeaders(
395
406
  out,
396
407
  visitorForwardHeaders(incoming, process.env.AILK_VISITOR_FORWARD_KEY),
397
408
  );
409
+ // #8438 D12 — the banner's visitor id, read from THIS request's own
410
+ // `ailk-vid` cookie, never from a client-sent `x-ailk-*` header (the
411
+ // allow-list above already strips those). Only the three lead routes:
412
+ // the link between a banner choice and a client record is made there
413
+ // (D11), and the schedule routes have no consent writer to link.
414
+ if (LEAD_ROUTE_PATHS.has(entry.path)) {
415
+ const visitorId = readConsentVisitorFromCookieHeader(incoming.get("cookie"));
416
+ if (visitorId) out["x-ailk-consent-visitor"] = visitorId;
417
+ }
398
418
  }
399
419
  return out;
400
420
  }
@@ -57,7 +57,6 @@
57
57
  },
58
58
  "consent": {
59
59
  "title": "Cookie Preferences",
60
- "body": "We use analytics to understand how visitors use this site. We don't sell your data.",
61
60
  "acceptLabel": "Accept",
62
61
  "rejectLabel": "Reject",
63
62
  "policyLinkLabel": "Privacy policy",
@@ -74,12 +73,19 @@
74
73
  },
75
74
  "about": {
76
75
  "heading": "About",
77
- "emptyState": "About page content coming soon."
76
+ "emptyState": "About page content coming soon.",
77
+ "viewProfile": "View profile"
78
78
  },
79
79
  "blog": {
80
80
  "heading": "Blog",
81
81
  "emptyState": "No posts yet. Drop MDX files into",
82
- "emptyStateTrailing": "to populate this list."
82
+ "emptyStateTrailing": "to populate this list.",
83
+ "allTopics": "All",
84
+ "topicsNav": "Blog topics",
85
+ "latestPosts": "Latest posts",
86
+ "previousPosts": "Previous posts",
87
+ "readMore": "Read more",
88
+ "sidebar": "More from this site"
83
89
  },
84
90
  "docs": {
85
91
  "heading": "Docs",
@@ -119,7 +125,8 @@
119
125
  "sending": "Sending…",
120
126
  "sendMessage": "Send message",
121
127
  "successMessage": "Thank you for reaching out! We will be in touch shortly.",
122
- "consentStatement": "I accept the [Terms](terms).",
128
+ "consentStatement": "I accept the [Terms](terms), [Privacy Policy](privacy), and [Working Agreement](working_agreement).",
129
+ "consentRequired": "Please accept the terms to send your message.",
123
130
  "genericError": "Something went wrong, please try again.",
124
131
  "unavailableHeading": "Get in touch",
125
132
  "unavailableBody": "Email us at {email} and we'll reply.",
@@ -318,9 +325,8 @@
318
325
  "emptyState": "Resources listing coming soon."
319
326
  },
320
327
  "schedule": {
321
- "title": "Book a call",
328
+ "heading": "Book time with me",
322
329
  "description": "Pick a time that works for you.",
323
- "eyebrow": "Book a call",
324
330
  "headline": "Pick a time that works",
325
331
  "confirmed": {
326
332
  "title": "You're booked",
@@ -353,6 +359,10 @@
353
359
  "heading": "Terms of Service",
354
360
  "emptyState": "Terms of service content coming soon."
355
361
  },
362
+ "workingAgreement": {
363
+ "heading": "Working Agreement",
364
+ "emptyState": "Working agreement content coming soon."
365
+ },
356
366
  "testimonials": {
357
367
  "heading": "Testimonials",
358
368
  "emptyState": "Testimonials coming soon."
@@ -535,6 +545,7 @@
535
545
  "signUpSubmit": "Create account",
536
546
  "signUpFooterPrompt": "Already have an account?",
537
547
  "signUpFooterLink": "Sign in",
548
+ "signUpAgreementsLegend": "Agreements",
538
549
  "signUpNotConfiguredSubhead": "Sign-up is not configured for this site yet. Set a provider's environment variables to enable it.",
539
550
  "signUpFailed": "We couldn't create your account. Check your details and try again.",
540
551
  "providerFailed": "We couldn't start sign-up with that provider. Try again.",
@@ -622,6 +633,17 @@
622
633
  "rateLimited": "Too many requests. Try again later.",
623
634
  "failed": "We couldn't send your review. Try again."
624
635
  }
636
+ },
637
+ "proposal": {
638
+ "caption": "Proposal",
639
+ "price": "Price {amount}",
640
+ "approve": "Approve proposal",
641
+ "heading": "Your decision on version {version}",
642
+ "pay": "Pay now",
643
+ "paying": "Opening payment…",
644
+ "paid": "Paid {date}",
645
+ "paidPending": "Payment received — it may take a minute to show here.",
646
+ "payError": "We couldn't open the payment page. Try again."
625
647
  }
626
648
  },
627
649
  "frame": {
@@ -771,14 +793,32 @@
771
793
  "passwordUnavailable": "We couldn't check your password settings."
772
794
  },
773
795
  "notifications": {
774
- "title": "Notifications"
796
+ "title": "Notifications",
797
+ "lead": "What we send you.",
798
+ "empty": "There's nothing to set here yet."
775
799
  },
776
800
  "signIn": {
777
801
  "lead": "How you sign in to this site."
778
802
  },
779
803
  "privacy": {
804
+ "title": "Privacy",
780
805
  "lead": "Cookies and your data.",
781
- "cookies": "Cookie settings",
806
+ "summary": "How we use your data.",
807
+ "policyLink": "Read our privacy policy",
808
+ "cookiesTitle": "Cookies",
809
+ "cookiesAccepted": "Accepted",
810
+ "cookiesDeclined": "Declined",
811
+ "cookiesUnset": "Not chosen yet",
812
+ "cookiesChange": "Change",
813
+ "consentsTitle": "Data use",
814
+ "consentsEmpty": "This site asks for no other data-use consents.",
815
+ "agreementsTitle": "Agreements accepted",
816
+ "agreementsEmpty": "None yet",
817
+ "version": "Version {version}",
818
+ "given": "Given {date}",
819
+ "withdrawn": "Withdrawn {date}",
820
+ "accepted": "Accepted {date}",
821
+ "unavailable": "Your consent choices can't be shown right now.",
782
822
  "deletion": "Request account deletion",
783
823
  "deletionLead": "Ask the site owner to delete your account and its data. We'll confirm by email.",
784
824
  "deletionSentSite": "Sent to the site operator. They'll confirm by email.",
@@ -809,6 +849,12 @@
809
849
  }
810
850
  }
811
851
  },
852
+ "agreements": {
853
+ "titleNew": "Please accept our terms",
854
+ "titleUpdated": "We've updated our terms",
855
+ "accept": "Accept",
856
+ "error": "We couldn't save that. Try again."
857
+ },
812
858
  "gate": {
813
859
  "emptyHeadline": "Sign-in isn't available yet",
814
860
  "emptyDescription": "This site hasn't turned on sign-in for members yet. Check back soon, or contact the site owner.",
@@ -862,7 +908,26 @@
862
908
  "heading": "My messages",
863
909
  "sentAt": "Sent",
864
910
  "file": "File",
865
- "download": "Download {filename}"
911
+ "download": "Download {filename}",
912
+ "openOnboarding": "Open onboarding"
913
+ },
914
+ "onboarding": {
915
+ "title": "Onboarding",
916
+ "unavailable": "Onboarding isn't open to you here right now.",
917
+ "back": "Back to your account",
918
+ "optional": "Optional",
919
+ "submit": "Send answers",
920
+ "submitting": "Sending…",
921
+ "sent": "Thank you — your answers were sent.",
922
+ "retry": "Retry",
923
+ "errors": {
924
+ "load": "We couldn't load your onboarding questions.",
925
+ "onboardingOff": "Onboarding isn't open to you here right now.",
926
+ "answersInvalid": "Some answers do not fit these questions. Check them and try again.",
927
+ "linkNotHttps": "A link must start with https://.",
928
+ "tooManyRequests": "Too many submissions. Try again later.",
929
+ "failed": "Your answers could not be sent. Try again."
930
+ }
866
931
  },
867
932
  "subscription": {
868
933
  "headline": "Your payment didn't go through",
@@ -1079,5 +1144,14 @@
1079
1144
  "sending": "Subscribing…",
1080
1145
  "success": "You're subscribed. Thanks for signing up!",
1081
1146
  "consentStatement": "I accept the [Terms](terms)."
1147
+ },
1148
+ "unsubscribe": {
1149
+ "title": "Unsubscribe",
1150
+ "lead": "Stop these emails?",
1151
+ "button": "Unsubscribe",
1152
+ "done": "You're unsubscribed from {description}.",
1153
+ "already": "You were already unsubscribed.",
1154
+ "invalid": "This link isn't valid.",
1155
+ "unavailable": "Unsubscribing isn't available right now."
1082
1156
  }
1083
1157
  }
@@ -5,6 +5,7 @@ import bundleAnalyzer from "@next/bundle-analyzer";
5
5
  import { withSentryConfig } from "@sentry/nextjs";
6
6
  import createNextIntlPlugin from "next-intl/plugin";
7
7
  import { legacyRedirects } from "@working-theory/templates/routes";
8
+ import { parseStealth } from "@working-theory/web-i18n/stealth";
8
9
 
9
10
  // @next/bundle-analyzer — enabled when ANALYZE=true (set by ci-bundle.yml).
10
11
  // analyzerMode 'json' writes .next/analyze/client.json (and nodejs.json,
@@ -16,11 +17,16 @@ const withBundleAnalyzer = bundleAnalyzer({
16
17
  analyzerMode: "json",
17
18
  });
18
19
 
19
- // When STEALTH=true, every response carries X-Robots-Tag: noindex, nofollow,
20
+ // When STEALTH is on, every response carries X-Robots-Tag: noindex, nofollow,
20
21
  // noarchive — paired with the <meta robots> tag in app/layout.tsx. Layer 2 of
21
22
  // the three-layer stealth posture (#884); new AILK installs default to
22
23
  // STEALTH=true so a forked instance doesn't ship indexable by accident.
23
- const STEALTH = process.env.STEALTH === "true";
24
+ // parseStealth is the one shared parse (#8511): true, 1, yes or on turn it on;
25
+ // false, 0, no, off, empty or unset leave it off; any other value throws here,
26
+ // so `next build` (and a non-standalone `next start`) fails before a page
27
+ // renders. A standalone server.js does not load this file, so there the
28
+ // robots route and every page's <meta robots> throw per request instead.
29
+ const STEALTH = parseStealth(process.env.STEALTH);
24
30
 
25
31
  // next-intl plugin — no explicit path: uses next-intl's default convention
26
32
  // lookup (./i18n/request.ts). We use `next build --webpack` (see package.json)
@@ -1,5 +1,53 @@
1
1
  # @working-theory/database
2
2
 
3
+ ## 0.42.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#8740](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8740) [`d1461bf`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d1461bff10a69aa4cf05627e393bd1902c358b00) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The database seed writes the default workspace's Contact form magnet ([#8661](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8661)): one live contact-kind magnet at slug `contact`, insert-only, so `/contact` reads it on a freshly seeded site.
8
+
9
+ - Updated dependencies [[`cb2f3a5`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/cb2f3a50c38fe77b92aa52d1462d3d11a5981981), [`425e253`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/425e2538dfd22dfb6f0b6439d473fa2aafcbb883), [`da9253b`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/da9253bb6f8c792e7e75cfe1c1a7bba5ca569e26), [`df1b8e4`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/df1b8e485f9cdd1885925430b7aaa186ffdf6ac1), [`d4e0598`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d4e0598c7659d9349bd56ba184e4ab941e3fde62)]:
10
+ - @working-theory/validation@0.42.0
11
+
12
+ ## 0.41.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [#8540](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8540) [`48ff411`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/48ff41198ef01163412118249f51d64b66d4ce42) Thanks [@claudio-planck](https://github.com/claudio-planck)! - The API's per-IP rate limiter counts in a shared database table when one is configured, so one visitor's limit holds across every concurrent isolate ([#5040](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/5040)).
17
+
18
+ New model `RateLimitCounter` (table `rate_limit_counters`, prefix `rlc`) in the `ops` namespace, holding one row per hashed bucket key. Run the migration before deploying the code that reads it — code deployed first falls back to the per-isolate in-process limiter, safely, until the table exists.
19
+
20
+ - [#8545](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8545) [`41bc2df`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/41bc2df800c8fe69e2cbb528be68f579abfbd80d) Thanks [@claudio-planck](https://github.com/claudio-planck)! - The Person record redesign ([#8514](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8514), docs/specs/8514-redesign-the-person-record-page-spec.md).
21
+
22
+ - **database** — `people` gains the team's private note on a client (`notes`, who last saved it, when, and which agent), with format and pairing CHECKs. The same migration then removes `people-read` and `member-pieces-manage` from agents of members, where they were inert; deploy it before the code.
23
+ - **validation** — members now hold `people-read`, `people-write` and `member-pieces-manage`. The new `AGENT_WITHHELD_PERMISSIONS` and `roleAllows` keep every agent off those three by role: an agent holds the grantable two only by an explicit grant, and `people-write` never. New `personNotes*` and `planOfferings*` schemas.
24
+ - **api-client** — `people.notes` and `people.setNotes`.
25
+ - **metadata** — two signed-in workspace tools, `get_person_notes` and `set_person_notes` (thirteen in all).
26
+ - **ui** — `PhoneNumberField` shows its country format as a "Format: …" caption under the field and no longer puts the example in the placeholder.
27
+ - **templates** — `ContactDetailsForm`, the one contact form the account page and the Admin share; Save stays disabled until something changes.
28
+
29
+ - [#8574](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8574) [`83b7a96`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/83b7a96ccd115230f82e111a9776c8e2136b6bf9) Thanks [@claudio-planck](https://github.com/claudio-planck)! - Standard client onboarding diagnostic and Start onboarding ([#8515](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8515), docs/specs/8515-standard-client-onboarding-diagnostic-and-spec.md).
30
+
31
+ - `@working-theory/database`: `interactions_outbound_shape` admits one outbound payload, `{"action": "onboarding"}` (migration `20261017120000_outbound_message_action`; no row is rewritten).
32
+ - `@working-theory/validation`: `ONBOARDING_FLOW_SLUG`, `ONBOARDING_FLOW_TITLE` and `onboardingFlowConfig`, the standard onboarding magnet every workspace holds; `OUTBOUND_MESSAGE_ACTIONS` and `onboardingStartBodySchema`; `memberMessageSchema` gains `action`; `memberOnboardingFormResponseSchema`, `memberOnboardingStepSchema`, `memberOnboardingReceiptSchema` and the `onboarding_off` member error code.
33
+ - `@working-theory/api-client`: `member.onboarding` and `member.submitOnboarding`.
34
+
35
+ - [#8572](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8572) [`16abdb4`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/16abdb45ef60cc46aba35d4179960468b8df1a8b) Thanks [@claudio-planck](https://github.com/claudio-planck)! - Proposals as an artifact kind ([#8516](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8516), docs/specs/8516-proposals-as-an-artifact-kind-spec.md).
36
+
37
+ - `@working-theory/database`: `pieces` gains `kind` (`deliverable` by default, so every existing piece is unchanged), a proposal's optional price, the client's Checkout Session and the paid stamp, with their CHECKs and one unique index (migration `20261016120000_piece_kind`, DDL only).
38
+ - `@working-theory/validation`: `PIECE_KINDS`, `pieceKindSchema`, `PROPOSAL_CURRENCIES`, `proposalPriceSchema`; `pieceSchema` serves `kind`; the new `proposals.ts` wire (`proposalCreateBodySchema`, `proposalSchema`, `proposalsListResponseSchema`, `proposalResultSchema`, `proposalPaymentsSchema`); `memberArtifactSchema` gains optional `kind`, `price` and `payment`; `isStripeCheckoutUrl`, `memberProposalPaymentResponseSchema` and three member error codes.
39
+ - `@working-theory/api-client`: `people.proposals`, `people.createProposal` and `member.payArtifact`.
40
+ - `@working-theory/metadata`: the `list_proposals` and `create_proposal` workspace tools.
41
+ - `@working-theory/stripe-client`: `createQuotedCheckoutSession` takes optional `expiresAt` and `paymentMethodTypes`; existing callers' sessions are unchanged.
42
+
43
+ ### Patch Changes
44
+
45
+ - [#8551](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8551) [`13e1d6f`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/13e1d6f74ef3db02b9cdedb5f33f84ad1a9a0d66) Thanks [@samuelhenry](https://github.com/samuelhenry)! - Person gains a private team note (notes, notes_updated_at, notes_updated_by, notes_updated_by_agent) and tenant-db gains its one reader and writer ([#8514](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8514), PR 1 of 2 — the fence half; the API, MCP and Admin surfaces follow in PR 2).
46
+
47
+ - Updated dependencies [[`1b0d07a`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/1b0d07acc0c97f519bf7a2dccf108d6b98c24065), [`48ff411`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/48ff41198ef01163412118249f51d64b66d4ce42), [`98b82b0`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/98b82b0e1cf29be3ba33886ebc7b8294ebff52f0), [`41bc2df`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/41bc2df800c8fe69e2cbb528be68f579abfbd80d), [`83b7a96`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/83b7a96ccd115230f82e111a9776c8e2136b6bf9), [`16abdb4`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/16abdb45ef60cc46aba35d4179960468b8df1a8b)]:
48
+ - @working-theory/validation@0.41.0
49
+ - @working-theory/ids@0.14.0
50
+
3
51
  ## 0.40.0
4
52
 
5
53
  ### Minor Changes