omnigateway 0.2.2 → 0.4.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 +506 -9
- package/bin/omni.js +3022 -163
- package/gateway.js +4094 -662
- package/package.json +1 -1
- package/public/assets/{Chip-DJwp7gBu.js → Chip-BltJSw8A.js} +5 -5
- package/public/assets/Confirm-gd8Y6snu.js +4 -0
- package/public/assets/CopyValue-Dds-Ow9a.js +27 -0
- package/public/assets/{Field-BDtp692L.js → Field-D-14HF0x.js} +11 -11
- package/public/assets/Lamp-BDf7-dMa.js +25 -0
- package/public/assets/Match-CgNxphvS.js +1 -0
- package/public/assets/Meter-stpt3vQu.js +14 -0
- package/public/assets/Modal-Dj3LvMS9.js +82 -0
- package/public/assets/Rack-B4c0dbIb.js +180 -0
- package/public/assets/{Readout-CEAoN5hu.js → Readout-Du15WYx0.js} +5 -5
- package/public/assets/{States-BEKLros0.js → States-CxLDlCaw.js} +5 -5
- package/public/assets/{Table-BBypbVWj.js → Table-CVDZ69Zx.js} +1 -1
- package/public/assets/Toggle-DMk46RX2.js +39 -0
- package/public/assets/TokenBreakdown-BySwFK6Q.js +31 -0
- package/public/assets/_app-Dx_xnlud.js +1 -0
- package/public/assets/_app.accounts-DvBYnbfS.js +64 -0
- package/public/assets/_app.console-CoaRdkB1.js +44 -0
- package/public/assets/_app.database-CES5ec5g.js +39 -0
- package/public/assets/_app.index-DwOZe1A-.js +62 -0
- package/public/assets/_app.keys-B5HrAu2U.js +76 -0
- package/public/assets/_app.logs-NbfIexz0.js +68 -0
- package/public/assets/_app.models-B1MdFRtQ.js +148 -0
- package/public/assets/_app.plugins._pluginId-ByMziW8p.js +20 -0
- package/public/assets/_app.settings-BkDkGkKF.js +46 -0
- package/public/assets/_app.usage-DNpM8Nub.js +83 -0
- package/public/assets/dist-9ps0Qd15.js +1 -0
- package/public/assets/index-gMv8Qs7m.js +178 -0
- package/public/assets/login-cCXnl34r.js +36 -0
- package/public/assets/matchContext-BOenO3J2.js +1 -0
- package/public/assets/plus-fXfUTt6_.js +1 -0
- package/public/assets/queries-CIOPbRcE.js +142 -0
- package/public/assets/reasons-Cb8yF4Oy.js +1 -0
- package/public/assets/shared-B4o-2quR.js +84 -0
- package/public/assets/{trash-2-CLYNAAJA.js → trash-2-ZPh3619D.js} +1 -1
- package/public/index.html +7 -2
- package/public/shared/chunks/jsx-runtime-CznXPbDH.js +1 -0
- package/public/shared/chunks/react-GitiQ0dt.js +1 -0
- package/public/shared/chunks/react-dom-BTlfKTOo.js +1 -0
- package/public/shared/chunks/rolldown-runtime-CbXtAM7H.js +1 -0
- package/public/shared/jsx-runtime.js +1 -0
- package/public/shared/react-dom-client.js +9 -0
- package/public/shared/react-dom.js +1 -0
- package/public/shared/react-query.js +1 -0
- package/public/shared/react.js +1 -0
- package/public/shared/styled-components.js +7 -0
- package/public/assets/Confirm-CVLz9F7a.js +0 -4
- package/public/assets/CopyValue-CNZbWS59.js +0 -27
- package/public/assets/Lamp-B65uWKbW.js +0 -25
- package/public/assets/Meter-BREBUFGX.js +0 -14
- package/public/assets/Modal-DbydSAlp.js +0 -82
- package/public/assets/Rack-Bbo4JfEX.js +0 -151
- package/public/assets/Toggle-BUr71O4J.js +0 -39
- package/public/assets/TokenBreakdown-C1ziK9d1.js +0 -31
- package/public/assets/_app-ByQ1bPbe.js +0 -1
- package/public/assets/_app.accounts-BTug_Xfo.js +0 -64
- package/public/assets/_app.console-DKyGL9xx.js +0 -44
- package/public/assets/_app.index-CSw9gmMf.js +0 -62
- package/public/assets/_app.keys-07Kbw4RS.js +0 -39
- package/public/assets/_app.logs-C9QiV39p.js +0 -32
- package/public/assets/_app.models-CDqVrAeF.js +0 -148
- package/public/assets/_app.settings-DmUEeTaP.js +0 -38
- package/public/assets/_app.usage-B1WuMGRr.js +0 -83
- package/public/assets/dist-Bka7ErcH.js +0 -1
- package/public/assets/index-DJfb1i4P.js +0 -186
- package/public/assets/login-DKZYqRh0.js +0 -33
- package/public/assets/queries-D8LJNQpL.js +0 -144
- package/public/assets/shared-CIhZ6FCO.js +0 -84
package/bin/omni.js
CHANGED
|
@@ -102,9 +102,19 @@ function formatSpan(ms) {
|
|
|
102
102
|
function formatUsd(value) {
|
|
103
103
|
return `$${value.toFixed(value < 1 ? 4 : 2)}`;
|
|
104
104
|
}
|
|
105
|
+
function formatBytes(bytes) {
|
|
106
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
107
|
+
return "\u2014";
|
|
108
|
+
if (bytes < 1024)
|
|
109
|
+
return `${Math.round(bytes)} B`;
|
|
110
|
+
const kb = bytes / 1024;
|
|
111
|
+
if (kb < 1024)
|
|
112
|
+
return `${kb.toFixed(1)} KB`;
|
|
113
|
+
return `${(kb / 1024).toFixed(1)} MB`;
|
|
114
|
+
}
|
|
105
115
|
|
|
106
116
|
// apps/cli/src/run.ts
|
|
107
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
117
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
108
118
|
import { homedir as homedir3 } from "os";
|
|
109
119
|
|
|
110
120
|
// packages/control/src/adminAuth.ts
|
|
@@ -161,9 +171,34 @@ function createAdminAuth(store, opts) {
|
|
|
161
171
|
},
|
|
162
172
|
logout(token) {
|
|
163
173
|
sessions.delete(token);
|
|
174
|
+
},
|
|
175
|
+
invalidateSessions() {
|
|
176
|
+
sessions.clear();
|
|
164
177
|
}
|
|
165
178
|
};
|
|
166
179
|
}
|
|
180
|
+
// packages/control/src/bodies.ts
|
|
181
|
+
async function readRequestBody(store, requestId) {
|
|
182
|
+
const read = await store.bodies.get(requestId);
|
|
183
|
+
if (read === null) {
|
|
184
|
+
return {
|
|
185
|
+
requestId,
|
|
186
|
+
detailState: "none",
|
|
187
|
+
truncated: false,
|
|
188
|
+
sizeBytes: 0,
|
|
189
|
+
at: null,
|
|
190
|
+
artifact: null
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
requestId: read.row.requestId,
|
|
195
|
+
detailState: read.row.detailState,
|
|
196
|
+
truncated: read.row.truncated,
|
|
197
|
+
sizeBytes: read.row.sizeBytes,
|
|
198
|
+
at: read.row.at,
|
|
199
|
+
artifact: read.artifact
|
|
200
|
+
};
|
|
201
|
+
}
|
|
167
202
|
// packages/ir/src/betas.ts
|
|
168
203
|
var CONTEXT_1M_BETA = "context-1m-2025-08-07";
|
|
169
204
|
var CONTEXT_1M_TOKENS = 1e6;
|
|
@@ -362,6 +397,7 @@ function loadConfig(env) {
|
|
|
362
397
|
const staticDir = env.OMNI_STATIC_DIR?.trim();
|
|
363
398
|
const logFile = env.OMNI_LOG_FILE?.trim();
|
|
364
399
|
const exposeClaudeCodeAliases = TRUTHY.has((env.OMNI_EXPOSE_CLAUDE_CODE_ALIASES ?? "").trim().toLowerCase());
|
|
400
|
+
const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
|
|
365
401
|
const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
|
|
366
402
|
const logLevel = parseLogLevel(rawLogLevel);
|
|
367
403
|
return {
|
|
@@ -374,6 +410,7 @@ function loadConfig(env) {
|
|
|
374
410
|
baseUrl,
|
|
375
411
|
staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir,
|
|
376
412
|
exposeClaudeCodeAliases,
|
|
413
|
+
bodyLoggingAllowed,
|
|
377
414
|
logFile: logFile === undefined || logFile.length === 0 ? null : logFile
|
|
378
415
|
};
|
|
379
416
|
}
|
|
@@ -513,14 +550,16 @@ function billingBlock() {
|
|
|
513
550
|
return `${BILLING_PREFIX} cc_version=${CLI_VERSION}.${BUILD_REVISION}; ` + `cc_entrypoint=cli; cch=${CCH_PLACEHOLDER};`;
|
|
514
551
|
}
|
|
515
552
|
var BANNED_SUBSTRINGS = [
|
|
516
|
-
"github.com/anomalyco/
|
|
517
|
-
"
|
|
518
|
-
"github.com/
|
|
519
|
-
"github.com/
|
|
520
|
-
"
|
|
553
|
+
"github.com/anomalyco/opencode",
|
|
554
|
+
"opencode.ai/docs",
|
|
555
|
+
"github.com/cline/cline",
|
|
556
|
+
"github.com/getcursor/cursor",
|
|
557
|
+
"continue.dev",
|
|
558
|
+
"hermes-agent.nousresearch.com"
|
|
521
559
|
];
|
|
560
|
+
var IDENTITY_PREFIXES = ["You are OpenCode", "You are Hermes Agent"];
|
|
522
561
|
var REWRITES = [
|
|
523
|
-
["if
|
|
562
|
+
["if OpenCode honestly", "if the assistant honestly"],
|
|
524
563
|
[
|
|
525
564
|
"Here is some useful information about the environment you are running in:",
|
|
526
565
|
"Environment context you are running in:"
|
|
@@ -529,7 +568,7 @@ var REWRITES = [
|
|
|
529
568
|
function applyAnthropicSystem(system) {
|
|
530
569
|
const kept = [];
|
|
531
570
|
for (const block of system) {
|
|
532
|
-
const text = block.text.split(/\n{2,}/).filter((p) => !BANNED_SUBSTRINGS.some((b) => p.includes(b))).filter((p) => !p.trimStart().startsWith(
|
|
571
|
+
const text = block.text.split(/\n{2,}/).filter((p) => !BANNED_SUBSTRINGS.some((b) => p.includes(b))).filter((p) => !IDENTITY_PREFIXES.some((i) => p.trimStart().startsWith(i))).filter((p) => !p.includes(BILLING_PREFIX)).filter((p) => p.trim() !== AGENT_PREAMBLE).join(`
|
|
533
572
|
|
|
534
573
|
`);
|
|
535
574
|
let rewritten = text;
|
|
@@ -17811,6 +17850,45 @@ function date4(params) {
|
|
|
17811
17850
|
|
|
17812
17851
|
// node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
17813
17852
|
config(en_default());
|
|
17853
|
+
// packages/ratelimit/src/catalog.ts
|
|
17854
|
+
var WINDOWS = ["1m", "5h", "1w"];
|
|
17855
|
+
var WINDOW_MS = {
|
|
17856
|
+
"1m": 60000,
|
|
17857
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
17858
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
17859
|
+
};
|
|
17860
|
+
var countLimit = exports_external.number().int().positive().nullable();
|
|
17861
|
+
var spendLimit = exports_external.number().positive().nullable();
|
|
17862
|
+
var countWindows = exports_external.object({
|
|
17863
|
+
"1m": countLimit.optional(),
|
|
17864
|
+
"5h": countLimit.optional(),
|
|
17865
|
+
"1w": countLimit.optional()
|
|
17866
|
+
}).strict();
|
|
17867
|
+
var spendWindows = exports_external.object({
|
|
17868
|
+
"5h": spendLimit.optional(),
|
|
17869
|
+
"1w": spendLimit.optional()
|
|
17870
|
+
}).strict();
|
|
17871
|
+
var limitConfigSchema = exports_external.object({
|
|
17872
|
+
requests: countWindows.optional(),
|
|
17873
|
+
tokens: countWindows.optional(),
|
|
17874
|
+
spend: spendWindows.optional(),
|
|
17875
|
+
concurrency: countLimit.optional()
|
|
17876
|
+
}).strict();
|
|
17877
|
+
function assertNoProto(value, path) {
|
|
17878
|
+
if (typeof value !== "object" || value === null)
|
|
17879
|
+
return;
|
|
17880
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
17881
|
+
if (key === "__proto__") {
|
|
17882
|
+
throw new Error(`limits cannot name "__proto__"${path === "" ? "" : ` under ${path}`}`);
|
|
17883
|
+
}
|
|
17884
|
+
assertNoProto(value[key], path === "" ? key : `${path}.${key}`);
|
|
17885
|
+
}
|
|
17886
|
+
}
|
|
17887
|
+
function parseLimitConfig(value) {
|
|
17888
|
+
assertNoProto(value, "");
|
|
17889
|
+
return limitConfigSchema.parse(value);
|
|
17890
|
+
}
|
|
17891
|
+
|
|
17814
17892
|
// packages/control/src/schemas.ts
|
|
17815
17893
|
function parseOrThrow(schema, body2) {
|
|
17816
17894
|
const result = schema.safeParse(body2);
|
|
@@ -17888,7 +17966,13 @@ var modelSchema = exports_external.object({
|
|
|
17888
17966
|
var keyCreateSchema = exports_external.object({
|
|
17889
17967
|
label: exports_external.string().min(1).default("api key"),
|
|
17890
17968
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
|
|
17891
|
-
|
|
17969
|
+
limits: limitConfigSchema.default({}),
|
|
17970
|
+
bodyLoggingOptOut: exports_external.boolean().default(false)
|
|
17971
|
+
}).strict();
|
|
17972
|
+
var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
|
|
17973
|
+
var retentionSchema = exports_external.object({
|
|
17974
|
+
keepLatest: exports_external.number().int().min(1).max(100),
|
|
17975
|
+
maxAgeDays: exports_external.number().int().min(1).max(3650)
|
|
17892
17976
|
}).strict();
|
|
17893
17977
|
var settingsSchema = exports_external.object({
|
|
17894
17978
|
weights: exports_external.object({
|
|
@@ -17905,7 +17989,11 @@ var settingsSchema = exports_external.object({
|
|
|
17905
17989
|
breakerCooldownMs: exports_external.number().int().positive(),
|
|
17906
17990
|
logRetentionDays: exports_external.number().int().min(1),
|
|
17907
17991
|
quotaPollIntervalMs: exports_external.number().int().min(0),
|
|
17908
|
-
rtkEnabled: exports_external.boolean()
|
|
17992
|
+
rtkEnabled: exports_external.boolean(),
|
|
17993
|
+
bodyLoggingEnabled: exports_external.boolean(),
|
|
17994
|
+
bodyLoggingCaptureStreamChunks: exports_external.boolean(),
|
|
17995
|
+
snapshotKeepLatest: retentionSchema.shape.keepLatest.optional(),
|
|
17996
|
+
snapshotMaxAgeDays: retentionSchema.shape.maxAgeDays.optional()
|
|
17909
17997
|
});
|
|
17910
17998
|
var credentialPatchSchema = exports_external.object({
|
|
17911
17999
|
label: exports_external.string().min(1).optional(),
|
|
@@ -18128,7 +18216,11 @@ var DEFAULT_SETTINGS = {
|
|
|
18128
18216
|
breakerCooldownMs: 30000,
|
|
18129
18217
|
logRetentionDays: 30,
|
|
18130
18218
|
quotaPollIntervalMs: 300000,
|
|
18131
|
-
rtkEnabled: false
|
|
18219
|
+
rtkEnabled: false,
|
|
18220
|
+
bodyLoggingEnabled: false,
|
|
18221
|
+
bodyLoggingCaptureStreamChunks: false,
|
|
18222
|
+
snapshotKeepLatest: 5,
|
|
18223
|
+
snapshotMaxAgeDays: 30
|
|
18132
18224
|
};
|
|
18133
18225
|
|
|
18134
18226
|
// packages/router/src/quota.ts
|
|
@@ -18267,6 +18359,10 @@ function rank(input) {
|
|
|
18267
18359
|
return { candidates: scored, excluded };
|
|
18268
18360
|
}
|
|
18269
18361
|
|
|
18362
|
+
// packages/store/src/bodies/artifact.ts
|
|
18363
|
+
import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "fs/promises";
|
|
18364
|
+
import { dirname, join, sep } from "path";
|
|
18365
|
+
|
|
18270
18366
|
// packages/store/src/encryption.ts
|
|
18271
18367
|
var PREFIX = "enc:v1";
|
|
18272
18368
|
var IV_BYTES = 12;
|
|
@@ -18306,6 +18402,362 @@ function unhex(s) {
|
|
|
18306
18402
|
out[i] = Number.parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
18307
18403
|
return out;
|
|
18308
18404
|
}
|
|
18405
|
+
|
|
18406
|
+
// packages/store/src/bodies/bound.ts
|
|
18407
|
+
var MAX_STRING_BYTES = 64 * 1024;
|
|
18408
|
+
var MAX_ARRAY_ITEMS = 24;
|
|
18409
|
+
var MAX_DEPTH = 6;
|
|
18410
|
+
var MAX_OBJECT_KEYS = 80;
|
|
18411
|
+
var STRING_TRUNCATION_MARKER = "\u2026[truncated]";
|
|
18412
|
+
var DEPTH_MARKER = `[omitted: nesting past ${MAX_DEPTH} levels]`;
|
|
18413
|
+
var encoder = new TextEncoder;
|
|
18414
|
+
var decoder = new TextDecoder;
|
|
18415
|
+
var CUT_BYTES = MAX_STRING_BYTES - encoder.encode(STRING_TRUNCATION_MARKER).length - 3;
|
|
18416
|
+
function truncateToBytes(value) {
|
|
18417
|
+
const bytes = encoder.encode(value);
|
|
18418
|
+
if (bytes.length <= MAX_STRING_BYTES)
|
|
18419
|
+
return value;
|
|
18420
|
+
return decoder.decode(bytes.subarray(0, CUT_BYTES)) + STRING_TRUNCATION_MARKER;
|
|
18421
|
+
}
|
|
18422
|
+
function boundValue(value) {
|
|
18423
|
+
let truncated = false;
|
|
18424
|
+
const walk = (input, depth) => {
|
|
18425
|
+
if (typeof input === "string") {
|
|
18426
|
+
const cut = truncateToBytes(input);
|
|
18427
|
+
if (cut !== input)
|
|
18428
|
+
truncated = true;
|
|
18429
|
+
return cut;
|
|
18430
|
+
}
|
|
18431
|
+
if (Array.isArray(input)) {
|
|
18432
|
+
if (depth > MAX_DEPTH) {
|
|
18433
|
+
truncated = true;
|
|
18434
|
+
return DEPTH_MARKER;
|
|
18435
|
+
}
|
|
18436
|
+
const kept = input.length > MAX_ARRAY_ITEMS ? input.slice(-MAX_ARRAY_ITEMS) : input;
|
|
18437
|
+
if (kept.length !== input.length)
|
|
18438
|
+
truncated = true;
|
|
18439
|
+
return kept.map((item) => walk(item, depth + 1));
|
|
18440
|
+
}
|
|
18441
|
+
if (input !== null && typeof input === "object") {
|
|
18442
|
+
if (depth > MAX_DEPTH) {
|
|
18443
|
+
truncated = true;
|
|
18444
|
+
return DEPTH_MARKER;
|
|
18445
|
+
}
|
|
18446
|
+
const entries = Object.entries(input);
|
|
18447
|
+
const kept = entries.length > MAX_OBJECT_KEYS ? entries.slice(0, MAX_OBJECT_KEYS) : entries;
|
|
18448
|
+
if (kept.length !== entries.length)
|
|
18449
|
+
truncated = true;
|
|
18450
|
+
return Object.fromEntries(kept.map(([k, v]) => [k, walk(v, depth + 1)]));
|
|
18451
|
+
}
|
|
18452
|
+
return input;
|
|
18453
|
+
};
|
|
18454
|
+
const bounded = walk(value, 1);
|
|
18455
|
+
return { value: bounded, truncated };
|
|
18456
|
+
}
|
|
18457
|
+
|
|
18458
|
+
// packages/store/src/bodies/mask.ts
|
|
18459
|
+
var ELIDED = "[redacted]";
|
|
18460
|
+
var BEARER = /\b(bearer)\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
18461
|
+
var PREFIXED_KEY = /(?<![A-Za-z0-9_-])(?:sk|ak|pk)-[A-Za-z0-9_-]{8,}/g;
|
|
18462
|
+
var VENDOR_KEY = /(?<![A-Za-z0-9_-])(?:gh[posu]_|github_pat_|AIza|GOCSPX-)[A-Za-z0-9_-]{8,}/g;
|
|
18463
|
+
var VENDOR_PREFIX = /^(?:gh[posu]_|github_pat_|AIza|GOCSPX-)/;
|
|
18464
|
+
var XAI_KEY = /(?<![A-Za-z0-9_-])xai-[A-Za-z0-9_]{8,}/g;
|
|
18465
|
+
var OPAQUE = /[A-Za-z0-9_-]{41,}/g;
|
|
18466
|
+
var MASK_RULES = [
|
|
18467
|
+
{
|
|
18468
|
+
id: "bearer",
|
|
18469
|
+
pattern: BEARER,
|
|
18470
|
+
keep: (match) => match.length - match.replace(/^bearer\s+/i, "").length
|
|
18471
|
+
},
|
|
18472
|
+
{ id: "prefixedKey", pattern: PREFIXED_KEY, keep: () => 3 },
|
|
18473
|
+
{
|
|
18474
|
+
id: "vendorKey",
|
|
18475
|
+
pattern: VENDOR_KEY,
|
|
18476
|
+
keep: (match) => VENDOR_PREFIX.exec(match)?.[0].length ?? 0
|
|
18477
|
+
},
|
|
18478
|
+
{ id: "xaiKey", pattern: XAI_KEY, keep: () => "xai-".length },
|
|
18479
|
+
{ id: "opaque", pattern: OPAQUE, keep: () => 0 }
|
|
18480
|
+
];
|
|
18481
|
+
function maskString(value) {
|
|
18482
|
+
return MASK_RULES.reduce((masked, rule) => masked.replace(rule.pattern, (match) => `${match.slice(0, rule.keep(match))}${ELIDED}`), value);
|
|
18483
|
+
}
|
|
18484
|
+
function maskSecrets(value) {
|
|
18485
|
+
if (typeof value === "string")
|
|
18486
|
+
return maskString(value);
|
|
18487
|
+
if (Array.isArray(value))
|
|
18488
|
+
return value.map(maskSecrets);
|
|
18489
|
+
if (value !== null && typeof value === "object") {
|
|
18490
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskSecrets(v)]));
|
|
18491
|
+
}
|
|
18492
|
+
return value;
|
|
18493
|
+
}
|
|
18494
|
+
|
|
18495
|
+
// packages/store/src/bodies/artifact.ts
|
|
18496
|
+
var ARTIFACT_SCHEMA_VERSION = 1;
|
|
18497
|
+
var MAX_ARTIFACT_BYTES = 512 * 1024;
|
|
18498
|
+
var BODY_ROW_CAP = 1e5;
|
|
18499
|
+
var BODIES_DIRNAME = "request_bodies";
|
|
18500
|
+
var encoder2 = new TextEncoder;
|
|
18501
|
+
function bodiesDirFor(databasePath) {
|
|
18502
|
+
return join(dirname(databasePath), BODIES_DIRNAME);
|
|
18503
|
+
}
|
|
18504
|
+
var SAFE_REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
18505
|
+
function isSafeRequestId(id) {
|
|
18506
|
+
return SAFE_REQUEST_ID.test(id);
|
|
18507
|
+
}
|
|
18508
|
+
function relPathFor(requestId, at) {
|
|
18509
|
+
const date5 = new Date(at);
|
|
18510
|
+
const yyyy = String(date5.getUTCFullYear()).padStart(4, "0");
|
|
18511
|
+
const mm = String(date5.getUTCMonth() + 1).padStart(2, "0");
|
|
18512
|
+
const dd = String(date5.getUTCDate()).padStart(2, "0");
|
|
18513
|
+
return `${yyyy}/${mm}/${dd}/${requestId}.json.enc`;
|
|
18514
|
+
}
|
|
18515
|
+
async function sha256Hex(bytes) {
|
|
18516
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
18517
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
18518
|
+
}
|
|
18519
|
+
function bodyPair(pair) {
|
|
18520
|
+
const request2 = boundValue(maskSecrets(pair.request));
|
|
18521
|
+
const response = boundValue(maskSecrets(pair.response));
|
|
18522
|
+
return {
|
|
18523
|
+
request: request2.value,
|
|
18524
|
+
response: response.value,
|
|
18525
|
+
truncated: pair.truncated === true || request2.truncated || response.truncated
|
|
18526
|
+
};
|
|
18527
|
+
}
|
|
18528
|
+
function omission(serializedBytes) {
|
|
18529
|
+
return {
|
|
18530
|
+
omitted: true,
|
|
18531
|
+
reason: `artifact exceeded ${MAX_ARTIFACT_BYTES} bytes after structural bounding`,
|
|
18532
|
+
serializedBytes
|
|
18533
|
+
};
|
|
18534
|
+
}
|
|
18535
|
+
function omitBodies(artifact, marker) {
|
|
18536
|
+
return {
|
|
18537
|
+
...artifact,
|
|
18538
|
+
client: { request: marker, response: marker, truncated: true },
|
|
18539
|
+
attempts: artifact.attempts.map((attempt) => ({
|
|
18540
|
+
attempt: attempt.attempt,
|
|
18541
|
+
provider: attempt.provider,
|
|
18542
|
+
request: marker,
|
|
18543
|
+
response: marker,
|
|
18544
|
+
streamChunks: null,
|
|
18545
|
+
truncated: true
|
|
18546
|
+
}))
|
|
18547
|
+
};
|
|
18548
|
+
}
|
|
18549
|
+
function prepareArtifact(input) {
|
|
18550
|
+
const client = bodyPair(input.client);
|
|
18551
|
+
const attempts = input.attempts.map((attempt) => {
|
|
18552
|
+
const pair = bodyPair(attempt);
|
|
18553
|
+
const chunks = attempt.streamChunks === null ? null : boundValue(attempt.streamChunks.map(maskString));
|
|
18554
|
+
return {
|
|
18555
|
+
attempt: attempt.attempt,
|
|
18556
|
+
provider: attempt.provider,
|
|
18557
|
+
request: pair.request,
|
|
18558
|
+
response: pair.response,
|
|
18559
|
+
streamChunks: chunks === null ? null : chunks.value,
|
|
18560
|
+
truncated: pair.truncated || (chunks?.truncated ?? false)
|
|
18561
|
+
};
|
|
18562
|
+
});
|
|
18563
|
+
const error51 = boundValue(maskSecrets(input.error));
|
|
18564
|
+
const bounded = {
|
|
18565
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
18566
|
+
requestId: input.requestId,
|
|
18567
|
+
at: input.at,
|
|
18568
|
+
client,
|
|
18569
|
+
attempts,
|
|
18570
|
+
error: error51.value
|
|
18571
|
+
};
|
|
18572
|
+
const json7 = JSON.stringify(bounded);
|
|
18573
|
+
const size = encoder2.encode(json7).length;
|
|
18574
|
+
if (size <= MAX_ARTIFACT_BYTES)
|
|
18575
|
+
return { artifact: bounded, json: json7 };
|
|
18576
|
+
const marker = omission(size);
|
|
18577
|
+
const omitted = omitBodies(bounded, marker);
|
|
18578
|
+
const omittedJson = JSON.stringify(omitted);
|
|
18579
|
+
if (encoder2.encode(omittedJson).length <= MAX_ARTIFACT_BYTES) {
|
|
18580
|
+
return { artifact: omitted, json: omittedJson };
|
|
18581
|
+
}
|
|
18582
|
+
const stripped = { ...omitted, error: marker };
|
|
18583
|
+
return { artifact: stripped, json: JSON.stringify(stripped) };
|
|
18584
|
+
}
|
|
18585
|
+
async function sealArtifact(key, json7) {
|
|
18586
|
+
const bytes = encoder2.encode(await encrypt(key, json7));
|
|
18587
|
+
return { bytes, sha256: await sha256Hex(bytes) };
|
|
18588
|
+
}
|
|
18589
|
+
async function readArtifact(key, dir, relPath, expectedSha256) {
|
|
18590
|
+
let bytes;
|
|
18591
|
+
try {
|
|
18592
|
+
bytes = new Uint8Array(await readFile(join(dir, relPath)));
|
|
18593
|
+
} catch {
|
|
18594
|
+
return { ok: false, failure: "missing" };
|
|
18595
|
+
}
|
|
18596
|
+
try {
|
|
18597
|
+
if (expectedSha256 !== null && await sha256Hex(bytes) !== expectedSha256) {
|
|
18598
|
+
return { ok: false, failure: "corrupt" };
|
|
18599
|
+
}
|
|
18600
|
+
const parsed = JSON.parse(await decrypt(key, new TextDecoder().decode(bytes)));
|
|
18601
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18602
|
+
return { ok: false, failure: "corrupt" };
|
|
18603
|
+
}
|
|
18604
|
+
return { ok: true, artifact: parsed };
|
|
18605
|
+
} catch {
|
|
18606
|
+
return { ok: false, failure: "corrupt" };
|
|
18607
|
+
}
|
|
18608
|
+
}
|
|
18609
|
+
async function writeArtifact(dir, relPath, bytes) {
|
|
18610
|
+
const full = join(dir, relPath);
|
|
18611
|
+
await mkdir(dirname(full), { recursive: true });
|
|
18612
|
+
await writeFile(full, bytes);
|
|
18613
|
+
}
|
|
18614
|
+
async function deleteArtifact(dir, relPath) {
|
|
18615
|
+
const full = join(dir, relPath);
|
|
18616
|
+
try {
|
|
18617
|
+
await unlink(full);
|
|
18618
|
+
} catch {
|
|
18619
|
+
return;
|
|
18620
|
+
}
|
|
18621
|
+
let parent = dirname(full);
|
|
18622
|
+
while (parent.length > dir.length && parent.startsWith(dir + sep)) {
|
|
18623
|
+
try {
|
|
18624
|
+
await rmdir(parent);
|
|
18625
|
+
} catch {
|
|
18626
|
+
return;
|
|
18627
|
+
}
|
|
18628
|
+
parent = dirname(parent);
|
|
18629
|
+
}
|
|
18630
|
+
}
|
|
18631
|
+
async function listArtifacts(dir) {
|
|
18632
|
+
const out = [];
|
|
18633
|
+
const walk = async (current, prefix) => {
|
|
18634
|
+
let entries;
|
|
18635
|
+
try {
|
|
18636
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
18637
|
+
} catch {
|
|
18638
|
+
return;
|
|
18639
|
+
}
|
|
18640
|
+
for (const entry of entries) {
|
|
18641
|
+
const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
18642
|
+
if (entry.isDirectory())
|
|
18643
|
+
await walk(join(current, entry.name), rel);
|
|
18644
|
+
else if (entry.isFile())
|
|
18645
|
+
out.push(rel);
|
|
18646
|
+
}
|
|
18647
|
+
};
|
|
18648
|
+
await walk(dir, "");
|
|
18649
|
+
return out;
|
|
18650
|
+
}
|
|
18651
|
+
// packages/store/src/sqlite/bodies.ts
|
|
18652
|
+
function toDetailState(value) {
|
|
18653
|
+
return value === "ready" || value === "missing" || value === "corrupt" ? value : "none";
|
|
18654
|
+
}
|
|
18655
|
+
var toRow = (r) => ({
|
|
18656
|
+
requestId: r.request_id,
|
|
18657
|
+
at: r.at,
|
|
18658
|
+
relPath: r.rel_path,
|
|
18659
|
+
sizeBytes: r.size_bytes,
|
|
18660
|
+
sha256: r.sha256,
|
|
18661
|
+
detailState: toDetailState(r.detail_state),
|
|
18662
|
+
truncated: r.truncated === 1
|
|
18663
|
+
});
|
|
18664
|
+
function createBodyRepo(db, key, dir) {
|
|
18665
|
+
const removeRows = async (rows) => {
|
|
18666
|
+
for (const row of rows) {
|
|
18667
|
+
if (row.relPath !== null)
|
|
18668
|
+
await deleteArtifact(dir, row.relPath);
|
|
18669
|
+
}
|
|
18670
|
+
const remove = db.prepare("DELETE FROM request_bodies WHERE request_id = ?");
|
|
18671
|
+
db.transaction(() => {
|
|
18672
|
+
for (const row of rows)
|
|
18673
|
+
remove.run(row.requestId);
|
|
18674
|
+
})();
|
|
18675
|
+
return rows.length;
|
|
18676
|
+
};
|
|
18677
|
+
const recordState = (requestId, state) => {
|
|
18678
|
+
db.run("UPDATE request_bodies SET detail_state = ? WHERE request_id = ?", [state, requestId]);
|
|
18679
|
+
};
|
|
18680
|
+
return {
|
|
18681
|
+
async put(artifact) {
|
|
18682
|
+
if (!isSafeRequestId(artifact.requestId)) {
|
|
18683
|
+
throw new Error("request id is not safe to use as an artifact path segment");
|
|
18684
|
+
}
|
|
18685
|
+
const prepared = prepareArtifact(artifact);
|
|
18686
|
+
const sealed = await sealArtifact(key, prepared.json);
|
|
18687
|
+
const relPath = relPathFor(artifact.requestId, artifact.at);
|
|
18688
|
+
await writeArtifact(dir, relPath, sealed.bytes);
|
|
18689
|
+
const truncated = prepared.artifact.client.truncated || prepared.artifact.attempts.some((attempt) => attempt.truncated);
|
|
18690
|
+
const row = {
|
|
18691
|
+
requestId: artifact.requestId,
|
|
18692
|
+
at: artifact.at,
|
|
18693
|
+
relPath,
|
|
18694
|
+
sizeBytes: sealed.bytes.length,
|
|
18695
|
+
sha256: sealed.sha256,
|
|
18696
|
+
detailState: "ready",
|
|
18697
|
+
truncated
|
|
18698
|
+
};
|
|
18699
|
+
db.run(`INSERT INTO request_bodies (request_id, at, rel_path, size_bytes, sha256, detail_state, truncated)
|
|
18700
|
+
VALUES (?,?,?,?,?,?,?)
|
|
18701
|
+
ON CONFLICT (request_id) DO UPDATE SET
|
|
18702
|
+
at = excluded.at,
|
|
18703
|
+
rel_path = excluded.rel_path,
|
|
18704
|
+
size_bytes = excluded.size_bytes,
|
|
18705
|
+
sha256 = excluded.sha256,
|
|
18706
|
+
detail_state = excluded.detail_state,
|
|
18707
|
+
truncated = excluded.truncated`, [
|
|
18708
|
+
row.requestId,
|
|
18709
|
+
row.at,
|
|
18710
|
+
row.relPath,
|
|
18711
|
+
row.sizeBytes,
|
|
18712
|
+
row.sha256,
|
|
18713
|
+
row.detailState,
|
|
18714
|
+
row.truncated ? 1 : 0
|
|
18715
|
+
]);
|
|
18716
|
+
return row;
|
|
18717
|
+
},
|
|
18718
|
+
async get(requestId) {
|
|
18719
|
+
const found = db.query("SELECT * FROM request_bodies WHERE request_id = ?").get(requestId);
|
|
18720
|
+
if (found === null)
|
|
18721
|
+
return null;
|
|
18722
|
+
const row = toRow(found);
|
|
18723
|
+
if (row.relPath === null || row.detailState === "none")
|
|
18724
|
+
return { row, artifact: null };
|
|
18725
|
+
const read = await readArtifact(key, dir, row.relPath, row.sha256);
|
|
18726
|
+
if (read.ok) {
|
|
18727
|
+
if (row.detailState !== "ready")
|
|
18728
|
+
recordState(requestId, "ready");
|
|
18729
|
+
return { row: { ...row, detailState: "ready" }, artifact: read.artifact };
|
|
18730
|
+
}
|
|
18731
|
+
recordState(requestId, read.failure);
|
|
18732
|
+
return { row: { ...row, detailState: read.failure }, artifact: null };
|
|
18733
|
+
},
|
|
18734
|
+
async prune(olderThan) {
|
|
18735
|
+
const rows = db.query("SELECT * FROM request_bodies WHERE at < ?").all(olderThan).map(toRow);
|
|
18736
|
+
return removeRows(rows);
|
|
18737
|
+
},
|
|
18738
|
+
async pruneToCap(cap = BODY_ROW_CAP) {
|
|
18739
|
+
const total = db.query("SELECT COUNT(*) AS n FROM request_bodies").get()?.n ?? 0;
|
|
18740
|
+
if (total <= cap)
|
|
18741
|
+
return 0;
|
|
18742
|
+
const rows = db.query("SELECT * FROM request_bodies ORDER BY at ASC, request_id ASC LIMIT ?").all(total - cap).map(toRow);
|
|
18743
|
+
return removeRows(rows);
|
|
18744
|
+
},
|
|
18745
|
+
async sweepOrphans() {
|
|
18746
|
+
const known = new Set(db.query("SELECT rel_path FROM request_bodies WHERE rel_path IS NOT NULL").all().map((r) => r.rel_path));
|
|
18747
|
+
const claimed = db.query("SELECT COUNT(*) AS n FROM request_bodies WHERE rel_path = ?");
|
|
18748
|
+
let removed = 0;
|
|
18749
|
+
for (const rel of await listArtifacts(dir)) {
|
|
18750
|
+
if (known.has(rel))
|
|
18751
|
+
continue;
|
|
18752
|
+
if ((claimed.get(rel)?.n ?? 0) > 0)
|
|
18753
|
+
continue;
|
|
18754
|
+
await deleteArtifact(dir, rel);
|
|
18755
|
+
removed += 1;
|
|
18756
|
+
}
|
|
18757
|
+
return removed;
|
|
18758
|
+
}
|
|
18759
|
+
};
|
|
18760
|
+
}
|
|
18309
18761
|
// packages/store/src/sqlite/config.ts
|
|
18310
18762
|
var SETTINGS_KEY = "settings";
|
|
18311
18763
|
var ADMIN_HASH_KEY = "adminPasswordHash";
|
|
@@ -18361,6 +18813,8 @@ function createConfigRepo(db, emit2 = () => {}) {
|
|
|
18361
18813
|
...DEFAULT_SETTINGS,
|
|
18362
18814
|
...stored,
|
|
18363
18815
|
rtkEnabled: stored.rtkEnabled === true,
|
|
18816
|
+
bodyLoggingEnabled: stored.bodyLoggingEnabled === true,
|
|
18817
|
+
bodyLoggingCaptureStreamChunks: stored.bodyLoggingCaptureStreamChunks === true,
|
|
18364
18818
|
weights: knownWeights(stored.weights)
|
|
18365
18819
|
};
|
|
18366
18820
|
} catch {
|
|
@@ -18391,6 +18845,29 @@ function createConfigRepo(db, emit2 = () => {}) {
|
|
|
18391
18845
|
};
|
|
18392
18846
|
}
|
|
18393
18847
|
// packages/store/src/sqlite/credentials.ts
|
|
18848
|
+
function toHealth(r) {
|
|
18849
|
+
return {
|
|
18850
|
+
credentialId: r.credential_id,
|
|
18851
|
+
model: r.model,
|
|
18852
|
+
breakerState: r.breaker_state,
|
|
18853
|
+
consecutiveFailures: r.consecutive_failures,
|
|
18854
|
+
openedAt: r.opened_at,
|
|
18855
|
+
rateLimitedUntil: r.rate_limited_until,
|
|
18856
|
+
ewmaTtftMs: r.ewma_ttft_ms,
|
|
18857
|
+
lastUsedAt: r.last_used_at
|
|
18858
|
+
};
|
|
18859
|
+
}
|
|
18860
|
+
var UPSERT_HEALTH = `INSERT INTO credential_health
|
|
18861
|
+
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
18862
|
+
rate_limited_until, ewma_ttft_ms, last_used_at)
|
|
18863
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
18864
|
+
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
18865
|
+
breaker_state = excluded.breaker_state,
|
|
18866
|
+
consecutive_failures = excluded.consecutive_failures,
|
|
18867
|
+
opened_at = excluded.opened_at,
|
|
18868
|
+
rate_limited_until = excluded.rate_limited_until,
|
|
18869
|
+
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
18870
|
+
last_used_at = excluded.last_used_at`;
|
|
18394
18871
|
function createCredentialRepo(db, key, emit2 = () => {}) {
|
|
18395
18872
|
const open = async (v) => v === null ? null : decrypt(key, v);
|
|
18396
18873
|
const secretsFrom = async (row) => ({
|
|
@@ -18565,29 +19042,10 @@ function createCredentialRepo(db, key, emit2 = () => {}) {
|
|
|
18565
19042
|
emit2({ type: "credentialsChanged" });
|
|
18566
19043
|
},
|
|
18567
19044
|
async listHealth() {
|
|
18568
|
-
return db.query("SELECT * FROM credential_health").all().map(
|
|
18569
|
-
credentialId: r.credential_id,
|
|
18570
|
-
model: r.model,
|
|
18571
|
-
breakerState: r.breaker_state,
|
|
18572
|
-
consecutiveFailures: r.consecutive_failures,
|
|
18573
|
-
openedAt: r.opened_at,
|
|
18574
|
-
rateLimitedUntil: r.rate_limited_until,
|
|
18575
|
-
ewmaTtftMs: r.ewma_ttft_ms,
|
|
18576
|
-
lastUsedAt: r.last_used_at
|
|
18577
|
-
}));
|
|
19045
|
+
return db.query("SELECT * FROM credential_health").all().map(toHealth);
|
|
18578
19046
|
},
|
|
18579
19047
|
async saveHealth(rows) {
|
|
18580
|
-
const stmt = db.prepare(
|
|
18581
|
-
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
18582
|
-
rate_limited_until, ewma_ttft_ms, last_used_at)
|
|
18583
|
-
VALUES (?,?,?,?,?,?,?,?)
|
|
18584
|
-
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
18585
|
-
breaker_state = excluded.breaker_state,
|
|
18586
|
-
consecutive_failures = excluded.consecutive_failures,
|
|
18587
|
-
opened_at = excluded.opened_at,
|
|
18588
|
-
rate_limited_until = excluded.rate_limited_until,
|
|
18589
|
-
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
18590
|
-
last_used_at = excluded.last_used_at`);
|
|
19048
|
+
const stmt = db.prepare(UPSERT_HEALTH);
|
|
18591
19049
|
db.transaction(() => {
|
|
18592
19050
|
for (const r of rows) {
|
|
18593
19051
|
stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
|
|
@@ -18595,6 +19053,18 @@ function createCredentialRepo(db, key, emit2 = () => {}) {
|
|
|
18595
19053
|
})();
|
|
18596
19054
|
emit2({ type: "healthSaved", rows });
|
|
18597
19055
|
},
|
|
19056
|
+
async updateHealth(credentialId, model, apply) {
|
|
19057
|
+
const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
|
|
19058
|
+
const stmt = db.prepare(UPSERT_HEALTH);
|
|
19059
|
+
const written = db.transaction(() => {
|
|
19060
|
+
const row = current.get(credentialId, model);
|
|
19061
|
+
const next = apply(row === null ? null : toHealth(row));
|
|
19062
|
+
stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil, next.ewmaTtftMs, next.lastUsedAt);
|
|
19063
|
+
return next;
|
|
19064
|
+
})();
|
|
19065
|
+
emit2({ type: "healthSaved", rows: [written] });
|
|
19066
|
+
return written;
|
|
19067
|
+
},
|
|
18598
19068
|
async listQuota() {
|
|
18599
19069
|
return db.query("SELECT * FROM quota_windows").all().map((r) => ({
|
|
18600
19070
|
credentialId: r.credential_id,
|
|
@@ -18906,7 +19376,219 @@ CREATE INDEX quota_samples_observed ON quota_samples (observed_at);
|
|
|
18906
19376
|
ALTER TABLE quota_windows ADD COLUMN window_ms INTEGER;
|
|
18907
19377
|
`;
|
|
18908
19378
|
|
|
19379
|
+
// packages/store/src/sqlite/migrations/008_body_logging.sql
|
|
19380
|
+
var _008_body_logging_default = `-- Captured request and response bodies, and the api key opt-out that suppresses
|
|
19381
|
+
-- them.
|
|
19382
|
+
--
|
|
19383
|
+
-- The row holds a pointer, never a body. Bodies are the largest thing this
|
|
19384
|
+
-- gateway could ever store \u2014 a single conversation with a pasted file in it
|
|
19385
|
+
-- dwarfs the whole of \`request_logs\` \u2014 and SQLite would carry every one of them
|
|
19386
|
+
-- through the same page cache, the same WAL, and the same backup that the
|
|
19387
|
+
-- routing tables live in. A prompt corpus inlined here would make the operating
|
|
19388
|
+
-- database grow without bound, make every \`VACUUM\` copy it, and put plaintext
|
|
19389
|
+
-- rows one \`sqlite3\` invocation away. The artifact instead lives at
|
|
19390
|
+
-- \`<dirname(databasePath)>/request_bodies/YYYY/MM/DD/<request_id>.json.enc\`,
|
|
19391
|
+
-- encrypted under the same key as provider credentials, and this row records
|
|
19392
|
+
-- only where it is and whether it can still be trusted. OmniRoute reached the
|
|
19393
|
+
-- same layout the expensive way: its \`call_logs_v1_legacy\` table exists purely
|
|
19394
|
+
-- to drain inline blobs it had already shipped into file artifacts.
|
|
19395
|
+
--
|
|
19396
|
+
-- \`rel_path\` is relative so moving an installation does not invalidate every
|
|
19397
|
+
-- row, and nullable because a row can exist with no artifact behind it: capture
|
|
19398
|
+
-- may have been suppressed, or the file may have been swept.
|
|
19399
|
+
--
|
|
19400
|
+
-- \`sha256\` is taken over the bytes as stored \u2014 the ciphertext \u2014 not over the
|
|
19401
|
+
-- plaintext. Truncation or bit-rot on disk is then detectable by a reader that
|
|
19402
|
+
-- does not hold \`OMNI_ENCRYPTION_KEY\` at all, which is what makes \`corrupt\` a
|
|
19403
|
+
-- state the reader can report rather than an exception it has to raise.
|
|
19404
|
+
--
|
|
19405
|
+
-- \`detail_state\` is one of \`none\`, \`ready\`, \`missing\`, or \`corrupt\`. A file tree
|
|
19406
|
+
-- and a table it is not written to transactionally will drift \u2014 a crash between
|
|
19407
|
+
-- the two writes is enough \u2014 so the reader records what it observed and hands
|
|
19408
|
+
-- back the metadata. \`missing\` and \`corrupt\` are answers, not failures.
|
|
19409
|
+
--
|
|
19410
|
+
-- \`truncated\` is set when structural bounding altered anything, so a reader can
|
|
19411
|
+
-- say "this is not the whole payload" without diffing it against nothing.
|
|
19412
|
+
--
|
|
19413
|
+
-- No foreign key to \`request_logs\`. Expiry is performed explicitly, deleting the
|
|
19414
|
+
-- file and the row together, because \`ON DELETE CASCADE\` only fires while the
|
|
19415
|
+
-- \`foreign_keys\` pragma is on: a pragma silently off would turn expiry of a
|
|
19416
|
+
-- prompt corpus into indefinite retention of one, and that failure is invisible
|
|
19417
|
+
-- until someone goes looking.
|
|
19418
|
+
CREATE TABLE request_bodies (
|
|
19419
|
+
request_id TEXT PRIMARY KEY,
|
|
19420
|
+
at INTEGER NOT NULL,
|
|
19421
|
+
rel_path TEXT,
|
|
19422
|
+
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
19423
|
+
sha256 TEXT,
|
|
19424
|
+
detail_state TEXT NOT NULL DEFAULT 'none',
|
|
19425
|
+
truncated INTEGER NOT NULL DEFAULT 0
|
|
19426
|
+
);
|
|
19427
|
+
|
|
19428
|
+
-- Descending, because every read of this table is time-ordered from the newest
|
|
19429
|
+
-- end: the retention sweep walks the oldest rows and the row cap walks them in
|
|
19430
|
+
-- the same direction, and the console only ever asks about recent requests.
|
|
19431
|
+
CREATE INDEX idx_request_bodies_at ON request_bodies (at DESC);
|
|
19432
|
+
|
|
19433
|
+
-- Per-key suppression, checked before any capture work begins.
|
|
19434
|
+
--
|
|
19435
|
+
-- A shared installation can be serving one client whose payloads must not be
|
|
19436
|
+
-- retained while capturing everything else, and that client's operator cannot be
|
|
19437
|
+
-- asked to trust a global switch they do not control. Default 0 keeps every
|
|
19438
|
+
-- existing key on the installation-wide setting, which is itself off by default.
|
|
19439
|
+
ALTER TABLE api_keys ADD COLUMN body_logging_opt_out INTEGER NOT NULL DEFAULT 0;
|
|
19440
|
+
`;
|
|
19441
|
+
|
|
19442
|
+
// packages/store/src/sqlite/migrations/009_key_limits.sql
|
|
19443
|
+
var _009_key_limits_default = `-- Per-key limits as a sparse \`(dimension, window)\` matrix, replacing the single
|
|
19444
|
+
-- requests-per-minute integer.
|
|
19445
|
+
--
|
|
19446
|
+
-- One JSON column rather than a column per pair, because the matrix is sparse
|
|
19447
|
+
-- and the pairs are not fixed: \`requests\` and \`tokens\` are meaningful at all
|
|
19448
|
+
-- three windows, \`spend\` only at the two long ones, and \`concurrency\` is a gauge
|
|
19449
|
+
-- with no window at all. Twelve mostly-null integer columns would encode the
|
|
19450
|
+
-- same thing while making every future pair a migration.
|
|
19451
|
+
--
|
|
19452
|
+
-- The JSON keys are a storage contract in the same class as \`RTK_FILTER_IDS\`:
|
|
19453
|
+
-- the dimension and window names are persisted in every row, so adding a name is
|
|
19454
|
+
-- free and renaming or removing one loses data. Unlike RTK ids, which are
|
|
19455
|
+
-- dropped silently on read, an unknown limit key is a parse failure \u2014 a limit
|
|
19456
|
+
-- the gateway cannot understand must never be read as "no limit", because that
|
|
19457
|
+
-- fails open on a control the operator explicitly set.
|
|
19458
|
+
--
|
|
19459
|
+
-- \`NOT NULL DEFAULT '{}'\` rather than nullable, so "unlimited" has exactly one
|
|
19460
|
+
-- spelling at the column level and every reader parses the same shape.
|
|
19461
|
+
ALTER TABLE api_keys ADD COLUMN limits TEXT NOT NULL DEFAULT '{}';
|
|
19462
|
+
|
|
19463
|
+
-- The old ceiling was requests per minute and nothing else, so it lands whole in
|
|
19464
|
+
-- \`requests["1m"]\`. A NULL meant unlimited and stays unlimited as the default
|
|
19465
|
+
-- \`{}\`: an absent key and an explicit null both mean the same thing, and the
|
|
19466
|
+
-- empty object is the shape a newly minted key starts at.
|
|
19467
|
+
--
|
|
19468
|
+
-- Only a value the reader accepts is carried over. \`rate_limit_per_min\` was
|
|
19469
|
+
-- \`INTEGER\` with no \`CHECK\`, so a hand-edited install can hold \`0\`, \`-5\` or
|
|
19470
|
+
-- \`1.5\` in it, while the new schema is \`z.number().int().positive()\`. Backfilled
|
|
19471
|
+
-- unconditionally, each of those writes a matrix \`parseLimitConfig\` refuses \u2014
|
|
19472
|
+
-- which is \`limits: null\`, which is every request for that key answered
|
|
19473
|
+
-- \`INTERNAL\` at the auth chokepoint. An upgrade must not manufacture the
|
|
19474
|
+
-- unreadable row the design describes as arising only from meddling.
|
|
19475
|
+
--
|
|
19476
|
+
-- Anything else stays at the \`{}\` default, which is unlimited: a ceiling that
|
|
19477
|
+
-- was already nonsense bounded nothing before this migration either, so
|
|
19478
|
+
-- dropping it changes no behaviour and leaves the key serving.
|
|
19479
|
+
--
|
|
19480
|
+
-- \`typeof()\` rather than a range test alone. SQLite's INTEGER affinity is a
|
|
19481
|
+
-- preference, not a constraint: \`1.5\` stays REAL and \`'sixty'\` stays TEXT, and
|
|
19482
|
+
-- TEXT sorts above every number so a bare \`> 0\` would admit the string.
|
|
19483
|
+
UPDATE api_keys
|
|
19484
|
+
SET limits = json_object('requests', json_object('1m', rate_limit_per_min))
|
|
19485
|
+
WHERE typeof(rate_limit_per_min) = 'integer'
|
|
19486
|
+
AND rate_limit_per_min > 0;
|
|
19487
|
+
|
|
19488
|
+
ALTER TABLE api_keys DROP COLUMN rate_limit_per_min;
|
|
19489
|
+
|
|
19490
|
+
-- Correctness-adjacent, not an optimisation, and it must not be dropped later as
|
|
19491
|
+
-- redundant with \`idx_request_logs_at\`.
|
|
19492
|
+
--
|
|
19493
|
+
-- \`request_logs\` leads its existing indexes with \`at\` and with \`credential_id\`;
|
|
19494
|
+
-- nothing leads with \`api_key_id\`. A sliding weekly sum for one key against
|
|
19495
|
+
-- \`idx_request_logs_at\` scans every row in the week for every key on the
|
|
19496
|
+
-- install, and that scan sits on the request hot path.
|
|
19497
|
+
--
|
|
19498
|
+
-- Composite order matters: \`(api_key_id, at DESC)\` lets the range scan start at
|
|
19499
|
+
-- the key. \`(at DESC, api_key_id)\` does not.
|
|
19500
|
+
CREATE INDEX idx_request_logs_key_at ON request_logs (api_key_id, at DESC);
|
|
19501
|
+
`;
|
|
19502
|
+
|
|
19503
|
+
// packages/store/src/sqlite/migrations/010_usage_rollup.sql
|
|
19504
|
+
var _010_usage_rollup_default = `-- Per-key hourly counters, so a long sliding window is read rather than scanned.
|
|
19505
|
+
--
|
|
19506
|
+
-- \`sumSince\` is on the admission path of every request, and \`bun:sqlite\` is
|
|
19507
|
+
-- synchronous: a \`SELECT SUM\` over a week of one key's rows blocks the entire
|
|
19508
|
+
-- event loop for its duration \u2014 \`/health\`, \`/api/*\`, the quiesce latch, and
|
|
19509
|
+
-- every other key's traffic with it. Measured on one machine, one key, WAL:
|
|
19510
|
+
-- 149ms at 0.2M rows in the window, 2.1s at 2M, 10.8s at 8M. The cost is
|
|
19511
|
+
-- O(accumulated history) and has no ceiling, and the eager-refresh rule makes it
|
|
19512
|
+
-- worst exactly when a key is busiest \u2014 a key inside the last tenth of a long
|
|
19513
|
+
-- ceiling reads through on every admission. The same reads against this table
|
|
19514
|
+
-- are flat at ~0.017ms, because a week is 168 buckets however much traffic each
|
|
19515
|
+
-- one summarises.
|
|
19516
|
+
--
|
|
19517
|
+
-- This reverses the design's own rejection of a counter table, and the reason it
|
|
19518
|
+
-- can be reversed is that this table is *derived*. \`request_logs\` stays the
|
|
19519
|
+
-- source of truth; every figure here is reproducible from it by one grouped
|
|
19520
|
+
-- select. The original objection was that a counter table can disagree with the
|
|
19521
|
+
-- log and nobody can tell which is right \u2014 that objection is answered by there
|
|
19522
|
+
-- being a rebuild, a restore that runs it, and a \`doctor\` check that compares
|
|
19523
|
+
-- the two. It would not be answered by care.
|
|
19524
|
+
--
|
|
19525
|
+
-- \`hour\` is \`at / 3600000\`, floored, and every SQL side spells that
|
|
19526
|
+
-- \`CAST(at / 3600000 AS INTEGER)\`. The cast is not decoration: nothing validates
|
|
19527
|
+
-- \`at\`, and SQLite's \`/\` is integer division only when both operands are
|
|
19528
|
+
-- integers, so a fractional \`at\` would give this INTEGER column a REAL key that
|
|
19529
|
+
-- no later integer-hour write ever merges with. Truncation toward zero and
|
|
19530
|
+
-- JavaScript's \`Math.floor\` then agree on every epoch this side of 1970, which
|
|
19531
|
+
-- is every epoch a request log holds.
|
|
19532
|
+
--
|
|
19533
|
+
-- \`api_key_id\` is NOT NULL and anonymous rows are left out: a WITHOUT ROWID
|
|
19534
|
+
-- primary key cannot hold a NULL, and nothing reads this table except a per-key
|
|
19535
|
+
-- lookup that could never match one.
|
|
19536
|
+
CREATE TABLE usage_rollup (
|
|
19537
|
+
api_key_id TEXT NOT NULL,
|
|
19538
|
+
hour INTEGER NOT NULL,
|
|
19539
|
+
requests INTEGER NOT NULL DEFAULT 0,
|
|
19540
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19541
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19542
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19543
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19544
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
19545
|
+
PRIMARY KEY (api_key_id, hour)
|
|
19546
|
+
) WITHOUT ROWID;
|
|
19547
|
+
|
|
19548
|
+
-- The backfill is this migration's \`after\` hook rather than an INSERT here, and
|
|
19549
|
+
-- it is the same \`rebuildRollup\` a restore runs. Seeding an existing install and
|
|
19550
|
+
-- repairing a suspect one are the same statement, so they are the same code and
|
|
19551
|
+
-- cannot drift into disagreeing about which rows count.
|
|
19552
|
+
`;
|
|
19553
|
+
|
|
19554
|
+
// packages/store/src/sqlite/migrations/011_plugin_migrations.sql
|
|
19555
|
+
var _011_plugin_migrations_default = `-- The plugin migration ledger, kept deliberately apart from core's \`migrations\`.
|
|
19556
|
+
--
|
|
19557
|
+
-- Core's track is a single ascending integer owned by this repository. A plugin
|
|
19558
|
+
-- ships its own numbered migrations and knows nothing about ours, so the two
|
|
19559
|
+
-- cannot share a counter: if they did, installing a plugin would consume numbers
|
|
19560
|
+
-- that core's next migration then collides with, and the collision would surface
|
|
19561
|
+
-- as "migration already applied" on an upgrade rather than as anything legible.
|
|
19562
|
+
-- Core's next migration is 012 regardless of how many migrations any plugin
|
|
19563
|
+
-- ships, and a plugin's version 1 is its own version 1 forever.
|
|
19564
|
+
--
|
|
19565
|
+
-- The primary key is (plugin_id, version) rather than a surrogate id because
|
|
19566
|
+
-- that pair *is* the identity: applying the same version twice is the thing this
|
|
19567
|
+
-- table exists to prevent, and a uniqueness constraint states it once instead of
|
|
19568
|
+
-- leaving it to whichever code path happens to check first.
|
|
19569
|
+
--
|
|
19570
|
+
-- \`applied_at\` is milliseconds, matching every other timestamp in this schema.
|
|
19571
|
+
-- It is recorded for an operator reading the table during an incident, not read
|
|
19572
|
+
-- by the host: the host asks only whether a row exists.
|
|
19573
|
+
--
|
|
19574
|
+
-- Note what is *not* here: no row for the plugin's tables, and no cascade. A
|
|
19575
|
+
-- plugin's tables outlive its rows here on purpose \u2014 see \`PluginRepo.dropAll\`
|
|
19576
|
+
-- and \`orphanTables\`. Dropping a table because a plugin is currently absent
|
|
19577
|
+
-- destroys data irreversibly, and a restore is precisely the moment a plugin is
|
|
19578
|
+
-- most likely to be absent.
|
|
19579
|
+
CREATE TABLE plugin_migrations (
|
|
19580
|
+
plugin_id TEXT NOT NULL,
|
|
19581
|
+
version INTEGER NOT NULL,
|
|
19582
|
+
applied_at INTEGER NOT NULL,
|
|
19583
|
+
PRIMARY KEY (plugin_id, version)
|
|
19584
|
+
);
|
|
19585
|
+
`;
|
|
19586
|
+
|
|
18909
19587
|
// packages/store/src/sqlite/rollup.ts
|
|
19588
|
+
var HOUR_MS = 3600000;
|
|
19589
|
+
function hourOf(at) {
|
|
19590
|
+
return Math.floor(at / HOUR_MS);
|
|
19591
|
+
}
|
|
18910
19592
|
function startOfLocalDay(at) {
|
|
18911
19593
|
const day = new Date(at);
|
|
18912
19594
|
day.setHours(0, 0, 0, 0);
|
|
@@ -19015,6 +19697,88 @@ function backfillDaily(db) {
|
|
|
19015
19697
|
upsert(db, group.key, group.counters);
|
|
19016
19698
|
return groups.size;
|
|
19017
19699
|
}
|
|
19700
|
+
var HOURLY_UPSERT = `
|
|
19701
|
+
INSERT INTO usage_rollup
|
|
19702
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
19703
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
19704
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
19705
|
+
ON CONFLICT (api_key_id, hour)
|
|
19706
|
+
DO UPDATE SET
|
|
19707
|
+
requests = requests + excluded.requests,
|
|
19708
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
19709
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
19710
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
19711
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
19712
|
+
cost_usd = cost_usd + excluded.cost_usd`;
|
|
19713
|
+
function rollupHour(db, log) {
|
|
19714
|
+
const apiKeyId = log.apiKeyId;
|
|
19715
|
+
if (apiKeyId === null)
|
|
19716
|
+
return;
|
|
19717
|
+
db.run(HOURLY_UPSERT, [
|
|
19718
|
+
apiKeyId,
|
|
19719
|
+
hourOf(log.at),
|
|
19720
|
+
1,
|
|
19721
|
+
log.inputTokens,
|
|
19722
|
+
log.outputTokens,
|
|
19723
|
+
log.cacheReadTokens,
|
|
19724
|
+
log.cacheWriteTokens,
|
|
19725
|
+
log.costUsd
|
|
19726
|
+
]);
|
|
19727
|
+
}
|
|
19728
|
+
var REBUILD = `
|
|
19729
|
+
INSERT INTO usage_rollup
|
|
19730
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
19731
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
19732
|
+
SELECT api_key_id,
|
|
19733
|
+
CAST(at / 3600000 AS INTEGER),
|
|
19734
|
+
COUNT(*),
|
|
19735
|
+
COALESCE(SUM(input_tokens), 0),
|
|
19736
|
+
COALESCE(SUM(output_tokens), 0),
|
|
19737
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
19738
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
19739
|
+
COALESCE(SUM(cost_usd), 0)
|
|
19740
|
+
FROM request_logs
|
|
19741
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
19742
|
+
GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
|
|
19743
|
+
function rebuildRollup(db) {
|
|
19744
|
+
db.run("DELETE FROM usage_rollup");
|
|
19745
|
+
db.run(REBUILD);
|
|
19746
|
+
}
|
|
19747
|
+
function auditRollup(db) {
|
|
19748
|
+
const row = db.query(`WITH truth AS (
|
|
19749
|
+
SELECT api_key_id AS k,
|
|
19750
|
+
CAST(at / 3600000 AS INTEGER) AS h,
|
|
19751
|
+
COUNT(*) AS requests,
|
|
19752
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
19753
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
19754
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
19755
|
+
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
19756
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
19757
|
+
FROM request_logs
|
|
19758
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
19759
|
+
GROUP BY k, h
|
|
19760
|
+
)
|
|
19761
|
+
SELECT (SELECT COUNT(*) FROM truth) AS buckets,
|
|
19762
|
+
(SELECT COUNT(*)
|
|
19763
|
+
FROM truth
|
|
19764
|
+
LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
19765
|
+
WHERE r.api_key_id IS NULL
|
|
19766
|
+
OR r.requests <> truth.requests
|
|
19767
|
+
OR r.input_tokens <> truth.input_tokens
|
|
19768
|
+
OR r.output_tokens <> truth.output_tokens
|
|
19769
|
+
OR r.cache_read_tokens <> truth.cache_read_tokens
|
|
19770
|
+
OR r.cache_write_tokens <> truth.cache_write_tokens
|
|
19771
|
+
OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
|
|
19772
|
+
) AS mismatched,
|
|
19773
|
+
(SELECT COUNT(*)
|
|
19774
|
+
FROM usage_rollup r
|
|
19775
|
+
LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
19776
|
+
WHERE truth.k IS NULL
|
|
19777
|
+
) AS orphans`).get();
|
|
19778
|
+
const buckets = row?.buckets ?? 0;
|
|
19779
|
+
const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
|
|
19780
|
+
return { buckets, mismatched, ok: mismatched === 0 };
|
|
19781
|
+
}
|
|
19018
19782
|
function backfillRtkUsage(db) {
|
|
19019
19783
|
const groups = new Map;
|
|
19020
19784
|
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
@@ -19051,7 +19815,11 @@ var MIGRATIONS = [
|
|
|
19051
19815
|
{ id: 4, sql: _004_request_state_default },
|
|
19052
19816
|
{ id: 5, sql: _005_rtk_metrics_default },
|
|
19053
19817
|
{ id: 6, sql: _006_rtk_usage_default, after: backfillRtkUsage },
|
|
19054
|
-
{ id: 7, sql: _007_quota_samples_default }
|
|
19818
|
+
{ id: 7, sql: _007_quota_samples_default },
|
|
19819
|
+
{ id: 8, sql: _008_body_logging_default },
|
|
19820
|
+
{ id: 9, sql: _009_key_limits_default },
|
|
19821
|
+
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
|
|
19822
|
+
{ id: 11, sql: _011_plugin_migrations_default }
|
|
19055
19823
|
];
|
|
19056
19824
|
function openDb(path) {
|
|
19057
19825
|
const db = new Database(path, { create: true });
|
|
@@ -19082,44 +19850,290 @@ async function hashApiKey(raw) {
|
|
|
19082
19850
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
|
|
19083
19851
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
19084
19852
|
}
|
|
19085
|
-
|
|
19853
|
+
function parseLimits(id, raw, logger2) {
|
|
19854
|
+
try {
|
|
19855
|
+
const parsed = JSON.parse(raw);
|
|
19856
|
+
return parseLimitConfig(parsed);
|
|
19857
|
+
} catch {
|
|
19858
|
+
logger2.error("api key limits unreadable", { apiKeyId: id });
|
|
19859
|
+
return null;
|
|
19860
|
+
}
|
|
19861
|
+
}
|
|
19862
|
+
var toKey = (r, logger2) => ({
|
|
19086
19863
|
id: r.id,
|
|
19087
19864
|
label: r.label,
|
|
19088
19865
|
prefix: r.prefix,
|
|
19089
19866
|
hash: r.hash,
|
|
19090
19867
|
modelAllowlist: r.model_allowlist === null ? null : JSON.parse(r.model_allowlist),
|
|
19091
|
-
|
|
19868
|
+
limits: parseLimits(r.id, r.limits, logger2),
|
|
19869
|
+
bodyLoggingOptOut: r.body_logging_opt_out === 1,
|
|
19092
19870
|
createdAt: r.created_at,
|
|
19093
19871
|
revokedAt: r.revoked_at
|
|
19094
19872
|
});
|
|
19095
|
-
function createKeyRepo(db) {
|
|
19873
|
+
function createKeyRepo(db, logger2 = noopLogger) {
|
|
19096
19874
|
return {
|
|
19097
19875
|
async list() {
|
|
19098
|
-
return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map(toKey);
|
|
19876
|
+
return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map((row) => toKey(row, logger2));
|
|
19099
19877
|
},
|
|
19100
19878
|
async findByHash(hash3) {
|
|
19101
19879
|
const row = db.query("SELECT * FROM api_keys WHERE hash = ?").get(hash3);
|
|
19102
|
-
return row ? toKey(row) : null;
|
|
19880
|
+
return row ? toKey(row, logger2) : null;
|
|
19103
19881
|
},
|
|
19104
19882
|
async create(input) {
|
|
19105
19883
|
const now = Date.now();
|
|
19106
|
-
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist,
|
|
19107
|
-
|
|
19884
|
+
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
19885
|
+
body_logging_opt_out, created_at, revoked_at)
|
|
19886
|
+
VALUES (?,?,?,?,?,?,?,?,NULL)`, [
|
|
19108
19887
|
input.id,
|
|
19109
19888
|
input.label,
|
|
19110
19889
|
input.prefix,
|
|
19111
19890
|
input.hash,
|
|
19112
19891
|
input.modelAllowlist === null ? null : JSON.stringify(input.modelAllowlist),
|
|
19113
|
-
input.
|
|
19892
|
+
JSON.stringify(parseLimitConfig(input.limits)),
|
|
19893
|
+
input.bodyLoggingOptOut ? 1 : 0,
|
|
19114
19894
|
now
|
|
19115
19895
|
]);
|
|
19116
19896
|
return { ...input, createdAt: now, revokedAt: null };
|
|
19117
19897
|
},
|
|
19898
|
+
async setLimits(id, limits) {
|
|
19899
|
+
db.run("UPDATE api_keys SET limits = ? WHERE id = ?", [
|
|
19900
|
+
JSON.stringify(parseLimitConfig(limits)),
|
|
19901
|
+
id
|
|
19902
|
+
]);
|
|
19903
|
+
},
|
|
19118
19904
|
async revoke(id) {
|
|
19119
19905
|
db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
|
|
19120
19906
|
}
|
|
19121
19907
|
};
|
|
19122
19908
|
}
|
|
19909
|
+
// packages/store/src/sqlite/maintenance.ts
|
|
19910
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
19911
|
+
var REQUIRED_TABLES = [
|
|
19912
|
+
"api_keys",
|
|
19913
|
+
"credential_health",
|
|
19914
|
+
"credentials",
|
|
19915
|
+
"migrations",
|
|
19916
|
+
"quota_windows",
|
|
19917
|
+
"request_logs",
|
|
19918
|
+
"settings",
|
|
19919
|
+
"virtual_models"
|
|
19920
|
+
];
|
|
19921
|
+
var UNREADABLE = "unreadable";
|
|
19922
|
+
function sqlLiteral(value) {
|
|
19923
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
19924
|
+
}
|
|
19925
|
+
function createMaintenanceRepo(db) {
|
|
19926
|
+
return {
|
|
19927
|
+
async stats() {
|
|
19928
|
+
const pageSize = db.query("PRAGMA page_size").get();
|
|
19929
|
+
const pageCount = db.query("PRAGMA page_count").get();
|
|
19930
|
+
const freelist = db.query("PRAGMA freelist_count").get();
|
|
19931
|
+
const version2 = db.query("SELECT MAX(id) AS version FROM migrations").get();
|
|
19932
|
+
return {
|
|
19933
|
+
pageSize: pageSize?.page_size ?? 0,
|
|
19934
|
+
pageCount: pageCount?.page_count ?? 0,
|
|
19935
|
+
freelistCount: freelist?.freelist_count ?? 0,
|
|
19936
|
+
schemaVersion: version2?.version ?? 0
|
|
19937
|
+
};
|
|
19938
|
+
},
|
|
19939
|
+
async vacuum() {
|
|
19940
|
+
db.run("VACUUM");
|
|
19941
|
+
db.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
19942
|
+
},
|
|
19943
|
+
async snapshotTo(path) {
|
|
19944
|
+
db.run(`VACUUM INTO ${sqlLiteral(path)}`);
|
|
19945
|
+
},
|
|
19946
|
+
async inspect(path) {
|
|
19947
|
+
let file2;
|
|
19948
|
+
try {
|
|
19949
|
+
file2 = new Database2(path, { readonly: true });
|
|
19950
|
+
} catch {
|
|
19951
|
+
return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
|
|
19952
|
+
}
|
|
19953
|
+
try {
|
|
19954
|
+
const problems = file2.query("PRAGMA quick_check").all();
|
|
19955
|
+
const quickCheck = problems[0]?.quick_check ?? UNREADABLE;
|
|
19956
|
+
const tables = file2.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' " + "ORDER BY name").all().map((r) => r.name);
|
|
19957
|
+
const present = new Set(tables);
|
|
19958
|
+
const counts = {};
|
|
19959
|
+
for (const table2 of REQUIRED_TABLES) {
|
|
19960
|
+
if (!present.has(table2))
|
|
19961
|
+
continue;
|
|
19962
|
+
const n = file2.query(`SELECT COUNT(*) AS n FROM ${table2}`).get();
|
|
19963
|
+
counts[table2] = n?.n ?? 0;
|
|
19964
|
+
}
|
|
19965
|
+
const complete = REQUIRED_TABLES.every((t) => present.has(t));
|
|
19966
|
+
return { ok: quickCheck === "ok" && complete, quickCheck, tables, counts };
|
|
19967
|
+
} catch {
|
|
19968
|
+
return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
|
|
19969
|
+
} finally {
|
|
19970
|
+
file2.close();
|
|
19971
|
+
}
|
|
19972
|
+
}
|
|
19973
|
+
};
|
|
19974
|
+
}
|
|
19975
|
+
// packages/store/src/sqlite/plugins.ts
|
|
19976
|
+
var PLUGIN_ID = /^[a-z][a-z0-9-]{0,31}$/;
|
|
19977
|
+
var TABLE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
|
|
19978
|
+
var PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
|
|
19979
|
+
var CORE_TABLES = [
|
|
19980
|
+
"api_keys",
|
|
19981
|
+
"credential_health",
|
|
19982
|
+
"credentials",
|
|
19983
|
+
"migrations",
|
|
19984
|
+
"plugin_migrations",
|
|
19985
|
+
"quota_samples",
|
|
19986
|
+
"quota_windows",
|
|
19987
|
+
"request_bodies",
|
|
19988
|
+
"request_logs",
|
|
19989
|
+
"settings",
|
|
19990
|
+
"usage_daily",
|
|
19991
|
+
"usage_rollup",
|
|
19992
|
+
"virtual_models"
|
|
19993
|
+
];
|
|
19994
|
+
var CORE_TABLE_REFERENCE = new RegExp(`\\b(?:${CORE_TABLES.join("|")})\\b`);
|
|
19995
|
+
var PREFIX3 = "plugin_";
|
|
19996
|
+
function stripNoise(sql) {
|
|
19997
|
+
let out = "";
|
|
19998
|
+
let i = 0;
|
|
19999
|
+
while (i < sql.length) {
|
|
20000
|
+
const c = sql[i];
|
|
20001
|
+
const next = sql[i + 1];
|
|
20002
|
+
if (c === "'") {
|
|
20003
|
+
i += 1;
|
|
20004
|
+
while (i < sql.length) {
|
|
20005
|
+
if (sql[i] === "'") {
|
|
20006
|
+
if (sql[i + 1] === "'")
|
|
20007
|
+
i += 2;
|
|
20008
|
+
else
|
|
20009
|
+
break;
|
|
20010
|
+
} else
|
|
20011
|
+
i += 1;
|
|
20012
|
+
}
|
|
20013
|
+
i += 1;
|
|
20014
|
+
out += " ";
|
|
20015
|
+
} else if (c === "-" && next === "-") {
|
|
20016
|
+
while (i < sql.length && sql[i] !== `
|
|
20017
|
+
`)
|
|
20018
|
+
i += 1;
|
|
20019
|
+
out += " ";
|
|
20020
|
+
} else if (c === "/" && next === "*") {
|
|
20021
|
+
i += 2;
|
|
20022
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
|
|
20023
|
+
i += 1;
|
|
20024
|
+
i += 2;
|
|
20025
|
+
out += " ";
|
|
20026
|
+
} else {
|
|
20027
|
+
out += c;
|
|
20028
|
+
i += 1;
|
|
20029
|
+
}
|
|
20030
|
+
}
|
|
20031
|
+
return out;
|
|
20032
|
+
}
|
|
20033
|
+
function tableFor(pluginId, name) {
|
|
20034
|
+
return `"${PREFIX3}${pluginId}_${name}"`;
|
|
20035
|
+
}
|
|
20036
|
+
function prefixFor(pluginId) {
|
|
20037
|
+
return `${PREFIX3}${pluginId}_`;
|
|
20038
|
+
}
|
|
20039
|
+
function assertPluginId(pluginId) {
|
|
20040
|
+
if (!PLUGIN_ID.test(pluginId)) {
|
|
20041
|
+
throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
|
|
20042
|
+
}
|
|
20043
|
+
}
|
|
20044
|
+
function prepare(pluginId, sql) {
|
|
20045
|
+
assertPluginId(pluginId);
|
|
20046
|
+
const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
|
|
20047
|
+
if (!TABLE_NAME.test(name)) {
|
|
20048
|
+
throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
|
|
20049
|
+
}
|
|
20050
|
+
return tableFor(pluginId, name);
|
|
20051
|
+
});
|
|
20052
|
+
const found = CORE_TABLE_REFERENCE.exec(stripNoise(expanded));
|
|
20053
|
+
if (found !== null) {
|
|
20054
|
+
throw new Error(`plugin sql may not reference the core table ${found[0]}`);
|
|
20055
|
+
}
|
|
20056
|
+
return expanded;
|
|
20057
|
+
}
|
|
20058
|
+
function toBinding(value, index) {
|
|
20059
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
|
|
20060
|
+
return value;
|
|
20061
|
+
}
|
|
20062
|
+
throw new Error(`plugin sql parameter ${index} is not a bindable value`);
|
|
20063
|
+
}
|
|
20064
|
+
var toBindings = (params) => (params ?? []).map(toBinding);
|
|
20065
|
+
function createPluginRepo(db) {
|
|
20066
|
+
const pluginTables = () => db.query("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all().map((r) => r.name).filter((name) => name.startsWith(PREFIX3) && name !== "plugin_migrations");
|
|
20067
|
+
const tablesOf = (pluginId) => {
|
|
20068
|
+
const prefix = prefixFor(pluginId);
|
|
20069
|
+
return pluginTables().filter((name) => name.startsWith(prefix));
|
|
20070
|
+
};
|
|
20071
|
+
const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
|
|
20072
|
+
return {
|
|
20073
|
+
migrate(pluginId, migrations) {
|
|
20074
|
+
assertPluginId(pluginId);
|
|
20075
|
+
const done = appliedVersions(pluginId);
|
|
20076
|
+
const applied = [];
|
|
20077
|
+
const ordered = [...migrations].sort((a, b) => a.version - b.version);
|
|
20078
|
+
for (const migration of ordered) {
|
|
20079
|
+
if (done.has(migration.version))
|
|
20080
|
+
continue;
|
|
20081
|
+
try {
|
|
20082
|
+
if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
|
|
20083
|
+
throw new Error("migration version must be a positive integer");
|
|
20084
|
+
}
|
|
20085
|
+
const sql = prepare(pluginId, migration.sql);
|
|
20086
|
+
db.transaction(() => {
|
|
20087
|
+
db.run(sql);
|
|
20088
|
+
db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
|
|
20089
|
+
})();
|
|
20090
|
+
applied.push(migration.version);
|
|
20091
|
+
} catch (error51) {
|
|
20092
|
+
return {
|
|
20093
|
+
applied,
|
|
20094
|
+
failed: {
|
|
20095
|
+
version: migration.version,
|
|
20096
|
+
reason: error51 instanceof Error ? error51.message : String(error51)
|
|
20097
|
+
}
|
|
20098
|
+
};
|
|
20099
|
+
}
|
|
20100
|
+
}
|
|
20101
|
+
return { applied };
|
|
20102
|
+
},
|
|
20103
|
+
run(pluginId, sql, params) {
|
|
20104
|
+
db.run(prepare(pluginId, sql), toBindings(params));
|
|
20105
|
+
},
|
|
20106
|
+
all(pluginId, sql, params) {
|
|
20107
|
+
return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
|
|
20108
|
+
},
|
|
20109
|
+
get(pluginId, sql, params) {
|
|
20110
|
+
const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
|
|
20111
|
+
return row === null || row === undefined ? null : row;
|
|
20112
|
+
},
|
|
20113
|
+
transaction(pluginId, fn) {
|
|
20114
|
+
assertPluginId(pluginId);
|
|
20115
|
+
return db.transaction(fn)();
|
|
20116
|
+
},
|
|
20117
|
+
listTables(pluginId) {
|
|
20118
|
+
assertPluginId(pluginId);
|
|
20119
|
+
return tablesOf(pluginId);
|
|
20120
|
+
},
|
|
20121
|
+
dropAll(pluginId) {
|
|
20122
|
+
assertPluginId(pluginId);
|
|
20123
|
+
const tables = tablesOf(pluginId);
|
|
20124
|
+
db.transaction(() => {
|
|
20125
|
+
for (const table2 of tables)
|
|
20126
|
+
db.run(`DROP TABLE IF EXISTS "${table2}"`);
|
|
20127
|
+
db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
|
|
20128
|
+
})();
|
|
20129
|
+
return tables.length;
|
|
20130
|
+
},
|
|
20131
|
+
orphanTables(installedIds) {
|
|
20132
|
+
const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
|
|
20133
|
+
return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
|
|
20134
|
+
}
|
|
20135
|
+
};
|
|
20136
|
+
}
|
|
19123
20137
|
// packages/rtk/src/catalog.ts
|
|
19124
20138
|
var RTK_FILTER_IDS = [
|
|
19125
20139
|
"git-diff",
|
|
@@ -19186,7 +20200,7 @@ var GROUP_COLUMN = {
|
|
|
19186
20200
|
requestedModel: "requested_model",
|
|
19187
20201
|
apiKey: "api_key_id",
|
|
19188
20202
|
provider: "resolved_provider",
|
|
19189
|
-
hour: "at / 3600000",
|
|
20203
|
+
hour: "CAST(at / 3600000 AS INTEGER)",
|
|
19190
20204
|
day: null
|
|
19191
20205
|
},
|
|
19192
20206
|
daily: {
|
|
@@ -19283,8 +20297,30 @@ function createUsageRepo(db) {
|
|
|
19283
20297
|
const complete = db.transaction((log) => {
|
|
19284
20298
|
db.run(COMPLETE, values(log, "done"));
|
|
19285
20299
|
const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
|
|
19286
|
-
if (stored !== null)
|
|
19287
|
-
|
|
20300
|
+
if (stored !== null) {
|
|
20301
|
+
const restored = toLog(stored);
|
|
20302
|
+
rollupLog(db, restored);
|
|
20303
|
+
rollupHour(db, restored);
|
|
20304
|
+
}
|
|
20305
|
+
});
|
|
20306
|
+
const pruneLogs = db.transaction((olderThan) => {
|
|
20307
|
+
db.run("DELETE FROM request_logs WHERE at < ?", [olderThan]);
|
|
20308
|
+
const removed = db.query("SELECT changes() AS n").get()?.n ?? 0;
|
|
20309
|
+
const boundary = hourOf(olderThan);
|
|
20310
|
+
db.run("DELETE FROM usage_rollup WHERE hour <= ?", [boundary]);
|
|
20311
|
+
db.run(`INSERT INTO usage_rollup
|
|
20312
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
20313
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
20314
|
+
SELECT api_key_id, ?, COUNT(*),
|
|
20315
|
+
COALESCE(SUM(input_tokens), 0),
|
|
20316
|
+
COALESCE(SUM(output_tokens), 0),
|
|
20317
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
20318
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
20319
|
+
COALESCE(SUM(cost_usd), 0)
|
|
20320
|
+
FROM request_logs
|
|
20321
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL AND at >= ? AND at < ?
|
|
20322
|
+
GROUP BY api_key_id`, [boundary, boundary * HOUR_MS, (boundary + 1) * HOUR_MS]);
|
|
20323
|
+
return removed;
|
|
19288
20324
|
});
|
|
19289
20325
|
return {
|
|
19290
20326
|
async begin(log) {
|
|
@@ -19308,6 +20344,38 @@ function createUsageRepo(db) {
|
|
|
19308
20344
|
async recent(limit) {
|
|
19309
20345
|
return db.query("SELECT * FROM request_logs ORDER BY at DESC LIMIT ?").all(limit).map(toLog);
|
|
19310
20346
|
},
|
|
20347
|
+
async sumSince(apiKeyId, sinceMs) {
|
|
20348
|
+
const boundary = hourOf(sinceMs);
|
|
20349
|
+
const whole = db.query(`SELECT COALESCE(SUM(requests), 0) AS requests,
|
|
20350
|
+
COALESCE(SUM(input_tokens + output_tokens
|
|
20351
|
+
+ cache_read_tokens + cache_write_tokens), 0) AS tokens,
|
|
20352
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
20353
|
+
FROM usage_rollup
|
|
20354
|
+
WHERE api_key_id = ? AND hour > ?`).get(apiKeyId, boundary);
|
|
20355
|
+
const edge = db.query(`SELECT COUNT(*) AS requests,
|
|
20356
|
+
COALESCE(SUM(input_tokens + output_tokens
|
|
20357
|
+
+ cache_read_tokens + cache_write_tokens), 0) AS tokens,
|
|
20358
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
20359
|
+
FROM request_logs
|
|
20360
|
+
WHERE api_key_id = ? AND state = 'done' AND at >= ? AND at < ?`).get(apiKeyId, sinceMs, (boundary + 1) * HOUR_MS);
|
|
20361
|
+
return {
|
|
20362
|
+
requests: (whole?.requests ?? 0) + (edge?.requests ?? 0),
|
|
20363
|
+
tokens: (whole?.tokens ?? 0) + (edge?.tokens ?? 0),
|
|
20364
|
+
costUsd: (whole?.cost_usd ?? 0) + (edge?.cost_usd ?? 0)
|
|
20365
|
+
};
|
|
20366
|
+
},
|
|
20367
|
+
async rebuildRollup() {
|
|
20368
|
+
rebuildRollup(db);
|
|
20369
|
+
},
|
|
20370
|
+
async auditRollup() {
|
|
20371
|
+
return auditRollup(db);
|
|
20372
|
+
},
|
|
20373
|
+
async oldestSince(apiKeyId, sinceMs) {
|
|
20374
|
+
const row = db.query(`SELECT MIN(at) AS at
|
|
20375
|
+
FROM request_logs
|
|
20376
|
+
WHERE api_key_id = ? AND state = 'done' AND at >= ?`).get(apiKeyId, sinceMs);
|
|
20377
|
+
return row?.at ?? null;
|
|
20378
|
+
},
|
|
19311
20379
|
async aggregate(q) {
|
|
19312
20380
|
const grain = q.grain ?? "raw";
|
|
19313
20381
|
const daily = grain === "daily";
|
|
@@ -19348,8 +20416,7 @@ function createUsageRepo(db) {
|
|
|
19348
20416
|
});
|
|
19349
20417
|
},
|
|
19350
20418
|
async prune(olderThan) {
|
|
19351
|
-
|
|
19352
|
-
return db.query("SELECT changes() AS n").get()?.n ?? 0;
|
|
20419
|
+
return pruneLogs(olderThan);
|
|
19353
20420
|
},
|
|
19354
20421
|
async pruneDaily(olderThan) {
|
|
19355
20422
|
db.run("DELETE FROM usage_daily WHERE day < ?", [olderThan]);
|
|
@@ -19361,8 +20428,6 @@ function createUsageRepo(db) {
|
|
|
19361
20428
|
// packages/store/src/sqlite/store.ts
|
|
19362
20429
|
async function createStore(opts) {
|
|
19363
20430
|
const logger2 = opts.logger ?? noopLogger;
|
|
19364
|
-
const db = openDb(opts.path);
|
|
19365
|
-
logger2.debug("store opened", { path: opts.path });
|
|
19366
20431
|
const listeners = new Set;
|
|
19367
20432
|
const emit2 = (change) => {
|
|
19368
20433
|
for (const listener of listeners) {
|
|
@@ -19371,23 +20436,123 @@ async function createStore(opts) {
|
|
|
19371
20436
|
} catch {}
|
|
19372
20437
|
}
|
|
19373
20438
|
};
|
|
20439
|
+
const open = () => {
|
|
20440
|
+
const db = openDb(opts.path);
|
|
20441
|
+
logger2.debug("store opened", { path: opts.path });
|
|
20442
|
+
return {
|
|
20443
|
+
db,
|
|
20444
|
+
credentials: createCredentialRepo(db, opts.encryptionKey, emit2),
|
|
20445
|
+
config: createConfigRepo(db, emit2),
|
|
20446
|
+
keys: createKeyRepo(db, logger2),
|
|
20447
|
+
usage: createUsageRepo(db),
|
|
20448
|
+
bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
20449
|
+
maintenance: createMaintenanceRepo(db),
|
|
20450
|
+
plugins: createPluginRepo(db)
|
|
20451
|
+
};
|
|
20452
|
+
};
|
|
20453
|
+
let handle = open();
|
|
20454
|
+
let live = true;
|
|
20455
|
+
const closeHandle = () => {
|
|
20456
|
+
if (!live)
|
|
20457
|
+
return;
|
|
20458
|
+
handle.db.close();
|
|
20459
|
+
live = false;
|
|
20460
|
+
};
|
|
19374
20461
|
return {
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
19380
|
-
|
|
19381
|
-
|
|
19382
|
-
|
|
19383
|
-
|
|
19384
|
-
|
|
20462
|
+
databasePath: opts.path,
|
|
20463
|
+
credentials: {
|
|
20464
|
+
list: () => handle.credentials.list(),
|
|
20465
|
+
listRouting: () => handle.credentials.listRouting(),
|
|
20466
|
+
get: (id) => handle.credentials.get(id),
|
|
20467
|
+
create: (input) => handle.credentials.create(input),
|
|
20468
|
+
update: (id, patch) => handle.credentials.update(id, patch),
|
|
20469
|
+
updateSecrets: (id, secrets, expiresAt) => handle.credentials.updateSecrets(id, secrets, expiresAt),
|
|
20470
|
+
remove: (id) => handle.credentials.remove(id),
|
|
20471
|
+
listHealth: () => handle.credentials.listHealth(),
|
|
20472
|
+
saveHealth: (rows) => handle.credentials.saveHealth(rows),
|
|
20473
|
+
updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
|
|
20474
|
+
listQuota: () => handle.credentials.listQuota(),
|
|
20475
|
+
saveQuota: (rows) => handle.credentials.saveQuota(rows),
|
|
20476
|
+
listQuotaSamples: (q) => handle.credentials.listQuotaSamples(q),
|
|
20477
|
+
pruneQuotaSamples: (olderThan) => handle.credentials.pruneQuotaSamples(olderThan)
|
|
19385
20478
|
},
|
|
19386
|
-
|
|
19387
|
-
|
|
19388
|
-
|
|
20479
|
+
config: {
|
|
20480
|
+
listModels: () => handle.config.listModels(),
|
|
20481
|
+
putModel: (model) => handle.config.putModel(model),
|
|
20482
|
+
removeModel: (id) => handle.config.removeModel(id),
|
|
20483
|
+
getSettings: () => handle.config.getSettings(),
|
|
20484
|
+
putSettings: (patch) => handle.config.putSettings(patch),
|
|
20485
|
+
getAdminPasswordHash: () => handle.config.getAdminPasswordHash(),
|
|
20486
|
+
setAdminPasswordHashIfAbsent: (hash3) => handle.config.setAdminPasswordHashIfAbsent(hash3),
|
|
20487
|
+
setAdminPasswordHash: (hash3) => handle.config.setAdminPasswordHash(hash3)
|
|
20488
|
+
},
|
|
20489
|
+
keys: {
|
|
20490
|
+
list: () => handle.keys.list(),
|
|
20491
|
+
findByHash: (hash3) => handle.keys.findByHash(hash3),
|
|
20492
|
+
create: (input) => handle.keys.create(input),
|
|
20493
|
+
setLimits: (id, limits) => handle.keys.setLimits(id, limits),
|
|
20494
|
+
revoke: (id) => handle.keys.revoke(id)
|
|
20495
|
+
},
|
|
20496
|
+
usage: {
|
|
20497
|
+
begin: (log) => handle.usage.begin(log),
|
|
20498
|
+
route: (id, target) => handle.usage.route(id, target),
|
|
20499
|
+
append: (log) => handle.usage.append(log),
|
|
20500
|
+
sweepPending: () => handle.usage.sweepPending(),
|
|
20501
|
+
recent: (limit) => handle.usage.recent(limit),
|
|
20502
|
+
aggregate: (q) => handle.usage.aggregate(q),
|
|
20503
|
+
sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
|
|
20504
|
+
oldestSince: (apiKeyId, sinceMs) => handle.usage.oldestSince(apiKeyId, sinceMs),
|
|
20505
|
+
rebuildRollup: () => handle.usage.rebuildRollup(),
|
|
20506
|
+
auditRollup: () => handle.usage.auditRollup(),
|
|
20507
|
+
prune: (olderThan) => handle.usage.prune(olderThan),
|
|
20508
|
+
pruneDaily: (olderThan) => handle.usage.pruneDaily(olderThan)
|
|
20509
|
+
},
|
|
20510
|
+
bodies: {
|
|
20511
|
+
put: (artifact) => handle.bodies.put(artifact),
|
|
20512
|
+
get: (requestId) => handle.bodies.get(requestId),
|
|
20513
|
+
prune: (olderThan) => handle.bodies.prune(olderThan),
|
|
20514
|
+
pruneToCap: (cap) => handle.bodies.pruneToCap(cap),
|
|
20515
|
+
sweepOrphans: () => handle.bodies.sweepOrphans()
|
|
20516
|
+
},
|
|
20517
|
+
maintenance: {
|
|
20518
|
+
stats: () => handle.maintenance.stats(),
|
|
20519
|
+
vacuum: () => handle.maintenance.vacuum(),
|
|
20520
|
+
snapshotTo: (path) => handle.maintenance.snapshotTo(path),
|
|
20521
|
+
inspect: (path) => handle.maintenance.inspect(path)
|
|
20522
|
+
},
|
|
20523
|
+
plugins: {
|
|
20524
|
+
migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
|
|
20525
|
+
run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
|
|
20526
|
+
all(pluginId, sql, params) {
|
|
20527
|
+
return handle.plugins.all(pluginId, sql, params);
|
|
20528
|
+
},
|
|
20529
|
+
get(pluginId, sql, params) {
|
|
20530
|
+
return handle.plugins.get(pluginId, sql, params);
|
|
20531
|
+
},
|
|
20532
|
+
transaction(pluginId, fn) {
|
|
20533
|
+
return handle.plugins.transaction(pluginId, fn);
|
|
20534
|
+
},
|
|
20535
|
+
listTables: (pluginId) => handle.plugins.listTables(pluginId),
|
|
20536
|
+
dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
|
|
20537
|
+
orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
|
|
20538
|
+
},
|
|
20539
|
+
routing: {
|
|
20540
|
+
version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
20541
|
+
subscribe(listener) {
|
|
20542
|
+
listeners.add(listener);
|
|
20543
|
+
return () => listeners.delete(listener);
|
|
20544
|
+
}
|
|
20545
|
+
},
|
|
20546
|
+
async reopen() {
|
|
20547
|
+
closeHandle();
|
|
20548
|
+
handle = open();
|
|
20549
|
+
live = true;
|
|
20550
|
+
},
|
|
20551
|
+
close: closeHandle
|
|
20552
|
+
};
|
|
20553
|
+
}
|
|
19389
20554
|
// packages/control/src/quota/burn.ts
|
|
19390
|
-
var
|
|
20555
|
+
var HOUR_MS2 = 3600000;
|
|
19391
20556
|
function windowStartOf(window) {
|
|
19392
20557
|
if (window.resetsAt === null)
|
|
19393
20558
|
return null;
|
|
@@ -19410,9 +20575,9 @@ function burnFor(window, input) {
|
|
|
19410
20575
|
}
|
|
19411
20576
|
const windowStartsAt = windowStartOf(window);
|
|
19412
20577
|
const elapsedMs = windowStartsAt === null ? null : window.observedAt - windowStartsAt;
|
|
19413
|
-
const elapsedHours = elapsedMs === null ? null : elapsedMs /
|
|
20578
|
+
const elapsedHours = elapsedMs === null ? null : elapsedMs / HOUR_MS2;
|
|
19414
20579
|
const ratePerHour = elapsedHours === null ? null : elapsedHours <= 0 || window.used === 0 ? 0 : window.used / elapsedHours;
|
|
19415
|
-
const exhaustsAt = window.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window.observedAt + Math.max(0, window.limit - window.used) / ratePerHour *
|
|
20580
|
+
const exhaustsAt = window.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window.observedAt + Math.max(0, window.limit - window.used) / ratePerHour * HOUR_MS2;
|
|
19416
20581
|
return {
|
|
19417
20582
|
credentialId: window.credentialId,
|
|
19418
20583
|
windowType: window.windowType,
|
|
@@ -19592,6 +20757,243 @@ async function patchCredential(deps, id, input) {
|
|
|
19592
20757
|
async function removeCredential(store, id) {
|
|
19593
20758
|
await store.credentials.remove(id);
|
|
19594
20759
|
}
|
|
20760
|
+
// packages/control/src/database.ts
|
|
20761
|
+
import { basename, dirname as dirname2, join as join2, resolve } from "path";
|
|
20762
|
+
var exclusive = null;
|
|
20763
|
+
async function withExclusive(label2, operation) {
|
|
20764
|
+
if (exclusive !== null) {
|
|
20765
|
+
throw new GatewayError("CONFLICT", `${exclusive} is already running on this database`);
|
|
20766
|
+
}
|
|
20767
|
+
exclusive = label2;
|
|
20768
|
+
try {
|
|
20769
|
+
return await operation();
|
|
20770
|
+
} finally {
|
|
20771
|
+
exclusive = null;
|
|
20772
|
+
}
|
|
20773
|
+
}
|
|
20774
|
+
var SNAPSHOTS_DIRNAME = "snapshots";
|
|
20775
|
+
function snapshotsDir(deps) {
|
|
20776
|
+
return join2(dirname2(deps.store.databasePath), SNAPSHOTS_DIRNAME);
|
|
20777
|
+
}
|
|
20778
|
+
function snapshotName(at, reason) {
|
|
20779
|
+
const stamp = new Date(at).toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
20780
|
+
return `db_${stamp}_${reason}.sqlite`;
|
|
20781
|
+
}
|
|
20782
|
+
var SNAPSHOT_ID = /^db_(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z_([A-Za-z]+)\.sqlite$/;
|
|
20783
|
+
function parseSnapshotName(name) {
|
|
20784
|
+
const match = SNAPSHOT_ID.exec(name);
|
|
20785
|
+
if (match === null)
|
|
20786
|
+
return null;
|
|
20787
|
+
const [, date5, hh, mm, ss, ms, reason] = match;
|
|
20788
|
+
const at = Date.parse(`${date5}T${hh}:${mm}:${ss}.${ms}Z`);
|
|
20789
|
+
if (Number.isNaN(at) || reason === undefined)
|
|
20790
|
+
return null;
|
|
20791
|
+
return { at, reason };
|
|
20792
|
+
}
|
|
20793
|
+
function snapshotPath(deps, id) {
|
|
20794
|
+
const dir = resolve(snapshotsDir(deps));
|
|
20795
|
+
const path = resolve(dir, id);
|
|
20796
|
+
if (parseSnapshotName(id) === null || dirname2(path) !== dir) {
|
|
20797
|
+
throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
|
|
20798
|
+
}
|
|
20799
|
+
const real = deps.fs.realpath(path);
|
|
20800
|
+
if (real !== null && dirname2(real) !== (deps.fs.realpath(dir) ?? dir)) {
|
|
20801
|
+
throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
|
|
20802
|
+
}
|
|
20803
|
+
return path;
|
|
20804
|
+
}
|
|
20805
|
+
function listSnapshots(deps) {
|
|
20806
|
+
const dir = snapshotsDir(deps);
|
|
20807
|
+
const snapshots = [];
|
|
20808
|
+
for (const name of deps.fs.readdir(dir)) {
|
|
20809
|
+
const parsed = parseSnapshotName(name);
|
|
20810
|
+
if (parsed === null)
|
|
20811
|
+
continue;
|
|
20812
|
+
snapshots.push({
|
|
20813
|
+
id: name,
|
|
20814
|
+
filename: name,
|
|
20815
|
+
createdAt: parsed.at,
|
|
20816
|
+
sizeBytes: deps.fs.stat(join2(dir, name))?.size ?? 0,
|
|
20817
|
+
reason: parsed.reason
|
|
20818
|
+
});
|
|
20819
|
+
}
|
|
20820
|
+
return snapshots.sort((a, b) => b.createdAt - a.createdAt);
|
|
20821
|
+
}
|
|
20822
|
+
var DAY_MS = 86400000;
|
|
20823
|
+
var SNAPSHOT_HEADROOM = 1.1;
|
|
20824
|
+
function retentionOf(settings) {
|
|
20825
|
+
return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
|
|
20826
|
+
}
|
|
20827
|
+
function applyRetention(deps, policy, now) {
|
|
20828
|
+
const dir = snapshotsDir(deps);
|
|
20829
|
+
const cutoff = now - policy.maxAgeDays * DAY_MS;
|
|
20830
|
+
const snapshots = listSnapshots(deps);
|
|
20831
|
+
const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
|
|
20832
|
+
let removed = 0;
|
|
20833
|
+
snapshots.forEach((snapshot, index) => {
|
|
20834
|
+
if (index === 0)
|
|
20835
|
+
return;
|
|
20836
|
+
if (snapshot.id === undo)
|
|
20837
|
+
return;
|
|
20838
|
+
if (index < policy.keepLatest && snapshot.createdAt >= cutoff)
|
|
20839
|
+
return;
|
|
20840
|
+
deps.fs.unlink(join2(dir, snapshot.id));
|
|
20841
|
+
removed++;
|
|
20842
|
+
});
|
|
20843
|
+
return removed;
|
|
20844
|
+
}
|
|
20845
|
+
var STAGING_MAX_AGE_MS = 60 * 60 * 1000;
|
|
20846
|
+
async function writeSnapshot(deps, input) {
|
|
20847
|
+
const dir = snapshotsDir(deps);
|
|
20848
|
+
const at = deps.now();
|
|
20849
|
+
const filename = snapshotName(at, input.reason);
|
|
20850
|
+
const path = join2(dir, filename);
|
|
20851
|
+
deps.fs.mkdir(dir);
|
|
20852
|
+
if (deps.fs.stat(path) !== null) {
|
|
20853
|
+
throw new GatewayError("CONFLICT", "a snapshot for this instant already exists");
|
|
20854
|
+
}
|
|
20855
|
+
const free = deps.fs.freeBytes(dir);
|
|
20856
|
+
const needed = Math.ceil((deps.fs.stat(deps.store.databasePath)?.size ?? 0) * SNAPSHOT_HEADROOM);
|
|
20857
|
+
if (free !== null && free < needed) {
|
|
20858
|
+
throw new GatewayError("CONFLICT", "not enough free disk space to write a snapshot");
|
|
20859
|
+
}
|
|
20860
|
+
try {
|
|
20861
|
+
await deps.store.maintenance.snapshotTo(path);
|
|
20862
|
+
} catch (error51) {
|
|
20863
|
+
deps.fs.unlink(path);
|
|
20864
|
+
throw error51;
|
|
20865
|
+
}
|
|
20866
|
+
const info = {
|
|
20867
|
+
id: filename,
|
|
20868
|
+
filename,
|
|
20869
|
+
createdAt: at,
|
|
20870
|
+
sizeBytes: deps.fs.stat(path)?.size ?? 0,
|
|
20871
|
+
reason: input.reason
|
|
20872
|
+
};
|
|
20873
|
+
if (input.force !== true) {
|
|
20874
|
+
applyRetention(deps, retentionOf(await deps.store.config.getSettings()), at);
|
|
20875
|
+
}
|
|
20876
|
+
return info;
|
|
20877
|
+
}
|
|
20878
|
+
async function createSnapshot(deps, input) {
|
|
20879
|
+
return withExclusive("a snapshot", () => writeSnapshot(deps, input));
|
|
20880
|
+
}
|
|
20881
|
+
|
|
20882
|
+
class SwapFailedError extends Error {
|
|
20883
|
+
preRestoreSnapshotId;
|
|
20884
|
+
cause;
|
|
20885
|
+
reopened;
|
|
20886
|
+
constructor(preRestoreSnapshotId, cause, reopened) {
|
|
20887
|
+
super("the database swap failed and the database may be incomplete");
|
|
20888
|
+
this.preRestoreSnapshotId = preRestoreSnapshotId;
|
|
20889
|
+
this.cause = cause;
|
|
20890
|
+
this.reopened = reopened;
|
|
20891
|
+
this.name = "SwapFailedError";
|
|
20892
|
+
}
|
|
20893
|
+
}
|
|
20894
|
+
var NAMED_TABLES = 5;
|
|
20895
|
+
function describeTables(tables) {
|
|
20896
|
+
if (tables.length === 0)
|
|
20897
|
+
return "it has no tables at all";
|
|
20898
|
+
const named = tables.slice(0, NAMED_TABLES);
|
|
20899
|
+
const rest = tables.length - named.length;
|
|
20900
|
+
return `it has ${named.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
|
|
20901
|
+
}
|
|
20902
|
+
async function swapIn(deps, candidate) {
|
|
20903
|
+
const inspection = await deps.store.maintenance.inspect(candidate.path);
|
|
20904
|
+
if (!inspection.ok) {
|
|
20905
|
+
throw new GatewayError("BAD_REQUEST", inspection.quickCheck === "ok" ? `that file is a database, but not one of ours: ${describeTables(inspection.tables)}` : `that file failed its integrity check: ${inspection.quickCheck}`);
|
|
20906
|
+
}
|
|
20907
|
+
const preRestoreSnapshot = await writeSnapshot(deps, { reason: "preRestore", force: true });
|
|
20908
|
+
const adminHashBefore = await deps.store.config.getAdminPasswordHash();
|
|
20909
|
+
const live = deps.store.databasePath;
|
|
20910
|
+
const staged = `${live}.incoming`;
|
|
20911
|
+
try {
|
|
20912
|
+
if (candidate.consume)
|
|
20913
|
+
deps.fs.rename(candidate.path, staged);
|
|
20914
|
+
else
|
|
20915
|
+
deps.fs.copyFile(candidate.path, staged);
|
|
20916
|
+
} catch (error51) {
|
|
20917
|
+
deps.fs.unlink(staged);
|
|
20918
|
+
throw error51;
|
|
20919
|
+
}
|
|
20920
|
+
try {
|
|
20921
|
+
deps.store.close();
|
|
20922
|
+
deps.fs.unlink(`${live}-wal`);
|
|
20923
|
+
deps.fs.unlink(`${live}-shm`);
|
|
20924
|
+
deps.fs.rename(staged, live);
|
|
20925
|
+
await deps.store.reopen();
|
|
20926
|
+
} catch (error51) {
|
|
20927
|
+
let reopened = true;
|
|
20928
|
+
try {
|
|
20929
|
+
await deps.store.reopen();
|
|
20930
|
+
} catch {
|
|
20931
|
+
reopened = false;
|
|
20932
|
+
}
|
|
20933
|
+
throw new SwapFailedError(preRestoreSnapshot.id, error51, reopened);
|
|
20934
|
+
}
|
|
20935
|
+
const adminHashAfter = await deps.store.config.getAdminPasswordHash();
|
|
20936
|
+
try {
|
|
20937
|
+
await deps.store.usage.rebuildRollup();
|
|
20938
|
+
} catch (error51) {
|
|
20939
|
+
(deps.logger ?? noopLogger).warn("usage rollup not rebuilt after the swap; run omni doctor", {
|
|
20940
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
20941
|
+
});
|
|
20942
|
+
}
|
|
20943
|
+
try {
|
|
20944
|
+
await deps.reapplyPluginSchema?.();
|
|
20945
|
+
} catch (error51) {
|
|
20946
|
+
(deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
|
|
20947
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
20948
|
+
});
|
|
20949
|
+
}
|
|
20950
|
+
return {
|
|
20951
|
+
ok: true,
|
|
20952
|
+
counts: inspection.counts,
|
|
20953
|
+
preRestoreSnapshot,
|
|
20954
|
+
adminPasswordChanged: adminHashAfter !== adminHashBefore
|
|
20955
|
+
};
|
|
20956
|
+
}
|
|
20957
|
+
async function restoreSnapshot(deps, id) {
|
|
20958
|
+
const path = snapshotPath(deps, id);
|
|
20959
|
+
if (deps.fs.stat(path) === null)
|
|
20960
|
+
throw new GatewayError("BAD_REQUEST", "no such snapshot");
|
|
20961
|
+
return withExclusive("a restore", () => swapIn(deps, { path, consume: false }));
|
|
20962
|
+
}
|
|
20963
|
+
var MAX_IMPORT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
20964
|
+
async function vacuum(deps) {
|
|
20965
|
+
return withExclusive("a vacuum", async () => {
|
|
20966
|
+
const before = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
|
|
20967
|
+
const startedAt = deps.now();
|
|
20968
|
+
await deps.store.maintenance.vacuum();
|
|
20969
|
+
const after = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
|
|
20970
|
+
return {
|
|
20971
|
+
reclaimedBytes: Math.max(0, before - after),
|
|
20972
|
+
durationMs: Math.max(0, deps.now() - startedAt)
|
|
20973
|
+
};
|
|
20974
|
+
});
|
|
20975
|
+
}
|
|
20976
|
+
async function getDatabaseOverview(deps) {
|
|
20977
|
+
const live = deps.store.databasePath;
|
|
20978
|
+
const stats = await deps.store.maintenance.stats();
|
|
20979
|
+
const settings = await deps.store.config.getSettings();
|
|
20980
|
+
const snapshots = listSnapshots(deps);
|
|
20981
|
+
return {
|
|
20982
|
+
stats,
|
|
20983
|
+
fileBytes: deps.fs.stat(live)?.size ?? 0,
|
|
20984
|
+
walBytes: deps.fs.stat(`${live}-wal`)?.size ?? 0,
|
|
20985
|
+
bodiesBytes: deps.fs.dirBytes(bodiesDirFor(live)),
|
|
20986
|
+
logicalBytes: stats.pageSize * stats.pageCount,
|
|
20987
|
+
freePageBytes: stats.pageSize * stats.freelistCount,
|
|
20988
|
+
freeDiskBytes: deps.fs.freeBytes(dirname2(live)),
|
|
20989
|
+
retention: retentionOf(settings),
|
|
20990
|
+
snapshots: {
|
|
20991
|
+
count: snapshots.length,
|
|
20992
|
+
totalBytes: snapshots.reduce((sum, snapshot) => sum + snapshot.sizeBytes, 0),
|
|
20993
|
+
latestAt: snapshots[0]?.createdAt ?? null
|
|
20994
|
+
}
|
|
20995
|
+
};
|
|
20996
|
+
}
|
|
19595
20997
|
// packages/control/src/dryRun.ts
|
|
19596
20998
|
async function dryRun(deps, modelId, input) {
|
|
19597
20999
|
const need = parseOrThrow(dryRunSchema, input);
|
|
@@ -19636,17 +21038,67 @@ async function dryRun(deps, modelId, input) {
|
|
|
19636
21038
|
};
|
|
19637
21039
|
}
|
|
19638
21040
|
// packages/control/src/keys.ts
|
|
19639
|
-
|
|
21041
|
+
function configuredWindows(limits) {
|
|
21042
|
+
const needed = new Set;
|
|
21043
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
21044
|
+
const windows = limits[dimension];
|
|
21045
|
+
if (windows === undefined)
|
|
21046
|
+
continue;
|
|
21047
|
+
for (const window of WINDOWS) {
|
|
21048
|
+
const limit = windows[window];
|
|
21049
|
+
if (limit !== undefined && limit !== null)
|
|
21050
|
+
needed.add(window);
|
|
21051
|
+
}
|
|
21052
|
+
}
|
|
21053
|
+
return [...needed];
|
|
21054
|
+
}
|
|
21055
|
+
async function readLimitUsage(store, id, limits, now) {
|
|
21056
|
+
if (limits === null)
|
|
21057
|
+
return [];
|
|
21058
|
+
const sums = new Map;
|
|
21059
|
+
for (const window of configuredWindows(limits)) {
|
|
21060
|
+
sums.set(window, await store.usage.sumSince(id, now - WINDOW_MS[window]));
|
|
21061
|
+
}
|
|
21062
|
+
const readings = [];
|
|
21063
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
21064
|
+
const windows = limits[dimension];
|
|
21065
|
+
if (windows === undefined)
|
|
21066
|
+
continue;
|
|
21067
|
+
for (const window of WINDOWS) {
|
|
21068
|
+
const limit = windows[window];
|
|
21069
|
+
if (limit === undefined || limit === null)
|
|
21070
|
+
continue;
|
|
21071
|
+
const sum = sums.get(window);
|
|
21072
|
+
const used = sum === undefined ? null : dimension === "requests" ? sum.requests : dimension === "tokens" ? sum.tokens : sum.costUsd;
|
|
21073
|
+
readings.push({ dimension, window, limit, used });
|
|
21074
|
+
}
|
|
21075
|
+
}
|
|
21076
|
+
if (limits.concurrency !== undefined && limits.concurrency !== null) {
|
|
21077
|
+
readings.push({
|
|
21078
|
+
dimension: "concurrency",
|
|
21079
|
+
window: null,
|
|
21080
|
+
limit: limits.concurrency,
|
|
21081
|
+
used: null
|
|
21082
|
+
});
|
|
21083
|
+
}
|
|
21084
|
+
return readings;
|
|
21085
|
+
}
|
|
21086
|
+
async function toSummary(store, key, now) {
|
|
21087
|
+
return {
|
|
21088
|
+
id: key.id,
|
|
21089
|
+
label: key.label,
|
|
21090
|
+
prefix: key.prefix,
|
|
21091
|
+
modelAllowlist: key.modelAllowlist,
|
|
21092
|
+
limits: key.limits,
|
|
21093
|
+
limitUsage: await readLimitUsage(store, key.id, key.limits, now),
|
|
21094
|
+
bodyLoggingOptOut: key.bodyLoggingOptOut,
|
|
21095
|
+
createdAt: key.createdAt,
|
|
21096
|
+
revokedAt: key.revokedAt
|
|
21097
|
+
};
|
|
21098
|
+
}
|
|
21099
|
+
async function listKeys(store, now = Date.now()) {
|
|
19640
21100
|
const keys = await store.keys.list();
|
|
19641
|
-
return keys.map((
|
|
19642
|
-
id: k.id,
|
|
19643
|
-
label: k.label,
|
|
19644
|
-
prefix: k.prefix,
|
|
19645
|
-
modelAllowlist: k.modelAllowlist,
|
|
19646
|
-
rateLimitPerMin: k.rateLimitPerMin,
|
|
19647
|
-
createdAt: k.createdAt,
|
|
19648
|
-
revokedAt: k.revokedAt
|
|
19649
|
-
}));
|
|
21101
|
+
return Promise.all(keys.map((key) => toSummary(store, key, now)));
|
|
19650
21102
|
}
|
|
19651
21103
|
async function createKey(store, input) {
|
|
19652
21104
|
const body2 = parseOrThrow(keyCreateSchema, input);
|
|
@@ -19657,10 +21109,19 @@ async function createKey(store, input) {
|
|
|
19657
21109
|
prefix: raw.slice(0, 12),
|
|
19658
21110
|
hash: await hashApiKey(raw),
|
|
19659
21111
|
modelAllowlist: body2.modelAllowlist,
|
|
19660
|
-
|
|
21112
|
+
limits: body2.limits,
|
|
21113
|
+
bodyLoggingOptOut: body2.bodyLoggingOptOut
|
|
19661
21114
|
});
|
|
19662
21115
|
return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
|
|
19663
21116
|
}
|
|
21117
|
+
async function setKeyLimits(store, id, input, now = Date.now()) {
|
|
21118
|
+
const body2 = parseOrThrow(keyLimitsSchema, input);
|
|
21119
|
+
const key = (await store.keys.list()).find((entry) => entry.id === id);
|
|
21120
|
+
if (key === undefined)
|
|
21121
|
+
throw new GatewayError("BAD_REQUEST", "no such api key");
|
|
21122
|
+
await store.keys.setLimits(id, body2.limits);
|
|
21123
|
+
return toSummary(store, { ...key, limits: body2.limits }, now);
|
|
21124
|
+
}
|
|
19664
21125
|
async function revokeKey(store, id) {
|
|
19665
21126
|
await store.keys.revoke(id);
|
|
19666
21127
|
}
|
|
@@ -19779,8 +21240,754 @@ async function putModel(store, id, input) {
|
|
|
19779
21240
|
throw stranded;
|
|
19780
21241
|
await store.config.putModel(model);
|
|
19781
21242
|
}
|
|
19782
|
-
async function removeModel(store, id) {
|
|
19783
|
-
await store.config.removeModel(id);
|
|
21243
|
+
async function removeModel(store, id) {
|
|
21244
|
+
await store.config.removeModel(id);
|
|
21245
|
+
}
|
|
21246
|
+
// packages/control/src/nodeFs.ts
|
|
21247
|
+
import {
|
|
21248
|
+
copyFileSync,
|
|
21249
|
+
mkdirSync,
|
|
21250
|
+
readdirSync,
|
|
21251
|
+
readFileSync,
|
|
21252
|
+
realpathSync,
|
|
21253
|
+
renameSync,
|
|
21254
|
+
rmSync,
|
|
21255
|
+
statfsSync,
|
|
21256
|
+
statSync,
|
|
21257
|
+
writeFileSync
|
|
21258
|
+
} from "fs";
|
|
21259
|
+
import { join as join4 } from "path";
|
|
21260
|
+
|
|
21261
|
+
// packages/control/src/plugins.ts
|
|
21262
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
21263
|
+
import { basename as basename2, join as join3, resolve as resolve2, sep as sep2 } from "path";
|
|
21264
|
+
|
|
21265
|
+
// packages/plugin-api/src/version.ts
|
|
21266
|
+
var PLUGIN_API_VERSION = 1;
|
|
21267
|
+
var DASHBOARD_SDK_VERSION = "0.1.0";
|
|
21268
|
+
// packages/plugin-api/src/manifest.ts
|
|
21269
|
+
var CAPABILITIES = [
|
|
21270
|
+
"storage",
|
|
21271
|
+
"files",
|
|
21272
|
+
"net:outbound",
|
|
21273
|
+
"events:request",
|
|
21274
|
+
"events:limit"
|
|
21275
|
+
];
|
|
21276
|
+
var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
21277
|
+
var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
|
|
21278
|
+
var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
|
|
21279
|
+
message: "entry must be a relative path with no '..' segment"
|
|
21280
|
+
});
|
|
21281
|
+
var originSchema = exports_external.string().refine((value) => {
|
|
21282
|
+
let url2;
|
|
21283
|
+
try {
|
|
21284
|
+
url2 = new URL(value);
|
|
21285
|
+
} catch {
|
|
21286
|
+
return false;
|
|
21287
|
+
}
|
|
21288
|
+
if (url2.protocol !== "https:" && url2.protocol !== "http:")
|
|
21289
|
+
return false;
|
|
21290
|
+
if (url2.hostname === "")
|
|
21291
|
+
return false;
|
|
21292
|
+
if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
|
|
21293
|
+
return false;
|
|
21294
|
+
return value === url2.origin || value === `${url2.origin}/`;
|
|
21295
|
+
}, { message: "origin must be scheme://host with no path, query or fragment" });
|
|
21296
|
+
var navSchema = exports_external.object({
|
|
21297
|
+
label: exports_external.string().min(1).max(32)
|
|
21298
|
+
}).strict();
|
|
21299
|
+
var manifestSchema = exports_external.object({
|
|
21300
|
+
id: idSchema,
|
|
21301
|
+
name: exports_external.string().min(1).max(64),
|
|
21302
|
+
version: exports_external.string().min(1).max(32),
|
|
21303
|
+
api: exports_external.number().int().nonnegative(),
|
|
21304
|
+
sdk: exports_external.string().min(1).max(32).optional(),
|
|
21305
|
+
server: entrySchema.optional(),
|
|
21306
|
+
ui: entrySchema.optional(),
|
|
21307
|
+
nav: navSchema.optional(),
|
|
21308
|
+
capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
|
|
21309
|
+
origins: exports_external.array(originSchema).min(1).optional(),
|
|
21310
|
+
defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21311
|
+
}).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
|
|
21312
|
+
message: "manifest must declare a server entry, a ui entry, or both"
|
|
21313
|
+
}).refine((m) => m.ui === undefined || m.sdk !== undefined, {
|
|
21314
|
+
message: "a ui entry requires an sdk range"
|
|
21315
|
+
}).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
|
|
21316
|
+
message: "net:outbound requires origins"
|
|
21317
|
+
}).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
|
|
21318
|
+
message: "origins requires the net:outbound capability"
|
|
21319
|
+
});
|
|
21320
|
+
function safeParseManifest(input) {
|
|
21321
|
+
const parsed = manifestSchema.safeParse(input);
|
|
21322
|
+
if (parsed.success)
|
|
21323
|
+
return { ok: true, manifest: parsed.data };
|
|
21324
|
+
const first = parsed.error.issues[0];
|
|
21325
|
+
const path = first?.path.join(".") ?? "";
|
|
21326
|
+
const message = first?.message ?? "invalid manifest";
|
|
21327
|
+
return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
|
|
21328
|
+
}
|
|
21329
|
+
function isApiCompatible(manifest) {
|
|
21330
|
+
return manifest.api === PLUGIN_API_VERSION;
|
|
21331
|
+
}
|
|
21332
|
+
|
|
21333
|
+
// packages/control/src/plugins.ts
|
|
21334
|
+
var PLUGINS_DIRNAME = "plugins";
|
|
21335
|
+
function pluginsDir(root) {
|
|
21336
|
+
return join3(root, PLUGINS_DIRNAME);
|
|
21337
|
+
}
|
|
21338
|
+
var MANIFEST_FILENAME = "omni-plugin.json";
|
|
21339
|
+
var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
|
|
21340
|
+
var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
21341
|
+
var NPM_TARBALL_ROOT = "package";
|
|
21342
|
+
function readManifest(fs, home) {
|
|
21343
|
+
const raw = fs.readText(join3(home, MANIFEST_FILENAME));
|
|
21344
|
+
if (raw === null) {
|
|
21345
|
+
return { check: "manifest", reason: `no ${MANIFEST_FILENAME}`, fatal: true };
|
|
21346
|
+
}
|
|
21347
|
+
let document;
|
|
21348
|
+
try {
|
|
21349
|
+
document = JSON.parse(raw);
|
|
21350
|
+
} catch (error51) {
|
|
21351
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21352
|
+
return {
|
|
21353
|
+
check: "manifest",
|
|
21354
|
+
reason: `${MANIFEST_FILENAME} is not valid JSON: ${detail}`,
|
|
21355
|
+
fatal: true
|
|
21356
|
+
};
|
|
21357
|
+
}
|
|
21358
|
+
const parsed = safeParseManifest(document);
|
|
21359
|
+
if (!parsed.ok)
|
|
21360
|
+
return { check: "manifest", reason: parsed.reason, fatal: true };
|
|
21361
|
+
return parsed.manifest;
|
|
21362
|
+
}
|
|
21363
|
+
function checkEntry(fs, home, entry, label2, fatal) {
|
|
21364
|
+
const target = resolve2(home, entry);
|
|
21365
|
+
if (target !== home && !target.startsWith(`${home}${sep2}`)) {
|
|
21366
|
+
return {
|
|
21367
|
+
check: "entry",
|
|
21368
|
+
reason: `${label2} entry resolves outside the plugin directory`,
|
|
21369
|
+
fatal
|
|
21370
|
+
};
|
|
21371
|
+
}
|
|
21372
|
+
if (!fs.isFile(target)) {
|
|
21373
|
+
return { check: "entry", reason: `${label2} entry ${entry} does not exist`, fatal };
|
|
21374
|
+
}
|
|
21375
|
+
return null;
|
|
21376
|
+
}
|
|
21377
|
+
function verifyPlugin(deps, root, id) {
|
|
21378
|
+
const home = join3(pluginsDir(root), id);
|
|
21379
|
+
if (!ID_PATTERN2.test(id) || !deps.fs.isDirectory(home)) {
|
|
21380
|
+
throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
|
|
21381
|
+
}
|
|
21382
|
+
return reportFor(deps, home, id);
|
|
21383
|
+
}
|
|
21384
|
+
function reportFor(deps, home, id) {
|
|
21385
|
+
const problems = [];
|
|
21386
|
+
const read = readManifest(deps.fs, home);
|
|
21387
|
+
if (!("id" in read)) {
|
|
21388
|
+
problems.push(read);
|
|
21389
|
+
return { id, path: home, manifest: null, problems, loadable: false };
|
|
21390
|
+
}
|
|
21391
|
+
const manifest = read;
|
|
21392
|
+
if (manifest.id !== id) {
|
|
21393
|
+
problems.push({
|
|
21394
|
+
check: "id",
|
|
21395
|
+
reason: `manifest id ${manifest.id} does not match its directory ${id}`,
|
|
21396
|
+
fatal: true
|
|
21397
|
+
});
|
|
21398
|
+
}
|
|
21399
|
+
if (!isApiCompatible(manifest)) {
|
|
21400
|
+
problems.push({
|
|
21401
|
+
check: "api",
|
|
21402
|
+
reason: `plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`,
|
|
21403
|
+
fatal: true
|
|
21404
|
+
});
|
|
21405
|
+
}
|
|
21406
|
+
if (manifest.server !== undefined) {
|
|
21407
|
+
const problem = checkEntry(deps.fs, home, manifest.server, "server", true);
|
|
21408
|
+
if (problem !== null)
|
|
21409
|
+
problems.push(problem);
|
|
21410
|
+
}
|
|
21411
|
+
if (manifest.ui !== undefined) {
|
|
21412
|
+
const problem = checkEntry(deps.fs, home, manifest.ui, "ui", false);
|
|
21413
|
+
if (problem !== null)
|
|
21414
|
+
problems.push(problem);
|
|
21415
|
+
}
|
|
21416
|
+
const sdk = sdkProblem(deps, manifest);
|
|
21417
|
+
if (sdk !== null)
|
|
21418
|
+
problems.push(sdk);
|
|
21419
|
+
return {
|
|
21420
|
+
id,
|
|
21421
|
+
path: home,
|
|
21422
|
+
manifest,
|
|
21423
|
+
problems,
|
|
21424
|
+
loadable: !problems.some((problem) => problem.fatal)
|
|
21425
|
+
};
|
|
21426
|
+
}
|
|
21427
|
+
function sdkProblem(deps, manifest) {
|
|
21428
|
+
if (manifest.ui === undefined || manifest.sdk === undefined)
|
|
21429
|
+
return null;
|
|
21430
|
+
if (deps.sdkVersion === undefined)
|
|
21431
|
+
return null;
|
|
21432
|
+
if (Bun.semver.satisfies(deps.sdkVersion, manifest.sdk))
|
|
21433
|
+
return null;
|
|
21434
|
+
return {
|
|
21435
|
+
check: "sdk",
|
|
21436
|
+
reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}`,
|
|
21437
|
+
fatal: false
|
|
21438
|
+
};
|
|
21439
|
+
}
|
|
21440
|
+
function listPlugins(deps, root) {
|
|
21441
|
+
const dir = pluginsDir(root);
|
|
21442
|
+
const summaries = [];
|
|
21443
|
+
for (const entry of [...deps.fs.readdir(dir)].sort()) {
|
|
21444
|
+
const home = join3(dir, entry);
|
|
21445
|
+
if (!deps.fs.isDirectory(home))
|
|
21446
|
+
continue;
|
|
21447
|
+
if (!ID_PATTERN2.test(entry)) {
|
|
21448
|
+
summaries.push({
|
|
21449
|
+
id: entry,
|
|
21450
|
+
path: home,
|
|
21451
|
+
manifest: null,
|
|
21452
|
+
problems: [
|
|
21453
|
+
{
|
|
21454
|
+
check: "id",
|
|
21455
|
+
reason: `directory name ${entry} is not a valid plugin id`,
|
|
21456
|
+
fatal: true
|
|
21457
|
+
}
|
|
21458
|
+
],
|
|
21459
|
+
loadable: false,
|
|
21460
|
+
name: null,
|
|
21461
|
+
version: null,
|
|
21462
|
+
api: null,
|
|
21463
|
+
sdk: null,
|
|
21464
|
+
capabilities: [],
|
|
21465
|
+
origins: []
|
|
21466
|
+
});
|
|
21467
|
+
continue;
|
|
21468
|
+
}
|
|
21469
|
+
const report = reportFor(deps, home, entry);
|
|
21470
|
+
const manifest = report.manifest;
|
|
21471
|
+
summaries.push({
|
|
21472
|
+
...report,
|
|
21473
|
+
name: manifest?.name ?? null,
|
|
21474
|
+
version: manifest?.version ?? null,
|
|
21475
|
+
api: manifest?.api ?? null,
|
|
21476
|
+
sdk: manifest?.sdk ?? null,
|
|
21477
|
+
capabilities: manifest?.capabilities ?? [],
|
|
21478
|
+
origins: manifest?.origins ?? []
|
|
21479
|
+
});
|
|
21480
|
+
}
|
|
21481
|
+
return summaries;
|
|
21482
|
+
}
|
|
21483
|
+
var BLOCK = 512;
|
|
21484
|
+
var decoder2 = new TextDecoder;
|
|
21485
|
+
function fieldText(header, offset, length) {
|
|
21486
|
+
const slice = header.subarray(offset, offset + length);
|
|
21487
|
+
const end = slice.indexOf(0);
|
|
21488
|
+
return decoder2.decode(end === -1 ? slice : slice.subarray(0, end));
|
|
21489
|
+
}
|
|
21490
|
+
function fieldOctal(header, offset, length) {
|
|
21491
|
+
const text = fieldText(header, offset, length).trim();
|
|
21492
|
+
if (text.length === 0)
|
|
21493
|
+
return 0;
|
|
21494
|
+
const value = Number.parseInt(text, 8);
|
|
21495
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : -1;
|
|
21496
|
+
}
|
|
21497
|
+
function isSafeArchivePath(path) {
|
|
21498
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("\x00"))
|
|
21499
|
+
return false;
|
|
21500
|
+
if (path.includes("\\"))
|
|
21501
|
+
return false;
|
|
21502
|
+
return !path.split("/").includes("..");
|
|
21503
|
+
}
|
|
21504
|
+
function readTar(bytes) {
|
|
21505
|
+
const files = new Map;
|
|
21506
|
+
let total = 0;
|
|
21507
|
+
let longName = null;
|
|
21508
|
+
let offset = 0;
|
|
21509
|
+
while (offset + BLOCK <= bytes.length) {
|
|
21510
|
+
const header = bytes.subarray(offset, offset + BLOCK);
|
|
21511
|
+
if (header.every((byte) => byte === 0))
|
|
21512
|
+
break;
|
|
21513
|
+
const size = fieldOctal(header, 124, 12);
|
|
21514
|
+
if (size < 0)
|
|
21515
|
+
throw new GatewayError("BAD_REQUEST", "archive has an unreadable entry size");
|
|
21516
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
21517
|
+
const name = fieldText(header, 0, 100);
|
|
21518
|
+
const prefix = fieldText(header, 345, 155);
|
|
21519
|
+
offset += BLOCK;
|
|
21520
|
+
total += size;
|
|
21521
|
+
if (total > MAX_PLUGIN_BYTES) {
|
|
21522
|
+
throw new GatewayError("BAD_REQUEST", `archive unpacks to more than ${MAX_PLUGIN_BYTES} bytes`);
|
|
21523
|
+
}
|
|
21524
|
+
if (offset + size > bytes.length) {
|
|
21525
|
+
throw new GatewayError("BAD_REQUEST", `archive is truncated: entry ${name || "(unnamed)"} declares ${size} bytes and the archive ends before them`);
|
|
21526
|
+
}
|
|
21527
|
+
const data = bytes.subarray(offset, offset + size);
|
|
21528
|
+
offset += Math.ceil(size / BLOCK) * BLOCK;
|
|
21529
|
+
if (type === "1" || type === "2") {
|
|
21530
|
+
throw new GatewayError("BAD_REQUEST", `archive contains a link (${name}); plugin packages must be plain files`);
|
|
21531
|
+
}
|
|
21532
|
+
if (type === "L") {
|
|
21533
|
+
longName = decoder2.decode(data).replace(/\0+$/, "");
|
|
21534
|
+
continue;
|
|
21535
|
+
}
|
|
21536
|
+
if (type === "K" || type === "x" || type === "g")
|
|
21537
|
+
continue;
|
|
21538
|
+
if (type !== "0" && type !== "\x00") {
|
|
21539
|
+
longName = null;
|
|
21540
|
+
continue;
|
|
21541
|
+
}
|
|
21542
|
+
const path = longName ?? (prefix.length > 0 ? `${prefix}/${name}` : name);
|
|
21543
|
+
longName = null;
|
|
21544
|
+
if (!isSafeArchivePath(path)) {
|
|
21545
|
+
throw new GatewayError("BAD_REQUEST", `archive contains an unsafe path: ${path}`);
|
|
21546
|
+
}
|
|
21547
|
+
files.set(path, new Uint8Array(data));
|
|
21548
|
+
}
|
|
21549
|
+
return files;
|
|
21550
|
+
}
|
|
21551
|
+
function gunzipIfNeeded(bytes) {
|
|
21552
|
+
if (bytes[0] !== 31 || bytes[1] !== 139)
|
|
21553
|
+
return bytes;
|
|
21554
|
+
try {
|
|
21555
|
+
return Bun.gunzipSync(new Uint8Array(bytes));
|
|
21556
|
+
} catch (error51) {
|
|
21557
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21558
|
+
throw new GatewayError("BAD_REQUEST", `could not decompress the archive: ${detail}`);
|
|
21559
|
+
}
|
|
21560
|
+
}
|
|
21561
|
+
function stripRoot(files) {
|
|
21562
|
+
const roots = new Set;
|
|
21563
|
+
for (const path of files.keys()) {
|
|
21564
|
+
const slash = path.indexOf("/");
|
|
21565
|
+
if (slash <= 0)
|
|
21566
|
+
return { files, claimedId: null };
|
|
21567
|
+
roots.add(path.slice(0, slash));
|
|
21568
|
+
}
|
|
21569
|
+
const [only] = [...roots];
|
|
21570
|
+
if (roots.size !== 1 || only === undefined)
|
|
21571
|
+
return { files, claimedId: null };
|
|
21572
|
+
const stripped = new Map;
|
|
21573
|
+
for (const [path, bytes] of files)
|
|
21574
|
+
stripped.set(path.slice(only.length + 1), bytes);
|
|
21575
|
+
return { files: stripped, claimedId: only === NPM_TARBALL_ROOT ? null : only };
|
|
21576
|
+
}
|
|
21577
|
+
function readTree(fs, dir, prefix = "") {
|
|
21578
|
+
const files = new Map;
|
|
21579
|
+
let total = 0;
|
|
21580
|
+
for (const entry of fs.readdir(dir)) {
|
|
21581
|
+
const path = join3(dir, entry);
|
|
21582
|
+
const relative = prefix.length === 0 ? entry : `${prefix}/${entry}`;
|
|
21583
|
+
if (fs.isDirectory(path)) {
|
|
21584
|
+
for (const [nested, bytes2] of readTree(fs, path, relative))
|
|
21585
|
+
files.set(nested, bytes2);
|
|
21586
|
+
continue;
|
|
21587
|
+
}
|
|
21588
|
+
const bytes = fs.readBytes(path);
|
|
21589
|
+
if (bytes === null)
|
|
21590
|
+
continue;
|
|
21591
|
+
total += bytes.byteLength;
|
|
21592
|
+
if (total > MAX_PLUGIN_BYTES) {
|
|
21593
|
+
throw new GatewayError("BAD_REQUEST", `${dir} holds more than ${MAX_PLUGIN_BYTES} bytes`);
|
|
21594
|
+
}
|
|
21595
|
+
files.set(relative, bytes);
|
|
21596
|
+
}
|
|
21597
|
+
return files;
|
|
21598
|
+
}
|
|
21599
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
21600
|
+
var NPM_PACKUMENT_ACCEPT = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
|
|
21601
|
+
var NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
21602
|
+
var EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
21603
|
+
var SRI_ALGORITHMS = ["sha512", "sha384", "sha256"];
|
|
21604
|
+
function asRecord(value) {
|
|
21605
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
21606
|
+
return null;
|
|
21607
|
+
return value;
|
|
21608
|
+
}
|
|
21609
|
+
function asString(value) {
|
|
21610
|
+
return typeof value === "string" ? value : null;
|
|
21611
|
+
}
|
|
21612
|
+
function parseNpmSpec(spec) {
|
|
21613
|
+
const at = spec.lastIndexOf("@");
|
|
21614
|
+
const name = at <= 0 ? spec : spec.slice(0, at);
|
|
21615
|
+
const version2 = at <= 0 ? null : spec.slice(at + 1);
|
|
21616
|
+
if (name.length > 214 || !NPM_NAME_PATTERN.test(name))
|
|
21617
|
+
return null;
|
|
21618
|
+
return { name, version: version2 };
|
|
21619
|
+
}
|
|
21620
|
+
function registryUrl(deps) {
|
|
21621
|
+
const raw = deps.registry ?? DEFAULT_NPM_REGISTRY;
|
|
21622
|
+
let url2;
|
|
21623
|
+
try {
|
|
21624
|
+
url2 = new URL(raw);
|
|
21625
|
+
} catch {
|
|
21626
|
+
throw new GatewayError("BAD_REQUEST", `registry ${raw} is not a URL`);
|
|
21627
|
+
}
|
|
21628
|
+
if (url2.protocol !== "https:") {
|
|
21629
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; ${raw} is not`);
|
|
21630
|
+
}
|
|
21631
|
+
return url2;
|
|
21632
|
+
}
|
|
21633
|
+
function packumentUrl(registry2, name) {
|
|
21634
|
+
return `${registry2.href.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
|
|
21635
|
+
}
|
|
21636
|
+
function resolveDist(document, spec) {
|
|
21637
|
+
const root = asRecord(document);
|
|
21638
|
+
if (root === null) {
|
|
21639
|
+
throw new GatewayError("BAD_REQUEST", `the registry's answer for ${spec.name} is not a package`);
|
|
21640
|
+
}
|
|
21641
|
+
const versions2 = asRecord(root.versions);
|
|
21642
|
+
if (versions2 === null) {
|
|
21643
|
+
throw new GatewayError("BAD_REQUEST", `the registry lists no versions of ${spec.name}`);
|
|
21644
|
+
}
|
|
21645
|
+
let wanted = spec.version;
|
|
21646
|
+
if (wanted === null) {
|
|
21647
|
+
const tags = asRecord(root["dist-tags"]);
|
|
21648
|
+
wanted = tags === null ? null : asString(tags.latest);
|
|
21649
|
+
if (wanted === null) {
|
|
21650
|
+
throw new GatewayError("BAD_REQUEST", `the registry names no dist-tags.latest for ${spec.name}; install ${spec.name}@<exact version>`);
|
|
21651
|
+
}
|
|
21652
|
+
}
|
|
21653
|
+
const entry = asRecord(versions2[wanted]);
|
|
21654
|
+
if (entry === null) {
|
|
21655
|
+
throw new GatewayError("BAD_REQUEST", `the registry has no version ${wanted} of ${spec.name}`);
|
|
21656
|
+
}
|
|
21657
|
+
const dist = asRecord(entry.dist);
|
|
21658
|
+
const tarball = dist === null ? null : asString(dist.tarball);
|
|
21659
|
+
if (dist === null || tarball === null) {
|
|
21660
|
+
throw new GatewayError("BAD_REQUEST", `the registry's entry for ${spec.name}@${wanted} names no tarball`);
|
|
21661
|
+
}
|
|
21662
|
+
return {
|
|
21663
|
+
version: wanted,
|
|
21664
|
+
dist: { tarball, integrity: asString(dist.integrity), shasum: asString(dist.shasum) }
|
|
21665
|
+
};
|
|
21666
|
+
}
|
|
21667
|
+
function checkTarballUrl(tarball, registry2, what) {
|
|
21668
|
+
let url2;
|
|
21669
|
+
try {
|
|
21670
|
+
url2 = new URL(tarball);
|
|
21671
|
+
} catch {
|
|
21672
|
+
throw new GatewayError("BAD_REQUEST", `the registry gave ${what} a tarball that is not a URL`);
|
|
21673
|
+
}
|
|
21674
|
+
if (url2.protocol !== "https:") {
|
|
21675
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; the registry points ${what} at ${tarball}`);
|
|
21676
|
+
}
|
|
21677
|
+
if (url2.host !== registry2.host) {
|
|
21678
|
+
throw new GatewayError("BAD_REQUEST", `registry ${registry2.host} points ${what} at ${url2.host}; ` + "install that URL directly if it is the one you meant");
|
|
21679
|
+
}
|
|
21680
|
+
}
|
|
21681
|
+
function digestsFor(dist, what) {
|
|
21682
|
+
const digests = [];
|
|
21683
|
+
if (dist.integrity !== null) {
|
|
21684
|
+
for (const entry of dist.integrity.trim().split(/\s+/)) {
|
|
21685
|
+
const dash = entry.indexOf("-");
|
|
21686
|
+
const algorithm = dash === -1 ? "" : entry.slice(0, dash);
|
|
21687
|
+
const value = dash === -1 ? "" : entry.slice(dash + 1);
|
|
21688
|
+
if (value.length === 0)
|
|
21689
|
+
continue;
|
|
21690
|
+
const known = SRI_ALGORITHMS.find((candidate) => candidate === algorithm);
|
|
21691
|
+
if (known === undefined)
|
|
21692
|
+
continue;
|
|
21693
|
+
digests.push({
|
|
21694
|
+
algorithm: known,
|
|
21695
|
+
expected: Buffer2.from(value, "base64").toString("hex"),
|
|
21696
|
+
source: `${known} integrity`
|
|
21697
|
+
});
|
|
21698
|
+
}
|
|
21699
|
+
}
|
|
21700
|
+
if (digests.length === 0 && dist.shasum !== null && /^[0-9a-f]{40}$/i.test(dist.shasum)) {
|
|
21701
|
+
digests.push({ algorithm: "sha1", expected: dist.shasum.toLowerCase(), source: "sha1 shasum" });
|
|
21702
|
+
}
|
|
21703
|
+
if (digests.length === 0) {
|
|
21704
|
+
throw new GatewayError("BAD_REQUEST", `the registry advertises no usable integrity or shasum for ${what}; ` + "refusing to install bytes nothing vouches for");
|
|
21705
|
+
}
|
|
21706
|
+
return digests;
|
|
21707
|
+
}
|
|
21708
|
+
function verifyDigests(bytes, digests, what) {
|
|
21709
|
+
for (const digest of digests) {
|
|
21710
|
+
const actual = new Bun.CryptoHasher(digest.algorithm).update(bytes).digest("hex");
|
|
21711
|
+
if (actual === digest.expected)
|
|
21712
|
+
return;
|
|
21713
|
+
}
|
|
21714
|
+
const first = digests[0];
|
|
21715
|
+
throw new GatewayError("BAD_REQUEST", `${what} does not match the ${first?.source ?? "digest"} the registry advertised; ` + "the download was corrupted or the registry served something else");
|
|
21716
|
+
}
|
|
21717
|
+
async function npmPayload(deps, spec) {
|
|
21718
|
+
if (spec.version !== null && !EXACT_VERSION_PATTERN.test(spec.version)) {
|
|
21719
|
+
throw new GatewayError("BAD_REQUEST", `${spec.name}@${spec.version} is not an exact version; this installer resolves no ranges ` + `or tags \u2014 install ${spec.name}@1.2.3, or ${spec.name} for the registry's latest`);
|
|
21720
|
+
}
|
|
21721
|
+
const fetchBytes = deps.fetchBytes;
|
|
21722
|
+
if (fetchBytes === undefined) {
|
|
21723
|
+
throw new GatewayError("BAD_REQUEST", "this caller cannot install from a registry");
|
|
21724
|
+
}
|
|
21725
|
+
const registry2 = registryUrl(deps);
|
|
21726
|
+
const document = parseJson(await fetchBytes(packumentUrl(registry2, spec.name), NPM_PACKUMENT_ACCEPT), `the registry's answer for ${spec.name}`);
|
|
21727
|
+
const { version: version2, dist } = resolveDist(document, spec);
|
|
21728
|
+
const what = `${spec.name}@${version2}`;
|
|
21729
|
+
checkTarballUrl(dist.tarball, registry2, what);
|
|
21730
|
+
const digests = digestsFor(dist, what);
|
|
21731
|
+
const bytes = await fetchBytes(dist.tarball);
|
|
21732
|
+
verifyDigests(bytes, digests, what);
|
|
21733
|
+
return stripRoot(readTar(gunzipIfNeeded(bytes)));
|
|
21734
|
+
}
|
|
21735
|
+
function parseJson(bytes, what) {
|
|
21736
|
+
try {
|
|
21737
|
+
return JSON.parse(decoder2.decode(bytes));
|
|
21738
|
+
} catch (error51) {
|
|
21739
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21740
|
+
throw new GatewayError("BAD_REQUEST", `${what} is not valid JSON: ${detail}`);
|
|
21741
|
+
}
|
|
21742
|
+
}
|
|
21743
|
+
async function loadPayload(deps, spec) {
|
|
21744
|
+
if (spec.startsWith("http://")) {
|
|
21745
|
+
throw new GatewayError("BAD_REQUEST", "plugins may only be installed over https");
|
|
21746
|
+
}
|
|
21747
|
+
if (spec.startsWith("https://")) {
|
|
21748
|
+
if (deps.fetchBytes === undefined) {
|
|
21749
|
+
throw new GatewayError("BAD_REQUEST", "this caller cannot install from a URL");
|
|
21750
|
+
}
|
|
21751
|
+
return stripRoot(readTar(gunzipIfNeeded(await deps.fetchBytes(spec))));
|
|
21752
|
+
}
|
|
21753
|
+
const source = resolve2(spec);
|
|
21754
|
+
if (deps.fs.isDirectory(source)) {
|
|
21755
|
+
const files = readTree(deps.fs, source);
|
|
21756
|
+
const claimed = basename2(source);
|
|
21757
|
+
return { files, claimedId: ID_PATTERN2.test(claimed) ? claimed : null };
|
|
21758
|
+
}
|
|
21759
|
+
const bytes = deps.fs.readBytes(source);
|
|
21760
|
+
if (bytes !== null)
|
|
21761
|
+
return stripRoot(readTar(gunzipIfNeeded(bytes)));
|
|
21762
|
+
const npm = parseNpmSpec(spec);
|
|
21763
|
+
if (npm !== null)
|
|
21764
|
+
return npmPayload(deps, npm);
|
|
21765
|
+
throw new GatewayError("BAD_REQUEST", `no directory or archive at ${source}, and "${spec}" is not a package name`);
|
|
21766
|
+
}
|
|
21767
|
+
async function installPlugin(deps, root, spec) {
|
|
21768
|
+
const payload = await loadPayload(deps, spec);
|
|
21769
|
+
const raw = payload.files.get(MANIFEST_FILENAME);
|
|
21770
|
+
if (raw === undefined) {
|
|
21771
|
+
throw new GatewayError("BAD_REQUEST", `${spec} has no ${MANIFEST_FILENAME} at its root`);
|
|
21772
|
+
}
|
|
21773
|
+
let document;
|
|
21774
|
+
try {
|
|
21775
|
+
document = JSON.parse(decoder2.decode(raw));
|
|
21776
|
+
} catch (error51) {
|
|
21777
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21778
|
+
throw new GatewayError("BAD_REQUEST", `${MANIFEST_FILENAME} is not valid JSON: ${detail}`);
|
|
21779
|
+
}
|
|
21780
|
+
const parsed = safeParseManifest(document);
|
|
21781
|
+
if (!parsed.ok)
|
|
21782
|
+
throw new GatewayError("BAD_REQUEST", `invalid ${MANIFEST_FILENAME}: ${parsed.reason}`);
|
|
21783
|
+
const manifest = parsed.manifest;
|
|
21784
|
+
const targetId = payload.claimedId ?? manifest.id;
|
|
21785
|
+
if (manifest.id !== targetId) {
|
|
21786
|
+
throw new GatewayError("BAD_REQUEST", `manifest id ${manifest.id} does not match its directory ${targetId}; ` + "rename one of them so the gateway loads what you installed");
|
|
21787
|
+
}
|
|
21788
|
+
const dir = pluginsDir(root);
|
|
21789
|
+
const target = join3(dir, targetId);
|
|
21790
|
+
const replaced = deps.fs.isDirectory(target);
|
|
21791
|
+
const staging = join3(dir, `.staging-${targetId}`);
|
|
21792
|
+
deps.fs.rm(staging);
|
|
21793
|
+
try {
|
|
21794
|
+
for (const [path, bytes] of payload.files) {
|
|
21795
|
+
const destination = join3(staging, path);
|
|
21796
|
+
deps.fs.mkdir(join3(destination, ".."));
|
|
21797
|
+
deps.fs.writeBytes(destination, bytes);
|
|
21798
|
+
}
|
|
21799
|
+
deps.fs.rm(target);
|
|
21800
|
+
deps.fs.rename(staging, target);
|
|
21801
|
+
} finally {
|
|
21802
|
+
deps.fs.rm(staging);
|
|
21803
|
+
}
|
|
21804
|
+
return {
|
|
21805
|
+
id: targetId,
|
|
21806
|
+
name: manifest.name,
|
|
21807
|
+
version: manifest.version,
|
|
21808
|
+
path: target,
|
|
21809
|
+
replaced,
|
|
21810
|
+
restartRequired: true
|
|
21811
|
+
};
|
|
21812
|
+
}
|
|
21813
|
+
function removePlugin(deps, root, id, options = {}) {
|
|
21814
|
+
if (!ID_PATTERN2.test(id)) {
|
|
21815
|
+
throw new GatewayError("BAD_REQUEST", `"${id}" is not a valid plugin id`);
|
|
21816
|
+
}
|
|
21817
|
+
const target = join3(pluginsDir(root), id);
|
|
21818
|
+
const present = deps.fs.isDirectory(target);
|
|
21819
|
+
const purge = options.purge === true;
|
|
21820
|
+
if (purge && deps.store === undefined) {
|
|
21821
|
+
throw new GatewayError("INTERNAL", "purging a plugin's data needs a database");
|
|
21822
|
+
}
|
|
21823
|
+
const tables = purge && deps.store !== undefined ? deps.store.plugins.listTables(id) : [];
|
|
21824
|
+
if (!present && tables.length === 0) {
|
|
21825
|
+
throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
|
|
21826
|
+
}
|
|
21827
|
+
if (present)
|
|
21828
|
+
deps.fs.rm(target);
|
|
21829
|
+
if (purge && deps.store !== undefined)
|
|
21830
|
+
deps.store.plugins.dropAll(id);
|
|
21831
|
+
return { id, removed: present, droppedTables: tables };
|
|
21832
|
+
}
|
|
21833
|
+
function orphanPluginTables(deps, root, store) {
|
|
21834
|
+
const installed = listPlugins(deps, root).map((plugin) => plugin.id);
|
|
21835
|
+
return store.plugins.orphanTables(installed);
|
|
21836
|
+
}
|
|
21837
|
+
|
|
21838
|
+
// packages/control/src/nodeFs.ts
|
|
21839
|
+
function nodeDatabaseFs() {
|
|
21840
|
+
const dirBytes = (dir) => {
|
|
21841
|
+
let entries;
|
|
21842
|
+
try {
|
|
21843
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
|
|
21844
|
+
} catch {
|
|
21845
|
+
return 0;
|
|
21846
|
+
}
|
|
21847
|
+
let total = 0;
|
|
21848
|
+
for (const entry of entries) {
|
|
21849
|
+
const path = join4(dir, entry.name);
|
|
21850
|
+
if (entry.isDirectory()) {
|
|
21851
|
+
total += dirBytes(path);
|
|
21852
|
+
continue;
|
|
21853
|
+
}
|
|
21854
|
+
try {
|
|
21855
|
+
total += statSync(path).size;
|
|
21856
|
+
} catch {}
|
|
21857
|
+
}
|
|
21858
|
+
return total;
|
|
21859
|
+
};
|
|
21860
|
+
return {
|
|
21861
|
+
readdir: (dir) => {
|
|
21862
|
+
try {
|
|
21863
|
+
return readdirSync(dir);
|
|
21864
|
+
} catch {
|
|
21865
|
+
return [];
|
|
21866
|
+
}
|
|
21867
|
+
},
|
|
21868
|
+
stat: (path) => {
|
|
21869
|
+
try {
|
|
21870
|
+
const stat = statSync(path);
|
|
21871
|
+
return { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
21872
|
+
} catch {
|
|
21873
|
+
return null;
|
|
21874
|
+
}
|
|
21875
|
+
},
|
|
21876
|
+
unlink: (path) => {
|
|
21877
|
+
rmSync(path, { force: true });
|
|
21878
|
+
},
|
|
21879
|
+
rename: (from, to) => renameSync(from, to),
|
|
21880
|
+
copyFile: (from, to) => copyFileSync(from, to),
|
|
21881
|
+
mkdir: (dir) => {
|
|
21882
|
+
mkdirSync(dir, { recursive: true });
|
|
21883
|
+
},
|
|
21884
|
+
realpath: (path) => {
|
|
21885
|
+
try {
|
|
21886
|
+
return realpathSync(path);
|
|
21887
|
+
} catch {
|
|
21888
|
+
return null;
|
|
21889
|
+
}
|
|
21890
|
+
},
|
|
21891
|
+
freeBytes: (dir) => {
|
|
21892
|
+
try {
|
|
21893
|
+
const stat = statfsSync(dir);
|
|
21894
|
+
return Number(stat.bavail) * Number(stat.bsize);
|
|
21895
|
+
} catch {
|
|
21896
|
+
return null;
|
|
21897
|
+
}
|
|
21898
|
+
},
|
|
21899
|
+
dirBytes
|
|
21900
|
+
};
|
|
21901
|
+
}
|
|
21902
|
+
function nodePluginFs() {
|
|
21903
|
+
return {
|
|
21904
|
+
readdir: (dir) => {
|
|
21905
|
+
try {
|
|
21906
|
+
return readdirSync(dir);
|
|
21907
|
+
} catch {
|
|
21908
|
+
return [];
|
|
21909
|
+
}
|
|
21910
|
+
},
|
|
21911
|
+
readText: (path) => {
|
|
21912
|
+
try {
|
|
21913
|
+
return readFileSync(path, "utf8");
|
|
21914
|
+
} catch {
|
|
21915
|
+
return null;
|
|
21916
|
+
}
|
|
21917
|
+
},
|
|
21918
|
+
readBytes: (path) => {
|
|
21919
|
+
try {
|
|
21920
|
+
return new Uint8Array(readFileSync(path));
|
|
21921
|
+
} catch {
|
|
21922
|
+
return null;
|
|
21923
|
+
}
|
|
21924
|
+
},
|
|
21925
|
+
writeBytes: (path, bytes) => {
|
|
21926
|
+
writeFileSync(path, bytes);
|
|
21927
|
+
},
|
|
21928
|
+
isDirectory: (path) => {
|
|
21929
|
+
try {
|
|
21930
|
+
return statSync(path).isDirectory();
|
|
21931
|
+
} catch {
|
|
21932
|
+
return false;
|
|
21933
|
+
}
|
|
21934
|
+
},
|
|
21935
|
+
isFile: (path) => {
|
|
21936
|
+
try {
|
|
21937
|
+
return statSync(path).isFile();
|
|
21938
|
+
} catch {
|
|
21939
|
+
return false;
|
|
21940
|
+
}
|
|
21941
|
+
},
|
|
21942
|
+
mkdir: (dir) => {
|
|
21943
|
+
mkdirSync(dir, { recursive: true });
|
|
21944
|
+
},
|
|
21945
|
+
rm: (path) => {
|
|
21946
|
+
rmSync(path, { force: true, recursive: true });
|
|
21947
|
+
},
|
|
21948
|
+
rename: (from, to) => renameSync(from, to)
|
|
21949
|
+
};
|
|
21950
|
+
}
|
|
21951
|
+
function nodeFetchBytes(options = {}) {
|
|
21952
|
+
const impl = options.fetchImpl ?? fetch;
|
|
21953
|
+
const maxBytes = options.maxBytes ?? MAX_PLUGIN_BYTES;
|
|
21954
|
+
return async (url2, accept) => {
|
|
21955
|
+
if (!url2.startsWith("https://")) {
|
|
21956
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be fetched over https; ${url2} is not`);
|
|
21957
|
+
}
|
|
21958
|
+
const response = await impl(url2, {
|
|
21959
|
+
headers: accept === undefined ? {} : { accept },
|
|
21960
|
+
redirect: "follow"
|
|
21961
|
+
});
|
|
21962
|
+
if (!response.ok) {
|
|
21963
|
+
throw new GatewayError("BAD_REQUEST", `${url2} answered ${response.status}`);
|
|
21964
|
+
}
|
|
21965
|
+
const reader = response.body?.getReader();
|
|
21966
|
+
if (reader === undefined)
|
|
21967
|
+
return new Uint8Array(0);
|
|
21968
|
+
const chunks = [];
|
|
21969
|
+
let total = 0;
|
|
21970
|
+
for (;; ) {
|
|
21971
|
+
const { done, value } = await reader.read();
|
|
21972
|
+
if (done)
|
|
21973
|
+
break;
|
|
21974
|
+
if (value === undefined)
|
|
21975
|
+
continue;
|
|
21976
|
+
total += value.byteLength;
|
|
21977
|
+
if (total > maxBytes) {
|
|
21978
|
+
await reader.cancel();
|
|
21979
|
+
throw new GatewayError("BAD_REQUEST", `${url2} sent more than ${maxBytes} bytes`);
|
|
21980
|
+
}
|
|
21981
|
+
chunks.push(value);
|
|
21982
|
+
}
|
|
21983
|
+
const bytes = new Uint8Array(total);
|
|
21984
|
+
let offset = 0;
|
|
21985
|
+
for (const chunk of chunks) {
|
|
21986
|
+
bytes.set(chunk, offset);
|
|
21987
|
+
offset += chunk.byteLength;
|
|
21988
|
+
}
|
|
21989
|
+
return bytes;
|
|
21990
|
+
};
|
|
19784
21991
|
}
|
|
19785
21992
|
// packages/control/src/oauth/pkce.ts
|
|
19786
21993
|
function randomToken() {
|
|
@@ -20692,8 +22899,8 @@ function createRefresher(deps) {
|
|
|
20692
22899
|
};
|
|
20693
22900
|
}
|
|
20694
22901
|
// packages/control/src/quota/history.ts
|
|
20695
|
-
var
|
|
20696
|
-
var
|
|
22902
|
+
var DAY_MS2 = 24 * 60 * 60 * 1000;
|
|
22903
|
+
var HOUR_MS3 = 3600000;
|
|
20697
22904
|
async function gatewayRatesFor(store, windows) {
|
|
20698
22905
|
const rates = [];
|
|
20699
22906
|
for (const window of windows) {
|
|
@@ -20717,7 +22924,7 @@ async function gatewayRatesFor(store, windows) {
|
|
|
20717
22924
|
rates.push({
|
|
20718
22925
|
credentialId: window.credentialId,
|
|
20719
22926
|
windowType: window.windowType,
|
|
20720
|
-
gatewayRatePerHour: tokens2 / ((window.observedAt - since) /
|
|
22927
|
+
gatewayRatePerHour: tokens2 / ((window.observedAt - since) / HOUR_MS3)
|
|
20721
22928
|
});
|
|
20722
22929
|
}
|
|
20723
22930
|
return rates;
|
|
@@ -20725,7 +22932,7 @@ async function gatewayRatesFor(store, windows) {
|
|
|
20725
22932
|
async function quotaHistory(deps, input) {
|
|
20726
22933
|
const now = deps.now();
|
|
20727
22934
|
const settings = await deps.store.config.getSettings();
|
|
20728
|
-
const oldest = now - settings.logRetentionDays *
|
|
22935
|
+
const oldest = now - settings.logRetentionDays * DAY_MS2;
|
|
20729
22936
|
const since = Math.max(optionalNumber(input.since, oldest), oldest);
|
|
20730
22937
|
const until = Math.min(optionalNumber(input.until, now), now);
|
|
20731
22938
|
const raw = input.credentialId?.trim();
|
|
@@ -20748,9 +22955,13 @@ async function getSettings(store) {
|
|
|
20748
22955
|
return store.config.getSettings();
|
|
20749
22956
|
}
|
|
20750
22957
|
async function putSettings(store, input) {
|
|
20751
|
-
const
|
|
20752
|
-
|
|
20753
|
-
|
|
22958
|
+
const { snapshotKeepLatest, snapshotMaxAgeDays, ...rest } = parseOrThrow(settingsSchema, input);
|
|
22959
|
+
const patch = {
|
|
22960
|
+
...rest,
|
|
22961
|
+
...snapshotKeepLatest === undefined ? {} : { snapshotKeepLatest },
|
|
22962
|
+
...snapshotMaxAgeDays === undefined ? {} : { snapshotMaxAgeDays }
|
|
22963
|
+
};
|
|
22964
|
+
return store.config.putSettings(patch);
|
|
20754
22965
|
}
|
|
20755
22966
|
// packages/control/src/setup.ts
|
|
20756
22967
|
var KEY_PLACEHOLDER = "<your OmniGateway key>";
|
|
@@ -20863,7 +23074,7 @@ function opencodeConfig(described, input, mapping) {
|
|
|
20863
23074
|
return { path: "opencode.json", contents };
|
|
20864
23075
|
}
|
|
20865
23076
|
// packages/control/src/tail.ts
|
|
20866
|
-
import { closeSync, fstatSync, openSync, readSync, statSync } from "fs";
|
|
23077
|
+
import { closeSync, fstatSync, openSync, readSync, statSync as statSync2 } from "fs";
|
|
20867
23078
|
var CHUNK = 64 * 1024;
|
|
20868
23079
|
var MAX_BYTES = 8 * 1024 * 1024;
|
|
20869
23080
|
function tailFile(path, lines) {
|
|
@@ -20901,7 +23112,7 @@ function tailFile(path, lines) {
|
|
|
20901
23112
|
}
|
|
20902
23113
|
function fileExists(path) {
|
|
20903
23114
|
try {
|
|
20904
|
-
return
|
|
23115
|
+
return statSync2(path).isFile();
|
|
20905
23116
|
} catch {
|
|
20906
23117
|
return false;
|
|
20907
23118
|
}
|
|
@@ -20985,9 +23196,9 @@ function requirePositional(parsed, index, name) {
|
|
|
20985
23196
|
}
|
|
20986
23197
|
|
|
20987
23198
|
// apps/cli/src/context.ts
|
|
20988
|
-
import { existsSync, readFileSync } from "fs";
|
|
23199
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
20989
23200
|
import { homedir } from "os";
|
|
20990
|
-
import { isAbsolute, join, resolve } from "path";
|
|
23201
|
+
import { isAbsolute, join as join5, resolve as resolve3 } from "path";
|
|
20991
23202
|
class CliError extends Error {
|
|
20992
23203
|
exitCode;
|
|
20993
23204
|
constructor(message, exitCode = 1) {
|
|
@@ -20996,21 +23207,21 @@ class CliError extends Error {
|
|
|
20996
23207
|
this.exitCode = exitCode;
|
|
20997
23208
|
}
|
|
20998
23209
|
}
|
|
20999
|
-
var DEFAULT_ROOT =
|
|
23210
|
+
var DEFAULT_ROOT = join5(homedir(), ".config", "omnigateway");
|
|
21000
23211
|
function looksLikeRoot(dir) {
|
|
21001
|
-
return existsSync(
|
|
23212
|
+
return existsSync(join5(dir, ".env")) || existsSync(join5(dir, "omnigateway.db")) || existsSync(join5(dir, "apps", "gateway", "src", "index.ts"));
|
|
21002
23213
|
}
|
|
21003
23214
|
function resolveRoot(flags, env2, cwd) {
|
|
21004
23215
|
const withEnvFile = (root, source) => {
|
|
21005
|
-
const envFile =
|
|
23216
|
+
const envFile = join5(root, ".env");
|
|
21006
23217
|
return { root, source, envFile: existsSync(envFile) ? envFile : null };
|
|
21007
23218
|
};
|
|
21008
23219
|
if (flags.root !== undefined && flags.root.length > 0) {
|
|
21009
|
-
return withEnvFile(
|
|
23220
|
+
return withEnvFile(resolve3(cwd, flags.root), "flag");
|
|
21010
23221
|
}
|
|
21011
23222
|
const fromEnv = env2.OMNI_ROOT;
|
|
21012
23223
|
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
21013
|
-
return withEnvFile(
|
|
23224
|
+
return withEnvFile(resolve3(cwd, fromEnv), "env");
|
|
21014
23225
|
}
|
|
21015
23226
|
if (looksLikeRoot(cwd))
|
|
21016
23227
|
return withEnvFile(cwd, "cwd");
|
|
@@ -21041,7 +23252,7 @@ function createContext(parsed, options = {}) {
|
|
|
21041
23252
|
const processEnv = options.env ?? process.env;
|
|
21042
23253
|
const cwd = options.cwd ?? process.cwd();
|
|
21043
23254
|
const root = resolveRoot({ root: stringFlag(parsed.values, "root") }, processEnv, cwd);
|
|
21044
|
-
const fileEnv = root.envFile === null ? {} : parseEnvFile(
|
|
23255
|
+
const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync2(root.envFile, "utf8"));
|
|
21045
23256
|
const env2 = { ...processEnv, ...fileEnv };
|
|
21046
23257
|
const rawDbFlag = stringFlag(parsed.values, "db");
|
|
21047
23258
|
const dbFlag = rawDbFlag === undefined || rawDbFlag.trim().length === 0 ? undefined : rawDbFlag;
|
|
@@ -21057,7 +23268,7 @@ function createContext(parsed, options = {}) {
|
|
|
21057
23268
|
configError = error51 instanceof Error ? error51.message : "invalid configuration";
|
|
21058
23269
|
}
|
|
21059
23270
|
const configuredPath = dbFlag ?? config2?.databasePath ?? "omnigateway.db";
|
|
21060
|
-
const databasePath = isAbsolute(configuredPath) ? configuredPath :
|
|
23271
|
+
const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve3(root.root, configuredPath);
|
|
21061
23272
|
const warnings = suppressedDbPath === null ? [] : [
|
|
21062
23273
|
`ignoring OMNI_DB_PATH=${suppressedDbPath} from the environment because --root was given; using ${databasePath}`
|
|
21063
23274
|
];
|
|
@@ -21099,6 +23310,143 @@ function createContext(parsed, options = {}) {
|
|
|
21099
23310
|
};
|
|
21100
23311
|
}
|
|
21101
23312
|
|
|
23313
|
+
// apps/cli/src/command.ts
|
|
23314
|
+
var PROVIDER_TONE = {
|
|
23315
|
+
anthropic: "magenta",
|
|
23316
|
+
openai: "green",
|
|
23317
|
+
kimi: "blue",
|
|
23318
|
+
kilo: "orange",
|
|
23319
|
+
grok: "yellow",
|
|
23320
|
+
custom: "cyan"
|
|
23321
|
+
};
|
|
23322
|
+
function provider(ctx, id) {
|
|
23323
|
+
return paint(ctx, PROVIDER_TONE[id], id);
|
|
23324
|
+
}
|
|
23325
|
+
function state(ctx, ok, text) {
|
|
23326
|
+
return paint(ctx, ok ? "green" : "red", text);
|
|
23327
|
+
}
|
|
23328
|
+
|
|
23329
|
+
// apps/cli/src/commands/bodies.ts
|
|
23330
|
+
var ABSENCE = {
|
|
23331
|
+
none: [
|
|
23332
|
+
"not captured",
|
|
23333
|
+
"Body capture was not running for this request. It needs OMNI_BODY_LOGGING_ALLOWED in the environment and bodyLoggingEnabled turned on, and the calling key must not have opted out."
|
|
23334
|
+
],
|
|
23335
|
+
missing: [
|
|
23336
|
+
"captured, then lost",
|
|
23337
|
+
"This request was captured, but its artifact is no longer on disk. Retention or the row cap has since pruned it, or something removed the file underneath the gateway."
|
|
23338
|
+
],
|
|
23339
|
+
corrupt: [
|
|
23340
|
+
"captured, but unreadable",
|
|
23341
|
+
"This request was captured and the artifact is still on disk, but it failed its checksum or would not decrypt. Changing OMNI_ENCRYPTION_KEY invalidates every artifact written under the old one."
|
|
23342
|
+
]
|
|
23343
|
+
};
|
|
23344
|
+
var encoder3 = new TextEncoder;
|
|
23345
|
+
function payloadBytes(value) {
|
|
23346
|
+
if (value === null || value === undefined)
|
|
23347
|
+
return 0;
|
|
23348
|
+
return encoder3.encode(JSON.stringify(value)).length;
|
|
23349
|
+
}
|
|
23350
|
+
function frame(read) {
|
|
23351
|
+
return fields([
|
|
23352
|
+
["STATE", read.detailState],
|
|
23353
|
+
["CAPTURED", formatTime(read.at)],
|
|
23354
|
+
["SIZE", `${formatBytes(read.sizeBytes)} on disk`],
|
|
23355
|
+
["TRUNCATED", read.truncated ? "yes" : "no"]
|
|
23356
|
+
]);
|
|
23357
|
+
}
|
|
23358
|
+
function pairs(ctx, artifact) {
|
|
23359
|
+
const frames = artifact.attempts.some((attempt) => attempt.streamChunks !== null);
|
|
23360
|
+
const label2 = (text, truncated) => truncated ? `${text} (truncated)` : text;
|
|
23361
|
+
return table([
|
|
23362
|
+
{ header: "PAIR" },
|
|
23363
|
+
{ header: "PROVIDER" },
|
|
23364
|
+
{ header: "REQUEST", align: "right" },
|
|
23365
|
+
{ header: "RESPONSE", align: "right" },
|
|
23366
|
+
...frames ? [{ header: "FRAMES", align: "right" }] : [],
|
|
23367
|
+
{ header: "REQUEST IS" }
|
|
23368
|
+
], [
|
|
23369
|
+
[
|
|
23370
|
+
label2("CLIENT", artifact.client.truncated),
|
|
23371
|
+
"",
|
|
23372
|
+
formatBytes(payloadBytes(artifact.client.request)),
|
|
23373
|
+
formatBytes(payloadBytes(artifact.client.response)),
|
|
23374
|
+
...frames ? ["\u2014"] : [],
|
|
23375
|
+
"pre-RTK"
|
|
23376
|
+
],
|
|
23377
|
+
...artifact.attempts.map((attempt) => [
|
|
23378
|
+
label2(`ATTEMPT ${attempt.attempt}`, attempt.truncated),
|
|
23379
|
+
provider(ctx, attempt.provider),
|
|
23380
|
+
formatBytes(payloadBytes(attempt.request)),
|
|
23381
|
+
formatBytes(payloadBytes(attempt.response)),
|
|
23382
|
+
...frames ? [attempt.streamChunks === null ? "\u2014" : String(attempt.streamChunks.length)] : [],
|
|
23383
|
+
"post-RTK"
|
|
23384
|
+
])
|
|
23385
|
+
]);
|
|
23386
|
+
}
|
|
23387
|
+
function block(ctx, heading, value) {
|
|
23388
|
+
const body2 = value === null || value === undefined ? "(nothing recorded for this half \u2014 it never happened, or never completed)" : JSON.stringify(value, null, 2);
|
|
23389
|
+
return `${paint(ctx, "dim", heading)}
|
|
23390
|
+
${body2}`;
|
|
23391
|
+
}
|
|
23392
|
+
function full(ctx, artifact) {
|
|
23393
|
+
const sections = [
|
|
23394
|
+
block(ctx, "CLIENT REQUEST (pre-RTK)", artifact.client.request),
|
|
23395
|
+
block(ctx, "CLIENT RESPONSE", artifact.client.response)
|
|
23396
|
+
];
|
|
23397
|
+
for (const attempt of artifact.attempts) {
|
|
23398
|
+
const name = `ATTEMPT ${attempt.attempt} ${provider(ctx, attempt.provider)}`;
|
|
23399
|
+
sections.push(block(ctx, `${name} REQUEST (post-RTK)`, attempt.request));
|
|
23400
|
+
sections.push(block(ctx, `${name} RESPONSE`, attempt.response));
|
|
23401
|
+
if (attempt.streamChunks !== null) {
|
|
23402
|
+
sections.push(block(ctx, `${name} STREAM FRAMES (${attempt.streamChunks.length})`, attempt.streamChunks));
|
|
23403
|
+
}
|
|
23404
|
+
}
|
|
23405
|
+
if (artifact.error !== null && artifact.error !== undefined) {
|
|
23406
|
+
sections.push(block(ctx, "ERROR", artifact.error));
|
|
23407
|
+
}
|
|
23408
|
+
return sections.join(`
|
|
23409
|
+
|
|
23410
|
+
`);
|
|
23411
|
+
}
|
|
23412
|
+
var bodies = {
|
|
23413
|
+
usage: "bodies <request-id> [--full]",
|
|
23414
|
+
summary: "Show captured bodies for one request; withheld unless --full",
|
|
23415
|
+
options: { full: { type: "boolean" } },
|
|
23416
|
+
async run(args, { ctx, writer }) {
|
|
23417
|
+
const requestId = requirePositional(args, 0, "request id");
|
|
23418
|
+
const read = await readRequestBody(await ctx.store(), requestId);
|
|
23419
|
+
emit(ctx, writer, read, () => {
|
|
23420
|
+
const artifact = read.artifact;
|
|
23421
|
+
if (artifact === null) {
|
|
23422
|
+
const [legend, message] = ABSENCE[read.detailState === "ready" ? "corrupt" : read.detailState];
|
|
23423
|
+
return [frame(read), "", legend, message].join(`
|
|
23424
|
+
`);
|
|
23425
|
+
}
|
|
23426
|
+
const lines = [
|
|
23427
|
+
frame(read),
|
|
23428
|
+
"",
|
|
23429
|
+
pairs(ctx, artifact),
|
|
23430
|
+
"",
|
|
23431
|
+
paint(ctx, "dim", "REQUEST and RESPONSE are the stored payload, after masking and bounding; neither is the size sent over the wire")
|
|
23432
|
+
];
|
|
23433
|
+
if (artifact.attempts.length > 0) {
|
|
23434
|
+
lines.push("", paint(ctx, "dim", "the client request is pre-RTK and every attempt request is post-RTK; they are not the same payload"));
|
|
23435
|
+
}
|
|
23436
|
+
if (artifact.error !== null && artifact.error !== undefined) {
|
|
23437
|
+
lines.push("", "an error was recorded for this request");
|
|
23438
|
+
}
|
|
23439
|
+
if (boolFlag(args.values, "full")) {
|
|
23440
|
+
lines.push("", full(ctx, artifact));
|
|
23441
|
+
} else {
|
|
23442
|
+
lines.push("", "bodies withheld; pass --full to print them, --json for the artifact");
|
|
23443
|
+
}
|
|
23444
|
+
return lines.join(`
|
|
23445
|
+
`);
|
|
23446
|
+
});
|
|
23447
|
+
}
|
|
23448
|
+
};
|
|
23449
|
+
|
|
21102
23450
|
// apps/cli/src/commands/connect.ts
|
|
21103
23451
|
var DEVICE_TIMEOUT_MS = 600000;
|
|
21104
23452
|
var connect = {
|
|
@@ -21146,21 +23494,21 @@ async function pollUntilAuthorized(flows, start, deps) {
|
|
|
21146
23494
|
}
|
|
21147
23495
|
|
|
21148
23496
|
// apps/cli/src/service.ts
|
|
21149
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as
|
|
23497
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
21150
23498
|
import { homedir as homedir2 } from "os";
|
|
21151
|
-
import { dirname, join as
|
|
23499
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
21152
23500
|
var UNIT_NAME2 = "omnigateway.service";
|
|
21153
23501
|
function unitPath(scope) {
|
|
21154
|
-
return scope === "system" ?
|
|
23502
|
+
return scope === "system" ? join6("/etc/systemd/system", UNIT_NAME2) : join6(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
|
|
21155
23503
|
}
|
|
21156
23504
|
function systemctlArgs(scope, args) {
|
|
21157
23505
|
return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
|
|
21158
23506
|
}
|
|
21159
23507
|
function pidFile(stateDir) {
|
|
21160
|
-
return
|
|
23508
|
+
return join6(stateDir, "gateway.pid");
|
|
21161
23509
|
}
|
|
21162
23510
|
function logFile(stateDir) {
|
|
21163
|
-
return
|
|
23511
|
+
return join6(stateDir, "gateway.log");
|
|
21164
23512
|
}
|
|
21165
23513
|
function supervisedLogFile(deps) {
|
|
21166
23514
|
const configured = deps.logFile?.trim();
|
|
@@ -21168,8 +23516,8 @@ function supervisedLogFile(deps) {
|
|
|
21168
23516
|
}
|
|
21169
23517
|
function defaultStateDir(env2) {
|
|
21170
23518
|
const xdg = env2.XDG_STATE_HOME;
|
|
21171
|
-
const base = typeof xdg === "string" && xdg.length > 0 ? xdg :
|
|
21172
|
-
return
|
|
23519
|
+
const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join6(homedir2(), ".local", "state");
|
|
23520
|
+
return join6(base, "omnigateway");
|
|
21173
23521
|
}
|
|
21174
23522
|
function unitFile(input) {
|
|
21175
23523
|
return `[Unit]
|
|
@@ -21180,7 +23528,7 @@ Wants=network-online.target
|
|
|
21180
23528
|
[Service]
|
|
21181
23529
|
Type=simple
|
|
21182
23530
|
WorkingDirectory=${input.root}
|
|
21183
|
-
EnvironmentFile=${
|
|
23531
|
+
EnvironmentFile=${join6(input.root, ".env")}
|
|
21184
23532
|
ExecStart=${input.bun} ${input.entrypoint}
|
|
21185
23533
|
Restart=on-failure
|
|
21186
23534
|
RestartSec=2
|
|
@@ -21196,7 +23544,7 @@ function readPid(stateDir) {
|
|
|
21196
23544
|
const file2 = pidFile(stateDir);
|
|
21197
23545
|
if (!existsSync2(file2))
|
|
21198
23546
|
return null;
|
|
21199
|
-
const pid = Number(
|
|
23547
|
+
const pid = Number(readFileSync3(file2, "utf8").trim());
|
|
21200
23548
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
21201
23549
|
}
|
|
21202
23550
|
function livePid(deps) {
|
|
@@ -21205,20 +23553,20 @@ function livePid(deps) {
|
|
|
21205
23553
|
return null;
|
|
21206
23554
|
if (deps.alive(pid))
|
|
21207
23555
|
return pid;
|
|
21208
|
-
|
|
23556
|
+
rmSync2(pidFile(deps.stateDir), { force: true });
|
|
21209
23557
|
return null;
|
|
21210
23558
|
}
|
|
21211
23559
|
async function status(deps) {
|
|
21212
23560
|
if (unitInstalled(deps)) {
|
|
21213
23561
|
const active = await deps.run(systemctlArgs(deps.scope, ["is-active", UNIT_NAME2]));
|
|
21214
|
-
const
|
|
23562
|
+
const state2 = active.stdout.trim() || active.stderr.trim() || "unknown";
|
|
21215
23563
|
const shown = await deps.run(systemctlArgs(deps.scope, ["show", UNIT_NAME2, "--property=MainPID", "--value"]));
|
|
21216
23564
|
const pid2 = Number(shown.stdout.trim());
|
|
21217
23565
|
return {
|
|
21218
23566
|
supervisor: "systemd",
|
|
21219
|
-
running:
|
|
23567
|
+
running: state2 === "active",
|
|
21220
23568
|
pid: Number.isInteger(pid2) && pid2 > 0 ? pid2 : null,
|
|
21221
|
-
state,
|
|
23569
|
+
state: state2,
|
|
21222
23570
|
unitPath: deps.unitPath,
|
|
21223
23571
|
logFile: null
|
|
21224
23572
|
};
|
|
@@ -21253,16 +23601,16 @@ async function start(deps, input) {
|
|
|
21253
23601
|
healthy: await waitForHealth(deps, input.baseUrl)
|
|
21254
23602
|
};
|
|
21255
23603
|
}
|
|
21256
|
-
|
|
23604
|
+
mkdirSync2(deps.stateDir, { recursive: true });
|
|
21257
23605
|
const file2 = supervisedLogFile(deps);
|
|
21258
|
-
|
|
23606
|
+
mkdirSync2(dirname3(file2), { recursive: true });
|
|
21259
23607
|
const pid = deps.spawn({
|
|
21260
23608
|
argv: input.argv,
|
|
21261
23609
|
cwd: deps.root,
|
|
21262
23610
|
env: { ...input.env, OMNI_LOG_FILE: file2 },
|
|
21263
23611
|
logFile: file2
|
|
21264
23612
|
});
|
|
21265
|
-
|
|
23613
|
+
writeFileSync2(pidFile(deps.stateDir), `${pid}
|
|
21266
23614
|
`);
|
|
21267
23615
|
return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
|
|
21268
23616
|
}
|
|
@@ -21299,7 +23647,7 @@ async function stop(deps) {
|
|
|
21299
23647
|
}
|
|
21300
23648
|
await deps.sleep(STOP_INTERVAL_MS);
|
|
21301
23649
|
}
|
|
21302
|
-
|
|
23650
|
+
rmSync2(pidFile(deps.stateDir), { force: true });
|
|
21303
23651
|
return { supervisor: "pidfile", stopped: true };
|
|
21304
23652
|
}
|
|
21305
23653
|
async function install(deps, input) {
|
|
@@ -21307,8 +23655,8 @@ async function install(deps, input) {
|
|
|
21307
23655
|
if (existsSync2(path) && !input.force) {
|
|
21308
23656
|
throw new Error(`${path} already exists; pass --force to replace it`);
|
|
21309
23657
|
}
|
|
21310
|
-
|
|
21311
|
-
|
|
23658
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
23659
|
+
writeFileSync2(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
|
|
21312
23660
|
const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
|
|
21313
23661
|
let enabled = false;
|
|
21314
23662
|
if (input.enable) {
|
|
@@ -21322,7 +23670,7 @@ async function uninstall(deps) {
|
|
|
21322
23670
|
if (!existsSync2(path))
|
|
21323
23671
|
return { path, removed: false };
|
|
21324
23672
|
await deps.run(systemctlArgs(deps.scope, ["disable", "--now", UNIT_NAME2]));
|
|
21325
|
-
|
|
23673
|
+
rmSync2(path, { force: true });
|
|
21326
23674
|
await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
|
|
21327
23675
|
return { path, removed: true };
|
|
21328
23676
|
}
|
|
@@ -21417,22 +23765,6 @@ var console_ = {
|
|
|
21417
23765
|
}
|
|
21418
23766
|
};
|
|
21419
23767
|
|
|
21420
|
-
// apps/cli/src/command.ts
|
|
21421
|
-
var PROVIDER_TONE = {
|
|
21422
|
-
anthropic: "magenta",
|
|
21423
|
-
openai: "green",
|
|
21424
|
-
kimi: "blue",
|
|
21425
|
-
kilo: "orange",
|
|
21426
|
-
grok: "yellow",
|
|
21427
|
-
custom: "cyan"
|
|
21428
|
-
};
|
|
21429
|
-
function provider(ctx, id) {
|
|
21430
|
-
return paint(ctx, PROVIDER_TONE[id], id);
|
|
21431
|
-
}
|
|
21432
|
-
function state(ctx, ok, text) {
|
|
21433
|
-
return paint(ctx, ok ? "green" : "red", text);
|
|
21434
|
-
}
|
|
21435
|
-
|
|
21436
23768
|
// apps/cli/src/commands/credentials.ts
|
|
21437
23769
|
function condition(credential) {
|
|
21438
23770
|
if (!credential.enabled)
|
|
@@ -21658,6 +23990,9 @@ var credentialsHealth = {
|
|
|
21658
23990
|
|
|
21659
23991
|
// apps/cli/src/commands/db.ts
|
|
21660
23992
|
import { existsSync as existsSync3 } from "fs";
|
|
23993
|
+
async function database(env2) {
|
|
23994
|
+
return { store: await env2.ctx.store(), fs: nodeDatabaseFs(), now: env2.ctx.now };
|
|
23995
|
+
}
|
|
21661
23996
|
var dbMigrate = {
|
|
21662
23997
|
usage: "db migrate",
|
|
21663
23998
|
summary: "Create or upgrade the database schema",
|
|
@@ -21671,8 +24006,234 @@ var dbMigrate = {
|
|
|
21671
24006
|
]));
|
|
21672
24007
|
}
|
|
21673
24008
|
};
|
|
24009
|
+
var dbStats = {
|
|
24010
|
+
usage: "db stats",
|
|
24011
|
+
summary: "Show database size, free pages, and what snapshots are held",
|
|
24012
|
+
async run(_args, env2) {
|
|
24013
|
+
const { ctx, writer } = env2;
|
|
24014
|
+
const overview = await getDatabaseOverview(await database(env2));
|
|
24015
|
+
emit(ctx, writer, overview, () => fields([
|
|
24016
|
+
["database", ctx.databasePath],
|
|
24017
|
+
["size", formatBytes(overview.fileBytes)],
|
|
24018
|
+
["write-ahead log", formatBytes(overview.walBytes)],
|
|
24019
|
+
["captured bodies", `${formatBytes(overview.bodiesBytes)} (never snapshotted)`],
|
|
24020
|
+
["free pages", `${formatBytes(overview.freePageBytes)} \u2014 reclaimed by omni db vacuum`],
|
|
24021
|
+
["schema version", String(overview.stats.schemaVersion)],
|
|
24022
|
+
[
|
|
24023
|
+
"snapshots",
|
|
24024
|
+
`${overview.snapshots.count} (${formatBytes(overview.snapshots.totalBytes)}), latest ${formatTime(overview.snapshots.latestAt)}`
|
|
24025
|
+
],
|
|
24026
|
+
[
|
|
24027
|
+
"retention",
|
|
24028
|
+
`keep ${overview.retention.keepLatest}, up to ${overview.retention.maxAgeDays} days`
|
|
24029
|
+
],
|
|
24030
|
+
[
|
|
24031
|
+
"free disk",
|
|
24032
|
+
overview.freeDiskBytes === null ? "unknown" : formatBytes(overview.freeDiskBytes)
|
|
24033
|
+
]
|
|
24034
|
+
]));
|
|
24035
|
+
}
|
|
24036
|
+
};
|
|
24037
|
+
var dbSnapshots = {
|
|
24038
|
+
usage: "db snapshots",
|
|
24039
|
+
summary: "List the snapshots held for this installation",
|
|
24040
|
+
async run(_args, env2) {
|
|
24041
|
+
const { ctx, writer } = env2;
|
|
24042
|
+
const deps = await database(env2);
|
|
24043
|
+
const snapshots = listSnapshots(deps);
|
|
24044
|
+
emit(ctx, writer, { snapshots }, () => {
|
|
24045
|
+
if (snapshots.length === 0) {
|
|
24046
|
+
return `no snapshots in ${snapshotsDir(deps)}; take one with omni db backup`;
|
|
24047
|
+
}
|
|
24048
|
+
return table([
|
|
24049
|
+
{ header: "ID" },
|
|
24050
|
+
{ header: "TAKEN" },
|
|
24051
|
+
{ header: "SIZE", align: "right" },
|
|
24052
|
+
{ header: "REASON" }
|
|
24053
|
+
], snapshots.map((snapshot) => [
|
|
24054
|
+
snapshot.id,
|
|
24055
|
+
formatTime(snapshot.createdAt),
|
|
24056
|
+
formatBytes(snapshot.sizeBytes),
|
|
24057
|
+
snapshot.reason
|
|
24058
|
+
]));
|
|
24059
|
+
});
|
|
24060
|
+
}
|
|
24061
|
+
};
|
|
24062
|
+
var dbBackup = {
|
|
24063
|
+
usage: "db backup",
|
|
24064
|
+
summary: "Take a snapshot of the database, pruning by the retention policy",
|
|
24065
|
+
async run(_args, env2) {
|
|
24066
|
+
const { ctx, writer } = env2;
|
|
24067
|
+
const snapshot = await createSnapshot(await database(env2), { reason: "manual" });
|
|
24068
|
+
emit(ctx, writer, snapshot, () => {
|
|
24069
|
+
note(ctx, writer, "a snapshot carries encrypted credentials and key hashes; captured bodies are not in it");
|
|
24070
|
+
return fields([
|
|
24071
|
+
["snapshot", snapshot.id],
|
|
24072
|
+
["size", formatBytes(snapshot.sizeBytes)],
|
|
24073
|
+
["taken", formatTime(snapshot.createdAt)]
|
|
24074
|
+
]);
|
|
24075
|
+
});
|
|
24076
|
+
}
|
|
24077
|
+
};
|
|
24078
|
+
var dbVacuum = {
|
|
24079
|
+
usage: "db vacuum",
|
|
24080
|
+
summary: "Rewrite the database, reclaiming the pages deletion left free",
|
|
24081
|
+
async run(_args, env2) {
|
|
24082
|
+
const { ctx, writer } = env2;
|
|
24083
|
+
note(ctx, writer, "compacting; the gateway's writes will block until this finishes\u2026");
|
|
24084
|
+
const result = await vacuum(await database(env2));
|
|
24085
|
+
emit(ctx, writer, result, () => fields([
|
|
24086
|
+
["reclaimed", formatBytes(result.reclaimedBytes)],
|
|
24087
|
+
["took", `${result.durationMs} ms`]
|
|
24088
|
+
]));
|
|
24089
|
+
}
|
|
24090
|
+
};
|
|
24091
|
+
var dbRestore = {
|
|
24092
|
+
usage: "db restore <id>",
|
|
24093
|
+
summary: "Replace the database with a snapshot, keeping a copy of what was there",
|
|
24094
|
+
async run(args, env2) {
|
|
24095
|
+
const { ctx, writer, prompt } = env2;
|
|
24096
|
+
const id = requirePositional(args, 0, "snapshot id");
|
|
24097
|
+
const running = await status(env2.service());
|
|
24098
|
+
if (running.running) {
|
|
24099
|
+
const who = running.pid === null ? running.supervisor : `${running.supervisor}, pid ${running.pid}`;
|
|
24100
|
+
throw new CliError(`a gateway is running (${who}) against this installation; run omni stop first, ` + "or restore from the dashboard, which swaps the file behind its own quiesce latch");
|
|
24101
|
+
}
|
|
24102
|
+
if (!await prompt.confirm(`replace ${ctx.databasePath} with ${id}?`)) {
|
|
24103
|
+
throw new CliError("cancelled");
|
|
24104
|
+
}
|
|
24105
|
+
const result = await restoreSnapshot(await database(env2), id);
|
|
24106
|
+
emit(ctx, writer, result, () => fields([
|
|
24107
|
+
["restored", id],
|
|
24108
|
+
["undo", result.preRestoreSnapshot.id],
|
|
24109
|
+
...Object.entries(result.counts).map(([table2, count]) => [table2, String(count)])
|
|
24110
|
+
]));
|
|
24111
|
+
}
|
|
24112
|
+
};
|
|
21674
24113
|
|
|
21675
24114
|
// apps/cli/src/commands/keys.ts
|
|
24115
|
+
var WINDOWED = new Set(["requests", "tokens", "spend"]);
|
|
24116
|
+
var PROTO = "__proto__";
|
|
24117
|
+
function rejectProto(flag, name) {
|
|
24118
|
+
if (name === PROTO)
|
|
24119
|
+
throw new UsageError(`${flag} cannot name "${PROTO}"`);
|
|
24120
|
+
}
|
|
24121
|
+
function parseLimitFlags(entries, flag, into = {}) {
|
|
24122
|
+
const limits = into;
|
|
24123
|
+
for (const entry of entries) {
|
|
24124
|
+
const equals = entry.indexOf("=");
|
|
24125
|
+
if (equals <= 0) {
|
|
24126
|
+
throw new UsageError(`${flag} must be dimension:window=value, got "${entry}"`);
|
|
24127
|
+
}
|
|
24128
|
+
const pair = entry.slice(0, equals);
|
|
24129
|
+
const raw = entry.slice(equals + 1);
|
|
24130
|
+
const value = Number(raw);
|
|
24131
|
+
if (raw.trim().length === 0 || !Number.isFinite(value)) {
|
|
24132
|
+
throw new UsageError(`${flag} ${pair} must be a number, got "${raw}"`);
|
|
24133
|
+
}
|
|
24134
|
+
const colon = pair.indexOf(":");
|
|
24135
|
+
if (colon < 0) {
|
|
24136
|
+
if (WINDOWED.has(pair)) {
|
|
24137
|
+
throw new UsageError(`${flag} ${pair} needs a window, e.g. ${flag} ${pair}:1m=60`);
|
|
24138
|
+
}
|
|
24139
|
+
rejectProto(flag, pair);
|
|
24140
|
+
limits[pair] = value;
|
|
24141
|
+
continue;
|
|
24142
|
+
}
|
|
24143
|
+
const dimension = pair.slice(0, colon);
|
|
24144
|
+
const window = pair.slice(colon + 1);
|
|
24145
|
+
if (dimension.length === 0 || window.length === 0) {
|
|
24146
|
+
throw new UsageError(`${flag} must be dimension:window=value, got "${entry}"`);
|
|
24147
|
+
}
|
|
24148
|
+
rejectProto(flag, dimension);
|
|
24149
|
+
rejectProto(flag, window);
|
|
24150
|
+
const existing = limits[dimension];
|
|
24151
|
+
const windows = typeof existing === "object" ? existing : {};
|
|
24152
|
+
windows[window] = value;
|
|
24153
|
+
limits[dimension] = windows;
|
|
24154
|
+
}
|
|
24155
|
+
return limits;
|
|
24156
|
+
}
|
|
24157
|
+
function toLoose(limits) {
|
|
24158
|
+
const loose = {};
|
|
24159
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
24160
|
+
const windows = limits[dimension];
|
|
24161
|
+
if (windows === undefined)
|
|
24162
|
+
continue;
|
|
24163
|
+
const kept = {};
|
|
24164
|
+
for (const [window, value] of Object.entries(windows)) {
|
|
24165
|
+
if (value === null || value === undefined)
|
|
24166
|
+
continue;
|
|
24167
|
+
kept[window] = value;
|
|
24168
|
+
}
|
|
24169
|
+
if (Object.keys(kept).length > 0)
|
|
24170
|
+
loose[dimension] = kept;
|
|
24171
|
+
}
|
|
24172
|
+
if (limits.concurrency !== undefined && limits.concurrency !== null) {
|
|
24173
|
+
loose.concurrency = limits.concurrency;
|
|
24174
|
+
}
|
|
24175
|
+
return loose;
|
|
24176
|
+
}
|
|
24177
|
+
function applyUnsetFlags(limits, entries) {
|
|
24178
|
+
for (const entry of entries) {
|
|
24179
|
+
if (entry.includes("=")) {
|
|
24180
|
+
throw new UsageError(`--unset names a limit to remove, not a value, got "${entry}"`);
|
|
24181
|
+
}
|
|
24182
|
+
const colon = entry.indexOf(":");
|
|
24183
|
+
if (colon < 0) {
|
|
24184
|
+
if (WINDOWED.has(entry)) {
|
|
24185
|
+
throw new UsageError(`--unset ${entry} needs a window, e.g. --unset ${entry}:1m`);
|
|
24186
|
+
}
|
|
24187
|
+
rejectProto("--unset", entry);
|
|
24188
|
+
if (limits[entry] === undefined)
|
|
24189
|
+
throw new UsageError(`this key has no ${entry} limit`);
|
|
24190
|
+
delete limits[entry];
|
|
24191
|
+
continue;
|
|
24192
|
+
}
|
|
24193
|
+
const dimension = entry.slice(0, colon);
|
|
24194
|
+
const window = entry.slice(colon + 1);
|
|
24195
|
+
if (dimension.length === 0 || window.length === 0) {
|
|
24196
|
+
throw new UsageError(`--unset must be dimension:window, got "${entry}"`);
|
|
24197
|
+
}
|
|
24198
|
+
rejectProto("--unset", dimension);
|
|
24199
|
+
rejectProto("--unset", window);
|
|
24200
|
+
const windows = limits[dimension];
|
|
24201
|
+
if (typeof windows !== "object" || windows[window] === undefined) {
|
|
24202
|
+
throw new UsageError(`this key has no ${entry} limit`);
|
|
24203
|
+
}
|
|
24204
|
+
delete windows[window];
|
|
24205
|
+
if (Object.keys(windows).length === 0)
|
|
24206
|
+
delete limits[dimension];
|
|
24207
|
+
}
|
|
24208
|
+
}
|
|
24209
|
+
function limitRow(reading) {
|
|
24210
|
+
const money = reading.dimension === "spend";
|
|
24211
|
+
const amount = (value) => money ? formatUsd(value) : String(value);
|
|
24212
|
+
return [
|
|
24213
|
+
reading.dimension,
|
|
24214
|
+
reading.window ?? "\u2014",
|
|
24215
|
+
amount(reading.limit),
|
|
24216
|
+
reading.used === null ? "\u2014" : amount(reading.used),
|
|
24217
|
+
reading.used === null ? "\u2014" : `${Math.round(reading.used / reading.limit * 100)}%`
|
|
24218
|
+
];
|
|
24219
|
+
}
|
|
24220
|
+
function summarizeLimits(limits) {
|
|
24221
|
+
const parts = [];
|
|
24222
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
24223
|
+
const windows = limits[dimension];
|
|
24224
|
+
if (windows === undefined)
|
|
24225
|
+
continue;
|
|
24226
|
+
for (const [window, value] of Object.entries(windows)) {
|
|
24227
|
+
if (value === null || value === undefined)
|
|
24228
|
+
continue;
|
|
24229
|
+
parts.push(`${dimension}:${window}=${value}`);
|
|
24230
|
+
}
|
|
24231
|
+
}
|
|
24232
|
+
if (limits.concurrency !== undefined && limits.concurrency !== null) {
|
|
24233
|
+
parts.push(`concurrency=${limits.concurrency}`);
|
|
24234
|
+
}
|
|
24235
|
+
return parts.length === 0 ? "\u2014" : parts.join(" ");
|
|
24236
|
+
}
|
|
21676
24237
|
var keysList = {
|
|
21677
24238
|
usage: "keys list",
|
|
21678
24239
|
summary: "List gateway API keys",
|
|
@@ -21686,7 +24247,8 @@ var keysList = {
|
|
|
21686
24247
|
{ header: "LABEL" },
|
|
21687
24248
|
{ header: "PREFIX" },
|
|
21688
24249
|
{ header: "MODELS" },
|
|
21689
|
-
{ header: "
|
|
24250
|
+
{ header: "LIMITS" },
|
|
24251
|
+
{ header: "BODY CAPTURE" },
|
|
21690
24252
|
{ header: "STATE" },
|
|
21691
24253
|
{ header: "CREATED" }
|
|
21692
24254
|
], keys.map((key) => [
|
|
@@ -21694,7 +24256,8 @@ var keysList = {
|
|
|
21694
24256
|
key.label,
|
|
21695
24257
|
key.prefix,
|
|
21696
24258
|
key.modelAllowlist === null ? "all" : key.modelAllowlist.length === 0 ? "none" : key.modelAllowlist.join(","),
|
|
21697
|
-
key.
|
|
24259
|
+
key.limits === null ? paint(ctx, "red", "unreadable") : summarizeLimits(key.limits),
|
|
24260
|
+
key.bodyLoggingOptOut ? "no bodies" : "\u2014",
|
|
21698
24261
|
state(ctx, key.revokedAt === null, key.revokedAt === null ? "active" : "revoked"),
|
|
21699
24262
|
formatTime(key.createdAt)
|
|
21700
24263
|
]));
|
|
@@ -21702,19 +24265,21 @@ var keysList = {
|
|
|
21702
24265
|
}
|
|
21703
24266
|
};
|
|
21704
24267
|
var keysCreate = {
|
|
21705
|
-
usage: "keys create [--label L] [--allow <model> ...] [--
|
|
24268
|
+
usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--no-bodies]",
|
|
21706
24269
|
summary: "Mint a gateway API key, printed once",
|
|
21707
24270
|
options: {
|
|
21708
24271
|
label: { type: "string" },
|
|
21709
24272
|
allow: { type: "string", multiple: true },
|
|
21710
|
-
|
|
24273
|
+
limit: { type: "string", multiple: true },
|
|
24274
|
+
"no-bodies": { type: "boolean" }
|
|
21711
24275
|
},
|
|
21712
24276
|
async run(args, { ctx, writer }) {
|
|
21713
24277
|
const allow = listFlag(args.values, "allow");
|
|
21714
24278
|
const created = await createKey(await ctx.store(), {
|
|
21715
24279
|
...stringFlag(args.values, "label") === undefined ? {} : { label: stringFlag(args.values, "label") },
|
|
21716
24280
|
modelAllowlist: allow ?? null,
|
|
21717
|
-
|
|
24281
|
+
limits: parseLimitFlags(listFlag(args.values, "limit") ?? [], "--limit"),
|
|
24282
|
+
bodyLoggingOptOut: boolFlag(args.values, "no-bodies")
|
|
21718
24283
|
});
|
|
21719
24284
|
emit(ctx, writer, created, () => {
|
|
21720
24285
|
note(ctx, writer, paint(ctx, "yellow", "this key is shown once and stored only as a hash"));
|
|
@@ -21722,6 +24287,63 @@ var keysCreate = {
|
|
|
21722
24287
|
});
|
|
21723
24288
|
}
|
|
21724
24289
|
};
|
|
24290
|
+
var keysLimits = {
|
|
24291
|
+
usage: "keys limits <id> [--set <d>:<w>=N ...] [--unset <d>:<w> ...]",
|
|
24292
|
+
summary: "Show or edit one key's limits, with what has been used against them",
|
|
24293
|
+
options: {
|
|
24294
|
+
set: { type: "string", multiple: true },
|
|
24295
|
+
unset: { type: "string", multiple: true }
|
|
24296
|
+
},
|
|
24297
|
+
async run(args, { ctx, writer }) {
|
|
24298
|
+
const id = requirePositional(args, 0, "key id");
|
|
24299
|
+
const sets = listFlag(args.values, "set") ?? [];
|
|
24300
|
+
const unsets = listFlag(args.values, "unset") ?? [];
|
|
24301
|
+
const store = await ctx.store();
|
|
24302
|
+
const existing = (await listKeys(store)).find((entry) => entry.id === id);
|
|
24303
|
+
if (existing === undefined)
|
|
24304
|
+
throw new CliError(`no api key "${id}"`);
|
|
24305
|
+
let key = existing;
|
|
24306
|
+
if (sets.length > 0 || unsets.length > 0) {
|
|
24307
|
+
if (existing.limits === null) {
|
|
24308
|
+
if (unsets.length > 0) {
|
|
24309
|
+
throw new CliError(`the stored limits for "${id}" cannot be read, so there is nothing to unset; ` + "replace them with --set instead");
|
|
24310
|
+
}
|
|
24311
|
+
note(ctx, writer, paint(ctx, "yellow", "the stored limits could not be read and are being replaced"));
|
|
24312
|
+
}
|
|
24313
|
+
const next = existing.limits === null ? {} : toLoose(existing.limits);
|
|
24314
|
+
applyUnsetFlags(next, unsets);
|
|
24315
|
+
parseLimitFlags(sets, "--set", next);
|
|
24316
|
+
key = await setKeyLimits(store, id, { limits: next });
|
|
24317
|
+
}
|
|
24318
|
+
emit(ctx, writer, key, () => {
|
|
24319
|
+
const head = fields([
|
|
24320
|
+
["id", key.id],
|
|
24321
|
+
["label", key.label],
|
|
24322
|
+
["prefix", `${key.prefix}\u2026`]
|
|
24323
|
+
]);
|
|
24324
|
+
if (key.limits === null) {
|
|
24325
|
+
return `${head}
|
|
24326
|
+
|
|
24327
|
+
${paint(ctx, "red", "limits unreadable")}: this key is refused at /v1 until they are replaced with --set`;
|
|
24328
|
+
}
|
|
24329
|
+
if (key.limitUsage.length === 0) {
|
|
24330
|
+
return `${head}
|
|
24331
|
+
|
|
24332
|
+
no limits configured; this key is unlimited`;
|
|
24333
|
+
}
|
|
24334
|
+
return `${head}
|
|
24335
|
+
|
|
24336
|
+
${table([
|
|
24337
|
+
{ header: "DIMENSION" },
|
|
24338
|
+
{ header: "WINDOW" },
|
|
24339
|
+
{ header: "LIMIT", align: "right" },
|
|
24340
|
+
{ header: "USED", align: "right" },
|
|
24341
|
+
{ header: "USE%", align: "right" }
|
|
24342
|
+
], key.limitUsage.map(limitRow))}
|
|
24343
|
+
${paint(ctx, "dim", "usage is counted from completed requests still inside each window")}`;
|
|
24344
|
+
});
|
|
24345
|
+
}
|
|
24346
|
+
};
|
|
21725
24347
|
var keysRevoke = {
|
|
21726
24348
|
usage: "keys revoke <id>",
|
|
21727
24349
|
summary: "Revoke a gateway API key, keeping its usage history",
|
|
@@ -21985,6 +24607,168 @@ ${candidates}${excluded}`;
|
|
|
21985
24607
|
});
|
|
21986
24608
|
}
|
|
21987
24609
|
};
|
|
24610
|
+
// packages/plugin-api/src/events.ts
|
|
24611
|
+
var WINDOW_MS2 = {
|
|
24612
|
+
"1m": 60000,
|
|
24613
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
24614
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
24615
|
+
};
|
|
24616
|
+
// apps/cli/src/commands/plugins.ts
|
|
24617
|
+
function pluginDeps() {
|
|
24618
|
+
return { fs: nodePluginFs(), sdkVersion: DASHBOARD_SDK_VERSION };
|
|
24619
|
+
}
|
|
24620
|
+
function installDeps(ctx, args) {
|
|
24621
|
+
const flag = stringFlag(args.values, "registry");
|
|
24622
|
+
const raw = flag ?? ctx.env.OMNI_PLUGIN_REGISTRY;
|
|
24623
|
+
const registry2 = raw === undefined || raw.trim().length === 0 ? undefined : raw.trim();
|
|
24624
|
+
return {
|
|
24625
|
+
...pluginDeps(),
|
|
24626
|
+
fetchBytes: nodeFetchBytes(),
|
|
24627
|
+
...registry2 === undefined ? {} : { registry: registry2 }
|
|
24628
|
+
};
|
|
24629
|
+
}
|
|
24630
|
+
function pluginState(ctx, plugin) {
|
|
24631
|
+
if (!plugin.loadable)
|
|
24632
|
+
return state(ctx, false, "will not load");
|
|
24633
|
+
if (plugin.problems.length > 0)
|
|
24634
|
+
return paint(ctx, "yellow", "loads, with warnings");
|
|
24635
|
+
return state(ctx, true, "ok");
|
|
24636
|
+
}
|
|
24637
|
+
var pluginList = {
|
|
24638
|
+
usage: "plugin list",
|
|
24639
|
+
summary: "List installed plugins and whether the gateway would load each one",
|
|
24640
|
+
async run(_args, { ctx, writer }) {
|
|
24641
|
+
const root = ctx.root.root;
|
|
24642
|
+
const plugins = listPlugins(pluginDeps(), root);
|
|
24643
|
+
emit(ctx, writer, { pluginsDir: pluginsDir(root), plugins }, () => {
|
|
24644
|
+
if (plugins.length === 0) {
|
|
24645
|
+
return `no plugins installed; unpack one into ${pluginsDir(root)} with: omni plugin install <path-or-package>`;
|
|
24646
|
+
}
|
|
24647
|
+
const rows = plugins.map((plugin) => [
|
|
24648
|
+
plugin.id,
|
|
24649
|
+
plugin.name ?? "\u2014",
|
|
24650
|
+
plugin.version ?? "\u2014",
|
|
24651
|
+
plugin.api === null ? "\u2014" : String(plugin.api),
|
|
24652
|
+
plugin.sdk ?? "\u2014",
|
|
24653
|
+
plugin.capabilities.length === 0 ? "\u2014" : plugin.capabilities.join(","),
|
|
24654
|
+
pluginState(ctx, plugin)
|
|
24655
|
+
]);
|
|
24656
|
+
const body2 = table([
|
|
24657
|
+
{ header: "ID" },
|
|
24658
|
+
{ header: "NAME" },
|
|
24659
|
+
{ header: "VERSION" },
|
|
24660
|
+
{ header: "API" },
|
|
24661
|
+
{ header: "SDK" },
|
|
24662
|
+
{ header: "CAPABILITIES" },
|
|
24663
|
+
{ header: "STATE" }
|
|
24664
|
+
], rows);
|
|
24665
|
+
const notes = plugins.flatMap((plugin) => plugin.problems.map((problem) => `${plugin.id}: ${paint(ctx, problem.fatal ? "red" : "yellow", problem.reason)}`));
|
|
24666
|
+
return notes.length === 0 ? body2 : `${body2}
|
|
24667
|
+
|
|
24668
|
+
${notes.join(`
|
|
24669
|
+
`)}`;
|
|
24670
|
+
});
|
|
24671
|
+
}
|
|
24672
|
+
};
|
|
24673
|
+
var pluginVerify = {
|
|
24674
|
+
usage: "plugin verify <id>",
|
|
24675
|
+
summary: "Run every load-time check on one plugin without loading it",
|
|
24676
|
+
async run(args, { ctx, writer }) {
|
|
24677
|
+
const id = requirePositional(args, 0, "plugin id");
|
|
24678
|
+
const report = verifyPlugin(pluginDeps(), ctx.root.root, id);
|
|
24679
|
+
emit(ctx, writer, report, () => {
|
|
24680
|
+
const manifest = report.manifest;
|
|
24681
|
+
const head = fields([
|
|
24682
|
+
["id", report.id],
|
|
24683
|
+
["path", report.path],
|
|
24684
|
+
["name", manifest?.name ?? "\u2014"],
|
|
24685
|
+
["version", manifest?.version ?? "\u2014"],
|
|
24686
|
+
["api", manifest === null ? "\u2014" : String(manifest.api)],
|
|
24687
|
+
["sdk", manifest?.sdk ?? "\u2014"],
|
|
24688
|
+
["server", manifest?.server ?? "\u2014"],
|
|
24689
|
+
["ui", manifest?.ui ?? "\u2014"],
|
|
24690
|
+
[
|
|
24691
|
+
"capabilities",
|
|
24692
|
+
manifest === null || manifest.capabilities.length === 0 ? "\u2014" : manifest.capabilities.join(",")
|
|
24693
|
+
],
|
|
24694
|
+
["origins", manifest?.origins?.join(",") ?? "\u2014"]
|
|
24695
|
+
]);
|
|
24696
|
+
if (report.problems.length === 0) {
|
|
24697
|
+
return `${head}
|
|
24698
|
+
|
|
24699
|
+
${state(ctx, true, "ok")}: this plugin would load`;
|
|
24700
|
+
}
|
|
24701
|
+
const lines = report.problems.map((problem) => ` ${paint(ctx, problem.fatal ? "red" : "yellow", problem.check)} ${problem.reason}`);
|
|
24702
|
+
const verdict = report.loadable ? paint(ctx, "yellow", "this plugin would load, with the warnings above") : state(ctx, false, "this plugin would be skipped at boot");
|
|
24703
|
+
return `${head}
|
|
24704
|
+
|
|
24705
|
+
${lines.join(`
|
|
24706
|
+
`)}
|
|
24707
|
+
|
|
24708
|
+
${verdict}`;
|
|
24709
|
+
});
|
|
24710
|
+
if (!report.loadable)
|
|
24711
|
+
throw new CliError(`plugin "${id}" would not load`);
|
|
24712
|
+
}
|
|
24713
|
+
};
|
|
24714
|
+
var pluginInstall = {
|
|
24715
|
+
usage: "plugin install <path|tarball|https url|package[@version]> [--registry <url>]",
|
|
24716
|
+
summary: "Unpack a plugin into this installation, running nothing from it",
|
|
24717
|
+
options: { registry: { type: "string" } },
|
|
24718
|
+
async run(args, { ctx, writer }) {
|
|
24719
|
+
const spec = requirePositional(args, 0, "plugin directory, tarball, url, or package name");
|
|
24720
|
+
const result = await installPlugin(installDeps(ctx, args), ctx.root.root, spec);
|
|
24721
|
+
emit(ctx, writer, result, () => {
|
|
24722
|
+
note(ctx, writer, paint(ctx, "dim", "no code from the package was executed; verify it before restarting"));
|
|
24723
|
+
const verb = result.replaced ? "replaced" : "installed";
|
|
24724
|
+
return [
|
|
24725
|
+
`${verb} ${result.name} ${result.version} at ${result.path}`,
|
|
24726
|
+
paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart")
|
|
24727
|
+
].join(`
|
|
24728
|
+
`);
|
|
24729
|
+
});
|
|
24730
|
+
}
|
|
24731
|
+
};
|
|
24732
|
+
var pluginRemove = {
|
|
24733
|
+
usage: "plugin remove <id> [--purge]",
|
|
24734
|
+
summary: "Remove a plugin, optionally dropping its stored data too",
|
|
24735
|
+
options: { purge: { type: "boolean" } },
|
|
24736
|
+
async run(args, { ctx, writer, prompt }) {
|
|
24737
|
+
const id = requirePositional(args, 0, "plugin id");
|
|
24738
|
+
const purge = boolFlag(args.values, "purge");
|
|
24739
|
+
const question = purge ? `remove plugin "${id}" AND permanently drop its stored data?` : `remove plugin "${id}"? its stored data is kept`;
|
|
24740
|
+
if (!await prompt.confirm(question))
|
|
24741
|
+
throw new CliError("cancelled");
|
|
24742
|
+
const deps = purge ? { fs: nodePluginFs(), store: await ctx.store() } : { fs: nodePluginFs() };
|
|
24743
|
+
const result = removePlugin(deps, ctx.root.root, id, { purge });
|
|
24744
|
+
emit(ctx, writer, result, () => {
|
|
24745
|
+
const lines = [];
|
|
24746
|
+
lines.push(result.removed ? `removed ${id}` : `no directory for ${id} to remove`);
|
|
24747
|
+
if (purge) {
|
|
24748
|
+
lines.push(result.droppedTables.length === 0 ? "no stored tables to drop" : `dropped ${result.droppedTables.length} table(s): ${result.droppedTables.join(", ")}`);
|
|
24749
|
+
} else {
|
|
24750
|
+
lines.push(paint(ctx, "dim", "stored tables and migration history kept; --purge drops them"));
|
|
24751
|
+
}
|
|
24752
|
+
lines.push(paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart"));
|
|
24753
|
+
return lines.join(`
|
|
24754
|
+
`);
|
|
24755
|
+
});
|
|
24756
|
+
}
|
|
24757
|
+
};
|
|
24758
|
+
function doctorPluginDeps() {
|
|
24759
|
+
return pluginDeps();
|
|
24760
|
+
}
|
|
24761
|
+
function pluginDoctorLines(ctx, plugins) {
|
|
24762
|
+
return plugins.map((plugin) => {
|
|
24763
|
+
const version2 = plugin.version === null ? "" : ` ${plugin.version}`;
|
|
24764
|
+
const api2 = plugin.api === null ? "api ?" : `api ${plugin.api}`;
|
|
24765
|
+
const sdk = plugin.sdk === null ? "no ui" : `sdk ${plugin.sdk}`;
|
|
24766
|
+
const reasons = plugin.problems.filter((problem) => problem.fatal).map((p) => p.reason);
|
|
24767
|
+
const warnings = plugin.problems.length - reasons.length;
|
|
24768
|
+
const verdict = reasons.length > 0 ? state(ctx, false, `will not load: ${reasons.join("; ")}`) : warnings > 0 ? paint(ctx, "yellow", `loads, ${warnings} warning(s); see omni plugin verify ${plugin.id}`) : state(ctx, true, "ok");
|
|
24769
|
+
return `${plugin.id}${version2} (${api2}, ${sdk}) ${verdict}`;
|
|
24770
|
+
});
|
|
24771
|
+
}
|
|
21988
24772
|
|
|
21989
24773
|
// apps/cli/src/commands/quota.ts
|
|
21990
24774
|
var WINDOW_ORDER = {
|
|
@@ -22089,7 +24873,7 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
22089
24873
|
|
|
22090
24874
|
// apps/cli/src/runtime.ts
|
|
22091
24875
|
import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
|
|
22092
|
-
import { join as
|
|
24876
|
+
import { join as join7 } from "path";
|
|
22093
24877
|
var runCommand = async (argv) => {
|
|
22094
24878
|
const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
|
|
22095
24879
|
const [stdout, stderr, code] = await Promise.all([
|
|
@@ -22146,9 +24930,9 @@ function processAlive(pid) {
|
|
|
22146
24930
|
}
|
|
22147
24931
|
function gatewayEntrypoint(root, cliDir = import.meta.dir) {
|
|
22148
24932
|
const candidates = [
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
24933
|
+
join7(root, "apps", "gateway", "src", "index.ts"),
|
|
24934
|
+
join7(cliDir, "..", "gateway.js"),
|
|
24935
|
+
join7(cliDir, "gateway.js")
|
|
22152
24936
|
];
|
|
22153
24937
|
return candidates.find((path) => existsSync4(path)) ?? null;
|
|
22154
24938
|
}
|
|
@@ -22275,6 +25059,25 @@ var serviceUninstall = {
|
|
|
22275
25059
|
emit(ctx, writer, result, () => result.removed ? `removed ${result.path}` : `no unit at ${result.path}`);
|
|
22276
25060
|
}
|
|
22277
25061
|
};
|
|
25062
|
+
async function rollupState(ctx) {
|
|
25063
|
+
if (ctx.configError !== null || !existsSync5(ctx.databasePath))
|
|
25064
|
+
return null;
|
|
25065
|
+
try {
|
|
25066
|
+
const audit = await (await ctx.store()).usage.auditRollup();
|
|
25067
|
+
return audit.ok ? `ok (${audit.buckets} hourly buckets)` : `${audit.mismatched} of ${audit.buckets} hourly buckets disagree with request_logs`;
|
|
25068
|
+
} catch {
|
|
25069
|
+
return null;
|
|
25070
|
+
}
|
|
25071
|
+
}
|
|
25072
|
+
async function orphanTables(ctx) {
|
|
25073
|
+
if (ctx.configError !== null || !existsSync5(ctx.databasePath))
|
|
25074
|
+
return null;
|
|
25075
|
+
try {
|
|
25076
|
+
return orphanPluginTables(doctorPluginDeps(), ctx.root.root, await ctx.store());
|
|
25077
|
+
} catch {
|
|
25078
|
+
return null;
|
|
25079
|
+
}
|
|
25080
|
+
}
|
|
22278
25081
|
var doctor = {
|
|
22279
25082
|
usage: "doctor",
|
|
22280
25083
|
summary: "Check what this CLI resolved, and whether it can do anything with it",
|
|
@@ -22284,6 +25087,9 @@ var doctor = {
|
|
|
22284
25087
|
const key = ctx.env.OMNI_ENCRYPTION_KEY;
|
|
22285
25088
|
const unit = unitInstalled(deps);
|
|
22286
25089
|
const status2 = await status(deps);
|
|
25090
|
+
const usageRollup = await rollupState(ctx);
|
|
25091
|
+
const plugins = listPlugins(doctorPluginDeps(), deps.root);
|
|
25092
|
+
const orphans = await orphanTables(ctx);
|
|
22287
25093
|
const checks3 = {
|
|
22288
25094
|
root: deps.root,
|
|
22289
25095
|
rootSource: ctx.root.source,
|
|
@@ -22293,10 +25099,14 @@ var doctor = {
|
|
|
22293
25099
|
encryptionKey: typeof key === "string" ? `present (${key.length} chars)` : "missing",
|
|
22294
25100
|
configError: ctx.configError,
|
|
22295
25101
|
gatewayEntrypoint: gatewayEntrypoint(deps.root),
|
|
25102
|
+
usageRollup,
|
|
22296
25103
|
unitInstalled: unit,
|
|
22297
25104
|
supervisor: status2.supervisor,
|
|
22298
25105
|
running: status2.running,
|
|
22299
25106
|
consoleSource: consoleSource(deps).source,
|
|
25107
|
+
pluginsDir: pluginsDir(deps.root),
|
|
25108
|
+
plugins,
|
|
25109
|
+
orphanPluginTables: orphans,
|
|
22300
25110
|
warnings: ctx.warnings
|
|
22301
25111
|
};
|
|
22302
25112
|
emit(ctx, writer, checks3, () => {
|
|
@@ -22311,9 +25121,18 @@ var doctor = {
|
|
|
22311
25121
|
["encryption key", checks3.encryptionKey],
|
|
22312
25122
|
["config", checks3.configError === null ? ok(true, "ok") : ok(false, checks3.configError)],
|
|
22313
25123
|
["entrypoint", checks3.gatewayEntrypoint ?? ok(false, "not found")],
|
|
25124
|
+
[
|
|
25125
|
+
"usage rollup",
|
|
25126
|
+
checks3.usageRollup === null ? paint(ctx, "dim", "not checked") : ok(checks3.usageRollup.startsWith("ok"), checks3.usageRollup)
|
|
25127
|
+
],
|
|
22314
25128
|
["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
|
|
22315
25129
|
["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
|
|
22316
25130
|
["console log", sourceHint(checks3.consoleSource)],
|
|
25131
|
+
...plugins.length === 0 ? [["plugins", paint(ctx, "dim", "none")]] : pluginDoctorLines(ctx, plugins).map((line, index) => [index === 0 ? "plugins" : "", line]),
|
|
25132
|
+
[
|
|
25133
|
+
"orphan plugin tables",
|
|
25134
|
+
orphans === null ? paint(ctx, "dim", "not checked") : orphans.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${orphans.length}: ${orphans.join(", ")}`)
|
|
25135
|
+
],
|
|
22317
25136
|
...checks3.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
|
|
22318
25137
|
]);
|
|
22319
25138
|
});
|
|
@@ -22342,31 +25161,59 @@ var settingsGet = {
|
|
|
22342
25161
|
emit(ctx, writer, { settings }, () => fields(flatten(settings)));
|
|
22343
25162
|
}
|
|
22344
25163
|
};
|
|
25164
|
+
function has(value, key) {
|
|
25165
|
+
return Object.hasOwn(value, key);
|
|
25166
|
+
}
|
|
25167
|
+
function currentValue(settings, head, tail) {
|
|
25168
|
+
if (!has(settings, head))
|
|
25169
|
+
return;
|
|
25170
|
+
const top = settings[head];
|
|
25171
|
+
if (tail === undefined)
|
|
25172
|
+
return top;
|
|
25173
|
+
if (top === null || typeof top !== "object")
|
|
25174
|
+
return;
|
|
25175
|
+
if (!has(top, tail))
|
|
25176
|
+
return;
|
|
25177
|
+
return top[tail];
|
|
25178
|
+
}
|
|
25179
|
+
function asNumber(path, raw) {
|
|
25180
|
+
const value = raw.trim().length === 0 ? Number.NaN : Number(raw);
|
|
25181
|
+
if (!Number.isFinite(value))
|
|
25182
|
+
throw new UsageError(`${path} must be a number, got "${raw}"`);
|
|
25183
|
+
return value;
|
|
25184
|
+
}
|
|
25185
|
+
function asBoolean(path, raw) {
|
|
25186
|
+
const value = raw.trim().toLowerCase();
|
|
25187
|
+
if (value === "true")
|
|
25188
|
+
return true;
|
|
25189
|
+
if (value === "false")
|
|
25190
|
+
return false;
|
|
25191
|
+
throw new UsageError(`${path} must be true or false, got "${raw}"`);
|
|
25192
|
+
}
|
|
22345
25193
|
var settingsSet = {
|
|
22346
25194
|
usage: "settings set <path> <value>",
|
|
22347
|
-
summary: "Change one setting, e.g. weights.cost 0.4",
|
|
25195
|
+
summary: "Change one setting, e.g. weights.cost 0.4 or rtkEnabled true",
|
|
22348
25196
|
async run(args, { ctx, writer }) {
|
|
22349
25197
|
const path = requirePositional(args, 0, "setting path");
|
|
22350
25198
|
const raw = requirePositional(args, 1, "value");
|
|
22351
25199
|
const store = await ctx.store();
|
|
22352
25200
|
const current = await getSettings(store);
|
|
22353
|
-
const value = raw.trim().length === 0 ? Number.NaN : Number(raw);
|
|
22354
|
-
if (!Number.isFinite(value))
|
|
22355
|
-
throw new UsageError(`${path} must be a number, got "${raw}"`);
|
|
22356
25201
|
const [head, tail] = path.split(".");
|
|
22357
|
-
if (head === undefined || !(head
|
|
25202
|
+
if (head === undefined || !has(current, head))
|
|
22358
25203
|
throw new UsageError(`no setting "${path}"`);
|
|
22359
|
-
const next = tail === undefined ? { ...current, [head]: value } : { ...current, weights: { ...current.weights, [tail]: value } };
|
|
22360
25204
|
if (tail !== undefined && head !== "weights") {
|
|
22361
25205
|
throw new UsageError(`"${head}" has no sub-settings`);
|
|
22362
25206
|
}
|
|
25207
|
+
const existing = currentValue(current, head, tail);
|
|
25208
|
+
const value = typeof existing === "boolean" ? asBoolean(path, raw) : asNumber(path, raw);
|
|
25209
|
+
const next = tail === undefined ? { ...current, [head]: value } : { ...current, weights: { ...current.weights, [tail]: value } };
|
|
22363
25210
|
const saved = await putSettings(store, next);
|
|
22364
25211
|
emit(ctx, writer, { settings: saved }, () => `${path} = ${value}`);
|
|
22365
25212
|
}
|
|
22366
25213
|
};
|
|
22367
25214
|
|
|
22368
25215
|
// apps/cli/src/commands/setup.ts
|
|
22369
|
-
import { join as
|
|
25216
|
+
import { join as join8 } from "path";
|
|
22370
25217
|
var OPTIONS = {
|
|
22371
25218
|
dir: { type: "string" },
|
|
22372
25219
|
key: { type: "string" },
|
|
@@ -22398,7 +25245,7 @@ ${f.contents}`).join(`
|
|
|
22398
25245
|
}
|
|
22399
25246
|
}
|
|
22400
25247
|
function at(dir, file2) {
|
|
22401
|
-
return { path:
|
|
25248
|
+
return { path: join8(dir, file2.path), contents: file2.contents };
|
|
22402
25249
|
}
|
|
22403
25250
|
async function promptMapping(models, prompt) {
|
|
22404
25251
|
const choices = models.map(({ model }) => model.id).join(", ");
|
|
@@ -22432,11 +25279,11 @@ var setupClaude = {
|
|
|
22432
25279
|
options: OPTIONS,
|
|
22433
25280
|
async run(args, { ctx, writer, prompt, setupFs }) {
|
|
22434
25281
|
const models = await described(ctx);
|
|
22435
|
-
const dir = stringFlag(args.values, "dir") ??
|
|
25282
|
+
const dir = stringFlag(args.values, "dir") ?? join8(setupFs.homeDir, ".claude");
|
|
22436
25283
|
const key = stringFlag(args.values, "key");
|
|
22437
25284
|
const dryRun2 = boolFlag(args.values, "dry-run");
|
|
22438
25285
|
const mapping = await promptMapping(models, prompt);
|
|
22439
|
-
const path =
|
|
25286
|
+
const path = join8(dir, "settings.json");
|
|
22440
25287
|
const file2 = claudeSettings(models, {
|
|
22441
25288
|
baseUrl: baseUrl(ctx),
|
|
22442
25289
|
discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
|
|
@@ -22665,6 +25512,7 @@ var COMMANDS = {
|
|
|
22665
25512
|
restart,
|
|
22666
25513
|
doctor,
|
|
22667
25514
|
logs,
|
|
25515
|
+
bodies,
|
|
22668
25516
|
console: console_,
|
|
22669
25517
|
usage,
|
|
22670
25518
|
quota,
|
|
@@ -22688,13 +25536,23 @@ var COMMANDS = {
|
|
|
22688
25536
|
"models catalog": modelsCatalog,
|
|
22689
25537
|
"keys list": keysList,
|
|
22690
25538
|
"keys create": keysCreate,
|
|
25539
|
+
"keys limits": keysLimits,
|
|
22691
25540
|
"keys revoke": keysRevoke,
|
|
25541
|
+
"plugin list": pluginList,
|
|
25542
|
+
"plugin verify": pluginVerify,
|
|
25543
|
+
"plugin install": pluginInstall,
|
|
25544
|
+
"plugin remove": pluginRemove,
|
|
22692
25545
|
"settings get": settingsGet,
|
|
22693
25546
|
"settings set": settingsSet,
|
|
22694
25547
|
"setup claude": setupClaude,
|
|
22695
25548
|
"setup opencode": setupOpencode,
|
|
22696
25549
|
"admin set-password": adminSetPassword,
|
|
22697
|
-
"db migrate": dbMigrate
|
|
25550
|
+
"db migrate": dbMigrate,
|
|
25551
|
+
"db stats": dbStats,
|
|
25552
|
+
"db snapshots": dbSnapshots,
|
|
25553
|
+
"db backup": dbBackup,
|
|
25554
|
+
"db restore": dbRestore,
|
|
25555
|
+
"db vacuum": dbVacuum
|
|
22698
25556
|
};
|
|
22699
25557
|
var VALUED_GLOBALS = new Set(["--root", "--db"]);
|
|
22700
25558
|
function resolveCommand(argv) {
|
|
@@ -22733,12 +25591,13 @@ function resolveCommand(argv) {
|
|
|
22733
25591
|
var GROUPS = [
|
|
22734
25592
|
{
|
|
22735
25593
|
title: "Gateway",
|
|
22736
|
-
prefixes: ["status", "start", "stop", "restart", "doctor", "logs", "console"]
|
|
25594
|
+
prefixes: ["status", "start", "stop", "restart", "doctor", "logs", "bodies", "console"]
|
|
22737
25595
|
},
|
|
22738
25596
|
{ title: "Service", prefixes: ["service "] },
|
|
22739
25597
|
{ title: "Accounts", prefixes: ["connect", "credentials "] },
|
|
22740
25598
|
{ title: "Models", prefixes: ["models "] },
|
|
22741
25599
|
{ title: "Keys", prefixes: ["keys "] },
|
|
25600
|
+
{ title: "Plugins", prefixes: ["plugin "] },
|
|
22742
25601
|
{ title: "Configuration", prefixes: ["settings ", "admin ", "db "] },
|
|
22743
25602
|
{ title: "Reports", prefixes: ["usage", "quota"] }
|
|
22744
25603
|
];
|
|
@@ -22795,10 +25654,10 @@ async function readHidden() {
|
|
|
22795
25654
|
stdin.setRawMode?.(true);
|
|
22796
25655
|
stdin.resume();
|
|
22797
25656
|
try {
|
|
22798
|
-
const
|
|
25657
|
+
const decoder3 = new TextDecoder;
|
|
22799
25658
|
let line = "";
|
|
22800
25659
|
for await (const chunk of stdin) {
|
|
22801
|
-
const text =
|
|
25660
|
+
const text = decoder3.decode(chunk, { stream: true });
|
|
22802
25661
|
for (const char of text) {
|
|
22803
25662
|
if (char === "\r" || char === `
|
|
22804
25663
|
`)
|
|
@@ -22819,10 +25678,10 @@ async function readHidden() {
|
|
|
22819
25678
|
}
|
|
22820
25679
|
}
|
|
22821
25680
|
async function readLine() {
|
|
22822
|
-
const
|
|
25681
|
+
const decoder3 = new TextDecoder;
|
|
22823
25682
|
let line = "";
|
|
22824
25683
|
for await (const chunk of process.stdin) {
|
|
22825
|
-
const text =
|
|
25684
|
+
const text = decoder3.decode(chunk, { stream: true });
|
|
22826
25685
|
const newline = text.indexOf(`
|
|
22827
25686
|
`);
|
|
22828
25687
|
if (newline >= 0)
|
|
@@ -22862,10 +25721,10 @@ function createPrompt(ctx, writer) {
|
|
|
22862
25721
|
}
|
|
22863
25722
|
|
|
22864
25723
|
// apps/cli/src/setupFs.ts
|
|
22865
|
-
import { dirname as
|
|
25724
|
+
import { dirname as dirname4 } from "path";
|
|
22866
25725
|
function atomicWriteFile(path, contents, ops) {
|
|
22867
25726
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
22868
|
-
ops.mkdir(
|
|
25727
|
+
ops.mkdir(dirname4(path));
|
|
22869
25728
|
try {
|
|
22870
25729
|
ops.write(temporary, contents);
|
|
22871
25730
|
ops.rename(temporary, path);
|
|
@@ -22918,12 +25777,12 @@ async function run(argv, writer, options = {}) {
|
|
|
22918
25777
|
setupFs: options.setupFs ?? {
|
|
22919
25778
|
homeDir: homedir3(),
|
|
22920
25779
|
cwd: process.cwd(),
|
|
22921
|
-
read: (path) => existsSync6(path) ?
|
|
25780
|
+
read: (path) => existsSync6(path) ? readFileSync4(path, "utf8") : null,
|
|
22922
25781
|
write: (path, contents) => atomicWriteFile(path, contents, {
|
|
22923
|
-
mkdir: (directory) =>
|
|
22924
|
-
write: (temporary, data) =>
|
|
22925
|
-
rename:
|
|
22926
|
-
remove: (temporary) =>
|
|
25782
|
+
mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
|
|
25783
|
+
write: (temporary, data) => writeFileSync3(temporary, data),
|
|
25784
|
+
rename: renameSync2,
|
|
25785
|
+
remove: (temporary) => rmSync3(temporary, { force: true })
|
|
22927
25786
|
})
|
|
22928
25787
|
},
|
|
22929
25788
|
service: () => options.service?.({ root: ctx.root.root, env: ctx.env }) ?? createServiceDeps({ root: ctx.root.root, env: ctx.env, scope, now: ctx.now }),
|