@spinabot/brigade 1.0.1 → 1.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/convex/_generated/api.d.ts +85 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/admin.d.ts +57 -0
- package/convex/admin.ts +315 -0
- package/convex/auth.d.ts +159 -0
- package/convex/auth.ts +217 -0
- package/convex/blobs.d.ts +38 -0
- package/convex/blobs.ts +115 -0
- package/convex/channels.d.ts +150 -0
- package/convex/channels.ts +455 -0
- package/convex/config.d.ts +67 -0
- package/convex/config.ts +168 -0
- package/convex/cron.d.ts +237 -0
- package/convex/cron.ts +199 -0
- package/convex/execApprovals.d.ts +31 -0
- package/convex/execApprovals.ts +58 -0
- package/convex/extensions.d.ts +30 -0
- package/convex/extensions.ts +51 -0
- package/convex/health.d.ts +18 -0
- package/convex/health.ts +69 -0
- package/convex/instance.d.ts +34 -0
- package/convex/instance.ts +82 -0
- package/convex/logs.d.ts +178 -0
- package/convex/logs.ts +253 -0
- package/convex/memory.d.ts +354 -0
- package/convex/memory.ts +536 -0
- package/convex/messages.d.ts +124 -0
- package/convex/messages.ts +347 -0
- package/convex/org.d.ts +75 -0
- package/convex/org.ts +99 -0
- package/convex/schema.d.ts +1130 -0
- package/convex/schema.ts +847 -0
- package/convex/sessions.d.ts +100 -0
- package/convex/sessions.ts +105 -0
- package/convex/skills.d.ts +73 -0
- package/convex/skills.ts +102 -0
- package/convex/subagents.d.ts +214 -0
- package/convex/subagents.ts +99 -0
- package/convex/tsconfig.json +23 -0
- package/convex/whatsappAuth.d.ts +52 -0
- package/convex/whatsappAuth.ts +151 -0
- package/convex/workspace.d.ts +49 -0
- package/convex/workspace.ts +106 -0
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/convex-cmd.d.ts +27 -0
- package/dist/cli/commands/convex-cmd.d.ts.map +1 -0
- package/dist/cli/commands/convex-cmd.js +162 -0
- package/dist/cli/commands/convex-cmd.js.map +1 -0
- package/dist/cli/program/build-program.d.ts.map +1 -1
- package/dist/cli/program/build-program.js +64 -0
- package/dist/cli/program/build-program.js.map +1 -1
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +39 -0
- package/dist/config/paths.js.map +1 -1
- package/package.json +7 -1
- package/scripts/convex-dev.mjs +321 -0
- package/scripts/convex-push.mjs +69 -0
- package/scripts/install-convex.mjs +123 -0
package/convex/admin.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// convex/admin.ts — instance-level inspect + factory reset.
|
|
2
|
+
//
|
|
3
|
+
// Single-operator deployments hold exactly one Brigade instance, so "reset
|
|
4
|
+
// the instance" means "delete every row in every Brigade table" (plus any
|
|
5
|
+
// File-Storage objects rows point at). Used by the onboarding wizard's
|
|
6
|
+
// "start fresh" choice and the `brigade store reset` CLI.
|
|
7
|
+
//
|
|
8
|
+
// Deletion runs SERVER-SIDE and SELF-SCHEDULES (`resetStart` → `resetWorker`):
|
|
9
|
+
// a long-lived instance can hold hundreds of thousands of rows (cron runs,
|
|
10
|
+
// session events) and a single Convex mutation has op/time/byte limits, so each
|
|
11
|
+
// worker deletes one small batch then reschedules itself until its table drains.
|
|
12
|
+
// All tables drain concurrently; the client just polls `resetStatus`. The older
|
|
13
|
+
// client-paced `resetPage` is kept for back-compat / tests.
|
|
14
|
+
|
|
15
|
+
import { v } from "convex/values";
|
|
16
|
+
import { internal } from "./_generated/api.js";
|
|
17
|
+
import { internalMutation, mutation, query } from "./_generated/server.js";
|
|
18
|
+
import type { MutationCtx } from "./_generated/server.js";
|
|
19
|
+
|
|
20
|
+
// Every table in convex/schema.ts. Kept as an explicit literal union so a
|
|
21
|
+
// future schema addition that forgets to extend this list fails loudly in
|
|
22
|
+
// review rather than silently surviving a "factory reset".
|
|
23
|
+
const RESETTABLE_TABLES = [
|
|
24
|
+
"brigadeConfig",
|
|
25
|
+
"brigadeConfigAudit",
|
|
26
|
+
"brigadeConfigBackups",
|
|
27
|
+
"configHealth",
|
|
28
|
+
"personaFiles",
|
|
29
|
+
"workspaceState",
|
|
30
|
+
"memoryFacts",
|
|
31
|
+
"memoryExtractCursors",
|
|
32
|
+
"memoryConsolidateState",
|
|
33
|
+
"memoryEvents",
|
|
34
|
+
"sessions",
|
|
35
|
+
"sessionTranscriptRecords",
|
|
36
|
+
"sessionInboxEvents",
|
|
37
|
+
"sessionEvents",
|
|
38
|
+
"subsystemLog",
|
|
39
|
+
"cronJobs",
|
|
40
|
+
"cronRuns",
|
|
41
|
+
"cronServiceState",
|
|
42
|
+
"channelAccess",
|
|
43
|
+
"whatsappAuthFile",
|
|
44
|
+
"channelMediaBlob",
|
|
45
|
+
"authProfiles",
|
|
46
|
+
"profileState",
|
|
47
|
+
"authFiles",
|
|
48
|
+
"systemMeta",
|
|
49
|
+
"whatsappAuthCreds",
|
|
50
|
+
"whatsappAuthKeys",
|
|
51
|
+
"execApprovals",
|
|
52
|
+
"skills",
|
|
53
|
+
"extensions",
|
|
54
|
+
"orgDeriveAudit",
|
|
55
|
+
"orgChartCache",
|
|
56
|
+
"subagentRuns",
|
|
57
|
+
"gatewayCoord",
|
|
58
|
+
"brigadeBlobs",
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export type ResettableTable = (typeof RESETTABLE_TABLES)[number];
|
|
62
|
+
|
|
63
|
+
const TableName = v.union(
|
|
64
|
+
...RESETTABLE_TABLES.map((t) => v.literal(t)),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Tables whose rows can point at File-Storage objects — the object must be
|
|
68
|
+
// deleted BEFORE the row or it becomes an orphan (storage isn't ref-counted).
|
|
69
|
+
const STORAGE_SPILL_TABLES = new Set<string>([
|
|
70
|
+
"channelMediaBlob",
|
|
71
|
+
"whatsappAuthKeys",
|
|
72
|
+
"brigadeBlobs",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// High-volume session/log/run tables probed (presence only, never counted) by
|
|
76
|
+
// instanceSummary so the "found an existing Brigade" headline doesn't imply an
|
|
77
|
+
// empty backend when thousands of event/log rows are actually present.
|
|
78
|
+
const ACTIVITY_TABLES = [
|
|
79
|
+
"sessionEvents",
|
|
80
|
+
"cronRuns",
|
|
81
|
+
"subsystemLog",
|
|
82
|
+
"sessionInboxEvents",
|
|
83
|
+
"sessionTranscriptRecords",
|
|
84
|
+
"subagentRuns",
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
/** The list the reset client iterates — exported via query so the CLI and
|
|
88
|
+
* the server can never drift on which tables exist. */
|
|
89
|
+
export const listResettableTables = query({
|
|
90
|
+
args: {},
|
|
91
|
+
handler: async () => [...RESETTABLE_TABLES],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Headline summary for "found an existing Brigade in this backend". */
|
|
95
|
+
export const instanceSummary = query({
|
|
96
|
+
args: {},
|
|
97
|
+
handler: async (ctx) => {
|
|
98
|
+
const configRow = await ctx.db.query("brigadeConfig").first();
|
|
99
|
+
const memories = await ctx.db.query("memoryFacts").take(1001);
|
|
100
|
+
const sessions = await ctx.db.query("sessions").take(1001);
|
|
101
|
+
const cronJobs = await ctx.db.query("cronJobs").take(1001);
|
|
102
|
+
const personas = await ctx.db.query("personaFiles").take(1001);
|
|
103
|
+
const waCreds = await ctx.db.query("whatsappAuthCreds").take(1);
|
|
104
|
+
const fp = await ctx.db
|
|
105
|
+
.query("systemMeta")
|
|
106
|
+
.withIndex("by_key", (q) => q.eq("key", "encryptionFingerprint"))
|
|
107
|
+
.first();
|
|
108
|
+
// High-volume session/log/run tables hold the bulk of a long-lived
|
|
109
|
+
// instance, but their rows can be large (event payloads, transcript chunks)
|
|
110
|
+
// — counting them with .take(1001) could exceed the 16 MiB query read cap.
|
|
111
|
+
// So we only PROBE for presence (take(1)) to report that history exists,
|
|
112
|
+
// rather than imply "0" when thousands of rows are actually there.
|
|
113
|
+
let hasActivity = false;
|
|
114
|
+
for (const t of ACTIVITY_TABLES) {
|
|
115
|
+
if ((await ctx.db.query(t).take(1)).length > 0) {
|
|
116
|
+
hasActivity = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const cap = (n: number): number => Math.min(n, 1000);
|
|
121
|
+
return {
|
|
122
|
+
hasData:
|
|
123
|
+
configRow !== null ||
|
|
124
|
+
memories.length > 0 ||
|
|
125
|
+
sessions.length > 0 ||
|
|
126
|
+
cronJobs.length > 0 ||
|
|
127
|
+
personas.length > 0,
|
|
128
|
+
createdAtMs: configRow?._creationTime ?? null,
|
|
129
|
+
counts: {
|
|
130
|
+
memories: cap(memories.length),
|
|
131
|
+
sessions: cap(sessions.length),
|
|
132
|
+
cronJobs: cap(cronJobs.length),
|
|
133
|
+
personas: cap(personas.length),
|
|
134
|
+
},
|
|
135
|
+
hasActivity,
|
|
136
|
+
whatsappLinked: waCreds.length > 0,
|
|
137
|
+
storedKeyFingerprint: (fp?.value as string | undefined) ?? null,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/** Rough byte size of a row, for read-budgeting only. The sealed ArrayBuffer
|
|
143
|
+
* columns (transcript chunks, media, auth, blobs) dominate; everything else
|
|
144
|
+
* is tiny. This drives WHEN we stop reading more pages — the actual read cost
|
|
145
|
+
* is charged by Convex on each `.take()`. */
|
|
146
|
+
function estimateRowBytes(row: Record<string, unknown>): number {
|
|
147
|
+
let n = 64; // per-row overhead
|
|
148
|
+
for (const value of Object.values(row)) {
|
|
149
|
+
if (value instanceof ArrayBuffer) n += value.byteLength;
|
|
150
|
+
else if (typeof value === "string") n += value.length;
|
|
151
|
+
else if (typeof value === "number" || typeof value === "boolean") n += 8;
|
|
152
|
+
else if (value && typeof value === "object") n += JSON.stringify(value).length;
|
|
153
|
+
}
|
|
154
|
+
return n;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Delete ONE bounded batch from a table (reaping File-Storage spills first).
|
|
158
|
+
* Shared by the legacy client-paced `resetPage` and the server-scheduled
|
|
159
|
+
* `resetWorker`. Returns `{ deleted, done }`; `done` is true ONLY when the
|
|
160
|
+
* table is fully drained — the caller loops/reschedules while `!done`.
|
|
161
|
+
*
|
|
162
|
+
* Two caps keep every batch comfortably under Convex's per-execution ceiling:
|
|
163
|
+
* a row cap (`maxRows`) and a byte cap (`READ_CEILING`). Convex KILLS a function
|
|
164
|
+
* that exceeds its op/time/byte budget, and that kill is not catchable inside
|
|
165
|
+
* the function — so the only robust defense is to keep each batch small and let
|
|
166
|
+
* the caller chain more. Large-row tables (transcript chunks, media, auth,
|
|
167
|
+
* blobs near the 1 MiB doc limit) trip the byte cap after a handful of rows;
|
|
168
|
+
* small-row tables clear up to `maxRows`. Deleted rows drop out of the next
|
|
169
|
+
* `.take()`, so we always read from the front with no cursor. */
|
|
170
|
+
async function drainOneBatch(
|
|
171
|
+
ctx: MutationCtx,
|
|
172
|
+
table: ResettableTable,
|
|
173
|
+
maxRows: number,
|
|
174
|
+
readCeiling = 4 * 1024 * 1024, // conservative default; legacy resetPage passes 6 MiB
|
|
175
|
+
): Promise<{ deleted: number; done: boolean }> {
|
|
176
|
+
const INNER = 8; // small read window; deleted rows drop out of the next take()
|
|
177
|
+
let removed = 0;
|
|
178
|
+
let bytesRead = 0;
|
|
179
|
+
let drained = false;
|
|
180
|
+
let capped = false;
|
|
181
|
+
while (removed < maxRows && !capped) {
|
|
182
|
+
// Never read more than the batch still wants, so `removed` can't overshoot
|
|
183
|
+
// `maxRows` by up to INNER-1.
|
|
184
|
+
const want = Math.min(INNER, maxRows - removed);
|
|
185
|
+
const rows = await ctx.db.query(table).take(want);
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
bytesRead += estimateRowBytes(row as unknown as Record<string, unknown>);
|
|
188
|
+
if (STORAGE_SPILL_TABLES.has(table)) {
|
|
189
|
+
const storageId = (row as { storageId?: string }).storageId;
|
|
190
|
+
if (storageId) {
|
|
191
|
+
try {
|
|
192
|
+
await ctx.storage.delete(storageId as never);
|
|
193
|
+
} catch {
|
|
194
|
+
// Already gone — the row delete below still proceeds.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
await ctx.db.delete(row._id);
|
|
199
|
+
removed += 1;
|
|
200
|
+
// Stop the MOMENT we cross the read budget — mid-pass, so a handful of
|
|
201
|
+
// ~1 MiB rows can't blow ~INNER MiB past the cap before the next check.
|
|
202
|
+
if (bytesRead >= readCeiling) {
|
|
203
|
+
capped = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// A short read means the table is now empty — but only trust that when we
|
|
208
|
+
// did NOT stop early on the byte cap (a capped pass read a full `want`).
|
|
209
|
+
if (!capped && rows.length < want) {
|
|
210
|
+
drained = true;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { deleted: removed, done: drained };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Legacy client-paced single batch. Behaviour-identical to the original
|
|
218
|
+
* `resetPage` (200-row default, 6 MiB read ceiling). Kept for back-compat; the
|
|
219
|
+
* onboarding wizard and `brigade store reset` now use the server-scheduled path
|
|
220
|
+
* below. */
|
|
221
|
+
export const resetPage = mutation({
|
|
222
|
+
args: { table: TableName, limit: v.optional(v.number()) },
|
|
223
|
+
handler: async (ctx, args) => {
|
|
224
|
+
const maxRows = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 200;
|
|
225
|
+
return await drainOneBatch(ctx, args.table, maxRows, 6 * 1024 * 1024);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
// Server-side, self-scheduling factory reset — scales to any table size.
|
|
231
|
+
//
|
|
232
|
+
// `resetStart` seeds one progress row per table and schedules a `resetWorker`
|
|
233
|
+
// for each. Every worker deletes one small batch then reschedules ITSELF until
|
|
234
|
+
// its table is drained, so deletion runs entirely on the backend: no per-page
|
|
235
|
+
// client round-trips, no single mega-transaction to time out, and all tables
|
|
236
|
+
// drain concurrently. The client calls `resetStart` once then polls
|
|
237
|
+
// `resetStatus` until `done`.
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
const WORKER_BATCH = 100; // rows per scheduled transaction (small = self-host safe)
|
|
241
|
+
const SPILL_BATCH = 25; // spill tables do a storage.delete per row — go smaller
|
|
242
|
+
|
|
243
|
+
export const resetStart = mutation({
|
|
244
|
+
args: { runId: v.string() },
|
|
245
|
+
handler: async (ctx, args) => {
|
|
246
|
+
// Clear any prior progress rows so a re-run starts from a clean slate.
|
|
247
|
+
for (const row of await ctx.db.query("resetProgress").collect()) {
|
|
248
|
+
await ctx.db.delete(row._id);
|
|
249
|
+
}
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
for (const table of RESETTABLE_TABLES) {
|
|
252
|
+
await ctx.db.insert("resetProgress", {
|
|
253
|
+
runId: args.runId,
|
|
254
|
+
table,
|
|
255
|
+
deleted: 0,
|
|
256
|
+
done: false,
|
|
257
|
+
updatedAt: now,
|
|
258
|
+
});
|
|
259
|
+
await ctx.scheduler.runAfter(0, internal.admin.resetWorker, {
|
|
260
|
+
runId: args.runId,
|
|
261
|
+
table,
|
|
262
|
+
batch: STORAGE_SPILL_TABLES.has(table) ? SPILL_BATCH : WORKER_BATCH,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return { runId: args.runId, tablesTotal: RESETTABLE_TABLES.length };
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
export const resetWorker = internalMutation({
|
|
270
|
+
args: { runId: v.string(), table: TableName, batch: v.number() },
|
|
271
|
+
handler: async (ctx, args) => {
|
|
272
|
+
const { deleted, done } = await drainOneBatch(ctx, args.table, args.batch);
|
|
273
|
+
const row = await ctx.db
|
|
274
|
+
.query("resetProgress")
|
|
275
|
+
.withIndex("by_run_table", (q) => q.eq("runId", args.runId).eq("table", args.table))
|
|
276
|
+
.first();
|
|
277
|
+
if (row) {
|
|
278
|
+
await ctx.db.patch(row._id, {
|
|
279
|
+
deleted: row.deleted + deleted,
|
|
280
|
+
done: done || deleted === 0,
|
|
281
|
+
updatedAt: Date.now(),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// Reschedule the SAME table until it reports drained. A drained or empty
|
|
285
|
+
// batch ends the chain — no infinite reschedule.
|
|
286
|
+
if (!done && deleted > 0) {
|
|
287
|
+
await ctx.scheduler.runAfter(0, internal.admin.resetWorker, {
|
|
288
|
+
runId: args.runId,
|
|
289
|
+
table: args.table,
|
|
290
|
+
batch: args.batch,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
export const resetStatus = query({
|
|
297
|
+
args: { runId: v.string() },
|
|
298
|
+
handler: async (ctx, args) => {
|
|
299
|
+
const rows = await ctx.db
|
|
300
|
+
.query("resetProgress")
|
|
301
|
+
.withIndex("by_run", (q) => q.eq("runId", args.runId))
|
|
302
|
+
.collect();
|
|
303
|
+
if (rows.length === 0) return null;
|
|
304
|
+
const deletedTotal = rows.reduce((sum, r) => sum + r.deleted, 0);
|
|
305
|
+
const tablesDone = rows.filter((r) => r.done).length;
|
|
306
|
+
return {
|
|
307
|
+
done: tablesDone >= rows.length,
|
|
308
|
+
deletedTotal,
|
|
309
|
+
tablesTotal: rows.length,
|
|
310
|
+
tablesDone,
|
|
311
|
+
tables: rows.map((r) => ({ table: r.table, deleted: r.deleted, done: r.done })),
|
|
312
|
+
updatedAt: Math.max(...rows.map((r) => r.updatedAt)),
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
});
|
package/convex/auth.d.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export declare const listProfiles: import("convex/server").RegisteredQuery<"public", {
|
|
2
|
+
agentId: string;
|
|
3
|
+
ownerId: string;
|
|
4
|
+
}, Promise<{
|
|
5
|
+
_id: import("convex/values").GenericId<"authProfiles">;
|
|
6
|
+
_creationTime: number;
|
|
7
|
+
alias?: string | undefined;
|
|
8
|
+
expires?: number | undefined;
|
|
9
|
+
metadata?: any;
|
|
10
|
+
keyEnc?: ArrayBuffer | undefined;
|
|
11
|
+
keyRef?: {
|
|
12
|
+
id: string;
|
|
13
|
+
source: string;
|
|
14
|
+
provider: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
tokenEnc?: ArrayBuffer | undefined;
|
|
17
|
+
tokenRef?: {
|
|
18
|
+
id: string;
|
|
19
|
+
source: string;
|
|
20
|
+
provider: string;
|
|
21
|
+
} | undefined;
|
|
22
|
+
accessEnc?: ArrayBuffer | undefined;
|
|
23
|
+
refreshEnc?: ArrayBuffer | undefined;
|
|
24
|
+
type: "api_key" | "oauth" | "token";
|
|
25
|
+
profileId: string;
|
|
26
|
+
agentId: string;
|
|
27
|
+
provider: string;
|
|
28
|
+
ownerId: string;
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
}[]>>;
|
|
31
|
+
export declare const getProfile: import("convex/server").RegisteredQuery<"public", {
|
|
32
|
+
profileId: string;
|
|
33
|
+
agentId: string;
|
|
34
|
+
ownerId: string;
|
|
35
|
+
}, Promise<{
|
|
36
|
+
_id: import("convex/values").GenericId<"authProfiles">;
|
|
37
|
+
_creationTime: number;
|
|
38
|
+
alias?: string | undefined;
|
|
39
|
+
expires?: number | undefined;
|
|
40
|
+
metadata?: any;
|
|
41
|
+
keyEnc?: ArrayBuffer | undefined;
|
|
42
|
+
keyRef?: {
|
|
43
|
+
id: string;
|
|
44
|
+
source: string;
|
|
45
|
+
provider: string;
|
|
46
|
+
} | undefined;
|
|
47
|
+
tokenEnc?: ArrayBuffer | undefined;
|
|
48
|
+
tokenRef?: {
|
|
49
|
+
id: string;
|
|
50
|
+
source: string;
|
|
51
|
+
provider: string;
|
|
52
|
+
} | undefined;
|
|
53
|
+
accessEnc?: ArrayBuffer | undefined;
|
|
54
|
+
refreshEnc?: ArrayBuffer | undefined;
|
|
55
|
+
type: "api_key" | "oauth" | "token";
|
|
56
|
+
profileId: string;
|
|
57
|
+
agentId: string;
|
|
58
|
+
provider: string;
|
|
59
|
+
ownerId: string;
|
|
60
|
+
updatedAt: number;
|
|
61
|
+
} | null>>;
|
|
62
|
+
export declare const upsertProfile: import("convex/server").RegisteredMutation<"public", {
|
|
63
|
+
alias?: string | undefined;
|
|
64
|
+
expires?: number | undefined;
|
|
65
|
+
metadata?: any;
|
|
66
|
+
keyEnc?: ArrayBuffer | undefined;
|
|
67
|
+
keyRef?: {
|
|
68
|
+
id: string;
|
|
69
|
+
source: string;
|
|
70
|
+
provider: string;
|
|
71
|
+
} | undefined;
|
|
72
|
+
tokenEnc?: ArrayBuffer | undefined;
|
|
73
|
+
tokenRef?: {
|
|
74
|
+
id: string;
|
|
75
|
+
source: string;
|
|
76
|
+
provider: string;
|
|
77
|
+
} | undefined;
|
|
78
|
+
accessEnc?: ArrayBuffer | undefined;
|
|
79
|
+
refreshEnc?: ArrayBuffer | undefined;
|
|
80
|
+
type: "api_key" | "oauth" | "token";
|
|
81
|
+
profileId: string;
|
|
82
|
+
agentId: string;
|
|
83
|
+
provider: string;
|
|
84
|
+
ownerId: string;
|
|
85
|
+
}, Promise<{
|
|
86
|
+
profileId: string;
|
|
87
|
+
updated: boolean;
|
|
88
|
+
}>>;
|
|
89
|
+
export declare const deleteProfile: import("convex/server").RegisteredMutation<"public", {
|
|
90
|
+
profileId: string;
|
|
91
|
+
agentId: string;
|
|
92
|
+
ownerId: string;
|
|
93
|
+
}, Promise<{
|
|
94
|
+
deleted: boolean;
|
|
95
|
+
}>>;
|
|
96
|
+
export declare const loadState: import("convex/server").RegisteredQuery<"public", {
|
|
97
|
+
agentId: string;
|
|
98
|
+
ownerId: string;
|
|
99
|
+
}, Promise<{
|
|
100
|
+
_id: import("convex/values").GenericId<"profileState">;
|
|
101
|
+
_creationTime: number;
|
|
102
|
+
disabledUntil?: number | undefined;
|
|
103
|
+
cooldownUntil?: number | undefined;
|
|
104
|
+
cooldownModel?: string | undefined;
|
|
105
|
+
errorCount?: number | undefined;
|
|
106
|
+
lastUsed?: number | undefined;
|
|
107
|
+
cooldownReason?: string | undefined;
|
|
108
|
+
disabledReason?: string | undefined;
|
|
109
|
+
failureCounts?: any;
|
|
110
|
+
lastFailureAt?: number | undefined;
|
|
111
|
+
explicitOrder?: number | undefined;
|
|
112
|
+
profileId: string;
|
|
113
|
+
agentId: string;
|
|
114
|
+
provider: string;
|
|
115
|
+
ownerId: string;
|
|
116
|
+
isLastGood: boolean;
|
|
117
|
+
}[]>>;
|
|
118
|
+
export declare const readAuthFile: import("convex/server").RegisteredQuery<"public", {
|
|
119
|
+
agentId: string;
|
|
120
|
+
kind: "auth-state" | "profile-state" | "models";
|
|
121
|
+
ownerId: string;
|
|
122
|
+
}, Promise<{
|
|
123
|
+
_id: import("convex/values").GenericId<"authFiles">;
|
|
124
|
+
_creationTime: number;
|
|
125
|
+
agentId: string;
|
|
126
|
+
payload: ArrayBuffer;
|
|
127
|
+
kind: "auth-state" | "profile-state" | "models";
|
|
128
|
+
ownerId: string;
|
|
129
|
+
updatedAt: number;
|
|
130
|
+
} | null>>;
|
|
131
|
+
export declare const writeAuthFile: import("convex/server").RegisteredMutation<"public", {
|
|
132
|
+
agentId: string;
|
|
133
|
+
payload: ArrayBuffer;
|
|
134
|
+
kind: "auth-state" | "profile-state" | "models";
|
|
135
|
+
ownerId: string;
|
|
136
|
+
}, Promise<{
|
|
137
|
+
updated: boolean;
|
|
138
|
+
}>>;
|
|
139
|
+
export declare const upsertState: import("convex/server").RegisteredMutation<"public", {
|
|
140
|
+
disabledUntil?: number | undefined;
|
|
141
|
+
cooldownUntil?: number | undefined;
|
|
142
|
+
cooldownModel?: string | undefined;
|
|
143
|
+
errorCount?: number | undefined;
|
|
144
|
+
lastUsed?: number | undefined;
|
|
145
|
+
cooldownReason?: string | undefined;
|
|
146
|
+
disabledReason?: string | undefined;
|
|
147
|
+
failureCounts?: any;
|
|
148
|
+
lastFailureAt?: number | undefined;
|
|
149
|
+
explicitOrder?: number | undefined;
|
|
150
|
+
profileId: string;
|
|
151
|
+
agentId: string;
|
|
152
|
+
provider: string;
|
|
153
|
+
ownerId: string;
|
|
154
|
+
isLastGood: boolean;
|
|
155
|
+
}, Promise<{
|
|
156
|
+
profileId: string;
|
|
157
|
+
updated: boolean;
|
|
158
|
+
}>>;
|
|
159
|
+
//# sourceMappingURL=auth.d.ts.map
|
package/convex/auth.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// convex/auth.ts
|
|
2
|
+
//
|
|
3
|
+
// Convex functions for the authProfiles + profileState tables. Each
|
|
4
|
+
// operator gets one logical "agent" namespace per agentId; the agentId is
|
|
5
|
+
// part of every row's primary key.
|
|
6
|
+
//
|
|
7
|
+
// Profiles carry encrypted secrets (`keyEnc` / `tokenEnc` / etc.) — the
|
|
8
|
+
// adapter (ConvexAuthStore) handles encrypt-on-write / decrypt-on-read
|
|
9
|
+
// via the per-owner DEK so primitive code only ever sees plaintext.
|
|
10
|
+
|
|
11
|
+
import { v } from "convex/values";
|
|
12
|
+
import { mutation, query } from "./_generated/server.js";
|
|
13
|
+
|
|
14
|
+
const ProfileType = v.union(
|
|
15
|
+
v.literal("api_key"),
|
|
16
|
+
v.literal("oauth"),
|
|
17
|
+
v.literal("token"),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const SecretRef = v.object({
|
|
21
|
+
source: v.string(),
|
|
22
|
+
provider: v.string(),
|
|
23
|
+
id: v.string(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// authProfiles
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export const listProfiles = query({
|
|
31
|
+
args: { ownerId: v.string(), agentId: v.string() },
|
|
32
|
+
handler: async (ctx, args) => {
|
|
33
|
+
return ctx.db
|
|
34
|
+
.query("authProfiles")
|
|
35
|
+
.withIndex("by_owner_agent", (q) =>
|
|
36
|
+
q.eq("ownerId", args.ownerId).eq("agentId", args.agentId),
|
|
37
|
+
)
|
|
38
|
+
.collect();
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const getProfile = query({
|
|
43
|
+
args: {
|
|
44
|
+
ownerId: v.string(),
|
|
45
|
+
agentId: v.string(),
|
|
46
|
+
profileId: v.string(),
|
|
47
|
+
},
|
|
48
|
+
handler: async (ctx, args) => {
|
|
49
|
+
return ctx.db
|
|
50
|
+
.query("authProfiles")
|
|
51
|
+
.withIndex("by_owner_agent_profileId", (q) =>
|
|
52
|
+
q
|
|
53
|
+
.eq("ownerId", args.ownerId)
|
|
54
|
+
.eq("agentId", args.agentId)
|
|
55
|
+
.eq("profileId", args.profileId),
|
|
56
|
+
)
|
|
57
|
+
.first();
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const upsertProfile = mutation({
|
|
62
|
+
args: {
|
|
63
|
+
ownerId: v.string(),
|
|
64
|
+
agentId: v.string(),
|
|
65
|
+
profileId: v.string(),
|
|
66
|
+
provider: v.string(),
|
|
67
|
+
alias: v.optional(v.string()),
|
|
68
|
+
type: ProfileType,
|
|
69
|
+
keyEnc: v.optional(v.bytes()),
|
|
70
|
+
keyRef: v.optional(SecretRef),
|
|
71
|
+
tokenEnc: v.optional(v.bytes()),
|
|
72
|
+
tokenRef: v.optional(SecretRef),
|
|
73
|
+
accessEnc: v.optional(v.bytes()),
|
|
74
|
+
refreshEnc: v.optional(v.bytes()),
|
|
75
|
+
expires: v.optional(v.number()),
|
|
76
|
+
metadata: v.optional(v.any()),
|
|
77
|
+
},
|
|
78
|
+
handler: async (ctx, args) => {
|
|
79
|
+
const existing = await ctx.db
|
|
80
|
+
.query("authProfiles")
|
|
81
|
+
.withIndex("by_owner_agent_profileId", (q) =>
|
|
82
|
+
q
|
|
83
|
+
.eq("ownerId", args.ownerId)
|
|
84
|
+
.eq("agentId", args.agentId)
|
|
85
|
+
.eq("profileId", args.profileId),
|
|
86
|
+
)
|
|
87
|
+
.first();
|
|
88
|
+
const payload = { ...args, updatedAt: Date.now() };
|
|
89
|
+
if (existing) {
|
|
90
|
+
await ctx.db.replace(existing._id, payload);
|
|
91
|
+
return { profileId: args.profileId, updated: true };
|
|
92
|
+
}
|
|
93
|
+
await ctx.db.insert("authProfiles", payload);
|
|
94
|
+
return { profileId: args.profileId, updated: false };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const deleteProfile = mutation({
|
|
99
|
+
args: {
|
|
100
|
+
ownerId: v.string(),
|
|
101
|
+
agentId: v.string(),
|
|
102
|
+
profileId: v.string(),
|
|
103
|
+
},
|
|
104
|
+
handler: async (ctx, args) => {
|
|
105
|
+
const existing = await ctx.db
|
|
106
|
+
.query("authProfiles")
|
|
107
|
+
.withIndex("by_owner_agent_profileId", (q) =>
|
|
108
|
+
q
|
|
109
|
+
.eq("ownerId", args.ownerId)
|
|
110
|
+
.eq("agentId", args.agentId)
|
|
111
|
+
.eq("profileId", args.profileId),
|
|
112
|
+
)
|
|
113
|
+
.first();
|
|
114
|
+
if (!existing) return { deleted: false };
|
|
115
|
+
await ctx.db.delete(existing._id);
|
|
116
|
+
return { deleted: true };
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// profileState (per-profile cooldown / last-good / failure counters)
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
export const loadState = query({
|
|
125
|
+
args: { ownerId: v.string(), agentId: v.string() },
|
|
126
|
+
handler: async (ctx, args) => {
|
|
127
|
+
return ctx.db
|
|
128
|
+
.query("profileState")
|
|
129
|
+
.withIndex("by_owner_agent_profileId", (q) =>
|
|
130
|
+
q.eq("ownerId", args.ownerId).eq("agentId", args.agentId),
|
|
131
|
+
)
|
|
132
|
+
.collect();
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// authFiles (whole-file state blobs — auth-state.json / profile-state.json)
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
const AuthFileKind = v.union(
|
|
141
|
+
v.literal("auth-state"),
|
|
142
|
+
v.literal("profile-state"),
|
|
143
|
+
v.literal("models"),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
export const readAuthFile = query({
|
|
147
|
+
args: { ownerId: v.string(), agentId: v.string(), kind: AuthFileKind },
|
|
148
|
+
handler: async (ctx, args) => {
|
|
149
|
+
return ctx.db
|
|
150
|
+
.query("authFiles")
|
|
151
|
+
.withIndex("by_owner_agent_kind", (q) =>
|
|
152
|
+
q.eq("ownerId", args.ownerId).eq("agentId", args.agentId).eq("kind", args.kind),
|
|
153
|
+
)
|
|
154
|
+
.first();
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const writeAuthFile = mutation({
|
|
159
|
+
args: {
|
|
160
|
+
ownerId: v.string(),
|
|
161
|
+
agentId: v.string(),
|
|
162
|
+
kind: AuthFileKind,
|
|
163
|
+
payload: v.bytes(),
|
|
164
|
+
},
|
|
165
|
+
handler: async (ctx, args) => {
|
|
166
|
+
const existing = await ctx.db
|
|
167
|
+
.query("authFiles")
|
|
168
|
+
.withIndex("by_owner_agent_kind", (q) =>
|
|
169
|
+
q.eq("ownerId", args.ownerId).eq("agentId", args.agentId).eq("kind", args.kind),
|
|
170
|
+
)
|
|
171
|
+
.first();
|
|
172
|
+
const row = { ...args, updatedAt: Date.now() };
|
|
173
|
+
if (existing) {
|
|
174
|
+
await ctx.db.replace(existing._id, row);
|
|
175
|
+
return { updated: true };
|
|
176
|
+
}
|
|
177
|
+
await ctx.db.insert("authFiles", row);
|
|
178
|
+
return { updated: false };
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export const upsertState = mutation({
|
|
183
|
+
args: {
|
|
184
|
+
ownerId: v.string(),
|
|
185
|
+
agentId: v.string(),
|
|
186
|
+
profileId: v.string(),
|
|
187
|
+
provider: v.string(),
|
|
188
|
+
lastUsed: v.optional(v.number()),
|
|
189
|
+
cooldownUntil: v.optional(v.number()),
|
|
190
|
+
cooldownReason: v.optional(v.string()),
|
|
191
|
+
cooldownModel: v.optional(v.string()),
|
|
192
|
+
disabledUntil: v.optional(v.number()),
|
|
193
|
+
disabledReason: v.optional(v.string()),
|
|
194
|
+
errorCount: v.optional(v.number()),
|
|
195
|
+
failureCounts: v.optional(v.any()),
|
|
196
|
+
lastFailureAt: v.optional(v.number()),
|
|
197
|
+
isLastGood: v.boolean(),
|
|
198
|
+
explicitOrder: v.optional(v.number()),
|
|
199
|
+
},
|
|
200
|
+
handler: async (ctx, args) => {
|
|
201
|
+
const existing = await ctx.db
|
|
202
|
+
.query("profileState")
|
|
203
|
+
.withIndex("by_owner_agent_profileId", (q) =>
|
|
204
|
+
q
|
|
205
|
+
.eq("ownerId", args.ownerId)
|
|
206
|
+
.eq("agentId", args.agentId)
|
|
207
|
+
.eq("profileId", args.profileId),
|
|
208
|
+
)
|
|
209
|
+
.first();
|
|
210
|
+
if (existing) {
|
|
211
|
+
await ctx.db.replace(existing._id, args);
|
|
212
|
+
return { profileId: args.profileId, updated: true };
|
|
213
|
+
}
|
|
214
|
+
await ctx.db.insert("profileState", args);
|
|
215
|
+
return { profileId: args.profileId, updated: false };
|
|
216
|
+
},
|
|
217
|
+
});
|