History

init effect

Rhys Sullivan committed 8 months ago

Commit dd5e8041 parent 93bf95a76 files · +8759 −287Browse files

Unified · Split

76 files
  1. experiemental/approval-client.ts +143 −157
  2. experiemental/mcp-memory-approvals.ts +155 −85
  3. v2/.gitignore +1 −0
  4. v2/.vscode/settings.json +4 −0
  5. v2/AGENTS.md +1 −0
  6. v2/CLAUDE.md +17 −0
  7. v2/PLAN.md +369 −0
  8. v2/apps/convex/.gitignore +2 −0
  9. v2/apps/convex/README.md +4 −0
  10. v2/apps/convex/convex/README.md +90 −0
  11. v2/apps/convex/convex/_generated/api.d.ts +51 −0
  12. v2/apps/convex/convex/_generated/api.js +23 −0
  13. v2/apps/convex/convex/_generated/dataModel.d.ts +60 −0
  14. v2/apps/convex/convex/_generated/server.d.ts +143 −0
  15. v2/apps/convex/convex/_generated/server.js +93 −0
  16. v2/apps/convex/convex/http.ts +13 −0
  17. v2/apps/convex/convex/mcp.ts +9 −0
  18. v2/apps/convex/convex/schema.ts +3 −0
  19. v2/apps/convex/convex/tsconfig.json +25 −0
  20. v2/apps/convex/package.json +8 −1
  21. v2/apps/convex/tsconfig.json +1 −1
  22. v2/apps/pm/README.md +5 −0
  23. v2/apps/pm/package.json +5 −0
  24. v2/apps/pm/src/main.ts +31 −0
  25. v2/apps/pm/tsconfig.json +3 −0
  26. v2/bun.lock +699 −0
  27. v2/package.json +6 −1
  28. v2/packages/confect-core/README.md +0 −5
  29. v2/packages/confect-core/package.json +0 −14
  30. v2/packages/confect-core/src/index.ts +0 −1
  31. v2/packages/confect-core/src/schema.ts +0 −6
  32. v2/packages/confect-core/tsconfig.json +0 −4
  33. v2/packages/confect/package.json +35 −0
  34. v2/packages/confect/src/client.ts +160 −0
  35. v2/packages/confect/src/ctx.ts +683 −0
  36. v2/packages/confect/src/index.ts +43 −0
  37. v2/packages/confect/src/pagination.ts +19 −0
  38. v2/packages/confect/src/rpc/client.test.ts +776 −0
  39. v2/packages/confect/src/rpc/client.ts +1105 −0
  40. v2/packages/confect/src/rpc/go-to-definition.test.ts +87 −0
  41. v2/packages/confect/src/rpc/index.ts +49 −0
  42. v2/packages/confect/src/rpc/server.test.ts +1239 −0
  43. v2/packages/confect/src/rpc/server.ts +671 −0
  44. v2/packages/confect/src/rpc/telemetry.test.ts +112 −0
  45. v2/packages/confect/src/rpc/telemetry.ts +134 −0
  46. v2/packages/confect/src/schema.test.ts +235 −0
  47. v2/packages/confect/src/schema.ts +400 −0
  48. v2/packages/confect/src/testing.ts +129 −0
  49. v2/packages/confect/src/validators.test.ts +247 −0
  50. v2/packages/confect/src/validators.ts +217 −0
  51. v2/packages/confect/tsconfig.json +8 −0
  52. v2/packages/confect/vitest.config.mts +11 −0
  53. v2/packages/mcp-gateway/README.md +6 −1
  54. v2/packages/mcp-gateway/package.json +7 −0
  55. v2/packages/mcp-gateway/src/index.ts +1 −0
  56. v2/packages/mcp-gateway/src/server.ts +70 −0
  57. v2/packages/schema/README.md +1 −1
  58. v2/packages/source-manager/package.json +8 −0
  59. v2/packages/source-manager/tsconfig.json +3 −0
  60. v2/packages/state-contracts/README.md +6 −2
  61. v2/packages/state-contracts/package.json +8 −1
  62. v2/packages/state-contracts/src/index.ts +2 −0
  63. v2/packages/state-contracts/src/profiles.ts +7 −0
  64. v2/packages/state-contracts/src/tool-artifacts.ts +12 −0
  65. v2/packages/state-convex/README.md +7 −2
  66. v2/packages/state-convex/package.json +10 −1
  67. v2/packages/state-convex/src/convex-ids.ts +25 −0
  68. v2/packages/state-convex/src/index.ts +3 −0
  69. v2/packages/state-convex/src/profiles.ts +33 −0
  70. v2/packages/state-convex/src/schema.ts +56 −0
  71. v2/packages/state-local-file/README.md +6 −2
  72. v2/packages/state-local-file/package.json +9 −1
  73. v2/packages/state-local-file/src/index.ts +7 −0
  74. v2/packages/state-local-file/src/tool-artifact-store.ts +138 −0
  75. v2/packages/state-local-file/tsconfig.json +3 −0
  76. v2/tsconfig.json +7 −1
experiemental/approval-client.ts+143−157
@@ -1,65 +1,69 @@
1+import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12 import path from "node:path";
23
34 type ApprovalStatus = "pending" | "approved" | "denied";
45
56 interface ApprovalItem {
67 id: string;
7− title: string;
8− details: string;
9− status: ApprovalStatus;
8+ action: string;
9+ justification: string;
1010 createdAt: number;
11+ status: ApprovalStatus;
12+ decidedAt: number | null;
1113 }
1214
13−function parseArgs(argv: string[]): { session: string; inline: boolean; tmuxSession: string; agentCommand: string } {
14− const session = argv[0] && !argv[0].startsWith("-") ? argv[0] : "claude";
15− const inline = argv.includes("--inline");
15+interface PersistedState {
16+ approvals: ApprovalItem[];
17+}
18+
19+interface ParsedArgs {
20+ session: string;
21+ inline: boolean;
22+ tmuxSession: string;
23+ agentCommand: string;
24+ approvalId: string | null;
25+}
1626
17− const tmuxSessionFlagIndex = argv.indexOf("--tmux-session");
18− const tmuxSession = tmuxSessionFlagIndex >= 0 && argv[tmuxSessionFlagIndex + 1]
19− ? String(argv[tmuxSessionFlagIndex + 1])
20− : "executor-approvals";
27+const DEFAULT_MCP_CONFIG_PATH = path.resolve(import.meta.dir, "claude-mcp-demo.json");
28+const HOME_DIR = process.env.HOME ?? ".";
29+const STATE_PATH = path.join(HOME_DIR, ".executor-lite-approvals", "mcp-state.json");
2130
22− const agentCommandFlagIndex = argv.indexOf("--agent-cmd");
23− const agentCommand = agentCommandFlagIndex >= 0 && argv[agentCommandFlagIndex + 1]
24− ? String(argv[agentCommandFlagIndex + 1])
25− : session;
31+mkdirSync(path.dirname(STATE_PATH), { recursive: true });
2632
27− return { session, inline, tmuxSession, agentCommand };
33+function shellQuote(value: string): string {
34+ return `'${value.replace(/'/g, "'\\''")}'`;
2835 }
2936
30−function hasTmuxBinary(): boolean {
31− const result = Bun.spawnSync({ cmd: ["tmux", "-V"] });
32− return result.exitCode === 0;
37+function getFlag(argv: string[], name: string): string | null {
38+ const index = argv.indexOf(name);
39+ if (index < 0) return null;
40+ const value = argv[index + 1];
41+ return typeof value === "string" ? value : null;
3342 }
3443
35−function getTmuxClients(): string[] {
36− const result = Bun.spawnSync({ cmd: ["tmux", "list-clients", "-F", "#{client_name}"] });
37− if (result.exitCode !== 0) return [];
44+function defaultAgentCommand(session: string): string {
45+ if (session === "claude") {
46+ const allowedTools = "mcp__memory-approval-demo__dangerous_action";
47+ return `claude --strict-mcp-config --mcp-config ${shellQuote(DEFAULT_MCP_CONFIG_PATH)} --allowedTools ${shellQuote(allowedTools)} --permission-mode bypassPermissions`;
48+ }
3849
39− return Buffer.from(result.stdout)
40− .toString("utf8")
41− .split("\n")
42− .map((line) => line.trim())
43− .filter((line) => line.length > 0 && line !== "no current client");
50+ return session;
4451 }
4552
46−async function launchTmuxPopup(session: string, clientTarget: string): Promise<void> {
47− const scriptPath = path.resolve(import.meta.dir, "approval-client.ts");
48− const command = `bun run ${scriptPath} ${session} --inline`;
49− Bun.spawnSync({
50− cmd: ["tmux", "display-popup", "-t", clientTarget, "-E", "-w", "82%", "-h", "70%", command],
51− stdin: "inherit",
52− stdout: "inherit",
53− stderr: "inherit",
54− });
55−}
53+function parseArgs(argv: string[]): ParsedArgs {
54+ const positionalSession = argv[0] && !argv[0].startsWith("-") ? argv[0] : "claude";
55+ const session = getFlag(argv, "--session") ?? positionalSession;
56+ const inline = argv.includes("--inline");
5657
57−function shellQuote(value: string): string {
58− return `'${value.replace(/'/g, "'\\''")}'`;
58+ const tmuxSession = getFlag(argv, "--tmux-session") ?? "executor-approvals";
59+ const agentCommand = getFlag(argv, "--agent-cmd") ?? defaultAgentCommand(session);
60+ const approvalId = getFlag(argv, "--approval-id");
61+
62+ return { session, inline, tmuxSession, agentCommand, approvalId };
5963 }
6064
61−function tmuxSessionExists(tmuxSession: string): boolean {
62− const result = Bun.spawnSync({ cmd: ["tmux", "has-session", "-t", tmuxSession] });
65+function hasTmuxBinary(): boolean {
66+ const result = Bun.spawnSync({ cmd: ["tmux", "-V"] });
6367 return result.exitCode === 0;
6468 }
6569
@@ -73,32 +77,24 @@ function runTmuxCommand(cmd: string[]): boolean { · 7 unmodified lines
7377 return result.exitCode === 0;
7478 }
7579
80+function tmuxSessionExists(tmuxSession: string): boolean {
81+ const result = Bun.spawnSync({ cmd: ["tmux", "has-session", "-t", tmuxSession] });
82+ return result.exitCode === 0;
83+}
84+
7685 function ensureSessionUiOptions(tmuxSession: string): void {
7786 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "mouse", "on"]);
7887 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "pane-border-status", "top"]);
7988 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "pane-border-format", "#{pane_index}: #{pane_title}"]);
8089 }
8190
82−function setPaneTitles(windowTarget: string): void {
83− runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.0`, "-T", "Claude"]);
84− runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.1`, "-T", "Approvals"]);
85−}
86−
87−async function launchManagedTmuxWorkspace(session: string, tmuxSession: string, agentCommand: string): Promise<boolean> {
88− const scriptPath = path.resolve(import.meta.dir, "approval-client.ts");
89− const approvalCommand = `bun run ${shellQuote(scriptPath)} ${shellQuote(session)} --inline`;
90−
91+async function launchManagedTmuxWorkspace(tmuxSession: string, agentCommand: string): Promise<boolean> {
9192 if (!tmuxSessionExists(tmuxSession)) {
9293 const created = runTmuxCommand(["tmux", "new-session", "-d", "-s", tmuxSession, agentCommand]);
9394 if (!created) return false;
9495
9596 ensureSessionUiOptions(tmuxSession);
96−
97− const split = runTmuxCommand(["tmux", "split-window", "-t", `${tmuxSession}:0`, "-v", "-p", "30", approvalCommand]);
98− if (!split) return false;
99−
100− setPaneTitles(`${tmuxSession}:0`);
101− runTmuxCommand(["tmux", "select-pane", "-t", `${tmuxSession}:0.0`]);
97+ runTmuxCommand(["tmux", "select-pane", "-t", `${tmuxSession}:0.0`, "-T", "Claude"]);
10298 return runTmuxCommand(["tmux", "attach-session", "-t", tmuxSession]);
10399 }
104100
@@ -109,84 +105,96 @@ async function launchManagedTmuxWorkspace(session: string, tmuxSession: string, · 4 unmodified lines
109105 const createdWindow = runTmuxCommand(["tmux", "new-window", "-t", `${tmuxSession}:`, "-n", windowName, agentCommand]);
110106 if (!createdWindow) return false;
111107
112− const splitWindow = runTmuxCommand(["tmux", "split-window", "-t", windowTarget, "-v", "-p", "30", approvalCommand]);
113− if (!splitWindow) return false;
114−
115− setPaneTitles(windowTarget);
116− runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.0`]);
108+ runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.0`, "-T", "Claude"]);
109+ runTmuxCommand(["tmux", "select-window", "-t", windowTarget]);
117110 return runTmuxCommand(["tmux", "attach-session", "-t", tmuxSession]);
118111 }
119112
120−function makeApproval(session: string, index: number): ApprovalItem {
121− const actions = [
122− "Write DNS record",
123− "Delete production file",
124− "Rotate API token",
125− "Deploy migration",
126− "Run destructive cleanup",
127− ];
128−
129− const title = actions[index % actions.length] ?? "Tool action";
130− const id = `appr_${crypto.randomUUID().slice(0, 8)}`;
131−
132− return {
133− id,
134− title,
135− details: `Session: ${session} -> tools.example.action()`,
136− status: "pending",
137− createdAt: Date.now(),
138− };
113+function loadState(): PersistedState {
114+ if (!existsSync(STATE_PATH)) return { approvals: [] };
115+
116+ try {
117+ const text = readFileSync(STATE_PATH, "utf8");
118+ const parsed = JSON.parse(text) as Partial<PersistedState>;
119+ if (!parsed || !Array.isArray(parsed.approvals)) {
120+ return { approvals: [] };
121+ }
122+
123+ return {
124+ approvals: parsed.approvals.filter((entry): entry is ApprovalItem => {
125+ return (
126+ Boolean(entry) &&
127+ typeof entry.id === "string" &&
128+ typeof entry.action === "string" &&
129+ typeof entry.justification === "string" &&
130+ typeof entry.createdAt === "number" &&
131+ (entry.status === "pending" || entry.status === "approved" || entry.status === "denied")
132+ );
133+ }),
134+ };
135+ } catch {
136+ return { approvals: [] };
137+ }
139138 }
140139
141−function statusMarker(status: ApprovalStatus): string {
142− if (status === "approved") return "[approved]";
143− if (status === "denied") return "[denied]";
144− return "[pending]";
140+function saveState(state: PersistedState): void {
141+ const tempPath = `${STATE_PATH}.tmp`;
142+ writeFileSync(tempPath, JSON.stringify(state, null, 2));
143+ renameSync(tempPath, STATE_PATH);
145144 }
146145
147−function clearAndRender(session: string, approvals: ApprovalItem[], selected: number): void {
146+function findApproval(state: PersistedState, approvalId: string | null): ApprovalItem | null {
147+ if (approvalId) {
148+ return state.approvals.find((item) => item.id === approvalId) ?? null;
149+ }
150+
151+ return state.approvals.find((item) => item.status === "pending") ?? null;
152+}
153+
154+function decideApproval(approvalId: string, status: ApprovalStatus): ApprovalItem | null {
155+ const state = loadState();
156+ const target = state.approvals.find((item) => item.id === approvalId);
157+ if (!target || target.status !== "pending") return null;
158+
159+ target.status = status;
160+ target.decidedAt = Date.now();
161+ saveState(state);
162+ return target;
163+}
164+
165+function renderInline(session: string, current: ApprovalItem | null): void {
148166 process.stdout.write("\x1Bc");
149− process.stdout.write("Approval Popup Demo\n");
167+ process.stdout.write("Approval Pane (MCP-driven)\n");
150168 process.stdout.write(`Session: ${session}\n`);
151− process.stdout.write("Controls: j/k move a approve d deny n new request q quit\n");
152− process.stdout.write("tmux tip: Ctrl+b then o to switch panes (mouse is enabled).\n");
153− process.stdout.write("-".repeat(72) + "\n");
169+ process.stdout.write("Controls: a approve d deny q quit\n");
170+ process.stdout.write("This pane only appears when an MCP approval is pending.\n");
171+ process.stdout.write("-".repeat(84) + "\n");
154172
155− if (approvals.length === 0) {
156− process.stdout.write("No approvals. Press n to enqueue one.\n");
173+ if (!current) {
174+ process.stdout.write("No pending approval. Closing...\n");
157175 return;
158176 }
159177
160− approvals.forEach((item, idx) => {
161− const pointer = idx === selected ? ">" : " ";
162− const time = new Date(item.createdAt).toLocaleTimeString();
163− process.stdout.write(`${pointer} ${statusMarker(item.status)} ${item.id} ${item.title} (${time})\n`);
164− process.stdout.write(` ${item.details}\n`);
165− });
178+ const created = new Date(current.createdAt).toLocaleTimeString();
179+ process.stdout.write(`[pending] ${current.id}\n`);
180+ process.stdout.write(`action: ${current.action}\n`);
181+ process.stdout.write(`created: ${created}\n`);
182+ if (current.justification.trim().length > 0) {
183+ process.stdout.write(`justification: ${current.justification}\n`);
184+ }
166185 }
167186
168−async function runInlineUi(session: string): Promise<number> {
169− const approvals: ApprovalItem[] = [makeApproval(session, 0)];
170− let selected = 0;
171− let sequence = 1;
172−
173− const enqueueTimer = setInterval(() => {
174− approvals.unshift(makeApproval(session, sequence));
175− sequence += 1;
176− selected = 0;
177− clearAndRender(session, approvals, selected);
178− }, 12000);
187+async function runInlineUi(session: string, approvalId: string | null): Promise<number> {
188+ let current = findApproval(loadState(), approvalId);
189+ renderInline(session, current);
179190
180191 const cleanup = (): void => {
181− clearInterval(enqueueTimer);
182192 if (process.stdin.isTTY) {
183193 process.stdin.setRawMode(false);
184194 }
185195 process.stdin.pause();
186196 };
187197
188− clearAndRender(session, approvals, selected);
189−
190198 return await new Promise<number>((resolve) => {
191199 if (process.stdin.isTTY) {
192200 process.stdin.setRawMode(true);
@@ -200,59 +208,46 @@ async function runInlineUi(session: string): Promise<number> { · 7 unmodified lines
200208 resolve(code);
201209 };
202210
211+ const poll = setInterval(() => {
212+ current = findApproval(loadState(), approvalId);
213+ if (!current) {
214+ clearInterval(poll);
215+ renderInline(session, null);
216+ setTimeout(() => finish(0), 250);
217+ return;
218+ }
219+ renderInline(session, current);
220+ }, 500);
221+
203222 process.stdin.on("data", (chunk) => {
204223 const key = typeof chunk === "string" ? chunk : chunk.toString("utf8");
205224
206225 if (key === "\u0003" || key === "q") {
226+ clearInterval(poll);
207227 finish(0);
208228 return;
209229 }
210230
211− if (key === "j" || key === "\u001B[B") {
212− if (approvals.length > 0) {
213− selected = Math.min(approvals.length - 1, selected + 1);
214− }
215− clearAndRender(session, approvals, selected);
216− return;
217− }
218−
219− if (key === "k" || key === "\u001B[A") {
220− if (approvals.length > 0) {
221− selected = Math.max(0, selected - 1);
222− }
223− clearAndRender(session, approvals, selected);
224− return;
225− }
226−
227− if (key === "n") {
228− approvals.unshift(makeApproval(session, sequence));
229− sequence += 1;
230− selected = 0;
231− clearAndRender(session, approvals, selected);
232− return;
233− }
234−
235− const current = approvals[selected];
236− if (!current || current.status !== "pending") {
237− return;
238− }
231+ if (!current) return;
239232
240233 if (key === "a") {
241− current.status = "approved";
242− clearAndRender(session, approvals, selected);
234+ decideApproval(current.id, "approved");
235+ clearInterval(poll);
236+ finish(0);
243237 return;
244238 }
245239
246240 if (key === "d") {
247− current.status = "denied";
248− clearAndRender(session, approvals, selected);
241+ decideApproval(current.id, "denied");
242+ clearInterval(poll);
243+ finish(0);
249244 }
250245 });
251246 });
252247 }
253248
254249 async function main(): Promise<number> {
255− const { session, inline, tmuxSession, agentCommand } = parseArgs(process.argv.slice(2));
250+ const { session, inline, tmuxSession, agentCommand, approvalId } = parseArgs(process.argv.slice(2));
256251
257252 if (!inline) {
258253 if (!hasTmuxBinary()) {
@@ -261,25 +256,16 @@ async function main(): Promise<number> { · 2 unmodified lines
261256 return 1;
262257 }
263258
264− const clients = getTmuxClients();
265− const target = clients[0];
266− if (target) {
267− await launchTmuxPopup(session, target);
268− return 0;
269− }
270−
271− console.log(`No active tmux clients. Launching '${agentCommand}' with approvals in session '${tmuxSession}'...`);
272− const started = await launchManagedTmuxWorkspace(session, tmuxSession, agentCommand);
273− if (started) {
274− return 0;
275− }
259+ console.log(`Launching '${agentCommand}' in tmux session '${tmuxSession}'...`);
260+ const started = await launchManagedTmuxWorkspace(tmuxSession, agentCommand);
261+ if (started) return 0;
276262
277263 console.error("Unable to launch managed tmux workspace.");
278− console.error("You can still run the inline demo with --inline.");
264+ console.error("You can still run the inline pane with --inline.");
279265 return 1;
280266 }
281267
282− return await runInlineUi(session);
268+ return await runInlineUi(session, approvalId);
283269 }
284270
285271 const code = await main();
experiemental/mcp-memory-approvals.ts+155−85
@@ -1,30 +1,149 @@
1+import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2+import path from "node:path";
3+
14 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
25 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
36 import { z } from "zod";
47
5−type ApprovalDecision = "approved" | "denied";
8+type ApprovalStatus = "pending" | "approved" | "denied";
69
710 interface ApprovalRecord {
811 id: string;
912 action: string;
1013 justification: string;
1114 createdAt: number;
12− decision: ApprovalDecision;
13− decidedAt: number;
15+ status: ApprovalStatus;
16+ decidedAt: number | null;
17+}
18+
19+interface PersistedState {
20+ approvals: ApprovalRecord[];
21+}
22+
23+const HOME_DIR = process.env.HOME ?? ".";
24+const DATA_DIR = path.join(HOME_DIR, ".executor-lite-approvals");
25+const STATE_PATH = path.join(DATA_DIR, "mcp-state.json");
26+const APPROVAL_UI_SCRIPT = path.resolve(import.meta.dir, "approval-client.ts");
27+
28+mkdirSync(DATA_DIR, { recursive: true });
29+
30+const state: PersistedState = loadState();
31+
32+function shellQuote(value: string): string {
33+ return `'${value.replace(/'/g, "'\\''")}'`;
34+}
35+
36+function loadState(): PersistedState {
37+ if (!existsSync(STATE_PATH)) {
38+ return { approvals: [] };
39+ }
40+
41+ try {
42+ const text = readFileSync(STATE_PATH, "utf8");
43+ const parsed = JSON.parse(text) as Partial<PersistedState>;
44+ if (!parsed || !Array.isArray(parsed.approvals)) {
45+ return { approvals: [] };
46+ }
47+
48+ return {
49+ approvals: parsed.approvals.filter((entry): entry is ApprovalRecord => {
50+ return (
51+ Boolean(entry) &&
52+ typeof entry.id === "string" &&
53+ typeof entry.action === "string" &&
54+ typeof entry.justification === "string" &&
55+ typeof entry.createdAt === "number" &&
56+ (entry.status === "pending" || entry.status === "approved" || entry.status === "denied")
57+ );
58+ }),
59+ };
60+ } catch {
61+ return { approvals: [] };
62+ }
1463 }
1564
16−const approvals: ApprovalRecord[] = [];
65+function refreshStateFromDisk(): void {
66+ const disk = loadState();
67+ state.approvals = disk.approvals;
68+}
69+
70+function saveState(): void {
71+ const tempPath = `${STATE_PATH}.tmp`;
72+ writeFileSync(tempPath, JSON.stringify(state, null, 2));
73+ renameSync(tempPath, STATE_PATH);
74+}
75+
76+function recordPendingApproval(action: string, justification: string): ApprovalRecord {
77+ const entry: ApprovalRecord = {
78+ id: `appr_${crypto.randomUUID().slice(0, 8)}`,
79+ action,
80+ justification,
81+ createdAt: Date.now(),
82+ status: "pending",
83+ decidedAt: null,
84+ };
85+
86+ state.approvals = [entry];
87+ saveState();
88+ return entry;
89+}
90+
91+function removeApproval(approvalId: string): void {
92+ state.approvals = state.approvals.filter((item) => item.id !== approvalId);
93+ saveState();
94+}
95+
96+function markDeniedIfStillPending(approvalId: string): void {
97+ const item = state.approvals.find((entry) => entry.id === approvalId);
98+ if (!item || item.status !== "pending") return;
99+ item.status = "denied";
100+ item.decidedAt = Date.now();
101+ saveState();
102+}
103+
104+function maybeOpenApprovalPane(approvalId: string): void {
105+ if (!process.env.TMUX) return;
106+
107+ const command = `bun run ${shellQuote(APPROVAL_UI_SCRIPT)} --inline --session claude --approval-id ${shellQuote(approvalId)}`;
108+
109+ const split = Bun.spawnSync({
110+ cmd: ["tmux", "split-window", "-v", "-p", "30", command],
111+ stdin: "inherit",
112+ stdout: "ignore",
113+ stderr: "ignore",
114+ });
115+
116+ if (split.exitCode !== 0) return;
117+
118+ Bun.spawnSync({
119+ cmd: ["tmux", "select-pane", "-T", "Approvals"],
120+ stdin: "inherit",
121+ stdout: "ignore",
122+ stderr: "ignore",
123+ });
124+}
125+
126+async function waitForDecision(approvalId: string, timeoutMs: number): Promise<ApprovalStatus | "timeout"> {
127+ const deadline = Date.now() + timeoutMs;
128+
129+ while (Date.now() < deadline) {
130+ refreshStateFromDisk();
131+ const current = state.approvals.find((entry) => entry.id === approvalId);
132+ if (current && current.status !== "pending") {
133+ return current.status;
134+ }
135+
136+ await Bun.sleep(350);
137+ }
138+
139+ return "timeout";
140+}
17141
18142 const mcp = new McpServer(
19143 { name: "memory-approval-demo", version: "0.0.1" },
20144 {
21145 capabilities: {
22146 tools: {},
23− experimental: {
24− elicitation: {
25− form: {},
26− },
27− },
28147 },
29148 },
30149 );
@@ -32,102 +151,53 @@ const mcp = new McpServer( · 1 unmodified line
32151 mcp.registerTool(
33152 "dangerous_action",
34153 {
35− description: "Runs a fake dangerous action after explicit approval.",
154+ description: "Queues a dangerous action and waits for sidecar approval from shared state.",
36155 inputSchema: {
37156 action: z.string().min(1),
38157 justification: z.string().optional(),
158+ approvalTimeoutMs: z.number().int().min(1_000).max(600_000).optional(),
39159 },
40160 },
41− async ({ action, justification }) => {
42− const approvalId = `appr_${crypto.randomUUID().slice(0, 8)}`;
43−
44− let decision: ApprovalDecision = "denied";
45−
46− try {
47− const response = await mcp.server.elicitInput({
48− mode: "form",
49− message: `Approve dangerous action '${action}'?`,
50− requestedSchema: {
51− type: "object",
52− properties: {
53− decision: {
54− type: "string",
55− oneOf: [
56− { const: "approved", title: "Approve" },
57− { const: "denied", title: "Deny" },
58− ],
59− },
60− },
61− required: ["decision"],
62− },
63− });
64−
65− if (response.action === "accept" && response.content?.decision === "approved") {
66− decision = "approved";
67− }
68− } catch (error) {
69− console.error("elicitation failed", error);
70− decision = "denied";
71− }
161+ async ({ action, justification, approvalTimeoutMs }) => {
162+ refreshStateFromDisk();
163+ const pending = recordPendingApproval(action, justification ?? "");
164+ maybeOpenApprovalPane(pending.id);
165+
166+ const timeoutMs = approvalTimeoutMs ?? 120_000;
167+ const decision = await waitForDecision(pending.id, timeoutMs);
168+
169+ refreshStateFromDisk();
170+ const current = state.approvals.find((item) => item.id === pending.id);
72171
73− const now = Date.now();
74− approvals.unshift({
75− id: approvalId,
76− action,
77− justification: justification ?? "",
78− createdAt: now,
79− decision,
80− decidedAt: now,
81− });
82−
83− if (decision === "denied") {
172+ if (decision === "approved" && current?.status === "approved") {
173+ removeApproval(pending.id);
84174 return {
85− content: [{ type: "text", text: `Denied '${action}'.` }],
175+ content: [{ type: "text", text: `Approved and executed '${action}'.` }],
86176 structuredContent: {
87− approvalId,
88− status: "denied",
177+ approvalId: pending.id,
178+ status: "approved",
89179 action,
180+ executed: true,
90181 },
91182 };
92183 }
93184
185+ if (decision === "timeout") {
186+ markDeniedIfStillPending(pending.id);
187+ }
188+
189+ removeApproval(pending.id);
190+
94191 return {
95− content: [{ type: "text", text: `Approved and executed '${action}'.` }],
192+ content: [{ type: "text", text: `Denied '${action}' (${decision === "timeout" ? "approval timed out" : "approval denied"}).` }],
96193 structuredContent: {
97− approvalId,
98− status: "approved",
194+ approvalId: pending.id,
195+ status: "denied",
99196 action,
100− executed: true,
101197 },
102198 };
103199 },
104200 );
105201
106−mcp.registerTool(
107− "approval_history",
108− {
109− description: "Lists in-memory approval decisions for this MCP server process.",
110− inputSchema: {
111− limit: z.number().int().min(1).max(100).optional(),
112− },
113− },
114− async ({ limit }) => {
115− const max = limit ?? 20;
116− const items = approvals.slice(0, max).map((entry) => ({
117− id: entry.id,
118− action: entry.action,
119− justification: entry.justification,
120− decision: entry.decision,
121− createdAt: entry.createdAt,
122− decidedAt: entry.decidedAt,
123− }));
124−
125− return {
126− content: [{ type: "text", text: `Returning ${items.length} approval entries.` }],
127− structuredContent: { items },
128− };
129− },
130−);
131−
132202 const transport = new StdioServerTransport();
133203 await mcp.connect(transport);
v2/.gitignore+1−0
@@ -0,0 +1 @@
1+.reference/
v2/.vscode/settings.json+4−0
@@ -0,0 +1,4 @@
1+{
2+ "typescript.tsdk": "./node_modules/typescript/lib",
3+ "typescript.enablePromptUseWorkspaceTsdk": true
4+}
v2/AGENTS.md+1−0
@@ -0,0 +1 @@
1+CLAUDE.md
v2/CLAUDE.md+17−0
@@ -0,0 +1,17 @@
1+# v2 Agent Notes
2+
3+<!-- effect-solutions:start -->
4+
5+## Effect Best Practices
6+
7+**IMPORTANT:** Always consult effect-solutions before writing Effect code.
8+
9+1. Run `effect-solutions list` to see available guides
10+2. Run `effect-solutions show <topic>...` for relevant patterns (supports multiple topics)
11+3. Search `.reference/effect/` for real implementations (run `effect-solutions setup` first)
12+
13+Topics: quick-start, project-setup, tsconfig, basics, services-and-layers, data-modeling, error-handling, config, testing, cli.
14+
15+Never guess at Effect patterns - check the guide first.
16+
17+<!-- effect-solutions:end -->
v2/PLAN.md+369−0
@@ -0,0 +1,369 @@
1+# Executor v2 Plan
2+
3+Reference implementations and inspiration for this plan:
4+ `../AnswerOverflow`
5+ `../quickhub`
6+ `../create-epoch-app`
7+
8+## 1) Why v2 exists
9+
10+Executor today is optimized for enterprise/team correctness (workspaces, org flows, cloud control), but has too much friction for per-user adoption.
11+
12+v2 changes the default:
13+
14+- **First-run must be local and immediate** (`npx executor <agent>` works fast).
15+- **No database dependency** in local-lite mode.
16+- **Cloud/self-host are upgrade targets**, not prerequisites.
17+- **One architecture for all modes** via adapters and shared contracts.
18+
19+---
20+
21+## 2) Product principles
22+
23+1. **Local-first**
24+ - User gets value without login, org setup, or web onboarding.
25+2. **One mental model**
26+ - Same commands, same execution semantics across local and remote targets.
27+3. **Schema-first**
28+ - Effect Schema is the single source of truth for persisted/domain models.
29+4. **Execute-first tooling**
30+ - Top-level model tool surface is minimal; code execution is primary.
31+5. **Progressive complexity**
32+ - Advanced controls exist but are hidden until needed.
33+
34+---
35+
36+## 3) Product modes and targets
37+
38+### Modes
39+
40+- **local-lite**: PM daemon + in-memory hot state + file persistence. No DB.
41+- **remote**: Convex-backed target (applies to both cloud and self-hosted).
42+
43+### Key simplification
44+
45+- `cloud` and `self-hosted` are not separate architecture branches.
46+- They are both **remote Convex targets**; only URL/auth/deployment ownership differs.
47+
48+---
49+
50+## 4) Control + execution model
51+
52+### Control role (conceptual)
53+
54+Control includes source registry, policies, approvals/input workflow, identity/link state, and sync metadata.
55+
56+- In local-lite: handled by `apps/pm`.
57+- In remote: handled by Convex + web stack.
58+
59+### Execution role
60+
61+Execution remains the runtime engine that runs generated TypeScript and calls `tools.*` via proxy invocation.
62+
63+---
64+
65+## 5) Monorepo shape (Executor-only)
66+
67+```text
68+v2/
69+ apps/
70+ cli/
71+ pm/
72+ web/
73+ convex/
74+
75+ packages/
76+ confect/ # imported from quickhub; Effect<->Convex schema bridge
77+ schema/ # canonical Effect schemas
78+ persistence-ports/
79+ persistence-local/
80+ persistence-convex/
81+ domain/
82+ approvals/
83+ source-manager/
84+ engine/
85+ mcp-gateway/
86+ oauth/
87+ secrets/
88+ sync/
89+ rpc/
90+ sdk/
91+ ai-sdk-adapter/
92+```
93+
94+---
95+
96+## 6) Dependency order (high-level)
97+
98+```text
99+confect -> schema -> persistence-ports
100+
101+persistence-ports -> persistence-local
102+persistence-ports -> persistence-convex
103+
104+schema + persistence-ports + oauth + secrets -> domain
105+
106+domain -> approvals/source-manager/engine
107+approvals/source-manager/engine -> mcp-gateway
108+
109+domain/features -> rpc -> sdk -> ai-sdk-adapter
110+
111+apps/pm -> mcp-gateway + domain + persistence-local + rpc
112+apps/convex -> mcp-gateway + domain + persistence-convex + rpc
113+apps/cli/web -> sdk
114+```
115+
116+Rules:
117+- `schema` has no dependency on feature/runtime packages.
118+- `domain` depends on interfaces, not concrete state adapters.
119+- apps are leaves; packages do not import apps.
120+
121+---
122+
123+## 7) Local-lite PM architecture
124+
125+`apps/pm` is the local daemon and source of hot truth.
126+
127+Responsibilities:
128+- MCP endpoint surface
129+- source registration and status lifecycle
130+- approvals/input broker
131+- task orchestration and runtime execution coordination
132+- persistence (snapshot + event log)
133+
134+### Persistence model
135+
136+- Hot state: in-memory
137+- Durable: file-based
138+ - `snapshot.json`
139+ - `events.jsonl` (WAL)
140+ - `artifacts/` (compiled tool manifests)
141+ - secrets in secure store / dedicated protected file
142+
143+Crash/restart behavior:
144+- Rehydrate from snapshot + events.
145+- Rebuild derived indexes in memory.
146+- Mark previously running tasks terminally with restart reason.
147+
148+---
149+
150+## 8) Data model strategy
151+
152+Effect Schema is canonical (`packages/schema`).
153+
154+Current scaffold includes:
155+- domain IDs (`src/ids.ts`)
156+- enums/common primitives
157+- model schemas:
158+ - profile, workspace
159+ - source, tool artifact
160+ - credential ref, oauth token
161+ - policy, approval
162+ - task run, sync state
163+ - event envelope
164+- snapshot/event schemas
165+
166+### Convex ID compatibility
167+
168+IDs are now intentionally split by layer:
169+- **Domain IDs** live in `packages/schema` as portable branded IDs.
170+- **Convex table IDs** live in `packages/persistence-convex/src/convex-ids.ts` via `Id("table")` from `@executor-v2/confect`.
171+
172+This keeps canonical schema backend-agnostic while preserving strict table-typed IDs inside the Convex adapter boundary.
173+
174+---
175+
176+## 9) Tooling and MCP surface
177+
178+### Top-level model-facing tools
179+
180+Keep top-level surface minimal:
181+- `execute` (primary)
182+- optional diagnostics (`health`, `version`) only if needed
183+
184+### Configuration model
185+
186+Executor config is handled as tool calls from inside `execute` via built-in namespace:
187+- `tools.executor.sources.add(...)`
188+- `tools.executor.sources.list(...)`
189+- `tools.executor.sources.remove(...)`
190+- other `tools.executor.*` controls as needed
191+
192+No mandatory top-level `set_enabled` in v1 (remove/add is enough for initial UX).
193+
194+### Execution semantics
195+
196+- Agent writes TS using `tools.*`.
197+- Runtime proxy resolves tool calls.
198+- Credentials/policies/approvals are applied server-side during invocation.
199+
200+---
201+
202+## 10) Dynamic source registration
203+
204+Goal: user can ask the agent to use a service; agent can register the source through Executor tooling.
205+
206+- Discovery itself may happen outside Executor (model/web).
207+- Registration happens via Executor control tools (`tools.executor.sources.add`).
208+
209+Registration flow states:
210+- `draft -> probing -> auth_required -> connected | error`
211+
212+Auth handling:
213+- MCP: dynamic auth/OAuth detection, client registration where needed
214+- OpenAPI: inspect security schemes, prompt for API key if required
215+
216+Secrets are entered through host UX, not model transcript.
217+
218+---
219+
220+## 11) Approvals and elicitation/input
221+
222+Use one internal broker for human-required inputs:
223+- approval
224+- confirm
225+- form input
226+- secret input
227+
228+### Capability-aware delivery
229+
230+- If client supports MCP elicitation: inline prompt.
231+- If not: out-of-band path (tmux pane, local web, notifications).
232+
233+### Terminal UX
234+
235+For fullscreen terminal clients:
236+- tmux split/popup watcher
237+- auto-show when pending approvals appear
238+- auto-hide after queue drains (with debounce)
239+
240+### Important boundary
241+
242+Approval/input actions are **host UX callbacks**, not AI SDK model tools.
243+
244+---
245+
246+## 12) AI SDK strategy
247+
248+API shape should be Executor session/adapters, not single-source helper APIs.
249+
250+### Adapter-first target design
251+
252+`Executor.create({ adapter })` with adapters such as:
253+- `inproc` (embedded runtime, no install)
254+- `pm` (local daemon transport)
255+- `remote` (Convex target transport)
256+
257+### Public SDK shape
258+
259+Primary operations:
260+- execute code
261+- register/list/remove sources
262+- stream run status/results
263+
264+Do not expose approval/input-response as model tools.
265+Use host callbacks (e.g. `onInputRequest`) instead.
266+
267+---
268+
269+## 13) Security posture
270+
271+- PM binds to loopback by default.
272+- Secrets never pass through model args/transcripts.
273+- Credential references in state; secret values in secure store.
274+- Strict origin/token checks if hosted web talks to localhost target.
275+- Audit events store decisions/actions, not raw secret material.
276+
277+---
278+
279+## 14) Upgrade and sync
280+
281+Primary path:
282+- `local-lite -> remote`
283+
284+Optional reverse:
285+- `remote -> local-lite` export/backup
286+
287+Sync principles:
288+- deterministic source identity
289+- idempotent imports
290+- explicit credential migration policy
291+- migration cursor/mapping persisted in local state
292+
293+---
294+
295+## 15) Rollout phases
296+
297+### Phase 0: Architecture + contracts
298+- finalize package boundaries
299+- finalize schema model set and invariants
300+- define state/repository interfaces
301+
302+### Phase 1: Local-lite core
303+- PM daemon skeleton
304+- file persistence (snapshot + WAL)
305+- execute path with runtime proxy integration
306+- basic source add/list/remove through `tools.executor.*`
307+
308+### Phase 2: Input/approval UX
309+- InputBroker + pending state persistence
310+- tmux watcher + local web fallback
311+- capability-aware elicitation fallback paths
312+
313+### Phase 3: Remote parity
314+- persistence-convex adapter and Convex app wiring
315+- shared RPC contracts for pm/convex
316+- target switching in CLI/web
317+
318+### Phase 4: Sync + upgrades
319+- local->remote promotion pipeline
320+- idempotent mapping/cursor strategy
321+- conflict handling and dry-run reporting
322+
323+### Phase 5: AI SDK adapters
324+- inproc adapter
325+- pm transport adapter
326+- remote transport adapter
327+- host callback API for inputs
328+
329+---
330+
331+## 16) Success criteria
332+
333+- First successful `npx executor <agent>` run with no login in <2 minutes.
334+- No required database for local-lite mode.
335+- Same execution semantics across local and remote targets.
336+- Agent can add sources and execute against them in one flow.
337+- Approval/input is usable in fullscreen terminal clients.
338+
339+---
340+
341+## 17) Non-goals (initial)
342+
343+- Recreating all enterprise admin UX in local-lite.
344+- Exposing huge top-level tool catalogs to the model by default.
345+- Maintaining separate cloud vs self-host architecture branches.
346+
347+---
348+
349+## 18) Current scaffold status
350+
351+Implemented in `v2` so far:
352+- monorepo app/package skeleton
353+- `confect` imported from quickhub
354+- schema package scaffold with Effect models
355+- domain ID groundwork in `schema/src/ids.ts`
356+- Convex table ID groundwork in `persistence-convex/src/convex-ids.ts`
357+
358+Not implemented yet:
359+- runtime wiring, adapters, rpc handlers, PM behavior, sync engine
360+
361+---
362+
363+## 19) Immediate next steps
364+
365+1. Define `persistence-ports` interfaces against current schema models.
366+2. Implement minimal `persistence-local` snapshot + event log codec.
367+3. Wire a minimal `apps/pm` execute flow (single run lifecycle).
368+4. Add first `tools.executor.sources.add/list/remove` control operations.
369+5. Add basic InputBroker contract (without full UI).
v2/apps/convex/.gitignore+2−0
@@ -0,0 +1,2 @@
1+
2+.env.local
v2/apps/convex/README.md+4−0
@@ -1,3 +1,7 @@
11 # app-convex
22
33 Convex remote server app scaffold for Executor v2.
4+
5+Current scaffold includes:
6+- Convex schema wiring at `convex/schema.ts`, sourced from `@executor-v2/persistence-convex`
7+- bare-minimum MCP HTTP endpoint with a stub tool at `convex/http.ts` and `convex/mcp.ts`
v2/apps/convex/convex/README.md+90−0
@@ -0,0 +1,90 @@
1+# Welcome to your Convex functions directory!
2+
3+Write your Convex functions here.
4+See https://docs.convex.dev/functions for more.
5+
6+A query function that takes two arguments looks like:
7+
8+```ts
9+// convex/myFunctions.ts
10+import { query } from "./_generated/server";
11+import { v } from "convex/values";
12+
13+export const myQueryFunction = query({
14+ // Validators for arguments.
15+ args: {
16+ first: v.number(),
17+ second: v.string(),
18+ },
19+
20+ // Function implementation.
21+ handler: async (ctx, args) => {
22+ // Read the database as many times as you need here.
23+ // See https://docs.convex.dev/database/reading-data.
24+ const documents = await ctx.db.query("tablename").collect();
25+
26+ // Arguments passed from the client are properties of the args object.
27+ console.log(args.first, args.second);
28+
29+ // Write arbitrary JavaScript here: filter, aggregate, build derived data,
30+ // remove non-public properties, or create new objects.
31+ return documents;
32+ },
33+});
34+```
35+
36+Using this query function in a React component looks like:
37+
38+```ts
39+const data = useQuery(api.myFunctions.myQueryFunction, {
40+ first: 10,
41+ second: "hello",
42+});
43+```
44+
45+A mutation function looks like:
46+
47+```ts
48+// convex/myFunctions.ts
49+import { mutation } from "./_generated/server";
50+import { v } from "convex/values";
51+
52+export const myMutationFunction = mutation({
53+ // Validators for arguments.
54+ args: {
55+ first: v.string(),
56+ second: v.string(),
57+ },
58+
59+ // Function implementation.
60+ handler: async (ctx, args) => {
61+ // Insert or modify documents in the database here.
62+ // Mutations can also read from the database like queries.
63+ // See https://docs.convex.dev/database/writing-data.
64+ const message = { body: args.first, author: args.second };
65+ const id = await ctx.db.insert("messages", message);
66+
67+ // Optionally, return a value from your mutation.
68+ return await ctx.db.get("messages", id);
69+ },
70+});
71+```
72+
73+Using this mutation function in a React component looks like:
74+
75+```ts
76+const mutation = useMutation(api.myFunctions.myMutationFunction);
77+function handleButtonPress() {
78+ // fire and forget, the most common way to use mutations
79+ mutation({ first: "Hello!", second: "me" });
80+ // OR
81+ // use the result once the mutation has completed
82+ mutation({ first: "Hello!", second: "me" }).then((result) =>
83+ console.log(result),
84+ );
85+}
86+```
87+
88+Use the Convex CLI to push your functions to a deployment. See everything
89+the Convex CLI can do by running `npx convex -h` in your project root
90+directory. To learn more, launch the docs with `npx convex docs`.
v2/apps/convex/convex/_generated/api.d.ts+51−0
@@ -0,0 +1,51 @@
1+/* eslint-disable */
2+/**
3+ * Generated `api` utility.
4+ *
5+ * THIS CODE IS AUTOMATICALLY GENERATED.
6+ *
7+ * To regenerate, run `npx convex dev`.
8+ * @module
9+ */
10+
11+import type * as http from "../http.js";
12+import type * as mcp from "../mcp.js";
13+
14+import type {
15+ ApiFromModules,
16+ FilterApi,
17+ FunctionReference,
18+} from "convex/server";
19+
20+declare const fullApi: ApiFromModules<{
21+ http: typeof http;
22+ mcp: typeof mcp;
23+}>;
24+
25+/**
26+ * A utility for referencing Convex functions in your app's public API.
27+ *
28+ * Usage:
29+ * ```js
30+ * const myFunctionReference = api.myModule.myFunction;
31+ * ```
32+ */
33+export declare const api: FilterApi<
34+ typeof fullApi,
35+ FunctionReference<any, "public">
36+>;
37+
38+/**
39+ * A utility for referencing Convex functions in your app's internal API.
40+ *
41+ * Usage:
42+ * ```js
43+ * const myFunctionReference = internal.myModule.myFunction;
44+ * ```
45+ */
46+export declare const internal: FilterApi<
47+ typeof fullApi,
48+ FunctionReference<any, "internal">
49+>;
50+
51+export declare const components: {};
v2/apps/convex/convex/_generated/api.js+23−0
@@ -0,0 +1,23 @@
1+/* eslint-disable */
2+/**
3+ * Generated `api` utility.
4+ *
5+ * THIS CODE IS AUTOMATICALLY GENERATED.
6+ *
7+ * To regenerate, run `npx convex dev`.
8+ * @module
9+ */
10+
11+import { anyApi, componentsGeneric } from "convex/server";
12+
13+/**
14+ * A utility for referencing Convex functions in your app's API.
15+ *
16+ * Usage:
17+ * ```js
18+ * const myFunctionReference = api.myModule.myFunction;
19+ * ```
20+ */
21+export const api = anyApi;
22+export const internal = anyApi;
23+export const components = componentsGeneric();
v2/apps/convex/convex/_generated/dataModel.d.ts+60−0
@@ -0,0 +1,60 @@
1+/* eslint-disable */
2+/**
3+ * Generated data model types.
4+ *
5+ * THIS CODE IS AUTOMATICALLY GENERATED.
6+ *
7+ * To regenerate, run `npx convex dev`.
8+ * @module
9+ */
10+
11+import type {
12+ DataModelFromSchemaDefinition,
13+ DocumentByName,
14+ TableNamesInDataModel,
15+ SystemTableNames,
16+} from "convex/server";
17+import type { GenericId } from "convex/values";
18+import schema from "../schema.js";
19+
20+/**
21+ * The names of all of your Convex tables.
22+ */
23+export type TableNames = TableNamesInDataModel<DataModel>;
24+
25+/**
26+ * The type of a document stored in Convex.
27+ *
28+ * @typeParam TableName - A string literal type of the table name (like "users").
29+ */
30+export type Doc<TableName extends TableNames> = DocumentByName<
31+ DataModel,
32+ TableName
33+>;
34+
35+/**
36+ * An identifier for a document in Convex.
37+ *
38+ * Convex documents are uniquely identified by their `Id`, which is accessible
39+ * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
40+ *
41+ * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
42+ *
43+ * IDs are just strings at runtime, but this type can be used to distinguish them from other
44+ * strings when type checking.
45+ *
46+ * @typeParam TableName - A string literal type of the table name (like "users").
47+ */
48+export type Id<TableName extends TableNames | SystemTableNames> =
49+ GenericId<TableName>;
50+
51+/**
52+ * A type describing your Convex data model.
53+ *
54+ * This type includes information about what tables you have, the type of
55+ * documents stored in those tables, and the indexes defined on them.
56+ *
57+ * This type is used to parameterize methods like `queryGeneric` and
58+ * `mutationGeneric` to make them type-safe.
59+ */
60+export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
v2/apps/convex/convex/_generated/server.d.ts+143−0
@@ -0,0 +1,143 @@
1+/* eslint-disable */
2+/**
3+ * Generated utilities for implementing server-side Convex query and mutation functions.
4+ *
5+ * THIS CODE IS AUTOMATICALLY GENERATED.
6+ *
7+ * To regenerate, run `npx convex dev`.
8+ * @module
9+ */
10+
11+import {
12+ ActionBuilder,
13+ HttpActionBuilder,
14+ MutationBuilder,
15+ QueryBuilder,
16+ GenericActionCtx,
17+ GenericMutationCtx,
18+ GenericQueryCtx,
19+ GenericDatabaseReader,
20+ GenericDatabaseWriter,
21+} from "convex/server";
22+import type { DataModel } from "./dataModel.js";
23+
24+/**
25+ * Define a query in this Convex app's public API.
26+ *
27+ * This function will be allowed to read your Convex database and will be accessible from the client.
28+ *
29+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
30+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
31+ */
32+export declare const query: QueryBuilder<DataModel, "public">;
33+
34+/**
35+ * Define a query that is only accessible from other Convex functions (but not from the client).
36+ *
37+ * This function will be allowed to read from your Convex database. It will not be accessible from the client.
38+ *
39+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
40+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
41+ */
42+export declare const internalQuery: QueryBuilder<DataModel, "internal">;
43+
44+/**
45+ * Define a mutation in this Convex app's public API.
46+ *
47+ * This function will be allowed to modify your Convex database and will be accessible from the client.
48+ *
49+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
50+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
51+ */
52+export declare const mutation: MutationBuilder<DataModel, "public">;
53+
54+/**
55+ * Define a mutation that is only accessible from other Convex functions (but not from the client).
56+ *
57+ * This function will be allowed to modify your Convex database. It will not be accessible from the client.
58+ *
59+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
60+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
61+ */
62+export declare const internalMutation: MutationBuilder<DataModel, "internal">;
63+
64+/**
65+ * Define an action in this Convex app's public API.
66+ *
67+ * An action is a function which can execute any JavaScript code, including non-deterministic
68+ * code and code with side-effects, like calling third-party services.
69+ * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
70+ * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
71+ *
72+ * @param func - The action. It receives an {@link ActionCtx} as its first argument.
73+ * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
74+ */
75+export declare const action: ActionBuilder<DataModel, "public">;
76+
77+/**
78+ * Define an action that is only accessible from other Convex functions (but not from the client).
79+ *
80+ * @param func - The function. It receives an {@link ActionCtx} as its first argument.
81+ * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
82+ */
83+export declare const internalAction: ActionBuilder<DataModel, "internal">;
84+
85+/**
86+ * Define an HTTP action.
87+ *
88+ * The wrapped function will be used to respond to HTTP requests received
89+ * by a Convex deployment if the requests matches the path and method where
90+ * this action is routed. Be sure to route your httpAction in `convex/http.js`.
91+ *
92+ * @param func - The function. It receives an {@link ActionCtx} as its first argument
93+ * and a Fetch API `Request` object as its second.
94+ * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
95+ */
96+export declare const httpAction: HttpActionBuilder;
97+
98+/**
99+ * A set of services for use within Convex query functions.
100+ *
101+ * The query context is passed as the first argument to any Convex query
102+ * function run on the server.
103+ *
104+ * This differs from the {@link MutationCtx} because all of the services are
105+ * read-only.
106+ */
107+export type QueryCtx = GenericQueryCtx<DataModel>;
108+
109+/**
110+ * A set of services for use within Convex mutation functions.
111+ *
112+ * The mutation context is passed as the first argument to any Convex mutation
113+ * function run on the server.
114+ */
115+export type MutationCtx = GenericMutationCtx<DataModel>;
116+
117+/**
118+ * A set of services for use within Convex action functions.
119+ *
120+ * The action context is passed as the first argument to any Convex action
121+ * function run on the server.
122+ */
123+export type ActionCtx = GenericActionCtx<DataModel>;
124+
125+/**
126+ * An interface to read from the database within Convex query functions.
127+ *
128+ * The two entry points are {@link DatabaseReader.get}, which fetches a single
129+ * document by its {@link Id}, or {@link DatabaseReader.query}, which starts
130+ * building a query.
131+ */
132+export type DatabaseReader = GenericDatabaseReader<DataModel>;
133+
134+/**
135+ * An interface to read from and write to the database within Convex mutation
136+ * functions.
137+ *
138+ * Convex guarantees that all writes within a single mutation are
139+ * executed atomically, so you never have to worry about partial writes leaving
140+ * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
141+ * for the guarantees Convex provides your functions.
142+ */
143+export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
v2/apps/convex/convex/_generated/server.js+93−0
@@ -0,0 +1,93 @@
1+/* eslint-disable */
2+/**
3+ * Generated utilities for implementing server-side Convex query and mutation functions.
4+ *
5+ * THIS CODE IS AUTOMATICALLY GENERATED.
6+ *
7+ * To regenerate, run `npx convex dev`.
8+ * @module
9+ */
10+
11+import {
12+ actionGeneric,
13+ httpActionGeneric,
14+ queryGeneric,
15+ mutationGeneric,
16+ internalActionGeneric,
17+ internalMutationGeneric,
18+ internalQueryGeneric,
19+} from "convex/server";
20+
21+/**
22+ * Define a query in this Convex app's public API.
23+ *
24+ * This function will be allowed to read your Convex database and will be accessible from the client.
25+ *
26+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
27+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
28+ */
29+export const query = queryGeneric;
30+
31+/**
32+ * Define a query that is only accessible from other Convex functions (but not from the client).
33+ *
34+ * This function will be allowed to read from your Convex database. It will not be accessible from the client.
35+ *
36+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
37+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
38+ */
39+export const internalQuery = internalQueryGeneric;
40+
41+/**
42+ * Define a mutation in this Convex app's public API.
43+ *
44+ * This function will be allowed to modify your Convex database and will be accessible from the client.
45+ *
46+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
47+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
48+ */
49+export const mutation = mutationGeneric;
50+
51+/**
52+ * Define a mutation that is only accessible from other Convex functions (but not from the client).
53+ *
54+ * This function will be allowed to modify your Convex database. It will not be accessible from the client.
55+ *
56+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
57+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
58+ */
59+export const internalMutation = internalMutationGeneric;
60+
61+/**
62+ * Define an action in this Convex app's public API.
63+ *
64+ * An action is a function which can execute any JavaScript code, including non-deterministic
65+ * code and code with side-effects, like calling third-party services.
66+ * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
67+ * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
68+ *
69+ * @param func - The action. It receives an {@link ActionCtx} as its first argument.
70+ * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
71+ */
72+export const action = actionGeneric;
73+
74+/**
75+ * Define an action that is only accessible from other Convex functions (but not from the client).
76+ *
77+ * @param func - The function. It receives an {@link ActionCtx} as its first argument.
78+ * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
79+ */
80+export const internalAction = internalActionGeneric;
81+
82+/**
83+ * Define an HTTP action.
84+ *
85+ * The wrapped function will be used to respond to HTTP requests received
86+ * by a Convex deployment if the requests matches the path and method where
87+ * this action is routed. Be sure to route your httpAction in `convex/http.js`.
88+ *
89+ * @param func - The function. It receives an {@link ActionCtx} as its first argument
90+ * and a Fetch API `Request` object as its second.
91+ * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
92+ */
93+export const httpAction = httpActionGeneric;
v2/apps/convex/convex/http.ts+13−0
@@ -0,0 +1,13 @@
1+import { httpRouter } from "convex/server";
2+import { mcpHandler } from "./mcp";
3+
4+const http = httpRouter();
5+
6+http.route({ path: "/mcp", method: "POST", handler: mcpHandler });
7+http.route({ path: "/mcp", method: "GET", handler: mcpHandler });
8+http.route({ path: "/mcp", method: "DELETE", handler: mcpHandler });
9+http.route({ path: "/v1/mcp", method: "POST", handler: mcpHandler });
10+http.route({ path: "/v1/mcp", method: "GET", handler: mcpHandler });
11+http.route({ path: "/v1/mcp", method: "DELETE", handler: mcpHandler });
12+
13+export default http;
v2/apps/convex/convex/mcp.ts+9−0
@@ -0,0 +1,9 @@
1+import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway";
2+import { httpAction } from "./_generated/server";
3+
4+export const mcpHandler = httpAction(async (_ctx, request) =>
5+ handleMcpHttpRequest(request, {
6+ target: "remote",
7+ serverName: "executor-v2-convex",
8+ serverVersion: "0.0.0",
9+ }));
v2/apps/convex/convex/schema.ts+3−0
@@ -0,0 +1,3 @@
1+import { executorConfectSchema } from "@executor-v2/persistence-convex";
2+
3+export default executorConfectSchema.convexSchemaDefinition;
v2/apps/convex/convex/tsconfig.json+25−0
@@ -0,0 +1,25 @@
1+{
2+ /* This TypeScript project config describes the environment that
3+ * Convex functions run in and is used to typecheck them.
4+ * You can modify it, but some settings are required to use Convex.
5+ */
6+ "compilerOptions": {
7+ /* These settings are not required by Convex and can be modified. */
8+ "allowJs": true,
9+ "strict": true,
10+ "moduleResolution": "Bundler",
11+ "jsx": "react-jsx",
12+ "skipLibCheck": true,
13+ "allowSyntheticDefaultImports": true,
14+
15+ /* These compiler options are required by Convex */
16+ "target": "ESNext",
17+ "lib": ["ES2021", "dom"],
18+ "forceConsistentCasingInFileNames": true,
19+ "module": "ESNext",
20+ "isolatedModules": true,
21+ "noEmit": true
22+ },
23+ "include": ["./**/*"],
24+ "exclude": ["./_generated"]
25+}
v2/apps/convex/package.json+8−1
@@ -3,6 +3,13 @@ · 2 unmodified lines
33 "private": true,
44 "type": "module",
55 "scripts": {
6− "typecheck": "bunx tsc --noEmit -p tsconfig.json"
6+ "dev": "bunx convex dev",
7+ "typecheck": "bunx tsc --noEmit -p tsconfig.json",
8+ "convex:codegen": "bunx convex codegen"
9+ },
10+ "dependencies": {
11+ "@executor-v2/mcp-gateway": "workspace:*",
12+ "@executor-v2/persistence-convex": "workspace:*",
13+ "convex": "^1.31.7"
714 }
815 }
v2/apps/convex/tsconfig.json+1−1
@@ -1,4 +1,4 @@
11 {
22 "extends": "../../tsconfig.json",
3− "include": ["src"]
3+ "include": ["src", "convex"]
44 }
v2/apps/pm/README.md+5−0
@@ -1,3 +1,8 @@
11 # app-pm
22
33 Local Process Manager app scaffold for Executor v2.
4+
5+Current scaffold includes:
6+- local MCP endpoint at `POST /mcp`
7+- health endpoint at `GET /healthz`
8+- MCP stub tool routing via `@executor-v2/mcp-gateway`
v2/apps/pm/package.json+5−0
@@ -3,6 +3,11 @@ · 2 unmodified lines
33 "private": true,
44 "type": "module",
55 "scripts": {
6+ "dev": "bun --watch src/main.ts",
7+ "start": "bun src/main.ts",
68 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
9+ },
10+ "dependencies": {
11+ "@executor-v2/mcp-gateway": "workspace:*"
712 }
813 }
v2/apps/pm/src/main.ts+31−0
@@ -0,0 +1,31 @@
1+import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway";
2+
3+const port = Number(Bun.env.PORT ?? 8787);
4+
5+const handleMcp = async (request: Request): Promise<Response> =>
6+ handleMcpHttpRequest(request, {
7+ target: "local",
8+ serverName: "executor-v2-pm",
9+ serverVersion: "0.0.0",
10+ });
11+
12+const server = Bun.serve({
13+ port,
14+ routes: {
15+ "/healthz": {
16+ GET: () => Response.json({ ok: true, service: "pm" }, { status: 200 }),
17+ },
18+ "/mcp": {
19+ GET: handleMcp,
20+ POST: handleMcp,
21+ DELETE: handleMcp,
22+ },
23+ "/v1/mcp": {
24+ GET: handleMcp,
25+ POST: handleMcp,
26+ DELETE: handleMcp,
27+ },
28+ },
29+});
30+
31+console.log(`executor-v2 PM listening on http://127.0.0.1:${server.port}`);
v2/apps/pm/tsconfig.json+3−0
@@ -1,4 +1,7 @@
11 {
22 "extends": "../../tsconfig.json",
3+ "compilerOptions": {
4+ "types": ["bun"]
5+ },
36 "include": ["src"]
47 }
v2/bun.lock+699−0
@@ -0,0 +1,699 @@
1+{
2+ "lockfileVersion": 1,
3+ "configVersion": 1,
4+ "workspaces": {
5+ "": {
6+ "name": "executor-v2",
7+ "devDependencies": {
8+ "@effect/language-service": "^0.76.0",
9+ "typescript": "^5.9.3",
10+ },
11+ },
12+ "apps/cli": {
13+ "name": "@executor-v2/app-cli",
14+ },
15+ "apps/convex": {
16+ "name": "@executor-v2/app-convex",
17+ "dependencies": {
18+ "@executor-v2/mcp-gateway": "workspace:*",
19+ "@executor-v2/persistence-convex": "workspace:*",
20+ "convex": "^1.31.7",
21+ },
22+ },
23+ "apps/pm": {
24+ "name": "@executor-v2/app-pm",
25+ "dependencies": {
26+ "@executor-v2/mcp-gateway": "workspace:*",
27+ },
28+ },
29+ "apps/web": {
30+ "name": "@executor-v2/app-web",
31+ },
32+ "packages/ai-sdk-adapter": {
33+ "name": "@executor-v2/ai-sdk-adapter",
34+ },
35+ "packages/approvals": {
36+ "name": "@executor-v2/approvals",
37+ },
38+ "packages/confect": {
39+ "name": "@executor-v2/confect",
40+ "version": "0.0.0",
41+ "dependencies": {
42+ "@effect-atom/atom": "^0.4.11",
43+ "@effect-atom/atom-react": "^0.4.4",
44+ "@effect/platform": "^0.93.6",
45+ "@effect/rpc": "^0.72.2",
46+ "convex": "^1.32.0",
47+ "effect": "^3.19.8",
48+ "next": "^16.1.6",
49+ },
50+ "devDependencies": {
51+ "@effect/vitest": "^0.27.0",
52+ "@types/node": "^20.19.9",
53+ "typescript": "^5.9.2",
54+ "vitest": "^3.2.4",
55+ },
56+ },
57+ "packages/domain": {
58+ "name": "@executor-v2/domain",
59+ },
60+ "packages/engine": {
61+ "name": "@executor-v2/engine",
62+ },
63+ "packages/mcp-gateway": {
64+ "name": "@executor-v2/mcp-gateway",
65+ "dependencies": {
66+ "@modelcontextprotocol/sdk": "^1.26.0",
67+ "zod": "^4.3.6",
68+ },
69+ },
70+ "packages/oauth": {
71+ "name": "@executor-v2/oauth",
72+ },
73+ "packages/rpc": {
74+ "name": "@executor-v2/rpc",
75+ },
76+ "packages/schema": {
77+ "name": "@executor-v2/schema",
78+ "dependencies": {
79+ "effect": "^3.19.4",
80+ },
81+ },
82+ "packages/sdk": {
83+ "name": "@executor-v2/sdk",
84+ },
85+ "packages/secrets": {
86+ "name": "@executor-v2/secrets",
87+ },
88+ "packages/source-manager": {
89+ "name": "@executor-v2/source-manager",
90+ "dependencies": {
91+ "@executor-v2/persistence-ports": "workspace:*",
92+ "@executor-v2/schema": "workspace:*",
93+ "effect": "^3.19.8",
94+ },
95+ },
96+ "packages/state-contracts": {
97+ "name": "@executor-v2/persistence-ports",
98+ "dependencies": {
99+ "@executor-v2/schema": "workspace:*",
100+ "effect": "^3.19.8",
101+ },
102+ },
103+ "packages/state-convex": {
104+ "name": "@executor-v2/persistence-convex",
105+ "dependencies": {
106+ "@executor-v2/confect": "workspace:*",
107+ "@executor-v2/persistence-ports": "workspace:*",
108+ "@executor-v2/schema": "workspace:*",
109+ "effect": "^3.19.8",
110+ },
111+ },
112+ "packages/state-local-file": {
113+ "name": "@executor-v2/persistence-local",
114+ "dependencies": {
115+ "@executor-v2/persistence-ports": "workspace:*",
116+ "@executor-v2/schema": "workspace:*",
117+ "effect": "^3.19.8",
118+ },
119+ },
120+ "packages/sync": {
121+ "name": "@executor-v2/sync",
122+ },
123+ },
124+ "packages": {
125+ "@effect-atom/atom": ["@effect-atom/atom@0.4.13", "", { "peerDependencies": { "@effect/experimental": "^0.57.0", "@effect/platform": "^0.93.0", "@effect/rpc": "^0.72.1", "effect": "^3.19.0" } }, "sha512-wYCPDg/vN2IWTqxho7kTTSivh//V2TSAcdkQvLXMJWww8YzhcCGmCiDpT6cefyNXvHj+qZrqYB54mrUqG6EYCA=="],
126+
127+ "@effect-atom/atom-react": ["@effect-atom/atom-react@0.4.6", "", { "dependencies": { "@effect-atom/atom": "^0.4.13" }, "peerDependencies": { "effect": "^3.19", "react": ">=18 <20", "scheduler": "*" } }, "sha512-ZvncuSQsCcUrsddD00JcKDMraPge0AiDMGiJqKc7ProvagCVrxB+Emy5uHM0l0SLGUx4X56bFE0d1tp9Z/Bf+g=="],
128+
129+ "@effect/experimental": ["@effect/experimental@0.57.11", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.93.6", "effect": "^3.19.9", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-M5uug3Drs/gyTHLfA+XzcIZQGUEV/Jn5yi1POki4oZswhpzNmsVTHl4THpxAordRKwa5lFvTSlsRP684YH7pSw=="],
130+
131+ "@effect/language-service": ["@effect/language-service@0.76.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-tCFQjdvdeqSx+j+QCA9dbz5K4BJtsvIknH8sHft8pTT8ClmVr7uAghD7TcufCtDOZQunXRXyKqQZzErduqhSYQ=="],
132+
133+ "@effect/platform": ["@effect/platform@0.93.8", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.12" } }, "sha512-xTEy6fyTy4ijmFC3afKgtvYtn/JyPoIov4ZSUWJZUv3VeOcUPNGrrqG6IJlWkXs3NhvSywKv7wc1kw3epCQVZw=="],
134+
135+ "@effect/rpc": ["@effect/rpc@0.72.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.93.3", "effect": "^3.19.5" } }, "sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ=="],
136+
137+ "@effect/vitest": ["@effect/vitest@0.27.0", "", { "peerDependencies": { "effect": "^3.19.0", "vitest": "^3.2.0" } }, "sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ=="],
138+
139+ "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
140+
141+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
142+
143+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
144+
145+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="],
146+
147+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="],
148+
149+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="],
150+
151+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="],
152+
153+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="],
154+
155+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="],
156+
157+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="],
158+
159+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="],
160+
161+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="],
162+
163+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="],
164+
165+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="],
166+
167+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="],
168+
169+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="],
170+
171+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="],
172+
173+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="],
174+
175+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="],
176+
177+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="],
178+
179+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="],
180+
181+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="],
182+
183+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="],
184+
185+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="],
186+
187+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="],
188+
189+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
190+
191+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
192+
193+ "@executor-v2/ai-sdk-adapter": ["@executor-v2/ai-sdk-adapter@workspace:packages/ai-sdk-adapter"],
194+
195+ "@executor-v2/app-cli": ["@executor-v2/app-cli@workspace:apps/cli"],
196+
197+ "@executor-v2/app-convex": ["@executor-v2/app-convex@workspace:apps/convex"],
198+
199+ "@executor-v2/app-pm": ["@executor-v2/app-pm@workspace:apps/pm"],
200+
201+ "@executor-v2/app-web": ["@executor-v2/app-web@workspace:apps/web"],
202+
203+ "@executor-v2/approvals": ["@executor-v2/approvals@workspace:packages/approvals"],
204+
205+ "@executor-v2/confect": ["@executor-v2/confect@workspace:packages/confect"],
206+
207+ "@executor-v2/domain": ["@executor-v2/domain@workspace:packages/domain"],
208+
209+ "@executor-v2/engine": ["@executor-v2/engine@workspace:packages/engine"],
210+
211+ "@executor-v2/mcp-gateway": ["@executor-v2/mcp-gateway@workspace:packages/mcp-gateway"],
212+
213+ "@executor-v2/oauth": ["@executor-v2/oauth@workspace:packages/oauth"],
214+
215+ "@executor-v2/persistence-convex": ["@executor-v2/persistence-convex@workspace:packages/state-convex"],
216+
217+ "@executor-v2/persistence-local": ["@executor-v2/persistence-local@workspace:packages/state-local-file"],
218+
219+ "@executor-v2/persistence-ports": ["@executor-v2/persistence-ports@workspace:packages/state-contracts"],
220+
221+ "@executor-v2/rpc": ["@executor-v2/rpc@workspace:packages/rpc"],
222+
223+ "@executor-v2/schema": ["@executor-v2/schema@workspace:packages/schema"],
224+
225+ "@executor-v2/sdk": ["@executor-v2/sdk@workspace:packages/sdk"],
226+
227+ "@executor-v2/secrets": ["@executor-v2/secrets@workspace:packages/secrets"],
228+
229+ "@executor-v2/source-manager": ["@executor-v2/source-manager@workspace:packages/source-manager"],
230+
231+ "@executor-v2/sync": ["@executor-v2/sync@workspace:packages/sync"],
232+
233+ "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
234+
235+ "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
236+
237+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
238+
239+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
240+
241+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
242+
243+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
244+
245+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
246+
247+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
248+
249+ "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
250+
251+ "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
252+
253+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
254+
255+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
256+
257+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
258+
259+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
260+
261+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
262+
263+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
264+
265+ "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
266+
267+ "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
268+
269+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
270+
271+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
272+
273+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
274+
275+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
276+
277+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
278+
279+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
280+
281+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
282+
283+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
284+
285+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
286+
287+ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
288+
289+ "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
290+
291+ "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
292+
293+ "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="],
294+
295+ "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="],
296+
297+ "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="],
298+
299+ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
300+
301+ "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
302+
303+ "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
304+
305+ "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
306+
307+ "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
308+
309+ "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
310+
311+ "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
312+
313+ "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
314+
315+ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
316+
317+ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
318+
319+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
320+
321+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
322+
323+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
324+
325+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
326+
327+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
328+
329+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
330+
331+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
332+
333+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
334+
335+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
336+
337+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
338+
339+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
340+
341+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
342+
343+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
344+
345+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
346+
347+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
348+
349+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
350+
351+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
352+
353+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
354+
355+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
356+
357+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
358+
359+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
360+
361+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
362+
363+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
364+
365+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
366+
367+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
368+
369+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
370+
371+ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
372+
373+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
374+
375+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
376+
377+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
378+
379+ "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
380+
381+ "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
382+
383+ "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
384+
385+ "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
386+
387+ "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
388+
389+ "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
390+
391+ "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
392+
393+ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
394+
395+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
396+
397+ "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
398+
399+ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
400+
401+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
402+
403+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
404+
405+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
406+
407+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
408+
409+ "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
410+
411+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
412+
413+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
414+
415+ "caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
416+
417+ "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
418+
419+ "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
420+
421+ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
422+
423+ "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
424+
425+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
426+
427+ "convex": ["convex@1.32.0", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-5FlajdLpW75pdLS+/CgGH5H6yeRuA+ru50AKJEYbJpmyILUS+7fdTvsdTaQ7ZFXMv0gE8mX4S+S3AtJ94k0mfw=="],
428+
429+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
430+
431+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
432+
433+ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
434+
435+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
436+
437+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
438+
439+ "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
440+
441+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
442+
443+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
444+
445+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
446+
447+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
448+
449+ "effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="],
450+
451+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
452+
453+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
454+
455+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
456+
457+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
458+
459+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
460+
461+ "esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
462+
463+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
464+
465+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
466+
467+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
468+
469+ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
470+
471+ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
472+
473+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
474+
475+ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
476+
477+ "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="],
478+
479+ "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
480+
481+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
482+
483+ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
484+
485+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
486+
487+ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
488+
489+ "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
490+
491+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
492+
493+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
494+
495+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
496+
497+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
498+
499+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
500+
501+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
502+
503+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
504+
505+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
506+
507+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
508+
509+ "hono": ["hono@4.12.2", "", {}, "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg=="],
510+
511+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
512+
513+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
514+
515+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
516+
517+ "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
518+
519+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
520+
521+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
522+
523+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
524+
525+ "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
526+
527+ "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
528+
529+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
530+
531+ "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
532+
533+ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
534+
535+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
536+
537+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
538+
539+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
540+
541+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
542+
543+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
544+
545+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
546+
547+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
548+
549+ "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],
550+
551+ "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
552+
553+ "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
554+
555+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
556+
557+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
558+
559+ "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
560+
561+ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
562+
563+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
564+
565+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
566+
567+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
568+
569+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
570+
571+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
572+
573+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
574+
575+ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
576+
577+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
578+
579+ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
580+
581+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
582+
583+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
584+
585+ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
586+
587+ "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
588+
589+ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
590+
591+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
592+
593+ "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
594+
595+ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
596+
597+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
598+
599+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
600+
601+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
602+
603+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
604+
605+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
606+
607+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
608+
609+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
610+
611+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
612+
613+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
614+
615+ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
616+
617+ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
618+
619+ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
620+
621+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
622+
623+ "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
624+
625+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
626+
627+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
628+
629+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
630+
631+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
632+
633+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
634+
635+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
636+
637+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
638+
639+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
640+
641+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
642+
643+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
644+
645+ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
646+
647+ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
648+
649+ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
650+
651+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
652+
653+ "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
654+
655+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
656+
657+ "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
658+
659+ "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
660+
661+ "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
662+
663+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
664+
665+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
666+
667+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
668+
669+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
670+
671+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
672+
673+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
674+
675+ "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
676+
677+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
678+
679+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
680+
681+ "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
682+
683+ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
684+
685+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
686+
687+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
688+
689+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
690+
691+ "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
692+
693+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
694+
695+ "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
696+
697+ "vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
698+ }
699+}
v2/package.json+6−1
@@ -8,6 +8,11 @@ · 7 unmodified lines
88 "packages/*"
99 ],
1010 "scripts": {
11− "typecheck": "bunx tsc --noEmit"
11+ "prepare": "effect-language-service patch",
12+ "typecheck": "tsc --noEmit"
13+ },
14+ "devDependencies": {
15+ "@effect/language-service": "^0.76.0",
16+ "typescript": "^5.9.3"
1217 }
1318 }
v2/packages/confect-core/README.md+0−5
@@ -1,5 +0,0 @@
1−# confect-core
2−
3−Effect schema helper primitives scaffold for Executor v2.
4−
5−Current scaffold includes basic helpers in `src/schema.ts`.
v2/packages/confect-core/package.json+0−14
@@ -1,14 +0,0 @@
1−{
2− "name": "@executor-v2/confect-core",
3− "private": true,
4− "type": "module",
5− "exports": {
6− ".": "./src/index.ts"
7− },
8− "scripts": {
9− "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10− },
11− "dependencies": {
12− "effect": "^3.19.4"
13− }
14−}
v2/packages/confect-core/src/index.ts+0−1
@@ -1 +0,0 @@
1−export * from "./schema";
v2/packages/confect-core/src/schema.ts+0−6
@@ -1,6 +0,0 @@
1−import { Schema } from "effect";
2−
3−export const TimestampMsSchema = Schema.Number;
4−
5−export const makeIdSchema = (brandName: string) =>
6− Schema.String.pipe(Schema.brand(brandName));
v2/packages/confect-core/tsconfig.json+0−4
@@ -1,4 +0,0 @@
1−{
2− "extends": "../../tsconfig.json",
3− "include": ["src"]
4−}
v2/packages/confect/package.json+35−0
@@ -0,0 +1,35 @@
1+{
2+ "name": "@executor-v2/confect",
3+ "version": "0.0.0",
4+ "private": true,
5+ "type": "module",
6+ "exports": {
7+ ".": "./src/index.ts",
8+ "./schema": "./src/schema.ts",
9+ "./validators": "./src/validators.ts",
10+ "./ctx": "./src/ctx.ts",
11+ "./client": "./src/client.ts",
12+ "./rpc": "./src/rpc/index.ts",
13+ "./testing": "./src/testing.ts"
14+ },
15+ "scripts": {
16+ "test": "vitest run --no-watch",
17+ "test:watch": "vitest",
18+ "typecheck": "bunx tsc --noEmit -p tsconfig.json"
19+ },
20+ "dependencies": {
21+ "@effect-atom/atom": "^0.4.11",
22+ "@effect-atom/atom-react": "^0.4.4",
23+ "@effect/platform": "^0.93.6",
24+ "@effect/rpc": "^0.72.2",
25+ "convex": "^1.32.0",
26+ "effect": "^3.19.8",
27+ "next": "^16.1.6"
28+ },
29+ "devDependencies": {
30+ "@effect/vitest": "^0.27.0",
31+ "@types/node": "^20.19.9",
32+ "typescript": "^5.9.2",
33+ "vitest": "^3.2.4"
34+ }
35+}
v2/packages/confect/src/client.ts+160−0
@@ -0,0 +1,160 @@
1+import type { FunctionReference, FunctionReturnType } from "convex/server";
2+import {
3+ ConvexClient as ConvexClientImpl,
4+ ConvexHttpClient,
5+} from "convex/browser";
6+import { Context, Effect, Layer, Stream } from "effect";
7+
8+export interface ConvexRequestMetadata {
9+ readonly headers?: Readonly<Record<string, string>>;
10+}
11+
12+export interface ConvexClientService {
13+ query<Query extends FunctionReference<"query">>(
14+ query: Query,
15+ args: Query["_args"],
16+ requestMetadata?: ConvexRequestMetadata,
17+ ): Effect.Effect<FunctionReturnType<Query>>;
18+
19+ mutation<Mutation extends FunctionReference<"mutation">>(
20+ mutation: Mutation,
21+ args: Mutation["_args"],
22+ requestMetadata?: ConvexRequestMetadata,
23+ ): Effect.Effect<FunctionReturnType<Mutation>>;
24+
25+ action<Action extends FunctionReference<"action">>(
26+ action: Action,
27+ args: Action["_args"],
28+ requestMetadata?: ConvexRequestMetadata,
29+ ): Effect.Effect<FunctionReturnType<Action>>;
30+
31+ subscribe<Query extends FunctionReference<"query">>(
32+ query: Query,
33+ args: Query["_args"],
34+ ): Stream.Stream<FunctionReturnType<Query>>;
35+}
36+
37+export class ConvexClient extends Context.Tag("@confect/ConvexClient")<
38+ ConvexClient,
39+ ConvexClientService
40+>() {}
41+
42+export const ConvexClientLayer = (
43+ url: string,
44+): Layer.Layer<ConvexClient> => {
45+ const wsClient = new ConvexClientImpl(url);
46+
47+ // Shared HTTP client instance — no custom headers that would trigger CORS
48+ // preflight failures in browsers. Telemetry context is already embedded
49+ // in the payload by `withOptionalRpcTelemetryContext`, so transport-level
50+ // headers are not needed for browser clients.
51+ const httpClient = new ConvexHttpClient(url);
52+
53+ const syncAuth = () => {
54+ const auth = wsClient.getAuth();
55+ if (auth !== undefined) {
56+ httpClient.setAuth(auth.token);
57+ }
58+ };
59+
60+ const service: ConvexClientService = {
61+ query: <Query extends FunctionReference<"query">>(
62+ query: Query,
63+ args: Query["_args"],
64+ _requestMetadata?: ConvexRequestMetadata,
65+ ): Effect.Effect<FunctionReturnType<Query>> =>
66+ Effect.promise(() => {
67+ syncAuth();
68+ return httpClient.query(query, args);
69+ }),
70+
71+ mutation: <Mutation extends FunctionReference<"mutation">>(
72+ mutation: Mutation,
73+ args: Mutation["_args"],
74+ _requestMetadata?: ConvexRequestMetadata,
75+ ): Effect.Effect<FunctionReturnType<Mutation>> =>
76+ Effect.promise(() => {
77+ syncAuth();
78+ return httpClient.mutation(mutation, args);
79+ }),
80+
81+ action: <Action extends FunctionReference<"action">>(
82+ action: Action,
83+ args: Action["_args"],
84+ _requestMetadata?: ConvexRequestMetadata,
85+ ): Effect.Effect<FunctionReturnType<Action>> =>
86+ Effect.promise(() => {
87+ syncAuth();
88+ return httpClient.action(action, args);
89+ }),
90+
91+ subscribe: <Query extends FunctionReference<"query">>(
92+ query: Query,
93+ args: Query["_args"],
94+ ): Stream.Stream<FunctionReturnType<Query>> =>
95+ Stream.async<FunctionReturnType<Query>>((emit) => {
96+ const unsubscribe = wsClient.onUpdate(query, args, (result) => {
97+ emit.single(result);
98+ });
99+ return Effect.sync(() => {
100+ unsubscribe();
101+ });
102+ }),
103+ };
104+
105+ return Layer.succeed(ConvexClient, service);
106+};
107+
108+/**
109+ * Server-only layer that uses only `ConvexHttpClient` (no WebSocket).
110+ * Safe for use in Next.js server components, route handlers, and server actions.
111+ * Subscriptions are not supported and will fail with a defect if called.
112+ */
113+export const ConvexHttpClientLayer = (
114+ url: string,
115+ config: {
116+ readonly authToken?: string;
117+ } = {},
118+): Layer.Layer<ConvexClient> => {
119+ const httpClient = new ConvexHttpClient(url);
120+ if (config.authToken !== undefined && config.authToken.length > 0) {
121+ httpClient.setAuth(config.authToken);
122+ }
123+
124+ const service: ConvexClientService = {
125+ query: <Query extends FunctionReference<"query">>(
126+ query: Query,
127+ args: Query["_args"],
128+ _requestMetadata?: ConvexRequestMetadata,
129+ ): Effect.Effect<FunctionReturnType<Query>> =>
130+ Effect.promise(() => httpClient.query(query, args)),
131+
132+ mutation: <Mutation extends FunctionReference<"mutation">>(
133+ mutation: Mutation,
134+ args: Mutation["_args"],
135+ _requestMetadata?: ConvexRequestMetadata,
136+ ): Effect.Effect<FunctionReturnType<Mutation>> =>
137+ Effect.promise(() => httpClient.mutation(mutation, args)),
138+
139+ action: <Action extends FunctionReference<"action">>(
140+ action: Action,
141+ args: Action["_args"],
142+ _requestMetadata?: ConvexRequestMetadata,
143+ ): Effect.Effect<FunctionReturnType<Action>> =>
144+ Effect.promise(() => httpClient.action(action, args)),
145+
146+ subscribe: <Query extends FunctionReference<"query">>(
147+ _query: Query,
148+ _args: Query["_args"],
149+ ): Stream.Stream<FunctionReturnType<Query>> =>
150+ Stream.fromEffect(
151+ Effect.die(
152+ new Error(
153+ "ConvexHttpClientLayer does not support subscriptions. Use ConvexClientLayer (with WebSocket) for real-time subscriptions.",
154+ ),
155+ ),
156+ ),
157+ };
158+
159+ return Layer.succeed(ConvexClient, service);
160+};
v2/packages/confect/src/ctx.ts+683−0
@@ -0,0 +1,683 @@
1+import type {
2+ FunctionReference,
3+ FunctionReturnType,
4+ GenericDatabaseReader,
5+ GenericDatabaseWriter,
6+ GenericMutationCtx,
7+ GenericQueryCtx,
8+ GenericActionCtx,
9+ OptionalRestArgs,
10+ PaginationOptions,
11+ PaginationResult,
12+ Expression,
13+ FilterBuilder,
14+ IndexRange,
15+ SearchFilter,
16+ GenericDataModel,
17+ GenericDocument,
18+ GenericTableInfo,
19+ WithoutSystemFields,
20+ Scheduler,
21+} from "convex/server";
22+import type { GenericId, Value } from "convex/values";
23+import * as Chunk from "effect/Chunk";
24+import * as Context from "effect/Context";
25+import * as Effect from "effect/Effect";
26+import { pipe } from "effect/Function";
27+import * as Option from "effect/Option";
28+import * as Schema from "effect/Schema";
29+import * as Stream from "effect/Stream";
30+
31+import type {
32+ GenericConfectSchema,
33+ TableNamesInSchema,
34+ DocumentFromTable,
35+ EncodedDocumentFromTable,
36+} from "./schema";
37+
38+export type {
39+ GenericConfectSchema,
40+ TableNamesInSchema,
41+ DocumentFromTable,
42+ EncodedDocumentFromTable,
43+};
44+
45+/**
46+ * A loosely-typed index range builder that allows chaining `.eq`, `.gt`, `.gte`, `.lt`, `.lte`
47+ * with string field names and `Value` values. This is the type exposed to Confect consumers since
48+ * Confect erases table-specific index type information.
49+ *
50+ * Unlike `IndexRangeBuilder<GenericDocument, string[], number>` (where `.eq()` resolves to plain
51+ * `IndexRange` due to tuple-length arithmetic on `string[]`), this interface is self-referential:
52+ * `.eq()` always returns `LooseIndexRangeBuilder`, allowing multi-field `.eq().eq()` chains.
53+ */
54+export interface LooseIndexRangeBuilder extends IndexRange {
55+ eq(fieldName: string, value: Value): LooseIndexRangeBuilder;
56+ gt(fieldName: string, value: Value): LooseIndexRangeBuilder;
57+ gte(fieldName: string, value: Value): LooseIndexRangeBuilder;
58+ lt(fieldName: string, value: Value): IndexRange;
59+ lte(fieldName: string, value: Value): IndexRange;
60+}
61+
62+type TableSchemas<Tables extends GenericConfectSchema> = {
63+ [TableName in TableNamesInSchema<Tables>]: Schema.Schema<
64+ DocumentFromTable<Tables, TableName>,
65+ EncodedDocumentFromTable<Tables, TableName>
66+ >;
67+};
68+
69+interface ConfectQuery<
70+ Tables extends GenericConfectSchema,
71+ TableName extends TableNamesInSchema<Tables>,
72+> {
73+ filter(
74+ predicate: (q: FilterBuilder<GenericTableInfo>) => Expression<boolean>,
75+ ): ConfectQuery<Tables, TableName>;
76+
77+ order(order: "asc" | "desc"): ConfectOrderedQuery<Tables, TableName>;
78+
79+ paginate(
80+ paginationOpts: PaginationOptions,
81+ ): Effect.Effect<PaginationResult<DocumentFromTable<Tables, TableName>>>;
82+
83+ collect(): Effect.Effect<Array<DocumentFromTable<Tables, TableName>>>;
84+
85+ take(n: number): Effect.Effect<Array<DocumentFromTable<Tables, TableName>>>;
86+
87+ first(): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>>;
88+
89+ unique(): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>, NotUniqueError>;
90+
91+ stream(): Stream.Stream<DocumentFromTable<Tables, TableName>>;
92+}
93+
94+interface ConfectOrderedQuery<
95+ Tables extends GenericConfectSchema,
96+ TableName extends TableNamesInSchema<Tables>,
97+> extends Omit<ConfectQuery<Tables, TableName>, "order"> {}
98+
99+export class NotUniqueError extends Schema.TaggedError<NotUniqueError>()(
100+ "NotUniqueError",
101+ {},
102+) {}
103+
104+interface QueryLike {
105+ [Symbol.asyncIterator](): AsyncIterator<Record<string, Value>>;
106+ filter(predicate: (q: FilterBuilder<GenericTableInfo>) => Expression<boolean>): QueryLike;
107+ order(order: "asc" | "desc"): QueryLike;
108+ paginate(opts: PaginationOptions): Promise<PaginationResult<Record<string, Value>>>;
109+}
110+
111+interface QueryInitializerLike extends QueryLike {
112+ fullTableScan(): QueryLike;
113+ withIndex(indexName: string, indexRange?: (q: LooseIndexRangeBuilder) => IndexRange): QueryLike;
114+ withSearchIndex(indexName: string, searchFilter: (q: unknown) => SearchFilter): QueryLike;
115+}
116+
117+class ConfectQueryImpl<
118+ Tables extends GenericConfectSchema,
119+ TableName extends TableNamesInSchema<Tables>,
120+> implements ConfectQuery<Tables, TableName>
121+{
122+ constructor(
123+ private readonly q: QueryLike,
124+ private readonly tableSchema: Schema.Schema<
125+ DocumentFromTable<Tables, TableName>,
126+ EncodedDocumentFromTable<Tables, TableName>
127+ >,
128+ private readonly tableName: TableName,
129+ ) {}
130+
131+ private decode(doc: Record<string, Value>): DocumentFromTable<Tables, TableName> {
132+ return Schema.decodeUnknownSync(this.tableSchema)(doc);
133+ }
134+
135+ filter(
136+ predicate: (q: FilterBuilder<GenericTableInfo>) => Expression<boolean>,
137+ ): ConfectQuery<Tables, TableName> {
138+ return new ConfectQueryImpl(
139+ this.q.filter(predicate),
140+ this.tableSchema,
141+ this.tableName,
142+ );
143+ }
144+
145+ order(order: "asc" | "desc"): ConfectOrderedQuery<Tables, TableName> {
146+ return new ConfectQueryImpl(
147+ this.q.order(order),
148+ this.tableSchema,
149+ this.tableName,
150+ );
151+ }
152+
153+ paginate(
154+ paginationOpts: PaginationOptions,
155+ ): Effect.Effect<PaginationResult<DocumentFromTable<Tables, TableName>>> {
156+ return pipe(
157+ Effect.promise(() => this.q.paginate(paginationOpts)),
158+ Effect.map((result) => ({
159+ ...result,
160+ page: result.page.map((doc) => this.decode(doc)),
161+ })),
162+ );
163+ }
164+
165+ collect(): Effect.Effect<Array<DocumentFromTable<Tables, TableName>>> {
166+ return pipe(
167+ this.stream(),
168+ Stream.runCollect,
169+ Effect.map((chunk) => Chunk.toArray(chunk)),
170+ );
171+ }
172+
173+ take(n: number): Effect.Effect<Array<DocumentFromTable<Tables, TableName>>> {
174+ return pipe(
175+ this.stream(),
176+ Stream.take(n),
177+ Stream.runCollect,
178+ Effect.map((chunk) => Chunk.toArray(chunk)),
179+ );
180+ }
181+
182+ first(): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>> {
183+ return pipe(this.stream(), Stream.runHead);
184+ }
185+
186+ unique(): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>, NotUniqueError> {
187+ return pipe(
188+ this.stream(),
189+ Stream.take(2),
190+ Stream.runCollect,
191+ Effect.flatMap((chunk) => {
192+ if (Chunk.size(chunk) > 1) {
193+ return Effect.fail(new NotUniqueError());
194+ }
195+ return Effect.succeed(Chunk.get(chunk, 0));
196+ }),
197+ );
198+ }
199+
200+ stream(): Stream.Stream<DocumentFromTable<Tables, TableName>> {
201+ return pipe(
202+ Stream.fromAsyncIterable(this.q, (e) => e as Error),
203+ Stream.map((doc) => this.decode(doc)),
204+ Stream.orDie,
205+ );
206+ }
207+}
208+
209+interface ConfectQueryInitializer<
210+ Tables extends GenericConfectSchema,
211+ TableName extends TableNamesInSchema<Tables>,
212+> extends ConfectQuery<Tables, TableName> {
213+ fullTableScan(): ConfectQuery<Tables, TableName>;
214+
215+ withIndex<IndexName extends string>(
216+ indexName: IndexName,
217+ indexRange?: (q: LooseIndexRangeBuilder) => IndexRange,
218+ ): ConfectQuery<Tables, TableName>;
219+
220+ withSearchIndex<IndexName extends string>(
221+ indexName: IndexName,
222+ searchFilter: (q: unknown) => SearchFilter,
223+ ): ConfectOrderedQuery<Tables, TableName>;
224+}
225+
226+class ConfectQueryInitializerImpl<
227+ Tables extends GenericConfectSchema,
228+ TableName extends TableNamesInSchema<Tables>,
229+> implements ConfectQueryInitializer<Tables, TableName>
230+{
231+ constructor(
232+ private readonly q: QueryInitializerLike,
233+ private readonly tableSchema: Schema.Schema<
234+ DocumentFromTable<Tables, TableName>,
235+ EncodedDocumentFromTable<Tables, TableName>
236+ >,
237+ private readonly tableName: TableName,
238+ ) {}
239+
240+ fullTableScan(): ConfectQuery<Tables, TableName> {
241+ return new ConfectQueryImpl(
242+ this.q.fullTableScan(),
243+ this.tableSchema,
244+ this.tableName,
245+ );
246+ }
247+
248+ withIndex<IndexName extends string>(
249+ indexName: IndexName,
250+ indexRange?: (q: LooseIndexRangeBuilder) => IndexRange,
251+ ): ConfectQuery<Tables, TableName> {
252+ return new ConfectQueryImpl(
253+ this.q.withIndex(indexName, indexRange),
254+ this.tableSchema,
255+ this.tableName,
256+ );
257+ }
258+
259+ withSearchIndex<IndexName extends string>(
260+ indexName: IndexName,
261+ searchFilter: (q: unknown) => SearchFilter,
262+ ): ConfectOrderedQuery<Tables, TableName> {
263+ return new ConfectQueryImpl(
264+ this.q.withSearchIndex(indexName, searchFilter),
265+ this.tableSchema,
266+ this.tableName,
267+ );
268+ }
269+
270+ filter(
271+ predicate: (q: FilterBuilder<GenericTableInfo>) => Expression<boolean>,
272+ ): ConfectQuery<Tables, TableName> {
273+ return this.fullTableScan().filter(predicate);
274+ }
275+
276+ order(order: "asc" | "desc"): ConfectOrderedQuery<Tables, TableName> {
277+ return this.fullTableScan().order(order);
278+ }
279+
280+ paginate(
281+ paginationOpts: PaginationOptions,
282+ ): Effect.Effect<PaginationResult<DocumentFromTable<Tables, TableName>>> {
283+ return this.fullTableScan().paginate(paginationOpts);
284+ }
285+
286+ collect(): Effect.Effect<Array<DocumentFromTable<Tables, TableName>>> {
287+ return this.fullTableScan().collect();
288+ }
289+
290+ take(n: number): Effect.Effect<Array<DocumentFromTable<Tables, TableName>>> {
291+ return this.fullTableScan().take(n);
292+ }
293+
294+ first(): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>> {
295+ return this.fullTableScan().first();
296+ }
297+
298+ unique(): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>, NotUniqueError> {
299+ return this.fullTableScan().unique();
300+ }
301+
302+ stream(): Stream.Stream<DocumentFromTable<Tables, TableName>> {
303+ return this.fullTableScan().stream();
304+ }
305+}
306+
307+export interface ConfectDatabaseReader<Tables extends GenericConfectSchema> {
308+ query<TableName extends TableNamesInSchema<Tables>>(
309+ tableName: TableName,
310+ ): ConfectQueryInitializer<Tables, TableName>;
311+
312+ get<TableName extends TableNamesInSchema<Tables>>(
313+ id: GenericId<TableName>,
314+ ): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>>;
315+
316+ normalizeId<TableName extends TableNamesInSchema<Tables>>(
317+ tableName: TableName,
318+ id: string,
319+ ): Option.Option<GenericId<TableName>>;
320+}
321+
322+export interface ConfectDatabaseWriter<Tables extends GenericConfectSchema>
323+ extends ConfectDatabaseReader<Tables> {
324+ insert<TableName extends TableNamesInSchema<Tables>>(
325+ table: TableName,
326+ value: WithoutSystemFields<DocumentFromTable<Tables, TableName>>,
327+ ): Effect.Effect<GenericId<TableName>>;
328+
329+ patch<TableName extends TableNamesInSchema<Tables>>(
330+ id: GenericId<TableName>,
331+ value: Partial<WithoutSystemFields<DocumentFromTable<Tables, TableName>>>,
332+ ): Effect.Effect<void>;
333+
334+ replace<TableName extends TableNamesInSchema<Tables>>(
335+ id: GenericId<TableName>,
336+ value: WithoutSystemFields<DocumentFromTable<Tables, TableName>>,
337+ ): Effect.Effect<void>;
338+
339+ delete(id: GenericId<string>): Effect.Effect<void>;
340+}
341+
342+class ConfectDatabaseReaderImpl<Tables extends GenericConfectSchema>
343+ implements ConfectDatabaseReader<Tables>
344+{
345+ constructor(
346+ protected readonly db: GenericDatabaseReader<GenericDataModel>,
347+ protected readonly tableSchemas: TableSchemas<Tables>,
348+ ) {}
349+
350+ protected decode<TableName extends TableNamesInSchema<Tables>>(
351+ tableName: TableName,
352+ doc: Record<string, Value>,
353+ ): DocumentFromTable<Tables, TableName> {
354+ return Schema.decodeUnknownSync(this.tableSchemas[tableName])(doc);
355+ }
356+
357+ query<TableName extends TableNamesInSchema<Tables>>(
358+ tableName: TableName,
359+ ): ConfectQueryInitializer<Tables, TableName> {
360+ return new ConfectQueryInitializerImpl(
361+ this.db.query(tableName) as unknown as QueryInitializerLike,
362+ this.tableSchemas[tableName],
363+ tableName,
364+ );
365+ }
366+
367+ get<TableName extends TableNamesInSchema<Tables>>(
368+ id: GenericId<TableName>,
369+ ): Effect.Effect<Option.Option<DocumentFromTable<Tables, TableName>>> {
370+ return pipe(
371+ Effect.promise(() => this.db.get(id)),
372+ Effect.map((doc) => {
373+ if (doc === null) {
374+ return Option.none<DocumentFromTable<Tables, TableName>>();
375+ }
376+ const tableName = this.getTableNameFromId(id);
377+ if (Option.isNone(tableName)) {
378+ return Option.none<DocumentFromTable<Tables, TableName>>();
379+ }
380+ return Option.some(this.decode(tableName.value, doc as Record<string, Value>));
381+ }),
382+ );
383+ }
384+
385+ normalizeId<TableName extends TableNamesInSchema<Tables>>(
386+ tableName: TableName,
387+ id: string,
388+ ): Option.Option<GenericId<TableName>> {
389+ const normalized = this.db.normalizeId(tableName, id);
390+ return Option.fromNullable(normalized);
391+ }
392+
393+ private getTableNameFromId<TableName extends TableNamesInSchema<Tables>>(
394+ id: GenericId<TableName>,
395+ ): Option.Option<TableName> {
396+ for (const tableName of Object.keys(this.tableSchemas)) {
397+ const normalized = this.db.normalizeId(tableName, id);
398+ if (normalized !== null) {
399+ return Option.some(tableName as TableName);
400+ }
401+ }
402+ return Option.none();
403+ }
404+}
405+
406+class ConfectDatabaseWriterImpl<Tables extends GenericConfectSchema>
407+ extends ConfectDatabaseReaderImpl<Tables>
408+ implements ConfectDatabaseWriter<Tables>
409+{
410+ constructor(
411+ protected override readonly db: GenericDatabaseWriter<GenericDataModel>,
412+ tableSchemas: TableSchemas<Tables>,
413+ ) {
414+ super(db, tableSchemas);
415+ }
416+
417+ insert<TableName extends TableNamesInSchema<Tables>>(
418+ table: TableName,
419+ value: WithoutSystemFields<DocumentFromTable<Tables, TableName>>,
420+ ): Effect.Effect<GenericId<TableName>> {
421+ return Effect.promise(() =>
422+ (this.db as GenericDatabaseWriter<GenericDataModel>).insert(
423+ table,
424+ value as Record<string, Value>,
425+ ),
426+ ) as Effect.Effect<GenericId<TableName>>;
427+ }
428+
429+ patch<TableName extends TableNamesInSchema<Tables>>(
430+ id: GenericId<TableName>,
431+ value: Partial<WithoutSystemFields<DocumentFromTable<Tables, TableName>>>,
432+ ): Effect.Effect<void> {
433+ return Effect.promise(() =>
434+ (this.db as GenericDatabaseWriter<GenericDataModel>).patch(
435+ id,
436+ value as Record<string, Value>,
437+ ),
438+ );
439+ }
440+
441+ replace<TableName extends TableNamesInSchema<Tables>>(
442+ id: GenericId<TableName>,
443+ value: WithoutSystemFields<DocumentFromTable<Tables, TableName>>,
444+ ): Effect.Effect<void> {
445+ return Effect.promise(() =>
446+ (this.db as GenericDatabaseWriter<GenericDataModel>).replace(
447+ id,
448+ value as Record<string, Value>,
449+ ),
450+ );
451+ }
452+
453+ delete(id: GenericId<string>): Effect.Effect<void> {
454+ return Effect.promise(() =>
455+ (this.db as GenericDatabaseWriter<GenericDataModel>).delete(id),
456+ );
457+ }
458+}
459+
460+export interface ConfectAuth {
461+ getUserIdentity(): Effect.Effect<Option.Option<{
462+ tokenIdentifier: string;
463+ subject: string;
464+ issuer: string;
465+ [key: string]: unknown;
466+ }>>;
467+}
468+
469+class ConfectAuthImpl implements ConfectAuth {
470+ constructor(private readonly auth: { getUserIdentity: () => Promise<unknown> }) {}
471+
472+ getUserIdentity(): Effect.Effect<Option.Option<{
473+ tokenIdentifier: string;
474+ subject: string;
475+ issuer: string;
476+ [key: string]: unknown;
477+ }>> {
478+ return pipe(
479+ Effect.promise(() => this.auth.getUserIdentity()),
480+ Effect.map((identity) =>
481+ identity
482+ ? Option.some(
483+ identity as {
484+ tokenIdentifier: string;
485+ subject: string;
486+ issuer: string;
487+ [key: string]: unknown;
488+ },
489+ )
490+ : Option.none(),
491+ ),
492+ );
493+ }
494+}
495+
496+/**
497+ * Unwrap `ExitEncoded<A, E, D>` to just `A` at the type level.
498+ *
499+ * Confect-registered functions return `ExitEncoded` on the wire, so
500+ * `FunctionReturnType<Ref>` resolves to `ExitEncoded<A, E, D>`.
501+ * This utility type extracts the success value `A` so that
502+ * `yield* ctx.runQuery(...)` gives you the logical return type.
503+ *
504+ * For non-Confect functions (plain Convex), the type passes through unchanged.
505+ */
506+type UnwrapExitEncoded<T> =
507+ // Use Extract to isolate the Success branch of the ExitEncoded union
508+ Extract<T, { readonly _tag: "Success" }> extends {
509+ readonly _tag: "Success";
510+ readonly value: infer A;
511+ }
512+ ? A
513+ : T;
514+
515+export interface ConfectQueryCtx<Tables extends GenericConfectSchema> {
516+ runQuery<Query extends FunctionReference<"query", "public" | "internal">>(
517+ query: Query,
518+ ...args: OptionalRestArgs<Query>
519+ ): Effect.Effect<UnwrapExitEncoded<FunctionReturnType<Query>>>;
520+
521+ db: ConfectDatabaseReader<Tables>;
522+ auth: ConfectAuth;
523+
524+ /**
525+ * The raw Convex query context, useful for passing to Convex components
526+ * (e.g. `@convex-dev/aggregate`) that expect standard Convex context objects.
527+ */
528+ rawCtx: GenericQueryCtx<GenericDataModel>;
529+}
530+
531+export interface ConfectMutationCtx<Tables extends GenericConfectSchema> {
532+ runQuery<Query extends FunctionReference<"query", "public" | "internal">>(
533+ query: Query,
534+ ...args: OptionalRestArgs<Query>
535+ ): Effect.Effect<UnwrapExitEncoded<FunctionReturnType<Query>>>;
536+
537+ runMutation<Mutation extends FunctionReference<"mutation", "public" | "internal">>(
538+ mutation: Mutation,
539+ ...args: OptionalRestArgs<Mutation>
540+ ): Effect.Effect<UnwrapExitEncoded<FunctionReturnType<Mutation>>>;
541+
542+ /** Raw Convex scheduler for scheduling functions from mutations. */
543+ scheduler: Scheduler;
544+
545+ db: ConfectDatabaseWriter<Tables>;
546+ auth: ConfectAuth;
547+
548+ /**
549+ * The raw Convex mutation context, useful for passing to Convex components
550+ * (e.g. `@convex-dev/aggregate`) that expect standard Convex context objects.
551+ */
552+ rawCtx: GenericMutationCtx<GenericDataModel>;
553+}
554+
555+export interface ConfectActionCtx<Tables extends GenericConfectSchema> {
556+ runQuery<Query extends FunctionReference<"query", "public" | "internal">>(
557+ query: Query,
558+ ...args: OptionalRestArgs<Query>
559+ ): Effect.Effect<UnwrapExitEncoded<FunctionReturnType<Query>>>;
560+
561+ runMutation<Mutation extends FunctionReference<"mutation", "public" | "internal">>(
562+ mutation: Mutation,
563+ ...args: OptionalRestArgs<Mutation>
564+ ): Effect.Effect<UnwrapExitEncoded<FunctionReturnType<Mutation>>>;
565+
566+ runAction<Action extends FunctionReference<"action", "public" | "internal">>(
567+ action: Action,
568+ ...args: OptionalRestArgs<Action>
569+ ): Effect.Effect<UnwrapExitEncoded<FunctionReturnType<Action>>>;
570+
571+ scheduler: Scheduler;
572+ auth: ConfectAuth;
573+}
574+
575+export const ConfectQueryCtx = <Tables extends GenericConfectSchema>() =>
576+ Context.GenericTag<ConfectQueryCtx<Tables>>("@confect/ConfectQueryCtx");
577+
578+export const ConfectMutationCtx = <Tables extends GenericConfectSchema>() =>
579+ Context.GenericTag<ConfectMutationCtx<Tables>>("@confect/ConfectMutationCtx");
580+
581+export const ConfectActionCtx = <Tables extends GenericConfectSchema>() =>
582+ Context.GenericTag<ConfectActionCtx<Tables>>("@confect/ConfectActionCtx");
583+
584+export const makeQueryCtx = <Tables extends GenericConfectSchema>(
585+ ctx: GenericQueryCtx<GenericDataModel>,
586+ tableSchemas: TableSchemas<Tables>,
587+): ConfectQueryCtx<Tables> => ({
588+ runQuery: <Query extends FunctionReference<"query", "public" | "internal">>(
589+ query: Query,
590+ ...args: OptionalRestArgs<Query>
591+ ) =>
592+ Effect.promise(() => ctx.runQuery(query, ...args)).pipe(
593+ Effect.flatMap(unwrapIfExitEncoded),
594+ ),
595+ db: new ConfectDatabaseReaderImpl(ctx.db, tableSchemas),
596+ auth: new ConfectAuthImpl(ctx.auth),
597+ rawCtx: ctx,
598+});
599+
600+export const makeMutationCtx = <Tables extends GenericConfectSchema>(
601+ ctx: GenericMutationCtx<GenericDataModel>,
602+ tableSchemas: TableSchemas<Tables>,
603+): ConfectMutationCtx<Tables> => ({
604+ runQuery: <Query extends FunctionReference<"query", "public" | "internal">>(
605+ query: Query,
606+ ...args: OptionalRestArgs<Query>
607+ ) =>
608+ Effect.promise(() => ctx.runQuery(query, ...args)).pipe(
609+ Effect.flatMap(unwrapIfExitEncoded),
610+ ),
611+ runMutation: <Mutation extends FunctionReference<"mutation", "public" | "internal">>(
612+ mutation: Mutation,
613+ ...args: OptionalRestArgs<Mutation>
614+ ) =>
615+ Effect.promise(() => ctx.runMutation(mutation, ...args)).pipe(
616+ Effect.flatMap(unwrapIfExitEncoded),
617+ ),
618+ scheduler: ctx.scheduler,
619+ db: new ConfectDatabaseWriterImpl(ctx.db, tableSchemas),
620+ auth: new ConfectAuthImpl(ctx.auth),
621+ rawCtx: ctx,
622+});
623+
624+/**
625+ * Detect and unwrap Confect ExitEncoded results from cross-function calls.
626+ *
627+ * When a Confect action/mutation/query calls another Confect function via
628+ * `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction`, the raw Convex runtime
629+ * returns the function's wire format — which for Confect functions is
630+ * `ExitEncoded<Success, Error>` (a tagged union `{_tag: "Success", value}` |
631+ * `{_tag: "Failure", cause}`).
632+ *
633+ * Without unwrapping, `yield* ctx.runQuery(...)` gives you the raw ExitEncoded
634+ * object instead of the logical success value, forcing users to manually check
635+ * `_tag` and extract `.value`. This helper makes cross-function calls
636+ * transparent: Success values are extracted, Failure causes become Effect failures.
637+ */
638+const unwrapIfExitEncoded = <T>(result: T): Effect.Effect<T> => {
639+ if (
640+ typeof result === "object" &&
641+ result !== null &&
642+ "_tag" in result
643+ ) {
644+ const tagged = result as { _tag: string; value?: unknown; cause?: unknown };
645+ if (tagged._tag === "Success") {
646+ return Effect.succeed(tagged.value as T);
647+ }
648+ if (tagged._tag === "Failure") {
649+ // Re-throw as an Effect failure so the caller's error channel catches it
650+ return Effect.die(tagged.cause);
651+ }
652+ }
653+ // Not an ExitEncoded — return as-is (e.g. plain Convex functions)
654+ return Effect.succeed(result);
655+};
656+
657+export const makeActionCtx = <Tables extends GenericConfectSchema>(
658+ ctx: GenericActionCtx<GenericDataModel>,
659+): ConfectActionCtx<Tables> => ({
660+ runQuery: <Query extends FunctionReference<"query", "public" | "internal">>(
661+ query: Query,
662+ ...args: OptionalRestArgs<Query>
663+ ) =>
664+ Effect.promise(() => ctx.runQuery(query, ...args)).pipe(
665+ Effect.flatMap(unwrapIfExitEncoded),
666+ ),
667+ runMutation: <Mutation extends FunctionReference<"mutation", "public" | "internal">>(
668+ mutation: Mutation,
669+ ...args: OptionalRestArgs<Mutation>
670+ ) =>
671+ Effect.promise(() => ctx.runMutation(mutation, ...args)).pipe(
672+ Effect.flatMap(unwrapIfExitEncoded),
673+ ),
674+ runAction: <Action extends FunctionReference<"action", "public" | "internal">>(
675+ action: Action,
676+ ...args: OptionalRestArgs<Action>
677+ ) =>
678+ Effect.promise(() => ctx.runAction(action, ...args)).pipe(
679+ Effect.flatMap(unwrapIfExitEncoded),
680+ ),
681+ scheduler: ctx.scheduler,
682+ auth: new ConfectAuthImpl(ctx.auth),
683+});
v2/packages/confect/src/index.ts+43−0
@@ -0,0 +1,43 @@
1+export {
2+ defineSchema,
3+ defineTable,
4+ type ConfectSchemaDefinition,
5+ type ConfectTableDefinition,
6+ type GenericConfectSchema,
7+ type TableNamesInSchema,
8+ type DocumentFromTable,
9+ type EncodedDocumentFromTable,
10+ type ConfectDataModel,
11+ type TablesFromSchemaDefinition,
12+} from "./schema";
13+
14+export {
15+ schemaToValidator,
16+ schemaToObjectValidator,
17+ Id,
18+ UnsupportedSchemaError,
19+ TopLevelMustBeObjectError,
20+ IndexSignaturesNotSupportedError,
21+} from "./validators";
22+
23+export {
24+ NotUniqueError,
25+ ConfectQueryCtx,
26+ ConfectMutationCtx,
27+ ConfectActionCtx,
28+ makeQueryCtx,
29+ makeMutationCtx,
30+ makeActionCtx,
31+ type ConfectDatabaseReader,
32+ type ConfectDatabaseWriter,
33+ type ConfectAuth,
34+ type LooseIndexRangeBuilder,
35+} from "./ctx";
36+
37+export { ConvexClient, ConvexClientLayer, ConvexHttpClientLayer, type ConvexClientService } from "./client";
38+
39+export {
40+ Cursor,
41+ PaginationOptionsSchema,
42+ PaginationResultSchema,
43+} from "./pagination";
v2/packages/confect/src/pagination.ts+19−0
@@ -0,0 +1,19 @@
1+import * as Schema from "effect/Schema";
2+
3+export const Cursor = Schema.String.pipe(Schema.brand("Cursor"));
4+export type Cursor = typeof Cursor.Type;
5+
6+export const PaginationOptionsSchema = Schema.Struct({
7+ cursor: Schema.NullOr(Cursor),
8+ numItems: Schema.Number,
9+});
10+export type PaginationOptionsSchema = typeof PaginationOptionsSchema.Type;
11+
12+export const PaginationResultSchema = <Item extends Schema.Schema.AnyNoContext>(
13+ item: Item,
14+) =>
15+ Schema.Struct({
16+ page: Schema.Array(item),
17+ isDone: Schema.Boolean,
18+ continueCursor: Cursor,
19+ });
v2/packages/confect/src/rpc/client.test.ts+776−0
@@ -0,0 +1,776 @@
1+import { describe, expect, it } from "vitest";
2+import { Effect, Layer, Option, Schema, Stream } from "effect";
3+import { Atom, Registry, Result } from "@effect-atom/atom";
4+import { createRpcClient, createServerRpcQuery, RpcDefectError } from "./client";
5+import { createRpcFactory, makeRpcModule } from "./server";
6+import { defineTable, defineSchema } from "../schema";
7+import { ConvexClient, type ConvexClientService } from "../client";
8+
9+const testSchema = defineSchema({
10+ guestbook: defineTable(
11+ Schema.Struct({
12+ author: Schema.String,
13+ message: Schema.String,
14+ }),
15+ ),
16+});
17+
18+const factory = createRpcFactory({ schema: testSchema });
19+
20+// Define endpoints
21+const addEndpoint = factory.mutation({
22+ payload: { author: Schema.String, message: Schema.String },
23+ success: Schema.String,
24+});
25+addEndpoint.implement((payload) =>
26+ Effect.gen(function* () {
27+ return `Added message from ${payload.author}`;
28+ }),
29+);
30+
31+const listEndpoint = factory.query({
32+ success: Schema.Array(Schema.Struct({ author: Schema.String, message: Schema.String })),
33+});
34+listEndpoint.implement(() =>
35+ Effect.gen(function* () {
36+ return [{ author: "Alice", message: "Hello" }];
37+ }),
38+);
39+
40+const getEndpoint = factory.query({
41+ payload: { id: Schema.String },
42+ success: Schema.Struct({ author: Schema.String, message: Schema.String }),
43+ error: Schema.Struct({ _tag: Schema.Literal("NotFound") }),
44+});
45+getEndpoint.implement((payload) =>
46+ Effect.gen(function* () {
47+ if (payload.id === "not-found") {
48+ return yield* Effect.fail({ _tag: "NotFound" as const });
49+ }
50+ return { author: "Bob", message: "Test" };
51+ }),
52+);
53+
54+const sendNotificationEndpoint = factory.action({
55+ payload: { userId: Schema.String },
56+ success: Schema.Struct({ sent: Schema.Boolean }),
57+});
58+sendNotificationEndpoint.implement((_payload) =>
59+ Effect.gen(function* () {
60+ return { sent: true };
61+ }),
62+);
63+
64+const guestbookModule = makeRpcModule({
65+ add: addEndpoint,
66+ list: listEndpoint,
67+ get: getEndpoint,
68+ sendNotification: sendNotificationEndpoint,
69+});
70+
71+describe("RPC Client", () => {
72+ describe("createRpcClient", () => {
73+ it("creates client with runtime property", () => {
74+ const mockApi = {
75+ add: guestbookModule.handlers.add,
76+ list: guestbookModule.handlers.list,
77+ get: guestbookModule.handlers.get,
78+ sendNotification: guestbookModule.handlers.sendNotification,
79+ };
80+
81+ const client = createRpcClient<typeof guestbookModule>(
82+ mockApi as never,
83+ { url: "https://test.convex.cloud" },
84+ );
85+
86+ expect(client.runtime).toBeDefined();
87+ expect(typeof client.runtime.atom).toBe("function");
88+ expect(typeof client.runtime.fn).toBe("function");
89+ expect(typeof client.runtime.pull).toBe("function");
90+ });
91+
92+ it("creates typed endpoint proxies for each module endpoint", () => {
93+ const mockApi = {
94+ add: guestbookModule.handlers.add,
95+ list: guestbookModule.handlers.list,
96+ get: guestbookModule.handlers.get,
97+ sendNotification: guestbookModule.handlers.sendNotification,
98+ };
99+
100+ const client = createRpcClient<typeof guestbookModule>(
101+ mockApi as never,
102+ { url: "https://test.convex.cloud" },
103+ );
104+
105+ expect(client.add).toBeDefined();
106+ expect(client.list).toBeDefined();
107+ expect(client.get).toBeDefined();
108+ expect(client.sendNotification).toBeDefined();
109+ });
110+
111+ it("mutation endpoints expose mutate AtomResultFn", () => {
112+ const mockApi = { add: guestbookModule.handlers.add };
113+ const client = createRpcClient<typeof guestbookModule>(
114+ mockApi as never,
115+ { url: "https://test.convex.cloud" },
116+ );
117+
118+ expect(client.add.mutate).toBeDefined();
119+ expect(typeof client.add.mutate).toBe("object");
120+ });
121+
122+ it("query endpoints expose query and subscription functions", () => {
123+ const mockApi = { list: guestbookModule.handlers.list };
124+ const client = createRpcClient<typeof guestbookModule>(
125+ mockApi as never,
126+ { url: "https://test.convex.cloud" },
127+ );
128+
129+ expect(typeof client.list.query).toBe("function");
130+ expect(typeof client.list.queryEffect).toBe("function");
131+ expect(typeof client.list.queryPromise).toBe("function");
132+ expect(typeof client.list.subscription).toBe("function");
133+ });
134+
135+ it("action endpoints expose call AtomResultFn", () => {
136+ const mockApi = { sendNotification: guestbookModule.handlers.sendNotification };
137+ const client = createRpcClient<typeof guestbookModule>(
138+ mockApi as never,
139+ { url: "https://test.convex.cloud" },
140+ );
141+
142+ expect(client.sendNotification.call).toBeDefined();
143+ expect(typeof client.sendNotification.callAsQuery).toBe("function");
144+ expect(typeof client.sendNotification.call).toBe("object");
145+ expect(typeof client.sendNotification.callEffect).toBe("function");
146+ expect(typeof client.sendNotification.callPromise).toBe("function");
147+ });
148+
149+ it("action callAsQuery returns atom for given payload", () => {
150+ const mockApi = { sendNotification: guestbookModule.handlers.sendNotification };
151+ const client = createRpcClient<typeof guestbookModule>(
152+ mockApi as never,
153+ { url: "https://test.convex.cloud" },
154+ );
155+
156+ const atom = client.sendNotification.callAsQuery({ userId: "user-1" });
157+ expect(atom).toBeDefined();
158+ expect(Atom.isAtom(atom)).toBe(true);
159+ });
160+
161+ it("mutation endpoints expose imperative mutateEffect and mutatePromise", () => {
162+ const mockApi = { add: guestbookModule.handlers.add };
163+ const client = createRpcClient<typeof guestbookModule>(
164+ mockApi as never,
165+ { url: "https://test.convex.cloud" },
166+ );
167+
168+ expect(typeof client.add.mutateEffect).toBe("function");
169+ expect(typeof client.add.mutatePromise).toBe("function");
170+ });
171+
172+ it("caches endpoint proxies (same reference on repeated access)", () => {
173+ const mockApi = { add: guestbookModule.handlers.add };
174+ const client = createRpcClient<typeof guestbookModule>(
175+ mockApi as never,
176+ { url: "https://test.convex.cloud" },
177+ );
178+
179+ const first = client.add;
180+ const second = client.add;
181+ expect(first).toBe(second);
182+ });
183+
184+ it("query function returns atom for given payload", () => {
185+ const mockApi = { get: guestbookModule.handlers.get };
186+ const client = createRpcClient<typeof guestbookModule>(
187+ mockApi as never,
188+ { url: "https://test.convex.cloud" },
189+ );
190+
191+ const atom = client.get.query({ id: "123" });
192+ expect(atom).toBeDefined();
193+ expect(Atom.isAtom(atom)).toBe(true);
194+ });
195+
196+ it("subscription function returns atom for given payload", () => {
197+ const mockApi = { list: guestbookModule.handlers.list };
198+ const client = createRpcClient<typeof guestbookModule>(
199+ mockApi as never,
200+ { url: "https://test.convex.cloud" },
201+ );
202+
203+ const atom = client.list.subscription({});
204+ expect(atom).toBeDefined();
205+ expect(Atom.isAtom(atom)).toBe(true);
206+ });
207+
208+ it("query can be disabled until caller is ready", () => {
209+ let queryCalls = 0;
210+ const service: ConvexClientService = {
211+ query: () => {
212+ queryCalls += 1;
213+ return Effect.die("query should be disabled");
214+ },
215+ mutation: () => Effect.die("not implemented"),
216+ action: () => Effect.die("not implemented"),
217+ subscribe: () => Stream.empty,
218+ };
219+ const layer = Layer.succeed(ConvexClient, service);
220+
221+ const mockApi = { list: guestbookModule.handlers.list };
222+ const client = createRpcClient<typeof guestbookModule>(
223+ mockApi as never,
224+ { url: "https://test.convex.cloud", layer },
225+ );
226+
227+ const atom = client.list.query({}, { enabled: false });
228+ const registry = Registry.make();
229+ const value = registry.get(atom);
230+
231+ expect(Result.isInitial(value)).toBe(true);
232+ expect(queryCalls).toBe(0);
233+
234+ const unmount = registry.mount(atom);
235+ expect(queryCalls).toBe(0);
236+ unmount();
237+ registry.dispose();
238+ });
239+
240+ it("subscription can be disabled until caller is ready", () => {
241+ let subscribeCalls = 0;
242+ const service: ConvexClientService = {
243+ query: () => Effect.die("not implemented"),
244+ mutation: () => Effect.die("not implemented"),
245+ action: () => Effect.die("not implemented"),
246+ subscribe: () => {
247+ subscribeCalls += 1;
248+ return Stream.empty;
249+ },
250+ };
251+ const layer = Layer.succeed(ConvexClient, service);
252+
253+ const mockApi = { list: guestbookModule.handlers.list };
254+ const client = createRpcClient<typeof guestbookModule>(
255+ mockApi as never,
256+ { url: "https://test.convex.cloud", layer },
257+ );
258+
259+ const atom = client.list.subscription({}, { enabled: false });
260+ const registry = Registry.make();
261+ const value = registry.get(atom);
262+
263+ expect(Result.isInitial(value)).toBe(true);
264+ expect(subscribeCalls).toBe(0);
265+
266+ const unmount = registry.mount(atom);
267+ expect(subscribeCalls).toBe(0);
268+ unmount();
269+ registry.dispose();
270+ });
271+ });
272+
273+ describe("RpcDefectError", () => {
274+ it("has correct _tag", () => {
275+ const error = new RpcDefectError({ defect: "test" });
276+ expect(error._tag).toBe("RpcDefectError");
277+ });
278+
279+ it("stores string defect", () => {
280+ const error = new RpcDefectError({ defect: "Unexpected server crash" });
281+ expect(error.defect).toBe("Unexpected server crash");
282+ });
283+
284+ it("stores complex object defect", () => {
285+ const complexDefect = {
286+ code: "INTERNAL_ERROR",
287+ message: "Database connection failed",
288+ stack: "Error at line 42...",
289+ metadata: { requestId: "abc123" },
290+ };
291+ const error = new RpcDefectError({ defect: complexDefect });
292+ expect(error.defect).toEqual(complexDefect);
293+ });
294+
295+ it("is instanceof Error", () => {
296+ const error = new RpcDefectError({ defect: "test" });
297+ expect(error).toBeInstanceOf(Error);
298+ });
299+ });
300+
301+ describe("type safety (compile-time checks)", () => {
302+ it("client.add.mutate is typed as AtomResultFn", () => {
303+ const mockApi = { add: guestbookModule.handlers.add };
304+ const client = createRpcClient<typeof guestbookModule>(
305+ mockApi as never,
306+ { url: "https://test.convex.cloud" },
307+ );
308+
309+ expect(client.add.mutate).toBeDefined();
310+ expect(Atom.isAtom(client.add.mutate)).toBe(true);
311+ });
312+
313+ it("client.get.query payload is typed from module definition", () => {
314+ const mockApi = { get: guestbookModule.handlers.get };
315+ const client = createRpcClient<typeof guestbookModule>(
316+ mockApi as never,
317+ { url: "https://test.convex.cloud" },
318+ );
319+
320+ const atom = client.get.query({ id: "123" });
321+ expect(atom).toBeDefined();
322+ });
323+
324+ it("client.sendNotification.call is typed as AtomResultFn", () => {
325+ const mockApi = { sendNotification: guestbookModule.handlers.sendNotification };
326+ const client = createRpcClient<typeof guestbookModule>(
327+ mockApi as never,
328+ { url: "https://test.convex.cloud" },
329+ );
330+
331+ expect(client.sendNotification.call).toBeDefined();
332+ expect(Atom.isAtom(client.sendNotification.call)).toBe(true);
333+ });
334+ });
335+});
336+
337+
338+
339+// ---------------------------------------------------------------------------
340+// Error round-trip helpers (server encode → client decode)
341+// ---------------------------------------------------------------------------
342+
343+import { Cause, Chunk, Exit, FiberId } from "effect";
344+import type { ExitEncoded } from "./server";
345+
346+class NotFoundError extends Schema.TaggedError<NotFoundError>()(
347+ "NotFoundError",
348+ { id: Schema.String },
349+) {}
350+
351+class ForbiddenError extends Schema.TaggedError<ForbiddenError>()(
352+ "ForbiddenError",
353+ {},
354+) {}
355+
356+const createServerEncoder = (
357+ successSchema: Schema.Schema.AnyNoContext,
358+ errorSchema: Schema.Schema.AnyNoContext | undefined,
359+) => {
360+ const exitSchemaVal = Schema.Exit({
361+ success: successSchema as Schema.Schema<unknown, unknown, never>,
362+ failure: (errorSchema ?? Schema.Never) as Schema.Schema<unknown, unknown, never>,
363+ defect: Schema.Defect,
364+ });
365+ return Schema.encodeSync(exitSchemaVal) as (
366+ exit: Exit.Exit<unknown, unknown>,
367+ ) => ExitEncoded;
368+};
369+
370+type CauseEncoded =
371+ | { readonly _tag: "Empty" }
372+ | { readonly _tag: "Fail"; readonly error: unknown }
373+ | { readonly _tag: "Die"; readonly defect: unknown }
374+ | { readonly _tag: "Interrupt"; readonly fiberId: unknown }
375+ | {
376+ readonly _tag: "Sequential";
377+ readonly left: CauseEncoded;
378+ readonly right: CauseEncoded;
379+ }
380+ | {
381+ readonly _tag: "Parallel";
382+ readonly left: CauseEncoded;
383+ readonly right: CauseEncoded;
384+ };
385+
386+const decodeCauseTest = (encoded: CauseEncoded): Cause.Cause<unknown> => {
387+ switch (encoded._tag) {
388+ case "Empty":
389+ return Cause.empty;
390+ case "Fail":
391+ return Cause.fail(encoded.error);
392+ case "Die":
393+ return Cause.die(encoded.defect);
394+ case "Interrupt":
395+ return Cause.interrupt(FiberId.none);
396+ case "Sequential":
397+ return Cause.sequential(
398+ decodeCauseTest(encoded.left),
399+ decodeCauseTest(encoded.right),
400+ );
401+ case "Parallel":
402+ return Cause.parallel(
403+ decodeCauseTest(encoded.left),
404+ decodeCauseTest(encoded.right),
405+ );
406+ }
407+};
408+
409+const decodeExitTest = (encoded: ExitEncoded): Exit.Exit<unknown, unknown> => {
410+ if (!encoded || typeof encoded !== "object" || !("_tag" in encoded)) {
411+ return Exit.fail(
412+ new RpcDefectError({
413+ defect: `Unexpected RPC response: ${JSON.stringify(encoded)}`,
414+ }),
415+ );
416+ }
417+ if (encoded._tag === "Success") {
418+ return Exit.succeed(encoded.value);
419+ }
420+ if (encoded._tag !== "Failure" || !encoded.cause) {
421+ return Exit.fail(
422+ new RpcDefectError({
423+ defect: `Unexpected exit tag: ${String(encoded._tag)}`,
424+ }),
425+ );
426+ }
427+ const cause = decodeCauseTest(encoded.cause as CauseEncoded);
428+ const failureOption = Cause.failureOption(cause);
429+ if (Option.isSome(failureOption)) {
430+ return Exit.fail(failureOption.value);
431+ }
432+ const defects = Cause.defects(cause);
433+ if (Chunk.isNonEmpty(defects)) {
434+ return Exit.fail(
435+ new RpcDefectError({ defect: Chunk.unsafeHead(defects) }),
436+ );
437+ }
438+ return Exit.fail(new RpcDefectError({ defect: "Empty cause" }));
439+};
440+
441+describe("Error round-trip (server encode → client decode)", () => {
442+ it("TaggedError _tag survives server encode → JSON → client decode", () => {
443+ const encodeExit = createServerEncoder(
444+ Schema.String,
445+ Schema.Union(NotFoundError, ForbiddenError),
446+ );
447+
448+ const serverExit = Exit.fail(new NotFoundError({ id: "test-123" }));
449+ const encoded = encodeExit(serverExit);
450+
451+ // Simulate JSON round-trip (Convex sends JSON over the wire)
452+ const wire = JSON.parse(JSON.stringify(encoded));
453+
454+ const clientExit = decodeExitTest(wire);
455+
456+ expect(Exit.isFailure(clientExit)).toBe(true);
457+ if (Exit.isFailure(clientExit)) {
458+ const error = Cause.failureOption(clientExit.cause);
459+ expect(Option.isSome(error)).toBe(true);
460+ if (Option.isSome(error)) {
461+ const err = error.value as Record<string, unknown>;
462+ expect(err._tag).toBe("NotFoundError");
463+ expect(err.id).toBe("test-123");
464+ }
465+ }
466+ });
467+
468+ it("Union TaggedError _tag is preserved for second variant", () => {
469+ const encodeExit = createServerEncoder(
470+ Schema.String,
471+ Schema.Union(NotFoundError, ForbiddenError),
472+ );
473+
474+ const serverExit = Exit.fail(new ForbiddenError());
475+ const encoded = encodeExit(serverExit);
476+ const wire = JSON.parse(JSON.stringify(encoded));
477+ const clientExit = decodeExitTest(wire);
478+
479+ expect(Exit.isFailure(clientExit)).toBe(true);
480+ if (Exit.isFailure(clientExit)) {
481+ const error = Cause.failureOption(clientExit.cause);
482+ expect(Option.isSome(error)).toBe(true);
483+ if (Option.isSome(error)) {
484+ const err = error.value as Record<string, unknown>;
485+ expect(err._tag).toBe("ForbiddenError");
486+ }
487+ }
488+ });
489+
490+ it("defect (Effect.die) is wrapped in RpcDefectError", () => {
491+ const encodeExit = createServerEncoder(Schema.String, undefined);
492+
493+ const serverExit = Exit.die("GitHub API error: 403 Forbidden");
494+ const encoded = encodeExit(serverExit);
495+ const wire = JSON.parse(JSON.stringify(encoded));
496+ const clientExit = decodeExitTest(wire);
497+
498+ expect(Exit.isFailure(clientExit)).toBe(true);
499+ if (Exit.isFailure(clientExit)) {
500+ const error = Cause.failureOption(clientExit.cause);
501+ expect(Option.isSome(error)).toBe(true);
502+ if (Option.isSome(error)) {
503+ expect(error.value).toBeInstanceOf(RpcDefectError);
504+ }
505+ }
506+ });
507+});
508+
509+// ---------------------------------------------------------------------------
510+// Server RPC query deduplication tests
511+// ---------------------------------------------------------------------------
512+
513+/**
514+ * Creates a mock ConvexHttpClientLayer that tracks how many actual query calls
515+ * are made. Each query resolves after a short delay to simulate network latency,
516+ * allowing us to test that concurrent calls are deduplicated.
517+ */
518+const createMockHttpLayer = () => {
519+ let callCount = 0;
520+ const callPayloads: Array<unknown> = [];
521+
522+ const service: ConvexClientService = {
523+ query: <Q extends import("convex/server").FunctionReference<"query">>(
524+ _query: Q,
525+ args: Q["_args"],
526+ ) => {
527+ callCount += 1;
528+ callPayloads.push(args);
529+ // Simulate network delay so concurrent calls overlap
530+ return Effect.promise(
531+ () =>
532+ new Promise<import("convex/server").FunctionReturnType<Q>>((resolve) => {
533+ setTimeout(
534+ () =>
535+ resolve({
536+ _tag: "Success",
537+ value: { data: "test-result" },
538+ } as import("convex/server").FunctionReturnType<Q>),
539+ 50,
540+ );
541+ }),
542+ );
543+ },
544+ mutation: () => Effect.die("not implemented"),
545+ action: () => Effect.die("not implemented"),
546+ subscribe: () => Stream.empty,
547+ };
548+
549+ const layer = Layer.succeed(ConvexClient, service);
550+
551+ return {
552+ layer,
553+ getCallCount: () => callCount,
554+ getCallPayloads: () => callPayloads,
555+ resetCounts: () => {
556+ callCount = 0;
557+ callPayloads.length = 0;
558+ },
559+ };
560+};
561+
562+describe("Server RPC query deduplication", () => {
563+ it("concurrent queryPromise calls with identical payload only fire one request", async () => {
564+ const mock = createMockHttpLayer();
565+
566+ const mockApi = {
567+ list: guestbookModule.handlers.list,
568+ };
569+
570+ const serverClient = createServerRpcQuery<typeof guestbookModule>(
571+ mockApi as never,
572+ { url: "https://test.convex.cloud", layer: mock.layer },
573+ );
574+
575+ // Fire 3 concurrent calls with the same payload
576+ const [r1, r2, r3] = await Promise.all([
577+ serverClient.list.queryPromise({}),
578+ serverClient.list.queryPromise({}),
579+ serverClient.list.queryPromise({}),
580+ ]);
581+
582+ // All should return the same result
583+ expect(r1).toEqual({ data: "test-result" });
584+ expect(r2).toEqual({ data: "test-result" });
585+ expect(r3).toEqual({ data: "test-result" });
586+
587+ // Only ONE actual query should have been made
588+ expect(mock.getCallCount()).toBe(1);
589+ });
590+
591+ it("concurrent calls with DIFFERENT payloads fire separate requests", async () => {
592+ const mock = createMockHttpLayer();
593+
594+ const mockApi = {
595+ get: guestbookModule.handlers.get,
596+ };
597+
598+ const serverClient = createServerRpcQuery<typeof guestbookModule>(
599+ mockApi as never,
600+ { url: "https://test.convex.cloud", layer: mock.layer },
601+ );
602+
603+ const [r1, r2] = await Promise.all([
604+ serverClient.get.queryPromise({ id: "a" }),
605+ serverClient.get.queryPromise({ id: "b" }),
606+ ]);
607+
608+ // Both resolve
609+ expect(r1).toEqual({ data: "test-result" });
610+ expect(r2).toEqual({ data: "test-result" });
611+
612+ // Two different payloads = two separate requests
613+ expect(mock.getCallCount()).toBe(2);
614+ });
615+
616+ it("scopes server query cache by auth token when configured", async () => {
617+ const mock = createMockHttpLayer();
618+
619+ const mockApi = {
620+ list: guestbookModule.handlers.list,
621+ };
622+
623+ let token = "token-a";
624+
625+ const serverClient = createServerRpcQuery<typeof guestbookModule>(
626+ mockApi as never,
627+ {
628+ url: "https://test.convex.cloud",
629+ layer: mock.layer,
630+ getAuthToken: async () => token,
631+ },
632+ );
633+
634+ await serverClient.list.queryPromise({});
635+ await serverClient.list.queryPromise({});
636+ expect(mock.getCallCount()).toBe(1);
637+
638+ token = "token-b";
639+ await serverClient.list.queryPromise({});
640+ expect(mock.getCallCount()).toBe(2);
641+ });
642+
643+ it("sequential calls after TTL expiry fire fresh requests", async () => {
644+ const mock = createMockHttpLayer();
645+
646+ const mockApi = {
647+ list: guestbookModule.handlers.list,
648+ };
649+
650+ const serverClient = createServerRpcQuery<typeof guestbookModule>(
651+ mockApi as never,
652+ { url: "https://test.convex.cloud", layer: mock.layer },
653+ );
654+
655+ // First call
656+ const r1 = await serverClient.list.queryPromise({});
657+ expect(r1).toEqual({ data: "test-result" });
658+
659+ // Second call happens immediately (within TTL) — should be cached
660+ const r2 = await serverClient.list.queryPromise({});
661+ expect(r2).toEqual({ data: "test-result" });
662+
663+ // Within the TTL window, both calls share the same cached result
664+ // The Cache coalesces in-flight requests AND caches completed results
665+ // for the TTL duration, so sequential calls within TTL only fire once.
666+ expect(mock.getCallCount()).toBe(1);
667+ });
668+
669+ it("deduplication works when first call rejects", async () => {
670+ let callCount = 0;
671+
672+ const failingService: ConvexClientService = {
673+ query: <Q extends import("convex/server").FunctionReference<"query">>(
674+ _query: Q,
675+ _args: Q["_args"],
676+ ) => {
677+ callCount += 1;
678+ return Effect.promise(
679+ () =>
680+ new Promise<import("convex/server").FunctionReturnType<Q>>((resolve) => {
681+ setTimeout(
682+ () =>
683+ resolve({
684+ _tag: "Failure",
685+ cause: {
686+ _tag: "Fail",
687+ error: { _tag: "SomeError" },
688+ },
689+ } as import("convex/server").FunctionReturnType<Q>),
690+ 50,
691+ );
692+ }),
693+ );
694+ },
695+ mutation: () => Effect.die("not implemented"),
696+ action: () => Effect.die("not implemented"),
697+ subscribe: () => Stream.empty,
698+ };
699+
700+ const failingLayer = Layer.succeed(ConvexClient, failingService);
701+
702+ const mockApi = {
703+ list: guestbookModule.handlers.list,
704+ };
705+
706+ const serverClient = createServerRpcQuery<typeof guestbookModule>(
707+ mockApi as never,
708+ { url: "https://test.convex.cloud", layer: failingLayer },
709+ );
710+
711+ // All 3 should reject
712+ const results = await Promise.allSettled([
713+ serverClient.list.queryPromise({}),
714+ serverClient.list.queryPromise({}),
715+ serverClient.list.queryPromise({}),
716+ ]);
717+
718+ expect(results[0].status).toBe("rejected");
719+ expect(results[1].status).toBe("rejected");
720+ expect(results[2].status).toBe("rejected");
721+
722+ // Only ONE actual call
723+ expect(callCount).toBe(1);
724+ });
725+});
726+
727+describe("RPC Module", () => {
728+ describe("makeRpcModule", () => {
729+ it("creates module with handlers object", () => {
730+ expect(guestbookModule.handlers).toBeDefined();
731+ expect(guestbookModule.handlers.add).toBeDefined();
732+ expect(guestbookModule.handlers.list).toBeDefined();
733+ expect(guestbookModule.handlers.get).toBeDefined();
734+ expect(guestbookModule.handlers.sendNotification).toBeDefined();
735+ });
736+
737+ it("creates module with rpcs object", () => {
738+ expect(guestbookModule.rpcs).toBeDefined();
739+ expect(guestbookModule.rpcs.add).toBeDefined();
740+ expect(guestbookModule.rpcs.list).toBeDefined();
741+ expect(guestbookModule.rpcs.get).toBeDefined();
742+ expect(guestbookModule.rpcs.sendNotification).toBeDefined();
743+ });
744+
745+ it("creates module with group for @effect/rpc compatibility", () => {
746+ expect(guestbookModule.group).toBeDefined();
747+ });
748+
749+ it("endpoint objects are accessible directly on module", () => {
750+ expect(guestbookModule.add).toBeDefined();
751+ expect(guestbookModule.add._tag).toBe("add");
752+ expect(guestbookModule.list._tag).toBe("list");
753+ expect(guestbookModule.get._tag).toBe("get");
754+ expect(guestbookModule.sendNotification._tag).toBe("sendNotification");
755+ });
756+
757+ it("handlers are Convex registered functions", () => {
758+ expect(typeof guestbookModule.handlers.add).toBe("function");
759+ expect(typeof guestbookModule.handlers.list).toBe("function");
760+ });
761+ });
762+
763+ describe("createRpcFactory", () => {
764+ it("creates factory with query, mutation, action methods", () => {
765+ expect(typeof factory.query).toBe("function");
766+ expect(typeof factory.mutation).toBe("function");
767+ expect(typeof factory.action).toBe("function");
768+ });
769+
770+ it("creates factory with internal variants", () => {
771+ expect(typeof factory.internalQuery).toBe("function");
772+ expect(typeof factory.internalMutation).toBe("function");
773+ expect(typeof factory.internalAction).toBe("function");
774+ });
775+ });
776+});
v2/packages/confect/src/rpc/client.ts+1105−0
@@ -0,0 +1,1105 @@
1+import type { Rpc } from "@effect/rpc";
2+import type {
3+ FunctionReference,
4+ RegisteredQuery,
5+ RegisteredMutation,
6+ RegisteredAction,
7+} from "convex/server";
8+import { Atom, Result } from "@effect-atom/atom";
9+import { Result as ResultModule, useAtomValue } from "@effect-atom/atom-react";
10+import * as Cause from "effect/Cause";
11+import { Cache, Chunk, Data, Duration, Effect, Exit, FiberId, Layer, Option, Stream } from "effect";
12+
13+import {
14+ ConvexClient,
15+ ConvexClientLayer,
16+ ConvexHttpClientLayer,
17+ type ConvexRequestMetadata,
18+} from "../client";
19+import type { AnyRpcModule, ExitEncoded, RpcEndpoint } from "./server";
20+import {
21+ makeRpcTransportHeaders,
22+ type RpcClientKind,
23+ withOptionalRpcTelemetryContext,
24+} from "./telemetry";
25+
26+export class RpcDefectError extends Data.TaggedError("RpcDefectError")<{
27+ readonly defect: unknown;
28+}> {
29+ get message(): string {
30+ return `RpcDefectError: ${extractDefectMessage(this.defect)}`;
31+ }
32+}
33+
34+/** Best-effort extraction of a human-readable message from an opaque defect. */
35+const extractDefectMessage = (defect: unknown): string => {
36+ if (defect === null || defect === undefined) return "Unknown defect";
37+ if (typeof defect === "string") return defect;
38+ if (defect instanceof Error) return defect.message;
39+ if (typeof defect === "object") {
40+ const obj = defect as Record<string, unknown>;
41+ // TaggedError / Data.TaggedError
42+ if (typeof obj._tag === "string" && typeof obj.message === "string") {
43+ return `[${obj._tag}] ${obj.message}`;
44+ }
45+ if (typeof obj._tag === "string") return obj._tag;
46+ if (typeof obj.message === "string") return obj.message;
47+ try {
48+ return JSON.stringify(defect);
49+ } catch {
50+ return String(defect);
51+ }
52+ }
53+ return String(defect);
54+};
55+
56+type EndpointPayload<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn>
57+ ? Rpc.Payload<R>
58+ : never;
59+
60+type EndpointSuccess<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn>
61+ ? Rpc.Success<R>
62+ : never;
63+
64+type EndpointError<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn>
65+ ? Rpc.Error<R>
66+ : never;
67+
68+type EndpointKind<E> = E extends RpcEndpoint<infer _Tag, infer _R, infer ConvexFn>
69+ ? ConvexFn extends RegisteredQuery<infer _V, infer _A, infer _R>
70+ ? "query"
71+ : ConvexFn extends RegisteredMutation<infer _V, infer _A, infer _R>
72+ ? "mutation"
73+ : ConvexFn extends RegisteredAction<infer _V, infer _A, infer _R>
74+ ? "action"
75+ : never
76+ : never;
77+
78+type IsPaginatedResult<T> = T extends {
79+ page: ReadonlyArray<infer _Item>;
80+ isDone: boolean;
81+ continueCursor: string;
82+}
83+ ? true
84+ : false;
85+
86+type ExtractPageItem<T> = T extends {
87+ page: ReadonlyArray<infer Item>;
88+ isDone: boolean;
89+ continueCursor: string;
90+}
91+ ? Item
92+ : never;
93+
94+type IsPaginatedPayload<T> = T extends {
95+ cursor: string | null;
96+ numItems: number;
97+}
98+ ? true
99+ : false;
100+
101+/**
102+ * Extra payload fields for a paginated endpoint – everything except the
103+ * pagination primitives (`cursor` and `numItems`) that the framework manages.
104+ */
105+type PaginatedExtraPayload<Payload> = Omit<Payload, "cursor" | "numItems">;
106+
107+type PaginatedArgs<Extra> = keyof Extra extends never
108+ ? [numItems: number]
109+ : [numItems: number, payload: Extra];
110+
111+export interface RpcReadOptions {
112+ readonly enabled?: boolean;
113+}
114+
115+export type RpcQueryClient<Payload, Success, Error> = {
116+ query: (
117+ payload: Payload,
118+ options?: RpcReadOptions,
119+ ) => Atom.Atom<Result.Result<Success, Error | RpcDefectError>>;
120+ queryEffect: (payload: Payload) => Effect.Effect<Success, Error | RpcDefectError>;
121+ queryPromise: (payload: Payload) => Promise<Success>;
122+ subscription: (
123+ payload: Payload,
124+ options?: RpcReadOptions,
125+ ) => Atom.Atom<Result.Result<Success, Error | RpcDefectError>>;
126+} & (IsPaginatedResult<Success> extends true
127+ ? IsPaginatedPayload<Payload> extends true
128+ ? {
129+ paginated: (
130+ ...args: PaginatedArgs<PaginatedExtraPayload<Payload>>
131+ ) => Atom.Writable<
132+ Atom.PullResult<ExtractPageItem<Success>, Error | RpcDefectError>,
133+ void
134+ >;
135+ }
136+ : {}
137+ : {});
138+
139+export type RpcMutationClient<Payload, Success, Error> = {
140+ mutate: Atom.AtomResultFn<Payload, Success, Error | RpcDefectError>;
141+ mutateEffect: (payload: Payload) => Effect.Effect<Success, Error | RpcDefectError>;
142+ mutatePromise: (payload: Payload) => Promise<Success>;
143+};
144+
145+export type RpcActionClient<Payload, Success, Error> = {
146+ call: Atom.AtomResultFn<Payload, Success, Error | RpcDefectError>;
147+ callEffect: (payload: Payload) => Effect.Effect<Success, Error | RpcDefectError>;
148+ callPromise: (payload: Payload) => Promise<Success>;
149+ callAsQuery: (
150+ payload: Payload,
151+ ) => Atom.Atom<Result.Result<Success, Error | RpcDefectError>>;
152+};
153+
154+type DecorateEndpoint<E, Shared extends Record<string, unknown> = {}> =
155+ EndpointKind<E> extends "query"
156+ ? RpcQueryClient<
157+ Omit<EndpointPayload<E>, keyof Shared>,
158+ EndpointSuccess<E>,
159+ EndpointError<E>
160+ >
161+ : EndpointKind<E> extends "mutation"
162+ ? RpcMutationClient<
163+ Omit<EndpointPayload<E>, keyof Shared>,
164+ EndpointSuccess<E>,
165+ EndpointError<E>
166+ >
167+ : EndpointKind<E> extends "action"
168+ ? RpcActionClient<
169+ Omit<EndpointPayload<E>, keyof Shared>,
170+ EndpointSuccess<E>,
171+ EndpointError<E>
172+ >
173+ : never;
174+
175+type EndpointsRecord = Record<string, RpcEndpoint<string, Rpc.Any, unknown>>;
176+
177+export type RpcModuleClientMethods<TEndpoints extends EndpointsRecord, Shared extends Record<string, unknown> = {}> = {
178+ readonly [K in keyof TEndpoints]: DecorateEndpoint<TEndpoints[K], Shared>;
179+};
180+
181+export interface RpcModuleClientConfig {
182+ readonly url: string;
183+ readonly layer?: Layer.Layer<ConvexClient>;
184+ readonly enablePayloadTelemetryFallback?: boolean;
185+}
186+
187+type ConvexApiModule = Record<string, FunctionReference<"query" | "mutation" | "action">>;
188+
189+type DecorateModuleEndpoints<TModule extends AnyRpcModule, Shared extends Record<string, unknown>> = {
190+ [K in keyof TModule]: TModule[K] extends RpcEndpoint<string, Rpc.Any, unknown>
191+ ? DecorateEndpoint<TModule[K], Shared>
192+ : never;
193+};
194+
195+type ExtractDecoratedEndpoints<TModule extends AnyRpcModule, Shared extends Record<string, unknown>> =
196+ Pick<DecorateModuleEndpoints<TModule, Shared>, {
197+ [K in keyof TModule]: TModule[K] extends RpcEndpoint<string, Rpc.Any, unknown> ? K : never;
198+ }[keyof TModule]>;
199+
200+export type RpcModuleClient<
201+ TModule extends AnyRpcModule,
202+ Shared extends Record<string, unknown> = {},
203+> = {
204+ readonly runtime: Atom.AtomRuntime<ConvexClient>;
205+} & ExtractDecoratedEndpoints<TModule, Shared>;
206+
207+type CauseEncoded<E = unknown, D = unknown> =
208+ | { readonly _tag: "Empty" }
209+ | { readonly _tag: "Fail"; readonly error: E }
210+ | { readonly _tag: "Die"; readonly defect: D }
211+ | { readonly _tag: "Interrupt"; readonly fiberId: unknown }
212+ | { readonly _tag: "Sequential"; readonly left: CauseEncoded<E, D>; readonly right: CauseEncoded<E, D> }
213+ | { readonly _tag: "Parallel"; readonly left: CauseEncoded<E, D>; readonly right: CauseEncoded<E, D> };
214+
215+const decodeCause = (encoded: CauseEncoded): Cause.Cause<unknown> => {
216+ switch (encoded._tag) {
217+ case "Empty":
218+ return Cause.empty;
219+ case "Fail":
220+ return Cause.fail(encoded.error);
221+ case "Die":
222+ return Cause.die(encoded.defect);
223+ case "Interrupt":
224+ return Cause.interrupt(FiberId.none);
225+ case "Sequential":
226+ return Cause.sequential(decodeCause(encoded.left), decodeCause(encoded.right));
227+ case "Parallel":
228+ return Cause.parallel(decodeCause(encoded.left), decodeCause(encoded.right));
229+ }
230+};
231+
232+const decodeExit = (encoded: ExitEncoded): Exit.Exit<unknown, unknown> => {
233+ // Safety: if the response isn't a valid ExitEncoded, wrap as defect
234+ if (
235+ !encoded ||
236+ typeof encoded !== "object" ||
237+ !("_tag" in encoded)
238+ ) {
239+ return Exit.fail(
240+ new RpcDefectError({ defect: `Unexpected RPC response: ${JSON.stringify(encoded)}` }),
241+ );
242+ }
243+ if (encoded._tag === "Success") {
244+ return Exit.succeed(encoded.value);
245+ }
246+ if (encoded._tag !== "Failure" || !encoded.cause) {
247+ return Exit.fail(
248+ new RpcDefectError({ defect: `Unexpected exit tag: ${String(encoded._tag)}` }),
249+ );
250+ }
251+ const cause = decodeCause(encoded.cause);
252+ const failureOption = Cause.failureOption(cause);
253+ if (Option.isSome(failureOption)) {
254+ return Exit.fail(failureOption.value);
255+ }
256+ const defects = Cause.defects(cause);
257+ if (Chunk.isNonEmpty(defects)) {
258+ return Exit.fail(new RpcDefectError({ defect: Chunk.unsafeHead(defects) }));
259+ }
260+ if (Cause.isInterrupted(cause)) {
261+ return Exit.fail(new RpcDefectError({ defect: "Interrupted" }));
262+ }
263+ return Exit.fail(new RpcDefectError({ defect: "Empty cause" }));
264+};
265+
266+const ensureNextRequestDataAccess = async () => {
267+ if (typeof window !== "undefined") {
268+ return;
269+ }
270+
271+ // `next/headers` exists only in Next.js server runtime. Attempt import and
272+ // request binding opportunistically; skip in tests and non-Next environments.
273+ try {
274+ const { headers } = await import("next/headers");
275+ await headers();
276+ } catch {
277+ return;
278+ }
279+};
280+
281+const withRpcClientSpan = <A>(
282+ kind: RpcClientKind,
283+ endpointTag: string,
284+ payload: unknown,
285+ enablePayloadTelemetryFallback: boolean,
286+ run: (
287+ payloadWithTelemetry: unknown,
288+ requestMetadata: ConvexRequestMetadata,
289+ ) => Effect.Effect<A, unknown, ConvexClient>,
290+): Effect.Effect<A, unknown, ConvexClient> =>
291+ Effect.useSpan(
292+ `rpc.client.${kind}.${endpointTag}`,
293+ {
294+ kind: "client",
295+ captureStackTrace: false,
296+ attributes: {
297+ "rpc.system": "convex",
298+ "rpc.method": endpointTag,
299+ "rpc.confect.kind": kind,
300+ },
301+ },
302+ (span) => {
303+ const requestMetadata: ConvexRequestMetadata = {
304+ headers: makeRpcTransportHeaders(span),
305+ };
306+ return run(
307+ withOptionalRpcTelemetryContext(
308+ kind,
309+ payload,
310+ span,
311+ enablePayloadTelemetryFallback,
312+ ),
313+ requestMetadata,
314+ );
315+ },
316+ );
317+
318+const createQueryEffect = (
319+ endpointTag: string,
320+ convexFn: FunctionReference<"query">,
321+ payload: unknown,
322+ enablePayloadTelemetryFallback: boolean,
323+): Effect.Effect<unknown, unknown, ConvexClient> =>
324+ withRpcClientSpan(
325+ "query",
326+ endpointTag,
327+ payload,
328+ enablePayloadTelemetryFallback,
329+ (payloadWithTelemetry, requestMetadata) =>
330+ Effect.gen(function* () {
331+ const client = yield* ConvexClient;
332+ const encodedExit = yield* client.query(
333+ convexFn,
334+ payloadWithTelemetry,
335+ requestMetadata,
336+ );
337+ const exit = decodeExit(encodedExit as ExitEncoded);
338+ if (Exit.isSuccess(exit)) {
339+ return exit.value;
340+ }
341+ return yield* exit;
342+ }),
343+ );
344+
345+const createMutationEffect = (
346+ endpointTag: string,
347+ convexFn: FunctionReference<"mutation">,
348+ payload: unknown,
349+ enablePayloadTelemetryFallback: boolean,
350+): Effect.Effect<unknown, unknown, ConvexClient> =>
351+ withRpcClientSpan(
352+ "mutation",
353+ endpointTag,
354+ payload,
355+ enablePayloadTelemetryFallback,
356+ (payloadWithTelemetry, requestMetadata) =>
357+ Effect.gen(function* () {
358+ const client = yield* ConvexClient;
359+ const encodedExit = yield* client.mutation(
360+ convexFn,
361+ payloadWithTelemetry,
362+ requestMetadata,
363+ );
364+ const exit = decodeExit(encodedExit as ExitEncoded);
365+ if (Exit.isSuccess(exit)) {
366+ return exit.value;
367+ }
368+ return yield* exit;
369+ }),
370+ );
371+
372+const createActionEffect = (
373+ endpointTag: string,
374+ convexFn: FunctionReference<"action">,
375+ payload: unknown,
376+ enablePayloadTelemetryFallback: boolean,
377+): Effect.Effect<unknown, unknown, ConvexClient> =>
378+ withRpcClientSpan(
379+ "action",
380+ endpointTag,
381+ payload,
382+ enablePayloadTelemetryFallback,
383+ (payloadWithTelemetry, requestMetadata) =>
384+ Effect.gen(function* () {
385+ const client = yield* ConvexClient;
386+ const encodedExit = yield* client.action(
387+ convexFn,
388+ payloadWithTelemetry,
389+ requestMetadata,
390+ );
391+ const exit = decodeExit(encodedExit as ExitEncoded);
392+ if (Exit.isSuccess(exit)) {
393+ return exit.value;
394+ }
395+ return yield* exit;
396+ }),
397+ );
398+
399+const createQueryAtom = (
400+ runtime: Atom.AtomRuntime<ConvexClient>,
401+ endpointTag: string,
402+ convexFn: FunctionReference<"query">,
403+ payload: unknown,
404+ enablePayloadTelemetryFallback: boolean,
405+): Atom.Atom<Result.Result<unknown, unknown>> => {
406+ return runtime.atom(
407+ createQueryEffect(
408+ endpointTag,
409+ convexFn,
410+ payload,
411+ enablePayloadTelemetryFallback,
412+ ),
413+ );
414+};
415+
416+const createSubscriptionAtom = (
417+ runtime: Atom.AtomRuntime<ConvexClient>,
418+ endpointTag: string,
419+ convexFn: FunctionReference<"query">,
420+ payload: unknown,
421+ enablePayloadTelemetryFallback: boolean,
422+): Atom.Atom<Result.Result<unknown, unknown>> => {
423+ return runtime.atom(
424+ Stream.unwrap(
425+ withRpcClientSpan(
426+ "query",
427+ endpointTag,
428+ payload,
429+ enablePayloadTelemetryFallback,
430+ (payloadWithTelemetry, _requestMetadata) =>
431+ Effect.gen(function* () {
432+ const client = yield* ConvexClient;
433+ return client.subscribe(convexFn, payloadWithTelemetry).pipe(
434+ Stream.mapEffect((encodedExit) => {
435+ const exit = decodeExit(encodedExit as ExitEncoded);
436+ if (Exit.isSuccess(exit)) {
437+ return Effect.succeed(exit.value);
438+ }
439+ return exit;
440+ }),
441+ );
442+ }),
443+ ),
444+ ),
445+ );
446+};
447+
448+const createActionQueryAtom = (
449+ runtime: Atom.AtomRuntime<ConvexClient>,
450+ endpointTag: string,
451+ convexFn: FunctionReference<"action">,
452+ payload: unknown,
453+ enablePayloadTelemetryFallback: boolean,
454+): Atom.Atom<Result.Result<unknown, unknown>> => {
455+ return runtime.atom(
456+ createActionEffect(
457+ endpointTag,
458+ convexFn,
459+ payload,
460+ enablePayloadTelemetryFallback,
461+ ),
462+ );
463+};
464+
465+const createMutationFn = (
466+ runtime: Atom.AtomRuntime<ConvexClient>,
467+ endpointTag: string,
468+ convexFn: FunctionReference<"mutation">,
469+ getShared: () => Record<string, unknown>,
470+ enablePayloadTelemetryFallback: boolean,
471+): Atom.AtomResultFn<unknown, unknown, unknown> => {
472+ return runtime.fn<unknown>()(
473+ Effect.fnUntraced(function* (payload) {
474+ const fullPayload = { ...getShared(), ...(payload as object) };
475+ return yield* createMutationEffect(
476+ endpointTag,
477+ convexFn,
478+ fullPayload,
479+ enablePayloadTelemetryFallback,
480+ );
481+ }),
482+ );
483+};
484+
485+const createActionFn = (
486+ runtime: Atom.AtomRuntime<ConvexClient>,
487+ endpointTag: string,
488+ convexFn: FunctionReference<"action">,
489+ getShared: () => Record<string, unknown>,
490+ enablePayloadTelemetryFallback: boolean,
491+): Atom.AtomResultFn<unknown, unknown, unknown> => {
492+ return runtime.fn<unknown>()(
493+ Effect.fnUntraced(function* (payload) {
494+ const fullPayload = { ...getShared(), ...(payload as object) };
495+ return yield* createActionEffect(
496+ endpointTag,
497+ convexFn,
498+ fullPayload,
499+ enablePayloadTelemetryFallback,
500+ );
501+ }),
502+ );
503+};
504+
505+interface PaginatedResult<T> {
506+ page: ReadonlyArray<T>;
507+ isDone: boolean;
508+ continueCursor: string;
509+}
510+
511+const createPaginatedAtom = (
512+ runtime: Atom.AtomRuntime<ConvexClient>,
513+ endpointTag: string,
514+ convexFn: FunctionReference<"query">,
515+ getShared: () => Record<string, unknown>,
516+ numItems: number,
517+ extraPayload: Record<string, unknown>,
518+ enablePayloadTelemetryFallback: boolean,
519+): Atom.Writable<Atom.PullResult<unknown, unknown>, void> => {
520+ return runtime.pull(
521+ Stream.paginateChunkEffect(null as string | null, (cursor) =>
522+ Effect.gen(function* () {
523+ const fullPayload = {
524+ ...getShared(),
525+ ...extraPayload,
526+ cursor,
527+ numItems,
528+ };
529+ const result = (yield* createQueryEffect(
530+ endpointTag,
531+ convexFn,
532+ fullPayload,
533+ enablePayloadTelemetryFallback,
534+ )) as PaginatedResult<unknown>;
535+ const nextCursor = result.isDone
536+ ? Option.none<string | null>()
537+ : Option.some(result.continueCursor);
538+
539+ return [Chunk.fromIterable(result.page), nextCursor] as const;
540+ }),
541+ ),
542+ );
543+};
544+
545+const noop = () => {};
546+
547+const isRpcReadEnabled = (options?: RpcReadOptions): boolean =>
548+ options?.enabled ?? true;
549+
550+const makeDisabledResultAtom = <Success, Error>() =>
551+ Atom.make(Result.initial<Success, Error>());
552+
553+export function createRpcClient<
554+ TModule extends AnyRpcModule,
555+ Shared extends Record<string, unknown> = {},
556+>(
557+ convexApi: ConvexApiModule,
558+ config: RpcModuleClientConfig,
559+ getShared: () => Shared = () => ({}) as Shared,
560+): RpcModuleClient<TModule, Shared> {
561+ const baseLayer = config.layer ?? ConvexClientLayer(config.url);
562+ const enablePayloadTelemetryFallback =
563+ config.enablePayloadTelemetryFallback ?? true;
564+ const runtime = Atom.runtime(baseLayer);
565+
566+ const queryFamilies = new Map<string, (payload: unknown) => Atom.Atom<Result.Result<unknown, unknown>>>();
567+ const subscriptionFamilies = new Map<string, (payload: unknown) => Atom.Atom<Result.Result<unknown, unknown>>>();
568+ const actionQueryFamilies = new Map<string, (payload: unknown) => Atom.Atom<Result.Result<unknown, unknown>>>();
569+ const mutationFns = new Map<string, Atom.AtomResultFn<unknown, unknown, unknown>>();
570+ const actionFns = new Map<string, Atom.AtomResultFn<unknown, unknown, unknown>>();
571+ const paginatedFamilies = new Map<string, (numItems: number, extra: Record<string, unknown>) => Atom.Writable<Atom.PullResult<unknown, unknown>, void>>();
572+
573+ const getQueryFamily = (tag: string) => {
574+ let family = queryFamilies.get(tag);
575+ if (!family) {
576+ const convexFn = convexApi[tag] as FunctionReference<"query">;
577+ family = Atom.family((p: unknown) => {
578+ const fullPayload = { ...getShared(), ...(p as object) };
579+ return createQueryAtom(
580+ runtime,
581+ tag,
582+ convexFn,
583+ fullPayload,
584+ enablePayloadTelemetryFallback,
585+ );
586+ });
587+ queryFamilies.set(tag, family);
588+ }
589+ return family;
590+ };
591+
592+ const getSubscriptionFamily = (tag: string) => {
593+ let family = subscriptionFamilies.get(tag);
594+ if (!family) {
595+ const convexFn = convexApi[tag] as FunctionReference<"query">;
596+ family = Atom.family((p: unknown) => {
597+ const fullPayload = { ...getShared(), ...(p as object) };
598+ return createSubscriptionAtom(
599+ runtime,
600+ tag,
601+ convexFn,
602+ fullPayload,
603+ enablePayloadTelemetryFallback,
604+ );
605+ });
606+ subscriptionFamilies.set(tag, family);
607+ }
608+ return family;
609+ };
610+
611+ const getMutationFn = (tag: string) => {
612+ let fn = mutationFns.get(tag);
613+ if (!fn) {
614+ const convexFn = convexApi[tag] as FunctionReference<"mutation">;
615+ fn = createMutationFn(
616+ runtime,
617+ tag,
618+ convexFn,
619+ getShared,
620+ enablePayloadTelemetryFallback,
621+ );
622+ mutationFns.set(tag, fn);
623+ }
624+ return fn;
625+ };
626+
627+ const getActionFn = (tag: string) => {
628+ let fn = actionFns.get(tag);
629+ if (!fn) {
630+ const convexFn = convexApi[tag] as FunctionReference<"action">;
631+ fn = createActionFn(
632+ runtime,
633+ tag,
634+ convexFn,
635+ getShared,
636+ enablePayloadTelemetryFallback,
637+ );
638+ actionFns.set(tag, fn);
639+ }
640+ return fn;
641+ };
642+
643+ const getActionQueryFamily = (tag: string) => {
644+ let family = actionQueryFamilies.get(tag);
645+ if (!family) {
646+ const convexFn = convexApi[tag] as FunctionReference<"action">;
647+ family = Atom.family((p: unknown) => {
648+ const fullPayload = { ...getShared(), ...(p as object) };
649+ return createActionQueryAtom(
650+ runtime,
651+ tag,
652+ convexFn,
653+ fullPayload,
654+ enablePayloadTelemetryFallback,
655+ );
656+ });
657+ actionQueryFamilies.set(tag, family);
658+ }
659+ return family;
660+ };
661+
662+ const getPaginatedFamily = (tag: string) => {
663+ let cached = paginatedFamilies.get(tag);
664+ if (!cached) {
665+ const convexFn = convexApi[tag] as FunctionReference<"query">;
666+ // Atom.family takes a single arg; use a stable JSON key so that
667+ // identical (numItems + extra) combinations share the same atom.
668+ const atomFamily = Atom.family((key: string) => {
669+ const { numItems, extra } = JSON.parse(key) as {
670+ numItems: number;
671+ extra: Record<string, unknown>;
672+ };
673+ return createPaginatedAtom(
674+ runtime,
675+ tag,
676+ convexFn,
677+ getShared,
678+ numItems,
679+ extra,
680+ enablePayloadTelemetryFallback,
681+ );
682+ });
683+ cached = (numItems: number, extra: Record<string, unknown>) =>
684+ atomFamily(JSON.stringify({ numItems, extra }));
685+ paginatedFamilies.set(tag, cached);
686+ }
687+ return cached;
688+ };
689+
690+ const endpointProxyCache = new Map<string, unknown>();
691+
692+ const proxy = new Proxy(noop, {
693+ get(_target, prop) {
694+ if (prop === "runtime") {
695+ return runtime;
696+ }
697+ if (prop === "then") {
698+ return undefined;
699+ }
700+ if (typeof prop !== "string") {
701+ return undefined;
702+ }
703+
704+ let endpointProxy = endpointProxyCache.get(prop);
705+ if (!endpointProxy) {
706+ const disabledResultAtom = makeDisabledResultAtom<
707+ never,
708+ RpcDefectError
709+ >();
710+
711+ const queryEffect = (payload: unknown) => {
712+ const convexFn = convexApi[prop] as FunctionReference<"query">;
713+ const fullPayload = { ...getShared(), ...(payload as object) };
714+ return createQueryEffect(
715+ prop,
716+ convexFn,
717+ fullPayload,
718+ enablePayloadTelemetryFallback,
719+ ).pipe(
720+ Effect.provide(baseLayer),
721+ );
722+ };
723+
724+ const mutateEffect = (payload: unknown) => {
725+ const convexFn = convexApi[prop] as FunctionReference<"mutation">;
726+ const fullPayload = { ...getShared(), ...(payload as object) };
727+ return createMutationEffect(
728+ prop,
729+ convexFn,
730+ fullPayload,
731+ enablePayloadTelemetryFallback,
732+ ).pipe(
733+ Effect.provide(baseLayer),
734+ );
735+ };
736+
737+ const callEffect = (payload: unknown) => {
738+ const convexFn = convexApi[prop] as FunctionReference<"action">;
739+ const fullPayload = { ...getShared(), ...(payload as object) };
740+ return createActionEffect(
741+ prop,
742+ convexFn,
743+ fullPayload,
744+ enablePayloadTelemetryFallback,
745+ ).pipe(
746+ Effect.provide(baseLayer),
747+ );
748+ };
749+
750+ endpointProxy = {
751+ query: (payload: unknown, options?: RpcReadOptions) =>
752+ isRpcReadEnabled(options)
753+ ? getQueryFamily(prop)(payload)
754+ : disabledResultAtom,
755+ queryEffect,
756+ queryPromise: (payload: unknown) => Effect.runPromise(queryEffect(payload)),
757+ subscription: (payload: unknown, options?: RpcReadOptions) =>
758+ isRpcReadEnabled(options)
759+ ? getSubscriptionFamily(prop)(payload)
760+ : disabledResultAtom,
761+ mutate: getMutationFn(prop),
762+ mutateEffect,
763+ mutatePromise: (payload: unknown) => Effect.runPromise(mutateEffect(payload)),
764+ call: getActionFn(prop),
765+ callAsQuery: (payload: unknown) => getActionQueryFamily(prop)(payload),
766+ callEffect,
767+ callPromise: (payload: unknown) => Effect.runPromise(callEffect(payload)),
768+ paginated: (numItems: number, extra: Record<string, unknown> = {}) => getPaginatedFamily(prop)(numItems, extra),
769+ };
770+ endpointProxyCache.set(prop, endpointProxy);
771+ }
772+ return endpointProxy;
773+ },
774+ });
775+
776+ return proxy as unknown as RpcModuleClient<TModule, Shared>;
777+}
778+
779+// ---------------------------------------------------------------------------
780+// Server-side RPC query client (HTTP-only, no WebSocket, no atoms)
781+// ---------------------------------------------------------------------------
782+
783+type ServerQueryEndpoint<Payload, Success> = {
784+ readonly queryPromise: (payload: Payload) => Promise<Success>;
785+};
786+
787+type ServerDecorateEndpoint<E, Shared extends Record<string, unknown> = {}> =
788+ EndpointKind<E> extends "query"
789+ ? ServerQueryEndpoint<
790+ Omit<EndpointPayload<E>, keyof Shared>,
791+ EndpointSuccess<E>
792+ >
793+ : never;
794+
795+export type ServerRpcModuleClient<
796+ TModule extends AnyRpcModule,
797+ Shared extends Record<string, unknown> = {},
798+> = {
799+ readonly [K in keyof TModule as TModule[K] extends RpcEndpoint<string, Rpc.Any, unknown>
800+ ? EndpointKind<TModule[K]> extends "query"
801+ ? K
802+ : never
803+ : never]: ServerDecorateEndpoint<TModule[K], Shared>;
804+};
805+
806+export interface ServerRpcClientConfig {
807+ readonly url: string;
808+ /**
809+ * Optional layer override (e.g. for testing). When provided, this layer is
810+ * used instead of creating a `ConvexHttpClientLayer` from `url`.
811+ */
812+ readonly layer?: Layer.Layer<ConvexClient>;
813+ /**
814+ * Optional auth token resolver for server-side requests.
815+ *
816+ * When provided, each `queryPromise` call resolves the current auth token
817+ * and uses a token-authenticated HTTP client. Cache keys are also scoped by
818+ * auth identity to avoid cross-user cache mixing.
819+ */
820+ readonly getAuthToken?: () => Promise<string | null | undefined>;
821+}
822+
823+/**
824+ * Create a server-side RPC client that only supports query promises.
825+ * Uses `ConvexHttpClientLayer` (HTTP only, no WebSocket).
826+ * Intended for use in Next.js server components and route handlers.
827+ *
828+ * Only query endpoints are exposed (mutations/actions are excluded).
829+ * Each endpoint has a single method: `queryPromise(payload) => Promise<Success>`.
830+ */
831+/**
832+ * Default TTL for the server-side query deduplication cache.
833+ *
834+ * During SSR multiple server components may call `queryPromise` with the
835+ * same endpoint + payload concurrently. `Cache` automatically coalesces
836+ * concurrent lookups for the same key into a single in-flight request.
837+ * Completed results are kept for this duration so that near-simultaneous
838+ * calls (within the same render pass) hit the cache rather than firing
839+ * duplicate HTTP requests. The TTL is intentionally short to avoid
840+ * serving stale data across unrelated requests on a long-lived server.
841+ */
842+const SERVER_QUERY_CACHE_TTL = Duration.seconds(5);
843+
844+/**
845+ * Maximum number of distinct (endpoint + payload) entries kept in the
846+ * server-side query cache. This is deliberately generous — each entry
847+ * is small (just the resolved value) and expires quickly.
848+ */
849+const SERVER_QUERY_CACHE_CAPACITY = 256;
850+
851+export function createServerRpcQuery<
852+ TModule extends AnyRpcModule,
853+ Shared extends Record<string, unknown> = {},
854+>(
855+ convexApi: ConvexApiModule,
856+ config: ServerRpcClientConfig,
857+ getShared: () => Shared = () => ({}) as Shared,
858+): ServerRpcModuleClient<TModule, Shared> {
859+ const defaultLayer = config.layer ?? ConvexHttpClientLayer(config.url);
860+
861+ const authScopeByToken = new Map<string, string>();
862+ const authLayerByScope = new Map<string, Layer.Layer<ConvexClient>>();
863+ let nextAuthScopeId = 0;
864+
865+ const AUTH_SCOPE_CACHE_LIMIT = 256;
866+
867+ const resolveAuthToken = async (): Promise<string | null> => {
868+ if (config.getAuthToken === undefined) {
869+ return null;
870+ }
871+
872+ const token = await config.getAuthToken();
873+ if (token === null || token === undefined || token.length === 0) {
874+ return null;
875+ }
876+
877+ const trimmed = token.trim();
878+ if (trimmed.length === 0) {
879+ return null;
880+ }
881+
882+ if (trimmed.startsWith("Bearer ")) {
883+ const bearerValue = trimmed.slice("Bearer ".length).trim();
884+ return bearerValue.length === 0 ? null : bearerValue;
885+ }
886+
887+ return trimmed;
888+ };
889+
890+ const resetAuthScopeCaches = () => {
891+ authScopeByToken.clear();
892+ authLayerByScope.clear();
893+ };
894+
895+ const getAuthScope = (token: string | null): string => {
896+ if (token === null) {
897+ return "anon";
898+ }
899+
900+ const existingScope = authScopeByToken.get(token);
901+ if (existingScope !== undefined) {
902+ return existingScope;
903+ }
904+
905+ nextAuthScopeId += 1;
906+ const scope = `auth-${nextAuthScopeId}`;
907+ authScopeByToken.set(token, scope);
908+
909+ if (authScopeByToken.size > AUTH_SCOPE_CACHE_LIMIT) {
910+ resetAuthScopeCaches();
911+ authScopeByToken.set(token, scope);
912+ }
913+
914+ return scope;
915+ };
916+
917+ const getLayerForScope = (
918+ authScope: string,
919+ authToken: string | null,
920+ ): Layer.Layer<ConvexClient> => {
921+ if (config.layer !== undefined) {
922+ return config.layer;
923+ }
924+
925+ const cachedLayer = authLayerByScope.get(authScope);
926+ if (cachedLayer !== undefined) {
927+ return cachedLayer;
928+ }
929+
930+ if (authToken === null) {
931+ authLayerByScope.set(authScope, defaultLayer);
932+ return defaultLayer;
933+ }
934+
935+ const nextLayer = ConvexHttpClientLayer(config.url, { authToken });
936+ authLayerByScope.set(authScope, nextLayer);
937+ return nextLayer;
938+ };
939+
940+ /**
941+ * A single `Cache<string, unknown, unknown>` keyed by
942+ * `json({ authScope, endpointName, payload })`.
943+ *
944+ * `Cache.make` returns an `Effect` — we run it once eagerly via
945+ * `Effect.runSync` wrapped in a lazy singleton so it's created on first
946+ * access. The lookup function is provided the layer so each cache miss
947+ * fires exactly one HTTP request through the `ConvexHttpClientLayer`.
948+ *
949+ * Because Effect's `Cache` coalesces concurrent lookups for the same key,
950+ * three concurrent calls to `queryPromise({ id: "x" })` result in a
951+ * single Convex HTTP call.
952+ */
953+
954+ // We store a Map of endpoint name → convex function ref so the lookup
955+ // can resolve which function to call from just the cache key string.
956+ const convexFnRegistry = new Map<string, FunctionReference<"query">>();
957+
958+ const cacheEffect = Cache.make({
959+ capacity: SERVER_QUERY_CACHE_CAPACITY,
960+ timeToLive: SERVER_QUERY_CACHE_TTL,
961+ lookup: (cacheKey: string) => {
962+ const decodedKey = JSON.parse(cacheKey);
963+ if (decodedKey === null || typeof decodedKey !== "object") {
964+ return Effect.die(new Error("Invalid server RPC cache key"));
965+ }
966+ if (!("endpointName" in decodedKey) || !("payload" in decodedKey)) {
967+ return Effect.die(new Error("Missing server RPC cache key fields"));
968+ }
969+ if (typeof decodedKey.endpointName !== "string") {
970+ return Effect.die(new Error("Invalid server RPC endpoint cache key"));
971+ }
972+ if (!("authScope" in decodedKey) || typeof decodedKey.authScope !== "string") {
973+ return Effect.die(new Error("Invalid server RPC auth scope cache key"));
974+ }
975+
976+ const endpointName = decodedKey.endpointName;
977+ const authScope = decodedKey.authScope;
978+ const fullPayload = decodedKey.payload;
979+ const convexFn = convexFnRegistry.get(endpointName);
980+ if (!convexFn) {
981+ return Effect.die(
982+ new Error(`No registered Convex function for endpoint "${endpointName}"`),
983+ );
984+ }
985+
986+ const requestLayer = authLayerByScope.get(authScope) ?? defaultLayer;
987+ return Effect.provide(
988+ createQueryEffect(endpointName, convexFn, fullPayload, false),
989+ requestLayer,
990+ );
991+ },
992+ });
993+
994+ const makeQueryCache = () =>
995+ Effect.runSync(Effect.provide(cacheEffect, defaultLayer));
996+
997+ let queryCache = Option.none<ReturnType<typeof makeQueryCache>>();
998+
999+ const getOrCreateQueryCache = () => {
1000+ if (Option.isSome(queryCache)) {
1001+ return queryCache.value;
1002+ }
1003+
1004+ const nextQueryCache = makeQueryCache();
1005+ queryCache = Option.some(nextQueryCache);
1006+ return nextQueryCache;
1007+ };
1008+
1009+ const endpointCache = new Map<string, ServerQueryEndpoint<unknown, unknown>>();
1010+
1011+ const getEndpoint = (prop: string): ServerQueryEndpoint<unknown, unknown> => {
1012+ let endpoint = endpointCache.get(prop);
1013+ if (!endpoint) {
1014+ const convexFn = convexApi[prop];
1015+ if (!convexFn) {
1016+ throw new Error(`No Convex function found for endpoint "${prop}"`);
1017+ }
1018+ convexFnRegistry.set(prop, convexFn as FunctionReference<"query">);
1019+
1020+ endpoint = {
1021+ queryPromise: async (payload: unknown) => {
1022+ await ensureNextRequestDataAccess();
1023+ const authToken = await resolveAuthToken();
1024+ const authScope = getAuthScope(authToken);
1025+
1026+ const payloadObject =
1027+ payload !== null && typeof payload === "object" ? payload : {};
1028+ const fullPayload = { ...getShared(), ...payloadObject };
1029+ const cacheKey = JSON.stringify({
1030+ authScope,
1031+ endpointName: prop,
1032+ payload: fullPayload,
1033+ });
1034+ getLayerForScope(authScope, authToken);
1035+
1036+ return Effect.runPromise(
1037+ Effect.gen(function* () {
1038+ const queryCache = yield* Effect.sync(getOrCreateQueryCache);
1039+ return yield* queryCache.get(cacheKey);
1040+ }),
1041+ );
1042+ },
1043+ };
1044+ endpointCache.set(prop, endpoint);
1045+ }
1046+ return endpoint;
1047+ };
1048+
1049+ const proxy = new Proxy({} as Record<string, ServerQueryEndpoint<unknown, unknown>>, {
1050+ get(_target, prop) {
1051+ if (prop === "then") return undefined;
1052+ if (typeof prop !== "string") return undefined;
1053+ return getEndpoint(prop);
1054+ },
1055+ });
1056+
1057+ return proxy as ServerRpcModuleClient<TModule, Shared>;
1058+}
1059+
1060+// ---------------------------------------------------------------------------
1061+// React hook: merge server-fetched data with real-time subscription
1062+// ---------------------------------------------------------------------------
1063+
1064+/**
1065+ * Merge server-fetched initial data with a real-time Confect RPC subscription.
1066+ *
1067+ * Use this in client components that receive server data via `use(promise)`
1068+ * (React Suspense) and also subscribe for live updates. The hook returns the
1069+ * latest subscription value when available, falling back to `initialData`.
1070+ *
1071+ * No `useEffect`, no `useState` — pure derivation in the render path.
1072+ *
1073+ * The subscription atom from `client.X.subscription(...)` is typed as
1074+ * `Atom<Result<unknown, unknown>>` because the RPC proxy loses generic info.
1075+ * The type of `initialData` anchors the return type: both the server query
1076+ * and the subscription call the same RPC endpoint, so the shapes match at
1077+ * runtime.
1078+ *
1079+ * @example
1080+ * ```tsx
1081+ * const initialData = use(serverPromise); // suspends
1082+ * const prAtom = useMemo(() => client.getPr.subscription({ ... }), [...]);
1083+ * const pr = useSubscriptionWithInitial(prAtom, initialData);
1084+ * ```
1085+ */
1086+export function useSubscriptionWithInitial<T>(
1087+ subscriptionAtom: Atom.Atom<Result.Result<unknown, unknown>>,
1088+ initialData: T,
1089+): T {
1090+ const result = useAtomValue(subscriptionAtom);
1091+
1092+ if (ResultModule.isInitial(result)) {
1093+ return initialData;
1094+ }
1095+
1096+ const valueOption = ResultModule.value(result);
1097+ if (Option.isSome(valueOption)) {
1098+ // Safe: the subscription and server query call the same RPC endpoint,
1099+ // so the runtime value matches T. The proxy erases the generic, but
1100+ // the hook restores type safety at its boundary.
1101+ return valueOption.value as T;
1102+ }
1103+
1104+ return initialData;
1105+}
v2/packages/confect/src/rpc/go-to-definition.test.ts+87−0
@@ -0,0 +1,87 @@
1+import path from "node:path";
2+import { fileURLToPath } from "node:url";
3+import ts from "typescript";
4+import { describe, expect, it } from "vitest";
5+
6+const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+const packageRoot = path.resolve(__dirname, "../..");
8+
9+const confectConfigPath = path.join(packageRoot, "tsconfig.json");
10+const configFile = ts.readConfigFile(confectConfigPath, ts.sys.readFile);
11+const parsedConfig = ts.parseJsonConfigFileContent(
12+ configFile.config,
13+ ts.sys,
14+ path.dirname(confectConfigPath),
15+);
16+
17+const testFile = path.join(packageRoot, "src/rpc/client.test.ts");
18+
19+const program = ts.createProgram({
20+ rootNames: [testFile],
21+ options: parsedConfig.options,
22+});
23+
24+const checker = program.getTypeChecker();
25+const sf = program.getSourceFile(testFile);
26+
27+function findNodeAtPosition(
28+ node: ts.Node,
29+ position: number,
30+): ts.Node | undefined {
31+ if (position >= node.getStart() && position < node.getEnd()) {
32+ for (const child of node.getChildren()) {
33+ const found = findNodeAtPosition(child, position);
34+ if (found) return found;
35+ }
36+ return node;
37+ }
38+ return undefined;
39+}
40+
41+function hasDeclarations(pattern: string, offset: number): boolean {
42+ if (!sf) return false;
43+
44+ const content = sf.getFullText();
45+ const idx = content.indexOf(pattern);
46+ if (idx === -1) return false;
47+
48+ const pos = idx + offset;
49+ const node = findNodeAtPosition(sf, pos);
50+ if (!node) return false;
51+
52+ const sym = checker.getSymbolAtLocation(node);
53+ if (!sym) return false;
54+
55+ const decls = sym.getDeclarations();
56+ return decls !== undefined && decls.length > 0;
57+}
58+
59+describe("Go-to-Definition", () => {
60+ it("should load the test file", () => {
61+ expect(sf).toBeDefined();
62+ });
63+
64+ describe("module endpoints", () => {
65+ it("guestbookModule.add has declarations", () => {
66+ expect(hasDeclarations("guestbookModule.add", "guestbookModule.".length)).toBe(true);
67+ });
68+
69+ it("guestbookModule.list has declarations", () => {
70+ expect(hasDeclarations("guestbookModule.list", "guestbookModule.".length)).toBe(true);
71+ });
72+
73+ it("guestbookModule.get has declarations", () => {
74+ expect(hasDeclarations("guestbookModule.get", "guestbookModule.".length)).toBe(true);
75+ });
76+ });
77+
78+ describe("client endpoints", () => {
79+ it("client.add has declarations", () => {
80+ expect(hasDeclarations("client.add", "client.".length)).toBe(true);
81+ });
82+
83+ it("client.list has declarations", () => {
84+ expect(hasDeclarations("client.list", "client.".length)).toBe(true);
85+ });
86+ });
87+});
v2/packages/confect/src/rpc/index.ts+49−0
@@ -0,0 +1,49 @@
1+export {
2+ Rpc,
3+ RpcGroup,
4+ RpcMiddleware,
5+ createRpcFactory,
6+ makeRpcModule,
7+ middleware,
8+ exitSchema,
9+ WrapperTypeId,
10+ isWrapper,
11+ wrap,
12+ fork,
13+ uninterruptible,
14+ type RpcModule,
15+ type AnyRpcModule,
16+ type RpcEndpoint,
17+ type UnbuiltRpcEndpoint,
18+ type RpcFactoryConfig,
19+ type RpcModuleOptions,
20+ type InferRpc,
21+ type InferFn,
22+ type InferModuleEndpoints,
23+ type ExitEncoded,
24+ type RpcInfo,
25+ type MiddlewareOptions,
26+ type ConfectMiddlewareFn,
27+ type MiddlewareImplementation,
28+ type Wrapper,
29+ type Handler,
30+ type ToHandler,
31+ type HandlersFrom,
32+ type HandlersContext,
33+} from "./server";
34+
35+export {
36+ createRpcClient,
37+ createServerRpcQuery,
38+ useSubscriptionWithInitial,
39+ RpcDefectError,
40+ type RpcReadOptions,
41+ type RpcModuleClient,
42+ type RpcModuleClientConfig,
43+ type RpcQueryClient,
44+ type RpcMutationClient,
45+ type RpcActionClient,
46+ type RpcModuleClientMethods,
47+ type ServerRpcModuleClient,
48+ type ServerRpcClientConfig,
49+} from "./client";
v2/packages/confect/src/rpc/server.test.ts+1239−0
@@ -0,0 +1,1239 @@
1+import { describe, expect, it, vi } from "vitest";
2+import { Effect, Schema, Context, Layer } from "effect";
3+import { Rpc, RpcMiddleware } from "@effect/rpc";
4+import {
5+ createRpcFactory,
6+ makeRpcModule,
7+ exitSchema,
8+ fork,
9+ uninterruptible,
10+ isWrapper,
11+ wrap,
12+ type ExitEncoded,
13+} from "./server";
14+import { defineSchema, defineTable } from "../schema";
15+
16+const testSchema = defineSchema({
17+ items: defineTable(
18+ Schema.Struct({
19+ name: Schema.String,
20+ value: Schema.Number,
21+ }),
22+ ),
23+});
24+
25+const createMockCtx = () => ({
26+ db: {
27+ query: vi.fn(),
28+ get: vi.fn(),
29+ insert: vi.fn(),
30+ patch: vi.fn(),
31+ replace: vi.fn(),
32+ delete: vi.fn(),
33+ normalizeId: vi.fn(),
34+ },
35+ auth: {
36+ getUserIdentity: vi.fn().mockResolvedValue(null),
37+ },
38+ runQuery: vi.fn(),
39+ runMutation: vi.fn(),
40+ runAction: vi.fn(),
41+ scheduler: {
42+ runAfter: vi.fn(),
43+ runAt: vi.fn(),
44+ },
45+});
46+
47+type ConvexHandlerInternal = (ctx: unknown, args: unknown) => Promise<ExitEncoded>;
48+
49+const getHandler = (handler: unknown): ConvexHandlerInternal => {
50+ const h = handler as { _handler: (ctx: unknown, args: unknown) => Promise<ExitEncoded> };
51+ return h._handler;
52+};
53+
54+type SuccessExit = { readonly _tag: "Success"; readonly value: unknown };
55+type FailureExit = { readonly _tag: "Failure"; readonly cause: unknown };
56+
57+const assertSuccess = (exit: ExitEncoded): SuccessExit => {
58+ if (exit._tag !== "Success") {
59+ throw new Error(`Expected Success exit, got ${exit._tag}`);
60+ }
61+ return exit as SuccessExit;
62+};
63+
64+const assertFailure = (exit: ExitEncoded): FailureExit => {
65+ if (exit._tag !== "Failure") {
66+ throw new Error(`Expected Failure exit, got ${exit._tag}`);
67+ }
68+ return exit as FailureExit;
69+};
70+
71+/** Helper: define + immediately implement an endpoint (convenience for tests) */
72+const withImpl = <T extends { implement: (handler: (...args: ReadonlyArray<never>) => Effect.Effect<never, never, never>) => void }>(
73+ endpoint: T,
74+ handler: Parameters<T["implement"]>[0],
75+): T => {
76+ endpoint.implement(handler);
77+ return endpoint;
78+};
79+
80+describe("RPC Server", () => {
81+ describe("createRpcFactory", () => {
82+ it("creates a factory with query, mutation, and action methods", () => {
83+ const factory = createRpcFactory({ schema: testSchema });
84+
85+ expect(factory.query).toBeDefined();
86+ expect(factory.mutation).toBeDefined();
87+ expect(factory.action).toBeDefined();
88+ expect(factory.internalQuery).toBeDefined();
89+ expect(factory.internalMutation).toBeDefined();
90+ expect(factory.internalAction).toBeDefined();
91+ });
92+
93+ describe("query endpoint", () => {
94+ it("creates a query endpoint with success schema", () => {
95+ const factory = createRpcFactory({ schema: testSchema });
96+
97+ const endpoint = factory.query({ success: Schema.String });
98+
99+ expect(endpoint.__unbuilt).toBe(true);
100+ expect(endpoint.kind).toBe("query");
101+ expect(endpoint.successSchema).toBe(Schema.String);
102+ });
103+
104+ it("creates a query endpoint with payload", () => {
105+ const factory = createRpcFactory({ schema: testSchema });
106+
107+ const endpoint = factory.query({
108+ payload: { name: Schema.String },
109+ success: Schema.String,
110+ });
111+
112+ expect(endpoint.__unbuilt).toBe(true);
113+ expect(endpoint.payloadFields).toHaveProperty("name");
114+ });
115+
116+ it("creates a query endpoint with error schema", () => {
117+ class CustomError extends Schema.TaggedError<CustomError>()(
118+ "CustomError",
119+ { message: Schema.String },
120+ ) {}
121+
122+ const factory = createRpcFactory({ schema: testSchema });
123+
124+ const endpoint = factory.query({
125+ success: Schema.String,
126+ error: CustomError,
127+ });
128+
129+ expect(endpoint.__unbuilt).toBe(true);
130+ expect(endpoint.errorSchema).toBe(CustomError);
131+ });
132+ });
133+
134+ describe("mutation endpoint", () => {
135+ it("creates a mutation endpoint", () => {
136+ const factory = createRpcFactory({ schema: testSchema });
137+
138+ const endpoint = factory.mutation({
139+ payload: { value: Schema.Number },
140+ success: Schema.Boolean,
141+ });
142+
143+ expect(endpoint.__unbuilt).toBe(true);
144+ expect(endpoint.kind).toBe("mutation");
145+ });
146+ });
147+
148+ describe("action endpoint", () => {
149+ it("creates an action endpoint", () => {
150+ const factory = createRpcFactory({ schema: testSchema });
151+
152+ const endpoint = factory.action({ success: Schema.Void });
153+
154+ expect(endpoint.__unbuilt).toBe(true);
155+ expect(endpoint.kind).toBe("action");
156+ });
157+ });
158+
159+ describe("internal endpoints", () => {
160+ it("creates internal query endpoint", () => {
161+ const factory = createRpcFactory({ schema: testSchema });
162+
163+ const endpoint = factory.internalQuery({ success: Schema.Number });
164+
165+ expect(endpoint.__unbuilt).toBe(true);
166+ expect(endpoint.kind).toBe("internalQuery");
167+ });
168+
169+ it("creates internal mutation endpoint", () => {
170+ const factory = createRpcFactory({ schema: testSchema });
171+
172+ const endpoint = factory.internalMutation({ success: Schema.Void });
173+
174+ expect(endpoint.__unbuilt).toBe(true);
175+ expect(endpoint.kind).toBe("internalMutation");
176+ });
177+
178+ it("creates internal action endpoint", () => {
179+ const factory = createRpcFactory({ schema: testSchema });
180+
181+ const endpoint = factory.internalAction({ success: Schema.String });
182+
183+ expect(endpoint.__unbuilt).toBe(true);
184+ expect(endpoint.kind).toBe("internalAction");
185+ });
186+ });
187+ });
188+
189+ describe("makeRpcModule", () => {
190+ it("builds endpoints with correct tags", () => {
191+ const factory = createRpcFactory({ schema: testSchema });
192+
193+ const getItems = factory.query({ success: Schema.Array(Schema.String) });
194+ getItems.implement(() => Effect.succeed([]));
195+
196+ const addItem = factory.mutation({
197+ payload: { name: Schema.String },
198+ success: Schema.String,
199+ });
200+ addItem.implement((args) => Effect.succeed(args.name));
201+
202+ const module = makeRpcModule({ getItems, addItem });
203+
204+ expect(module.getItems._tag).toBe("getItems");
205+ expect(module.addItem._tag).toBe("addItem");
206+ });
207+
208+ it("provides handlers for each endpoint", () => {
209+ const factory = createRpcFactory({ schema: testSchema });
210+
211+ const echo = factory.query({
212+ payload: { message: Schema.String },
213+ success: Schema.String,
214+ });
215+ echo.implement((args) => Effect.succeed(args.message));
216+
217+ const module = makeRpcModule({ echo });
218+
219+ expect(module.handlers.echo).toBeDefined();
220+ });
221+
222+ it("provides rpcs for each endpoint", () => {
223+ const factory = createRpcFactory({ schema: testSchema });
224+
225+ const myQuery = factory.query({ success: Schema.Void });
226+ myQuery.implement(() => Effect.void);
227+
228+ const module = makeRpcModule({ myQuery });
229+
230+ expect(module.rpcs.myQuery).toBeDefined();
231+ });
232+
233+ it("provides a group for all rpcs", () => {
234+ const factory = createRpcFactory({ schema: testSchema });
235+
236+ const first = factory.query({ success: Schema.String });
237+ first.implement(() => Effect.succeed("1"));
238+
239+ const second = factory.query({ success: Schema.Number });
240+ second.implement(() => Effect.succeed(2));
241+
242+ const module = makeRpcModule({ first, second });
243+
244+ expect(module.group).toBeDefined();
245+ });
246+ });
247+
248+ describe("basePayload", () => {
249+ it("merges base payload with endpoint payload", () => {
250+ const factory = createRpcFactory({
251+ schema: testSchema,
252+ basePayload: {
253+ tenantId: Schema.String,
254+ },
255+ });
256+
257+ const endpoint = factory.query({
258+ payload: { itemId: Schema.String },
259+ success: Schema.Void,
260+ });
261+ endpoint.implement((args) => {
262+ expect(args.tenantId).toBeDefined();
263+ expect(args.itemId).toBeDefined();
264+ return Effect.void;
265+ });
266+
267+ expect(endpoint.payloadFields).toHaveProperty("tenantId");
268+ expect(endpoint.payloadFields).toHaveProperty("itemId");
269+ });
270+ });
271+
272+ describe("ExitEncoded format", () => {
273+ const factory = createRpcFactory({ schema: testSchema });
274+
275+ it("encodes success correctly", async () => {
276+ const test = factory.query({ success: Schema.String });
277+ test.implement(() => Effect.succeed("hello"));
278+
279+ const module = makeRpcModule({ test });
280+
281+ const ctx = createMockCtx();
282+ const handler = getHandler(module.handlers.test);
283+ const result = await handler(ctx, {});
284+
285+ const success = assertSuccess(result);
286+ expect(success.value).toBe("hello");
287+ });
288+
289+ it("encodes failure correctly", async () => {
290+ class TestError extends Schema.TaggedError<TestError>()("TestError", {
291+ code: Schema.Number,
292+ }) {}
293+
294+ const test = factory.query({ success: Schema.String, error: TestError });
295+ test.implement(() => Effect.fail(new TestError({ code: 500 })));
296+
297+ const module = makeRpcModule({ test });
298+
299+ const ctx = createMockCtx();
300+ const handler = getHandler(module.handlers.test);
301+ const result = await handler(ctx, {});
302+
303+ const failure = assertFailure(result);
304+ expect(failure.cause).toEqual({
305+ _tag: "Fail",
306+ error: { _tag: "TestError", code: 500 },
307+ });
308+ });
309+
310+ it("encodes defects (thrown errors) correctly", async () => {
311+ const test = factory.query({ success: Schema.Void });
312+ test.implement(() => Effect.die(new Error("unexpected error")));
313+
314+ const module = makeRpcModule({ test });
315+
316+ const ctx = createMockCtx();
317+ const handler = getHandler(module.handlers.test);
318+ const result = await handler(ctx, {});
319+
320+ const failure = assertFailure(result);
321+ expect(failure.cause).toMatchObject({
322+ _tag: "Die",
323+ defect: {
324+ name: "Error",
325+ message: "unexpected error",
326+ },
327+ });
328+ });
329+
330+ it("handles payload decode errors as defects", async () => {
331+ const test = factory.query({
332+ payload: { num: Schema.Number },
333+ success: Schema.Void,
334+ });
335+ test.implement(() => Effect.void);
336+
337+ const module = makeRpcModule({ test });
338+
339+ const ctx = createMockCtx();
340+ const handler = getHandler(module.handlers.test);
341+ const result = await handler(ctx, { num: "not a number" });
342+
343+ const failure = assertFailure(result);
344+ expect(failure.cause).toHaveProperty("_tag", "Die");
345+ });
346+ });
347+
348+ describe("complex success/error schemas", () => {
349+ const factory = createRpcFactory({ schema: testSchema });
350+
351+ it("handles array success schema", async () => {
352+ const ItemSchema = Schema.Struct({
353+ id: Schema.String,
354+ name: Schema.String,
355+ });
356+
357+ const getItems = factory.query({ success: Schema.Array(ItemSchema) });
358+ getItems.implement(() =>
359+ Effect.succeed([
360+ { id: "1", name: "Item 1" },
361+ { id: "2", name: "Item 2" },
362+ ]),
363+ );
364+
365+ const module = makeRpcModule({ getItems });
366+
367+ const ctx = createMockCtx();
368+ const handler = getHandler(module.handlers.getItems);
369+ const result = await handler(ctx, {});
370+
371+ const success = assertSuccess(result);
372+ expect(success.value).toEqual([
373+ { id: "1", name: "Item 1" },
374+ { id: "2", name: "Item 2" },
375+ ]);
376+ });
377+
378+ it("handles union error schema", async () => {
379+ class NotFoundError extends Schema.TaggedError<NotFoundError>()(
380+ "NotFoundError",
381+ { id: Schema.String },
382+ ) {}
383+
384+ class ForbiddenError extends Schema.TaggedError<ForbiddenError>()(
385+ "ForbiddenError",
386+ {},
387+ ) {}
388+
389+ const ErrorSchema = Schema.Union(NotFoundError, ForbiddenError);
390+
391+ const getItem = factory.query({
392+ payload: { id: Schema.String },
393+ success: Schema.Void,
394+ error: ErrorSchema,
395+ });
396+ getItem.implement((args) =>
397+ Effect.fail(new NotFoundError({ id: args.id })),
398+ );
399+
400+ const module = makeRpcModule({ getItem });
401+
402+ const ctx = createMockCtx();
403+ const handler = getHandler(module.handlers.getItem);
404+ const result = await handler(ctx, { id: "123" });
405+
406+ const failure = assertFailure(result);
407+ expect(failure.cause).toEqual({
408+ _tag: "Fail",
409+ error: { _tag: "NotFoundError", id: "123" },
410+ });
411+ });
412+
413+ it("handles optional fields in payload", async () => {
414+ const search = factory.query({
415+ payload: {
416+ query: Schema.String,
417+ limit: Schema.optional(Schema.Number),
418+ },
419+ success: Schema.Array(Schema.String),
420+ });
421+ search.implement((args) => {
422+ const limit = args.limit ?? 10;
423+ return Effect.succeed(
424+ Array(limit)
425+ .fill(null)
426+ .map((_, i) => `${args.query}-${i}`),
427+ );
428+ });
429+
430+ const module = makeRpcModule({ search });
431+
432+ const ctx = createMockCtx();
433+ const handler = getHandler(module.handlers.search);
434+
435+ const resultWithLimit = await handler(ctx, {
436+ query: "test",
437+ limit: 3,
438+ });
439+ const successWithLimit = assertSuccess(resultWithLimit);
440+ expect((successWithLimit.value as Array<string>).length).toBe(3);
441+
442+ const resultWithoutLimit = await handler(ctx, {
443+ query: "test",
444+ });
445+ const successWithoutLimit = assertSuccess(resultWithoutLimit);
446+ expect((successWithoutLimit.value as Array<string>).length).toBe(10);
447+ });
448+ });
449+
450+ describe("middleware", () => {
451+ class CurrentUser extends Context.Tag("CurrentUser")<
452+ CurrentUser,
453+ { id: string; name: string }
454+ >() {}
455+
456+ class AuthMiddleware extends RpcMiddleware.Tag<AuthMiddleware>()(
457+ "AuthMiddleware",
458+ {
459+ provides: CurrentUser,
460+ },
461+ ) {}
462+
463+ it("provides service from middleware via Layer", async () => {
464+ const factory = createRpcFactory({ schema: testSchema });
465+
466+ const CurrentUserLive = Layer.succeed(
467+ CurrentUser,
468+ { id: "user-123", name: "Test User" },
469+ );
470+
471+ const whoami = factory.query({ success: Schema.String });
472+ whoami.implement(() =>
473+ Effect.gen(function* () {
474+ const user = yield* CurrentUser;
475+ return `Hello, ${user.name}!`;
476+ }),
477+ );
478+
479+ const module = makeRpcModule(
480+ {
481+ whoami: whoami.middleware(AuthMiddleware),
482+ },
483+ { middlewares: CurrentUserLive },
484+ );
485+
486+ const ctx = createMockCtx();
487+ const handler = getHandler(module.handlers.whoami);
488+ const result = await handler(ctx, {});
489+
490+ const success = assertSuccess(result);
491+ expect(success.value).toBe("Hello, Test User!");
492+ });
493+
494+ it("handler can access payload and middleware services", async () => {
495+ let receivedItemId: string | null = null;
496+
497+ const factory = createRpcFactory({ schema: testSchema });
498+
499+ const CurrentUserLive = Layer.succeed(
500+ CurrentUser,
501+ { id: "user-123", name: "Test User" },
502+ );
503+
504+ const test = factory.query({
505+ payload: { itemId: Schema.String },
506+ success: Schema.String,
507+ });
508+ test.implement((args) =>
509+ Effect.gen(function* () {
510+ receivedItemId = args.itemId;
511+ const user = yield* CurrentUser;
512+ return `${user.name} accessed ${args.itemId}`;
513+ }),
514+ );
515+
516+ const module = makeRpcModule(
517+ {
518+ test: test.middleware(AuthMiddleware),
519+ },
520+ { middlewares: CurrentUserLive },
521+ );
522+
523+ const ctx = createMockCtx();
524+ const handler = getHandler(module.handlers.test);
525+ const result = await handler(ctx, { itemId: "item-456" });
526+
527+ const success = assertSuccess(result);
528+ expect(receivedItemId).toEqual("item-456");
529+ expect(success.value).toBe("Test User accessed item-456");
530+ });
531+
532+ it("layer failure is encoded correctly", async () => {
533+ class AuthError extends Schema.TaggedError<AuthError>()("AuthError", {
534+ reason: Schema.String,
535+ }) {}
536+
537+ class FailingAuthMiddleware extends RpcMiddleware.Tag<FailingAuthMiddleware>()(
538+ "FailingAuthMiddleware",
539+ {
540+ provides: CurrentUser,
541+ failure: AuthError,
542+ },
543+ ) {}
544+
545+ const factory = createRpcFactory({ schema: testSchema });
546+
547+ const FailingAuthLive = Layer.fail(new AuthError({ reason: "Invalid token" }));
548+
549+ const protectedEndpoint = factory.query({
550+ success: Schema.String,
551+ error: AuthError,
552+ });
553+ protectedEndpoint.implement(() =>
554+ Effect.gen(function* () {
555+ yield* CurrentUser;
556+ return "secret";
557+ }),
558+ );
559+
560+ const module = makeRpcModule(
561+ {
562+ protected: protectedEndpoint.middleware(FailingAuthMiddleware),
563+ },
564+ { middlewares: FailingAuthLive },
565+ );
566+
567+ const ctx = createMockCtx();
568+ const handler = getHandler(module.handlers.protected);
569+ const result = await handler(ctx, {});
570+
571+ const failure = assertFailure(result);
572+ expect(failure.cause).toMatchObject({
573+ _tag: "Fail",
574+ error: { _tag: "AuthError", reason: "Invalid token" },
575+ });
576+ });
577+
578+ it("multiple services are provided via merged Layers", async () => {
579+ const executionOrder: Array<string> = [];
580+
581+ class Logger extends Context.Tag("Logger")<Logger, { log: (msg: string) => void }>() {}
582+
583+ class LoggerMiddleware extends RpcMiddleware.Tag<LoggerMiddleware>()(
584+ "LoggerMiddleware",
585+ {
586+ provides: Logger,
587+ },
588+ ) {}
589+
590+ const factory = createRpcFactory({ schema: testSchema });
591+
592+ const CurrentUserLive = Layer.succeed(
593+ CurrentUser,
594+ { id: "user-123", name: "Test User" },
595+ );
596+
597+ const LoggerLive = Layer.succeed(
598+ Logger,
599+ { log: (msg: string) => executionOrder.push(`log:${msg}`) },
600+ );
601+
602+ const ServicesLive = Layer.mergeAll(CurrentUserLive, LoggerLive);
603+
604+ const test = factory.query({ success: Schema.String });
605+ test.implement(() =>
606+ Effect.gen(function* () {
607+ const user = yield* CurrentUser;
608+ const logger = yield* Logger;
609+ logger.log("accessed");
610+ return user.name;
611+ }),
612+ );
613+
614+ const module = makeRpcModule(
615+ {
616+ test: test
617+ .middleware(AuthMiddleware)
618+ .middleware(LoggerMiddleware),
619+ },
620+ { middlewares: ServicesLive },
621+ );
622+
623+ const ctx = createMockCtx();
624+ const handler = getHandler(module.handlers.test);
625+ const result = await handler(ctx, {});
626+
627+ const success = assertSuccess(result);
628+ expect(success.value).toBe("Test User");
629+ expect(executionOrder).toContain("log:accessed");
630+ });
631+
632+ it("works without any middleware", async () => {
633+ const factory = createRpcFactory({ schema: testSchema });
634+
635+ const simple = factory.query({ success: Schema.Number });
636+ simple.implement(() => Effect.succeed(42));
637+
638+ const module = makeRpcModule({ simple });
639+
640+ const ctx = createMockCtx();
641+ const handler = getHandler(module.handlers.simple);
642+ const result = await handler(ctx, {});
643+
644+ const success = assertSuccess(result);
645+ expect(success.value).toBe(42);
646+ });
647+ });
648+
649+ describe("security and error encoding", () => {
650+ const factory = createRpcFactory({ schema: testSchema });
651+
652+ it("does not expose error stack traces in defects", async () => {
653+ const test = factory.query({ success: Schema.Void });
654+ test.implement(() => {
655+ const error = new Error("Internal error with sensitive info");
656+ error.stack = "Error: Internal error\n at secret/path/to/file.ts:42:13";
657+ return Effect.die(error);
658+ });
659+
660+ const module = makeRpcModule({ test });
661+
662+ const ctx = createMockCtx();
663+ const handler = getHandler(module.handlers.test);
664+ const result = await handler(ctx, {});
665+
666+ const failure = assertFailure(result);
667+ expect(failure.cause).toMatchObject({
668+ _tag: "Die",
669+ defect: {
670+ name: "Error",
671+ message: "Internal error with sensitive info",
672+ },
673+ });
674+ const causeStr = JSON.stringify(failure.cause);
675+ expect(causeStr).not.toContain("stack");
676+ expect(causeStr).not.toContain("secret/path");
677+ });
678+
679+ it("serializes non-Error defects safely", async () => {
680+ const test = factory.query({ success: Schema.Void });
681+ test.implement(() => Effect.die("raw string error"));
682+
683+ const module = makeRpcModule({ test });
684+
685+ const ctx = createMockCtx();
686+ const handler = getHandler(module.handlers.test);
687+ const result = await handler(ctx, {});
688+
689+ const failure = assertFailure(result);
690+ expect(failure.cause).toMatchObject({
691+ _tag: "Die",
692+ defect: "raw string error",
693+ });
694+ });
695+
696+ it("serializes object defects as JSON strings", async () => {
697+ const test = factory.query({ success: Schema.Void });
698+ test.implement(() =>
699+ Effect.die({ _tag: "CustomDefect", code: 500, details: "some details" }),
700+ );
701+
702+ const module = makeRpcModule({ test });
703+
704+ const ctx = createMockCtx();
705+ const handler = getHandler(module.handlers.test);
706+ const result = await handler(ctx, {});
707+
708+ const failure = assertFailure(result);
709+ expect(failure.cause).toMatchObject({
710+ _tag: "Die",
711+ });
712+ expect(typeof (failure.cause as { defect: unknown }).defect).toBe("string");
713+ expect((failure.cause as { defect: string }).defect).toContain("CustomDefect");
714+ });
715+
716+ it("encodes interrupt cause correctly", async () => {
717+ const test = factory.query({ success: Schema.Void });
718+ test.implement(() => Effect.interrupt);
719+
720+ const module = makeRpcModule({ test });
721+
722+ const ctx = createMockCtx();
723+ const handler = getHandler(module.handlers.test);
724+ const result = await handler(ctx, {});
725+
726+ const failure = assertFailure(result);
727+ expect(failure.cause).toHaveProperty("_tag", "Interrupt");
728+ });
729+
730+ it("encodes tagged errors with their schema", async () => {
731+ class ValidationError extends Schema.TaggedError<ValidationError>()(
732+ "ValidationError",
733+ {
734+ field: Schema.String,
735+ message: Schema.String,
736+ },
737+ ) {}
738+
739+ const test = factory.query({
740+ success: Schema.Void,
741+ error: ValidationError,
742+ });
743+ test.implement(() =>
744+ Effect.fail(new ValidationError({ field: "email", message: "Invalid format" })),
745+ );
746+
747+ const module = makeRpcModule({ test });
748+
749+ const ctx = createMockCtx();
750+ const handler = getHandler(module.handlers.test);
751+ const result = await handler(ctx, {});
752+
753+ const failure = assertFailure(result);
754+ expect(failure.cause).toMatchObject({
755+ _tag: "Fail",
756+ error: {
757+ _tag: "ValidationError",
758+ field: "email",
759+ message: "Invalid format",
760+ },
761+ });
762+ });
763+
764+ it("handles thrown sync exceptions as defects", async () => {
765+ const test = factory.query({ success: Schema.Void });
766+ test.implement(() =>
767+ Effect.sync(() => {
768+ throw new Error("Sync throw");
769+ }),
770+ );
771+
772+ const module = makeRpcModule({ test });
773+
774+ const ctx = createMockCtx();
775+ const handler = getHandler(module.handlers.test);
776+ const result = await handler(ctx, {});
777+
778+ const failure = assertFailure(result);
779+ expect(failure.cause).toMatchObject({
780+ _tag: "Die",
781+ defect: {
782+ name: "Error",
783+ message: "Sync throw",
784+ },
785+ });
786+ });
787+
788+ it("handles promise rejections as defects", async () => {
789+ const test = factory.query({ success: Schema.Void });
790+ test.implement(() =>
791+ Effect.promise(() => Promise.reject(new Error("Promise rejection"))),
792+ );
793+
794+ const module = makeRpcModule({ test });
795+
796+ const ctx = createMockCtx();
797+ const handler = getHandler(module.handlers.test);
798+ const result = await handler(ctx, {});
799+
800+ const failure = assertFailure(result);
801+ expect(failure.cause).toMatchObject({
802+ _tag: "Die",
803+ defect: {
804+ name: "Error",
805+ message: "Promise rejection",
806+ },
807+ });
808+ });
809+
810+ it("encodes nested error structures properly", async () => {
811+ class OuterError extends Schema.TaggedError<OuterError>()("OuterError", {
812+ inner: Schema.Struct({
813+ code: Schema.Number,
814+ details: Schema.Array(Schema.String),
815+ }),
816+ }) {}
817+
818+ const test = factory.query({
819+ success: Schema.Void,
820+ error: OuterError,
821+ });
822+ test.implement(() =>
823+ Effect.fail(
824+ new OuterError({
825+ inner: {
826+ code: 422,
827+ details: ["Field 1 invalid", "Field 2 missing"],
828+ },
829+ }),
830+ ),
831+ );
832+
833+ const module = makeRpcModule({ test });
834+
835+ const ctx = createMockCtx();
836+ const handler = getHandler(module.handlers.test);
837+ const result = await handler(ctx, {});
838+
839+ const failure = assertFailure(result);
840+ expect(failure.cause).toMatchObject({
841+ _tag: "Fail",
842+ error: {
843+ _tag: "OuterError",
844+ inner: {
845+ code: 422,
846+ details: ["Field 1 invalid", "Field 2 missing"],
847+ },
848+ },
849+ });
850+ });
851+ });
852+
853+ describe("@effect/rpc API compatibility", () => {
854+ describe("exitSchema", () => {
855+ it("returns exit schema for an RPC", () => {
856+ const rpc = Rpc.make("TestRpc", {
857+ payload: { id: Schema.String },
858+ success: Schema.Number,
859+ error: Schema.String,
860+ });
861+
862+ const schema = exitSchema(rpc);
863+ expect(schema).toBeDefined();
864+ });
865+ });
866+
867+ describe("wrapper utilities", () => {
868+ it("fork wraps an Effect", () => {
869+ const effect = Effect.succeed(42);
870+ const wrapped = fork(effect);
871+
872+ expect(isWrapper(wrapped)).toBe(true);
873+ expect(wrapped.fork).toBe(true);
874+ expect(wrapped.value).toBe(effect);
875+ });
876+
877+ it("uninterruptible wraps an Effect", () => {
878+ const effect = Effect.succeed(42);
879+ const wrapped = uninterruptible(effect);
880+
881+ expect(isWrapper(wrapped)).toBe(true);
882+ expect(wrapped.uninterruptible).toBe(true);
883+ expect(wrapped.value).toBe(effect);
884+ });
885+
886+ it("wrap with custom options", () => {
887+ const effect = Effect.succeed(42);
888+ const wrapped = wrap({ fork: true, uninterruptible: true })(effect);
889+
890+ expect(isWrapper(wrapped)).toBe(true);
891+ expect(wrapped.fork).toBe(true);
892+ expect(wrapped.uninterruptible).toBe(true);
893+ });
894+
895+ it("isWrapper returns false for non-wrappers", () => {
896+ expect(isWrapper({})).toBe(false);
897+ expect(isWrapper({ fork: true })).toBe(false);
898+ });
899+ });
900+
901+ });
902+
903+ describe("middleware chaining on individual RPCs", () => {
904+ it("allows chaining .middleware() on individual endpoints", () => {
905+ class AuthMiddleware extends RpcMiddleware.Tag<AuthMiddleware>()(
906+ "AuthMiddleware",
907+ {},
908+ ) {}
909+
910+ class LoggingMiddleware extends RpcMiddleware.Tag<LoggingMiddleware>()(
911+ "LoggingMiddleware",
912+ {},
913+ ) {}
914+
915+ const factory = createRpcFactory({ schema: testSchema });
916+
917+ const endpoint = factory
918+ .query({ success: Schema.String })
919+ .middleware(AuthMiddleware)
920+ .middleware(LoggingMiddleware);
921+
922+ expect(endpoint.__unbuilt).toBe(true);
923+ expect(endpoint.middlewares).toHaveLength(2);
924+ expect(endpoint.middlewares[0]).toBe(AuthMiddleware);
925+ expect(endpoint.middlewares[1]).toBe(LoggingMiddleware);
926+ });
927+ });
928+
929+ describe("per-request middleware execution", () => {
930+ class CurrentUser extends Context.Tag("CurrentUser")<
931+ CurrentUser,
932+ { id: string; name: string }
933+ >() {}
934+
935+ class AuthMiddleware extends RpcMiddleware.Tag<AuthMiddleware>()(
936+ "AuthMiddleware",
937+ {
938+ provides: CurrentUser,
939+ },
940+ ) {}
941+
942+ it("executes middleware function per request", async () => {
943+ const { middleware } = await import("./server");
944+ const factory = createRpcFactory({ schema: testSchema });
945+ let callCount = 0;
946+
947+ const authImpl = middleware(AuthMiddleware, (options) => {
948+ callCount++;
949+ return Effect.succeed({ id: `user-${callCount}`, name: `User ${callCount}` });
950+ });
951+
952+ const whoami = factory.query({ success: Schema.String });
953+ whoami.implement(() =>
954+ Effect.gen(function* () {
955+ const user = yield* CurrentUser;
956+ return user.name;
957+ }),
958+ );
959+
960+ const module = makeRpcModule(
961+ {
962+ whoami: whoami.middleware(AuthMiddleware),
963+ },
964+ { middlewares: [authImpl] },
965+ );
966+
967+ const ctx = createMockCtx();
968+ const handler = getHandler(module.handlers.whoami);
969+
970+ const result1 = await handler(ctx, {});
971+ const success1 = assertSuccess(result1);
972+ expect(success1.value).toBe("User 1");
973+
974+ const result2 = await handler(ctx, {});
975+ const success2 = assertSuccess(result2);
976+ expect(success2.value).toBe("User 2");
977+
978+ expect(callCount).toBe(2);
979+ });
980+
981+ it("middleware receives payload", async () => {
982+ const { middleware } = await import("./server");
983+ const factory = createRpcFactory({ schema: testSchema });
984+ let receivedPayload: unknown = null;
985+
986+ const authImpl = middleware(AuthMiddleware, (options) => {
987+ receivedPayload = options.payload;
988+ return Effect.succeed({ id: "user-1", name: "Test" });
989+ });
990+
991+ const test = factory.query({
992+ payload: { itemId: Schema.String },
993+ success: Schema.String,
994+ });
995+ test.implement((args) =>
996+ Effect.gen(function* () {
997+ const user = yield* CurrentUser;
998+ return `${user.name} accessed ${args.itemId}`;
999+ }),
1000+ );
1001+
1002+ const module = makeRpcModule(
1003+ {
1004+ test: test.middleware(AuthMiddleware),
1005+ },
1006+ { middlewares: [authImpl] },
1007+ );
1008+
1009+ const ctx = createMockCtx();
1010+ const handler = getHandler(module.handlers.test);
1011+ await handler(ctx, { itemId: "item-456" });
1012+
1013+ expect(receivedPayload).toEqual({ itemId: "item-456" });
1014+ });
1015+
1016+ it("middleware receives rpc info", async () => {
1017+ const { middleware } = await import("./server");
1018+ const factory = createRpcFactory({ schema: testSchema });
1019+ let receivedRpcInfo: unknown = null;
1020+
1021+ const authImpl = middleware(AuthMiddleware, (options) => {
1022+ receivedRpcInfo = options.rpc;
1023+ return Effect.succeed({ id: "user-1", name: "Test" });
1024+ });
1025+
1026+ const myEndpoint = factory.query({ success: Schema.String });
1027+ myEndpoint.implement(() =>
1028+ Effect.gen(function* () {
1029+ yield* CurrentUser;
1030+ return "ok";
1031+ }),
1032+ );
1033+
1034+ const module = makeRpcModule(
1035+ {
1036+ myEndpoint: myEndpoint.middleware(AuthMiddleware),
1037+ },
1038+ { middlewares: [authImpl] },
1039+ );
1040+
1041+ const ctx = createMockCtx();
1042+ const handler = getHandler(module.handlers.myEndpoint);
1043+ await handler(ctx, {});
1044+
1045+ expect(receivedRpcInfo).toEqual({ tag: "myEndpoint", kind: "query" });
1046+ });
1047+
1048+ it("middleware receives convex context", async () => {
1049+ const { middleware } = await import("./server");
1050+ const factory = createRpcFactory({ schema: testSchema });
1051+ let receivedCtx: unknown = null;
1052+
1053+ const authImpl = middleware(AuthMiddleware, (options) => {
1054+ receivedCtx = options.ctx;
1055+ return Effect.succeed({ id: "user-1", name: "Test" });
1056+ });
1057+
1058+ const test = factory.query({ success: Schema.String });
1059+ test.implement(() =>
1060+ Effect.gen(function* () {
1061+ yield* CurrentUser;
1062+ return "ok";
1063+ }),
1064+ );
1065+
1066+ const module = makeRpcModule(
1067+ {
1068+ test: test.middleware(AuthMiddleware),
1069+ },
1070+ { middlewares: [authImpl] },
1071+ );
1072+
1073+ const mockCtx = createMockCtx();
1074+ const handler = getHandler(module.handlers.test);
1075+ await handler(mockCtx, {});
1076+
1077+ expect(receivedCtx).toBe(mockCtx);
1078+ });
1079+
1080+ it("middleware can fail with typed error", async () => {
1081+ const { middleware } = await import("./server");
1082+
1083+ class AuthError extends Schema.TaggedError<AuthError>()("AuthError", {
1084+ reason: Schema.String,
1085+ }) {}
1086+
1087+ class FailingAuthMiddleware extends RpcMiddleware.Tag<FailingAuthMiddleware>()(
1088+ "FailingAuthMiddleware",
1089+ {
1090+ provides: CurrentUser,
1091+ failure: AuthError,
1092+ },
1093+ ) {}
1094+
1095+ const factory = createRpcFactory({ schema: testSchema });
1096+
1097+ const authImpl = middleware(FailingAuthMiddleware, () =>
1098+ Effect.fail(new AuthError({ reason: "Invalid token" })),
1099+ );
1100+
1101+ const protectedEndpoint = factory.query({
1102+ success: Schema.String,
1103+ error: AuthError,
1104+ });
1105+ protectedEndpoint.implement(() =>
1106+ Effect.gen(function* () {
1107+ yield* CurrentUser;
1108+ return "secret";
1109+ }),
1110+ );
1111+
1112+ const module = makeRpcModule(
1113+ {
1114+ protected: protectedEndpoint.middleware(FailingAuthMiddleware),
1115+ },
1116+ { middlewares: [authImpl] },
1117+ );
1118+
1119+ const ctx = createMockCtx();
1120+ const handler = getHandler(module.handlers.protected);
1121+ const result = await handler(ctx, {});
1122+
1123+ const failure = assertFailure(result);
1124+ expect(failure.cause).toMatchObject({
1125+ _tag: "Fail",
1126+ error: { _tag: "AuthError", reason: "Invalid token" },
1127+ });
1128+ });
1129+
1130+ it("multiple per-request middlewares execute in order", async () => {
1131+ const { middleware } = await import("./server");
1132+ const executionOrder: Array<string> = [];
1133+
1134+ class Logger extends Context.Tag("Logger")<Logger, { log: (msg: string) => void }>() {}
1135+
1136+ class LoggerMiddleware extends RpcMiddleware.Tag<LoggerMiddleware>()(
1137+ "LoggerMiddleware",
1138+ { provides: Logger },
1139+ ) {}
1140+
1141+ const factory = createRpcFactory({ schema: testSchema });
1142+
1143+ const authImpl = middleware(AuthMiddleware, () => {
1144+ executionOrder.push("auth");
1145+ return Effect.succeed({ id: "user-1", name: "Test" });
1146+ });
1147+
1148+ const loggerImpl = middleware(LoggerMiddleware, () => {
1149+ executionOrder.push("logger");
1150+ return Effect.succeed({ log: (msg: string) => executionOrder.push(`log:${msg}`) });
1151+ });
1152+
1153+ const test = factory.query({ success: Schema.String });
1154+ test.implement(() =>
1155+ Effect.gen(function* () {
1156+ const user = yield* CurrentUser;
1157+ const logger = yield* Logger;
1158+ logger.log("handler");
1159+ return user.name;
1160+ }),
1161+ );
1162+
1163+ const module = makeRpcModule(
1164+ {
1165+ test: test
1166+ .middleware(AuthMiddleware)
1167+ .middleware(LoggerMiddleware),
1168+ },
1169+ { middlewares: [authImpl, loggerImpl] },
1170+ );
1171+
1172+ const ctx = createMockCtx();
1173+ const handler = getHandler(module.handlers.test);
1174+ await handler(ctx, {});
1175+
1176+ expect(executionOrder).toEqual(["auth", "logger", "log:handler"]);
1177+ });
1178+
1179+ it("supports mixed static layer and per-request middleware", async () => {
1180+ const { middleware } = await import("./server");
1181+
1182+ class StaticService extends Context.Tag("StaticService")<
1183+ StaticService,
1184+ { value: string }
1185+ >() {}
1186+
1187+ class StaticMiddleware extends RpcMiddleware.Tag<StaticMiddleware>()(
1188+ "StaticMiddleware",
1189+ { provides: StaticService },
1190+ ) {}
1191+
1192+ const factory = createRpcFactory({ schema: testSchema });
1193+ let perRequestCallCount = 0;
1194+
1195+ const authImpl = middleware(AuthMiddleware, () => {
1196+ perRequestCallCount++;
1197+ return Effect.succeed({ id: `user-${perRequestCallCount}`, name: `User ${perRequestCallCount}` });
1198+ });
1199+
1200+ const staticLayer = Layer.succeed(StaticService, { value: "static-value" });
1201+
1202+ const test = factory.query({ success: Schema.String });
1203+ test.implement(() =>
1204+ Effect.gen(function* () {
1205+ const user = yield* CurrentUser;
1206+ const static_ = yield* StaticService;
1207+ return `${user.name}: ${static_.value}`;
1208+ }),
1209+ );
1210+
1211+ const module = makeRpcModule(
1212+ {
1213+ test: test
1214+ .middleware(AuthMiddleware)
1215+ .middleware(StaticMiddleware),
1216+ },
1217+ {
1218+ middlewares: {
1219+ implementations: [authImpl],
1220+ layer: staticLayer,
1221+ },
1222+ },
1223+ );
1224+
1225+ const ctx = createMockCtx();
1226+ const handler = getHandler(module.handlers.test);
1227+
1228+ const result1 = await handler(ctx, {});
1229+ const success1 = assertSuccess(result1);
1230+ expect(success1.value).toBe("User 1: static-value");
1231+
1232+ const result2 = await handler(ctx, {});
1233+ const success2 = assertSuccess(result2);
1234+ expect(success2.value).toBe("User 2: static-value");
1235+
1236+ expect(perRequestCallCount).toBe(2);
1237+ });
1238+ });
1239+});
v2/packages/confect/src/rpc/server.ts+671−0
@@ -0,0 +1,671 @@
1+import { Rpc, RpcGroup, RpcMiddleware } from "@effect/rpc";
2+import type {
3+ DefaultFunctionArgs,
4+ RegisteredMutation,
5+ RegisteredQuery,
6+ RegisteredAction,
7+ GenericQueryCtx,
8+ GenericMutationCtx,
9+ GenericActionCtx,
10+ GenericDataModel,
11+} from "convex/server";
12+import {
13+ queryGeneric,
14+ mutationGeneric,
15+ actionGeneric,
16+ internalQueryGeneric,
17+ internalMutationGeneric,
18+ internalActionGeneric,
19+} from "convex/server";
20+import { v } from "convex/values";
21+import * as Context from "effect/Context";
22+import * as Effect from "effect/Effect";
23+import * as Exit from "effect/Exit";
24+import * as Layer from "effect/Layer";
25+import * as Option from "effect/Option";
26+import * as Schema from "effect/Schema";
27+
28+import {
29+ ConfectActionCtx,
30+ ConfectMutationCtx,
31+ ConfectQueryCtx,
32+ makeQueryCtx,
33+ makeMutationCtx,
34+ makeActionCtx,
35+ type GenericConfectSchema,
36+ type TableNamesInSchema,
37+ type DocumentFromTable,
38+ type EncodedDocumentFromTable,
39+} from "../ctx";
40+import type { ConfectSchemaDefinition } from "../schema";
41+import { extractParentSpanFromPayload } from "./telemetry";
42+
43+export { Rpc, RpcGroup, RpcMiddleware };
44+
45+export {
46+ WrapperTypeId,
47+ type WrapperTypeId as WrapperTypeIdType,
48+ type Wrapper,
49+ isWrapper,
50+ wrap,
51+ fork,
52+ uninterruptible,
53+} from "@effect/rpc/Rpc";
54+
55+export type Handler<Tag extends string> = Rpc.Handler<Tag>;
56+export type ToHandler<R extends Rpc.Any> = Rpc.ToHandler<R>;
57+export type HandlersFrom<R extends Rpc.Any> = RpcGroup.HandlersFrom<R>;
58+export type HandlersContext<R extends Rpc.Any, Handlers> = RpcGroup.HandlersContext<R, Handlers>;
59+
60+type ConvexCtx = GenericQueryCtx<GenericDataModel> | GenericMutationCtx<GenericDataModel> | GenericActionCtx<GenericDataModel>;
61+
62+export interface RpcInfo {
63+ readonly tag: string;
64+ readonly kind: "query" | "mutation" | "action" | "internalQuery" | "internalMutation" | "internalAction";
65+}
66+
67+export interface MiddlewareOptions {
68+ readonly rpc: RpcInfo;
69+ readonly payload: unknown;
70+ readonly ctx: ConvexCtx;
71+}
72+
73+export interface ConfectMiddlewareFn<Service, E = never> {
74+ (options: MiddlewareOptions): Effect.Effect<Service, E>;
75+}
76+
77+export interface MiddlewareImplementation<M extends RpcMiddleware.TagClassAny = RpcMiddleware.TagClassAny> {
78+ readonly _tag: "MiddlewareImplementation";
79+ readonly middleware: M;
80+ readonly impl: ConfectMiddlewareFn<unknown, unknown>;
81+}
82+
83+export const middleware = <M extends RpcMiddleware.TagClassAny>(
84+ middlewareTag: M,
85+ impl: M extends { readonly provides: Context.Tag<infer _Id, infer S> }
86+ ? ConfectMiddlewareFn<S, M extends { readonly failure: Schema.Schema<infer E, infer _I, infer _R> } ? E : never>
87+ : ConfectMiddlewareFn<void, M extends { readonly failure: Schema.Schema<infer E, infer _I, infer _R> } ? E : never>,
88+): MiddlewareImplementation<M> => ({
89+ _tag: "MiddlewareImplementation",
90+ middleware: middlewareTag,
91+ impl: impl as ConfectMiddlewareFn<unknown, unknown>,
92+});
93+
94+type TableSchemas<Tables extends GenericConfectSchema> = {
95+ [TableName in TableNamesInSchema<Tables>]: Schema.Schema<
96+ DocumentFromTable<Tables, TableName>,
97+ EncodedDocumentFromTable<Tables, TableName>
98+ >;
99+};
100+
101+const extractTableSchemas = <Tables extends GenericConfectSchema>(
102+ tables: Tables,
103+): TableSchemas<Tables> => {
104+ const result: Record<string, Schema.Schema.AnyNoContext> = {};
105+ for (const [tableName, tableDef] of Object.entries(tables)) {
106+ result[tableName] = (tableDef as { documentSchema: Schema.Schema.AnyNoContext }).documentSchema;
107+ }
108+ return result as TableSchemas<Tables>;
109+};
110+
111+export interface RpcEndpoint<
112+ Tag extends string,
113+ R extends Rpc.Any,
114+ ConvexFn,
115+> {
116+ readonly _tag: Tag;
117+ readonly rpc: R;
118+ readonly fn: ConvexFn;
119+}
120+
121+// ---------------------------------------------------------------------------
122+// Handler ref — mutable slot filled by .implement()
123+// ---------------------------------------------------------------------------
124+
125+type HandlerFn = (payload: never) => Effect.Effect<unknown, unknown, unknown>;
126+
127+interface HandlerRef {
128+ current: HandlerFn | null;
129+ tag: string | null;
130+}
131+
132+const createHandlerRef = (): HandlerRef => ({ current: null, tag: null });
133+
134+// ---------------------------------------------------------------------------
135+// UnbuiltRpcEndpoint — definition only, no handler body
136+// ---------------------------------------------------------------------------
137+
138+/**
139+ * An endpoint whose schema has been defined but whose handler may or may not
140+ * be wired yet. Call `.implement(handler)` to provide the implementation.
141+ *
142+ * The handler is stored in a mutable ref so that the definition file (which
143+ * `api.d.ts` imports) never needs to contain handler code. The implementation
144+ * can live in a separate file, breaking circular type dependencies.
145+ */
146+export interface UnbuiltRpcEndpoint<
147+ PayloadFields extends Schema.Struct.Fields,
148+ Success extends Schema.Schema.AnyNoContext,
149+ Error extends Schema.Schema.AnyNoContext | undefined,
150+ ConvexFnType,
151+ Middlewares extends ReadonlyArray<RpcMiddleware.TagClassAny> = [],
152+> {
153+ readonly __unbuilt: true;
154+ readonly kind: string;
155+ readonly payloadFields: PayloadFields;
156+ readonly successSchema: Success;
157+ readonly errorSchema: Error | undefined;
158+ readonly middlewares: Middlewares;
159+ readonly build: (tag: string, middlewareConfig: MiddlewareConfig | undefined) => RpcEndpoint<string, Rpc.Any, ConvexFnType>;
160+ middleware<M extends RpcMiddleware.TagClassAny>(
161+ middleware: M,
162+ ): UnbuiltRpcEndpoint<PayloadFields, Success, Error, ConvexFnType, [...Middlewares, M]>;
163+
164+ /**
165+ * Wire the handler implementation for this endpoint.
166+ *
167+ * Can be called from a separate file to avoid circular type dependencies
168+ * with Convex's generated `api.d.ts`.
169+ */
170+ implement(
171+ handler: (
172+ payload: Schema.Struct.Type<PayloadFields>,
173+ ) => Effect.Effect<
174+ Schema.Schema.Type<Success>,
175+ Error extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<Error> : never,
176+ unknown
177+ >,
178+ ): void;
179+}
180+
181+export interface RpcFactoryConfig<
182+ ConfectSchema extends GenericConfectSchema,
183+ BasePayload extends Schema.Struct.Fields = {},
184+ BaseMiddlewares extends ReadonlyArray<RpcMiddleware.TagClassAny> = [],
185+> {
186+ readonly schema: ConfectSchemaDefinition<ConfectSchema>;
187+ readonly basePayload?: BasePayload;
188+ readonly baseMiddlewares?: BaseMiddlewares;
189+}
190+
191+export type ExitEncoded<A = unknown, E = unknown, D = unknown> = Schema.ExitEncoded<A, E, D>;
192+
193+type MiddlewareConfig = {
194+ readonly implementations: ReadonlyArray<MiddlewareImplementation>;
195+ readonly staticLayer?: Layer.Layer<unknown, unknown, never>;
196+};
197+
198+const StaticLayerReady = Context.GenericTag<true>("@confect/StaticLayerReady");
199+
200+const executeMiddlewares = (
201+ endpointMiddlewares: ReadonlyArray<RpcMiddleware.TagClassAny>,
202+ middlewareImpls: ReadonlyArray<MiddlewareImplementation>,
203+ options: MiddlewareOptions,
204+): Effect.Effect<Context.Context<never>, unknown, never> => {
205+ if (endpointMiddlewares.length === 0 || middlewareImpls.length === 0) {
206+ return Effect.succeed(Context.empty());
207+ }
208+
209+ return Effect.gen(function* () {
210+ let ctx: Context.Context<never> = Context.empty();
211+
212+ for (const endpointMiddleware of endpointMiddlewares) {
213+ const middlewareImpl = middlewareImpls.find(
214+ (candidate) => candidate.middleware === endpointMiddleware,
215+ );
216+ if (!middlewareImpl) {
217+ continue;
218+ }
219+
220+ const result = yield* middlewareImpl.impl(options);
221+ const providesTag = middlewareImpl.middleware.provides;
222+ if (providesTag) {
223+ ctx = Context.add(ctx, providesTag, result) as Context.Context<never>;
224+ }
225+ }
226+
227+ return ctx;
228+ });
229+};
230+
231+type EndpointKind = "query" | "mutation" | "action" | "internalQuery" | "internalMutation" | "internalAction";
232+
233+type ConvexRegistrar = (opts: { args: ReturnType<typeof v.any>; handler: (ctx: ConvexCtx, args: unknown) => Promise<ExitEncoded> }) => unknown;
234+
235+interface EndpointConfig<Ctx, ConvexFnType> {
236+ readonly kind: EndpointKind;
237+ readonly ctxTag: Context.Tag<Ctx, Ctx>;
238+ readonly makeCtx: (rawCtx: ConvexCtx) => Ctx;
239+ readonly registrar: ConvexRegistrar;
240+ readonly _convexFnType?: ConvexFnType;
241+}
242+
243+export const createRpcFactory = <
244+ ConfectSchema extends GenericConfectSchema,
245+ BasePayload extends Schema.Struct.Fields = {},
246+ BaseMiddlewares extends ReadonlyArray<RpcMiddleware.TagClassAny> = [],
247+>(
248+ config: RpcFactoryConfig<ConfectSchema, BasePayload, BaseMiddlewares>,
249+) => {
250+ const tableSchemas = extractTableSchemas(config.schema.tables);
251+ const basePayload = config.basePayload ?? ({} as BasePayload);
252+ const baseMiddlewares = config.baseMiddlewares ?? ([] as unknown as BaseMiddlewares);
253+
254+ type MergedPayload<P extends Schema.Struct.Fields> = BasePayload & P;
255+
256+ const makeConvexHandler = <
257+ PayloadFields extends Schema.Struct.Fields,
258+ Success extends Schema.Schema.AnyNoContext,
259+ Error extends Schema.Schema.AnyNoContext | undefined,
260+ Ctx,
261+ >(
262+ endpointConfig: EndpointConfig<Ctx, unknown>,
263+ tag: string,
264+ payloadFields: PayloadFields,
265+ successSchema: Success,
266+ errorSchema: Error,
267+ middlewareConfig: MiddlewareConfig | undefined,
268+ endpointMiddlewares: ReadonlyArray<RpcMiddleware.TagClassAny>,
269+ handlerRef: HandlerRef,
270+ ) => {
271+ const payloadSchema = Schema.Struct(payloadFields) as unknown as Schema.Schema<Schema.Struct.Type<PayloadFields>, Schema.Struct.Encoded<PayloadFields>, never>;
272+ const decodePayload = Schema.decodeUnknownSync(payloadSchema);
273+
274+ const exitSchemaVal = Schema.Exit({
275+ success: successSchema as Schema.Schema<unknown, unknown, never>,
276+ failure: (errorSchema ?? Schema.Never) as Schema.Schema<unknown, unknown, never>,
277+ defect: Schema.Defect,
278+ });
279+ const encodeExit = Schema.encodeSync(exitSchemaVal) as (exit: Exit.Exit<unknown, unknown>) => ExitEncoded;
280+
281+ const rpcInfo: RpcInfo = { tag, kind: endpointConfig.kind };
282+
283+ return async (rawCtx: ConvexCtx, args: unknown): Promise<ExitEncoded> => {
284+ if (!handlerRef.current) {
285+ throw new Error(
286+ `Handler not implemented for "${tag}". Call .implement() on the endpoint definition.`,
287+ );
288+ }
289+ const handler = handlerRef.current;
290+ const parentSpan = extractParentSpanFromPayload(args);
291+
292+ let decodedArgs: Schema.Struct.Type<PayloadFields>;
293+ try {
294+ decodedArgs = decodePayload(args);
295+ } catch (err) {
296+ return encodeExit(Exit.die(err));
297+ }
298+
299+ const middlewareOptions: MiddlewareOptions = {
300+ rpc: rpcInfo,
301+ payload: decodedArgs,
302+ ctx: rawCtx,
303+ };
304+
305+ const effect = Effect.gen(function* () {
306+ const middlewareCtx = middlewareConfig?.implementations?.length
307+ ? yield* executeMiddlewares(
308+ endpointMiddlewares,
309+ middlewareConfig.implementations,
310+ middlewareOptions,
311+ )
312+ : Context.empty();
313+
314+ const handlerEffect = handler(decodedArgs as never);
315+ const withMiddlewareCtx = Effect.provide(handlerEffect, middlewareCtx);
316+ const withConfectCtx = Effect.provideService(
317+ withMiddlewareCtx as Effect.Effect<Schema.Schema.Type<Success>, unknown, Ctx>,
318+ endpointConfig.ctxTag,
319+ endpointConfig.makeCtx(rawCtx),
320+ );
321+
322+ return yield* withConfectCtx;
323+ });
324+
325+ const tracedEffect = Effect.withSpan(
326+ effect,
327+ `rpc.server.${endpointConfig.kind}.${tag}`,
328+ {
329+ kind: "server",
330+ captureStackTrace: false,
331+ parent: Option.getOrUndefined(parentSpan),
332+ attributes: {
333+ "rpc.system": "convex",
334+ "rpc.method": tag,
335+ "rpc.confect.kind": endpointConfig.kind,
336+ },
337+ },
338+ );
339+
340+ const effectWithLayer = middlewareConfig?.staticLayer
341+ ? Effect.gen(function* () {
342+ yield* StaticLayerReady;
343+ return yield* tracedEffect;
344+ }).pipe(
345+ Effect.provide(
346+ Layer.mergeAll(
347+ middlewareConfig.staticLayer,
348+ Layer.succeed(StaticLayerReady, true),
349+ ),
350+ ),
351+ )
352+ : tracedEffect;
353+
354+ const exit = await Effect.runPromiseExit(effectWithLayer);
355+ return encodeExit(exit);
356+ };
357+ };
358+
359+ const createUnbuiltEndpoint = <
360+ PayloadFields extends Schema.Struct.Fields,
361+ Success extends Schema.Schema.AnyNoContext,
362+ Error extends Schema.Schema.AnyNoContext | undefined,
363+ ConvexFnType,
364+ Middlewares extends ReadonlyArray<RpcMiddleware.TagClassAny>,
365+ Ctx,
366+ >(
367+ endpointConfig: EndpointConfig<Ctx, ConvexFnType>,
368+ mergedPayload: PayloadFields,
369+ successSchema: Success,
370+ errorSchema: Error | undefined,
371+ middlewares: Middlewares,
372+ handlerRef: HandlerRef,
373+ ): UnbuiltRpcEndpoint<PayloadFields, Success, Error, ConvexFnType, Middlewares> => ({
374+ __unbuilt: true as const,
375+ kind: endpointConfig.kind,
376+ payloadFields: mergedPayload,
377+ successSchema: successSchema,
378+ errorSchema: errorSchema,
379+ middlewares,
380+ build: (tag, middlewareConfig): RpcEndpoint<string, Rpc.Any, ConvexFnType> => {
381+ handlerRef.tag = tag;
382+ const rpc = Rpc.make(tag, {
383+ payload: mergedPayload,
384+ success: successSchema,
385+ error: errorSchema,
386+ });
387+ const convexHandler = makeConvexHandler(
388+ endpointConfig,
389+ tag,
390+ mergedPayload,
391+ successSchema,
392+ errorSchema,
393+ middlewareConfig,
394+ middlewares,
395+ handlerRef,
396+ );
397+ const fn = endpointConfig.registrar({ args: v.any(), handler: convexHandler });
398+ return { _tag: tag, rpc, fn: fn as ConvexFnType };
399+ },
400+ middleware<M extends RpcMiddleware.TagClassAny>(
401+ middlewareTag: M,
402+ ): UnbuiltRpcEndpoint<PayloadFields, Success, Error, ConvexFnType, [...Middlewares, M]> {
403+ const newMiddlewares = [...middlewares, middlewareTag] as [...Middlewares, M];
404+ return createUnbuiltEndpoint(
405+ endpointConfig,
406+ mergedPayload,
407+ successSchema,
408+ errorSchema,
409+ newMiddlewares,
410+ handlerRef,
411+ );
412+ },
413+ implement(
414+ handler: (
415+ payload: Schema.Struct.Type<PayloadFields>,
416+ ) => Effect.Effect<
417+ Schema.Schema.Type<Success>,
418+ Error extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<Error> : never,
419+ unknown
420+ >,
421+ ): void {
422+ handlerRef.current = handler as HandlerFn;
423+ },
424+ });
425+
426+ /**
427+ * Create an endpoint definition method. The returned function takes only
428+ * schemas (no handler). Call `.implement(handler)` on the result to wire
429+ * the handler, potentially from a separate file.
430+ */
431+ const createEndpointMethod = <Ctx, ConvexFnType>(
432+ endpointConfig: EndpointConfig<Ctx, ConvexFnType>,
433+ ) => <
434+ PayloadFields extends Schema.Struct.Fields = {},
435+ Success extends Schema.Schema.AnyNoContext = typeof Schema.Void,
436+ Error extends Schema.Schema.AnyNoContext | undefined = undefined,
437+ >(
438+ options: {
439+ readonly payload?: PayloadFields;
440+ readonly success: Success;
441+ readonly error?: Error;
442+ },
443+ ): UnbuiltRpcEndpoint<MergedPayload<PayloadFields>, Success, Error, ConvexFnType, BaseMiddlewares> => {
444+ const mergedPayload = { ...basePayload, ...options.payload } as MergedPayload<PayloadFields>;
445+ const handlerRef = createHandlerRef();
446+ return createUnbuiltEndpoint(
447+ endpointConfig,
448+ mergedPayload,
449+ options.success,
450+ options.error,
451+ baseMiddlewares as unknown as BaseMiddlewares,
452+ handlerRef,
453+ );
454+ };
455+
456+ const queryConfig: EndpointConfig<ConfectQueryCtx<ConfectSchema>, RegisteredQuery<"public", DefaultFunctionArgs, Promise<ExitEncoded>>> = {
457+ kind: "query",
458+ ctxTag: ConfectQueryCtx<ConfectSchema>(),
459+ makeCtx: (ctx) => makeQueryCtx(ctx as GenericQueryCtx<GenericDataModel>, tableSchemas),
460+ registrar: queryGeneric,
461+ };
462+
463+ const mutationConfig: EndpointConfig<ConfectMutationCtx<ConfectSchema>, RegisteredMutation<"public", DefaultFunctionArgs, Promise<ExitEncoded>>> = {
464+ kind: "mutation",
465+ ctxTag: ConfectMutationCtx<ConfectSchema>(),
466+ makeCtx: (ctx) => makeMutationCtx(ctx as GenericMutationCtx<GenericDataModel>, tableSchemas),
467+ registrar: mutationGeneric,
468+ };
469+
470+ const actionConfig: EndpointConfig<ConfectActionCtx<ConfectSchema>, RegisteredAction<"public", DefaultFunctionArgs, Promise<ExitEncoded>>> = {
471+ kind: "action",
472+ ctxTag: ConfectActionCtx<ConfectSchema>(),
473+ makeCtx: (ctx) => makeActionCtx(ctx as GenericActionCtx<GenericDataModel>),
474+ registrar: actionGeneric,
475+ };
476+
477+ const internalQueryConfig: EndpointConfig<ConfectQueryCtx<ConfectSchema>, RegisteredQuery<"internal", DefaultFunctionArgs, Promise<ExitEncoded>>> = {
478+ kind: "internalQuery",
479+ ctxTag: ConfectQueryCtx<ConfectSchema>(),
480+ makeCtx: (ctx) => makeQueryCtx(ctx as GenericQueryCtx<GenericDataModel>, tableSchemas),
481+ registrar: internalQueryGeneric,
482+ };
483+
484+ const internalMutationConfig: EndpointConfig<ConfectMutationCtx<ConfectSchema>, RegisteredMutation<"internal", DefaultFunctionArgs, Promise<ExitEncoded>>> = {
485+ kind: "internalMutation",
486+ ctxTag: ConfectMutationCtx<ConfectSchema>(),
487+ makeCtx: (ctx) => makeMutationCtx(ctx as GenericMutationCtx<GenericDataModel>, tableSchemas),
488+ registrar: internalMutationGeneric,
489+ };
490+
491+ const internalActionConfig: EndpointConfig<ConfectActionCtx<ConfectSchema>, RegisteredAction<"internal", DefaultFunctionArgs, Promise<ExitEncoded>>> = {
492+ kind: "internalAction",
493+ ctxTag: ConfectActionCtx<ConfectSchema>(),
494+ makeCtx: (ctx) => makeActionCtx(ctx as GenericActionCtx<GenericDataModel>),
495+ registrar: internalActionGeneric,
496+ };
497+
498+ return {
499+ query: createEndpointMethod(queryConfig),
500+ mutation: createEndpointMethod(mutationConfig),
501+ action: createEndpointMethod(actionConfig),
502+ internalQuery: createEndpointMethod(internalQueryConfig),
503+ internalMutation: createEndpointMethod(internalMutationConfig),
504+ internalAction: createEndpointMethod(internalActionConfig),
505+ };
506+};
507+
508+export type InferRpc<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn> ? R : never;
509+
510+export type InferFn<E> = E extends RpcEndpoint<infer _Tag, infer _R, infer ConvexFn> ? ConvexFn : never;
511+
512+interface RpcModuleBase<
513+ Endpoints extends Record<string, RpcEndpoint<string, Rpc.Any, unknown>>,
514+> {
515+ readonly _def: {
516+ readonly endpoints: Endpoints;
517+ };
518+ readonly rpcs: { [K in keyof Endpoints]: InferRpc<Endpoints[K]> };
519+ readonly handlers: { [K in keyof Endpoints]: InferFn<Endpoints[K]> };
520+ readonly group: RpcGroup.RpcGroup<InferRpc<Endpoints[keyof Endpoints]>>;
521+}
522+
523+export type RpcModule<
524+ Endpoints extends Record<string, RpcEndpoint<string, Rpc.Any, unknown>>,
525+> = RpcModuleBase<Endpoints> & Endpoints;
526+
527+export interface AnyRpcModule {
528+ readonly _def: {
529+ readonly endpoints: Record<string, RpcEndpoint<string, Rpc.Any, unknown>>;
530+ };
531+ readonly rpcs: Record<string, Rpc.Any>;
532+ readonly handlers: Record<string, unknown>;
533+ readonly group: unknown;
534+}
535+
536+export type InferModuleEndpoints<M extends AnyRpcModule> = M["_def"]["endpoints"];
537+
538+interface AnyUnbuiltEndpoint {
539+ readonly __unbuilt: true;
540+ readonly kind: string;
541+ readonly payloadFields: Schema.Struct.Fields;
542+ readonly successSchema: Schema.Schema.AnyNoContext;
543+ readonly errorSchema: Schema.Schema.AnyNoContext | undefined;
544+ readonly middlewares: ReadonlyArray<RpcMiddleware.TagClassAny>;
545+ readonly build: (tag: string, middlewareConfig: MiddlewareConfig | undefined) => RpcEndpoint<string, Rpc.Any, unknown>;
546+ middleware<M extends RpcMiddleware.TagClassAny>(middleware: M): AnyUnbuiltEndpoint;
547+ // Variance note: `implement` sits in covariant position (method), its handler
548+ // parameter is contravariant, and the handler's `payload` is again contravariant
549+ // — making `payload` doubly contravariant = covariant overall. So for
550+ // `AnyUnbuiltEndpoint` to be a proper supertype of all concrete endpoints,
551+ // `payload` must be a supertype of all concrete payload types → `unknown`.
552+ implement(handler: (payload: unknown) => Effect.Effect<unknown, unknown, unknown>): void;
553+}
554+
555+type BuiltEndpoint<K extends string, U> = U extends UnbuiltRpcEndpoint<
556+ infer PayloadFields,
557+ infer Success,
558+ infer Error,
559+ infer ConvexFnType,
560+ infer _Middlewares
561+>
562+ ? RpcEndpoint<K, Rpc.Rpc<K, Schema.Struct<PayloadFields>, Success, Error extends Schema.Schema.AnyNoContext ? Error : typeof Schema.Never, never>, ConvexFnType>
563+ : never;
564+
565+type BuiltEndpoints<T extends Record<string, AnyUnbuiltEndpoint>> = {
566+ [K in keyof T & string]: BuiltEndpoint<K, T[K]>;
567+};
568+
569+const isUnbuilt = (value: unknown): value is AnyUnbuiltEndpoint =>
570+ typeof value === "object" && value !== null && "__unbuilt" in value && (value as { __unbuilt: unknown }).__unbuilt === true;
571+
572+type ExtractMiddlewares<T extends Record<string, AnyUnbuiltEndpoint>> = T[keyof T]["middlewares"][number];
573+
574+type ExtractMiddlewareProvides<T extends RpcMiddleware.TagClassAny> = T extends {
575+ readonly provides: Context.Tag<infer Id, infer _S>;
576+}
577+ ? Id
578+ : never;
579+
580+type ExtractAllMiddlewareProvides<T extends Record<string, AnyUnbuiltEndpoint>> = ExtractMiddlewareProvides<ExtractMiddlewares<T>>;
581+
582+type StaticMiddlewareLayer<Services> =
583+ | Layer.Layer<Services, unknown, never>
584+ | Layer.Layer<never, unknown, never>;
585+
586+export interface RpcModuleOptions<Services> {
587+ readonly middlewares?:
588+ | StaticMiddlewareLayer<Services>
589+ | ReadonlyArray<MiddlewareImplementation>
590+ | {
591+ readonly implementations?: ReadonlyArray<MiddlewareImplementation>;
592+ readonly layer?: StaticMiddlewareLayer<Services>;
593+ };
594+}
595+
596+const normalizeMiddlewareOptions = <Services>(
597+ options: RpcModuleOptions<Services>["middlewares"],
598+): MiddlewareConfig | undefined => {
599+ if (!options) {
600+ return undefined;
601+ }
602+
603+ if (Array.isArray(options)) {
604+ return {
605+ implementations: options,
606+ staticLayer: undefined,
607+ };
608+ }
609+
610+ if ("_tag" in options && options._tag === "MiddlewareImplementation") {
611+ return {
612+ implementations: [options as MiddlewareImplementation],
613+ staticLayer: undefined,
614+ };
615+ }
616+
617+ if (Layer.isLayer(options)) {
618+ return {
619+ implementations: [],
620+ staticLayer: options as Layer.Layer<unknown, unknown, never>,
621+ };
622+ }
623+
624+ const cfg = options as {
625+ readonly implementations?: ReadonlyArray<MiddlewareImplementation>;
626+ readonly layer?: StaticMiddlewareLayer<Services>;
627+ };
628+
629+ return {
630+ implementations: cfg.implementations ?? [],
631+ staticLayer: cfg.layer as Layer.Layer<unknown, unknown, never> | undefined,
632+ };
633+};
634+
635+export function makeRpcModule<
636+ const T extends Record<string, AnyUnbuiltEndpoint>,
637+>(
638+ unbuiltEndpoints: T,
639+ options?: RpcModuleOptions<ExtractAllMiddlewareProvides<T>>,
640+): RpcModuleBase<BuiltEndpoints<T>> & { readonly [K in keyof T]: BuiltEndpoint<K & string, T[K]> } {
641+ const rpcs = {} as Record<string, Rpc.Any>;
642+ const handlers = {} as Record<string, unknown>;
643+ const rpcList: Array<Rpc.Any> = [];
644+ const builtEndpoints = {} as Record<string, RpcEndpoint<string, Rpc.Any, unknown>>;
645+
646+ const middlewareConfig = normalizeMiddlewareOptions(options?.middlewares);
647+
648+ for (const key of Object.keys(unbuiltEndpoints)) {
649+ const unbuilt = unbuiltEndpoints[key]!;
650+ if (!isUnbuilt(unbuilt)) {
651+ throw new Error(`Expected unbuilt endpoint for key "${key}"`);
652+ }
653+ const endpoint = unbuilt.build(key, middlewareConfig);
654+ builtEndpoints[key] = endpoint;
655+ rpcs[key] = endpoint.rpc;
656+ handlers[key] = endpoint.fn;
657+ rpcList.push(endpoint.rpc);
658+ }
659+
660+ type Built = BuiltEndpoints<T>;
661+ const module = {
662+ _def: { endpoints: builtEndpoints },
663+ rpcs: rpcs as { [K in keyof Built]: InferRpc<Built[K]> },
664+ handlers: handlers as { [K in keyof Built]: InferFn<Built[K]> },
665+ group: RpcGroup.make(...rpcList) as unknown as RpcGroup.RpcGroup<InferRpc<Built[keyof Built]>>,
666+ };
667+
668+ return Object.assign(module, builtEndpoints) as RpcModuleBase<Built> & { readonly [K in keyof T]: BuiltEndpoint<K & string, T[K]> };
669+}
670+
671+export const exitSchema = Rpc.exitSchema;
v2/packages/confect/src/rpc/telemetry.test.ts+112−0
@@ -0,0 +1,112 @@
1+import { Effect, Option } from "effect";
2+import { describe, expect, it } from "vitest";
3+import {
4+ convexRequestIdHeader,
5+ extractParentSpanFromPayload,
6+ makeRpcTelemetryContext,
7+ makeRpcTransportHeaders,
8+ rpcTelemetryContextField,
9+ withOptionalRpcTelemetryContext,
10+ withRpcTelemetryContext,
11+} from "./telemetry";
12+
13+describe("RPC telemetry context", () => {
14+ it("roundtrips span context through payload metadata", async () => {
15+ const payloadWithTelemetry = await Effect.runPromise(
16+ Effect.useSpan("rpc.client.test", { kind: "client" }, (span) =>
17+ Effect.succeed(
18+ withRpcTelemetryContext(
19+ { message: "hello" },
20+ makeRpcTelemetryContext(span),
21+ ),
22+ ),
23+ ),
24+ );
25+
26+ const parentSpan = extractParentSpanFromPayload(payloadWithTelemetry);
27+ expect(Option.isSome(parentSpan)).toBe(true);
28+
29+ if (Option.isSome(parentSpan)) {
30+ expect(parentSpan.value.traceId).toHaveLength(32);
31+ expect(parentSpan.value.spanId).toHaveLength(16);
32+ }
33+ });
34+
35+ it("returns none when payload has no telemetry context", () => {
36+ const parentSpan = extractParentSpanFromPayload({ message: "hello" });
37+ expect(Option.isNone(parentSpan)).toBe(true);
38+ });
39+
40+ it("returns none for malformed telemetry payload", () => {
41+ const parentSpan = extractParentSpanFromPayload({
42+ confectTelemetryContext: {
43+ traceparent: 123,
44+ },
45+ });
46+
47+ expect(Option.isNone(parentSpan)).toBe(true);
48+ });
49+
50+ it("creates transport headers with trace context and request id", async () => {
51+ const headers = await Effect.runPromise(
52+ Effect.useSpan("rpc.client.test", { kind: "client" }, (span) =>
53+ Effect.succeed(makeRpcTransportHeaders(span)),
54+ ),
55+ );
56+
57+ expect(headers.traceparent).toBeDefined();
58+ const requestId = headers[convexRequestIdHeader];
59+ expect(requestId).toBeDefined();
60+ if (requestId !== undefined) {
61+ expect(requestId.length).toBeGreaterThan(0);
62+ }
63+ });
64+
65+ it("skips payload telemetry context when fallback is disabled", async () => {
66+ const payloadWithTelemetry = await Effect.runPromise(
67+ Effect.useSpan("rpc.client.test", { kind: "client" }, (span) =>
68+ Effect.succeed(
69+ withOptionalRpcTelemetryContext(
70+ "mutation",
71+ { message: "hello" },
72+ span,
73+ false,
74+ ),
75+ ),
76+ ),
77+ );
78+
79+ expect(payloadWithTelemetry).toEqual({ message: "hello" });
80+ });
81+
82+ it("adds payload telemetry context for mutations when fallback is enabled", async () => {
83+ const payloadWithTelemetry = await Effect.runPromise(
84+ Effect.useSpan("rpc.client.test", { kind: "client" }, (span) =>
85+ Effect.succeed(
86+ withOptionalRpcTelemetryContext(
87+ "mutation",
88+ { message: "hello" },
89+ span,
90+ true,
91+ ),
92+ ),
93+ ),
94+ );
95+
96+ expect(payloadWithTelemetry).toMatchObject({ message: "hello" });
97+ expect(payloadWithTelemetry).toHaveProperty(rpcTelemetryContextField);
98+ });
99+
100+ it("adds payload telemetry context for queries when fallback is enabled", async () => {
101+ const payloadWithTelemetry = await Effect.runPromise(
102+ Effect.useSpan("rpc.client.test", { kind: "client" }, (span) =>
103+ Effect.succeed(
104+ withOptionalRpcTelemetryContext("query", { page: 1 }, span, true),
105+ ),
106+ ),
107+ );
108+
109+ expect(payloadWithTelemetry).toMatchObject({ page: 1 });
110+ expect(payloadWithTelemetry).toHaveProperty(rpcTelemetryContextField);
111+ });
112+});
v2/packages/confect/src/rpc/telemetry.ts+134−0
@@ -0,0 +1,134 @@
1+import * as Headers from "@effect/platform/Headers";
2+import * as HttpTraceContext from "@effect/platform/HttpTraceContext";
3+import * as Option from "effect/Option";
4+import type * as Tracer from "effect/Tracer";
5+
6+export const rpcTelemetryContextField = "confectTelemetryContext";
7+export const convexRequestIdHeader = "convex-request-id";
8+
9+export type RpcClientKind = "query" | "mutation" | "action";
10+
11+export interface RpcTelemetryContext {
12+ readonly traceparent: string;
13+ readonly b3?: string;
14+ readonly tracestate?: string;
15+}
16+
17+const isRecord = (value: unknown): value is Record<string, unknown> =>
18+ typeof value === "object" && value !== null;
19+
20+const toStringValue = (value: unknown): Option.Option<string> =>
21+ typeof value === "string" ? Option.some(value) : Option.none();
22+
23+const getRecordValue = (
24+ record: Record<string, unknown>,
25+ key: string,
26+): Option.Option<string> =>
27+ Option.flatMap(Option.fromNullable(record[key]), toStringValue);
28+
29+export const makeRpcTelemetryContext = (
30+ span: Tracer.Span,
31+): RpcTelemetryContext => {
32+ const headers = HttpTraceContext.toHeaders(span);
33+ const traceparent = headers.traceparent;
34+ const tracestate = headers.tracestate;
35+
36+ if (traceparent === undefined) {
37+ return {
38+ traceparent: `00-${span.traceId}-${span.spanId}-${
39+ span.sampled ? "01" : "00"
40+ }`,
41+ tracestate,
42+ };
43+ }
44+
45+ if (headers.b3 === undefined) {
46+ return { traceparent, tracestate };
47+ }
48+
49+ return {
50+ traceparent,
51+ b3: headers.b3,
52+ tracestate,
53+ };
54+};
55+
56+export const makeRpcTransportHeaders = (
57+ span: Tracer.Span,
58+): Readonly<Record<string, string>> => {
59+ const telemetryContext = makeRpcTelemetryContext(span);
60+ const headers: Record<string, string> = {
61+ traceparent: telemetryContext.traceparent,
62+ [convexRequestIdHeader]: `${span.traceId}-${span.spanId}`,
63+ };
64+
65+ if (telemetryContext.b3 !== undefined) {
66+ headers.b3 = telemetryContext.b3;
67+ }
68+
69+ if (telemetryContext.tracestate !== undefined) {
70+ headers.tracestate = telemetryContext.tracestate;
71+ }
72+
73+ return headers;
74+};
75+
76+export const withOptionalRpcTelemetryContext = (
77+ _kind: RpcClientKind,
78+ payload: unknown,
79+ span: Tracer.Span,
80+ enablePayloadFallback: boolean,
81+): unknown => {
82+ if (!enablePayloadFallback) {
83+ return payload;
84+ }
85+
86+ return withRpcTelemetryContext(payload, makeRpcTelemetryContext(span));
87+};
88+
89+export const withRpcTelemetryContext = (
90+ payload: unknown,
91+ telemetryContext: RpcTelemetryContext,
92+): unknown => {
93+ if (!isRecord(payload)) {
94+ return payload;
95+ }
96+
97+ return {
98+ ...payload,
99+ [rpcTelemetryContextField]: telemetryContext,
100+ };
101+};
102+
103+export const extractParentSpanFromPayload = (
104+ payload: unknown,
105+): Option.Option<Tracer.ExternalSpan> => {
106+ if (!isRecord(payload)) {
107+ return Option.none();
108+ }
109+
110+ const rawTelemetryContext = payload[rpcTelemetryContextField];
111+ if (!isRecord(rawTelemetryContext)) {
112+ return Option.none();
113+ }
114+
115+ const traceparent = Option.getOrUndefined(
116+ getRecordValue(rawTelemetryContext, "traceparent"),
117+ );
118+ const b3 = Option.getOrUndefined(getRecordValue(rawTelemetryContext, "b3"));
119+ const tracestate = Option.getOrUndefined(
120+ getRecordValue(rawTelemetryContext, "tracestate"),
121+ );
122+
123+ if (traceparent === undefined && b3 === undefined) {
124+ return Option.none();
125+ }
126+
127+ return HttpTraceContext.fromHeaders(
128+ Headers.fromInput({
129+ traceparent,
130+ b3,
131+ tracestate,
132+ }),
133+ );
134+};
v2/packages/confect/src/schema.test.ts+235−0
@@ -0,0 +1,235 @@
1+import { describe, expect, it } from "vitest";
2+import { Schema } from "effect";
3+import { defineTable, defineSchema, type ConfectTableDefinition } from "./schema";
4+
5+describe("schema", () => {
6+ describe("defineTable", () => {
7+ it("creates a table definition from Effect Schema", () => {
8+ const userTable = defineTable(
9+ Schema.Struct({
10+ name: Schema.String,
11+ email: Schema.String,
12+ }),
13+ );
14+
15+ expect(userTable).toBeDefined();
16+ expect(userTable.tableDefinition).toBeDefined();
17+ expect(userTable.tableSchema).toBeDefined();
18+ expect(userTable.documentSchema).toBeDefined();
19+ });
20+
21+ it("includes system fields in documentSchema", () => {
22+ const postTable = defineTable(
23+ Schema.Struct({
24+ title: Schema.String,
25+ content: Schema.String,
26+ }),
27+ );
28+
29+ const doc = {
30+ _id: "123",
31+ _creationTime: Date.now(),
32+ title: "Hello",
33+ content: "World",
34+ };
35+
36+ const result = Schema.decodeUnknownSync(postTable.documentSchema)(doc);
37+ expect(result).toMatchObject({
38+ _id: "123",
39+ title: "Hello",
40+ content: "World",
41+ });
42+ });
43+
44+ describe("index", () => {
45+ it("adds an index to the table definition", () => {
46+ const table = defineTable(
47+ Schema.Struct({
48+ userId: Schema.String,
49+ createdAt: Schema.Number,
50+ }),
51+ ).index("by_userId", ["userId"]);
52+
53+ expect(table).toBeDefined();
54+ });
55+
56+ it("supports compound indexes", () => {
57+ const table = defineTable(
58+ Schema.Struct({
59+ userId: Schema.String,
60+ status: Schema.String,
61+ createdAt: Schema.Number,
62+ }),
63+ ).index("by_userId_and_status", ["userId", "status"]);
64+
65+ expect(table).toBeDefined();
66+ });
67+
68+ it("allows chaining multiple indexes", () => {
69+ const table = defineTable(
70+ Schema.Struct({
71+ userId: Schema.String,
72+ email: Schema.String,
73+ }),
74+ )
75+ .index("by_userId", ["userId"])
76+ .index("by_email", ["email"]);
77+
78+ expect(table).toBeDefined();
79+ });
80+ });
81+
82+ describe("searchIndex", () => {
83+ it("adds a search index to the table definition", () => {
84+ const table = defineTable(
85+ Schema.Struct({
86+ title: Schema.String,
87+ content: Schema.String,
88+ }),
89+ ).searchIndex("search_content", {
90+ searchField: "content",
91+ filterFields: [],
92+ });
93+
94+ expect(table).toBeDefined();
95+ });
96+ });
97+
98+ describe("vectorIndex", () => {
99+ it("adds a vector index to the table definition", () => {
100+ const table = defineTable(
101+ Schema.Struct({
102+ embedding: Schema.Array(Schema.Number),
103+ category: Schema.String,
104+ }),
105+ ).vectorIndex("by_embedding", {
106+ vectorField: "embedding",
107+ dimensions: 1536,
108+ filterFields: ["category"],
109+ });
110+
111+ expect(table).toBeDefined();
112+ });
113+ });
114+ });
115+
116+ describe("defineSchema", () => {
117+ it("creates a schema definition from multiple tables", () => {
118+ const schema = defineSchema({
119+ users: defineTable(
120+ Schema.Struct({
121+ name: Schema.String,
122+ email: Schema.String,
123+ }),
124+ ),
125+ posts: defineTable(
126+ Schema.Struct({
127+ title: Schema.String,
128+ authorId: Schema.String,
129+ }),
130+ ),
131+ });
132+
133+ expect(schema.tables.users).toBeDefined();
134+ expect(schema.tables.posts).toBeDefined();
135+ expect(schema.convexSchemaDefinition).toBeDefined();
136+ });
137+
138+ it("preserves table definitions in the tables property", () => {
139+ const userTable = defineTable(
140+ Schema.Struct({
141+ name: Schema.String,
142+ }),
143+ );
144+
145+ const schema = defineSchema({
146+ users: userTable,
147+ });
148+
149+ expect(schema.tables.users).toBe(userTable);
150+ });
151+
152+ it("creates convex-compatible schema definition", () => {
153+ const schema = defineSchema({
154+ items: defineTable(
155+ Schema.Struct({
156+ name: Schema.String,
157+ quantity: Schema.Number,
158+ }),
159+ ),
160+ });
161+
162+ expect(schema.convexSchemaDefinition).toBeDefined();
163+ expect(schema.convexSchemaDefinition.tables).toBeDefined();
164+ });
165+
166+ it("handles complex nested schemas", () => {
167+ const schema = defineSchema({
168+ orders: defineTable(
169+ Schema.Struct({
170+ items: Schema.Array(
171+ Schema.Struct({
172+ productId: Schema.String,
173+ quantity: Schema.Number,
174+ price: Schema.Number,
175+ }),
176+ ),
177+ status: Schema.Union(
178+ Schema.Literal("pending"),
179+ Schema.Literal("confirmed"),
180+ Schema.Literal("shipped"),
181+ Schema.Literal("delivered"),
182+ ),
183+ metadata: Schema.optional(
184+ Schema.Struct({
185+ notes: Schema.String,
186+ }),
187+ ),
188+ }),
189+ ),
190+ });
191+
192+ expect(schema.tables.orders).toBeDefined();
193+ });
194+ });
195+
196+ describe("type inference", () => {
197+ it("correctly infers document type with system fields", () => {
198+ const table = defineTable(
199+ Schema.Struct({
200+ name: Schema.String,
201+ age: Schema.Number,
202+ }),
203+ );
204+
205+ const validDoc = {
206+ _id: "doc123",
207+ _creationTime: 1234567890,
208+ name: "Alice",
209+ age: 30,
210+ };
211+
212+ const decoded = Schema.decodeUnknownSync(table.documentSchema)(validDoc);
213+ expect(decoded).toEqual(validDoc);
214+ });
215+
216+ it("rejects documents missing required fields", () => {
217+ const table = defineTable(
218+ Schema.Struct({
219+ name: Schema.String,
220+ required: Schema.String,
221+ }),
222+ );
223+
224+ const invalidDoc = {
225+ _id: "doc123",
226+ _creationTime: 1234567890,
227+ name: "Alice",
228+ };
229+
230+ expect(() =>
231+ Schema.decodeUnknownSync(table.documentSchema)(invalidDoc),
232+ ).toThrow();
233+ });
234+ });
235+});
v2/packages/confect/src/schema.ts+400−0
@@ -0,0 +1,400 @@
1+import {
2+ defineSchema as defineConvexSchema,
3+ defineTable as defineConvexTable,
4+ type GenericTableIndexes,
5+ type GenericTableSearchIndexes,
6+ type GenericTableVectorIndexes,
7+ type IndexTiebreakerField,
8+ type SchemaDefinition,
9+ type SearchIndexConfig,
10+ type TableDefinition,
11+ type VectorIndexConfig,
12+ type Expand,
13+ type SystemFields,
14+ type SystemIndexes,
15+ type IdField,
16+} from "convex/server";
17+import type { Validator } from "convex/values";
18+import { Record, Schema } from "effect";
19+
20+import { Id, schemaToObjectValidator } from "./validators";
21+
22+/**
23+ * Recursively removes `readonly` modifiers from all properties and arrays.
24+ *
25+ * Effect Schema produces readonly types (e.g. `readonly number[]`, `readonly` properties),
26+ * but Convex's `GenericDocument` requires mutable `Value[]`. This utility bridges the gap
27+ * at the type level — the runtime validators produced by `schemaToObjectValidator` are
28+ * already Convex-native (mutable), so this only affects TypeScript inference.
29+ */
30+type DeepMutable<T> =
31+ T extends ReadonlyArray<infer U>
32+ ? Array<DeepMutable<U>>
33+ : T extends object
34+ ? { -readonly [K in keyof T]: DeepMutable<T[K]> }
35+ : T;
36+
37+const SystemFieldsSchema = Schema.Struct({
38+ _id: Schema.String,
39+ _creationTime: Schema.Number,
40+});
41+
42+const createSystemFieldsSchema = <TableName extends string>(tableName: TableName) =>
43+ Schema.Struct({
44+ _id: Id(tableName),
45+ _creationTime: Schema.Number,
46+ });
47+
48+export interface ConfectTableDefinition<
49+ TableSchema extends Schema.Schema.AnyNoContext,
50+ TableValidator extends Validator<unknown, "required" | "optional", string> = Validator<
51+ DeepMutable<TableSchema["Encoded"]>,
52+ "required",
53+ string
54+ >,
55+ Indexes extends GenericTableIndexes = {},
56+ SearchIndexes extends GenericTableSearchIndexes = {},
57+ VectorIndexes extends GenericTableVectorIndexes = {},
58+> {
59+ tableDefinition: TableDefinition<
60+ TableValidator,
61+ Indexes,
62+ SearchIndexes,
63+ VectorIndexes
64+ >;
65+ tableSchema: TableSchema;
66+ documentSchema: Schema.Schema.AnyNoContext;
67+
68+ index<
69+ IndexName extends string,
70+ FirstFieldPath extends string,
71+ RestFieldPaths extends string[],
72+ >(
73+ name: IndexName,
74+ fields: [FirstFieldPath, ...RestFieldPaths],
75+ ): ConfectTableDefinition<
76+ TableSchema,
77+ TableValidator,
78+ Expand<
79+ Indexes &
80+ Record<
81+ IndexName,
82+ [FirstFieldPath, ...RestFieldPaths, IndexTiebreakerField]
83+ >
84+ >,
85+ SearchIndexes,
86+ VectorIndexes
87+ >;
88+
89+ searchIndex<
90+ IndexName extends string,
91+ SearchField extends string,
92+ FilterFields extends string = never,
93+ >(
94+ name: IndexName,
95+ indexConfig: Expand<SearchIndexConfig<SearchField, FilterFields>>,
96+ ): ConfectTableDefinition<
97+ TableSchema,
98+ TableValidator,
99+ Indexes,
100+ Expand<
101+ SearchIndexes &
102+ Record<
103+ IndexName,
104+ {
105+ searchField: SearchField;
106+ filterFields: FilterFields;
107+ }
108+ >
109+ >,
110+ VectorIndexes
111+ >;
112+
113+ vectorIndex<
114+ IndexName extends string,
115+ VectorField extends string,
116+ FilterFields extends string = never,
117+ >(
118+ name: IndexName,
119+ indexConfig: Expand<VectorIndexConfig<VectorField, FilterFields>>,
120+ ): ConfectTableDefinition<
121+ TableSchema,
122+ TableValidator,
123+ Indexes,
124+ SearchIndexes,
125+ Expand<
126+ VectorIndexes &
127+ Record<
128+ IndexName,
129+ {
130+ vectorField: VectorField;
131+ dimensions: number;
132+ filterFields: FilterFields;
133+ }
134+ >
135+ >
136+ >;
137+}
138+
139+class ConfectTableDefinitionImpl<
140+ TableSchema extends Schema.Schema.AnyNoContext,
141+ TableValidator extends Validator<unknown, "required" | "optional", string>,
142+ Indexes extends GenericTableIndexes = {},
143+ SearchIndexes extends GenericTableSearchIndexes = {},
144+ VectorIndexes extends GenericTableVectorIndexes = {},
145+> implements
146+ ConfectTableDefinition<
147+ TableSchema,
148+ TableValidator,
149+ Indexes,
150+ SearchIndexes,
151+ VectorIndexes
152+ >
153+{
154+ tableSchema: TableSchema;
155+ documentSchema: Schema.Schema.AnyNoContext;
156+ tableDefinition: TableDefinition<
157+ TableValidator,
158+ Indexes,
159+ SearchIndexes,
160+ VectorIndexes
161+ >;
162+
163+ constructor(tableSchema: TableSchema) {
164+ this.tableSchema = tableSchema;
165+ this.documentSchema = Schema.extend(SystemFieldsSchema, tableSchema) as unknown as Schema.Schema.AnyNoContext;
166+ const tableValidator = schemaToObjectValidator(tableSchema);
167+ this.tableDefinition = defineConvexTable(
168+ tableValidator,
169+ ) as unknown as TableDefinition<TableValidator, Indexes, SearchIndexes, VectorIndexes>;
170+ }
171+
172+ index<
173+ IndexName extends string,
174+ FirstFieldPath extends string,
175+ RestFieldPaths extends string[],
176+ >(
177+ name: IndexName,
178+ fields: [FirstFieldPath, ...RestFieldPaths],
179+ ): ConfectTableDefinition<
180+ TableSchema,
181+ TableValidator,
182+ Expand<
183+ Indexes &
184+ Record<
185+ IndexName,
186+ [FirstFieldPath, ...RestFieldPaths, IndexTiebreakerField]
187+ >
188+ >,
189+ SearchIndexes,
190+ VectorIndexes
191+ > {
192+ this.tableDefinition = this.tableDefinition.index(
193+ name,
194+ fields as unknown as [string, ...string[]],
195+ ) as unknown as TableDefinition<TableValidator, Indexes, SearchIndexes, VectorIndexes>;
196+ return this as unknown as ConfectTableDefinition<
197+ TableSchema,
198+ TableValidator,
199+ Expand<
200+ Indexes &
201+ Record<
202+ IndexName,
203+ [FirstFieldPath, ...RestFieldPaths, IndexTiebreakerField]
204+ >
205+ >,
206+ SearchIndexes,
207+ VectorIndexes
208+ >;
209+ }
210+
211+ searchIndex<
212+ IndexName extends string,
213+ SearchField extends string,
214+ FilterFields extends string = never,
215+ >(
216+ name: IndexName,
217+ indexConfig: Expand<SearchIndexConfig<SearchField, FilterFields>>,
218+ ): ConfectTableDefinition<
219+ TableSchema,
220+ TableValidator,
221+ Indexes,
222+ Expand<
223+ SearchIndexes &
224+ Record<
225+ IndexName,
226+ {
227+ searchField: SearchField;
228+ filterFields: FilterFields;
229+ }
230+ >
231+ >,
232+ VectorIndexes
233+ > {
234+ this.tableDefinition = this.tableDefinition.searchIndex(
235+ name,
236+ indexConfig as unknown as SearchIndexConfig<string, string>,
237+ ) as unknown as TableDefinition<TableValidator, Indexes, SearchIndexes, VectorIndexes>;
238+ return this as unknown as ConfectTableDefinition<
239+ TableSchema,
240+ TableValidator,
241+ Indexes,
242+ Expand<
243+ SearchIndexes &
244+ Record<
245+ IndexName,
246+ {
247+ searchField: SearchField;
248+ filterFields: FilterFields;
249+ }
250+ >
251+ >,
252+ VectorIndexes
253+ >;
254+ }
255+
256+ vectorIndex<
257+ IndexName extends string,
258+ VectorField extends string,
259+ FilterFields extends string = never,
260+ >(
261+ name: IndexName,
262+ indexConfig: Expand<VectorIndexConfig<VectorField, FilterFields>>,
263+ ): ConfectTableDefinition<
264+ TableSchema,
265+ TableValidator,
266+ Indexes,
267+ SearchIndexes,
268+ Expand<
269+ VectorIndexes &
270+ Record<
271+ IndexName,
272+ {
273+ vectorField: VectorField;
274+ dimensions: number;
275+ filterFields: FilterFields;
276+ }
277+ >
278+ >
279+ > {
280+ this.tableDefinition = this.tableDefinition.vectorIndex(
281+ name,
282+ indexConfig as unknown as VectorIndexConfig<string, string>,
283+ ) as unknown as TableDefinition<TableValidator, Indexes, SearchIndexes, VectorIndexes>;
284+ return this as unknown as ConfectTableDefinition<
285+ TableSchema,
286+ TableValidator,
287+ Indexes,
288+ SearchIndexes,
289+ Expand<
290+ VectorIndexes &
291+ Record<
292+ IndexName,
293+ {
294+ vectorField: VectorField;
295+ dimensions: number;
296+ filterFields: FilterFields;
297+ }
298+ >
299+ >
300+ >;
301+ }
302+}
303+
304+export const defineTable = <TableSchema extends Schema.Schema.AnyNoContext>(
305+ tableSchema: TableSchema,
306+): ConfectTableDefinition<TableSchema> => {
307+ return new ConfectTableDefinitionImpl(tableSchema);
308+};
309+
310+export type GenericConfectSchema = {
311+ [tableName: string]: ConfectTableDefinition<Schema.Schema.AnyNoContext>;
312+};
313+
314+export interface ConfectSchemaDefinition<
315+ Tables extends GenericConfectSchema,
316+> {
317+ tables: Tables;
318+ convexSchemaDefinition: SchemaDefinition<
319+ {
320+ [K in keyof Tables & string]: Tables[K]["tableDefinition"];
321+ },
322+ true
323+ >;
324+}
325+
326+export const defineSchema = <Tables extends GenericConfectSchema>(
327+ tables: Tables,
328+): ConfectSchemaDefinition<Tables> => {
329+ for (const [tableName, table] of Object.entries(tables)) {
330+ table.documentSchema = Schema.extend(
331+ createSystemFieldsSchema(tableName),
332+ table.tableSchema,
333+ );
334+ }
335+
336+ const convexTables = Record.map(
337+ tables,
338+ (table) => table.tableDefinition,
339+ ) as { [K in keyof Tables & string]: Tables[K]["tableDefinition"] };
340+
341+ return {
342+ tables,
343+ convexSchemaDefinition: defineConvexSchema(convexTables),
344+ };
345+};
346+
347+export type TableNamesInSchema<T extends GenericConfectSchema> = keyof T & string;
348+
349+export type DocumentFromTable<
350+ Tables extends GenericConfectSchema,
351+ TableName extends TableNamesInSchema<Tables>,
352+> = Expand<
353+ IdField<TableName> &
354+ SystemFields &
355+ DeepMutable<Tables[TableName]["tableSchema"]["Type"]>
356+>;
357+
358+export type EncodedDocumentFromTable<
359+ Tables extends GenericConfectSchema,
360+ TableName extends TableNamesInSchema<Tables>,
361+> = Expand<
362+ IdField<TableName> &
363+ SystemFields &
364+ DeepMutable<Tables[TableName]["tableSchema"]["Encoded"]>
365+>;
366+
367+export type ConfectDataModel<Tables extends GenericConfectSchema> = {
368+ [TableName in TableNamesInSchema<Tables>]: {
369+ document: DocumentFromTable<Tables, TableName>;
370+ encodedDocument: EncodedDocumentFromTable<Tables, TableName>;
371+ indexes: Tables[TableName]["tableDefinition"] extends TableDefinition<
372+ infer _V,
373+ infer I,
374+ infer _S,
375+ infer _Vec
376+ >
377+ ? Expand<I & SystemIndexes>
378+ : SystemIndexes;
379+ searchIndexes: Tables[TableName]["tableDefinition"] extends TableDefinition<
380+ infer _V,
381+ infer _I,
382+ infer S,
383+ infer _Vec
384+ >
385+ ? S
386+ : {};
387+ vectorIndexes: Tables[TableName]["tableDefinition"] extends TableDefinition<
388+ infer _V,
389+ infer _I,
390+ infer _S,
391+ infer Vec
392+ >
393+ ? Vec
394+ : {};
395+ };
396+};
397+
398+export type TablesFromSchemaDefinition<
399+ S extends ConfectSchemaDefinition<GenericConfectSchema>,
400+> = S extends ConfectSchemaDefinition<infer T> ? T : never;
v2/packages/confect/src/testing.ts+129−0
@@ -0,0 +1,129 @@
1+import type { FunctionReference, FunctionReturnType, SchemaDefinition, GenericSchema } from "convex/server";
2+import { Effect, Layer, Stream } from "effect";
3+import {
4+ ConvexClient,
5+ type ConvexClientService,
6+ type ConvexRequestMetadata,
7+} from "./client";
8+
9+export interface ConvexTestInstance {
10+ query<Query extends FunctionReference<"query">>(
11+ query: Query,
12+ args: Query["_args"],
13+ ): Promise<FunctionReturnType<Query>>;
14+
15+ mutation<Mutation extends FunctionReference<"mutation">>(
16+ mutation: Mutation,
17+ args: Mutation["_args"],
18+ ): Promise<FunctionReturnType<Mutation>>;
19+
20+ action<Action extends FunctionReference<"action">>(
21+ action: Action,
22+ args: Action["_args"],
23+ ): Promise<FunctionReturnType<Action>>;
24+
25+ onUpdate<Query extends FunctionReference<"query">>(
26+ query: Query,
27+ args: Query["_args"],
28+ callback: (result: FunctionReturnType<Query>) => void,
29+ ): () => void;
30+
31+ run<Output>(
32+ func: (ctx: { db: unknown; storage: unknown }) => Promise<Output>,
33+ ): Promise<Output>;
34+}
35+
36+export const ConvexClientTestLayer = (
37+ testClient: ConvexTestInstance,
38+): Layer.Layer<ConvexClient> => {
39+ const service: ConvexClientService = {
40+ query: <Query extends FunctionReference<"query">>(
41+ query: Query,
42+ args: Query["_args"],
43+ _requestMetadata?: ConvexRequestMetadata,
44+ ): Effect.Effect<FunctionReturnType<Query>> =>
45+ Effect.promise(() => testClient.query(query, args)),
46+
47+ mutation: <Mutation extends FunctionReference<"mutation">>(
48+ mutation: Mutation,
49+ args: Mutation["_args"],
50+ _requestMetadata?: ConvexRequestMetadata,
51+ ): Effect.Effect<FunctionReturnType<Mutation>> =>
52+ Effect.promise(() => testClient.mutation(mutation, args)),
53+
54+ action: <Action extends FunctionReference<"action">>(
55+ action: Action,
56+ args: Action["_args"],
57+ _requestMetadata?: ConvexRequestMetadata,
58+ ): Effect.Effect<FunctionReturnType<Action>> =>
59+ Effect.promise(() => testClient.action(action, args)),
60+
61+ subscribe: <Query extends FunctionReference<"query">>(
62+ query: Query,
63+ args: Query["_args"],
64+ ): Stream.Stream<FunctionReturnType<Query>> =>
65+ Stream.async<FunctionReturnType<Query>>((emit) => {
66+ const unsubscribe = testClient.onUpdate(query, args, (result) => {
67+ emit.single(result);
68+ });
69+ return Effect.sync(() => {
70+ unsubscribe();
71+ });
72+ }),
73+ };
74+
75+ return Layer.succeed(ConvexClient, service);
76+};
77+
78+export interface ConvexTestFactory {
79+ <SchemaDef extends SchemaDefinition<GenericSchema, boolean>>(
80+ schema: SchemaDef,
81+ modules?: Record<string, () => Promise<unknown>>,
82+ ): ConvexTestInstance;
83+}
84+
85+export interface MakeTestLayerOptions<SchemaDef extends SchemaDefinition<GenericSchema, boolean>> {
86+ readonly schema: SchemaDef;
87+ readonly modules?: Record<string, () => Promise<unknown>>;
88+ readonly convexTest: ConvexTestFactory;
89+}
90+
91+export interface ConvexTestLayer extends Layer.Layer<ConvexClient> {
92+ readonly testClient: ConvexTestInstance;
93+}
94+
95+export const makeTestLayer = <SchemaDef extends SchemaDefinition<GenericSchema, boolean>>(
96+ options: MakeTestLayerOptions<SchemaDef>,
97+): ConvexTestLayer => {
98+ const testClient = options.convexTest(options.schema, options.modules);
99+ const layer = ConvexClientTestLayer(testClient);
100+ return Object.assign(layer, { testClient });
101+};
102+
103+export interface CreateTestContextOptions<SchemaDef extends SchemaDefinition<GenericSchema, boolean>> {
104+ readonly schema: SchemaDef;
105+ readonly modules?: Record<string, () => Promise<unknown>>;
106+ readonly convexTest: ConvexTestFactory;
107+}
108+
109+export interface TestContext {
110+ readonly layer: Layer.Layer<ConvexClient>;
111+ readonly testClient: ConvexTestInstance;
112+ readonly runEffect: <A, E>(effect: Effect.Effect<A, E, ConvexClient>) => Promise<A>;
113+}
114+
115+export const createTestContext = <SchemaDef extends SchemaDefinition<GenericSchema, boolean>>(
116+ options: CreateTestContextOptions<SchemaDef>,
117+): TestContext => {
118+ const testClient = options.convexTest(options.schema, options.modules);
119+ const layer = ConvexClientTestLayer(testClient);
120+
121+ const runEffect = <A, E>(effect: Effect.Effect<A, E, ConvexClient>): Promise<A> =>
122+ Effect.runPromise(Effect.provide(effect, layer));
123+
124+ return {
125+ layer,
126+ testClient,
127+ runEffect,
128+ };
129+};
v2/packages/confect/src/validators.test.ts+247−0
@@ -0,0 +1,247 @@
1+import { describe, expect, it } from "vitest";
2+import { Schema } from "effect";
3+import {
4+ schemaToValidator,
5+ schemaToObjectValidator,
6+ Id,
7+ TopLevelMustBeObjectError,
8+ IndexSignaturesNotSupportedError,
9+} from "./validators";
10+
11+const getKind = (validator: unknown): string =>
12+ (validator as { kind: string }).kind;
13+
14+const getValue = (validator: unknown): unknown =>
15+ (validator as { value: unknown }).value;
16+
17+const getTableName = (validator: unknown): string =>
18+ (validator as { tableName: string }).tableName;
19+
20+const getIsOptional = (validator: unknown): string =>
21+ (validator as { isOptional: string }).isOptional;
22+
23+describe("validators", () => {
24+ describe("schemaToValidator", () => {
25+ describe("primitive types", () => {
26+ it("converts Schema.String to v.string()", () => {
27+ const validator = schemaToValidator(Schema.String);
28+ expect(getKind(validator)).toBe("string");
29+ });
30+
31+ it("converts Schema.Number to v.float64()", () => {
32+ const validator = schemaToValidator(Schema.Number);
33+ expect(getKind(validator)).toBe("float64");
34+ });
35+
36+ it("converts Schema.Boolean to v.boolean()", () => {
37+ const validator = schemaToValidator(Schema.Boolean);
38+ expect(getKind(validator)).toBe("boolean");
39+ });
40+
41+ it("converts Schema.BigInt to v.int64()", () => {
42+ const validator = schemaToValidator(Schema.BigIntFromSelf);
43+ expect(getKind(validator)).toBe("int64");
44+ });
45+
46+ it("converts Schema.Null to v.null()", () => {
47+ const validator = schemaToValidator(Schema.Null);
48+ expect(getKind(validator)).toBe("null");
49+ });
50+ });
51+
52+ describe("literal types", () => {
53+ it("converts string literal", () => {
54+ const validator = schemaToValidator(Schema.Literal("hello"));
55+ expect(getKind(validator)).toBe("literal");
56+ expect(getValue(validator)).toBe("hello");
57+ });
58+
59+ it("converts number literal", () => {
60+ const validator = schemaToValidator(Schema.Literal(42));
61+ expect(getKind(validator)).toBe("literal");
62+ expect(getValue(validator)).toBe(42);
63+ });
64+
65+ it("converts boolean literal", () => {
66+ const validator = schemaToValidator(Schema.Literal(true));
67+ expect(getKind(validator)).toBe("literal");
68+ expect(getValue(validator)).toBe(true);
69+ });
70+ });
71+
72+ describe("object types", () => {
73+ it("converts simple struct", () => {
74+ const schema = Schema.Struct({
75+ name: Schema.String,
76+ age: Schema.Number,
77+ });
78+ const validator = schemaToValidator(schema);
79+ expect(getKind(validator)).toBe("object");
80+ });
81+
82+ it("converts nested struct", () => {
83+ const schema = Schema.Struct({
84+ user: Schema.Struct({
85+ name: Schema.String,
86+ email: Schema.String,
87+ }),
88+ active: Schema.Boolean,
89+ });
90+ const validator = schemaToValidator(schema);
91+ expect(getKind(validator)).toBe("object");
92+ });
93+
94+ it("handles optional fields", () => {
95+ const schema = Schema.Struct({
96+ required: Schema.String,
97+ optional: Schema.optional(Schema.String),
98+ });
99+ const validator = schemaToValidator(schema);
100+ expect(getKind(validator)).toBe("object");
101+ });
102+ });
103+
104+ describe("array types", () => {
105+ it("converts array of primitives", () => {
106+ const schema = Schema.Array(Schema.String);
107+ const validator = schemaToValidator(schema);
108+ expect(getKind(validator)).toBe("array");
109+ });
110+
111+ it("converts array of objects", () => {
112+ const schema = Schema.Array(
113+ Schema.Struct({
114+ id: Schema.String,
115+ value: Schema.Number,
116+ }),
117+ );
118+ const validator = schemaToValidator(schema);
119+ expect(getKind(validator)).toBe("array");
120+ });
121+ });
122+
123+ describe("union types", () => {
124+ it("converts union of primitives", () => {
125+ const schema = Schema.Union(Schema.String, Schema.Number);
126+ const validator = schemaToValidator(schema);
127+ expect(getKind(validator)).toBe("union");
128+ });
129+
130+ it("converts union of literals", () => {
131+ const schema = Schema.Union(
132+ Schema.Literal("active"),
133+ Schema.Literal("inactive"),
134+ Schema.Literal("pending"),
135+ );
136+ const validator = schemaToValidator(schema);
137+ expect(getKind(validator)).toBe("union");
138+ });
139+
140+ it("converts nullable types", () => {
141+ const schema = Schema.NullOr(Schema.String);
142+ const validator = schemaToValidator(schema);
143+ expect(getKind(validator)).toBe("union");
144+ });
145+ });
146+
147+ describe("Id type", () => {
148+ it("converts Id schema to v.id()", () => {
149+ const schema = Id("users");
150+ const validator = schemaToValidator(schema);
151+ expect(getKind(validator)).toBe("id");
152+ expect(getTableName(validator)).toBe("users");
153+ });
154+
155+ it("works with different table names", () => {
156+ const postsId = Id("posts");
157+ const commentsId = Id("comments");
158+
159+ const postsValidator = schemaToValidator(postsId);
160+ const commentsValidator = schemaToValidator(commentsId);
161+
162+ expect(getTableName(postsValidator)).toBe("posts");
163+ expect(getTableName(commentsValidator)).toBe("comments");
164+ });
165+ });
166+
167+ describe("refinement types", () => {
168+ it("unwraps refinements to base type", () => {
169+ const schema = Schema.String.pipe(Schema.minLength(1));
170+ const validator = schemaToValidator(schema);
171+ expect(getKind(validator)).toBe("string");
172+ });
173+
174+ it("unwraps multiple refinements", () => {
175+ const schema = Schema.Number.pipe(Schema.positive(), Schema.int());
176+ const validator = schemaToValidator(schema);
177+ expect(getKind(validator)).toBe("float64");
178+ });
179+ });
180+
181+ describe("any/unknown types", () => {
182+ it("converts Schema.Unknown to v.any()", () => {
183+ const validator = schemaToValidator(Schema.Unknown);
184+ expect(getKind(validator)).toBe("any");
185+ });
186+
187+ it("converts Schema.Any to v.any()", () => {
188+ const validator = schemaToValidator(Schema.Any);
189+ expect(getKind(validator)).toBe("any");
190+ });
191+ });
192+ });
193+
194+ describe("schemaToObjectValidator", () => {
195+ it("extracts fields from struct schema", () => {
196+ const schema = Schema.Struct({
197+ name: Schema.String,
198+ count: Schema.Number,
199+ });
200+ const fields = schemaToObjectValidator(schema);
201+
202+ expect(fields.name).toBeDefined();
203+ expect(fields.count).toBeDefined();
204+ });
205+
206+ it("marks optional fields correctly", () => {
207+ const schema = Schema.Struct({
208+ required: Schema.String,
209+ optional: Schema.optional(Schema.Number),
210+ });
211+ const fields = schemaToObjectValidator(schema);
212+
213+ expect(getIsOptional(fields.required)).toBe("required");
214+ expect(getIsOptional(fields.optional)).toBe("optional");
215+ });
216+
217+ it("throws TopLevelMustBeObjectError for non-object schemas", () => {
218+ const schema = Schema.String;
219+ expect(() => schemaToObjectValidator(schema)).toThrow(
220+ TopLevelMustBeObjectError,
221+ );
222+ });
223+
224+ it("throws IndexSignaturesNotSupportedError for record types", () => {
225+ const schema = Schema.Record({
226+ key: Schema.String,
227+ value: Schema.Number,
228+ });
229+ expect(() => schemaToObjectValidator(schema)).toThrow(
230+ IndexSignaturesNotSupportedError,
231+ );
232+ });
233+ });
234+
235+ describe("Id helper", () => {
236+ it("creates a branded string schema", () => {
237+ const UserId = Id("users");
238+ const decoded = Schema.decodeSync(UserId)("user_123");
239+ expect(decoded).toBe("user_123");
240+ });
241+
242+ it("includes table name in identifier annotation", () => {
243+ const UserId = Id("users");
244+ expect(UserId.ast.annotations).toBeDefined();
245+ });
246+ });
247+});
v2/packages/confect/src/validators.ts+217−0
@@ -0,0 +1,217 @@
1+import { v } from "convex/values";
2+import type { GenericId, Validator } from "convex/values";
3+import { Data, Option, Schema, SchemaAST } from "effect";
4+
5+const getIdTableName = (ast: SchemaAST.AST): Option.Option<string> => {
6+ const annotations = ast.annotations;
7+ const identifier = annotations[SchemaAST.IdentifierAnnotationId];
8+ if (typeof identifier === "string" && identifier.startsWith("Id<")) {
9+ const match = identifier.match(/^Id<"?([^">]+)"?>$/);
10+ if (match?.[1]) {
11+ return Option.some(match[1]);
12+ }
13+ }
14+ return Option.none();
15+};
16+
17+export class UnsupportedSchemaError extends Data.TaggedError("UnsupportedSchemaError")<{
18+ readonly schemaType: string;
19+}> {}
20+
21+export class TopLevelMustBeObjectError extends Data.TaggedError("TopLevelMustBeObjectError")<{}> {}
22+
23+export class IndexSignaturesNotSupportedError extends Data.TaggedError(
24+ "IndexSignaturesNotSupportedError",
25+)<{}> {}
26+
27+type AnyValidator = Validator<unknown, "required" | "optional", string>;
28+type RequiredValidator = Validator<unknown, "required", string>;
29+
30+const compileAst = (
31+ ast: SchemaAST.AST,
32+ isOptionalProperty = false,
33+): AnyValidator => {
34+ switch (ast._tag) {
35+ case "Literal": {
36+ const literal = ast.literal;
37+ if (literal === null) {
38+ return v.null();
39+ }
40+ if (typeof literal === "string" || typeof literal === "number" || typeof literal === "bigint" || typeof literal === "boolean") {
41+ return v.literal(literal);
42+ }
43+ throw new UnsupportedSchemaError({ schemaType: "Literal" });
44+ }
45+
46+ case "BooleanKeyword":
47+ return v.boolean();
48+
49+ case "StringKeyword": {
50+ const tableName = getIdTableName(ast);
51+ if (Option.isSome(tableName)) {
52+ return v.id(tableName.value);
53+ }
54+ return v.string();
55+ }
56+
57+ case "NumberKeyword":
58+ return v.float64();
59+
60+ case "BigIntKeyword":
61+ return v.int64();
62+
63+ case "Union":
64+ return handleUnion(ast, isOptionalProperty);
65+
66+ case "TypeLiteral":
67+ return handleTypeLiteral(ast);
68+
69+ case "TupleType":
70+ return handleTupleType(ast);
71+
72+ case "UnknownKeyword":
73+ case "AnyKeyword":
74+ return v.any();
75+
76+ case "Refinement":
77+ return compileAst(ast.from);
78+
79+ case "Suspend":
80+ return v.any();
81+
82+ case "Declaration":
83+ return v.bytes();
84+
85+ default:
86+ throw new UnsupportedSchemaError({ schemaType: ast._tag });
87+ }
88+};
89+
90+const handleUnion = (
91+ unionAst: SchemaAST.Union,
92+ isOptionalProperty: boolean,
93+): AnyValidator => {
94+ const types = unionAst.types;
95+ const filteredTypes = isOptionalProperty
96+ ? types.filter((type) => !SchemaAST.isUndefinedKeyword(type))
97+ : types;
98+
99+ const validators: Array<AnyValidator> = [];
100+ for (const type of filteredTypes) {
101+ validators.push(compileAst(type));
102+ }
103+
104+ if (validators.length === 0) {
105+ return v.any();
106+ }
107+ if (validators.length === 1) {
108+ return validators[0]!;
109+ }
110+
111+ const [first, second, ...rest] = validators;
112+ return v.union(
113+ first as RequiredValidator,
114+ second as RequiredValidator,
115+ ...(rest as Array<RequiredValidator>),
116+ );
117+};
118+
119+const handleTypeLiteral = (
120+ typeLiteralAst: SchemaAST.TypeLiteral,
121+): AnyValidator => {
122+ if (typeLiteralAst.indexSignatures.length > 0) {
123+ throw new IndexSignaturesNotSupportedError();
124+ }
125+
126+ const fields: Record<string, AnyValidator> = {};
127+
128+ for (const prop of typeLiteralAst.propertySignatures) {
129+ if (typeof prop.name !== "string") {
130+ continue;
131+ }
132+
133+ const validator = compileAst(prop.type, prop.isOptional);
134+ fields[prop.name] = prop.isOptional ? v.optional(validator) : validator;
135+ }
136+
137+ return v.object(fields);
138+};
139+
140+const handleTupleType = (
141+ tupleTypeAst: SchemaAST.TupleType,
142+): AnyValidator => {
143+ const { elements, rest } = tupleTypeAst;
144+
145+ if (elements.length === 0 && rest.length > 0) {
146+ const itemValidator = compileAst(rest[0]!.type);
147+ return v.array(itemValidator as RequiredValidator);
148+ }
149+
150+ if (elements.length > 0) {
151+ const elementValidators: Array<AnyValidator> = [];
152+ for (const el of elements) {
153+ elementValidators.push(compileAst(el.type));
154+ }
155+
156+ if (elementValidators.length === 1) {
157+ return v.array(elementValidators[0] as RequiredValidator);
158+ }
159+
160+ const [first, second, ...restValidators] = elementValidators;
161+ return v.array(
162+ v.union(
163+ first as RequiredValidator,
164+ second as RequiredValidator,
165+ ...(restValidators as Array<RequiredValidator>),
166+ ),
167+ );
168+ }
169+
170+ return v.array(v.any());
171+};
172+
173+export const schemaToValidator = <S extends Schema.Schema.AnyNoContext>(
174+ schema: S,
175+): Validator<S["Encoded"], "required", string> => {
176+ const ast = Schema.encodedSchema(schema).ast;
177+ return compileAst(ast) as Validator<S["Encoded"], "required", string>;
178+};
179+
180+type IdSchema<TableName extends string> = Schema.Schema<
181+ GenericId<TableName>,
182+ string
183+>;
184+
185+export const Id: <TableName extends string>(
186+ tableName: TableName,
187+) => IdSchema<TableName> = <TableName extends string>(tableName: TableName) =>
188+ Schema.String.annotations({
189+ identifier: `Id<${tableName}>`,
190+ }) as unknown as IdSchema<TableName>;
191+
192+export const schemaToObjectValidator = <S extends Schema.Schema.AnyNoContext>(
193+ schema: S,
194+): Record<string, Validator<unknown, "required" | "optional", string>> => {
195+ const ast = Schema.encodedSchema(schema).ast;
196+
197+ if (ast._tag !== "TypeLiteral") {
198+ throw new TopLevelMustBeObjectError();
199+ }
200+
201+ if (ast.indexSignatures.length > 0) {
202+ throw new IndexSignaturesNotSupportedError();
203+ }
204+
205+ const fields: Record<string, AnyValidator> = {};
206+
207+ for (const prop of ast.propertySignatures) {
208+ if (typeof prop.name !== "string") {
209+ continue;
210+ }
211+
212+ const validator = compileAst(prop.type, prop.isOptional);
213+ fields[prop.name] = prop.isOptional ? v.optional(validator) : validator;
214+ }
215+
216+ return fields;
217+};
v2/packages/confect/tsconfig.json+8−0
@@ -0,0 +1,8 @@
1+{
2+ "extends": "../../tsconfig.json",
3+ "compilerOptions": {
4+ "types": ["node"]
5+ },
6+ "include": ["src/**/*"],
7+ "exclude": ["node_modules", "dist"]
8+}
v2/packages/confect/vitest.config.mts+11−0
@@ -0,0 +1,11 @@
1+import { defineConfig } from "vitest/config";
2+
3+export default defineConfig({
4+ test: {
5+ environment: "node",
6+ benchmark: {
7+ include: ["**/*.bench.ts"],
8+ reporters: ["default"],
9+ },
10+ },
11+});
v2/packages/mcp-gateway/README.md+6−1
@@ -1,3 +1,8 @@
11 # mcp-gateway
22
3−MCP gateway surface scaffold for Executor v2.
3+Bare-minimum MCP gateway for Executor v2.
4+
5+Current scaffold includes:
6+- MCP server built with `@modelcontextprotocol/sdk` in `src/server.ts`
7+- streamable HTTP request handling (`handleMcpHttpRequest`)
8+- one stub tool: `executor.ping`
v2/packages/mcp-gateway/package.json+7−0
@@ -2,7 +2,14 @@ · 1 unmodified line
22 "name": "@executor-v2/mcp-gateway",
33 "private": true,
44 "type": "module",
5+ "exports": {
6+ ".": "./src/index.ts"
7+ },
58 "scripts": {
69 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10+ },
11+ "dependencies": {
12+ "@modelcontextprotocol/sdk": "^1.26.0",
13+ "zod": "^4.3.6"
714 }
815 }
v2/packages/mcp-gateway/src/index.ts+1−0
@@ -0,0 +1 @@
1+export * from "./server";
v2/packages/mcp-gateway/src/server.ts+70−0
@@ -0,0 +1,70 @@
1+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3+import { z } from "zod";
4+
5+export type GatewayTarget = "local" | "remote";
6+
7+export type McpGatewayOptions = {
8+ target: GatewayTarget;
9+ serverName?: string;
10+ serverVersion?: string;
11+};
12+
13+const DEFAULT_SERVER_NAME = "executor-v2";
14+const DEFAULT_SERVER_VERSION = "0.0.0";
15+const STUB_TOOL_NAME = "executor.ping";
16+
17+const PingToolInput = z.object({
18+ message: z.string().optional(),
19+});
20+
21+const createStubMcpServer = (options: McpGatewayOptions): McpServer => {
22+ const mcp = new McpServer({
23+ name: options.serverName ?? DEFAULT_SERVER_NAME,
24+ version: options.serverVersion ?? DEFAULT_SERVER_VERSION,
25+ });
26+
27+ mcp.registerTool(
28+ STUB_TOOL_NAME,
29+ {
30+ description: "Stub MCP tool that replies with pong",
31+ inputSchema: PingToolInput,
32+ },
33+ async (input: { message?: string }) => {
34+ const text = input.message
35+ ? `pong (${options.target}) - ${input.message}`
36+ : `pong (${options.target})`;
37+
38+ return {
39+ content: [
40+ {
41+ type: "text" as const,
42+ text,
43+ },
44+ ],
45+ isError: false,
46+ };
47+ },
48+ );
49+
50+ return mcp;
51+};
52+
53+export const handleMcpHttpRequest = async (
54+ request: Request,
55+ options: McpGatewayOptions,
56+): Promise<Response> => {
57+ const transport = new WebStandardStreamableHTTPServerTransport({
58+ sessionIdGenerator: undefined,
59+ enableJsonResponse: true,
60+ });
61+ const mcp = createStubMcpServer(options);
62+
63+ try {
64+ await mcp.connect(transport);
65+ return await transport.handleRequest(request);
66+ } finally {
67+ await transport.close().catch(() => undefined);
68+ await mcp.close().catch(() => undefined);
69+ }
70+};
v2/packages/schema/README.md+1−1
@@ -3,7 +3,7 @@ · 2 unmodified lines
33 Canonical Effect schema package scaffold for Executor v2.
44
55 Current scaffold includes:
6−- branded IDs (`src/ids.ts`)
6+- branded domain IDs (`src/ids.ts`)
77 - shared enums and primitives (`src/enums.ts`, `src/common.ts`)
88 - core model schemas in `src/models/*`
99 - local snapshot/event schemas (`src/snapshot.ts`)
v2/packages/source-manager/package.json+8−0
@@ -2,7 +2,15 @@ · 1 unmodified line
22 "name": "@executor-v2/source-manager",
33 "private": true,
44 "type": "module",
5+ "exports": {
6+ ".": "./src/index.ts"
7+ },
58 "scripts": {
69 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10+ },
11+ "dependencies": {
12+ "@executor-v2/persistence-ports": "workspace:*",
13+ "@executor-v2/schema": "workspace:*",
14+ "effect": "^3.19.8"
715 }
816 }
v2/packages/source-manager/tsconfig.json+3−0
@@ -1,4 +1,7 @@
11 {
22 "extends": "../../tsconfig.json",
3+ "compilerOptions": {
4+ "types": ["node"]
5+ },
36 "include": ["src"]
47 }
v2/packages/state-contracts/README.md+6−2
@@ -1,3 +1,7 @@
1−# state-contracts
1+# persistence-ports
22
3−Storage and repository interface scaffold for Executor v2.
3+Persistence port interface scaffold for Executor v2.
4+
5+Current scaffold includes:
6+- `ProfileStore` in `src/profiles.ts`
7+- `ToolArtifactStore` in `src/tool-artifacts.ts`
v2/packages/state-contracts/package.json+8−1
@@ -1,8 +1,15 @@
11 {
2− "name": "@executor-v2/state-contracts",
2+ "name": "@executor-v2/persistence-ports",
33 "private": true,
44 "type": "module",
5+ "exports": {
6+ ".": "./src/index.ts"
7+ },
58 "scripts": {
69 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10+ },
11+ "dependencies": {
12+ "@executor-v2/schema": "workspace:*",
13+ "effect": "^3.19.8"
714 }
815 }
v2/packages/state-contracts/src/index.ts+2−0
@@ -0,0 +1,2 @@
1+export type { ProfileStore } from "./profiles";
2+export type { ToolArtifactStore } from "./tool-artifacts";
v2/packages/state-contracts/src/profiles.ts+7−0
@@ -0,0 +1,7 @@
1+import type { Profile, ProfileId } from "@executor-v2/schema";
2+import type * as Effect from "effect/Effect";
3+import type * as Option from "effect/Option";
4+
5+export interface ProfileStore {
6+ getById(id: ProfileId): Effect.Effect<Option.Option<Profile>>;
7+}
v2/packages/state-contracts/src/tool-artifacts.ts+12−0
@@ -0,0 +1,12 @@
1+import type { SourceId, ToolArtifact, WorkspaceId } from "@executor-v2/schema";
2+import type * as Effect from "effect/Effect";
3+import type * as Option from "effect/Option";
4+
5+export interface ToolArtifactStore {
6+ getBySource(
7+ workspaceId: WorkspaceId,
8+ sourceId: SourceId,
9+ ): Effect.Effect<Option.Option<ToolArtifact>>;
10+
11+ upsert(artifact: ToolArtifact): Effect.Effect<void>;
12+}
v2/packages/state-convex/README.md+7−2
@@ -1,3 +1,8 @@
1−# state-convex
1+# persistence-convex
22
3−Convex-backed state adapter scaffold for Executor v2.
3+Convex-backed persistence adapter scaffold for Executor v2.
4+
5+Current scaffold includes:
6+- Convex table-typed ID schemas in `src/convex-ids.ts`
7+- shared Convex schema projection in `src/schema.ts`
8+- first adapter implementation for `ProfileStore` in `src/profiles.ts`
v2/packages/state-convex/package.json+10−1
@@ -1,8 +1,17 @@
11 {
2− "name": "@executor-v2/state-convex",
2+ "name": "@executor-v2/persistence-convex",
33 "private": true,
44 "type": "module",
5+ "exports": {
6+ ".": "./src/index.ts"
7+ },
58 "scripts": {
69 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10+ },
11+ "dependencies": {
12+ "@executor-v2/confect": "workspace:*",
13+ "@executor-v2/schema": "workspace:*",
14+ "@executor-v2/persistence-ports": "workspace:*",
15+ "effect": "^3.19.8"
716 }
817 }
v2/packages/state-convex/src/convex-ids.ts+25−0
@@ -0,0 +1,25 @@
1+import { Id } from "@executor-v2/confect";
2+
3+export const ProfileDocIdSchema = Id("profiles");
4+export const WorkspaceDocIdSchema = Id("workspaces");
5+export const SourceDocIdSchema = Id("sources");
6+export const ToolArtifactDocIdSchema = Id("toolArtifacts");
7+export const CredentialRefDocIdSchema = Id("credentialRefs");
8+export const OAuthTokenDocIdSchema = Id("oauthTokens");
9+export const PolicyDocIdSchema = Id("policies");
10+export const ApprovalDocIdSchema = Id("approvals");
11+export const TaskRunDocIdSchema = Id("taskRuns");
12+export const SyncStateDocIdSchema = Id("syncStates");
13+export const EventDocIdSchema = Id("events");
14+
15+export type ProfileDocId = typeof ProfileDocIdSchema.Type;
16+export type WorkspaceDocId = typeof WorkspaceDocIdSchema.Type;
17+export type SourceDocId = typeof SourceDocIdSchema.Type;
18+export type ToolArtifactDocId = typeof ToolArtifactDocIdSchema.Type;
19+export type CredentialRefDocId = typeof CredentialRefDocIdSchema.Type;
20+export type OAuthTokenDocId = typeof OAuthTokenDocIdSchema.Type;
21+export type PolicyDocId = typeof PolicyDocIdSchema.Type;
22+export type ApprovalDocId = typeof ApprovalDocIdSchema.Type;
23+export type TaskRunDocId = typeof TaskRunDocIdSchema.Type;
24+export type SyncStateDocId = typeof SyncStateDocIdSchema.Type;
25+export type EventDocId = typeof EventDocIdSchema.Type;
v2/packages/state-convex/src/index.ts+3−0
@@ -0,0 +1,3 @@
1+export * from "./convex-ids";
2+export * from "./schema";
3+export * from "./profiles";
v2/packages/state-convex/src/profiles.ts+33−0
@@ -0,0 +1,33 @@
1+import type {
2+ ConfectQueryCtx,
3+ DocumentFromTable,
4+} from "@executor-v2/confect";
5+import { ProfileSchema, type Profile, type ProfileId } from "@executor-v2/schema";
6+import type { ProfileStore } from "@executor-v2/persistence-ports";
7+import * as Effect from "effect/Effect";
8+import { pipe } from "effect/Function";
9+import * as Option from "effect/Option";
10+import * as Schema from "effect/Schema";
11+
12+import type { ExecutorConfectTables } from "./schema";
13+
14+type ProfileDocument = DocumentFromTable<ExecutorConfectTables, "profiles">;
15+
16+const toProfile = (document: ProfileDocument): Profile => {
17+ const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...profile } = document;
18+ return Schema.decodeUnknownSync(ProfileSchema)(profile);
19+};
20+
21+export const makeConvexProfileStore = (
22+ ctx: ConfectQueryCtx<ExecutorConfectTables>,
23+): ProfileStore => ({
24+ getById: (id: ProfileId) =>
25+ pipe(
26+ ctx.db
27+ .query("profiles")
28+ .withIndex("by_domainId", (q) => q.eq("id", id))
29+ .unique(),
30+ Effect.orDie,
31+ Effect.map(Option.map(toProfile)),
32+ ),
33+});
v2/packages/state-convex/src/schema.ts+56−0
@@ -0,0 +1,56 @@
1+import { defineSchema, defineTable, type TablesFromSchemaDefinition } from "@executor-v2/confect";
2+import {
3+ ApprovalSchema,
4+ CredentialRefSchema,
5+ EventEnvelopeSchema,
6+ OAuthTokenSchema,
7+ PolicySchema,
8+ ProfileSchema,
9+ SourceSchema,
10+ SyncStateSchema,
11+ TaskRunSchema,
12+ ToolArtifactSchema,
13+ WorkspaceSchema,
14+} from "@executor-v2/schema";
15+
16+export const executorConfectSchema = defineSchema({
17+ profiles: defineTable(ProfileSchema).index("by_domainId", ["id"]),
18+ workspaces: defineTable(WorkspaceSchema)
19+ .index("by_domainId", ["id"])
20+ .index("by_profileId", ["profileId"]),
21+ sources: defineTable(SourceSchema)
22+ .index("by_domainId", ["id"])
23+ .index("by_workspaceId", ["workspaceId"]),
24+ toolArtifacts: defineTable(ToolArtifactSchema)
25+ .index("by_domainId", ["id"])
26+ .index("by_workspaceId", ["workspaceId"])
27+ .index("by_sourceId", ["sourceId"]),
28+ credentialRefs: defineTable(CredentialRefSchema)
29+ .index("by_domainId", ["id"])
30+ .index("by_workspaceId", ["workspaceId"])
31+ .index("by_sourceId", ["sourceId"]),
32+ oauthTokens: defineTable(OAuthTokenSchema)
33+ .index("by_domainId", ["id"])
34+ .index("by_workspaceId", ["workspaceId"])
35+ .index("by_sourceId", ["sourceId"]),
36+ policies: defineTable(PolicySchema)
37+ .index("by_domainId", ["id"])
38+ .index("by_workspaceId", ["workspaceId"]),
39+ approvals: defineTable(ApprovalSchema)
40+ .index("by_domainId", ["id"])
41+ .index("by_workspaceId", ["workspaceId"])
42+ .index("by_taskRunId", ["taskRunId"]),
43+ taskRuns: defineTable(TaskRunSchema)
44+ .index("by_domainId", ["id"])
45+ .index("by_workspaceId", ["workspaceId"])
46+ .index("by_sessionId", ["sessionId"]),
47+ syncStates: defineTable(SyncStateSchema)
48+ .index("by_domainId", ["id"])
49+ .index("by_workspaceId", ["workspaceId"]),
50+ events: defineTable(EventEnvelopeSchema)
51+ .index("by_domainId", ["id"])
52+ .index("by_workspaceId", ["workspaceId"])
53+ .index("by_workspaceId_sequence", ["workspaceId", "sequence"]),
54+});
55+
56+export type ExecutorConfectTables = TablesFromSchemaDefinition<typeof executorConfectSchema>;
v2/packages/state-local-file/README.md+6−2
@@ -1,3 +1,7 @@
1−# state-local-file
1+# persistence-local
22
3−Local file-backed state adapter scaffold for Executor v2.
3+Local file-backed persistence adapter scaffold for Executor v2.
4+
5+Current scaffold includes:
6+- local file `ToolArtifactStore` implementation in `src/tool-artifact-store.ts`
7+- atomic JSON persistence for extracted tool artifacts
v2/packages/state-local-file/package.json+9−1
@@ -1,8 +1,16 @@
11 {
2− "name": "@executor-v2/state-local-file",
2+ "name": "@executor-v2/persistence-local",
33 "private": true,
44 "type": "module",
5+ "exports": {
6+ ".": "./src/index.ts"
7+ },
58 "scripts": {
69 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10+ },
11+ "dependencies": {
12+ "@executor-v2/persistence-ports": "workspace:*",
13+ "@executor-v2/schema": "workspace:*",
14+ "effect": "^3.19.8"
715 }
816 }
v2/packages/state-local-file/src/index.ts+7−0
@@ -0,0 +1,7 @@
1+export {
2+ LocalPersistenceError,
3+ makeLocalToolArtifactStore,
4+ makeSourceId,
5+ makeWorkspaceId,
6+ type LocalToolArtifactStoreOptions,
7+} from "./tool-artifact-store";
v2/packages/state-local-file/src/tool-artifact-store.ts+138−0
@@ -0,0 +1,138 @@
1+import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2+import { dirname, resolve } from "node:path";
3+
4+import type { ToolArtifactStore } from "@executor-v2/persistence-ports";
5+import {
6+ SourceIdSchema,
7+ ToolArtifactSchema,
8+ WorkspaceIdSchema,
9+ type SourceId,
10+ type ToolArtifact,
11+ type WorkspaceId,
12+} from "@executor-v2/schema";
13+import * as Effect from "effect/Effect";
14+import { pipe } from "effect/Function";
15+import * as Option from "effect/Option";
16+import * as Schema from "effect/Schema";
17+
18+export class LocalPersistenceError extends Schema.TaggedError<LocalPersistenceError>()(
19+ "LocalPersistenceError",
20+ {
21+ operation: Schema.String,
22+ filePath: Schema.String,
23+ message: Schema.String,
24+ },
25+) {}
26+
27+const ToolArtifactListSchema = Schema.Array(ToolArtifactSchema);
28+
29+const decodeArtifacts = Schema.decodeUnknownSync(ToolArtifactListSchema);
30+const decodeWorkspaceId = Schema.decodeUnknownSync(WorkspaceIdSchema);
31+const decodeSourceId = Schema.decodeUnknownSync(SourceIdSchema);
32+
33+export type LocalToolArtifactStoreOptions = {
34+ rootDir: string;
35+};
36+
37+const defaultArtifactsFilePath = (rootDir: string): string =>
38+ resolve(rootDir, "tool-artifacts.json");
39+
40+const toLocalPersistenceError = (
41+ operation: string,
42+ filePath: string,
43+ cause: unknown,
44+): LocalPersistenceError =>
45+ new LocalPersistenceError({
46+ operation,
47+ filePath,
48+ message: cause instanceof Error ? cause.message : String(cause),
49+ });
50+
51+const readArtifacts = (filePath: string): Effect.Effect<Array<ToolArtifact>, LocalPersistenceError> =>
52+ pipe(
53+ Effect.tryPromise({
54+ try: () => readFile(filePath, "utf8"),
55+ catch: (cause) => toLocalPersistenceError("read", filePath, cause),
56+ }),
57+ Effect.flatMap((raw) =>
58+ Effect.try({
59+ try: () => decodeArtifacts(JSON.parse(raw)),
60+ catch: (cause) => toLocalPersistenceError("decode", filePath, cause),
61+ }),
62+ ),
63+ Effect.catchTag("LocalPersistenceError", (error) =>
64+ error.operation === "read" && error.message.includes("ENOENT")
65+ ? Effect.succeed([])
66+ : Effect.fail(error),
67+ ),
68+ );
69+
70+const writeArtifacts = (
71+ filePath: string,
72+ artifacts: Array<ToolArtifact>,
73+): Effect.Effect<void, LocalPersistenceError> => {
74+ const tempPath = `${filePath}.tmp`;
75+ return pipe(
76+ Effect.tryPromise({
77+ try: () => mkdir(dirname(filePath), { recursive: true }),
78+ catch: (cause) => toLocalPersistenceError("mkdir", filePath, cause),
79+ }),
80+ Effect.flatMap(() =>
81+ Effect.tryPromise({
82+ try: () => writeFile(tempPath, JSON.stringify(artifacts, null, 2), "utf8"),
83+ catch: (cause) => toLocalPersistenceError("write", tempPath, cause),
84+ }),
85+ ),
86+ Effect.flatMap(() =>
87+ Effect.tryPromise({
88+ try: () => rename(tempPath, filePath),
89+ catch: (cause) => toLocalPersistenceError("rename", filePath, cause),
90+ }),
91+ ),
92+ );
93+};
94+
95+export const makeLocalToolArtifactStore = (
96+ options: LocalToolArtifactStoreOptions,
97+): ToolArtifactStore => {
98+ const filePath = defaultArtifactsFilePath(options.rootDir);
99+
100+ return {
101+ getBySource: (workspaceId: WorkspaceId, sourceId: SourceId) =>
102+ pipe(
103+ readArtifacts(filePath),
104+ Effect.map((artifacts) => {
105+ const match = artifacts.find(
106+ (artifact) =>
107+ artifact.workspaceId === workspaceId && artifact.sourceId === sourceId,
108+ );
109+ return Option.fromNullable(match);
110+ }),
111+ ),
112+
113+ upsert: (artifact: ToolArtifact) =>
114+ pipe(
115+ readArtifacts(filePath),
116+ Effect.map((artifacts) => {
117+ const next = [...artifacts];
118+ const index = next.findIndex(
119+ (current) =>
120+ current.workspaceId === artifact.workspaceId &&
121+ current.sourceId === artifact.sourceId,
122+ );
123+
124+ if (index >= 0) {
125+ next[index] = artifact;
126+ } else {
127+ next.push(artifact);
128+ }
129+
130+ return next;
131+ }),
132+ Effect.flatMap((next) => writeArtifacts(filePath, next)),
133+ ),
134+ };
135+};
136+
137+export const makeWorkspaceId = (value: string): WorkspaceId => decodeWorkspaceId(value);
138+export const makeSourceId = (value: string): SourceId => decodeSourceId(value);
v2/packages/state-local-file/tsconfig.json+3−0
@@ -1,4 +1,7 @@
11 {
22 "extends": "../../tsconfig.json",
3+ "compilerOptions": {
4+ "types": ["node"]
5+ },
36 "include": ["src"]
47 }
v2/tsconfig.json+7−1
@@ -3,13 +3,19 @@ · 2 unmodified lines
33 "target": "ES2023",
44 "module": "ESNext",
55 "moduleResolution": "Bundler",
6+ "moduleDetection": "force",
67 "strict": true,
78 "noEmit": true,
89 "verbatimModuleSyntax": true,
910 "skipLibCheck": true,
1011 "resolveJsonModule": true,
1112 "allowSyntheticDefaultImports": true,
12− "types": []
13+ "types": [],
14+ "plugins": [
15+ {
16+ "name": "@effect/language-service"
17+ }
18+ ]
1319 },
1420 "include": [
1521 "apps/**/*.ts",

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t