create-ailk 0.17.0 → 0.19.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 (193) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +4 -1
  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/lib/env-catalog.js +5 -9
  9. package/dist/lib/license-key-files.d.ts +6 -3
  10. package/dist/lib/license-key-files.js +8 -2
  11. package/dist/lib/scaffold-render-sweep.d.ts +9 -0
  12. package/dist/lib/scaffold-render-sweep.js +24 -3
  13. package/dist/license-set.d.ts +1 -1
  14. package/dist/license-set.js +13 -8
  15. package/dist/module-architecture.js +119 -0
  16. package/dist/programmatic.js +3 -0
  17. package/dist/spoke-checkout.d.ts +11 -3
  18. package/dist/spoke-checkout.js +62 -12
  19. package/dist/starter-grid.d.ts +1 -1
  20. package/dist/surfaces.js +140 -1
  21. package/dist/template-manifest.d.ts +4 -3
  22. package/dist/topology.d.ts +15 -2
  23. package/dist/topology.js +83 -4
  24. package/package.json +5 -4
  25. package/templates/.env.example +3 -3
  26. package/templates/.github/workflows/ci.yml +5 -0
  27. package/templates/apps/api/CLAUDE.md +25 -7
  28. package/templates/apps/api/LICENSE +6 -5
  29. package/templates/apps/api/NOTICE +2 -0
  30. package/templates/apps/api/jest.config.cjs +4 -29
  31. package/templates/apps/api/package.json +1 -0
  32. package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
  33. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  34. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  35. package/templates/apps/api/src/lib/stripe.ts +81 -0
  36. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
  37. package/templates/apps/api/src/middleware/auth.ts +206 -46
  38. package/templates/apps/api/src/routes/leads/index.ts +4 -0
  39. package/templates/apps/api/src/server.ts +107 -0
  40. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
  41. package/templates/apps/api/src/services/lead-routing.ts +30 -2
  42. package/templates/apps/api/vercel.json +2 -2
  43. package/templates/apps/mcp/.env.example +10 -0
  44. package/templates/apps/mcp/CLAUDE.md +58 -7
  45. package/templates/apps/mcp/LICENSE +6 -5
  46. package/templates/apps/mcp/NOTICE +2 -0
  47. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  48. package/templates/apps/mcp/__tests__/app-entry.test.ts +325 -0
  49. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  50. package/templates/apps/mcp/__tests__/boot.test.ts +280 -0
  51. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  52. package/templates/apps/mcp/__tests__/index.test.ts +86 -47
  53. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +5 -6
  54. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +93 -0
  55. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  56. package/templates/apps/mcp/__tests__/server.test.ts +63 -6
  57. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  58. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  59. package/templates/apps/mcp/jest.config.cjs +3 -44
  60. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  61. package/templates/apps/mcp/src/app.ts +32 -0
  62. package/templates/apps/mcp/src/boot.ts +167 -0
  63. package/templates/apps/mcp/src/http-transport.ts +132 -88
  64. package/templates/apps/mcp/src/index.ts +19 -126
  65. package/templates/apps/mcp/src/lib/audit-single-page.ts +7 -6
  66. package/templates/apps/mcp/src/lib/content-adapter.ts +34 -5
  67. package/templates/apps/mcp/src/lib/topology.generated.ts +3 -0
  68. package/templates/apps/mcp/src/request-context.ts +52 -0
  69. package/templates/apps/mcp/src/server.ts +10 -2
  70. package/templates/apps/mcp/src/serverless-app.ts +88 -0
  71. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  72. package/templates/apps/mcp/src/tools/get_business_hours.ts +6 -5
  73. package/templates/apps/mcp/src/tools/get_faq.ts +6 -5
  74. package/templates/apps/mcp/src/tools/get_locations.ts +6 -5
  75. package/templates/apps/mcp/src/tools/get_offerings.ts +6 -5
  76. package/templates/apps/mcp/src/tools/get_pricing.ts +6 -5
  77. package/templates/apps/mcp/src/tools/get_site_info.ts +6 -5
  78. package/templates/apps/mcp/src/tools/list_pages.ts +8 -7
  79. package/templates/apps/mcp/src/tools/read_content.ts +7 -7
  80. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  81. package/templates/apps/mcp/src/tools/search_content.ts +6 -5
  82. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  83. package/templates/apps/mcp/tsconfig.json +2 -0
  84. package/templates/apps/web/LICENSE +6 -5
  85. package/templates/apps/web/NOTICE +2 -0
  86. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  87. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +16 -4
  88. package/templates/apps/web/app/.well-known/mcp.json/route.ts +3 -0
  89. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
  90. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
  91. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
  92. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  93. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
  94. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  95. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  96. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
  97. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  98. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  99. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
  100. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
  101. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  102. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  103. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
  104. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +25 -32
  105. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  106. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  107. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  115. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  116. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  117. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  118. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  119. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  120. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  121. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  123. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  124. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  125. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  126. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
  127. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  129. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  132. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  133. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
  134. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  135. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
  136. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  137. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  138. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  139. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  140. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  141. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  142. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  143. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  144. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  145. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  146. package/templates/apps/web/auth.ts +5 -1
  147. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  148. package/templates/apps/web/lib/__tests__/site-brand.test.ts +42 -0
  149. package/templates/apps/web/lib/account-config.ts +188 -52
  150. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  151. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  152. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  153. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  154. package/templates/apps/web/lib/site-brand.tsx +22 -6
  155. package/templates/apps/web/lib/topology.generated.ts +27 -0
  156. package/templates/apps/web/messages/en.json +156 -35
  157. package/templates/database/CHANGELOG.md +21 -0
  158. package/templates/database/LICENSE +6 -5
  159. package/templates/database/NOTICE +2 -0
  160. package/templates/database/README.md +1 -1
  161. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  162. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  163. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  164. package/templates/database/__tests__/seed.test.ts +54 -2
  165. package/templates/database/inbox/schema.prisma +90 -15
  166. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  167. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  168. package/templates/database/package.json +3 -2
  169. package/templates/database/seed.ts +34 -2
  170. package/versions.json +36 -36
  171. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  172. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  173. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  174. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  175. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  176. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  177. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  178. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  179. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  180. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  181. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  182. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  183. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  184. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  185. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  186. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  187. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  188. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  189. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  190. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  191. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  192. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  193. package/templates/database/__tests__/build.test.ts +0 -131
@@ -0,0 +1,192 @@
1
+ -- #8168 — the member area data model (spec
2
+ -- docs/specs/8168-member-area-data-model-spec.md D1–D3, D5, D7–D9, D12).
3
+ --
4
+ -- piece_versions (inbox) — one delivery of a piece to its client:
5
+ -- a private library file OR an https
6
+ -- link, numbered from 1 per piece, with
7
+ -- the client's review recorded on it.
8
+ -- NO round count, limit or remaining
9
+ -- column (#8140 disposition 3).
10
+ -- pieces.approved_at — set with an `approved` review; freezes
11
+ -- review (D5).
12
+ -- pieces_id_person_id_workspace_id_key — a unique index over a triple the
13
+ -- PK already makes unique: the target of
14
+ -- piece_versions_piece_fk, so "a piece of
15
+ -- THIS Person in THIS workspace" is one
16
+ -- database fact (D1).
17
+ -- interactions.kind / .payload — `message` · `quote` · `review`, and the
18
+ -- stored payload of a quote or review;
19
+ -- `inbound` joins `outbound` (D3). An
20
+ -- outbound row stays a payload-free
21
+ -- `message` — #7877's rows, unchanged.
22
+ -- flow_definitions.customers_enabled — whether a signed-in client may submit
23
+ -- the magnet from the member area (D7).
24
+ -- the `request-quote` magnet — one draft `contact` magnet with
25
+ -- customers_enabled = true, backfilled
26
+ -- into EVERY existing workspace (D8).
27
+ --
28
+ -- The only new foreign key stays inside the inbox namespace (ADR 0009
29
+ -- invariant 1). asset_id and delivered_by name rows of other namespaces:
30
+ -- plain columns with format CHECKs, no foreign key. The backfill reads
31
+ -- "workspaces" (auth namespace) in SQL only — no relation.
32
+ --
33
+ -- ADDITIVE: every ADD COLUMN is nullable or carries a constant default, so no
34
+ -- existing row fails a new CHECK — every existing interaction is an outbound
35
+ -- `message` with no payload, which the replaced and added CHECKs admit
36
+ -- (interactions_body_length keeps its 1–10000 rule for a `message`). The
37
+ -- unique index on "pieces" builds under the file's transaction, holding a
38
+ -- SHARE lock on "pieces" for one scan of a table that is a few rows per
39
+ -- client. The backfill inserts one row per workspace and NEVER overwrites a
40
+ -- workspace's existing (site_id, slug) row (ON CONFLICT DO NOTHING) —
41
+ -- re-running the block adds nothing.
42
+ --
43
+ -- Rollout: deploy this migration BEFORE any code that writes piece_versions,
44
+ -- pieces.approved_at, an inbound interaction, interactions.kind/payload or
45
+ -- flow_definitions.customers_enabled. Code written before it never names the
46
+ -- new columns, so it keeps working against the migrated database.
47
+ --
48
+ -- Rollback (safe while nothing but #8168's siblings use the new surface):
49
+ -- DELETE FROM "flow_definitions" WHERE "slug" = 'request-quote' AND "status" = 'draft' AND "customers_enabled";
50
+ -- ALTER TABLE "flow_definitions" DROP COLUMN "customers_enabled";
51
+ -- DELETE FROM "interactions" WHERE "direction" = 'inbound';
52
+ -- ALTER TABLE "interactions" DROP CONSTRAINT "interactions_kind_allowed",
53
+ -- DROP CONSTRAINT "interactions_outbound_shape", DROP CONSTRAINT "interactions_payload_by_kind",
54
+ -- DROP CONSTRAINT "interactions_payload_size", DROP CONSTRAINT "interactions_body_length",
55
+ -- DROP CONSTRAINT "interactions_direction_allowed";
56
+ -- ALTER TABLE "interactions" DROP COLUMN "kind", DROP COLUMN "payload";
57
+ -- then re-add #7877's interactions_direction_allowed (direction = 'outbound')
58
+ -- and interactions_body_length (1–10000 once trimmed) from
59
+ -- 20261011120000_member_area;
60
+ -- DROP TABLE "piece_versions"; DROP INDEX "pieces_id_person_id_workspace_id_key";
61
+ -- ALTER TABLE "pieces" DROP COLUMN "approved_at";
62
+ -- The first DELETE also removes an operator's own edits to a still-draft
63
+ -- seeded magnet; check for those before rolling back.
64
+
65
+ -- AlterTable
66
+ ALTER TABLE "flow_definitions" ADD COLUMN "customers_enabled" BOOLEAN NOT NULL DEFAULT false;
67
+
68
+ -- AlterTable
69
+ -- Existing rows and #7877's writer (which names no kind) stay `message`.
70
+ ALTER TABLE "interactions" ADD COLUMN "kind" VARCHAR(16) NOT NULL DEFAULT 'message',
71
+ ADD COLUMN "payload" JSONB;
72
+
73
+ -- AlterTable
74
+ ALTER TABLE "pieces" ADD COLUMN "approved_at" TIMESTAMP(3);
75
+
76
+ -- CreateTable
77
+ CREATE TABLE "piece_versions" (
78
+ "id" TEXT NOT NULL DEFAULT ('pver_'::text || (uuidv7())::text),
79
+ "workspace_id" TEXT NOT NULL,
80
+ "person_id" TEXT NOT NULL,
81
+ "piece_id" TEXT NOT NULL,
82
+ "version" INTEGER NOT NULL,
83
+ "asset_id" TEXT,
84
+ "link" VARCHAR(2048),
85
+ "note" TEXT,
86
+ "delivered_by" TEXT NOT NULL,
87
+ "delivered_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
88
+ "review" VARCHAR(24),
89
+ "review_comment" TEXT,
90
+ "reviewed_at" TIMESTAMP(3),
91
+
92
+ CONSTRAINT "piece_versions_pkey" PRIMARY KEY ("id")
93
+ );
94
+
95
+ -- CreateIndex
96
+ -- A client's artifacts: workspace_id = $1 AND person_id = $2 ORDER BY delivered_at DESC.
97
+ CREATE INDEX "piece_versions_workspace_id_person_id_delivered_at_idx" ON "piece_versions"("workspace_id", "person_id", "delivered_at");
98
+
99
+ -- CreateIndex
100
+ -- The public asset read's "is this asset a version file" check (AC-MAA-21).
101
+ CREATE INDEX "piece_versions_asset_id_idx" ON "piece_versions"("asset_id");
102
+
103
+ -- CreateIndex
104
+ -- A version number is a fact per piece; the writer numbers, this is the backstop.
105
+ CREATE UNIQUE INDEX "piece_versions_piece_id_version_key" ON "piece_versions"("piece_id", "version");
106
+
107
+ -- CreateIndex
108
+ -- Redundant with the PK; piece_versions_piece_fk needs it as its target.
109
+ CREATE UNIQUE INDEX "pieces_id_person_id_workspace_id_key" ON "pieces"("id", "person_id", "workspace_id");
110
+
111
+ -- AddForeignKey
112
+ -- A version names a piece of THIS Person in THIS workspace (the piece's own
113
+ -- FK already ties it to a Person of its workspace); deleting a piece with a
114
+ -- version is refused.
115
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_piece_fk" FOREIGN KEY ("piece_id", "person_id", "workspace_id") REFERENCES "pieces"("id", "person_id", "workspace_id") ON DELETE RESTRICT ON UPDATE CASCADE;
116
+
117
+ -- ADR-0031 rule 3 — piece_versions: pver_ (registered in
118
+ -- packages/ids/src/registry.ts; this migration's path is added to
119
+ -- migration-sync.test.ts's PK_CHECK_MIGRATIONS). Each reference column
120
+ -- carries the format CHECK of the entity it names, in the #7877 shape.
121
+ ALTER TABLE "piece_versions" ALTER COLUMN "id" TYPE text COLLATE "C";
122
+ ALTER TABLE "piece_versions" ALTER COLUMN "id" SET DEFAULT ('pver_'::text || (uuidv7())::text);
123
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_id_format" CHECK ("id" ~ '^pver_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
124
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_workspace_id_format" CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
125
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_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}$');
126
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_piece_id_format" CHECK ("piece_id" ~ '^piece_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
127
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_asset_id_format" CHECK ("asset_id" ~ '^asset_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
128
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_delivered_by_format" CHECK ("delivered_by" ~ '^user_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
129
+
130
+ -- D2 — a version's shape is held here, not by the writer alone.
131
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_version_positive"
132
+ CHECK ("version" >= 1);
133
+ -- A delivery is a file or a link: exactly one.
134
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_one_target"
135
+ CHECK (("asset_id" IS NULL) <> ("link" IS NULL));
136
+ -- An https scheme, a host with no userinfo, no whitespace anywhere — the
137
+ -- database floor under packages/validation's isHttpsLink (SEC-8168-5).
138
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_link_https"
139
+ CHECK ("link" IS NULL OR ("link" ~ '^https://[^[:space:]/?#@]+([/?#][^[:space:]]*)?$' AND char_length("link") <= 2048));
140
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_note_length"
141
+ CHECK ("note" IS NULL OR char_length(btrim("note")) BETWEEN 1 AND 2000);
142
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_review_allowed"
143
+ CHECK ("review" IS NULL OR "review" IN ('approved', 'changes-requested'));
144
+ -- A review and its timestamp are set together or not at all.
145
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_review_stamped"
146
+ CHECK (("review" IS NULL) = ("reviewed_at" IS NULL));
147
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_comment_needs_review"
148
+ CHECK ("review_comment" IS NULL OR "review" IS NOT NULL);
149
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_changes_need_comment"
150
+ CHECK ("review" IS DISTINCT FROM 'changes-requested' OR char_length(btrim(coalesce("review_comment", ''))) >= 1);
151
+ ALTER TABLE "piece_versions" ADD CONSTRAINT "piece_versions_comment_length"
152
+ CHECK ("review_comment" IS NULL OR char_length("review_comment") <= 5000);
153
+
154
+ -- D3 — the second direction and the kinds. Replaced IN PLACE under the same
155
+ -- names, so #7877's tests keep naming the same constraints.
156
+ ALTER TABLE "interactions" DROP CONSTRAINT "interactions_direction_allowed";
157
+ ALTER TABLE "interactions" ADD CONSTRAINT "interactions_direction_allowed"
158
+ CHECK ("direction" IN ('outbound', 'inbound'));
159
+ ALTER TABLE "interactions" ADD CONSTRAINT "interactions_kind_allowed"
160
+ CHECK ("kind" IN ('message', 'quote', 'review'));
161
+ -- An outbound row is a payload-free `message` — #7877's rows, unchanged.
162
+ ALTER TABLE "interactions" ADD CONSTRAINT "interactions_outbound_shape"
163
+ CHECK ("direction" <> 'outbound' OR ("kind" = 'message' AND "payload" IS NULL));
164
+ -- A quote or a review always carries its stored payload.
165
+ ALTER TABLE "interactions" ADD CONSTRAINT "interactions_payload_by_kind"
166
+ CHECK ("kind" = 'message' OR "payload" IS NOT NULL);
167
+ -- 64 items of a 200-character label and a 2000-character value fit with room (SEC-8168-4).
168
+ ALTER TABLE "interactions" ADD CONSTRAINT "interactions_payload_size"
169
+ CHECK ("payload" IS NULL OR octet_length("payload"::text) <= 262144);
170
+ -- A message body stays 1–10000 characters once trimmed; a quote or review
171
+ -- body may be empty; no body exceeds 10000.
172
+ ALTER TABLE "interactions" DROP CONSTRAINT "interactions_body_length";
173
+ ALTER TABLE "interactions" ADD CONSTRAINT "interactions_body_length"
174
+ CHECK (
175
+ char_length("body") <= 10000
176
+ AND ("kind" <> 'message' OR char_length(btrim("body")) BETWEEN 1 AND 10000)
177
+ );
178
+
179
+ -- D8/D9 — the request-quote magnet in every existing workspace. The config
180
+ -- literal below is test-held to `requestQuoteFlowConfig`
181
+ -- (packages/validation/src/schemas/flow.ts) by
182
+ -- database/__tests__/member-contract-migration.test.ts, and the block is
183
+ -- re-executed verbatim by member-contract.db.test.ts to prove it idempotent.
184
+ -- Do not edit one without the other.
185
+ -- BEGIN request-quote backfill
186
+ INSERT INTO "flow_definitions" ("site_id", "workspace_id", "slug", "config", "kind", "title", "status", "customers_enabled", "updated_at")
187
+ SELECT w."id", w."id", 'request-quote',
188
+ '{"slug":"request-quote","defaultLocale":"en","steps":[{"key":"request","type":"field","copy":{"title":{"en":"Request a quote"}},"fields":[{"name":"need","inputType":"textarea","required":true,"copy":{"label":{"en":"What do you need?"}}},{"name":"timing","inputType":"text","copy":{"label":{"en":"When do you need it?"}}},{"name":"budget","inputType":"text","copy":{"label":{"en":"Budget range"},"help":{"en":"A rough range is fine."}}},{"name":"link","inputType":"url","copy":{"label":{"en":"Link"},"help":{"en":"A brief, a document or an example. Must start with https://."}}}]}]}'::jsonb,
189
+ 'contact', 'Request a quote', 'draft', true, CURRENT_TIMESTAMP
190
+ FROM "workspaces" w
191
+ ON CONFLICT ("site_id", "slug") DO NOTHING;
192
+ -- END request-quote backfill
@@ -0,0 +1,20 @@
1
+ -- #8185 — record, at the moment the hub writes it, that a lead is a spoke's
2
+ -- live paid checkout (spec docs/specs/8185-spoke-checkout-purchases-spec.md
3
+ -- D1, D6, D10).
4
+ --
5
+ -- leads.spoke_checkout — set true by apps/api/src/services/lead-routing.ts
6
+ -- only for a spoke-keyed, unrelayed submit passing
7
+ -- the #7877 checkout plan rule's predicate. Written
8
+ -- at creation, never updated. NOT NULL with a
9
+ -- constant default, so existing rows read false and
10
+ -- nothing is rewritten; they are deliberately NOT
11
+ -- backfilled (D6: no stored fact proves an older
12
+ -- lead's provenance).
13
+ --
14
+ -- Rollout order (D10): apply this migration to a deployment's database BEFORE
15
+ -- deploying the API that writes the column. The old API is unaffected by it;
16
+ -- the new API's marked create fails without it.
17
+ --
18
+ -- Rollback: ALTER TABLE "leads" DROP COLUMN "spoke_checkout";
19
+
20
+ ALTER TABLE "leads" ADD COLUMN "spoke_checkout" BOOLEAN NOT NULL DEFAULT false;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@working-theory/database",
3
- "version": "0.38.0",
3
+ "version": "0.39.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.39.0",
3
+ "@working-theory/aeo": "0.39.0",
4
+ "@working-theory/agent-context": "0.39.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.39.0",
9
+ "@working-theory/api-client": "0.39.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.39.0",
12
+ "@working-theory/common": "0.39.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.39.0",
15
+ "@working-theory/content-projection": "0.39.0",
16
+ "@working-theory/email": "0.39.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.39.0",
19
+ "@working-theory/fumadocs-schema": "0.39.0",
20
+ "@working-theory/ids": "0.12.0",
21
+ "@working-theory/jsonld": "0.39.0",
22
+ "@working-theory/license": "0.39.0",
23
+ "@working-theory/metadata": "0.39.0",
24
+ "@working-theory/observability": "0.39.0",
25
+ "@working-theory/page-renderer": "0.39.0",
26
+ "@working-theory/plugin-mcp": "0.1.3",
27
+ "@working-theory/schema": "0.39.0",
28
+ "@working-theory/schema-pack-registry": "0.39.0",
29
+ "@working-theory/schema-validator": "0.39.0",
30
+ "@working-theory/sections-registry": "0.39.0",
31
+ "@working-theory/site-config": "0.39.0",
32
+ "@working-theory/spoke-checkout": "0.5.0",
33
+ "@working-theory/stripe-client": "0.39.0",
34
+ "@working-theory/templates": "0.39.0",
35
+ "@working-theory/theme": "0.39.0",
36
+ "@working-theory/ui": "0.39.0",
37
+ "@working-theory/validation": "0.39.0",
38
+ "@working-theory/web-i18n": "0.39.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.17",
46
+ "@working-theory/ui-docs-blog": "0.23.17",
47
+ "@working-theory/ui-scheduling": "0.23.17",
48
+ "@working-theory/ui-signal-collectors": "0.25.6",
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.39.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
- });