create-ailk 0.12.1 → 0.14.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 (163) hide show
  1. package/README.md +12 -4
  2. package/component-catalog.json +247 -33
  3. package/component-catalog.md +10 -6
  4. package/dist/apply-design.d.ts +21 -2
  5. package/dist/apply-design.js +120 -19
  6. package/dist/cli.js +1 -0
  7. package/dist/design-compile.d.ts +70 -2
  8. package/dist/design-compile.js +97 -1
  9. package/dist/design-home.d.ts +74 -0
  10. package/dist/design-home.js +256 -0
  11. package/dist/design-package.d.ts +39 -2
  12. package/dist/design-package.js +223 -9
  13. package/dist/design-presets.d.ts +17 -1
  14. package/dist/design-presets.js +17 -5
  15. package/dist/design-scaffold.d.ts +6 -25
  16. package/dist/design-scaffold.js +26 -74
  17. package/dist/index.d.ts +1 -0
  18. package/dist/lib/design-checks.d.ts +12 -1
  19. package/dist/lib/design-checks.js +38 -2
  20. package/dist/lib/module-license-gate.d.ts +9 -0
  21. package/dist/lib/module-license-gate.js +18 -0
  22. package/dist/module-architecture.js +27 -0
  23. package/dist/programmatic.js +70 -26
  24. package/dist/site-chrome.d.ts +38 -0
  25. package/dist/site-chrome.js +109 -2
  26. package/dist/site-fonts.d.ts +21 -0
  27. package/dist/site-fonts.js +54 -0
  28. package/dist/site-roster.d.ts +16 -0
  29. package/dist/site-roster.js +81 -0
  30. package/dist/starter-grid.js +15 -0
  31. package/dist/surfaces.js +12 -0
  32. package/dist/team-profiles.d.ts +29 -0
  33. package/dist/team-profiles.js +43 -0
  34. package/package.json +3 -3
  35. package/release-scope.json +1 -1
  36. package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
  37. package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
  38. package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
  39. package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
  40. package/site-templates/designs/minimalism/DESIGN.md +164 -34
  41. package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
  42. package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
  43. package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
  44. package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
  45. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
  46. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
  48. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
  49. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  50. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
  51. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
  52. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  53. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
  54. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
  55. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  56. package/templates/.env.example +23 -9
  57. package/templates/README.md +2 -0
  58. package/templates/apps/api/CLAUDE.md +24 -17
  59. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +98 -0
  60. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +321 -0
  61. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +117 -0
  62. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +39 -14
  63. package/templates/apps/api/src/config/index.ts +35 -20
  64. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +3 -0
  65. package/templates/apps/api/src/lib/__mocks__/prisma.ts +14 -0
  66. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +468 -0
  67. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +32 -2
  68. package/templates/apps/api/src/lib/agent-store.ts +521 -0
  69. package/templates/apps/api/src/lib/spoke-store.ts +13 -2
  70. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +384 -1
  71. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +61 -0
  72. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +176 -0
  73. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +76 -0
  74. package/templates/apps/api/src/middleware/auth.ts +219 -5
  75. package/templates/apps/api/src/middleware/default-tenant.ts +29 -8
  76. package/templates/apps/api/src/middleware/rate-limit.ts +35 -13
  77. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +76 -15
  78. package/templates/apps/api/src/routes/leads/index.ts +14 -6
  79. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -6
  80. package/templates/apps/api/src/routes/project-listings/index.ts +14 -9
  81. package/templates/apps/api/src/routes/project-listings/me.ts +4 -3
  82. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +7 -5
  83. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +198 -0
  84. package/templates/apps/api/src/server.ts +27 -0
  85. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +374 -57
  86. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +25 -2
  87. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +303 -17
  88. package/templates/apps/api/src/services/audit-log.ts +278 -47
  89. package/templates/apps/api/src/services/default-tenant.ts +55 -5
  90. package/templates/apps/api/src/services/leads-inbox.ts +12 -21
  91. package/templates/apps/api/src/services/project-listings.ts +1 -21
  92. package/templates/apps/api/src/services/public-site-resolver.ts +103 -13
  93. package/templates/apps/mcp/CLAUDE.md +44 -5
  94. package/templates/apps/mcp/__tests__/parity.test.ts +53 -1
  95. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +364 -0
  96. package/templates/apps/mcp/package.json +1 -1
  97. package/templates/apps/mcp/src/http-transport.ts +107 -1
  98. package/templates/apps/mcp/src/workspace-server.ts +193 -0
  99. package/templates/apps/web/.env.example +64 -37
  100. package/templates/apps/web/CLAUDE.md +11 -0
  101. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
  102. package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
  104. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  105. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  106. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
  107. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +69 -0
  108. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +64 -0
  109. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
  110. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
  111. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
  112. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
  113. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
  114. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
  115. package/templates/apps/web/app/[locale]/layout.tsx +156 -53
  116. package/templates/apps/web/app/[locale]/page.tsx +37 -0
  117. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  118. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
  119. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
  120. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
  121. package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
  122. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
  123. package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
  124. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
  125. package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
  126. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
  127. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
  128. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
  129. package/templates/apps/web/app/not-found.tsx +47 -0
  130. package/templates/apps/web/app/v1/[...path]/route.ts +16 -0
  131. package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
  132. package/templates/apps/web/lib/__tests__/relay.test.ts +515 -0
  133. package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
  134. package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
  135. package/templates/apps/web/lib/profile-back-link.ts +51 -0
  136. package/templates/apps/web/lib/relay.ts +279 -0
  137. package/templates/apps/web/lib/site-chrome.ts +12 -0
  138. package/templates/apps/web/lib/site-fonts.ts +15 -0
  139. package/templates/apps/web/lib/site-logo.tsx +39 -1
  140. package/templates/apps/web/lib/site-theme-head.ts +33 -0
  141. package/templates/apps/web/lib/site-theme.ts +13 -1
  142. package/templates/apps/web/messages/en.json +7 -1
  143. package/templates/apps/web/middleware.ts +5 -4
  144. package/templates/apps/web/next.config.mjs +6 -0
  145. package/templates/database/CHANGELOG.md +22 -0
  146. package/templates/database/__tests__/agents.db.test.ts +235 -0
  147. package/templates/database/auth/schema.prisma +61 -0
  148. package/templates/database/migrations/20261005120000_agents/migration.sql +84 -0
  149. package/templates/database/package.json +1 -1
  150. package/versions.json +35 -35
  151. package/site-templates/designs/minimalism/sample/README.md +0 -48
  152. package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
  153. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
  154. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
  155. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
  156. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
  157. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
  158. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
  159. package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
  160. package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
  161. package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
  162. package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
  163. package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @file agents.db.test.ts
3
+ *
4
+ * LIVE-DATABASE proof of the agents table (#3591, spec
5
+ * docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md D1; AC-1 and AC-2's
6
+ * cascade legs). Requires DATABASE_URL pointing at a Postgres with the
7
+ * migrations applied — the whole suite SKIPS when DATABASE_URL is unset,
8
+ * keeping the workspace Jest run DB-free (the repo convention; the
9
+ * `apps/api Route Tests (Postgres)` workflow exercises it).
10
+ *
11
+ * Uses the `pg` driver directly, as `spoke-registry.db.test.ts` does. What it
12
+ * proves that a mock cannot:
13
+ * 1. The six format CHECKs reject a malformed id.
14
+ * 2. `credential_hash` is unique.
15
+ * 3. Deleting the responsible person's membership, the workspace or the
16
+ * organization deletes the agent — so a re-invited person's new
17
+ * membership cannot revive it.
18
+ * 4. Deleting the membership of the person last shown the key sets
19
+ * `key_issued_to_membership_id` null and keeps the agent.
20
+ * 5. Deleting the creator's user row leaves the agent (no relation).
21
+ */
22
+ import { Client } from "pg";
23
+
24
+ const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
25
+
26
+ const AGENT_ID = /^agent_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
27
+
28
+ describeDb("the agents table (#3591, live DB)", () => {
29
+ let db: Client;
30
+ let organizationId: string;
31
+ let workspaceId: string;
32
+ let ownerUserId: string;
33
+ let personUserId: string;
34
+ let ownerMembershipId: string;
35
+ let personMembershipId: string;
36
+ let hashSeq = 0;
37
+
38
+ /** A fresh 64-character digest per insert, so the unique key never collides by accident. */
39
+ function nextHash(): string {
40
+ hashSeq += 1;
41
+ return hashSeq.toString(16).padStart(64, "0");
42
+ }
43
+
44
+ async function insertUser(tag: string): Promise<string> {
45
+ return (
46
+ await db.query<{ id: string }>(
47
+ `INSERT INTO "users" ("email", "updated_at") VALUES ($1, now()) RETURNING "id"`,
48
+ [`agents3591-${tag}@example.test`],
49
+ )
50
+ ).rows[0]!.id;
51
+ }
52
+
53
+ async function insertOrganization(slug: string): Promise<string> {
54
+ return (
55
+ await db.query<{ id: string }>(
56
+ `INSERT INTO "organizations" ("name", "slug", "updated_at")
57
+ VALUES ($1, $1, now()) RETURNING "id"`,
58
+ [`agents3591-${slug}`],
59
+ )
60
+ ).rows[0]!.id;
61
+ }
62
+
63
+ async function insertWorkspace(orgId: string, slug: string): Promise<string> {
64
+ return (
65
+ await db.query<{ id: string }>(
66
+ `INSERT INTO "workspaces" ("organization_id", "name", "slug", "updated_at")
67
+ VALUES ($1, $2, $2, now()) RETURNING "id"`,
68
+ [orgId, slug],
69
+ )
70
+ ).rows[0]!.id;
71
+ }
72
+
73
+ async function insertMembership(orgId: string, userId: string, role: string): Promise<string> {
74
+ return (
75
+ await db.query<{ id: string }>(
76
+ `INSERT INTO "memberships" ("organization_id", "user_id", "role")
77
+ VALUES ($1, $2, $3::"Role") RETURNING "id"`,
78
+ [orgId, userId, role],
79
+ )
80
+ ).rows[0]!.id;
81
+ }
82
+
83
+ type AgentColumns = {
84
+ id?: string;
85
+ organization_id: string;
86
+ workspace_id: string;
87
+ responsible_membership_id: string;
88
+ created_by_user_id: string;
89
+ key_issued_to_membership_id: string | null;
90
+ credential_hash: string;
91
+ };
92
+
93
+ function defaults(): AgentColumns {
94
+ return {
95
+ organization_id: organizationId,
96
+ workspace_id: workspaceId,
97
+ responsible_membership_id: personMembershipId,
98
+ created_by_user_id: ownerUserId,
99
+ key_issued_to_membership_id: ownerMembershipId,
100
+ credential_hash: nextHash(),
101
+ };
102
+ }
103
+
104
+ /** Insert one agent; the database mints its id unless one is given. */
105
+ async function insertAgent(overrides: Partial<AgentColumns> = {}): Promise<string> {
106
+ const row = { ...defaults(), ...overrides };
107
+ const columns = Object.keys(row);
108
+ const values = Object.values(row);
109
+ const { rows } = await db.query<{ id: string }>(
110
+ `INSERT INTO "agents" (${columns.map((c) => `"${c}"`).join(", ")}, "name", "credential_prefix", "updated_at")
111
+ VALUES (${columns.map((_, i) => `$${i + 1}`).join(", ")}, 'inbox helper', 'ailk_agt_abcdefgh', now())
112
+ RETURNING "id"`,
113
+ values,
114
+ );
115
+ return rows[0]!.id;
116
+ }
117
+
118
+ async function agentRow(id: string): Promise<{ key_issued_to_membership_id: string | null } | undefined> {
119
+ return (
120
+ await db.query<{ key_issued_to_membership_id: string | null }>(
121
+ `SELECT "key_issued_to_membership_id" FROM "agents" WHERE "id" = $1`,
122
+ [id],
123
+ )
124
+ ).rows[0];
125
+ }
126
+
127
+ beforeAll(async () => {
128
+ db = new Client({ connectionString: process.env.DATABASE_URL });
129
+ await db.connect();
130
+ organizationId = await insertOrganization("org");
131
+ workspaceId = await insertWorkspace(organizationId, "agents3591-ws");
132
+ ownerUserId = await insertUser("owner");
133
+ personUserId = await insertUser("person");
134
+ ownerMembershipId = await insertMembership(organizationId, ownerUserId, "owner");
135
+ personMembershipId = await insertMembership(organizationId, personUserId, "member");
136
+ });
137
+
138
+ afterAll(async () => {
139
+ await db.query(`DELETE FROM "organizations" WHERE "slug" LIKE 'agents3591-%'`);
140
+ await db.query(`DELETE FROM "users" WHERE "email" LIKE 'agents3591-%'`);
141
+ await db.end();
142
+ });
143
+
144
+ describe("format CHECKs (ADR-0031)", () => {
145
+ it("an agent takes a database-minted agent_ id, a member ceiling and is enabled", async () => {
146
+ const id = await insertAgent();
147
+ expect(id).toMatch(AGENT_ID);
148
+ const { rows } = await db.query<{ permission_level: string; enabled: boolean }>(
149
+ `SELECT "permission_level", "enabled" FROM "agents" WHERE "id" = $1`,
150
+ [id],
151
+ );
152
+ expect(rows[0]).toEqual({ permission_level: "member", enabled: true });
153
+ });
154
+
155
+ it.each([
156
+ ["id", "agents_id_format"],
157
+ ["organization_id", "agents_organization_id_format"],
158
+ ["workspace_id", "agents_workspace_id_format"],
159
+ ["responsible_membership_id", "agents_responsible_membership_id_format"],
160
+ ["key_issued_to_membership_id", "agents_key_issued_to_membership_id_format"],
161
+ ["created_by_user_id", "agents_created_by_user_id_format"],
162
+ ] as const)("REJECTS a malformed %s", async (column, constraint) => {
163
+ // The format CHECK fires before any foreign key.
164
+ await expect(insertAgent({ [column]: "bad" })).rejects.toThrow(new RegExp(constraint));
165
+ });
166
+
167
+ it("refuses a ceiling outside the enum", async () => {
168
+ const id = await insertAgent();
169
+ await expect(
170
+ db.query(`UPDATE "agents" SET "permission_level" = 'owner' WHERE "id" = $1`, [id]),
171
+ ).rejects.toThrow(/AgentPermissionLevel/);
172
+ });
173
+ });
174
+
175
+ it("a credential digest is unique", async () => {
176
+ const hash = nextHash();
177
+ await insertAgent({ credential_hash: hash });
178
+ await expect(insertAgent({ credential_hash: hash })).rejects.toThrow(
179
+ /agents_credential_hash_key/,
180
+ );
181
+ });
182
+
183
+ describe("cascades (D1 — leaving stops the agent)", () => {
184
+ it("deleting the person's membership deletes their agent, and a re-invite does not revive it", async () => {
185
+ const user = await insertUser("leaver");
186
+ const membership = await insertMembership(organizationId, user, "member");
187
+ const agent = await insertAgent({ responsible_membership_id: membership });
188
+
189
+ await db.query(`DELETE FROM "memberships" WHERE "id" = $1`, [membership]);
190
+ expect(await agentRow(agent)).toBeUndefined();
191
+
192
+ const reinvited = await insertMembership(organizationId, user, "member");
193
+ expect(reinvited).not.toBe(membership);
194
+ expect(await agentRow(agent)).toBeUndefined();
195
+ });
196
+
197
+ it("deleting the workspace deletes its agents", async () => {
198
+ const doomed = await insertWorkspace(organizationId, "agents3591-doomed");
199
+ const agent = await insertAgent({ workspace_id: doomed });
200
+ await db.query(`DELETE FROM "workspaces" WHERE "id" = $1`, [doomed]);
201
+ expect(await agentRow(agent)).toBeUndefined();
202
+ });
203
+
204
+ it("deleting the organization deletes its agents", async () => {
205
+ const org = await insertOrganization("doomed-org");
206
+ const ws = await insertWorkspace(org, "agents3591-doomed-org-ws");
207
+ const user = await insertUser("doomed-org-person");
208
+ const membership = await insertMembership(org, user, "owner");
209
+ const agent = await insertAgent({
210
+ organization_id: org,
211
+ workspace_id: ws,
212
+ responsible_membership_id: membership,
213
+ key_issued_to_membership_id: membership,
214
+ });
215
+ await db.query(`DELETE FROM "organizations" WHERE "id" = $1`, [org]);
216
+ expect(await agentRow(agent)).toBeUndefined();
217
+ });
218
+
219
+ it("deleting the key-holder's membership keeps the agent and clears key_issued_to_membership_id", async () => {
220
+ const admin = await insertUser("departing-admin");
221
+ const adminMembership = await insertMembership(organizationId, admin, "admin");
222
+ const agent = await insertAgent({ key_issued_to_membership_id: adminMembership });
223
+
224
+ await db.query(`DELETE FROM "memberships" WHERE "id" = $1`, [adminMembership]);
225
+ expect(await agentRow(agent)).toEqual({ key_issued_to_membership_id: null });
226
+ });
227
+
228
+ it("deleting the creator's user row leaves the agent (created_by_user_id has no relation)", async () => {
229
+ const creator = await insertUser("creator");
230
+ const agent = await insertAgent({ created_by_user_id: creator, key_issued_to_membership_id: null });
231
+ await db.query(`DELETE FROM "users" WHERE "id" = $1`, [creator]);
232
+ expect(await agentRow(agent)).toEqual({ key_issued_to_membership_id: null });
233
+ });
234
+ });
235
+ });
@@ -82,6 +82,8 @@ model Organization {
82
82
  memberships Membership[]
83
83
  invitations Invitation[]
84
84
  workspaces Workspace[]
85
+ // #3591 — the agents (delegates) of this organization's members.
86
+ agents Agent[]
85
87
 
86
88
  @@unique([billingProvider, billingCustomerId])
87
89
  @@map("organizations")
@@ -102,6 +104,8 @@ model Workspace {
102
104
  organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
103
105
  // #7532 — the hub's spoke registry rows bound to this workspace.
104
106
  spokes Spoke[]
107
+ // #3591 — the agents bound to this workspace.
108
+ agents Agent[]
105
109
 
106
110
  @@unique([organizationId, slug])
107
111
  @@index([organizationId])
@@ -137,6 +141,59 @@ model Spoke {
137
141
  @@map("spokes")
138
142
  }
139
143
 
144
+ /// AgentPermissionLevel — an agent's ceiling (#3591, spec
145
+ /// docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md D1, D5). The agent
146
+ /// acts with the LOWER of its person's `Role` and this. One value in v1;
147
+ /// raising the ceiling later is a new value here and in
148
+ /// `@working-theory/validation`'s `AGENT_PERMISSION_LEVELS`.
149
+ enum AgentPermissionLevel {
150
+ member
151
+ }
152
+
153
+ /// Agent — a stored delegate of ONE person, bound to ONE workspace (#3591,
154
+ /// spec docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md D1, D2).
155
+ ///
156
+ /// The agent signs in with a key (`ailk_agt_…`, shown once) and passes the
157
+ /// same tenant guard a browser session does, through its person's live
158
+ /// `Membership` (`responsibleMembershipId`). Removing that membership deletes
159
+ /// the agent (the FK below), so a re-invite cannot revive it.
160
+ ///
161
+ /// Only the key's SHA-256 digest is stored (`credentialHash`) plus a short
162
+ /// recognisable `credentialPrefix`; the key itself never is.
163
+ /// `keyIssuedToMembershipId` is the owner or admin who created or last
164
+ /// re-keyed the agent — the only person who has seen its key. Removing that
165
+ /// membership sets the column null and the guard refuses the agent until it
166
+ /// is re-keyed. `createdByUserId` is a plain column with no relation, so
167
+ /// deleting the creator does not delete the agents they made for others.
168
+ /// `apps/api/src/lib/agent-store.ts` is the only file that names
169
+ /// `prisma.agent`.
170
+ model Agent {
171
+ id String @id @default(dbgenerated("('agent_'::text || (uuidv7())::text)")) @db.Text
172
+ organizationId String @map("organization_id")
173
+ workspaceId String @map("workspace_id")
174
+ responsibleMembershipId String @map("responsible_membership_id")
175
+ name String @db.VarChar(80)
176
+ permissionLevel AgentPermissionLevel @default(member) @map("permission_level")
177
+ enabled Boolean @default(true)
178
+ credentialHash String @unique @map("credential_hash") @db.Char(64)
179
+ credentialPrefix String @map("credential_prefix") @db.VarChar(24)
180
+ lastActiveAt DateTime? @map("last_active_at")
181
+ createdByUserId String @map("created_by_user_id")
182
+ keyIssuedToMembershipId String? @map("key_issued_to_membership_id")
183
+ createdAt DateTime @default(now()) @map("created_at")
184
+ updatedAt DateTime @updatedAt @map("updated_at")
185
+
186
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
187
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
188
+ responsible Membership @relation("AgentResponsible", fields: [responsibleMembershipId], references: [id], onDelete: Cascade)
189
+ keyIssuedTo Membership? @relation("AgentKeyIssuedTo", fields: [keyIssuedToMembershipId], references: [id], onDelete: SetNull)
190
+
191
+ @@index([organizationId])
192
+ @@index([workspaceId])
193
+ @@index([responsibleMembershipId])
194
+ @@map("agents")
195
+ }
196
+
140
197
  /// Membership — User ↔ Organization with a Role. Managed by the Better Auth
141
198
  /// organization plugin (modelName "membership"). Org-level: grants visibility
142
199
  /// into every workspace of the organization.
@@ -149,6 +206,10 @@ model Membership {
149
206
 
150
207
  organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
151
208
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
209
+ // #3591 — the agents this person is responsible for, and the agents whose
210
+ // current key this person was the one shown.
211
+ agents Agent[] @relation("AgentResponsible")
212
+ agentKeysIssued Agent[] @relation("AgentKeyIssuedTo")
152
213
 
153
214
  @@unique([organizationId, userId])
154
215
  @@index([userId])
@@ -0,0 +1,84 @@
1
+ -- #3591 — the agent seat, step A (spec
2
+ -- docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md D1).
3
+ --
4
+ -- agents (auth) — one row per agent: a stored delegate of ONE person (its
5
+ -- responsible Membership), bound to ONE workspace. It stores
6
+ -- the SHA-256 digest of the agent's key, never the key.
7
+ -- Deleting the organization, the workspace or the person's
8
+ -- membership deletes the agent; deleting the membership of
9
+ -- the person who was last shown its key sets
10
+ -- key_issued_to_membership_id null, which the guard refuses
11
+ -- until the agent is re-keyed. created_by_user_id is a plain
12
+ -- column with no foreign key, so deleting the creator does
13
+ -- not delete the agents they made for someone else.
14
+ --
15
+ -- PURELY ADDITIVE: one new enum, one new table, its indexes and four FKs (to
16
+ -- "organizations", "workspaces" and twice to "memberships"). No statement
17
+ -- touches an existing table's rows or columns, so there is no pre-assertion
18
+ -- block — this migration cannot lose a row.
19
+ --
20
+ -- Rollback: DROP TABLE "agents"; DROP TYPE "AgentPermissionLevel". Safe while
21
+ -- nothing but #3591's own code reads them.
22
+
23
+ -- CreateEnum
24
+ CREATE TYPE "AgentPermissionLevel" AS ENUM ('member');
25
+
26
+ -- CreateTable
27
+ CREATE TABLE "agents" (
28
+ "id" TEXT NOT NULL DEFAULT ('agent_'::text || (uuidv7())::text),
29
+ "organization_id" TEXT NOT NULL,
30
+ "workspace_id" TEXT NOT NULL,
31
+ "responsible_membership_id" TEXT NOT NULL,
32
+ "name" VARCHAR(80) NOT NULL,
33
+ "permission_level" "AgentPermissionLevel" NOT NULL DEFAULT 'member',
34
+ "enabled" BOOLEAN NOT NULL DEFAULT true,
35
+ "credential_hash" CHAR(64) NOT NULL,
36
+ "credential_prefix" VARCHAR(24) NOT NULL,
37
+ "last_active_at" TIMESTAMP(3),
38
+ "created_by_user_id" TEXT NOT NULL,
39
+ "key_issued_to_membership_id" TEXT,
40
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
+ "updated_at" TIMESTAMP(3) NOT NULL,
42
+
43
+ CONSTRAINT "agents_pkey" PRIMARY KEY ("id")
44
+ );
45
+
46
+ -- CreateIndex
47
+ -- The lookup an agent's every request makes (`findAgentByKeyHash`).
48
+ CREATE UNIQUE INDEX "agents_credential_hash_key" ON "agents"("credential_hash");
49
+
50
+ -- CreateIndex
51
+ CREATE INDEX "agents_organization_id_idx" ON "agents"("organization_id");
52
+
53
+ -- CreateIndex
54
+ CREATE INDEX "agents_workspace_id_idx" ON "agents"("workspace_id");
55
+
56
+ -- CreateIndex
57
+ CREATE INDEX "agents_responsible_membership_id_idx" ON "agents"("responsible_membership_id");
58
+
59
+ -- AddForeignKey
60
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
61
+
62
+ -- AddForeignKey
63
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64
+
65
+ -- AddForeignKey
66
+ -- Leaving stops the agent: removing the person's membership deletes it.
67
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_responsible_membership_id_fkey" FOREIGN KEY ("responsible_membership_id") REFERENCES "memberships"("id") ON DELETE CASCADE ON UPDATE CASCADE;
68
+
69
+ -- AddForeignKey
70
+ -- A key dies with the person who received it: SET NULL, which the guard refuses.
71
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_key_issued_to_membership_id_fkey" FOREIGN KEY ("key_issued_to_membership_id") REFERENCES "memberships"("id") ON DELETE SET NULL ON UPDATE CASCADE;
72
+
73
+ -- ADR-0031 rule 3 — agents: agent_ (registered in packages/ids/src/registry.ts;
74
+ -- this migration's path is added to migration-sync.test.ts's
75
+ -- PK_CHECK_MIGRATIONS). Each reference column carries the format CHECK of the
76
+ -- entity it names, in the exact pattern the spoke migration uses.
77
+ ALTER TABLE "agents" ALTER COLUMN "id" TYPE text COLLATE "C";
78
+ ALTER TABLE "agents" ALTER COLUMN "id" SET DEFAULT ('agent_'::text || (uuidv7())::text);
79
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_id_format" CHECK ("id" ~ '^agent_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
80
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_organization_id_format" CHECK ("organization_id" ~ '^org_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
81
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_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}$');
82
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_responsible_membership_id_format" CHECK ("responsible_membership_id" ~ '^member_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
83
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_key_issued_to_membership_id_format" CHECK ("key_issued_to_membership_id" ~ '^member_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
84
+ ALTER TABLE "agents" ADD CONSTRAINT "agents_created_by_user_id_format" CHECK ("created_by_user_id" ~ '^user_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@working-theory/database",
3
- "version": "0.34.1",
3
+ "version": "0.36.0",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/versions.json CHANGED
@@ -1,52 +1,52 @@
1
1
  {
2
- "@working-theory/admin-ui": "0.34.1",
3
- "@working-theory/aeo": "0.34.1",
4
- "@working-theory/agent-context": "0.34.1",
2
+ "@working-theory/admin-ui": "0.36.0",
3
+ "@working-theory/aeo": "0.36.0",
4
+ "@working-theory/agent-context": "0.36.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.34.1",
9
- "@working-theory/api-client": "0.34.1",
8
+ "@working-theory/analytics": "0.36.0",
9
+ "@working-theory/api-client": "0.36.0",
10
10
  "@working-theory/cda-filesystem": "0.0.0",
11
- "@working-theory/cli": "0.34.1",
12
- "@working-theory/common": "0.34.1",
11
+ "@working-theory/cli": "0.36.0",
12
+ "@working-theory/common": "0.36.0",
13
13
  "@working-theory/consent": "0.1.0",
14
- "@working-theory/content-adapters": "0.34.1",
15
- "@working-theory/content-projection": "0.34.1",
16
- "@working-theory/email": "0.34.1",
14
+ "@working-theory/content-adapters": "0.36.0",
15
+ "@working-theory/content-projection": "0.36.0",
16
+ "@working-theory/email": "0.36.0",
17
17
  "@working-theory/env": "0.0.0",
18
- "@working-theory/eslint-config": "0.34.1",
19
- "@working-theory/fumadocs-schema": "0.34.1",
20
- "@working-theory/ids": "0.9.1",
21
- "@working-theory/jsonld": "0.34.1",
22
- "@working-theory/license": "0.34.1",
23
- "@working-theory/metadata": "0.34.1",
24
- "@working-theory/observability": "0.34.1",
25
- "@working-theory/page-renderer": "0.34.1",
26
- "@working-theory/plugin-mcp": "0.1.1",
27
- "@working-theory/schema": "0.34.1",
28
- "@working-theory/schema-pack-registry": "0.34.1",
29
- "@working-theory/schema-validator": "0.34.1",
30
- "@working-theory/sections-registry": "0.34.1",
31
- "@working-theory/site-config": "0.34.1",
32
- "@working-theory/stripe-client": "0.34.1",
33
- "@working-theory/templates": "0.34.1",
34
- "@working-theory/theme": "0.34.1",
35
- "@working-theory/ui": "0.34.1",
36
- "@working-theory/validation": "0.34.1",
37
- "@working-theory/web-i18n": "0.34.1",
18
+ "@working-theory/eslint-config": "0.36.0",
19
+ "@working-theory/fumadocs-schema": "0.36.0",
20
+ "@working-theory/ids": "0.10.0",
21
+ "@working-theory/jsonld": "0.36.0",
22
+ "@working-theory/license": "0.36.0",
23
+ "@working-theory/metadata": "0.36.0",
24
+ "@working-theory/observability": "0.36.0",
25
+ "@working-theory/page-renderer": "0.36.0",
26
+ "@working-theory/plugin-mcp": "0.1.2",
27
+ "@working-theory/schema": "0.36.0",
28
+ "@working-theory/schema-pack-registry": "0.36.0",
29
+ "@working-theory/schema-validator": "0.36.0",
30
+ "@working-theory/sections-registry": "0.36.0",
31
+ "@working-theory/site-config": "0.36.0",
32
+ "@working-theory/stripe-client": "0.36.0",
33
+ "@working-theory/templates": "0.36.0",
34
+ "@working-theory/theme": "0.36.0",
35
+ "@working-theory/ui": "0.36.0",
36
+ "@working-theory/validation": "0.36.0",
37
+ "@working-theory/web-i18n": "0.36.0",
38
38
  "@working-theory/billing-client": "0.0.0",
39
39
  "@working-theory/branding": "0.0.0",
40
40
  "@working-theory/license-client": "0.0.0",
41
41
  "@working-theory/license-manifest": "0.0.0",
42
42
  "@working-theory/partner-session": "0.0.0",
43
43
  "@working-theory/preview-protocol": "0.0.0",
44
- "@working-theory/ui-commerce": "0.23.12",
45
- "@working-theory/ui-docs-blog": "0.23.12",
46
- "@working-theory/ui-scheduling": "0.23.12",
47
- "@working-theory/ui-signal-collectors": "0.25.1",
44
+ "@working-theory/ui-commerce": "0.23.14",
45
+ "@working-theory/ui-docs-blog": "0.23.14",
46
+ "@working-theory/ui-scheduling": "0.23.14",
47
+ "@working-theory/ui-signal-collectors": "0.25.3",
48
48
  "@working-theory/web": "0.0.0",
49
49
  "@working-theory/api": "1.0.0",
50
50
  "@working-theory/mcp": "0.0.0",
51
- "@working-theory/database": "0.34.1"
51
+ "@working-theory/database": "0.36.0"
52
52
  }
@@ -1,48 +0,0 @@
1
- # The `minimalism` design's home-deck sample
2
-
3
- > Narrative moved here from the retired minimalism template's `TEMPLATE-MANIFEST.md` when
4
- > that tree retired (spec 6892 D4/D5) — `minimalism` is a design, not a template, so it
5
- > carries no generated manifest of its own. This file is NOT installed onto a scaffold
6
- > (`installDesignSample` copies only `content/**` and `public/**`); it documents the sample for
7
- > maintainers of this design.
8
-
9
- The second Pro template (#5589) that first wore this design was a reports-first site whose home
10
- is a **nine-slide scroll-snap deck**, shaped after the reference captured in
11
- `docs/research/bondcap-capture/`. Structure and design are the reference; **the copy is not** —
12
- every line in `content/en/HomePage/home.mdx` is placeholder English of the same shape and length.
13
-
14
- `layout.home: "deck"` (the `minimalism` design's own layout knob) makes every direct section
15
- child of `home.mdx` one full-viewport snap target, in order. The mark is hidden on slide 1 (the
16
- deck-hero landing state) and revealed from slide 2 on.
17
-
18
- | # | Slide | Section | Ground | Action |
19
- | --- | ----------------- | -------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------- |
20
- | 1 | Landing | `hero` (`backdrop`) | gradient band + backdrop | `{Primary CTA} →` → `/reports`, centered; 44px scroll cue. |
21
- | 2 | Tagline | `content-block` (`align: center`, `fill: viewport`) | light | — |
22
- | 3 | Our community | `logoCloud` (`actions`) | light | `Refer someone →` → `/referral`, bottom-centered at 44px |
23
- | 4 | Reports headline | `content-block` (`align: center`, `fill: viewport`) | light | — |
24
- | 5 | Special reports | `reports-list` (`list`, `tone: on-dark`) | dark (`gradient-dark`) | four dated titles → `/reports/<slug>` |
25
- | 6 | Favorites | `reports-list` (`list`, `tone: on-dark`) | dark (`gradient-dark`) | year-only titles → `/reports/<slug>` |
26
- | 7 | Services headline | `content-block` (`align: center`, `fill: viewport`) | light | `Hire me →` → `/meet` |
27
- | 8 | Partners | `team` (`with-large-images`, `hoverReveal`, `phoneColumns: 2`) | light | `About us →` → `/about`, bottom-centered at 44px |
28
- | 9 | Closing | `cta` (`minimal`) | light | `Contact me →` → `/contact`, centered |
29
-
30
- ## The site principle
31
-
32
- Every slide's action sits at the bottom, centered, at 44px or larger; grids are 2-up on a phone.
33
- This is held by bounded section props on the sections above, never by a `className` override or
34
- hand-rolled markup (`.claude/rules/frontend.md` § Chrome Composes Blocks).
35
-
36
- ## The 11 SVGs
37
-
38
- `public/hero-backdrop.svg`, `public/wordmark-placeholder.svg`, three `public/logo-placeholder-*.svg`
39
- and six `public/avatar-placeholder-*.svg` — all placeholders, referenced only from this sample's
40
- `home.mdx` (the avatars are also referenced by the retired bespoke `AboutPage` seed, which did not
41
- move with this design — its team slide is the only remaining consumer). They ride with the design
42
- because the deck uses all 11; a shape/design pair that does not install this sample never ships
43
- them (D4).
44
-
45
- ## Copy density
46
-
47
- This design declares `copy: { density: "sparse" }` in its `DESIGN.md` — its pages, this deck
48
- included, carry far fewer words than a standard site.