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
@@ -1,364 +0,0 @@
1
- // oss: false
2
- /**
3
- * The signed-in workspace surface at the transport (#3591 D15, D16 — spec
4
- * AC-5 and AC-6).
5
- *
6
- * Two real servers on ephemeral ports: apps/mcp's HTTP transport, and a stub
7
- * of apps/api that answers every twin route and REFUSES any request whose
8
- * `Authorization` header is not the caller's key — so a tool that forwarded
9
- * no key, or another one, fails here rather than passing on a mock.
10
- *
11
- * - each of the eleven tools forwards the caller's key to its twin route's
12
- * path, with its query and body, and returns the twin's `data`;
13
- * - a POST to `/mcp/workspace` with no key, a personal-token-shaped key or
14
- * another scheme is 401, and the stub sees no call;
15
- * - GET and DELETE are 405; a JSON-RPC batch is 400;
16
- * - when the stub answers 403 the tool error carries the code and status,
17
- * never the key;
18
- * - `/mcp` is unchanged: a key sent there changes nothing, and it lists no
19
- * workspace tool.
20
- *
21
- * No fixture holds a key-shaped literal: the key is built at run time.
22
- */
23
-
24
- import { createServer, type IncomingMessage, type Server } from "node:http";
25
- import type { AddressInfo } from "node:net";
26
-
27
- import type { FastifyInstance } from "fastify";
28
-
29
- import { SITE_ARGS, SITE_WIRE } from "../../../tests/parity/api-parity/fixtures/site-wire";
30
- import { buildHttpTransportApp } from "../src/http-transport.js";
31
- import { listRegisteredTools } from "../src/server.js";
32
- import { listWorkspaceTools } from "../src/workspace-server.js";
33
-
34
- const KEY = ["ailk", "agt", "w".repeat(43)].join("_");
35
- const PERSONAL_TOKEN = ["tok", "x".repeat(40)].join("_");
36
-
37
- const CONFIG = {
38
- path: "/mcp",
39
- stateless: true,
40
- rateLimits: { readPerMin: 1000, writePerMin: 1000 },
41
- cors: { writeAllowedOrigins: [] as string[] },
42
- abuse: { captureLeadDailyCapPerIp: 50, blockedUserAgents: "" },
43
- } as const;
44
-
45
- const WS = SITE_ARGS.get_inbox_message.workspaceId;
46
- const LEAD = SITE_ARGS.get_inbox_message.leadId;
47
- const LISTING = SITE_ARGS.get_inbox_application.id;
48
-
49
- /** Each tool: the twin request the stub must see, and the `data` it answers. */
50
- const TWINS: ReadonlyArray<{
51
- tool: keyof typeof SITE_WIRE;
52
- method: string;
53
- path: string;
54
- body?: unknown;
55
- }> = [
56
- { tool: "get_agent_context", method: "GET", path: "/v1/agent/context" },
57
- {
58
- tool: "list_inbox_messages",
59
- method: "GET",
60
- path: `/v1/workspaces/${WS}/messages?limit=25`,
61
- },
62
- {
63
- tool: "get_inbox_message",
64
- method: "GET",
65
- path: `/v1/workspaces/${WS}/messages/${LEAD}`,
66
- },
67
- {
68
- tool: "mark_inbox_message_done",
69
- method: "PATCH",
70
- path: `/v1/workspaces/${WS}/messages/${LEAD}`,
71
- body: { handled: true },
72
- },
73
- {
74
- tool: "accept_inbox_message",
75
- method: "POST",
76
- path: `/v1/workspaces/${WS}/messages/${LEAD}/accept`,
77
- body: { accept: true },
78
- },
79
- {
80
- tool: "list_inbox_submissions",
81
- method: "GET",
82
- path: `/v1/workspaces/${WS}/submissions?limit=25&state=unhandled`,
83
- },
84
- {
85
- tool: "get_inbox_submission",
86
- method: "GET",
87
- path: `/v1/workspaces/${WS}/submissions/${LEAD}`,
88
- },
89
- {
90
- tool: "mark_inbox_submission_done",
91
- method: "PATCH",
92
- path: `/v1/workspaces/${WS}/submissions/${LEAD}`,
93
- body: { handled: true },
94
- },
95
- {
96
- tool: "list_inbox_applications",
97
- method: "GET",
98
- path: "/v1/project-listings",
99
- },
100
- {
101
- tool: "get_inbox_application",
102
- method: "GET",
103
- path: `/v1/project-listings/${LISTING}`,
104
- },
105
- {
106
- tool: "decide_inbox_application",
107
- method: "PATCH",
108
- path: `/v1/project-listings/${LISTING}`,
109
- body: { status: "active" },
110
- },
111
- ];
112
-
113
- // ── The apps/api stub ────────────────────────────────────────────────────────
114
-
115
- interface SeenCall {
116
- method: string;
117
- path: string;
118
- authorization: string | undefined;
119
- body: unknown;
120
- }
121
-
122
- let api: Server;
123
- let seen: SeenCall[] = [];
124
- /** When set, every call is answered with this refusal instead. */
125
- let refuseWith: { status: number; code: string } | null = null;
126
-
127
- async function readBody(req: IncomingMessage): Promise<unknown> {
128
- let text = "";
129
- for await (const chunk of req) text += chunk;
130
- return text === "" ? undefined : JSON.parse(text);
131
- }
132
-
133
- function startApiStub(): Promise<Server> {
134
- const server = createServer(async (req, res) => {
135
- const body = await readBody(req);
136
- seen.push({
137
- method: req.method ?? "",
138
- path: req.url ?? "",
139
- authorization: req.headers["authorization"],
140
- body,
141
- });
142
- const send = (status: number, payload: unknown) => {
143
- res.writeHead(status, { "content-type": "application/json" });
144
- res.end(JSON.stringify(payload));
145
- };
146
- // The stub is apps/api's one check: no key, no data.
147
- if (req.headers["authorization"] !== `Bearer ${KEY}`) {
148
- return send(401, {
149
- ok: false,
150
- error: { code: "unauthorized", message: "Authentication required" },
151
- });
152
- }
153
- if (refuseWith) {
154
- return send(refuseWith.status, {
155
- ok: false,
156
- error: { code: refuseWith.code, message: "Refused" },
157
- });
158
- }
159
- const twin = TWINS.find(
160
- (t) => t.method === req.method && t.path === req.url,
161
- );
162
- if (!twin) {
163
- return send(404, {
164
- ok: false,
165
- error: { code: "not_found", message: "No such route" },
166
- });
167
- }
168
- return send(200, { ok: true, data: SITE_WIRE[twin.tool] });
169
- });
170
- return new Promise((resolve) =>
171
- server.listen(0, "127.0.0.1", () => resolve(server)),
172
- );
173
- }
174
-
175
- // ── The apps/mcp transport ───────────────────────────────────────────────────
176
-
177
- let app: FastifyInstance;
178
- let port: number;
179
- const priorModules = process.env.AILK_ENABLED_MODULES;
180
- const priorApiUrl = process.env.AILK_API_URL;
181
-
182
- beforeAll(async () => {
183
- process.env.AILK_ENABLED_MODULES = "workspace,multi-tenant,signal-collectors";
184
- api = await startApiStub();
185
- process.env.AILK_API_URL = `http://127.0.0.1:${(api.address() as AddressInfo).port}`;
186
- app = await buildHttpTransportApp(CONFIG);
187
- await app.listen({ host: "127.0.0.1", port: 0 });
188
- port = (app.server.address() as AddressInfo).port;
189
- });
190
-
191
- afterAll(async () => {
192
- await app.close();
193
- await new Promise((resolve) => api.close(resolve));
194
- if (priorModules === undefined) delete process.env.AILK_ENABLED_MODULES;
195
- else process.env.AILK_ENABLED_MODULES = priorModules;
196
- if (priorApiUrl === undefined) delete process.env.AILK_API_URL;
197
- else process.env.AILK_API_URL = priorApiUrl;
198
- });
199
-
200
- beforeEach(() => {
201
- seen = [];
202
- refuseWith = null;
203
- });
204
-
205
- type JsonRpcResponse = {
206
- result?: {
207
- tools?: { name: string }[];
208
- content?: { type: string; text: string }[];
209
- isError?: boolean;
210
- };
211
- error?: { code: number; message: string };
212
- };
213
-
214
- function rpc(method: string, params: object = {}, id = 1) {
215
- return { jsonrpc: "2.0", id, method, params };
216
- }
217
-
218
- function post(
219
- path: string,
220
- payload: unknown,
221
- headers: Record<string, string> = {},
222
- ): Promise<Response> {
223
- return fetch(`http://127.0.0.1:${port}${path}`, {
224
- method: "POST",
225
- body: JSON.stringify(payload),
226
- headers: {
227
- "content-type": "application/json",
228
- accept: "application/json, text/event-stream",
229
- ...headers,
230
- },
231
- });
232
- }
233
-
234
- const withKey = { authorization: `Bearer ${KEY}` };
235
-
236
- async function callTool(name: string, args: object) {
237
- const res = await post(
238
- "/mcp/workspace",
239
- rpc("tools/call", { name, arguments: args }),
240
- withKey,
241
- );
242
- expect(res.status).toBe(200);
243
- const text = await res.text();
244
- const body = JSON.parse(text) as JsonRpcResponse;
245
- return { text, result: body.result! };
246
- }
247
-
248
- // ── AC-5 — the eleven tools forward the key to their twins ───────────────────
249
-
250
- describe("/mcp/workspace — each tool forwards the caller's key to its twin (AC-5)", () => {
251
- it("lists exactly the eleven signed-in tools", async () => {
252
- const res = await post("/mcp/workspace", rpc("tools/list"), withKey);
253
- expect(res.status).toBe(200);
254
- const body = (await res.json()) as JsonRpcResponse;
255
- const names = body.result!.tools!.map((t) => t.name);
256
- expect(names).toEqual(TWINS.map((t) => t.tool));
257
- expect(names).toEqual(listWorkspaceTools());
258
- });
259
-
260
- it.each(TWINS)(
261
- "$tool → $method $path, with the key, returning the twin's data",
262
- async ({ tool, method, path, body }) => {
263
- const { result } = await callTool(tool, SITE_ARGS[tool]);
264
- expect(seen).toEqual([
265
- { method, path, authorization: `Bearer ${KEY}`, body },
266
- ]);
267
- expect(result.isError).toBeFalsy();
268
- expect(JSON.parse(result.content![0]!.text)).toEqual({
269
- ok: true,
270
- data: SITE_WIRE[tool],
271
- });
272
- },
273
- );
274
- });
275
-
276
- describe("/mcp/workspace — refused before any tool runs (AC-5)", () => {
277
- const call = rpc("tools/call", {
278
- name: "list_inbox_messages",
279
- arguments: SITE_ARGS.list_inbox_messages,
280
- });
281
-
282
- it.each<[string, Record<string, string>]>([
283
- ["no Authorization header", {}],
284
- [
285
- "a personal-token-shaped bearer",
286
- { authorization: `Bearer ${PERSONAL_TOKEN}` },
287
- ],
288
- ["an agent key under another scheme", { authorization: `Basic ${KEY}` }],
289
- ["a truncated agent key", { authorization: `Bearer ${KEY.slice(0, -1)}` }],
290
- ])("%s is 401, and apps/api sees no call", async (_label, headers) => {
291
- const res = await post("/mcp/workspace", call, headers);
292
- expect(res.status).toBe(401);
293
- expect(await res.json()).toEqual({ error: "unauthorized" });
294
- expect(seen).toEqual([]);
295
- });
296
-
297
- it.each(["GET", "DELETE"])("%s is 405 — the path keeps no session", async (method) => {
298
- const res = await fetch(`http://127.0.0.1:${port}/mcp/workspace`, {
299
- method,
300
- headers: withKey,
301
- });
302
- expect(res.status).toBe(405);
303
- expect(res.headers.get("allow")).toBe("POST");
304
- });
305
-
306
- it("a JSON-RPC batch is 400, and no call in it runs", async () => {
307
- const res = await post(
308
- "/mcp/workspace",
309
- [call, { ...call, id: 2 }],
310
- withKey,
311
- );
312
- expect(res.status).toBe(400);
313
- expect(seen).toEqual([]);
314
- });
315
- });
316
-
317
- describe("/mcp/workspace — a refusal from apps/api (AC-5)", () => {
318
- it("a 403 becomes a tool error carrying the code and status, never the key", async () => {
319
- refuseWith = { status: 403, code: "forbidden" };
320
- const { text, result } = await callTool(
321
- "decide_inbox_application",
322
- SITE_ARGS.decide_inbox_application,
323
- );
324
- expect(seen).toHaveLength(1);
325
- expect(result.isError).toBe(true);
326
- expect(JSON.parse(result.content![0]!.text)).toEqual({
327
- ok: false,
328
- error: { code: "forbidden", status: 403 },
329
- });
330
- expect(text).not.toContain(KEY);
331
- });
332
- });
333
-
334
- // ── AC-6 — the anonymous /mcp is unchanged ───────────────────────────────────
335
-
336
- describe("/mcp — the anonymous surface never reads Authorization (AC-6)", () => {
337
- async function listAt(headers: Record<string, string>) {
338
- const res = await post("/mcp", rpc("tools/list"), headers);
339
- expect(res.status).toBe(200);
340
- const body = (await res.json()) as JsonRpcResponse;
341
- return body.result!.tools!.map((t) => t.name);
342
- }
343
-
344
- it("lists the same anonymous tools with or without an agent key, and no workspace tool", async () => {
345
- const bare = await listAt({});
346
- expect(await listAt(withKey)).toEqual(bare);
347
- expect(bare.sort()).toEqual(listRegisteredTools().map((t) => t.name).sort());
348
- for (const name of TWINS.map((t) => t.tool)) {
349
- expect(bare).not.toContain(name);
350
- }
351
- });
352
-
353
- it("does not serve a workspace tool, even to a caller holding a key", async () => {
354
- const res = await post(
355
- "/mcp",
356
- rpc("tools/call", { name: "get_agent_context", arguments: {} }),
357
- withKey,
358
- );
359
- const text = await res.text();
360
- expect(text).toMatch(/get_agent_context/);
361
- expect(text).toMatch(/not found/i);
362
- expect(seen).toEqual([]);
363
- });
364
- });
@@ -1,68 +0,0 @@
1
- "use client"; // reads and writes the theme cookie and the root's data-theme
2
-
3
- /**
4
- * ColorModeSetting — User settings › General's color mode (#7430, spec 7423
5
- * D4; DESIGN.md §5.12a): Light · Auto · Dark as a `SegmentedControl` of
6
- * three equal cells. The settings page renders it only when the site's
7
- * theme defines more than one mode (operator fold-in 2026-10-01).
8
- *
9
- * The same cookie and the same `data-theme` contract the locale layout's
10
- * flash-prevention script reads: `light` / `dark` pass through; `system`
11
- * (Auto) follows `prefers-color-scheme`.
12
- */
13
-
14
- import { SegmentedControl } from "@working-theory/ui/primitives";
15
- import { useEffect, useState, type ReactElement } from "react";
16
-
17
- type Mode = "light" | "system" | "dark";
18
-
19
- const COOKIE_NAME = process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme";
20
- const ONE_YEAR = 60 * 60 * 24 * 365;
21
-
22
- function readMode(): Mode {
23
- const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
24
- const value = match?.[1];
25
- return value === "light" || value === "dark" ? value : "system";
26
- }
27
-
28
- function applyMode(mode: Mode): void {
29
- document.cookie = `${COOKIE_NAME}=${mode}; path=/; max-age=${ONE_YEAR}; SameSite=Lax`;
30
- const dark =
31
- mode === "dark" ||
32
- (mode === "system" &&
33
- typeof window.matchMedia === "function" &&
34
- window.matchMedia("(prefers-color-scheme: dark)").matches);
35
- document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
36
- }
37
-
38
- export function ColorModeSetting({
39
- labels,
40
- }: {
41
- labels: { title: string; light: string; auto: string; dark: string };
42
- }): ReactElement {
43
- // Auto until mounted — the server cannot read the cookie's effect, and the
44
- // first client render must match it.
45
- const [mode, setMode] = useState<Mode>("system");
46
- useEffect(() => {
47
- setMode(readMode());
48
- }, []);
49
-
50
- return (
51
- <SegmentedControl
52
- aria-label={labels.title}
53
- options={[
54
- { value: "light", label: labels.light },
55
- { value: "system", label: labels.auto },
56
- { value: "dark", label: labels.dark },
57
- ]}
58
- value={mode}
59
- onValueChange={(value) => {
60
- const next = value as Mode;
61
- setMode(next);
62
- applyMode(next);
63
- }}
64
- touchTargetFloor
65
- data-testid="account-color-mode"
66
- />
67
- );
68
- }
@@ -1,55 +0,0 @@
1
- "use client"; // submits through the account's Server Action and shows its outcome
2
-
3
- /**
4
- * DeletionRequest — User settings › Privacy's "Request account deletion"
5
- * (#7430, spec 7423 D10). Files a Support request of kind `deletion`; an
6
- * operator completes it (deletion needs an operator step today, so nothing
7
- * is deleted on click).
8
- */
9
-
10
- import { Button, InlineMessage } from "@working-theory/ui/primitives";
11
- import { useActionState, type ReactElement } from "react";
12
-
13
- import { submitSupportRequest, type SupportRequestState } from "../support-actions";
14
-
15
- export interface DeletionRequestLabels {
16
- action: string;
17
- subject: string;
18
- message: string;
19
- sent: string;
20
- error: string;
21
- }
22
-
23
- export function DeletionRequest({ labels }: { labels: DeletionRequestLabels }): ReactElement {
24
- const [state, formAction, pending] = useActionState<SupportRequestState, FormData>(
25
- submitSupportRequest,
26
- { kind: "idle" },
27
- );
28
-
29
- if (state.kind === "success") {
30
- return (
31
- <InlineMessage variant="success" role="status" className="text-body-sm">
32
- {labels.sent}
33
- </InlineMessage>
34
- );
35
- }
36
-
37
- return (
38
- <form action={formAction} className="flex flex-col items-start gap-2">
39
- <input type="hidden" name="kind" value="deletion" />
40
- <input type="hidden" name="subject" value={labels.subject} />
41
- <input type="hidden" name="message" value={labels.message} />
42
- <Button
43
- type="submit"
44
- variant="secondary"
45
- disabled={pending}
46
- data-testid="account-deletion-request"
47
- >
48
- {labels.action}
49
- </Button>
50
- {state.kind === "error" ? (
51
- <InlineMessage variant="error">{labels.error}</InlineMessage>
52
- ) : null}
53
- </form>
54
- );
55
- }
@@ -1,118 +0,0 @@
1
- "use client"; // edits the session's name through Better Auth's client, then refreshes the route
2
-
3
- /**
4
- * NameForm — User settings › General's name field (#7430, operator fold-in
5
- * 2026-10-01). An accepted applicant arrives with no name (#7418), so the
6
- * member SETS it here, and edits it later. Saved through Better Auth's own
7
- * `updateUser` (its `/update-user` endpoint — no new API), then the route
8
- * refreshes so the frame's avatar and the "Add your name" For-you item
9
- * re-read the session.
10
- *
11
- * DESIGN.md §5.12: the form ends with a hairline and a save row — "No
12
- * unsaved changes" / "Unsaved changes" and a Save that is disabled until
13
- * dirty.
14
- */
15
-
16
- import { Button, Input, InlineMessage, Label, Text } from "@working-theory/ui/primitives";
17
- import { createAuthClient } from "better-auth/react";
18
- import { useRouter } from "next/navigation";
19
- import { useId, useState, type FormEvent, type ReactElement } from "react";
20
-
21
- const authClient = createAuthClient();
22
-
23
- export interface NameFormLabels {
24
- name: string;
25
- placeholder: string;
26
- save: string;
27
- saving: string;
28
- saved: string;
29
- unsaved: string;
30
- clean: string;
31
- required: string;
32
- error: string;
33
- }
34
-
35
- export function NameForm({
36
- initialName,
37
- labels,
38
- }: {
39
- initialName: string;
40
- labels: NameFormLabels;
41
- }): ReactElement {
42
- const router = useRouter();
43
- const inputId = useId();
44
- const [saved, setSaved] = useState(initialName);
45
- const [name, setName] = useState(initialName);
46
- const [state, setState] = useState<"idle" | "saving" | "saved" | "required" | "error">("idle");
47
- const dirty = name !== saved;
48
-
49
- const onSubmit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
50
- event.preventDefault();
51
- const next = name.trim();
52
- if (!next) {
53
- setState("required");
54
- return;
55
- }
56
- setState("saving");
57
- try {
58
- const { error } = await authClient.updateUser({ name: next });
59
- if (error) {
60
- setState("error");
61
- return;
62
- }
63
- } catch {
64
- // A network failure throws rather than answering `error`.
65
- setState("error");
66
- return;
67
- }
68
- setSaved(next);
69
- setName(next);
70
- setState("saved");
71
- router.refresh();
72
- };
73
-
74
- const status =
75
- state === "saved" && !dirty ? labels.saved : dirty ? labels.unsaved : labels.clean;
76
-
77
- return (
78
- <form
79
- onSubmit={(event) => void onSubmit(event)}
80
- className="flex flex-col gap-2"
81
- data-testid="account-name-form"
82
- >
83
- <Label htmlFor={inputId}>{labels.name}</Label>
84
- <Input
85
- id={inputId}
86
- name="name"
87
- autoComplete="name"
88
- value={name}
89
- placeholder={labels.placeholder}
90
- onChange={(event) => {
91
- setName(event.target.value);
92
- if (state !== "saving") setState("idle");
93
- }}
94
- className="max-w-md"
95
- data-testid="account-name-input"
96
- />
97
- {state === "required" || state === "error" ? (
98
- <InlineMessage variant="error">
99
- {state === "required" ? labels.required : labels.error}
100
- </InlineMessage>
101
- ) : null}
102
- <div className="mt-2 flex items-center justify-between gap-4 border-t border-(--color-border) pt-4">
103
- <div aria-live="polite" data-testid="account-name-status">
104
- <Text as="p" variant="caption" tone="muted">
105
- {status}
106
- </Text>
107
- </div>
108
- <Button
109
- type="submit"
110
- disabled={!dirty || state === "saving"}
111
- data-testid="account-name-save"
112
- >
113
- {state === "saving" ? labels.saving : labels.save}
114
- </Button>
115
- </div>
116
- </form>
117
- );
118
- }