@dontcode2/backend 0.1.1 → 0.2.1
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 +94 -0
- package/dist/auth-device.d.ts +43 -0
- package/dist/auth.d.ts +102 -0
- package/dist/chunk-2OGEV57K.js +850 -0
- package/dist/chunk-2OGEV57K.js.map +1 -0
- package/dist/chunk-CAYYXFFZ.js +568 -0
- package/dist/chunk-CAYYXFFZ.js.map +1 -0
- package/dist/chunk-HSPHQ6OU.js +448 -0
- package/dist/chunk-HSPHQ6OU.js.map +1 -0
- package/dist/cli.cjs +1062 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +95 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +36 -0
- package/dist/cookies.d.ts +36 -0
- package/dist/credentials.d.ts +36 -0
- package/dist/db.d.ts +48 -0
- package/dist/errors.d.ts +38 -0
- package/dist/http.d.ts +48 -0
- package/dist/index.cjs +11 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -588
- package/dist/index.js +18 -536
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +972 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mock/cli.cjs +956 -0
- package/dist/mock/cli.cjs.map +1 -0
- package/dist/mock/cli.d.ts +2 -0
- package/dist/mock/cli.js +90 -0
- package/dist/mock/cli.js.map +1 -0
- package/dist/mock/db-query.d.ts +67 -0
- package/dist/mock/index.cjs +886 -0
- package/dist/mock/index.cjs.map +1 -0
- package/dist/mock/index.d.ts +19 -0
- package/dist/mock/index.js +7 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/mock/server.d.ts +36 -0
- package/dist/node.cjs +1016 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.js +28 -0
- package/dist/node.js.map +1 -0
- package/dist/session.d.ts +115 -0
- package/dist/storage.d.ts +46 -0
- package/dist/types.d.ts +160 -0
- package/package.json +32 -2
- package/dist/index.d.cts +0 -588
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/credentials.ts
|
|
27
|
+
var import_node_fs = require("fs");
|
|
28
|
+
var import_node_os = require("os");
|
|
29
|
+
var import_node_path = require("path");
|
|
30
|
+
function configDir() {
|
|
31
|
+
return process.env.DONTCODE_CONFIG_DIR || (0, import_node_path.join)((0, import_node_os.homedir)(), ".dontcode");
|
|
32
|
+
}
|
|
33
|
+
function credentialsPath() {
|
|
34
|
+
return (0, import_node_path.join)(configDir(), "credentials.json");
|
|
35
|
+
}
|
|
36
|
+
function readStore() {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse((0, import_node_fs.readFileSync)(credentialsPath(), "utf8"));
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeStore(store) {
|
|
44
|
+
const path = credentialsPath();
|
|
45
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true, mode: 448 });
|
|
46
|
+
(0, import_node_fs.writeFileSync)(path, JSON.stringify(store, null, 2), { mode: 384 });
|
|
47
|
+
}
|
|
48
|
+
function loadCredential(baseUrl3) {
|
|
49
|
+
return readStore()[baseUrl3] ?? null;
|
|
50
|
+
}
|
|
51
|
+
function saveCredential(cred) {
|
|
52
|
+
const store = readStore();
|
|
53
|
+
store[cred.base_url] = cred;
|
|
54
|
+
writeStore(store);
|
|
55
|
+
}
|
|
56
|
+
function clearCredential(baseUrl3) {
|
|
57
|
+
const store = readStore();
|
|
58
|
+
delete store[baseUrl3];
|
|
59
|
+
writeStore(store);
|
|
60
|
+
}
|
|
61
|
+
function isExpired(cred, skewMs = 3e4) {
|
|
62
|
+
return new Date(cred.expires_at).getTime() - skewMs <= Date.now();
|
|
63
|
+
}
|
|
64
|
+
function resolveActiveToken(baseUrl3) {
|
|
65
|
+
const env = process.env.DONTCODE_API_KEY;
|
|
66
|
+
if (env) return { token: env, source: "env" };
|
|
67
|
+
const cred = loadCredential(baseUrl3);
|
|
68
|
+
if (cred && !isExpired(cred)) {
|
|
69
|
+
return {
|
|
70
|
+
token: cred.access_token,
|
|
71
|
+
source: "device",
|
|
72
|
+
projectId: cred.project_id,
|
|
73
|
+
expiresAt: cred.expires_at
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { source: "none" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/errors.ts
|
|
80
|
+
var DontCodeError = class extends Error {
|
|
81
|
+
constructor(status, body) {
|
|
82
|
+
const message = typeof body?.error === "string" && body.error.length > 0 ? body.error : `DontCode request failed with status ${status}`;
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "DontCodeError";
|
|
85
|
+
this.status = status;
|
|
86
|
+
this.code = typeof body?.code === "string" ? body.code : void 0;
|
|
87
|
+
this.body = body ?? {};
|
|
88
|
+
}
|
|
89
|
+
/** True when the request was rejected by the per-key rate limiter. */
|
|
90
|
+
get rateLimited() {
|
|
91
|
+
return this.status === 429;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function isDontCodeError(err) {
|
|
95
|
+
if (err instanceof DontCodeError) return true;
|
|
96
|
+
return typeof err === "object" && err !== null && err.name === "DontCodeError";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/http.ts
|
|
100
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
101
|
+
var Transport = class {
|
|
102
|
+
constructor(config) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
}
|
|
105
|
+
headers(opts) {
|
|
106
|
+
const headers = {};
|
|
107
|
+
if (this.config.apiKey) headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
108
|
+
if (opts?.accessToken) headers["X-Access-Token"] = opts.accessToken;
|
|
109
|
+
return headers;
|
|
110
|
+
}
|
|
111
|
+
url(path) {
|
|
112
|
+
return `${this.config.baseUrl}${path}`;
|
|
113
|
+
}
|
|
114
|
+
timeout(opts) {
|
|
115
|
+
const value = opts?.timeoutMs ?? this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
116
|
+
return value > 0 ? value : 0;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One fetch, with a timeout that turns "hung socket" into a fast, typed
|
|
120
|
+
* failure. A timeout surfaces as `DontCodeError` with status 408 / code
|
|
121
|
+
* `Timeout`; any other transport failure (DNS, refused, offline) as status
|
|
122
|
+
* 0 / code `NetworkError`. Both are distinct from a real `401`, so an auth
|
|
123
|
+
* guard can tell "backend is down" apart from "user is signed out".
|
|
124
|
+
*/
|
|
125
|
+
async send(path, init, opts) {
|
|
126
|
+
const timeoutMs = this.timeout(opts);
|
|
127
|
+
const controller = timeoutMs > 0 ? new AbortController() : void 0;
|
|
128
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
129
|
+
try {
|
|
130
|
+
return await fetch(this.url(path), { ...init, signal: controller?.signal });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (controller?.signal.aborted) {
|
|
133
|
+
throw new DontCodeError(408, {
|
|
134
|
+
error: `Request to ${path} timed out after ${timeoutMs}ms`,
|
|
135
|
+
code: "Timeout"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
throw new DontCodeError(0, {
|
|
139
|
+
error: err instanceof Error ? err.message : "Network request failed",
|
|
140
|
+
code: "NetworkError"
|
|
141
|
+
});
|
|
142
|
+
} finally {
|
|
143
|
+
if (timer) clearTimeout(timer);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** GET and parse the JSON response. */
|
|
147
|
+
async get(path, opts) {
|
|
148
|
+
const res = await this.send(path, { method: "GET", headers: this.headers(opts) }, opts);
|
|
149
|
+
return this.parse(res);
|
|
150
|
+
}
|
|
151
|
+
/** POST a JSON body and parse the JSON response. */
|
|
152
|
+
async json(path, body, opts) {
|
|
153
|
+
const res = await this.send(
|
|
154
|
+
path,
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { ...this.headers(opts), "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify(body ?? {})
|
|
159
|
+
},
|
|
160
|
+
opts
|
|
161
|
+
);
|
|
162
|
+
return this.parse(res);
|
|
163
|
+
}
|
|
164
|
+
/** PUT a multipart form (file uploads). The runtime sets the boundary. */
|
|
165
|
+
async multipart(path, form, opts) {
|
|
166
|
+
const res = await this.send(path, { method: "PUT", headers: this.headers(opts), body: form }, opts);
|
|
167
|
+
return this.parse(res);
|
|
168
|
+
}
|
|
169
|
+
async parse(res) {
|
|
170
|
+
const raw = await res.text();
|
|
171
|
+
let data = null;
|
|
172
|
+
if (raw) {
|
|
173
|
+
try {
|
|
174
|
+
data = JSON.parse(raw);
|
|
175
|
+
} catch {
|
|
176
|
+
data = { error: raw };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const body = data && typeof data === "object" ? data : { error: res.statusText || "Request failed" };
|
|
181
|
+
throw new DontCodeError(res.status, body);
|
|
182
|
+
}
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/auth-device.ts
|
|
188
|
+
var START_PATH = "/api/v1/auth/device/start";
|
|
189
|
+
var TOKEN_PATH = "/api/v1/auth/device/token";
|
|
190
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
191
|
+
async function startDeviceAuth(baseUrl3, clientName) {
|
|
192
|
+
const transport = new Transport({ baseUrl: baseUrl3 });
|
|
193
|
+
return transport.json(START_PATH, { client_name: clientName });
|
|
194
|
+
}
|
|
195
|
+
async function pollDeviceToken(baseUrl3, start, opts = {}) {
|
|
196
|
+
const transport = new Transport({ baseUrl: baseUrl3 });
|
|
197
|
+
let intervalMs = Math.max(1, start.interval) * 1e3;
|
|
198
|
+
const expiry = Date.now() + start.expires_in * 1e3;
|
|
199
|
+
const deadline = opts.maxWaitMs && opts.maxWaitMs > 0 ? Math.min(expiry, Date.now() + opts.maxWaitMs) : expiry;
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
await sleep(intervalMs);
|
|
202
|
+
try {
|
|
203
|
+
return await transport.json(TOKEN_PATH, {
|
|
204
|
+
device_code: start.device_code
|
|
205
|
+
});
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (err instanceof DontCodeError) {
|
|
208
|
+
const message = err.body?.error ?? err.message;
|
|
209
|
+
if (err.status === 428 || message.includes("authorization_pending")) {
|
|
210
|
+
opts.onPending?.();
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (message.includes("slow_down")) {
|
|
214
|
+
intervalMs += 2e3;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const stillOpen = Date.now() < expiry;
|
|
222
|
+
throw new DontCodeError(408, {
|
|
223
|
+
error: stillOpen ? "Still waiting for browser approval." : "Device login timed out before approval. Start again.",
|
|
224
|
+
code: stillOpen ? "WaitTimeout" : "Timeout"
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async function openBrowser(url) {
|
|
228
|
+
try {
|
|
229
|
+
const { spawn } = await import("child_process");
|
|
230
|
+
const platform = process.platform;
|
|
231
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
232
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
233
|
+
spawn(command, args, { stdio: "ignore", detached: true }).unref();
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function login(options) {
|
|
238
|
+
const log = options.log ?? (() => {
|
|
239
|
+
});
|
|
240
|
+
const start = await startDeviceAuth(options.baseUrl, options.clientName);
|
|
241
|
+
log(
|
|
242
|
+
`
|
|
243
|
+
Open this URL to connect:
|
|
244
|
+
${start.verification_uri_complete}
|
|
245
|
+
|
|
246
|
+
Confirm this code matches:
|
|
247
|
+
${start.user_code}
|
|
248
|
+
|
|
249
|
+
Waiting for approval...
|
|
250
|
+
`
|
|
251
|
+
);
|
|
252
|
+
if (options.open !== false) await openBrowser(start.verification_uri_complete);
|
|
253
|
+
const token = await pollDeviceToken(options.baseUrl, start);
|
|
254
|
+
const cred = {
|
|
255
|
+
access_token: token.access_token,
|
|
256
|
+
project_id: token.project_id,
|
|
257
|
+
expires_at: new Date(Date.now() + token.expires_in * 1e3).toISOString(),
|
|
258
|
+
base_url: options.baseUrl
|
|
259
|
+
};
|
|
260
|
+
saveCredential(cred);
|
|
261
|
+
return cred;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/cookies.ts
|
|
265
|
+
var DEFAULT_SESSION_COOKIE_NAME = "dc_access_token";
|
|
266
|
+
var DEFAULT_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
|
|
267
|
+
function readSessionToken(cookieHeader, name = DEFAULT_SESSION_COOKIE_NAME) {
|
|
268
|
+
if (!cookieHeader) return null;
|
|
269
|
+
for (const pair of cookieHeader.split(";")) {
|
|
270
|
+
const eq = pair.indexOf("=");
|
|
271
|
+
if (eq === -1) continue;
|
|
272
|
+
if (pair.slice(0, eq).trim() !== name) continue;
|
|
273
|
+
const raw = pair.slice(eq + 1).trim();
|
|
274
|
+
if (!raw) return null;
|
|
275
|
+
try {
|
|
276
|
+
return decodeURIComponent(raw);
|
|
277
|
+
} catch {
|
|
278
|
+
return raw;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/session.ts
|
|
285
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
286
|
+
var DEFAULT_VERIFY_TIMEOUT_MS = 5e3;
|
|
287
|
+
var InMemorySessionCache = class {
|
|
288
|
+
constructor() {
|
|
289
|
+
this.store = /* @__PURE__ */ new Map();
|
|
290
|
+
}
|
|
291
|
+
get(token) {
|
|
292
|
+
const hit = this.store.get(token);
|
|
293
|
+
if (!hit) return void 0;
|
|
294
|
+
if (Date.now() >= hit.expiresAtMs) {
|
|
295
|
+
this.store.delete(token);
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
return hit.value;
|
|
299
|
+
}
|
|
300
|
+
set(token, value, ttlMs) {
|
|
301
|
+
this.store.set(token, { value, expiresAtMs: Date.now() + ttlMs });
|
|
302
|
+
}
|
|
303
|
+
delete(token) {
|
|
304
|
+
this.store.delete(token);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function base64UrlDecode(segment) {
|
|
308
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
309
|
+
const padded = base64.length % 4 === 0 ? base64 : base64 + "=".repeat(4 - base64.length % 4);
|
|
310
|
+
if (typeof atob === "function") {
|
|
311
|
+
const binary = atob(padded);
|
|
312
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
313
|
+
return new TextDecoder().decode(bytes);
|
|
314
|
+
}
|
|
315
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
316
|
+
}
|
|
317
|
+
function decodeAccessToken(token) {
|
|
318
|
+
if (!token || typeof token !== "string") return null;
|
|
319
|
+
const parts = token.split(".");
|
|
320
|
+
if (parts.length < 2) return null;
|
|
321
|
+
try {
|
|
322
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
323
|
+
if (!payload || typeof payload.sub !== "string") return null;
|
|
324
|
+
return payload;
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function isSessionExpired(input, opts = {}) {
|
|
330
|
+
const decoded = typeof input === "string" ? decodeAccessToken(input) : input;
|
|
331
|
+
if (!decoded || typeof decoded.exp !== "number") return false;
|
|
332
|
+
const nowSeconds = Date.now() / 1e3;
|
|
333
|
+
return nowSeconds >= decoded.exp - (opts.skewSeconds ?? 0);
|
|
334
|
+
}
|
|
335
|
+
function userFromClaims(decoded) {
|
|
336
|
+
return {
|
|
337
|
+
id: decoded.sub,
|
|
338
|
+
email: typeof decoded.email === "string" ? decoded.email : "",
|
|
339
|
+
role: decoded.role,
|
|
340
|
+
claims: decoded.claims
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
var SessionVerifier = class {
|
|
344
|
+
constructor(auth, options = {}) {
|
|
345
|
+
this.auth = auth;
|
|
346
|
+
this.cache = options.cache ?? new InMemorySessionCache();
|
|
347
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
348
|
+
this.verifyTimeoutMs = options.verifyTimeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS;
|
|
349
|
+
}
|
|
350
|
+
async getSession({ accessToken, mode = "optimistic" }) {
|
|
351
|
+
const decoded = decodeAccessToken(accessToken);
|
|
352
|
+
if (!decoded) return { status: "anonymous", user: null, verified: false };
|
|
353
|
+
if (isSessionExpired(decoded)) {
|
|
354
|
+
return { status: "expired", user: null, verified: false, expiresAt: decoded.exp };
|
|
355
|
+
}
|
|
356
|
+
const optimistic = {
|
|
357
|
+
status: "active",
|
|
358
|
+
user: userFromClaims(decoded),
|
|
359
|
+
verified: false,
|
|
360
|
+
expiresAt: decoded.exp
|
|
361
|
+
};
|
|
362
|
+
if (mode === "optimistic") return optimistic;
|
|
363
|
+
const cached = this.cache.get(accessToken);
|
|
364
|
+
if (cached) return cached;
|
|
365
|
+
try {
|
|
366
|
+
const { user } = await this.auth.me({
|
|
367
|
+
accessToken,
|
|
368
|
+
timeoutMs: this.verifyTimeoutMs
|
|
369
|
+
});
|
|
370
|
+
const result = user ? { status: "active", user, verified: true, expiresAt: decoded.exp } : { status: "anonymous", user: null, verified: true };
|
|
371
|
+
this.cache.set(accessToken, result, this.ttlMs);
|
|
372
|
+
return result;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (isDontCodeError(err) && err.status === 401) {
|
|
375
|
+
return { status: "anonymous", user: null, verified: true };
|
|
376
|
+
}
|
|
377
|
+
return { ...optimistic, status: "unavailable" };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/auth.ts
|
|
383
|
+
var AUTH_BASE = "/api/v1/auth";
|
|
384
|
+
var MfaApi = class {
|
|
385
|
+
constructor(transport) {
|
|
386
|
+
this.transport = transport;
|
|
387
|
+
}
|
|
388
|
+
/** Complete an MFA login. Pass the `challenge_token` from `login`, plus
|
|
389
|
+
* either the authenticator `code` or a `recoveryCode`. */
|
|
390
|
+
challenge(input) {
|
|
391
|
+
return this.transport.json(`${AUTH_BASE}/mfa/challenge`, {
|
|
392
|
+
challenge_token: input.challengeToken,
|
|
393
|
+
code: input.code,
|
|
394
|
+
recovery_code: input.recoveryCode
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/** Begin enrollment. Render the returned `otpauth_url` as a QR code.
|
|
398
|
+
* Enrollment stays pending until `enrollConfirm`. */
|
|
399
|
+
enroll(input) {
|
|
400
|
+
return this.transport.json(
|
|
401
|
+
`${AUTH_BASE}/mfa/enroll`,
|
|
402
|
+
{},
|
|
403
|
+
{ accessToken: input.accessToken }
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
/** Confirm enrollment with the first authenticator code. The returned
|
|
407
|
+
* `recovery_codes` are shown once and never again. */
|
|
408
|
+
enrollConfirm(input) {
|
|
409
|
+
return this.transport.json(
|
|
410
|
+
`${AUTH_BASE}/mfa/enroll/confirm`,
|
|
411
|
+
{ code: input.code },
|
|
412
|
+
{ accessToken: input.accessToken }
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
/** Turn MFA off. Proves possession of the second factor via `code` or
|
|
416
|
+
* `recoveryCode`. */
|
|
417
|
+
disable(input) {
|
|
418
|
+
return this.transport.json(
|
|
419
|
+
`${AUTH_BASE}/mfa/disable`,
|
|
420
|
+
{ code: input.code, recovery_code: input.recoveryCode },
|
|
421
|
+
{ accessToken: input.accessToken }
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var AuthApi = class {
|
|
426
|
+
constructor(transport, sessionOptions) {
|
|
427
|
+
this.transport = transport;
|
|
428
|
+
this.mfa = new MfaApi(transport);
|
|
429
|
+
this.sessions = new SessionVerifier(this, sessionOptions);
|
|
430
|
+
}
|
|
431
|
+
/** Create an account. If the project requires email verification the
|
|
432
|
+
* response has `verification_required: true` and NO tokens; collect a
|
|
433
|
+
* code and call `verifyEmail`, then `login`. */
|
|
434
|
+
signup(input) {
|
|
435
|
+
return this.transport.json(`${AUTH_BASE}/signup`, {
|
|
436
|
+
email: input.email,
|
|
437
|
+
password: input.password,
|
|
438
|
+
name: input.name,
|
|
439
|
+
role: input.role
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
/** Authenticate. Branch on `mfa_required`: when true you hold only a
|
|
443
|
+
* challenge (finish via `mfa.challenge`); otherwise `tokens` is your
|
|
444
|
+
* session. A 403 `EmailNotVerified` means the email step isn't done. */
|
|
445
|
+
login(input) {
|
|
446
|
+
return this.transport.json(`${AUTH_BASE}/login`, {
|
|
447
|
+
email: input.email,
|
|
448
|
+
password: input.password
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/** Validate the current credential (API key or device token) and report the
|
|
452
|
+
* project, the caller's role, and which capabilities that role grants.
|
|
453
|
+
* Backs the MCP "is my session still good" check. */
|
|
454
|
+
info() {
|
|
455
|
+
return this.transport.get("/api/v1/info");
|
|
456
|
+
}
|
|
457
|
+
/** Resolve the signed-in user from their access token, or `{ user: null }`.
|
|
458
|
+
* This is a network round-trip; for a per-navigation guard prefer
|
|
459
|
+
* `getSession`, which can answer offline and caches verified results. */
|
|
460
|
+
me(input) {
|
|
461
|
+
return this.transport.json(
|
|
462
|
+
`${AUTH_BASE}/me`,
|
|
463
|
+
{},
|
|
464
|
+
{ accessToken: input.accessToken, timeoutMs: input.timeoutMs }
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Resolve an access token into a session for a route guard, the one call
|
|
469
|
+
* that replaces "hit `me` on every navigation". Two modes:
|
|
470
|
+
*
|
|
471
|
+
* - `'optimistic'` (default): decode the token locally and trust its
|
|
472
|
+
* claims. Zero network, zero stall. The right default for gating page
|
|
473
|
+
* loads. It does NOT verify the signature and will not notice a
|
|
474
|
+
* server-side revocation until the token's own `exp`.
|
|
475
|
+
* - `'verified'`: confirm against the gateway's `me`, cached for a short
|
|
476
|
+
* TTL with a hard timeout. Use it before sensitive actions. On a
|
|
477
|
+
* timeout/outage it returns `status: 'unavailable'` with the optimistic
|
|
478
|
+
* user, so you choose whether to fail open rather than the SDK guessing.
|
|
479
|
+
*
|
|
480
|
+
* See the BYOC docs ("Sessions") for the full reasoning and best practices.
|
|
481
|
+
*/
|
|
482
|
+
getSession(input) {
|
|
483
|
+
return this.sessions.getSession(input);
|
|
484
|
+
}
|
|
485
|
+
/** Read the access token from a `Cookie` request header and resolve it, in
|
|
486
|
+
* one call. `name` defaults to `dc_access_token`. Returns the anonymous
|
|
487
|
+
* session when no cookie is present. */
|
|
488
|
+
sessionFromCookies(cookieHeader, options = {}) {
|
|
489
|
+
const token = readSessionToken(cookieHeader, options.cookieName);
|
|
490
|
+
if (!token) return Promise.resolve({ status: "anonymous", user: null, verified: false });
|
|
491
|
+
return this.sessions.getSession({ accessToken: token, mode: options.mode });
|
|
492
|
+
}
|
|
493
|
+
/** Decode an access token's claims locally without a network call or any
|
|
494
|
+
* signature check. Convenience re-export of `decodeAccessToken`. */
|
|
495
|
+
decodeToken(token) {
|
|
496
|
+
return decodeAccessToken(token);
|
|
497
|
+
}
|
|
498
|
+
/** Confirm the 6-digit code emailed at signup. */
|
|
499
|
+
verifyEmail(input) {
|
|
500
|
+
return this.transport.json(`${AUTH_BASE}/verify-email`, {
|
|
501
|
+
code: input.code,
|
|
502
|
+
email: input.email
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
forgotPassword(input) {
|
|
506
|
+
return this.transport.json(`${AUTH_BASE}/forgot-password`, {
|
|
507
|
+
email: input.email
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
resetPassword(input) {
|
|
511
|
+
return this.transport.json(`${AUTH_BASE}/reset-password`, {
|
|
512
|
+
code: input.code,
|
|
513
|
+
password: input.password,
|
|
514
|
+
email: input.email
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// src/db.ts
|
|
520
|
+
var DB_PATH = "/api/v1/db";
|
|
521
|
+
var MIGRATE_PATH = "/api/v1/db/migrate";
|
|
522
|
+
var TableQuery = class {
|
|
523
|
+
constructor(transport, tableName) {
|
|
524
|
+
this.transport = transport;
|
|
525
|
+
this.tableName = tableName;
|
|
526
|
+
}
|
|
527
|
+
async run(operation, options) {
|
|
528
|
+
const res = await this.transport.json(DB_PATH, {
|
|
529
|
+
operation,
|
|
530
|
+
tableName: this.tableName,
|
|
531
|
+
options
|
|
532
|
+
});
|
|
533
|
+
return res.data;
|
|
534
|
+
}
|
|
535
|
+
/** Rows matching the query (max 1000 per call). */
|
|
536
|
+
find(options = {}) {
|
|
537
|
+
return this.run("find", options);
|
|
538
|
+
}
|
|
539
|
+
/** Alias of `find`. */
|
|
540
|
+
findMany(options = {}) {
|
|
541
|
+
return this.run("findMany", options);
|
|
542
|
+
}
|
|
543
|
+
/** The first matching row, or `null`. */
|
|
544
|
+
findFirst(options = {}) {
|
|
545
|
+
return this.run("findFirst", options);
|
|
546
|
+
}
|
|
547
|
+
/** Alias of `findFirst`. */
|
|
548
|
+
findOne(options = {}) {
|
|
549
|
+
return this.run("findOne", options);
|
|
550
|
+
}
|
|
551
|
+
/** Insert one row. Returns `{ id }`. Unique/FK conflicts throw a 409
|
|
552
|
+
* DontCodeError, the supported idempotency signal. */
|
|
553
|
+
insert(data) {
|
|
554
|
+
return this.run("insert", { data });
|
|
555
|
+
}
|
|
556
|
+
/** Update rows matching `where`. Returns `{ count }`. */
|
|
557
|
+
update(input) {
|
|
558
|
+
return this.run("update", { where: input.where, data: input.data });
|
|
559
|
+
}
|
|
560
|
+
/** Delete rows matching `where`. Returns `{ count }`. */
|
|
561
|
+
delete(input) {
|
|
562
|
+
return this.run("delete", { where: input.where });
|
|
563
|
+
}
|
|
564
|
+
/** Count matching rows. */
|
|
565
|
+
count(options = {}) {
|
|
566
|
+
return this.run("count", options);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
function createDb(transport) {
|
|
570
|
+
const table = (tableName) => new TableQuery(transport, tableName);
|
|
571
|
+
const migrate = (input) => transport.json(MIGRATE_PATH, { sql: input.sql });
|
|
572
|
+
return new Proxy(table, {
|
|
573
|
+
get(target, prop, receiver) {
|
|
574
|
+
if (prop === "migrate") return migrate;
|
|
575
|
+
if (typeof prop !== "string" || prop === "then" || prop in target) {
|
|
576
|
+
return Reflect.get(target, prop, receiver);
|
|
577
|
+
}
|
|
578
|
+
return new TableQuery(transport, prop);
|
|
579
|
+
},
|
|
580
|
+
apply(_target, _thisArg, args) {
|
|
581
|
+
return table(args[0]);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/storage.ts
|
|
587
|
+
var STORAGE_PATH = "/api/v1/storage";
|
|
588
|
+
var DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
589
|
+
function toBlob(body, contentType) {
|
|
590
|
+
if (body instanceof Blob) return body;
|
|
591
|
+
if (typeof body === "string") return new Blob([body], { type: contentType });
|
|
592
|
+
if (body instanceof ArrayBuffer) return new Blob([body], { type: contentType });
|
|
593
|
+
if (ArrayBuffer.isView(body)) {
|
|
594
|
+
return new Blob([body], { type: contentType });
|
|
595
|
+
}
|
|
596
|
+
throw new TypeError("upload expects a Blob, ArrayBuffer, typed array, or string");
|
|
597
|
+
}
|
|
598
|
+
function fileName(path) {
|
|
599
|
+
return path.split("/").filter(Boolean).pop() ?? path;
|
|
600
|
+
}
|
|
601
|
+
var BucketClient = class {
|
|
602
|
+
constructor(transport, bucket) {
|
|
603
|
+
this.transport = transport;
|
|
604
|
+
this.bucket = bucket;
|
|
605
|
+
}
|
|
606
|
+
op(operation, params = {}) {
|
|
607
|
+
return this.transport.json(STORAGE_PATH, { operation, bucket: this.bucket, ...params });
|
|
608
|
+
}
|
|
609
|
+
/** List objects under `prefix`. */
|
|
610
|
+
list(prefix) {
|
|
611
|
+
return this.op("list", { prefix });
|
|
612
|
+
}
|
|
613
|
+
/** Delete one or more objects. Returns `{ deleted }`. */
|
|
614
|
+
remove(paths) {
|
|
615
|
+
return this.op("remove", { paths });
|
|
616
|
+
}
|
|
617
|
+
/** Move/rename an object within the bucket. */
|
|
618
|
+
move(from, to) {
|
|
619
|
+
return this.op("move", { from, to });
|
|
620
|
+
}
|
|
621
|
+
createFolder(path) {
|
|
622
|
+
return this.op("createFolder", { path });
|
|
623
|
+
}
|
|
624
|
+
/** Download an object inline (≤ 8 MB). Use `getTemporaryUrl` for larger files. */
|
|
625
|
+
download(path) {
|
|
626
|
+
return this.op("download", { path });
|
|
627
|
+
}
|
|
628
|
+
/** A short-lived signed URL (default 300s, max 7 days). */
|
|
629
|
+
getTemporaryUrl(path, expiresIn) {
|
|
630
|
+
return this.op("getTemporaryUrl", { path, expiresIn });
|
|
631
|
+
}
|
|
632
|
+
/** A presigned PUT URL for direct, large uploads (≤ no inline limit). */
|
|
633
|
+
presignUpload(path, contentType) {
|
|
634
|
+
return this.op("presignUpload", { path, contentType });
|
|
635
|
+
}
|
|
636
|
+
/** Upload bytes directly (≤ 100 MB). For larger files, `presignUpload`
|
|
637
|
+
* then PUT to the returned URL yourself. */
|
|
638
|
+
upload(path, body, contentType = DEFAULT_CONTENT_TYPE) {
|
|
639
|
+
const form = new FormData();
|
|
640
|
+
form.append("file", toBlob(body, contentType), fileName(path));
|
|
641
|
+
form.append("bucket", this.bucket);
|
|
642
|
+
form.append("path", path);
|
|
643
|
+
form.append("contentType", contentType);
|
|
644
|
+
return this.transport.multipart(STORAGE_PATH, form);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
var PublicBucketClient = class extends BucketClient {
|
|
648
|
+
constructor(transport) {
|
|
649
|
+
super(transport, "public");
|
|
650
|
+
}
|
|
651
|
+
/** The permanent public URL for an object. */
|
|
652
|
+
getUrl(path) {
|
|
653
|
+
return this.op("getUrl", { path });
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
function createStorage(transport) {
|
|
657
|
+
return {
|
|
658
|
+
public: new PublicBucketClient(transport),
|
|
659
|
+
private: new BucketClient(transport, "private")
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/client.ts
|
|
664
|
+
var DEFAULT_BASE_URL = "https://backend.dontcode.co";
|
|
665
|
+
function fromEnv(name) {
|
|
666
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
667
|
+
return process.env[name];
|
|
668
|
+
}
|
|
669
|
+
function dontcode(options = {}) {
|
|
670
|
+
const apiKey = options.apiKey ?? fromEnv("DONTCODE_API_KEY");
|
|
671
|
+
const baseUrl3 = (options.baseUrl ?? fromEnv("DONTCODE_API_URL") ?? DEFAULT_BASE_URL).replace(
|
|
672
|
+
/\/+$/,
|
|
673
|
+
""
|
|
674
|
+
);
|
|
675
|
+
const transport = new Transport({ apiKey, baseUrl: baseUrl3, timeoutMs: options.timeoutMs });
|
|
676
|
+
return {
|
|
677
|
+
auth: new AuthApi(transport, options.session),
|
|
678
|
+
db: createDb(transport),
|
|
679
|
+
storage: createStorage(transport)
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/mcp/server.ts
|
|
684
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
685
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
686
|
+
var import_zod = require("zod");
|
|
687
|
+
var SERVER_NAME = "dontcode-backend";
|
|
688
|
+
var SERVER_VERSION = "0.3.0";
|
|
689
|
+
function baseUrl() {
|
|
690
|
+
return (process.env.DONTCODE_API_URL || "https://backend.dontcode.co").replace(/\/+$/, "");
|
|
691
|
+
}
|
|
692
|
+
var pendingFlow = null;
|
|
693
|
+
function text(value) {
|
|
694
|
+
const body = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
695
|
+
return { content: [{ type: "text", text: body }] };
|
|
696
|
+
}
|
|
697
|
+
function failure(message) {
|
|
698
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
699
|
+
}
|
|
700
|
+
function describeError(err) {
|
|
701
|
+
if (isDontCodeError(err)) {
|
|
702
|
+
if (err.status === 401) {
|
|
703
|
+
return "Not signed in or the session expired. Use the `auth_login` tool, approve in the browser, then `auth_wait`.";
|
|
704
|
+
}
|
|
705
|
+
if (err.status === 403) {
|
|
706
|
+
return `Your project role does not allow that. (${err.message})`;
|
|
707
|
+
}
|
|
708
|
+
if (err.rateLimited) {
|
|
709
|
+
return `Rate limited. ${err.message}`;
|
|
710
|
+
}
|
|
711
|
+
return err.message;
|
|
712
|
+
}
|
|
713
|
+
return err instanceof Error ? err.message : "Unknown error";
|
|
714
|
+
}
|
|
715
|
+
function requireClient() {
|
|
716
|
+
const active = resolveActiveToken(baseUrl());
|
|
717
|
+
if (!active.token) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
"Not signed in. Use the `auth_login` tool first (or set DONTCODE_API_KEY for non-interactive use)."
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
return dontcode({ apiKey: active.token, baseUrl: baseUrl() });
|
|
723
|
+
}
|
|
724
|
+
async function run(fn) {
|
|
725
|
+
try {
|
|
726
|
+
return text(await fn());
|
|
727
|
+
} catch (err) {
|
|
728
|
+
return failure(describeError(err));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function createMcpServer() {
|
|
732
|
+
const server = new import_mcp.McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
733
|
+
const tool = (name, config, handler) => server.registerTool(name, config, handler);
|
|
734
|
+
tool(
|
|
735
|
+
"auth_login",
|
|
736
|
+
{
|
|
737
|
+
title: "Sign in to DontCode",
|
|
738
|
+
description: "Start a browser sign-in. Returns a URL and a short code; tell the user to open the URL, confirm the code matches, pick a project, and approve. Then call `auth_wait`. Not needed if DONTCODE_API_KEY is set.",
|
|
739
|
+
inputSchema: {
|
|
740
|
+
client_name: import_zod.z.string().optional().describe('Label shown to the user on the approval screen, e.g. "Claude Code".')
|
|
741
|
+
},
|
|
742
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
743
|
+
},
|
|
744
|
+
async ({ client_name }) => run(async () => {
|
|
745
|
+
const start = await startDeviceAuth(baseUrl(), client_name ?? "Claude Code (MCP)");
|
|
746
|
+
pendingFlow = start;
|
|
747
|
+
await openBrowser(start.verification_uri_complete);
|
|
748
|
+
return {
|
|
749
|
+
message: "Ask the user to open this URL, confirm the code, choose a project, and approve. Then call auth_wait.",
|
|
750
|
+
verification_uri: start.verification_uri_complete,
|
|
751
|
+
user_code: start.user_code,
|
|
752
|
+
expires_in_seconds: start.expires_in
|
|
753
|
+
};
|
|
754
|
+
})
|
|
755
|
+
);
|
|
756
|
+
tool(
|
|
757
|
+
"auth_wait",
|
|
758
|
+
{
|
|
759
|
+
title: "Wait for sign-in approval",
|
|
760
|
+
description: "Poll for the result of `auth_login`. Returns connected once the user approves in the browser, or asks you to call it again if still pending. Safe to call repeatedly.",
|
|
761
|
+
inputSchema: {},
|
|
762
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
763
|
+
},
|
|
764
|
+
async () => run(async () => {
|
|
765
|
+
if (!pendingFlow) {
|
|
766
|
+
return { status: "no_login_in_progress", hint: "Call auth_login first." };
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
const token = await pollDeviceToken(baseUrl(), pendingFlow, {
|
|
770
|
+
maxWaitMs: 5e4
|
|
771
|
+
});
|
|
772
|
+
saveCredential({
|
|
773
|
+
access_token: token.access_token,
|
|
774
|
+
project_id: token.project_id,
|
|
775
|
+
expires_at: new Date(Date.now() + token.expires_in * 1e3).toISOString(),
|
|
776
|
+
base_url: baseUrl()
|
|
777
|
+
});
|
|
778
|
+
pendingFlow = null;
|
|
779
|
+
return { status: "connected", project_id: token.project_id };
|
|
780
|
+
} catch (err) {
|
|
781
|
+
if (isDontCodeError(err) && err.code === "WaitTimeout") {
|
|
782
|
+
return {
|
|
783
|
+
status: "pending",
|
|
784
|
+
hint: "Still waiting for approval. Ask the user to approve, then call auth_wait again."
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
pendingFlow = null;
|
|
788
|
+
throw err;
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
);
|
|
792
|
+
tool(
|
|
793
|
+
"auth_status",
|
|
794
|
+
{
|
|
795
|
+
title: "Check the current session",
|
|
796
|
+
description: "Validate the current credential and report the project, your role, and what you are allowed to do.",
|
|
797
|
+
inputSchema: {},
|
|
798
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
799
|
+
},
|
|
800
|
+
async () => run(async () => {
|
|
801
|
+
const active = resolveActiveToken(baseUrl());
|
|
802
|
+
if (!active.token) {
|
|
803
|
+
return { signed_in: false, hint: "Use auth_login to sign in." };
|
|
804
|
+
}
|
|
805
|
+
const client = dontcode({ apiKey: active.token, baseUrl: baseUrl() });
|
|
806
|
+
const info = await client.auth.info();
|
|
807
|
+
return { signed_in: true, source: active.source, ...info };
|
|
808
|
+
})
|
|
809
|
+
);
|
|
810
|
+
tool(
|
|
811
|
+
"auth_logout",
|
|
812
|
+
{
|
|
813
|
+
title: "Forget the cached session",
|
|
814
|
+
description: "Remove the locally cached device token for this gateway.",
|
|
815
|
+
inputSchema: {},
|
|
816
|
+
annotations: { readOnlyHint: false, destructiveHint: true }
|
|
817
|
+
},
|
|
818
|
+
async () => run(async () => {
|
|
819
|
+
clearCredential(baseUrl());
|
|
820
|
+
pendingFlow = null;
|
|
821
|
+
return { ok: true };
|
|
822
|
+
})
|
|
823
|
+
);
|
|
824
|
+
tool(
|
|
825
|
+
"db_query",
|
|
826
|
+
{
|
|
827
|
+
title: "Query the database",
|
|
828
|
+
description: "Read rows from a table with a structured query (no raw SQL). Supports where/select/orderBy/limit/offset and count.",
|
|
829
|
+
inputSchema: {
|
|
830
|
+
table: import_zod.z.string(),
|
|
831
|
+
operation: import_zod.z.enum(["find", "findMany", "findFirst", "findOne", "count"]).default("find"),
|
|
832
|
+
where: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional(),
|
|
833
|
+
select: import_zod.z.array(import_zod.z.string()).optional(),
|
|
834
|
+
orderBy: import_zod.z.record(import_zod.z.string(), import_zod.z.enum(["asc", "desc"])).optional(),
|
|
835
|
+
limit: import_zod.z.number().int().positive().max(1e3).optional(),
|
|
836
|
+
offset: import_zod.z.number().int().nonnegative().optional()
|
|
837
|
+
},
|
|
838
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
839
|
+
},
|
|
840
|
+
async ({ table, operation, where, select, orderBy, limit, offset }) => run(async () => {
|
|
841
|
+
const t = requireClient().db(table);
|
|
842
|
+
if (operation === "count") return { count: await t.count({ where }) };
|
|
843
|
+
const options = { where, select, orderBy, limit, offset };
|
|
844
|
+
if (operation === "findFirst" || operation === "findOne") {
|
|
845
|
+
return { row: await t.findFirst(options) };
|
|
846
|
+
}
|
|
847
|
+
return { rows: await t.find(options) };
|
|
848
|
+
})
|
|
849
|
+
);
|
|
850
|
+
tool(
|
|
851
|
+
"db_insert",
|
|
852
|
+
{
|
|
853
|
+
title: "Insert a row",
|
|
854
|
+
description: "Insert one row into a table. Returns the new row id.",
|
|
855
|
+
inputSchema: { table: import_zod.z.string(), data: import_zod.z.record(import_zod.z.string(), import_zod.z.any()) },
|
|
856
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
857
|
+
},
|
|
858
|
+
async ({ table, data }) => run(async () => requireClient().db(table).insert(data))
|
|
859
|
+
);
|
|
860
|
+
tool(
|
|
861
|
+
"db_update",
|
|
862
|
+
{
|
|
863
|
+
title: "Update rows",
|
|
864
|
+
description: "Update rows matching a where clause. Returns the number of rows changed.",
|
|
865
|
+
inputSchema: {
|
|
866
|
+
table: import_zod.z.string(),
|
|
867
|
+
where: import_zod.z.record(import_zod.z.string(), import_zod.z.any()),
|
|
868
|
+
data: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
|
|
869
|
+
},
|
|
870
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
871
|
+
},
|
|
872
|
+
async ({ table, where, data }) => run(async () => requireClient().db(table).update({ where, data }))
|
|
873
|
+
);
|
|
874
|
+
tool(
|
|
875
|
+
"db_delete",
|
|
876
|
+
{
|
|
877
|
+
title: "Delete rows",
|
|
878
|
+
description: "Delete rows matching a where clause. Destructive: confirm with the user before calling. Returns the number of rows deleted.",
|
|
879
|
+
inputSchema: { table: import_zod.z.string(), where: import_zod.z.record(import_zod.z.string(), import_zod.z.any()) },
|
|
880
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
881
|
+
},
|
|
882
|
+
async ({ table, where }) => run(async () => requireClient().db(table).delete({ where }))
|
|
883
|
+
);
|
|
884
|
+
tool(
|
|
885
|
+
"db_migrate",
|
|
886
|
+
{
|
|
887
|
+
title: "Run a schema migration",
|
|
888
|
+
description: "Apply DDL (CREATE/ALTER/DROP TABLE, indexes, etc.) to the project database. Destructive and schema-shaping: confirm with the user, and note it needs an admin/owner role on device-token sessions.",
|
|
889
|
+
inputSchema: { sql: import_zod.z.string() },
|
|
890
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
891
|
+
},
|
|
892
|
+
async ({ sql }) => run(async () => requireClient().db.migrate({ sql }))
|
|
893
|
+
);
|
|
894
|
+
const bucketArg = import_zod.z.enum(["public", "private"]).default("private");
|
|
895
|
+
const bucketOf = (client, bucket) => bucket === "public" ? client.storage.public : client.storage.private;
|
|
896
|
+
tool(
|
|
897
|
+
"storage_list",
|
|
898
|
+
{
|
|
899
|
+
title: "List files",
|
|
900
|
+
description: "List objects in a storage bucket, optionally under a prefix.",
|
|
901
|
+
inputSchema: { bucket: bucketArg, prefix: import_zod.z.string().optional() },
|
|
902
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
903
|
+
},
|
|
904
|
+
async ({ bucket, prefix }) => run(async () => bucketOf(requireClient(), bucket).list(prefix))
|
|
905
|
+
);
|
|
906
|
+
tool(
|
|
907
|
+
"storage_get_url",
|
|
908
|
+
{
|
|
909
|
+
title: "Get a public URL",
|
|
910
|
+
description: "Get the permanent public URL for an object in the public bucket.",
|
|
911
|
+
inputSchema: { path: import_zod.z.string() },
|
|
912
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
913
|
+
},
|
|
914
|
+
async ({ path }) => run(async () => requireClient().storage.public.getUrl(path))
|
|
915
|
+
);
|
|
916
|
+
tool(
|
|
917
|
+
"storage_temporary_url",
|
|
918
|
+
{
|
|
919
|
+
title: "Get a temporary URL",
|
|
920
|
+
description: "Get a short-lived signed URL for an object (default 300s, max 7 days).",
|
|
921
|
+
inputSchema: {
|
|
922
|
+
bucket: bucketArg,
|
|
923
|
+
path: import_zod.z.string(),
|
|
924
|
+
expires_in: import_zod.z.number().int().positive().optional()
|
|
925
|
+
},
|
|
926
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
927
|
+
},
|
|
928
|
+
async ({ bucket, path, expires_in }) => run(async () => bucketOf(requireClient(), bucket).getTemporaryUrl(path, expires_in))
|
|
929
|
+
);
|
|
930
|
+
tool(
|
|
931
|
+
"storage_upload",
|
|
932
|
+
{
|
|
933
|
+
title: "Upload a text file",
|
|
934
|
+
description: "Upload UTF-8 text content to a path. For binary or large files, use storage_temporary_url + a direct PUT instead.",
|
|
935
|
+
inputSchema: {
|
|
936
|
+
bucket: bucketArg,
|
|
937
|
+
path: import_zod.z.string(),
|
|
938
|
+
content: import_zod.z.string(),
|
|
939
|
+
content_type: import_zod.z.string().optional()
|
|
940
|
+
},
|
|
941
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
942
|
+
},
|
|
943
|
+
async ({ bucket, path, content, content_type }) => run(
|
|
944
|
+
async () => bucketOf(requireClient(), bucket).upload(
|
|
945
|
+
path,
|
|
946
|
+
content,
|
|
947
|
+
content_type ?? "text/plain"
|
|
948
|
+
)
|
|
949
|
+
)
|
|
950
|
+
);
|
|
951
|
+
tool(
|
|
952
|
+
"storage_remove",
|
|
953
|
+
{
|
|
954
|
+
title: "Delete files",
|
|
955
|
+
description: "Delete one or more objects. Destructive: confirm with the user.",
|
|
956
|
+
inputSchema: { bucket: bucketArg, paths: import_zod.z.array(import_zod.z.string()).min(1) },
|
|
957
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
958
|
+
},
|
|
959
|
+
async ({ bucket, paths }) => run(async () => bucketOf(requireClient(), bucket).remove(paths))
|
|
960
|
+
);
|
|
961
|
+
tool(
|
|
962
|
+
"storage_move",
|
|
963
|
+
{
|
|
964
|
+
title: "Move or rename a file",
|
|
965
|
+
description: "Move/rename an object within a bucket.",
|
|
966
|
+
inputSchema: { bucket: bucketArg, from: import_zod.z.string(), to: import_zod.z.string() },
|
|
967
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
968
|
+
},
|
|
969
|
+
async ({ bucket, from, to }) => run(async () => bucketOf(requireClient(), bucket).move(from, to))
|
|
970
|
+
);
|
|
971
|
+
return server;
|
|
972
|
+
}
|
|
973
|
+
async function startMcpServer() {
|
|
974
|
+
const server = createMcpServer();
|
|
975
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
976
|
+
await server.connect(transport);
|
|
977
|
+
console.error(`[${SERVER_NAME}] MCP server ready on stdio (gateway: ${baseUrl()})`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/cli.ts
|
|
981
|
+
function baseUrl2() {
|
|
982
|
+
return (process.env.DONTCODE_API_URL || "https://backend.dontcode.co").replace(/\/+$/, "");
|
|
983
|
+
}
|
|
984
|
+
var HELP = `dontcode \u2014 developer CLI for DontCode Backend
|
|
985
|
+
|
|
986
|
+
Usage: dontcode <command>
|
|
987
|
+
|
|
988
|
+
Commands:
|
|
989
|
+
mcp Run the MCP server on stdio (configure this in your AI tool)
|
|
990
|
+
login Sign in through the browser and cache a short-lived token
|
|
991
|
+
logout Remove the cached token for the current gateway
|
|
992
|
+
status Show the current session (project, role, capabilities)
|
|
993
|
+
help Show this help
|
|
994
|
+
|
|
995
|
+
Environment:
|
|
996
|
+
DONTCODE_API_URL Gateway origin (default https://backend.dontcode.co)
|
|
997
|
+
DONTCODE_API_KEY A dc_ project key for non-interactive use (skips login)
|
|
998
|
+
DONTCODE_CONFIG_DIR Where the cached token lives (default ~/.dontcode)
|
|
999
|
+
`;
|
|
1000
|
+
async function cmdLogin() {
|
|
1001
|
+
const cred = await login({
|
|
1002
|
+
baseUrl: baseUrl2(),
|
|
1003
|
+
clientName: "dontcode CLI",
|
|
1004
|
+
log: (m) => process.stderr.write(m)
|
|
1005
|
+
});
|
|
1006
|
+
process.stderr.write(
|
|
1007
|
+
`
|
|
1008
|
+
Signed in. Project ${cred.project_id}, token valid until ${cred.expires_at}.
|
|
1009
|
+
`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
async function cmdStatus() {
|
|
1013
|
+
const active = resolveActiveToken(baseUrl2());
|
|
1014
|
+
if (!active.token) {
|
|
1015
|
+
process.stdout.write("Not signed in. Run `dontcode login`.\n");
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
const info = await dontcode({ apiKey: active.token, baseUrl: baseUrl2() }).auth.info();
|
|
1020
|
+
process.stdout.write(
|
|
1021
|
+
JSON.stringify({ source: active.source, ...info }, null, 2) + "\n"
|
|
1022
|
+
);
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
const message = isDontCodeError(err) ? err.message : String(err);
|
|
1025
|
+
process.stdout.write(`Session invalid: ${message}
|
|
1026
|
+
`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async function main() {
|
|
1030
|
+
const command = process.argv[2];
|
|
1031
|
+
switch (command) {
|
|
1032
|
+
case "mcp":
|
|
1033
|
+
await startMcpServer();
|
|
1034
|
+
break;
|
|
1035
|
+
case "login":
|
|
1036
|
+
await cmdLogin();
|
|
1037
|
+
break;
|
|
1038
|
+
case "logout":
|
|
1039
|
+
clearCredential(baseUrl2());
|
|
1040
|
+
process.stdout.write("Signed out.\n");
|
|
1041
|
+
break;
|
|
1042
|
+
case "status":
|
|
1043
|
+
await cmdStatus();
|
|
1044
|
+
break;
|
|
1045
|
+
case void 0:
|
|
1046
|
+
case "help":
|
|
1047
|
+
case "-h":
|
|
1048
|
+
case "--help":
|
|
1049
|
+
process.stdout.write(HELP);
|
|
1050
|
+
break;
|
|
1051
|
+
default:
|
|
1052
|
+
process.stderr.write(`Unknown command: ${command}
|
|
1053
|
+
|
|
1054
|
+
${HELP}`);
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
main().catch((err) => {
|
|
1059
|
+
process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
});
|
|
1062
|
+
//# sourceMappingURL=cli.cjs.map
|