convex-devtools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # Convex DevTools
2
+
3
+ <p align="center">
4
+ <img src="https://img.shields.io/npm/v/convex-devtools" alt="npm version">
5
+ <img src="https://img.shields.io/npm/l/convex-devtools" alt="license">
6
+ <img src="https://img.shields.io/npm/dt/convex-devtools" alt="downloads">
7
+ </p>
8
+
9
+ A standalone development tool for testing Convex queries, mutations, and actions with identity mocking, request saving, and auto-reloading schema discovery.
10
+
11
+ > ⚠️ **WARNING**: This tool is intended for **local development only**. It requires admin access to your Convex deployment.
12
+
13
+ ## Features
14
+
15
+ - 🔍 **Function Explorer** - Browse all your Convex queries, mutations, and actions in a tree view
16
+ - 🎭 **Identity Mocking** - Test functions as different users with custom roles and claims
17
+ - 💾 **Request Collections** - Save and organize requests like Postman
18
+ - 📜 **History** - View and replay previous function calls
19
+ - 🔄 **Auto-reload** - Schema updates automatically when your Convex files change
20
+ - 📤 **Import/Export** - Share collections with your team
21
+
22
+ ## Quick Start
23
+
24
+ ### Installation
25
+
26
+ ```bash
27
+ # Install globally
28
+ npm install -g convex-devtools
29
+
30
+ # Or with pnpm
31
+ pnpm add -g convex-devtools
32
+
33
+ # Or with yarn
34
+ yarn global add convex-devtools
35
+
36
+ # Or run directly with npx (no installation required)
37
+ npx convex-devtools
38
+ ```
39
+
40
+ ### Setup
41
+
42
+ 1. Navigate to your Convex project directory
43
+
44
+ 2. Add the following to your `.env.local` file:
45
+
46
+ ```env
47
+ CONVEX_DEVTOOLS_ENABLED=true
48
+ CONVEX_URL=https://your-deployment.convex.cloud
49
+ CONVEX_DEPLOY_KEY=prod:your-deploy-key-here
50
+ ```
51
+
52
+ 3. Get your deploy key from the [Convex Dashboard](https://dashboard.convex.dev) under **Settings → Deploy Keys**.
53
+
54
+ ### Running
55
+
56
+ ```bash
57
+ # Run in your Convex project directory
58
+ convex-devtools
59
+
60
+ # Or specify a different directory
61
+ convex-devtools --dir /path/to/your/project
62
+
63
+ # Run on a custom port
64
+ convex-devtools --port 3000
65
+
66
+ # Don't auto-open browser
67
+ convex-devtools --no-open
68
+ ```
69
+
70
+ The tool will automatically open in your browser at `http://localhost:5173`.
71
+
72
+ ## CLI Options
73
+
74
+ | Option | Description | Default |
75
+ | --------------------- | -------------------------------- | ------- |
76
+ | `-p, --port <number>` | Port for the devtools server | `5173` |
77
+ | `-d, --dir <path>` | Path to Convex project directory | `.` |
78
+ | `--no-open` | Don't open browser automatically | - |
79
+ | `-V, --version` | Display version number | - |
80
+ | `-h, --help` | Display help information | - |
81
+
82
+ ## Identity Mocking
83
+
84
+ The Identity Builder allows you to test functions as different users. You can set:
85
+
86
+ - **Subject** - The user's unique identifier (e.g., Clerk user ID)
87
+ - **Name/Email** - Display information
88
+ - **Roles** - Array of role strings (e.g., `["super_admin", "shop_admin"]`)
89
+ - **User Local ID** - Your app's internal user document ID
90
+ - **Custom Claims** - Any additional JWT claims your app uses
91
+
92
+ ### Example Identity
93
+
94
+ ```json
95
+ {
96
+ "subject": "clerk_user_123",
97
+ "name": "Test Admin",
98
+ "email": "admin@example.com",
99
+ "roles": ["super_admin"],
100
+ "user_local_id": "k975abc123def456"
101
+ }
102
+ ```
103
+
104
+ ## Collections
105
+
106
+ Save requests to collections for easy access:
107
+
108
+ 1. Select a function and configure its arguments
109
+ 2. Click the save icon in the request panel
110
+ 3. Choose or create a collection
111
+ 4. Give the request a descriptive name
112
+
113
+ ### Export/Import
114
+
115
+ - Click the export icon to download your collections as JSON
116
+ - Click the import icon to load collections from a JSON file
117
+ - Share collections with your team via version control
118
+
119
+ ### Export Format
120
+
121
+ The export format is a simple JSON structure:
122
+
123
+ ```json
124
+ {
125
+ "version": "1.0",
126
+ "exportedAt": "2025-01-29T10:00:00.000Z",
127
+ "collections": [
128
+ {
129
+ "id": "abc123",
130
+ "name": "Product Tests",
131
+ "requests": [
132
+ {
133
+ "id": "def456",
134
+ "name": "List all products",
135
+ "functionPath": "products/products:list",
136
+ "functionType": "query",
137
+ "args": "{}",
138
+ "identity": null,
139
+ "savedAt": "2025-01-29T10:00:00.000Z"
140
+ }
141
+ ],
142
+ "createdAt": "2025-01-29T10:00:00.000Z"
143
+ }
144
+ ]
145
+ }
146
+ ```
147
+
148
+ ## Programmatic Usage
149
+
150
+ You can also use convex-devtools programmatically in your Node.js projects:
151
+
152
+ ```typescript
153
+ import { createServer, SchemaWatcher, ConvexClient } from 'convex-devtools';
154
+
155
+ // Start a schema watcher
156
+ const schemaWatcher = new SchemaWatcher('/path/to/project');
157
+ await schemaWatcher.start();
158
+
159
+ // Create and start the server
160
+ const server = await createServer({
161
+ port: 5173,
162
+ projectDir: '/path/to/project',
163
+ convexUrl: 'https://your-deployment.convex.cloud',
164
+ deployKey: 'prod:your-deploy-key',
165
+ schemaWatcher,
166
+ });
167
+
168
+ // Clean up when done
169
+ schemaWatcher.stop();
170
+ server.close();
171
+ ```
172
+
173
+ ## Development
174
+
175
+ ### Prerequisites
176
+
177
+ - Node.js 18+
178
+ - pnpm (recommended) or npm
179
+
180
+ ### Setup
181
+
182
+ ```bash
183
+ # Clone the repository
184
+ git clone https://github.com/your-username/convex-devtools.git
185
+ cd convex-devtools
186
+
187
+ # Install dependencies
188
+ pnpm install
189
+
190
+ # Run in development mode
191
+ pnpm dev
192
+
193
+ # Build for production
194
+ pnpm build:all
195
+ ```
196
+
197
+ ### Project Structure
198
+
199
+ ```
200
+ convex-devtools/
201
+ ├── src/
202
+ │ ├── cli/ # CLI entry point
203
+ │ ├── components/ # React components for the UI
204
+ │ ├── server/ # Express server & Convex client
205
+ │ └── stores/ # Zustand state management
206
+ ├── public/ # Static assets
207
+ ├── dist/ # Built output (generated)
208
+ └── package.json
209
+ ```
210
+
211
+ ### Scripts
212
+
213
+ | Script | Description |
214
+ | ---------------- | -------------------------------- |
215
+ | `pnpm dev` | Start development server |
216
+ | `pnpm build` | Build the frontend |
217
+ | `pnpm build:cli` | Build the CLI |
218
+ | `pnpm build:all` | Build everything for production |
219
+ | `pnpm typecheck` | Run TypeScript type checking |
220
+ | `pnpm lint` | Run ESLint |
221
+ | `pnpm preview` | Preview production build locally |
222
+
223
+ ## Publishing to npm
224
+
225
+ ### First-time Setup
226
+
227
+ 1. Create an npm account at [npmjs.com](https://www.npmjs.com/signup)
228
+
229
+ 2. Login to npm from your terminal:
230
+
231
+ ```bash
232
+ npm login
233
+ ```
234
+
235
+ 3. Verify your login:
236
+ ```bash
237
+ npm whoami
238
+ ```
239
+
240
+ ### Building and Publishing
241
+
242
+ ```bash
243
+ # 1. Make sure all tests pass and there are no errors
244
+ pnpm typecheck
245
+ pnpm lint
246
+
247
+ # 2. Build the project
248
+ pnpm build:all
249
+
250
+ # 3. Update the version (choose one)
251
+ npm version patch # for bug fixes (0.1.0 -> 0.1.1)
252
+ npm version minor # for new features (0.1.0 -> 0.2.0)
253
+ npm version major # for breaking changes (0.1.0 -> 1.0.0)
254
+
255
+ # 4. Publish to npm
256
+ npm publish
257
+
258
+ # Or for a scoped package (if using @your-org/convex-devtools)
259
+ npm publish --access public
260
+ ```
261
+
262
+ ### Testing Before Publishing
263
+
264
+ ```bash
265
+ # Create a tarball to see what will be published
266
+ npm pack
267
+
268
+ # This creates convex-devtools-x.x.x.tgz
269
+ # You can inspect it or install it locally:
270
+ npm install ./convex-devtools-0.1.0.tgz -g
271
+
272
+ # Test the CLI
273
+ convex-devtools --help
274
+ ```
275
+
276
+ ### Publishing a Pre-release
277
+
278
+ ```bash
279
+ # For beta versions
280
+ npm version prerelease --preid=beta
281
+ npm publish --tag beta
282
+
283
+ # Users can install with: npm install convex-devtools@beta
284
+ ```
285
+
286
+ ## Security Considerations
287
+
288
+ - **Never use in production** - This tool has full admin access to your Convex deployment
289
+ - **Keep your deploy key secret** - Don't commit it to version control
290
+ - **Local development only** - The `CONVEX_DEVTOOLS_ENABLED` flag should never be set in production
291
+ - **Use dev deploy keys when possible** - Prefer `dev:xxx` keys over `prod:xxx` keys during development
292
+
293
+ ## Troubleshooting
294
+
295
+ ### "CONVEX_DEVTOOLS_ENABLED is not set to true"
296
+
297
+ Add `CONVEX_DEVTOOLS_ENABLED=true` to your `.env.local` file. This safety check ensures you don't accidentally run the devtools against production.
298
+
299
+ ### "Convex generated files not found"
300
+
301
+ Run `npx convex dev` in your project directory first to generate the required files.
302
+
303
+ ### "Identity mocking is disabled"
304
+
305
+ You need to set `CONVEX_DEPLOY_KEY` in your `.env.local` file. Get your deploy key from the Convex Dashboard under Settings → Deploy Keys.
306
+
307
+ ### Port already in use
308
+
309
+ Use the `--port` flag to specify a different port:
310
+
311
+ ```bash
312
+ convex-devtools --port 3001
313
+ ```
314
+
315
+ ## Contributing
316
+
317
+ Contributions are welcome! Please feel free to submit a Pull Request.
318
+
319
+ 1. Fork the repository
320
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
321
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
322
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
323
+ 5. Open a Pull Request
324
+
325
+ ## License
326
+
327
+ MIT © [Šahzudin Mahmić]
328
+
329
+ ## Related Projects
330
+
331
+ - [Convex](https://convex.dev) - The backend platform this tool is designed for
332
+ - [Convex Dashboard](https://dashboard.convex.dev) - Official Convex admin dashboard
@@ -0,0 +1,102 @@
1
+ import {
2
+ ConvexClient
3
+ } from "./chunk-A7PTPOQI.js";
4
+
5
+ // src/server/index.ts
6
+ import cors from "cors";
7
+ import express from "express";
8
+ import http from "http";
9
+ import path from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { WebSocket, WebSocketServer } from "ws";
12
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ async function createServer(config) {
14
+ const app = express();
15
+ app.use(cors());
16
+ app.use(express.json({ limit: "10mb" }));
17
+ const uiPath = path.join(__dirname, "..", "ui");
18
+ app.use(express.static(uiPath));
19
+ const convexClient = new ConvexClient(config.convexUrl, config.deployKey);
20
+ app.get("/api/schema", (_req, res) => {
21
+ const schema = config.schemaWatcher.getSchema();
22
+ if (!schema) {
23
+ res.status(503).json({ error: "Schema not yet loaded" });
24
+ return;
25
+ }
26
+ res.json(schema);
27
+ });
28
+ app.get("/api/health", (_req, res) => {
29
+ res.json({
30
+ status: "ok",
31
+ convexUrl: config.convexUrl,
32
+ projectDir: config.projectDir
33
+ });
34
+ });
35
+ app.post("/api/invoke", async (req, res) => {
36
+ const { functionPath, functionType, args, jwtToken } = req.body;
37
+ if (!functionPath || !functionType) {
38
+ res.status(400).json({ error: "Missing functionPath or functionType" });
39
+ return;
40
+ }
41
+ try {
42
+ const startTime = Date.now();
43
+ const result = await convexClient.invoke(
44
+ functionPath,
45
+ functionType,
46
+ args || {},
47
+ { jwtToken }
48
+ );
49
+ const duration = Date.now() - startTime;
50
+ res.json({
51
+ success: true,
52
+ result,
53
+ duration,
54
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
55
+ });
56
+ } catch (error) {
57
+ res.json({
58
+ success: false,
59
+ error: {
60
+ message: error.message,
61
+ code: error.code,
62
+ data: error.data
63
+ },
64
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
65
+ });
66
+ }
67
+ });
68
+ const server = http.createServer(app);
69
+ const wss = new WebSocketServer({ server, path: "/ws" });
70
+ const clients = /* @__PURE__ */ new Set();
71
+ wss.on("connection", (ws) => {
72
+ clients.add(ws);
73
+ const schema = config.schemaWatcher.getSchema();
74
+ if (schema) {
75
+ ws.send(JSON.stringify({ type: "schema", data: schema }));
76
+ }
77
+ ws.on("close", () => {
78
+ clients.delete(ws);
79
+ });
80
+ });
81
+ config.schemaWatcher.on("schema-updated", (schema) => {
82
+ const message = JSON.stringify({ type: "schema", data: schema });
83
+ for (const client of clients) {
84
+ if (client.readyState === WebSocket.OPEN) {
85
+ client.send(message);
86
+ }
87
+ }
88
+ });
89
+ app.get("*", (_req, res) => {
90
+ res.sendFile(path.join(uiPath, "index.html"));
91
+ });
92
+ return new Promise((resolve) => {
93
+ server.listen(config.port, () => {
94
+ resolve(server);
95
+ });
96
+ });
97
+ }
98
+
99
+ export {
100
+ createServer
101
+ };
102
+ //# sourceMappingURL=chunk-5AG6RQBI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["import cors from 'cors';\nimport express from 'express';\nimport http from 'http';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { WebSocket, WebSocketServer } from 'ws';\nimport { ConvexClient } from './convex-client.js';\nimport { SchemaWatcher } from './schema-watcher.js';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport interface ServerConfig {\n port: number;\n projectDir: string;\n convexUrl: string;\n deployKey: string;\n schemaWatcher: SchemaWatcher;\n}\n\nexport async function createServer(config: ServerConfig): Promise<http.Server> {\n const app = express();\n\n app.use(cors());\n app.use(express.json({ limit: '10mb' }));\n\n // Serve static UI files in production\n const uiPath = path.join(__dirname, '..', 'ui');\n app.use(express.static(uiPath));\n\n // Create Convex client\n const convexClient = new ConvexClient(config.convexUrl, config.deployKey);\n\n // API Routes\n app.get('/api/schema', (_req, res) => {\n const schema = config.schemaWatcher.getSchema();\n if (!schema) {\n res.status(503).json({ error: 'Schema not yet loaded' });\n return;\n }\n res.json(schema);\n });\n\n app.get('/api/health', (_req, res) => {\n res.json({\n status: 'ok',\n convexUrl: config.convexUrl,\n projectDir: config.projectDir,\n });\n });\n\n // Invoke a function\n app.post('/api/invoke', async (req, res) => {\n const { functionPath, functionType, args, jwtToken } = req.body;\n\n if (!functionPath || !functionType) {\n res.status(400).json({ error: 'Missing functionPath or functionType' });\n return;\n }\n\n try {\n const startTime = Date.now();\n const result = await convexClient.invoke(\n functionPath,\n functionType,\n args || {},\n { jwtToken }\n );\n const duration = Date.now() - startTime;\n\n res.json({\n success: true,\n result,\n duration,\n timestamp: new Date().toISOString(),\n });\n } catch (error: any) {\n res.json({\n success: false,\n error: {\n message: error.message,\n code: error.code,\n data: error.data,\n },\n timestamp: new Date().toISOString(),\n });\n }\n });\n\n // Create HTTP server\n const server = http.createServer(app);\n\n // WebSocket for real-time schema updates\n const wss = new WebSocketServer({ server, path: '/ws' });\n\n const clients = new Set<WebSocket>();\n\n wss.on('connection', (ws) => {\n clients.add(ws);\n\n // Send initial schema\n const schema = config.schemaWatcher.getSchema();\n if (schema) {\n ws.send(JSON.stringify({ type: 'schema', data: schema }));\n }\n\n ws.on('close', () => {\n clients.delete(ws);\n });\n });\n\n // Broadcast schema updates\n config.schemaWatcher.on('schema-updated', (schema) => {\n const message = JSON.stringify({ type: 'schema', data: schema });\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n }\n });\n\n // SPA fallback - serve index.html for non-API routes\n app.get('*', (_req, res) => {\n res.sendFile(path.join(uiPath, 'index.html'));\n });\n\n return new Promise((resolve) => {\n server.listen(config.port, () => {\n resolve(server);\n });\n });\n}\n"],"mappings":";;;;;AAAA,OAAO,UAAU;AACjB,OAAO,aAAa;AACpB,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,WAAW,uBAAuB;AAI3C,IAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAU7D,eAAsB,aAAa,QAA4C;AAC7E,QAAM,MAAM,QAAQ;AAEpB,MAAI,IAAI,KAAK,CAAC;AACd,MAAI,IAAI,QAAQ,KAAK,EAAE,OAAO,OAAO,CAAC,CAAC;AAGvC,QAAM,SAAS,KAAK,KAAK,WAAW,MAAM,IAAI;AAC9C,MAAI,IAAI,QAAQ,OAAO,MAAM,CAAC;AAG9B,QAAM,eAAe,IAAI,aAAa,OAAO,WAAW,OAAO,SAAS;AAGxE,MAAI,IAAI,eAAe,CAAC,MAAM,QAAQ;AACpC,UAAM,SAAS,OAAO,cAAc,UAAU;AAC9C,QAAI,CAAC,QAAQ;AACX,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,wBAAwB,CAAC;AACvD;AAAA,IACF;AACA,QAAI,KAAK,MAAM;AAAA,EACjB,CAAC;AAED,MAAI,IAAI,eAAe,CAAC,MAAM,QAAQ;AACpC,QAAI,KAAK;AAAA,MACP,QAAQ;AAAA,MACR,WAAW,OAAO;AAAA,MAClB,YAAY,OAAO;AAAA,IACrB,CAAC;AAAA,EACH,CAAC;AAGD,MAAI,KAAK,eAAe,OAAO,KAAK,QAAQ;AAC1C,UAAM,EAAE,cAAc,cAAc,MAAM,SAAS,IAAI,IAAI;AAE3D,QAAI,CAAC,gBAAgB,CAAC,cAAc;AAClC,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,uCAAuC,CAAC;AACtE;AAAA,IACF;AAEA,QAAI;AACF,YAAM,YAAY,KAAK,IAAI;AAC3B,YAAM,SAAS,MAAM,aAAa;AAAA,QAChC;AAAA,QACA;AAAA,QACA,QAAQ,CAAC;AAAA,QACT,EAAE,SAAS;AAAA,MACb;AACA,YAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,UAAI,KAAK;AAAA,QACP,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,CAAC;AAAA,IACH,SAAS,OAAY;AACnB,UAAI,KAAK;AAAA,QACP,SAAS;AAAA,QACT,OAAO;AAAA,UACL,SAAS,MAAM;AAAA,UACf,MAAM,MAAM;AAAA,UACZ,MAAM,MAAM;AAAA,QACd;AAAA,QACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAGD,QAAM,SAAS,KAAK,aAAa,GAAG;AAGpC,QAAM,MAAM,IAAI,gBAAgB,EAAE,QAAQ,MAAM,MAAM,CAAC;AAEvD,QAAM,UAAU,oBAAI,IAAe;AAEnC,MAAI,GAAG,cAAc,CAAC,OAAO;AAC3B,YAAQ,IAAI,EAAE;AAGd,UAAM,SAAS,OAAO,cAAc,UAAU;AAC9C,QAAI,QAAQ;AACV,SAAG,KAAK,KAAK,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,CAAC,CAAC;AAAA,IAC1D;AAEA,OAAG,GAAG,SAAS,MAAM;AACnB,cAAQ,OAAO,EAAE;AAAA,IACnB,CAAC;AAAA,EACH,CAAC;AAGD,SAAO,cAAc,GAAG,kBAAkB,CAAC,WAAW;AACpD,UAAM,UAAU,KAAK,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,CAAC;AAC/D,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,eAAe,UAAU,MAAM;AACxC,eAAO,KAAK,OAAO;AAAA,MACrB;AAAA,IACF;AAAA,EACF,CAAC;AAGD,MAAI,IAAI,KAAK,CAAC,MAAM,QAAQ;AAC1B,QAAI,SAAS,KAAK,KAAK,QAAQ,YAAY,CAAC;AAAA,EAC9C,CAAC;AAED,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,WAAO,OAAO,OAAO,MAAM,MAAM;AAC/B,cAAQ,MAAM;AAAA,IAChB,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
@@ -0,0 +1,137 @@
1
+ // src/server/convex-client.ts
2
+ var ConvexClient = class {
3
+ baseUrl;
4
+ deployKey;
5
+ constructor(convexUrl, deployKey) {
6
+ this.baseUrl = convexUrl;
7
+ this.deployKey = deployKey;
8
+ }
9
+ async invoke(functionPath, functionType, args = {}, options) {
10
+ const normalizedPath = this.normalizeFunctionPath(functionPath);
11
+ const endpoint = this.getEndpoint(functionType);
12
+ const url = `${this.baseUrl}/${endpoint}`;
13
+ const headers = {
14
+ "Content-Type": "application/json",
15
+ "Convex-Client": "convex-devtools-0.1.0"
16
+ };
17
+ if (options?.jwtToken) {
18
+ headers["Authorization"] = `Bearer ${options.jwtToken}`;
19
+ console.log("[ConvexClient] Using JWT token authentication");
20
+ } else if (this.deployKey) {
21
+ headers["Authorization"] = `Convex ${this.deployKey}`;
22
+ console.log("[ConvexClient] Using deploy key (admin) authentication");
23
+ }
24
+ const body = {
25
+ path: normalizedPath,
26
+ args: this.encodeArgs(args),
27
+ format: "json"
28
+ };
29
+ const response = await fetch(url, {
30
+ method: "POST",
31
+ headers,
32
+ body: JSON.stringify(body)
33
+ });
34
+ if (!response.ok) {
35
+ const errorText = await response.text();
36
+ let errorData;
37
+ try {
38
+ errorData = JSON.parse(errorText);
39
+ } catch {
40
+ errorData = { message: errorText };
41
+ }
42
+ const error = new Error(errorData.message || `HTTP ${response.status}`);
43
+ error.code = errorData.code;
44
+ error.data = errorData;
45
+ throw error;
46
+ }
47
+ const result = await response.json();
48
+ return this.decodeResult(result);
49
+ }
50
+ normalizeFunctionPath(path) {
51
+ if (path.includes(":")) {
52
+ return path;
53
+ }
54
+ if (path.includes(".")) {
55
+ const parts2 = path.split(".");
56
+ const funcName = parts2.pop();
57
+ return `${parts2.join("/")}:${funcName}`;
58
+ }
59
+ const parts = path.split("/");
60
+ if (parts.length > 1) {
61
+ const funcName = parts.pop();
62
+ return `${parts.join("/")}:${funcName}`;
63
+ }
64
+ return path;
65
+ }
66
+ getEndpoint(functionType) {
67
+ switch (functionType) {
68
+ case "query":
69
+ return "api/query";
70
+ case "mutation":
71
+ return "api/mutation";
72
+ case "action":
73
+ return "api/action";
74
+ }
75
+ }
76
+ encodeArgs(args) {
77
+ return this.convertToConvexJson(args);
78
+ }
79
+ convertToConvexJson(value) {
80
+ if (value === null || value === void 0) {
81
+ return value;
82
+ }
83
+ if (Array.isArray(value)) {
84
+ return value.map((v) => this.convertToConvexJson(v));
85
+ }
86
+ if (typeof value === "object") {
87
+ const obj = value;
88
+ if ("$type" in obj) {
89
+ return obj;
90
+ }
91
+ const converted = {};
92
+ for (const [k, v] of Object.entries(obj)) {
93
+ converted[k] = this.convertToConvexJson(v);
94
+ }
95
+ return converted;
96
+ }
97
+ if (typeof value === "bigint") {
98
+ return { $type: "bigint", value: value.toString() };
99
+ }
100
+ return value;
101
+ }
102
+ decodeResult(result) {
103
+ return this.convertFromConvexJson(result);
104
+ }
105
+ convertFromConvexJson(value) {
106
+ if (value === null || value === void 0) {
107
+ return value;
108
+ }
109
+ if (Array.isArray(value)) {
110
+ return value.map((v) => this.convertFromConvexJson(v));
111
+ }
112
+ if (typeof value === "object") {
113
+ const obj = value;
114
+ if ("$type" in obj) {
115
+ switch (obj.$type) {
116
+ case "bigint":
117
+ return BigInt(obj.value);
118
+ case "bytes":
119
+ return new Uint8Array(obj.value);
120
+ default:
121
+ return obj;
122
+ }
123
+ }
124
+ const result = {};
125
+ for (const [k, v] of Object.entries(obj)) {
126
+ result[k] = this.convertFromConvexJson(v);
127
+ }
128
+ return result;
129
+ }
130
+ return value;
131
+ }
132
+ };
133
+
134
+ export {
135
+ ConvexClient
136
+ };
137
+ //# sourceMappingURL=chunk-A7PTPOQI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/convex-client.ts"],"sourcesContent":["/**\n * Convex HTTP Client wrapper for DevTools\n * Supports two authentication methods:\n * 1. Deploy key - runs as admin (for admin operations)\n * 2. JWT token - runs as the authenticated user (from your auth provider like Clerk)\n */\n\nexport interface UserIdentity {\n subject: string;\n issuer?: string;\n tokenIdentifier?: string;\n name?: string;\n email?: string;\n pictureUrl?: string;\n // Custom claims\n [key: string]: unknown;\n}\n\nexport interface InvokeOptions {\n identity?: UserIdentity;\n jwtToken?: string; // Real JWT token from auth provider\n}\n\nexport class ConvexClient {\n private baseUrl: string;\n private deployKey: string;\n\n constructor(convexUrl: string, deployKey: string) {\n // Convert deployment URL to HTTP endpoint\n // e.g., https://happy-otter-123.convex.cloud -> https://happy-otter-123.convex.cloud\n this.baseUrl = convexUrl;\n this.deployKey = deployKey;\n }\n\n async invoke(\n functionPath: string,\n functionType: 'query' | 'mutation' | 'action',\n args: Record<string, unknown> = {},\n options?: InvokeOptions\n ): Promise<unknown> {\n // Normalize function path: module/submodule:functionName\n const normalizedPath = this.normalizeFunctionPath(functionPath);\n\n // Build the request\n const endpoint = this.getEndpoint(functionType);\n const url = `${this.baseUrl}/${endpoint}`;\n\n // Build headers\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Convex-Client': 'convex-devtools-0.1.0',\n };\n\n // Authentication priority:\n // 1. JWT token (authenticates as the user who owns the token)\n // 2. Deploy key (authenticates as admin)\n // 3. No auth (unauthenticated request)\n if (options?.jwtToken) {\n // Use Bearer token auth - this is what the Convex HTTP API expects for user auth\n headers['Authorization'] = `Bearer ${options.jwtToken}`;\n console.log('[ConvexClient] Using JWT token authentication');\n } else if (this.deployKey) {\n // Deploy key gives admin access but cannot impersonate users\n headers['Authorization'] = `Convex ${this.deployKey}`;\n console.log('[ConvexClient] Using deploy key (admin) authentication');\n }\n // Without any auth, calls will be unauthenticated\n\n const body = {\n path: normalizedPath,\n args: this.encodeArgs(args),\n format: 'json',\n };\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorData;\n try {\n errorData = JSON.parse(errorText);\n } catch {\n errorData = { message: errorText };\n }\n\n const error = new Error(errorData.message || `HTTP ${response.status}`);\n (error as any).code = errorData.code;\n (error as any).data = errorData;\n throw error;\n }\n\n const result = await response.json();\n return this.decodeResult(result);\n }\n\n private normalizeFunctionPath(path: string): string {\n // Convert various formats to Convex function path format\n // Input: \"products/products:list\" or \"products.products.list\" or \"products/products/list\"\n // Output: \"products/products:list\"\n\n // Already in correct format\n if (path.includes(':')) {\n return path;\n }\n\n // Dot notation: products.products.list -> products/products:list\n if (path.includes('.')) {\n const parts = path.split('.');\n const funcName = parts.pop()!;\n return `${parts.join('/')}:${funcName}`;\n }\n\n // Slash only: products/products/list -> products/products:list\n const parts = path.split('/');\n if (parts.length > 1) {\n const funcName = parts.pop()!;\n return `${parts.join('/')}:${funcName}`;\n }\n\n return path;\n }\n\n private getEndpoint(functionType: 'query' | 'mutation' | 'action'): string {\n switch (functionType) {\n case 'query':\n return 'api/query';\n case 'mutation':\n return 'api/mutation';\n case 'action':\n return 'api/action';\n }\n }\n\n private encodeArgs(args: Record<string, unknown>): Record<string, unknown> {\n // Convex uses a special encoding format for complex types\n // For now, we'll pass through JSON-serializable values\n // Special handling for Convex types like Id, etc.\n return this.convertToConvexJson(args) as Record<string, unknown>;\n }\n\n private convertToConvexJson(value: unknown): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.convertToConvexJson(v));\n }\n\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n\n // Check for special $type markers (Convex encoded JSON)\n if ('$type' in obj) {\n return obj; // Already encoded\n }\n\n // Regular object\n const converted: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n converted[k] = this.convertToConvexJson(v);\n }\n return converted;\n }\n\n // Handle BigInt\n if (typeof value === 'bigint') {\n return { $type: 'bigint', value: value.toString() };\n }\n\n return value;\n }\n\n private decodeResult(result: unknown): unknown {\n // Decode Convex-specific types back to JavaScript\n return this.convertFromConvexJson(result);\n }\n\n private convertFromConvexJson(value: unknown): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.convertFromConvexJson(v));\n }\n\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n\n // Check for special $type markers\n if ('$type' in obj) {\n switch (obj.$type) {\n case 'bigint':\n return BigInt(obj.value as string);\n case 'bytes':\n return new Uint8Array(obj.value as number[]);\n default:\n return obj;\n }\n }\n\n // Regular object\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = this.convertFromConvexJson(v);\n }\n return result;\n }\n\n return value;\n }\n}\n"],"mappings":"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","names":["parts"]}