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 @@
1import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1import path from "node:path";2import path from "node:path";
2 3
3type ApprovalStatus = "pending" | "approved" | "denied";4type ApprovalStatus = "pending" | "approved" | "denied";
4 5
5interface ApprovalItem {6interface ApprovalItem {
6 id: string;7 id: string;
7 title: string;8 action: string;
8 details: string;9 justification: string;
9 status: ApprovalStatus;
10 createdAt: number;10 createdAt: number;
11 status: ApprovalStatus;
12 decidedAt: number | null;
11}13}
12 14
13function parseArgs(argv: string[]): { session: string; inline: boolean; tmuxSession: string; agentCommand: string } {15interface PersistedState {
14 const session = argv[0] && !argv[0].startsWith("-") ? argv[0] : "claude";16 approvals: ApprovalItem[];
15 const inline = argv.includes("--inline");17}
18
19interface ParsedArgs {
20 session: string;
21 inline: boolean;
22 tmuxSession: string;
23 agentCommand: string;
24 approvalId: string | null;
25}
16 26
17 const tmuxSessionFlagIndex = argv.indexOf("--tmux-session");27const DEFAULT_MCP_CONFIG_PATH = path.resolve(import.meta.dir, "claude-mcp-demo.json");
18 const tmuxSession = tmuxSessionFlagIndex >= 0 && argv[tmuxSessionFlagIndex + 1]28const HOME_DIR = process.env.HOME ?? ".";
19 ? String(argv[tmuxSessionFlagIndex + 1])29const STATE_PATH = path.join(HOME_DIR, ".executor-lite-approvals", "mcp-state.json");
20 : "executor-approvals";
21 30
22 const agentCommandFlagIndex = argv.indexOf("--agent-cmd");31mkdirSync(path.dirname(STATE_PATH), { recursive: true });
23 const agentCommand = agentCommandFlagIndex >= 0 && argv[agentCommandFlagIndex + 1]
24 ? String(argv[agentCommandFlagIndex + 1])
25 : session;
26 32
27 return { session, inline, tmuxSession, agentCommand };33function shellQuote(value: string): string {
34 return `'${value.replace(/'/g, "'\\''")}'`;
28}35}
29 36
30function hasTmuxBinary(): boolean {37function getFlag(argv: string[], name: string): string | null {
31 const result = Bun.spawnSync({ cmd: ["tmux", "-V"] });38 const index = argv.indexOf(name);
32 return result.exitCode === 0;39 if (index < 0) return null;
40 const value = argv[index + 1];
41 return typeof value === "string" ? value : null;
33}42}
34 43
35function getTmuxClients(): string[] {44function defaultAgentCommand(session: string): string {
36 const result = Bun.spawnSync({ cmd: ["tmux", "list-clients", "-F", "#{client_name}"] });45 if (session === "claude") {
37 if (result.exitCode !== 0) return [];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 }
38 49
39 return Buffer.from(result.stdout)50 return session;
40 .toString("utf8")
41 .split("\n")
42 .map((line) => line.trim())
43 .filter((line) => line.length > 0 && line !== "no current client");
44}51}
45 52
46async function launchTmuxPopup(session: string, clientTarget: string): Promise<void> {53function parseArgs(argv: string[]): ParsedArgs {
47 const scriptPath = path.resolve(import.meta.dir, "approval-client.ts");54 const positionalSession = argv[0] && !argv[0].startsWith("-") ? argv[0] : "claude";
48 const command = `bun run ${scriptPath} ${session} --inline`;55 const session = getFlag(argv, "--session") ?? positionalSession;
49 Bun.spawnSync({56 const inline = argv.includes("--inline");
50 cmd: ["tmux", "display-popup", "-t", clientTarget, "-E", "-w", "82%", "-h", "70%", command],
51 stdin: "inherit",
52 stdout: "inherit",
53 stderr: "inherit",
54 });
55}
56 57
57function shellQuote(value: string): string {58 const tmuxSession = getFlag(argv, "--tmux-session") ?? "executor-approvals";
58 return `'${value.replace(/'/g, "'\\''")}'`;59 const agentCommand = getFlag(argv, "--agent-cmd") ?? defaultAgentCommand(session);
60 const approvalId = getFlag(argv, "--approval-id");
61
62 return { session, inline, tmuxSession, agentCommand, approvalId };
59}63}
60 64
61function tmuxSessionExists(tmuxSession: string): boolean {65function hasTmuxBinary(): boolean {
62 const result = Bun.spawnSync({ cmd: ["tmux", "has-session", "-t", tmuxSession] });66 const result = Bun.spawnSync({ cmd: ["tmux", "-V"] });
63 return result.exitCode === 0;67 return result.exitCode === 0;
64}68}
65 69
@@ -73,32 +77,24 @@ function runTmuxCommand(cmd: string[]): boolean { · 7 unmodified lines
73 return result.exitCode === 0;77 return result.exitCode === 0;
74}78}
75 79
80function tmuxSessionExists(tmuxSession: string): boolean {
81 const result = Bun.spawnSync({ cmd: ["tmux", "has-session", "-t", tmuxSession] });
82 return result.exitCode === 0;
83}
84
76function ensureSessionUiOptions(tmuxSession: string): void {85function ensureSessionUiOptions(tmuxSession: string): void {
77 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "mouse", "on"]);86 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "mouse", "on"]);
78 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "pane-border-status", "top"]);87 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "pane-border-status", "top"]);
79 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "pane-border-format", "#{pane_index}: #{pane_title}"]);88 runTmuxCommand(["tmux", "set-option", "-t", tmuxSession, "pane-border-format", "#{pane_index}: #{pane_title}"]);
80}89}
81 90
82function setPaneTitles(windowTarget: string): void {91async function launchManagedTmuxWorkspace(tmuxSession: string, agentCommand: string): Promise<boolean> {
83 runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.0`, "-T", "Claude"]);
84 runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.1`, "-T", "Approvals"]);
85}
86
87async 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 if (!tmuxSessionExists(tmuxSession)) {92 if (!tmuxSessionExists(tmuxSession)) {
92 const created = runTmuxCommand(["tmux", "new-session", "-d", "-s", tmuxSession, agentCommand]);93 const created = runTmuxCommand(["tmux", "new-session", "-d", "-s", tmuxSession, agentCommand]);
93 if (!created) return false;94 if (!created) return false;
94 95
95 ensureSessionUiOptions(tmuxSession);96 ensureSessionUiOptions(tmuxSession);
96 97 runTmuxCommand(["tmux", "select-pane", "-t", `${tmuxSession}:0.0`, "-T", "Claude"]);
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`]);
102 return runTmuxCommand(["tmux", "attach-session", "-t", tmuxSession]);98 return runTmuxCommand(["tmux", "attach-session", "-t", tmuxSession]);
103 }99 }
104 100
@@ -109,84 +105,96 @@ async function launchManagedTmuxWorkspace(session: string, tmuxSession: string, · 4 unmodified lines
109 const createdWindow = runTmuxCommand(["tmux", "new-window", "-t", `${tmuxSession}:`, "-n", windowName, agentCommand]);105 const createdWindow = runTmuxCommand(["tmux", "new-window", "-t", `${tmuxSession}:`, "-n", windowName, agentCommand]);
110 if (!createdWindow) return false;106 if (!createdWindow) return false;
111 107
112 const splitWindow = runTmuxCommand(["tmux", "split-window", "-t", windowTarget, "-v", "-p", "30", approvalCommand]);108 runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.0`, "-T", "Claude"]);
113 if (!splitWindow) return false;109 runTmuxCommand(["tmux", "select-window", "-t", windowTarget]);
114
115 setPaneTitles(windowTarget);
116 runTmuxCommand(["tmux", "select-pane", "-t", `${windowTarget}.0`]);
117 return runTmuxCommand(["tmux", "attach-session", "-t", tmuxSession]);110 return runTmuxCommand(["tmux", "attach-session", "-t", tmuxSession]);
118}111}
119 112
120function makeApproval(session: string, index: number): ApprovalItem {113function loadState(): PersistedState {
121 const actions = [114 if (!existsSync(STATE_PATH)) return { approvals: [] };
122 "Write DNS record",115
123 "Delete production file",116 try {
124 "Rotate API token",117 const text = readFileSync(STATE_PATH, "utf8");
125 "Deploy migration",118 const parsed = JSON.parse(text) as Partial<PersistedState>;
126 "Run destructive cleanup",119 if (!parsed || !Array.isArray(parsed.approvals)) {
127 ];120 return { approvals: [] };
128 121 }
129 const title = actions[index % actions.length] ?? "Tool action";122
130 const id = `appr_${crypto.randomUUID().slice(0, 8)}`;123 return {
131 124 approvals: parsed.approvals.filter((entry): entry is ApprovalItem => {
132 return {125 return (
133 id,126 Boolean(entry) &&
134 title,127 typeof entry.id === "string" &&
135 details: `Session: ${session} -> tools.example.action()`,128 typeof entry.action === "string" &&
136 status: "pending",129 typeof entry.justification === "string" &&
137 createdAt: Date.now(),130 typeof entry.createdAt === "number" &&
138 };131 (entry.status === "pending" || entry.status === "approved" || entry.status === "denied")
132 );
133 }),
134 };
135 } catch {
136 return { approvals: [] };
137 }
139}138}
140 139
141function statusMarker(status: ApprovalStatus): string {140function saveState(state: PersistedState): void {
142 if (status === "approved") return "[approved]";141 const tempPath = `${STATE_PATH}.tmp`;
143 if (status === "denied") return "[denied]";142 writeFileSync(tempPath, JSON.stringify(state, null, 2));
144 return "[pending]";143 renameSync(tempPath, STATE_PATH);
145}144}
146 145
147function clearAndRender(session: string, approvals: ApprovalItem[], selected: number): void {146function 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
154function 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
165function renderInline(session: string, current: ApprovalItem | null): void {
148 process.stdout.write("\x1Bc");166 process.stdout.write("\x1Bc");
149 process.stdout.write("Approval Popup Demo\n");167 process.stdout.write("Approval Pane (MCP-driven)\n");
150 process.stdout.write(`Session: ${session}\n`);168 process.stdout.write(`Session: ${session}\n`);
151 process.stdout.write("Controls: j/k move a approve d deny n new request q quit\n");169 process.stdout.write("Controls: a approve d deny q quit\n");
152 process.stdout.write("tmux tip: Ctrl+b then o to switch panes (mouse is enabled).\n");170 process.stdout.write("This pane only appears when an MCP approval is pending.\n");
153 process.stdout.write("-".repeat(72) + "\n");171 process.stdout.write("-".repeat(84) + "\n");
154 172
155 if (approvals.length === 0) {173 if (!current) {
156 process.stdout.write("No approvals. Press n to enqueue one.\n");174 process.stdout.write("No pending approval. Closing...\n");
157 return;175 return;
158 }176 }
159 177
160 approvals.forEach((item, idx) => {178 const created = new Date(current.createdAt).toLocaleTimeString();
161 const pointer = idx === selected ? ">" : " ";179 process.stdout.write(`[pending] ${current.id}\n`);
162 const time = new Date(item.createdAt).toLocaleTimeString();180 process.stdout.write(`action: ${current.action}\n`);
163 process.stdout.write(`${pointer} ${statusMarker(item.status)} ${item.id} ${item.title} (${time})\n`);181 process.stdout.write(`created: ${created}\n`);
164 process.stdout.write(` ${item.details}\n`);182 if (current.justification.trim().length > 0) {
165 });183 process.stdout.write(`justification: ${current.justification}\n`);
184 }
166}185}
167 186
168async function runInlineUi(session: string): Promise<number> {187async function runInlineUi(session: string, approvalId: string | null): Promise<number> {
169 const approvals: ApprovalItem[] = [makeApproval(session, 0)];188 let current = findApproval(loadState(), approvalId);
170 let selected = 0;189 renderInline(session, current);
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);
179 190
180 const cleanup = (): void => {191 const cleanup = (): void => {
181 clearInterval(enqueueTimer);
182 if (process.stdin.isTTY) {192 if (process.stdin.isTTY) {
183 process.stdin.setRawMode(false);193 process.stdin.setRawMode(false);
184 }194 }
185 process.stdin.pause();195 process.stdin.pause();
186 };196 };
187 197
188 clearAndRender(session, approvals, selected);
189
190 return await new Promise<number>((resolve) => {198 return await new Promise<number>((resolve) => {
191 if (process.stdin.isTTY) {199 if (process.stdin.isTTY) {
192 process.stdin.setRawMode(true);200 process.stdin.setRawMode(true);
@@ -200,59 +208,46 @@ async function runInlineUi(session: string): Promise<number> { · 7 unmodified lines
200 resolve(code);208 resolve(code);
201 };209 };
202 210
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
203 process.stdin.on("data", (chunk) => {222 process.stdin.on("data", (chunk) => {
204 const key = typeof chunk === "string" ? chunk : chunk.toString("utf8");223 const key = typeof chunk === "string" ? chunk : chunk.toString("utf8");
205 224
206 if (key === "\u0003" || key === "q") {225 if (key === "\u0003" || key === "q") {
226 clearInterval(poll);
207 finish(0);227 finish(0);
208 return;228 return;
209 }229 }
210 230
211 if (key === "j" || key === "\u001B[B") {231 if (!current) return;
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 }
239 232
240 if (key === "a") {233 if (key === "a") {
241 current.status = "approved";234 decideApproval(current.id, "approved");
242 clearAndRender(session, approvals, selected);235 clearInterval(poll);
236 finish(0);
243 return;237 return;
244 }238 }
245 239
246 if (key === "d") {240 if (key === "d") {
247 current.status = "denied";241 decideApproval(current.id, "denied");
248 clearAndRender(session, approvals, selected);242 clearInterval(poll);
243 finish(0);
249 }244 }
250 });245 });
251 });246 });
252}247}
253 248
254async function main(): Promise<number> {249async 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));
256 251
257 if (!inline) {252 if (!inline) {
258 if (!hasTmuxBinary()) {253 if (!hasTmuxBinary()) {
@@ -261,25 +256,16 @@ async function main(): Promise<number> { · 2 unmodified lines
261 return 1;256 return 1;
262 }257 }
263 258
264 const clients = getTmuxClients();259 console.log(`Launching '${agentCommand}' in tmux session '${tmuxSession}'...`);
265 const target = clients[0];260 const started = await launchManagedTmuxWorkspace(tmuxSession, agentCommand);
266 if (target) {261 if (started) return 0;
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 }
276 262
277 console.error("Unable to launch managed tmux workspace.");263 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.");
279 return 1;265 return 1;
280 }266 }
281 267
282 return await runInlineUi(session);268 return await runInlineUi(session, approvalId);
283}269}
284 270
285const code = await main();271const code = await main();
experiemental/mcp-memory-approvals.ts+155−85
@@ -1,30 +1,149 @@
1import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2import path from "node:path";
3
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";4import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";5import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3import { z } from "zod";6import { z } from "zod";
4 7
5type ApprovalDecision = "approved" | "denied";8type ApprovalStatus = "pending" | "approved" | "denied";
6 9
7interface ApprovalRecord {10interface ApprovalRecord {
8 id: string;11 id: string;
9 action: string;12 action: string;
10 justification: string;13 justification: string;
11 createdAt: number;14 createdAt: number;
12 decision: ApprovalDecision;15 status: ApprovalStatus;
13 decidedAt: number;16 decidedAt: number | null;
17}
18
19interface PersistedState {
20 approvals: ApprovalRecord[];
21}
22
23const HOME_DIR = process.env.HOME ?? ".";
24const DATA_DIR = path.join(HOME_DIR, ".executor-lite-approvals");
25const STATE_PATH = path.join(DATA_DIR, "mcp-state.json");
26const APPROVAL_UI_SCRIPT = path.resolve(import.meta.dir, "approval-client.ts");
27
28mkdirSync(DATA_DIR, { recursive: true });
29
30const state: PersistedState = loadState();
31
32function shellQuote(value: string): string {
33 return `'${value.replace(/'/g, "'\\''")}'`;
34}
35
36function 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 }
14}63}
15 64
16const approvals: ApprovalRecord[] = [];65function refreshStateFromDisk(): void {
66 const disk = loadState();
67 state.approvals = disk.approvals;
68}
69
70function saveState(): void {
71 const tempPath = `${STATE_PATH}.tmp`;
72 writeFileSync(tempPath, JSON.stringify(state, null, 2));
73 renameSync(tempPath, STATE_PATH);
74}
75
76function 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
91function removeApproval(approvalId: string): void {
92 state.approvals = state.approvals.filter((item) => item.id !== approvalId);
93 saveState();
94}
95
96function 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
104function 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
126async 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}
17 141
18const mcp = new McpServer(142const mcp = new McpServer(
19 { name: "memory-approval-demo", version: "0.0.1" },143 { name: "memory-approval-demo", version: "0.0.1" },
20 {144 {
21 capabilities: {145 capabilities: {
22 tools: {},146 tools: {},
23 experimental: {
24 elicitation: {
25 form: {},
26 },
27 },
28 },147 },
29 },148 },
30);149);
@@ -32,102 +151,53 @@ const mcp = new McpServer( · 1 unmodified line
32mcp.registerTool(151mcp.registerTool(
33 "dangerous_action",152 "dangerous_action",
34 {153 {
35 description: "Runs a fake dangerous action after explicit approval.",154 description: "Queues a dangerous action and waits for sidecar approval from shared state.",
36 inputSchema: {155 inputSchema: {
37 action: z.string().min(1),156 action: z.string().min(1),
38 justification: z.string().optional(),157 justification: z.string().optional(),
158 approvalTimeoutMs: z.number().int().min(1_000).max(600_000).optional(),
39 },159 },
40 },160 },
41 async ({ action, justification }) => {161 async ({ action, justification, approvalTimeoutMs }) => {
42 const approvalId = `appr_${crypto.randomUUID().slice(0, 8)}`;162 refreshStateFromDisk();
43 163 const pending = recordPendingApproval(action, justification ?? "");
44 let decision: ApprovalDecision = "denied";164 maybeOpenApprovalPane(pending.id);
45 165
46 try {166 const timeoutMs = approvalTimeoutMs ?? 120_000;
47 const response = await mcp.server.elicitInput({167 const decision = await waitForDecision(pending.id, timeoutMs);
48 mode: "form",168
49 message: `Approve dangerous action '${action}'?`,169 refreshStateFromDisk();
50 requestedSchema: {170 const current = state.approvals.find((item) => item.id === pending.id);
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 }
72 171
73 const now = Date.now();172 if (decision === "approved" && current?.status === "approved") {
74 approvals.unshift({173 removeApproval(pending.id);
75 id: approvalId,
76 action,
77 justification: justification ?? "",
78 createdAt: now,
79 decision,
80 decidedAt: now,
81 });
82
83 if (decision === "denied") {
84 return {174 return {
85 content: [{ type: "text", text: `Denied '${action}'.` }],175 content: [{ type: "text", text: `Approved and executed '${action}'.` }],
86 structuredContent: {176 structuredContent: {
87 approvalId,177 approvalId: pending.id,
88 status: "denied",178 status: "approved",
89 action,179 action,
180 executed: true,
90 },181 },
91 };182 };
92 }183 }
93 184
185 if (decision === "timeout") {
186 markDeniedIfStillPending(pending.id);
187 }
188
189 removeApproval(pending.id);
190
94 return {191 return {
95 content: [{ type: "text", text: `Approved and executed '${action}'.` }],192 content: [{ type: "text", text: `Denied '${action}' (${decision === "timeout" ? "approval timed out" : "approval denied"}).` }],
96 structuredContent: {193 structuredContent: {
97 approvalId,194 approvalId: pending.id,
98 status: "approved",195 status: "denied",
99 action,196 action,
100 executed: true,
101 },197 },
102 };198 };
103 },199 },
104);200);
105 201
106mcp.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
132const transport = new StdioServerTransport();202const transport = new StdioServerTransport();
133await mcp.connect(transport);203await 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 @@
1CLAUDE.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
91. Run `effect-solutions list` to see available guides
102. Run `effect-solutions show <topic>...` for relevant patterns (supports multiple topics)
113. Search `.reference/effect/` for real implementations (run `effect-solutions setup` first)
12
13Topics: quick-start, project-setup, tsconfig, basics, services-and-layers, data-modeling, error-handling, config, testing, cli.
14
15Never 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
3Reference implementations and inspiration for this plan:
4 `../AnswerOverflow`
5 `../quickhub`
6 `../create-epoch-app`
7
8## 1) Why v2 exists
9
10Executor today is optimized for enterprise/team correctness (workspaces, org flows, cloud control), but has too much friction for per-user adoption.
11
12v2 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
231. **Local-first**
24 - User gets value without login, org setup, or web onboarding.
252. **One mental model**
26 - Same commands, same execution semantics across local and remote targets.
273. **Schema-first**
28 - Effect Schema is the single source of truth for persisted/domain models.
294. **Execute-first tooling**
30 - Top-level model tool surface is minimal; code execution is primary.
315. **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
54Control 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
61Execution 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
68v2/
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
99confect -> schema -> persistence-ports
100
101persistence-ports -> persistence-local
102persistence-ports -> persistence-convex
103
104schema + persistence-ports + oauth + secrets -> domain
105
106domain -> approvals/source-manager/engine
107approvals/source-manager/engine -> mcp-gateway
108
109domain/features -> rpc -> sdk -> ai-sdk-adapter
110
111apps/pm -> mcp-gateway + domain + persistence-local + rpc
112apps/convex -> mcp-gateway + domain + persistence-convex + rpc
113apps/cli/web -> sdk
114```
115
116Rules:
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
127Responsibilities:
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
143Crash/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
152Effect Schema is canonical (`packages/schema`).
153
154Current 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
168IDs 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
172This 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
180Keep top-level surface minimal:
181- `execute` (primary)
182- optional diagnostics (`health`, `version`) only if needed
183
184### Configuration model
185
186Executor 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
192No 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
204Goal: 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
209Registration flow states:
210- `draft -> probing -> auth_required -> connected | error`
211
212Auth handling:
213- MCP: dynamic auth/OAuth detection, client registration where needed
214- OpenAPI: inspect security schemes, prompt for API key if required
215
216Secrets are entered through host UX, not model transcript.
217
218---
219
220## 11) Approvals and elicitation/input
221
222Use 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
235For 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
242Approval/input actions are **host UX callbacks**, not AI SDK model tools.
243
244---
245
246## 12) AI SDK strategy
247
248API 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
259Primary operations:
260- execute code
261- register/list/remove sources
262- stream run status/results
263
264Do not expose approval/input-response as model tools.
265Use 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
281Primary path:
282- `local-lite -> remote`
283
284Optional reverse:
285- `remote -> local-lite` export/backup
286
287Sync 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
351Implemented 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
358Not implemented yet:
359- runtime wiring, adapters, rpc handlers, PM behavior, sync engine
360
361---
362
363## 19) Immediate next steps
364
3651. Define `persistence-ports` interfaces against current schema models.
3662. Implement minimal `persistence-local` snapshot + event log codec.
3673. Wire a minimal `apps/pm` execute flow (single run lifecycle).
3684. Add first `tools.executor.sources.add/list/remove` control operations.
3695. 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 @@
1# app-convex1# app-convex
2 2
3Convex remote server app scaffold for Executor v2.3Convex remote server app scaffold for Executor v2.
4
5Current 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
3Write your Convex functions here.
4See https://docs.convex.dev/functions for more.
5
6A query function that takes two arguments looks like:
7
8```ts
9// convex/myFunctions.ts
10import { query } from "./_generated/server";
11import { v } from "convex/values";
12
13export 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
36Using this query function in a React component looks like:
37
38```ts
39const data = useQuery(api.myFunctions.myQueryFunction, {
40 first: 10,
41 second: "hello",
42});
43```
44
45A mutation function looks like:
46
47```ts
48// convex/myFunctions.ts
49import { mutation } from "./_generated/server";
50import { v } from "convex/values";
51
52export 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
73Using this mutation function in a React component looks like:
74
75```ts
76const mutation = useMutation(api.myFunctions.myMutationFunction);
77function 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
88Use the Convex CLI to push your functions to a deployment. See everything
89the Convex CLI can do by running `npx convex -h` in your project root
90directory. 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
11import type * as http from "../http.js";
12import type * as mcp from "../mcp.js";
13
14import type {
15 ApiFromModules,
16 FilterApi,
17 FunctionReference,
18} from "convex/server";
19
20declare 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 */
33export 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 */
46export declare const internal: FilterApi<
47 typeof fullApi,
48 FunctionReference<any, "internal">
49>;
50
51export 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
11import { 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 */
21export const api = anyApi;
22export const internal = anyApi;
23export 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
11import type {
12 DataModelFromSchemaDefinition,
13 DocumentByName,
14 TableNamesInDataModel,
15 SystemTableNames,
16} from "convex/server";
17import type { GenericId } from "convex/values";
18import schema from "../schema.js";
19
20/**
21 * The names of all of your Convex tables.
22 */
23export 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 */
30export 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 */
48export 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 */
60export 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
11import {
12 ActionBuilder,
13 HttpActionBuilder,
14 MutationBuilder,
15 QueryBuilder,
16 GenericActionCtx,
17 GenericMutationCtx,
18 GenericQueryCtx,
19 GenericDatabaseReader,
20 GenericDatabaseWriter,
21} from "convex/server";
22import 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 */
32export 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 */
42export 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 */
52export 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 */
62export 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 */
75export 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 */
83export 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 */
96export 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 */
107export 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 */
115export 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 */
123export 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 */
132export 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 */
143export 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
11import {
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 */
29export 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 */
39export 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 */
49export 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 */
59export 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 */
72export 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 */
80export 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 */
93export const httpAction = httpActionGeneric;
v2/apps/convex/convex/http.ts+13−0
@@ -0,0 +1,13 @@
1import { httpRouter } from "convex/server";
2import { mcpHandler } from "./mcp";
3
4const http = httpRouter();
5
6http.route({ path: "/mcp", method: "POST", handler: mcpHandler });
7http.route({ path: "/mcp", method: "GET", handler: mcpHandler });
8http.route({ path: "/mcp", method: "DELETE", handler: mcpHandler });
9http.route({ path: "/v1/mcp", method: "POST", handler: mcpHandler });
10http.route({ path: "/v1/mcp", method: "GET", handler: mcpHandler });
11http.route({ path: "/v1/mcp", method: "DELETE", handler: mcpHandler });
12
13export default http;
v2/apps/convex/convex/mcp.ts+9−0
@@ -0,0 +1,9 @@
1import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway";
2import { httpAction } from "./_generated/server";
3
4export 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 @@
1import { executorConfectSchema } from "@executor-v2/persistence-convex";
2
3export 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
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "scripts": {5 "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"
7 }14 }
8}15}
v2/apps/convex/tsconfig.json+1−1
@@ -1,4 +1,4 @@
1{1{
2 "extends": "../../tsconfig.json",2 "extends": "../../tsconfig.json",
3 "include": ["src"]3 "include": ["src", "convex"]
4}4}
v2/apps/pm/README.md+5−0
@@ -1,3 +1,8 @@
1# app-pm1# app-pm
2 2
3Local Process Manager app scaffold for Executor v2.3Local Process Manager app scaffold for Executor v2.
4
5Current 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
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "scripts": {5 "scripts": {
6 "dev": "bun --watch src/main.ts",
7 "start": "bun src/main.ts",
6 "typecheck": "bunx tsc --noEmit -p tsconfig.json"8 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
9 },
10 "dependencies": {
11 "@executor-v2/mcp-gateway": "workspace:*"
7 }12 }
8}13}
v2/apps/pm/src/main.ts+31−0
@@ -0,0 +1,31 @@
1import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway";
2
3const port = Number(Bun.env.PORT ?? 8787);
4
5const handleMcp = async (request: Request): Promise<Response> =>
6 handleMcpHttpRequest(request, {
7 target: "local",
8 serverName: "executor-v2-pm",
9 serverVersion: "0.0.0",
10 });
11
12const 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
31console.log(`executor-v2 PM listening on http://127.0.0.1:${server.port}`);
v2/apps/pm/tsconfig.json+3−0
@@ -1,4 +1,7 @@
1{1{
2 "extends": "../../tsconfig.json",2 "extends": "../../tsconfig.json",
3 "compilerOptions": {
4 "types": ["bun"]
5 },
3 "include": ["src"]6 "include": ["src"]
4}7}
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
8 "packages/*"8 "packages/*"
9 ],9 ],
10 "scripts": {10 "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"
12 }17 }
13}18}
v2/packages/confect-core/README.md+0−5
@@ -1,5 +0,0 @@
1# confect-core
2
3Effect schema helper primitives scaffold for Executor v2.
4
5Current 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 @@
1export * from "./schema";
v2/packages/confect-core/src/schema.ts+0−6
@@ -1,6 +0,0 @@
1import { Schema } from "effect";
2
3export const TimestampMsSchema = Schema.Number;
4
5export 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 @@
1import type { FunctionReference, FunctionReturnType } from "convex/server";
2import {
3 ConvexClient as ConvexClientImpl,
4 ConvexHttpClient,
5} from "convex/browser";
6import { Context, Effect, Layer, Stream } from "effect";
7
8export interface ConvexRequestMetadata {
9 readonly headers?: Readonly<Record<string, string>>;
10}
11
12export 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
37export class ConvexClient extends Context.Tag("@confect/ConvexClient")<
38 ConvexClient,
39 ConvexClientService
40>() {}
41
42export 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 */
113export 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 @@
1import 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";
22import type { GenericId, Value } from "convex/values";
23import * as Chunk from "effect/Chunk";
24import * as Context from "effect/Context";
25import * as Effect from "effect/Effect";
26import { pipe } from "effect/Function";
27import * as Option from "effect/Option";
28import * as Schema from "effect/Schema";
29import * as Stream from "effect/Stream";
30
31import type {
32 GenericConfectSchema,
33 TableNamesInSchema,
34 DocumentFromTable,
35 EncodedDocumentFromTable,
36} from "./schema";
37
38export 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 */
54export 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
62type TableSchemas<Tables extends GenericConfectSchema> = {
63 [TableName in TableNamesInSchema<Tables>]: Schema.Schema<
64 DocumentFromTable<Tables, TableName>,
65 EncodedDocumentFromTable<Tables, TableName>
66 >;
67};
68
69interface 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
94interface ConfectOrderedQuery<
95 Tables extends GenericConfectSchema,
96 TableName extends TableNamesInSchema<Tables>,
97> extends Omit<ConfectQuery<Tables, TableName>, "order"> {}
98
99export class NotUniqueError extends Schema.TaggedError<NotUniqueError>()(
100 "NotUniqueError",
101 {},
102) {}
103
104interface 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
111interface 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
117class 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
209interface 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
226class 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
307export 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
322export 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
342class 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
406class 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
460export interface ConfectAuth {
461 getUserIdentity(): Effect.Effect<Option.Option<{
462 tokenIdentifier: string;
463 subject: string;
464 issuer: string;
465 [key: string]: unknown;
466 }>>;
467}
468
469class 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 */
506type 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
515export 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
531export 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
555export 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
575export const ConfectQueryCtx = <Tables extends GenericConfectSchema>() =>
576 Context.GenericTag<ConfectQueryCtx<Tables>>("@confect/ConfectQueryCtx");
577
578export const ConfectMutationCtx = <Tables extends GenericConfectSchema>() =>
579 Context.GenericTag<ConfectMutationCtx<Tables>>("@confect/ConfectMutationCtx");
580
581export const ConfectActionCtx = <Tables extends GenericConfectSchema>() =>
582 Context.GenericTag<ConfectActionCtx<Tables>>("@confect/ConfectActionCtx");
583
584export 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
600export 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 */
638const 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
657export 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 @@
1export {
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
14export {
15 schemaToValidator,
16 schemaToObjectValidator,
17 Id,
18 UnsupportedSchemaError,
19 TopLevelMustBeObjectError,
20 IndexSignaturesNotSupportedError,
21} from "./validators";
22
23export {
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
37export { ConvexClient, ConvexClientLayer, ConvexHttpClientLayer, type ConvexClientService } from "./client";
38
39export {
40 Cursor,
41 PaginationOptionsSchema,
42 PaginationResultSchema,
43} from "./pagination";
v2/packages/confect/src/pagination.ts+19−0
@@ -0,0 +1,19 @@
1import * as Schema from "effect/Schema";
2
3export const Cursor = Schema.String.pipe(Schema.brand("Cursor"));
4export type Cursor = typeof Cursor.Type;
5
6export const PaginationOptionsSchema = Schema.Struct({
7 cursor: Schema.NullOr(Cursor),
8 numItems: Schema.Number,
9});
10export type PaginationOptionsSchema = typeof PaginationOptionsSchema.Type;
11
12export 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 @@
1import { describe, expect, it } from "vitest";
2import { Effect, Layer, Option, Schema, Stream } from "effect";
3import { Atom, Registry, Result } from "@effect-atom/atom";
4import { createRpcClient, createServerRpcQuery, RpcDefectError } from "./client";
5import { createRpcFactory, makeRpcModule } from "./server";
6import { defineTable, defineSchema } from "../schema";
7import { ConvexClient, type ConvexClientService } from "../client";
8
9const testSchema = defineSchema({
10 guestbook: defineTable(
11 Schema.Struct({
12 author: Schema.String,
13 message: Schema.String,
14 }),
15 ),
16});
17
18const factory = createRpcFactory({ schema: testSchema });
19
20// Define endpoints
21const addEndpoint = factory.mutation({
22 payload: { author: Schema.String, message: Schema.String },
23 success: Schema.String,
24});
25addEndpoint.implement((payload) =>
26 Effect.gen(function* () {
27 return `Added message from ${payload.author}`;
28 }),
29);
30
31const listEndpoint = factory.query({
32 success: Schema.Array(Schema.Struct({ author: Schema.String, message: Schema.String })),
33});
34listEndpoint.implement(() =>
35 Effect.gen(function* () {
36 return [{ author: "Alice", message: "Hello" }];
37 }),
38);
39
40const 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});
45getEndpoint.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
54const sendNotificationEndpoint = factory.action({
55 payload: { userId: Schema.String },
56 success: Schema.Struct({ sent: Schema.Boolean }),
57});
58sendNotificationEndpoint.implement((_payload) =>
59 Effect.gen(function* () {
60 return { sent: true };
61 }),
62);
63
64const guestbookModule = makeRpcModule({
65 add: addEndpoint,
66 list: listEndpoint,
67 get: getEndpoint,
68 sendNotification: sendNotificationEndpoint,
69});
70
71describe("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
343import { Cause, Chunk, Exit, FiberId } from "effect";
344import type { ExitEncoded } from "./server";
345
346class NotFoundError extends Schema.TaggedError<NotFoundError>()(
347 "NotFoundError",
348 { id: Schema.String },
349) {}
350
351class ForbiddenError extends Schema.TaggedError<ForbiddenError>()(
352 "ForbiddenError",
353 {},
354) {}
355
356const 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
370type 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
386const 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
409const 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
441describe("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 */
518const 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
562describe("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
727describe("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 @@
1import type { Rpc } from "@effect/rpc";
2import type {
3 FunctionReference,
4 RegisteredQuery,
5 RegisteredMutation,
6 RegisteredAction,
7} from "convex/server";
8import { Atom, Result } from "@effect-atom/atom";
9import { Result as ResultModule, useAtomValue } from "@effect-atom/atom-react";
10import * as Cause from "effect/Cause";
11import { Cache, Chunk, Data, Duration, Effect, Exit, FiberId, Layer, Option, Stream } from "effect";
12
13import {
14 ConvexClient,
15 ConvexClientLayer,
16 ConvexHttpClientLayer,
17 type ConvexRequestMetadata,
18} from "../client";
19import type { AnyRpcModule, ExitEncoded, RpcEndpoint } from "./server";
20import {
21 makeRpcTransportHeaders,
22 type RpcClientKind,
23 withOptionalRpcTelemetryContext,
24} from "./telemetry";
25
26export 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. */
35const 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
56type EndpointPayload<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn>
57 ? Rpc.Payload<R>
58 : never;
59
60type EndpointSuccess<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn>
61 ? Rpc.Success<R>
62 : never;
63
64type EndpointError<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn>
65 ? Rpc.Error<R>
66 : never;
67
68type 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
78type IsPaginatedResult<T> = T extends {
79 page: ReadonlyArray<infer _Item>;
80 isDone: boolean;
81 continueCursor: string;
82}
83 ? true
84 : false;
85
86type ExtractPageItem<T> = T extends {
87 page: ReadonlyArray<infer Item>;
88 isDone: boolean;
89 continueCursor: string;
90}
91 ? Item
92 : never;
93
94type 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 */
105type PaginatedExtraPayload<Payload> = Omit<Payload, "cursor" | "numItems">;
106
107type PaginatedArgs<Extra> = keyof Extra extends never
108 ? [numItems: number]
109 : [numItems: number, payload: Extra];
110
111export interface RpcReadOptions {
112 readonly enabled?: boolean;
113}
114
115export 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
139export 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
145export 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
154type 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
175type EndpointsRecord = Record<string, RpcEndpoint<string, Rpc.Any, unknown>>;
176
177export type RpcModuleClientMethods<TEndpoints extends EndpointsRecord, Shared extends Record<string, unknown> = {}> = {
178 readonly [K in keyof TEndpoints]: DecorateEndpoint<TEndpoints[K], Shared>;
179};
180
181export interface RpcModuleClientConfig {
182 readonly url: string;
183 readonly layer?: Layer.Layer<ConvexClient>;
184 readonly enablePayloadTelemetryFallback?: boolean;
185}
186
187type ConvexApiModule = Record<string, FunctionReference<"query" | "mutation" | "action">>;
188
189type 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
195type 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
200export type RpcModuleClient<
201 TModule extends AnyRpcModule,
202 Shared extends Record<string, unknown> = {},
203> = {
204 readonly runtime: Atom.AtomRuntime<ConvexClient>;
205} & ExtractDecoratedEndpoints<TModule, Shared>;
206
207type 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
215const 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
232const 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
266const 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
281const 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
318const 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
345const 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
372const 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
399const 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
416const 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
448const 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
465const 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
485const 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
505interface PaginatedResult<T> {
506 page: ReadonlyArray<T>;
507 isDone: boolean;
508 continueCursor: string;
509}
510
511const 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
545const noop = () => {};
546
547const isRpcReadEnabled = (options?: RpcReadOptions): boolean =>
548 options?.enabled ?? true;
549
550const makeDisabledResultAtom = <Success, Error>() =>
551 Atom.make(Result.initial<Success, Error>());
552
553export 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
783type ServerQueryEndpoint<Payload, Success> = {
784 readonly queryPromise: (payload: Payload) => Promise<Success>;
785};
786
787type 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
795export 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
806export 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 */
842const 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 */
849const SERVER_QUERY_CACHE_CAPACITY = 256;
850
851export 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 */
1086export 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 @@
1import path from "node:path";
2import { fileURLToPath } from "node:url";
3import ts from "typescript";
4import { describe, expect, it } from "vitest";
5
6const __dirname = path.dirname(fileURLToPath(import.meta.url));
7const packageRoot = path.resolve(__dirname, "../..");
8
9const confectConfigPath = path.join(packageRoot, "tsconfig.json");
10const configFile = ts.readConfigFile(confectConfigPath, ts.sys.readFile);
11const parsedConfig = ts.parseJsonConfigFileContent(
12 configFile.config,
13 ts.sys,
14 path.dirname(confectConfigPath),
15);
16
17const testFile = path.join(packageRoot, "src/rpc/client.test.ts");
18
19const program = ts.createProgram({
20 rootNames: [testFile],
21 options: parsedConfig.options,
22});
23
24const checker = program.getTypeChecker();
25const sf = program.getSourceFile(testFile);
26
27function 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
41function 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
59describe("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 @@
1export {
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
35export {
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 @@
1import { describe, expect, it, vi } from "vitest";
2import { Effect, Schema, Context, Layer } from "effect";
3import { Rpc, RpcMiddleware } from "@effect/rpc";
4import {
5 createRpcFactory,
6 makeRpcModule,
7 exitSchema,
8 fork,
9 uninterruptible,
10 isWrapper,
11 wrap,
12 type ExitEncoded,
13} from "./server";
14import { defineSchema, defineTable } from "../schema";
15
16const testSchema = defineSchema({
17 items: defineTable(
18 Schema.Struct({
19 name: Schema.String,
20 value: Schema.Number,
21 }),
22 ),
23});
24
25const 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
47type ConvexHandlerInternal = (ctx: unknown, args: unknown) => Promise<ExitEncoded>;
48
49const getHandler = (handler: unknown): ConvexHandlerInternal => {
50 const h = handler as { _handler: (ctx: unknown, args: unknown) => Promise<ExitEncoded> };
51 return h._handler;
52};
53
54type SuccessExit = { readonly _tag: "Success"; readonly value: unknown };
55type FailureExit = { readonly _tag: "Failure"; readonly cause: unknown };
56
57const 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
64const 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) */
72const 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
80describe("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 @@
1import { Rpc, RpcGroup, RpcMiddleware } from "@effect/rpc";
2import type {
3 DefaultFunctionArgs,
4 RegisteredMutation,
5 RegisteredQuery,
6 RegisteredAction,
7 GenericQueryCtx,
8 GenericMutationCtx,
9 GenericActionCtx,
10 GenericDataModel,
11} from "convex/server";
12import {
13 queryGeneric,
14 mutationGeneric,
15 actionGeneric,
16 internalQueryGeneric,
17 internalMutationGeneric,
18 internalActionGeneric,
19} from "convex/server";
20import { v } from "convex/values";
21import * as Context from "effect/Context";
22import * as Effect from "effect/Effect";
23import * as Exit from "effect/Exit";
24import * as Layer from "effect/Layer";
25import * as Option from "effect/Option";
26import * as Schema from "effect/Schema";
27
28import {
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";
40import type { ConfectSchemaDefinition } from "../schema";
41import { extractParentSpanFromPayload } from "./telemetry";
42
43export { Rpc, RpcGroup, RpcMiddleware };
44
45export {
46 WrapperTypeId,
47 type WrapperTypeId as WrapperTypeIdType,
48 type Wrapper,
49 isWrapper,
50 wrap,
51 fork,
52 uninterruptible,
53} from "@effect/rpc/Rpc";
54
55export type Handler<Tag extends string> = Rpc.Handler<Tag>;
56export type ToHandler<R extends Rpc.Any> = Rpc.ToHandler<R>;
57export type HandlersFrom<R extends Rpc.Any> = RpcGroup.HandlersFrom<R>;
58export type HandlersContext<R extends Rpc.Any, Handlers> = RpcGroup.HandlersContext<R, Handlers>;
59
60type ConvexCtx = GenericQueryCtx<GenericDataModel> | GenericMutationCtx<GenericDataModel> | GenericActionCtx<GenericDataModel>;
61
62export interface RpcInfo {
63 readonly tag: string;
64 readonly kind: "query" | "mutation" | "action" | "internalQuery" | "internalMutation" | "internalAction";
65}
66
67export interface MiddlewareOptions {
68 readonly rpc: RpcInfo;
69 readonly payload: unknown;
70 readonly ctx: ConvexCtx;
71}
72
73export interface ConfectMiddlewareFn<Service, E = never> {
74 (options: MiddlewareOptions): Effect.Effect<Service, E>;
75}
76
77export interface MiddlewareImplementation<M extends RpcMiddleware.TagClassAny = RpcMiddleware.TagClassAny> {
78 readonly _tag: "MiddlewareImplementation";
79 readonly middleware: M;
80 readonly impl: ConfectMiddlewareFn<unknown, unknown>;
81}
82
83export 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
94type TableSchemas<Tables extends GenericConfectSchema> = {
95 [TableName in TableNamesInSchema<Tables>]: Schema.Schema<
96 DocumentFromTable<Tables, TableName>,
97 EncodedDocumentFromTable<Tables, TableName>
98 >;
99};
100
101const 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
111export 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
125type HandlerFn = (payload: never) => Effect.Effect<unknown, unknown, unknown>;
126
127interface HandlerRef {
128 current: HandlerFn | null;
129 tag: string | null;
130}
131
132const 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 */
146export 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
181export 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
191export type ExitEncoded<A = unknown, E = unknown, D = unknown> = Schema.ExitEncoded<A, E, D>;
192
193type MiddlewareConfig = {
194 readonly implementations: ReadonlyArray<MiddlewareImplementation>;
195 readonly staticLayer?: Layer.Layer<unknown, unknown, never>;
196};
197
198const StaticLayerReady = Context.GenericTag<true>("@confect/StaticLayerReady");
199
200const 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
231type EndpointKind = "query" | "mutation" | "action" | "internalQuery" | "internalMutation" | "internalAction";
232
233type ConvexRegistrar = (opts: { args: ReturnType<typeof v.any>; handler: (ctx: ConvexCtx, args: unknown) => Promise<ExitEncoded> }) => unknown;
234
235interface 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
243export 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
508export type InferRpc<E> = E extends RpcEndpoint<infer _Tag, infer R, infer _ConvexFn> ? R : never;
509
510export type InferFn<E> = E extends RpcEndpoint<infer _Tag, infer _R, infer ConvexFn> ? ConvexFn : never;
511
512interface 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
523export type RpcModule<
524 Endpoints extends Record<string, RpcEndpoint<string, Rpc.Any, unknown>>,
525> = RpcModuleBase<Endpoints> & Endpoints;
526
527export 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
536export type InferModuleEndpoints<M extends AnyRpcModule> = M["_def"]["endpoints"];
537
538interface 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
555type 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
565type BuiltEndpoints<T extends Record<string, AnyUnbuiltEndpoint>> = {
566 [K in keyof T & string]: BuiltEndpoint<K, T[K]>;
567};
568
569const isUnbuilt = (value: unknown): value is AnyUnbuiltEndpoint =>
570 typeof value === "object" && value !== null && "__unbuilt" in value && (value as { __unbuilt: unknown }).__unbuilt === true;
571
572type ExtractMiddlewares<T extends Record<string, AnyUnbuiltEndpoint>> = T[keyof T]["middlewares"][number];
573
574type ExtractMiddlewareProvides<T extends RpcMiddleware.TagClassAny> = T extends {
575 readonly provides: Context.Tag<infer Id, infer _S>;
576}
577 ? Id
578 : never;
579
580type ExtractAllMiddlewareProvides<T extends Record<string, AnyUnbuiltEndpoint>> = ExtractMiddlewareProvides<ExtractMiddlewares<T>>;
581
582type StaticMiddlewareLayer<Services> =
583 | Layer.Layer<Services, unknown, never>
584 | Layer.Layer<never, unknown, never>;
585
586export 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
596const 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
635export 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
671export const exitSchema = Rpc.exitSchema;
v2/packages/confect/src/rpc/telemetry.test.ts+112−0
@@ -0,0 +1,112 @@
1import { Effect, Option } from "effect";
2import { describe, expect, it } from "vitest";
3import {
4 convexRequestIdHeader,
5 extractParentSpanFromPayload,
6 makeRpcTelemetryContext,
7 makeRpcTransportHeaders,
8 rpcTelemetryContextField,
9 withOptionalRpcTelemetryContext,
10 withRpcTelemetryContext,
11} from "./telemetry";
12
13describe("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 @@
1import * as Headers from "@effect/platform/Headers";
2import * as HttpTraceContext from "@effect/platform/HttpTraceContext";
3import * as Option from "effect/Option";
4import type * as Tracer from "effect/Tracer";
5
6export const rpcTelemetryContextField = "confectTelemetryContext";
7export const convexRequestIdHeader = "convex-request-id";
8
9export type RpcClientKind = "query" | "mutation" | "action";
10
11export interface RpcTelemetryContext {
12 readonly traceparent: string;
13 readonly b3?: string;
14 readonly tracestate?: string;
15}
16
17const isRecord = (value: unknown): value is Record<string, unknown> =>
18 typeof value === "object" && value !== null;
19
20const toStringValue = (value: unknown): Option.Option<string> =>
21 typeof value === "string" ? Option.some(value) : Option.none();
22
23const getRecordValue = (
24 record: Record<string, unknown>,
25 key: string,
26): Option.Option<string> =>
27 Option.flatMap(Option.fromNullable(record[key]), toStringValue);
28
29export 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
56export 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
76export 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
89export 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
103export 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 @@
1import { describe, expect, it } from "vitest";
2import { Schema } from "effect";
3import { defineTable, defineSchema, type ConfectTableDefinition } from "./schema";
4
5describe("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 @@
1import {
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";
17import type { Validator } from "convex/values";
18import { Record, Schema } from "effect";
19
20import { 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 */
30type 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
37const SystemFieldsSchema = Schema.Struct({
38 _id: Schema.String,
39 _creationTime: Schema.Number,
40});
41
42const createSystemFieldsSchema = <TableName extends string>(tableName: TableName) =>
43 Schema.Struct({
44 _id: Id(tableName),
45 _creationTime: Schema.Number,
46 });
47
48export 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
139class 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
304export const defineTable = <TableSchema extends Schema.Schema.AnyNoContext>(
305 tableSchema: TableSchema,
306): ConfectTableDefinition<TableSchema> => {
307 return new ConfectTableDefinitionImpl(tableSchema);
308};
309
310export type GenericConfectSchema = {
311 [tableName: string]: ConfectTableDefinition<Schema.Schema.AnyNoContext>;
312};
313
314export 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
326export 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
347export type TableNamesInSchema<T extends GenericConfectSchema> = keyof T & string;
348
349export 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
358export 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
367export 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
398export 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 @@
1import type { FunctionReference, FunctionReturnType, SchemaDefinition, GenericSchema } from "convex/server";
2import { Effect, Layer, Stream } from "effect";
3import {
4 ConvexClient,
5 type ConvexClientService,
6 type ConvexRequestMetadata,
7} from "./client";
8
9export 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
36export 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
78export interface ConvexTestFactory {
79 <SchemaDef extends SchemaDefinition<GenericSchema, boolean>>(
80 schema: SchemaDef,
81 modules?: Record<string, () => Promise<unknown>>,
82 ): ConvexTestInstance;
83}
84
85export interface MakeTestLayerOptions<SchemaDef extends SchemaDefinition<GenericSchema, boolean>> {
86 readonly schema: SchemaDef;
87 readonly modules?: Record<string, () => Promise<unknown>>;
88 readonly convexTest: ConvexTestFactory;
89}
90
91export interface ConvexTestLayer extends Layer.Layer<ConvexClient> {
92 readonly testClient: ConvexTestInstance;
93}
94
95export 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
103export interface CreateTestContextOptions<SchemaDef extends SchemaDefinition<GenericSchema, boolean>> {
104 readonly schema: SchemaDef;
105 readonly modules?: Record<string, () => Promise<unknown>>;
106 readonly convexTest: ConvexTestFactory;
107}
108
109export 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
115export 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 @@
1import { describe, expect, it } from "vitest";
2import { Schema } from "effect";
3import {
4 schemaToValidator,
5 schemaToObjectValidator,
6 Id,
7 TopLevelMustBeObjectError,
8 IndexSignaturesNotSupportedError,
9} from "./validators";
10
11const getKind = (validator: unknown): string =>
12 (validator as { kind: string }).kind;
13
14const getValue = (validator: unknown): unknown =>
15 (validator as { value: unknown }).value;
16
17const getTableName = (validator: unknown): string =>
18 (validator as { tableName: string }).tableName;
19
20const getIsOptional = (validator: unknown): string =>
21 (validator as { isOptional: string }).isOptional;
22
23describe("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 @@
1import { v } from "convex/values";
2import type { GenericId, Validator } from "convex/values";
3import { Data, Option, Schema, SchemaAST } from "effect";
4
5const 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
17export class UnsupportedSchemaError extends Data.TaggedError("UnsupportedSchemaError")<{
18 readonly schemaType: string;
19}> {}
20
21export class TopLevelMustBeObjectError extends Data.TaggedError("TopLevelMustBeObjectError")<{}> {}
22
23export class IndexSignaturesNotSupportedError extends Data.TaggedError(
24 "IndexSignaturesNotSupportedError",
25)<{}> {}
26
27type AnyValidator = Validator<unknown, "required" | "optional", string>;
28type RequiredValidator = Validator<unknown, "required", string>;
29
30const 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
90const 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
119const 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
140const 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
173export 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
180type IdSchema<TableName extends string> = Schema.Schema<
181 GenericId<TableName>,
182 string
183>;
184
185export 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
192export 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 @@
1import { defineConfig } from "vitest/config";
2
3export 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 @@
1# mcp-gateway1# mcp-gateway
2 2
3MCP gateway surface scaffold for Executor v2.3Bare-minimum MCP gateway for Executor v2.
4
5Current 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
2 "name": "@executor-v2/mcp-gateway",2 "name": "@executor-v2/mcp-gateway",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
5 "scripts": {8 "scripts": {
6 "typecheck": "bunx tsc --noEmit -p tsconfig.json"9 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10 },
11 "dependencies": {
12 "@modelcontextprotocol/sdk": "^1.26.0",
13 "zod": "^4.3.6"
7 }14 }
8}15}
v2/packages/mcp-gateway/src/index.ts+1−0
@@ -0,0 +1 @@
1export * from "./server";
v2/packages/mcp-gateway/src/server.ts+70−0
@@ -0,0 +1,70 @@
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3import { z } from "zod";
4
5export type GatewayTarget = "local" | "remote";
6
7export type McpGatewayOptions = {
8 target: GatewayTarget;
9 serverName?: string;
10 serverVersion?: string;
11};
12
13const DEFAULT_SERVER_NAME = "executor-v2";
14const DEFAULT_SERVER_VERSION = "0.0.0";
15const STUB_TOOL_NAME = "executor.ping";
16
17const PingToolInput = z.object({
18 message: z.string().optional(),
19});
20
21const 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
53export 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
3Canonical Effect schema package scaffold for Executor v2.3Canonical Effect schema package scaffold for Executor v2.
4 4
5Current scaffold includes:5Current scaffold includes:
6- branded IDs (`src/ids.ts`)6- branded domain IDs (`src/ids.ts`)
7- shared enums and primitives (`src/enums.ts`, `src/common.ts`)7- shared enums and primitives (`src/enums.ts`, `src/common.ts`)
8- core model schemas in `src/models/*`8- core model schemas in `src/models/*`
9- local snapshot/event schemas (`src/snapshot.ts`)9- local snapshot/event schemas (`src/snapshot.ts`)
v2/packages/source-manager/package.json+8−0
@@ -2,7 +2,15 @@ · 1 unmodified line
2 "name": "@executor-v2/source-manager",2 "name": "@executor-v2/source-manager",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
5 "scripts": {8 "scripts": {
6 "typecheck": "bunx tsc --noEmit -p tsconfig.json"9 "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"
7 }15 }
8}16}
v2/packages/source-manager/tsconfig.json+3−0
@@ -1,4 +1,7 @@
1{1{
2 "extends": "../../tsconfig.json",2 "extends": "../../tsconfig.json",
3 "compilerOptions": {
4 "types": ["node"]
5 },
3 "include": ["src"]6 "include": ["src"]
4}7}
v2/packages/state-contracts/README.md+6−2
@@ -1,3 +1,7 @@
1# state-contracts1# persistence-ports
2 2
3Storage and repository interface scaffold for Executor v2.3Persistence port interface scaffold for Executor v2.
4
5Current 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 @@
1{1{
2 "name": "@executor-v2/state-contracts",2 "name": "@executor-v2/persistence-ports",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
5 "scripts": {8 "scripts": {
6 "typecheck": "bunx tsc --noEmit -p tsconfig.json"9 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10 },
11 "dependencies": {
12 "@executor-v2/schema": "workspace:*",
13 "effect": "^3.19.8"
7 }14 }
8}15}
v2/packages/state-contracts/src/index.ts+2−0
@@ -0,0 +1,2 @@
1export type { ProfileStore } from "./profiles";
2export type { ToolArtifactStore } from "./tool-artifacts";
v2/packages/state-contracts/src/profiles.ts+7−0
@@ -0,0 +1,7 @@
1import type { Profile, ProfileId } from "@executor-v2/schema";
2import type * as Effect from "effect/Effect";
3import type * as Option from "effect/Option";
4
5export 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 @@
1import type { SourceId, ToolArtifact, WorkspaceId } from "@executor-v2/schema";
2import type * as Effect from "effect/Effect";
3import type * as Option from "effect/Option";
4
5export 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-convex1# persistence-convex
2 2
3Convex-backed state adapter scaffold for Executor v2.3Convex-backed persistence adapter scaffold for Executor v2.
4
5Current 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 @@
1{1{
2 "name": "@executor-v2/state-convex",2 "name": "@executor-v2/persistence-convex",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
5 "scripts": {8 "scripts": {
6 "typecheck": "bunx tsc --noEmit -p tsconfig.json"9 "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"
7 }16 }
8}17}
v2/packages/state-convex/src/convex-ids.ts+25−0
@@ -0,0 +1,25 @@
1import { Id } from "@executor-v2/confect";
2
3export const ProfileDocIdSchema = Id("profiles");
4export const WorkspaceDocIdSchema = Id("workspaces");
5export const SourceDocIdSchema = Id("sources");
6export const ToolArtifactDocIdSchema = Id("toolArtifacts");
7export const CredentialRefDocIdSchema = Id("credentialRefs");
8export const OAuthTokenDocIdSchema = Id("oauthTokens");
9export const PolicyDocIdSchema = Id("policies");
10export const ApprovalDocIdSchema = Id("approvals");
11export const TaskRunDocIdSchema = Id("taskRuns");
12export const SyncStateDocIdSchema = Id("syncStates");
13export const EventDocIdSchema = Id("events");
14
15export type ProfileDocId = typeof ProfileDocIdSchema.Type;
16export type WorkspaceDocId = typeof WorkspaceDocIdSchema.Type;
17export type SourceDocId = typeof SourceDocIdSchema.Type;
18export type ToolArtifactDocId = typeof ToolArtifactDocIdSchema.Type;
19export type CredentialRefDocId = typeof CredentialRefDocIdSchema.Type;
20export type OAuthTokenDocId = typeof OAuthTokenDocIdSchema.Type;
21export type PolicyDocId = typeof PolicyDocIdSchema.Type;
22export type ApprovalDocId = typeof ApprovalDocIdSchema.Type;
23export type TaskRunDocId = typeof TaskRunDocIdSchema.Type;
24export type SyncStateDocId = typeof SyncStateDocIdSchema.Type;
25export type EventDocId = typeof EventDocIdSchema.Type;
v2/packages/state-convex/src/index.ts+3−0
@@ -0,0 +1,3 @@
1export * from "./convex-ids";
2export * from "./schema";
3export * from "./profiles";
v2/packages/state-convex/src/profiles.ts+33−0
@@ -0,0 +1,33 @@
1import type {
2 ConfectQueryCtx,
3 DocumentFromTable,
4} from "@executor-v2/confect";
5import { ProfileSchema, type Profile, type ProfileId } from "@executor-v2/schema";
6import type { ProfileStore } from "@executor-v2/persistence-ports";
7import * as Effect from "effect/Effect";
8import { pipe } from "effect/Function";
9import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";
11
12import type { ExecutorConfectTables } from "./schema";
13
14type ProfileDocument = DocumentFromTable<ExecutorConfectTables, "profiles">;
15
16const toProfile = (document: ProfileDocument): Profile => {
17 const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...profile } = document;
18 return Schema.decodeUnknownSync(ProfileSchema)(profile);
19};
20
21export 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 @@
1import { defineSchema, defineTable, type TablesFromSchemaDefinition } from "@executor-v2/confect";
2import {
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
16export 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
56export type ExecutorConfectTables = TablesFromSchemaDefinition<typeof executorConfectSchema>;
v2/packages/state-local-file/README.md+6−2
@@ -1,3 +1,7 @@
1# state-local-file1# persistence-local
2 2
3Local file-backed state adapter scaffold for Executor v2.3Local file-backed persistence adapter scaffold for Executor v2.
4
5Current 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 @@
1{1{
2 "name": "@executor-v2/state-local-file",2 "name": "@executor-v2/persistence-local",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
5 "scripts": {8 "scripts": {
6 "typecheck": "bunx tsc --noEmit -p tsconfig.json"9 "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"
7 }15 }
8}16}
v2/packages/state-local-file/src/index.ts+7−0
@@ -0,0 +1,7 @@
1export {
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 @@
1import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2import { dirname, resolve } from "node:path";
3
4import type { ToolArtifactStore } from "@executor-v2/persistence-ports";
5import {
6 SourceIdSchema,
7 ToolArtifactSchema,
8 WorkspaceIdSchema,
9 type SourceId,
10 type ToolArtifact,
11 type WorkspaceId,
12} from "@executor-v2/schema";
13import * as Effect from "effect/Effect";
14import { pipe } from "effect/Function";
15import * as Option from "effect/Option";
16import * as Schema from "effect/Schema";
17
18export class LocalPersistenceError extends Schema.TaggedError<LocalPersistenceError>()(
19 "LocalPersistenceError",
20 {
21 operation: Schema.String,
22 filePath: Schema.String,
23 message: Schema.String,
24 },
25) {}
26
27const ToolArtifactListSchema = Schema.Array(ToolArtifactSchema);
28
29const decodeArtifacts = Schema.decodeUnknownSync(ToolArtifactListSchema);
30const decodeWorkspaceId = Schema.decodeUnknownSync(WorkspaceIdSchema);
31const decodeSourceId = Schema.decodeUnknownSync(SourceIdSchema);
32
33export type LocalToolArtifactStoreOptions = {
34 rootDir: string;
35};
36
37const defaultArtifactsFilePath = (rootDir: string): string =>
38 resolve(rootDir, "tool-artifacts.json");
39
40const 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
51const 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
70const 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
95export 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
137export const makeWorkspaceId = (value: string): WorkspaceId => decodeWorkspaceId(value);
138export const makeSourceId = (value: string): SourceId => decodeSourceId(value);
v2/packages/state-local-file/tsconfig.json+3−0
@@ -1,4 +1,7 @@
1{1{
2 "extends": "../../tsconfig.json",2 "extends": "../../tsconfig.json",
3 "compilerOptions": {
4 "types": ["node"]
5 },
3 "include": ["src"]6 "include": ["src"]
4}7}
v2/tsconfig.json+7−1
@@ -3,13 +3,19 @@ · 2 unmodified lines
3 "target": "ES2023",3 "target": "ES2023",
4 "module": "ESNext",4 "module": "ESNext",
5 "moduleResolution": "Bundler",5 "moduleResolution": "Bundler",
6 "moduleDetection": "force",
6 "strict": true,7 "strict": true,
7 "noEmit": true,8 "noEmit": true,
8 "verbatimModuleSyntax": true,9 "verbatimModuleSyntax": true,
9 "skipLibCheck": true,10 "skipLibCheck": true,
10 "resolveJsonModule": true,11 "resolveJsonModule": true,
11 "allowSyntheticDefaultImports": true,12 "allowSyntheticDefaultImports": true,
12 "types": []13 "types": [],
14 "plugins": [
15 {
16 "name": "@effect/language-service"
17 }
18 ]
13 },19 },
14 "include": [20 "include": [
15 "apps/**/*.ts",21 "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