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
@@ -0,0 +1,231 @@
1
+ -- #8435 — contact details on the client record (spec
2
+ -- docs/specs/8435-contact-details-on-the-client-spec.md D1, D2).
3
+ --
4
+ -- people (13 columns) — given name, family name, an E.164 telephone, the
5
+ -- magnet's six-part address, a company name, a job title,
6
+ -- and the confirmation pair (who last saved the contact by
7
+ -- hand, and when). Every column is nullable and has no
8
+ -- default, so Postgres changes only the catalog.
9
+ -- callbacks.person_id, subscribers.person_id — the Person a request or a
10
+ -- subscription belongs to: a composite foreign key onto
11
+ -- people(id, workspace_id), the #7877 shape, so "a Person
12
+ -- of the row's own workspace" is a database fact. NULL is
13
+ -- not checked (MATCH SIMPLE).
14
+ --
15
+ -- PURELY ADDITIVE, DDL ONLY. No existing row's values change here: the
16
+ -- backfill is the next migration, 20261014121000_person_contact_backfill,
17
+ -- split out so its row updates never run under the exclusive locks these
18
+ -- ALTER TABLEs take. Prisma runs this file as one transaction, so each ALTER
19
+ -- TABLE's ACCESS EXCLUSIVE lock on "people", "callbacks" and "subscribers"
20
+ -- is held until the file commits. Every CHECK and foreign key below is added
21
+ -- NOT VALID and then VALIDATEd over an all-NULL column or a table with no
22
+ -- person_id yet, and none of the three is the large "leads" table; the window
23
+ -- is accepted, as #852 accepted the same on "leads" (SEC-852-E, SEC-8435-C).
24
+ --
25
+ -- Rollout: deploy this migration BEFORE the API code that writes the columns;
26
+ -- code without them fails every callback and subscription write.
27
+ --
28
+ -- Rollback: ALTER TABLE "callbacks" DROP CONSTRAINT "callbacks_person_fk",
29
+ -- DROP COLUMN "person_id"; ALTER TABLE "subscribers" DROP CONSTRAINT
30
+ -- "subscribers_person_fk", DROP COLUMN "person_id"; ALTER TABLE "people"
31
+ -- DROP COLUMN "given_name", DROP COLUMN "family_name", DROP COLUMN
32
+ -- "telephone", DROP COLUMN "address_line1", DROP COLUMN "address_line2",
33
+ -- DROP COLUMN "address_city", DROP COLUMN "address_region", DROP COLUMN
34
+ -- "address_postal_code", DROP COLUMN "address_country", DROP COLUMN
35
+ -- "company_name", DROP COLUMN "job_title", DROP COLUMN "contact_confirmed_at",
36
+ -- DROP COLUMN "contact_confirmed_by"; (the indexes and CHECKs go with them).
37
+
38
+ -- AlterTable
39
+ ALTER TABLE "people" ADD COLUMN "address_city" VARCHAR(60),
40
+ ADD COLUMN "address_country" CHAR(2),
41
+ ADD COLUMN "address_line1" VARCHAR(100),
42
+ ADD COLUMN "address_line2" VARCHAR(60),
43
+ ADD COLUMN "address_postal_code" VARCHAR(20),
44
+ ADD COLUMN "address_region" VARCHAR(40),
45
+ ADD COLUMN "company_name" VARCHAR(200),
46
+ ADD COLUMN "contact_confirmed_at" TIMESTAMP(3),
47
+ ADD COLUMN "contact_confirmed_by" VARCHAR(16),
48
+ ADD COLUMN "family_name" VARCHAR(100),
49
+ ADD COLUMN "given_name" VARCHAR(100),
50
+ ADD COLUMN "job_title" VARCHAR(100),
51
+ ADD COLUMN "telephone" VARCHAR(16);
52
+
53
+ -- AlterTable
54
+ ALTER TABLE "callbacks" ADD COLUMN "person_id" TEXT;
55
+
56
+ -- AlterTable
57
+ ALTER TABLE "subscribers" ADD COLUMN "person_id" TEXT;
58
+
59
+ -- CreateIndex
60
+ CREATE INDEX "callbacks_person_id_idx" ON "callbacks"("person_id");
61
+
62
+ -- CreateIndex
63
+ CREATE INDEX "subscribers_person_id_idx" ON "subscribers"("person_id");
64
+
65
+ -- A stored telephone is E.164 (the form the phone field posts) or NULL.
66
+ ALTER TABLE "people" ADD CONSTRAINT "people_telephone_e164"
67
+ CHECK ("telephone" IS NULL OR "telephone" ~ '^\+[1-9][0-9]{6,14}$') NOT VALID;
68
+ ALTER TABLE "people" VALIDATE CONSTRAINT "people_telephone_e164";
69
+
70
+ -- The country is the upper-case ISO 3166-1 alpha-2 code.
71
+ ALTER TABLE "people" ADD CONSTRAINT "people_address_country_format"
72
+ CHECK ("address_country" IS NULL OR "address_country" ~ '^[A-Z]{2}$') NOT VALID;
73
+ ALTER TABLE "people" VALIDATE CONSTRAINT "people_address_country_format";
74
+
75
+ -- An address is stored whole or not at all: all six parts NULL, or every
76
+ -- part but the optional second line present.
77
+ ALTER TABLE "people" ADD CONSTRAINT "people_address_complete"
78
+ CHECK (
79
+ ("address_line1" IS NULL AND "address_line2" IS NULL AND "address_city" IS NULL AND "address_region" IS NULL AND "address_postal_code" IS NULL AND "address_country" IS NULL)
80
+ OR
81
+ ("address_line1" IS NOT NULL AND "address_city" IS NOT NULL AND "address_region" IS NOT NULL AND "address_postal_code" IS NOT NULL AND "address_country" IS NOT NULL)
82
+ ) NOT VALID;
83
+ ALTER TABLE "people" VALIDATE CONSTRAINT "people_address_complete";
84
+
85
+ -- The confirmation pair is written together, and the actor is one of two.
86
+ ALTER TABLE "people" ADD CONSTRAINT "people_contact_confirmed"
87
+ CHECK (
88
+ ("contact_confirmed_at" IS NULL AND "contact_confirmed_by" IS NULL)
89
+ OR
90
+ ("contact_confirmed_at" IS NOT NULL AND "contact_confirmed_by" IS NOT NULL AND "contact_confirmed_by" IN ('operator', 'client'))
91
+ ) NOT VALID;
92
+ ALTER TABLE "people" VALIDATE CONSTRAINT "people_contact_confirmed";
93
+
94
+ -- The two new reference columns carry the person_ format CHECK (the #852
95
+ -- shape), added NOT VALID and then validated over an all-NULL column.
96
+ ALTER TABLE "callbacks" ADD CONSTRAINT "callbacks_person_id_format" CHECK ("person_id" ~ '^person_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$') NOT VALID;
97
+ ALTER TABLE "callbacks" VALIDATE CONSTRAINT "callbacks_person_id_format";
98
+ ALTER TABLE "subscribers" ADD CONSTRAINT "subscribers_person_id_format" CHECK ("person_id" ~ '^person_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$') NOT VALID;
99
+ ALTER TABLE "subscribers" VALIDATE CONSTRAINT "subscribers_person_id_format";
100
+
101
+ -- AddForeignKey
102
+ -- A callback / subscriber names a Person of ITS OWN workspace; deleting a
103
+ -- Person with one is refused (the pieces_person_fk shape, #7877).
104
+ ALTER TABLE "callbacks" ADD CONSTRAINT "callbacks_person_fk" FOREIGN KEY ("person_id", "workspace_id") REFERENCES "people"("id", "workspace_id") ON DELETE RESTRICT ON UPDATE CASCADE NOT VALID;
105
+ ALTER TABLE "callbacks" VALIDATE CONSTRAINT "callbacks_person_fk";
106
+ ALTER TABLE "subscribers" ADD CONSTRAINT "subscribers_person_fk" FOREIGN KEY ("person_id", "workspace_id") REFERENCES "people"("id", "workspace_id") ON DELETE RESTRICT ON UPDATE CASCADE NOT VALID;
107
+ ALTER TABLE "subscribers" VALIDATE CONSTRAINT "subscribers_person_fk";
108
+
109
+ -- ===== #8435 contact backfill (idempotent; runs after the DDL above) =====
110
+ -- #8435 — the contact backfill (spec
111
+ -- docs/specs/8435-contact-details-on-the-client-spec.md D2).
112
+ --
113
+ -- Gives every newsletter subscriber and every callback that carries a
114
+ -- storable email a Person, stamps subscribers.person_id and
115
+ -- callbacks.person_id, and fills a Person's blank telephone from the oldest
116
+ -- callback phone that is already E.164 once spaces and punctuation are
117
+ -- removed. Set-based and idempotent: every insert is ON CONFLICT DO NOTHING
118
+ -- and every update is guarded by IS NULL, so running this text twice changes
119
+ -- nothing the first run wrote.
120
+ --
121
+ --
122
+ -- Normalization — the SQL twin of apps/api/src/services/person.ts's
123
+ -- normalizePersonEmail / isStorablePersonEmail (#852's expressions verbatim):
124
+ -- N(x) = lower(btrim(x, E' \t\n\r\f\x0B') COLLATE "C")
125
+ -- (\x0B is vertical tab; an E-string has no \v escape — E'\v'
126
+ -- is the letter v, which would strip the v of every .dev)
127
+ -- VALID(e) = e ~ '^[!-?A-~]+@[!-?A-~]+$' AND length(e) <= 254
128
+ -- NAME(x) = C0/C1 controls and bidi controls removed, trimmed, cut to
129
+ -- 200 characters; empty → NULL
130
+ -- A row whose email is not VALID makes no Person and keeps person_id NULL.
131
+ --
132
+ -- A callback's email is its payload.email when that is a JSON string and
133
+ -- VALID, else its buyer_email when VALID — the order the live code links in
134
+ -- (capture first, the confirm step only when capture left it NULL;
135
+ -- SEC-8435-G).
136
+ --
137
+ -- Rollback: nothing to undo on its own — dropping 20261014120000_person_contact's
138
+ -- columns removes the stamps and the telephones this wrote; the Persons it
139
+ -- created are ordinary Persons and are left in place.
140
+
141
+ -- 1. Subscribers, oldest first: one Person per (workspace, normalized email).
142
+ INSERT INTO "people" ("workspace_id", "email", "created_at", "updated_at")
143
+ SELECT DISTINCT ON (s."workspace_id", lower(btrim(s."email", E' \t\n\r\f\x0B') COLLATE "C"))
144
+ s."workspace_id",
145
+ lower(btrim(s."email", E' \t\n\r\f\x0B') COLLATE "C"),
146
+ s."created_at",
147
+ now()
148
+ FROM "subscribers" s
149
+ WHERE lower(btrim(s."email", E' \t\n\r\f\x0B') COLLATE "C") ~ '^[!-?A-~]+@[!-?A-~]+$'
150
+ AND length(lower(btrim(s."email", E' \t\n\r\f\x0B') COLLATE "C")) <= 254
151
+ ORDER BY s."workspace_id", lower(btrim(s."email", E' \t\n\r\f\x0B') COLLATE "C"), s."created_at" ASC, s."id" ASC
152
+ ON CONFLICT ("workspace_id", "email") DO NOTHING;
153
+
154
+ -- 2. Callbacks, oldest first: one Person per (workspace, normalized email),
155
+ -- named from the callback's own name. A Person a lead or a subscriber
156
+ -- already made keeps its name and created_at (ON CONFLICT DO NOTHING).
157
+ INSERT INTO "people" ("workspace_id", "email", "name", "created_at", "updated_at")
158
+ SELECT DISTINCT ON (ce."workspace_id", ce."email_n")
159
+ ce."workspace_id",
160
+ ce."email_n",
161
+ NULLIF(left(btrim(regexp_replace(ce."name", '[\x01-\x1F\x7F-\x9F‎‏‪-‮⁦-⁩]', '', 'g')), 200), ''),
162
+ ce."created_at",
163
+ now()
164
+ FROM (
165
+ SELECT c."id", c."workspace_id", c."name", c."phone", c."created_at",
166
+ (
167
+ SELECT v."e"
168
+ FROM (VALUES
169
+ (1, lower(btrim(CASE WHEN jsonb_typeof(c."payload" -> 'email') = 'string' THEN c."payload" ->> 'email' END, E' \t\n\r\f\x0B') COLLATE "C")),
170
+ (2, lower(btrim(c."buyer_email", E' \t\n\r\f\x0B') COLLATE "C"))
171
+ ) v("ord", "e")
172
+ WHERE v."e" ~ '^[!-?A-~]+@[!-?A-~]+$' AND length(v."e") <= 254
173
+ ORDER BY v."ord"
174
+ LIMIT 1
175
+ ) AS "email_n"
176
+ FROM "callbacks" c
177
+ ) ce
178
+ WHERE ce."email_n" IS NOT NULL
179
+ ORDER BY ce."workspace_id", ce."email_n", ce."created_at" ASC, ce."id" ASC
180
+ ON CONFLICT ("workspace_id", "email") DO NOTHING;
181
+
182
+ -- 3. Stamps: every subscriber and callback with a storable email points at
183
+ -- its workspace's Person; a row already stamped is left alone.
184
+ UPDATE "subscribers"
185
+ SET "person_id" = p."id"
186
+ FROM "people" p
187
+ WHERE "subscribers"."person_id" IS NULL
188
+ AND p."workspace_id" = "subscribers"."workspace_id"
189
+ AND p."email" = lower(btrim("subscribers"."email", E' \t\n\r\f\x0B') COLLATE "C");
190
+
191
+ UPDATE "callbacks"
192
+ SET "person_id" = p."id"
193
+ FROM (
194
+ SELECT c."id" AS "callback_id", c."workspace_id",
195
+ (
196
+ SELECT v."e"
197
+ FROM (VALUES
198
+ (1, lower(btrim(CASE WHEN jsonb_typeof(c."payload" -> 'email') = 'string' THEN c."payload" ->> 'email' END, E' \t\n\r\f\x0B') COLLATE "C")),
199
+ (2, lower(btrim(c."buyer_email", E' \t\n\r\f\x0B') COLLATE "C"))
200
+ ) v("ord", "e")
201
+ WHERE v."e" ~ '^[!-?A-~]+@[!-?A-~]+$' AND length(v."e") <= 254
202
+ ORDER BY v."ord"
203
+ LIMIT 1
204
+ ) AS "email_n"
205
+ FROM "callbacks" c
206
+ WHERE c."person_id" IS NULL
207
+ ) ce
208
+ JOIN "people" p ON p."workspace_id" = ce."workspace_id" AND p."email" = ce."email_n"
209
+ WHERE "callbacks"."id" = ce."callback_id"
210
+ AND "callbacks"."person_id" IS NULL;
211
+
212
+ -- 4. Telephone: for an uncurated Person (no linked user, never confirmed by
213
+ -- hand — the rule a capture obeys, D4) with no telephone, the oldest
214
+ -- stamped callback's phone with spaces and punctuation removed, when that
215
+ -- is E.164. A national-format phone fills nothing.
216
+ UPDATE "people" p
217
+ SET "telephone" = c."tel",
218
+ "updated_at" = now()
219
+ FROM (
220
+ SELECT DISTINCT ON (cb."person_id")
221
+ cb."person_id",
222
+ regexp_replace(cb."phone", '[ ().-]', '', 'g') AS "tel"
223
+ FROM "callbacks" cb
224
+ WHERE cb."person_id" IS NOT NULL
225
+ AND regexp_replace(cb."phone", '[ ().-]', '', 'g') ~ '^\+[1-9][0-9]{6,14}$'
226
+ ORDER BY cb."person_id", cb."created_at" ASC, cb."id" ASC
227
+ ) c
228
+ WHERE p."id" = c."person_id"
229
+ AND p."telephone" IS NULL
230
+ AND p."user_id" IS NULL
231
+ AND p."contact_confirmed_at" IS NULL;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@working-theory/database",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,7 +16,8 @@
16
16
  "inbox",
17
17
  "ops",
18
18
  "tenancy.ts",
19
- "prisma.config.ts"
19
+ "prisma.config.ts",
20
+ "NOTICE"
20
21
  ],
21
22
  "description": "AI Launch Kit database schema (single-tenant Prisma)",
22
23
  "type": "module",
@@ -25,6 +25,12 @@ import "dotenv/config";
25
25
 
26
26
  import { basename } from "node:path";
27
27
 
28
+ import {
29
+ REQUEST_QUOTE_FLOW_SLUG,
30
+ REQUEST_QUOTE_FLOW_TITLE,
31
+ requestQuoteFlowConfig,
32
+ } from "@working-theory/validation";
33
+
28
34
  import type { PrismaClient } from "./generated/client/client.js";
29
35
  import {
30
36
  DEFAULT_ORG_ID,
@@ -47,14 +53,14 @@ export {
47
53
  };
48
54
 
49
55
  /**
50
- * The DI surface `provisionDefaultWorkspace` needs — just the two Prisma
56
+ * The DI surface `provisionDefaultWorkspace` needs — just the three Prisma
51
57
  * delegates it touches. A `Pick` off the real `PrismaClient` type (not a
52
58
  * hand-rolled shape), so a real client satisfies it by construction and the
53
59
  * DB-free unit test can pass a lightweight stub with no live Postgres.
54
60
  */
55
61
  export type DefaultWorkspacePrismaClient = Pick<
56
62
  PrismaClient,
57
- "organization" | "workspace"
63
+ "organization" | "workspace" | "flowDefinition"
58
64
  >;
59
65
 
60
66
  /**
@@ -62,6 +68,12 @@ export type DefaultWorkspacePrismaClient = Pick<
62
68
  * yet; no-op (no create/upsert calls) once any Workspace is present — covering
63
69
  * both single-tenant re-run idempotency and a multi-tenant deployment that
64
70
  * already has real Workspaces.
71
+ *
72
+ * #8168 D8 (C4) — the default workspace also gets its `request-quote` magnet.
73
+ * A fresh install runs `migrate deploy` BEFORE this seed, so the migration's
74
+ * backfill finds no workspace and seeds nothing; this is where the default
75
+ * workspace's magnet comes from. Insert-if-absent: an `upsert` with an empty
76
+ * `update`, so an existing `(site_id, slug)` row is never overwritten.
65
77
  */
66
78
  export async function provisionDefaultWorkspace(
67
79
  prisma: DefaultWorkspacePrismaClient,
@@ -97,6 +109,26 @@ export async function provisionDefaultWorkspace(
97
109
  update: {},
98
110
  });
99
111
 
112
+ await prisma.flowDefinition.upsert({
113
+ where: {
114
+ siteId_slug: {
115
+ siteId: DEFAULT_WORKSPACE_ID,
116
+ slug: REQUEST_QUOTE_FLOW_SLUG,
117
+ },
118
+ },
119
+ create: {
120
+ siteId: DEFAULT_WORKSPACE_ID,
121
+ workspaceId: DEFAULT_WORKSPACE_ID,
122
+ slug: REQUEST_QUOTE_FLOW_SLUG,
123
+ config: requestQuoteFlowConfig,
124
+ kind: "contact",
125
+ title: REQUEST_QUOTE_FLOW_TITLE,
126
+ status: "draft",
127
+ customersEnabled: true,
128
+ },
129
+ update: {},
130
+ });
131
+
100
132
  return { created: true };
101
133
  }
102
134
 
package/versions.json CHANGED
@@ -1,53 +1,53 @@
1
1
  {
2
- "@working-theory/admin-ui": "0.38.0",
3
- "@working-theory/aeo": "0.38.0",
4
- "@working-theory/agent-context": "0.38.0",
2
+ "@working-theory/admin-ui": "0.40.0",
3
+ "@working-theory/aeo": "0.40.0",
4
+ "@working-theory/agent-context": "0.40.0",
5
5
  "@working-theory/agent-contract": "0.1.0",
6
6
  "@working-theory/agent-memory": "0.0.0",
7
7
  "@working-theory/ai": "0.1.0",
8
- "@working-theory/analytics": "0.38.0",
9
- "@working-theory/api-client": "0.38.0",
8
+ "@working-theory/analytics": "0.40.0",
9
+ "@working-theory/api-client": "0.40.0",
10
10
  "@working-theory/cda-filesystem": "0.0.0",
11
- "@working-theory/cli": "0.38.0",
12
- "@working-theory/common": "0.38.0",
11
+ "@working-theory/cli": "0.40.0",
12
+ "@working-theory/common": "0.40.0",
13
13
  "@working-theory/consent": "0.1.0",
14
- "@working-theory/content-adapters": "0.38.0",
15
- "@working-theory/content-projection": "0.38.0",
16
- "@working-theory/email": "0.38.0",
14
+ "@working-theory/content-adapters": "0.40.0",
15
+ "@working-theory/content-projection": "0.40.0",
16
+ "@working-theory/email": "0.40.0",
17
17
  "@working-theory/env": "0.0.0",
18
- "@working-theory/eslint-config": "0.38.0",
19
- "@working-theory/fumadocs-schema": "0.38.0",
20
- "@working-theory/ids": "0.11.0",
21
- "@working-theory/jsonld": "0.38.0",
22
- "@working-theory/license": "0.38.0",
23
- "@working-theory/metadata": "0.38.0",
24
- "@working-theory/observability": "0.38.0",
25
- "@working-theory/page-renderer": "0.38.0",
26
- "@working-theory/plugin-mcp": "0.1.2",
27
- "@working-theory/schema": "0.38.0",
28
- "@working-theory/schema-pack-registry": "0.38.0",
29
- "@working-theory/schema-validator": "0.38.0",
30
- "@working-theory/sections-registry": "0.38.0",
31
- "@working-theory/site-config": "0.38.0",
32
- "@working-theory/spoke-checkout": "0.4.0",
33
- "@working-theory/stripe-client": "0.38.0",
34
- "@working-theory/templates": "0.38.0",
35
- "@working-theory/theme": "0.38.0",
36
- "@working-theory/ui": "0.38.0",
37
- "@working-theory/validation": "0.38.0",
38
- "@working-theory/web-i18n": "0.38.0",
18
+ "@working-theory/eslint-config": "0.40.0",
19
+ "@working-theory/fumadocs-schema": "0.40.0",
20
+ "@working-theory/ids": "0.13.0",
21
+ "@working-theory/jsonld": "0.40.0",
22
+ "@working-theory/license": "0.40.0",
23
+ "@working-theory/metadata": "0.40.0",
24
+ "@working-theory/observability": "0.40.0",
25
+ "@working-theory/page-renderer": "0.40.0",
26
+ "@working-theory/plugin-mcp": "0.1.3",
27
+ "@working-theory/schema": "0.40.0",
28
+ "@working-theory/schema-pack-registry": "0.40.0",
29
+ "@working-theory/schema-validator": "0.40.0",
30
+ "@working-theory/sections-registry": "0.40.0",
31
+ "@working-theory/site-config": "0.40.0",
32
+ "@working-theory/spoke-checkout": "0.5.1",
33
+ "@working-theory/stripe-client": "0.40.0",
34
+ "@working-theory/templates": "0.40.0",
35
+ "@working-theory/theme": "0.40.0",
36
+ "@working-theory/ui": "0.40.0",
37
+ "@working-theory/validation": "0.40.0",
38
+ "@working-theory/web-i18n": "0.40.0",
39
39
  "@working-theory/billing-client": "0.0.0",
40
40
  "@working-theory/branding": "0.0.0",
41
41
  "@working-theory/license-client": "0.0.0",
42
42
  "@working-theory/license-manifest": "0.0.0",
43
43
  "@working-theory/partner-session": "0.0.0",
44
44
  "@working-theory/preview-protocol": "0.0.0",
45
- "@working-theory/ui-commerce": "0.23.16",
46
- "@working-theory/ui-docs-blog": "0.23.16",
47
- "@working-theory/ui-scheduling": "0.23.16",
48
- "@working-theory/ui-signal-collectors": "0.25.5",
45
+ "@working-theory/ui-commerce": "0.23.18",
46
+ "@working-theory/ui-docs-blog": "0.23.18",
47
+ "@working-theory/ui-scheduling": "0.23.18",
48
+ "@working-theory/ui-signal-collectors": "0.25.7",
49
49
  "@working-theory/web": "0.0.0",
50
50
  "@working-theory/api": "1.0.0",
51
51
  "@working-theory/mcp": "0.0.0",
52
- "@working-theory/database": "0.38.0"
52
+ "@working-theory/database": "0.40.0"
53
53
  }
@@ -1,104 +0,0 @@
1
- /**
2
- * @file content-write-production-boot.test.ts
3
- * @description A production build with `content-write` enabled boots without
4
- * pino-pretty (#7502).
5
- *
6
- * `services/revalidation.ts` is loaded only by the content-write routes, and it
7
- * used to build its logger with `createLogger({ service })` alone. The
8
- * observability package read a missing `environment` as development and asked
9
- * pino for a `pino-pretty` transport, which a production install does not carry:
10
- * pino threw `unable to determine transport target for "pino-pretty"` at module
11
- * load, `build()` rejected, and every route — `/health` included — answered 500.
12
- *
13
- * "Without pino-pretty installed" is simulated at the pino boundary: the mock
14
- * below throws pino's own error whenever a `pino-pretty` transport is requested,
15
- * and passes every other call through to the real pino. pino is mocked by its
16
- * resolved path because `apps/api` does not depend on it directly — only
17
- * `@working-theory/observability` does.
18
- *
19
- * Mirrors `module-exclusion.test.ts`'s external-boundary mocks (CORS,
20
- * rate-limit, email).
21
- */
22
-
23
- import type { FastifyInstance } from "fastify";
24
-
25
- jest.mock(
26
- require.resolve("pino", {
27
- paths: [`${__dirname}/../../../../packages/observability`],
28
- }),
29
- () => {
30
- const realPino = jest.requireActual(
31
- require.resolve("pino", {
32
- paths: [`${__dirname}/../../../../packages/observability`],
33
- }),
34
- );
35
- const withoutPinoPretty = (
36
- options?: { transport?: { target?: string } },
37
- ...rest: unknown[]
38
- ) => {
39
- if (options?.transport?.target === "pino-pretty") {
40
- throw new Error(
41
- 'unable to determine transport target for "pino-pretty"',
42
- );
43
- }
44
- return realPino(options, ...rest);
45
- };
46
- // pino exports itself as `default` and as `pino` too; both are replaced.
47
- return Object.assign(withoutPinoPretty, realPino, {
48
- default: withoutPinoPretty,
49
- pino: withoutPinoPretty,
50
- });
51
- },
52
- );
53
-
54
- jest.mock("@fastify/cors", () => {
55
- return jest.fn().mockResolvedValue(undefined);
56
- });
57
-
58
- jest.mock("@fastify/rate-limit", () => {
59
- return jest.fn().mockResolvedValue(undefined);
60
- });
61
-
62
- jest.mock("@working-theory/email", () => ({
63
- sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
64
- }));
65
-
66
- describe("production boot with content-write enabled (#7502)", () => {
67
- const prior = {
68
- NODE_ENV: process.env.NODE_ENV,
69
- AILK_ENABLED_MODULES: process.env.AILK_ENABLED_MODULES,
70
- };
71
- let app: FastifyInstance;
72
-
73
- beforeAll(async () => {
74
- process.env.NODE_ENV = "production";
75
- // content-write registers only alongside a tenancy module (server.ts).
76
- process.env.AILK_ENABLED_MODULES = "multi-tenant,content-write";
77
- // The config and the observability singleton read NODE_ENV at import, so
78
- // the server is loaded only after the environment is set.
79
- const { build } = await import("../server");
80
- app = await build();
81
- });
82
-
83
- afterAll(async () => {
84
- await app?.close();
85
- for (const [key, value] of Object.entries(prior)) {
86
- if (value === undefined) delete process.env[key];
87
- else process.env[key] = value;
88
- }
89
- });
90
-
91
- it("answers /health with 200", async () => {
92
- const res = await app.inject({ method: "GET", url: "/health" });
93
- expect(res.statusCode).toBe(200);
94
- });
95
-
96
- it("registers the content-write routes, so revalidation.ts was loaded", async () => {
97
- // Registered and session-less: 401, where an unregistered route is a 404.
98
- const res = await app.inject({
99
- method: "POST",
100
- url: "/v1/workspaces/wksp_00000000-0000-7000-8000-0000000000aa/content",
101
- });
102
- expect(res.statusCode).toBe(401);
103
- });
104
- });