omnigateway 0.3.0 → 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 +172 -1
- package/bin/omni.js +1117 -40
- package/gateway.js +947 -89
- package/package.json +1 -1
- package/public/assets/{Chip-DbKk1ExR.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-B7GOsJf8.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--CQnPjoO.js → Readout-Du15WYx0.js} +5 -5
- package/public/assets/{States-CQFkw1UZ.js → States-CxLDlCaw.js} +5 -5
- package/public/assets/{Table-Ba58Wg4-.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/shared-B4o-2quR.js +84 -0
- package/public/assets/{trash-2-DTWWI3vA.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-C5DYoI93.js +0 -4
- package/public/assets/CopyValue-CXjietiZ.js +0 -27
- package/public/assets/Lamp-QRSv-SqM.js +0 -25
- package/public/assets/Meter-Dngwbpjo.js +0 -14
- package/public/assets/Modal-DfltaSCz.js +0 -82
- package/public/assets/Rack-D1zBw9SW.js +0 -151
- package/public/assets/Toggle-DWrWc9-9.js +0 -39
- package/public/assets/TokenBreakdown-CyHJv0O5.js +0 -31
- package/public/assets/_app-CTDgfstQ.js +0 -1
- package/public/assets/_app.accounts-DP6T8z7I.js +0 -64
- package/public/assets/_app.console-DYr7TCAl.js +0 -44
- package/public/assets/_app.database-BzO1S_VR.js +0 -39
- package/public/assets/_app.index-CBt6b-lA.js +0 -62
- package/public/assets/_app.keys-B8HGsPPS.js +0 -76
- package/public/assets/_app.logs-DKsxa2Su.js +0 -68
- package/public/assets/_app.models-CZD2UpM-.js +0 -148
- package/public/assets/_app.settings-B2C1LQIF.js +0 -46
- package/public/assets/_app.usage-CDWUXtZy.js +0 -83
- package/public/assets/dist-IkJ0qg-4.js +0 -1
- package/public/assets/index-ClU3Xqui.js +0 -186
- package/public/assets/login-E3gh2_Cl.js +0 -36
- package/public/assets/plus-DkO4vG-T.js +0 -1
- package/public/assets/queries-BJHk_wso.js +0 -144
- package/public/assets/shared-Bp35cgFf.js +0 -84
package/gateway.js
CHANGED
|
@@ -5235,7 +5235,7 @@ var require_dist = __commonJS(function(exports) {
|
|
|
5235
5235
|
|
|
5236
5236
|
// apps/gateway/src/index.ts
|
|
5237
5237
|
import { existsSync as existsSync2 } from "fs";
|
|
5238
|
-
import { resolve as
|
|
5238
|
+
import { dirname as dirname5, join as join6, resolve as resolve6 } from "path";
|
|
5239
5239
|
|
|
5240
5240
|
// packages/control/src/adminAuth.ts
|
|
5241
5241
|
import { hash, verify } from "@node-rs/argon2";
|
|
@@ -5403,6 +5403,7 @@ var FIELD_ORDER = [
|
|
|
5403
5403
|
"requestedModel",
|
|
5404
5404
|
"credentialId",
|
|
5405
5405
|
"apiKeyId",
|
|
5406
|
+
"plugin",
|
|
5406
5407
|
"snapshotId",
|
|
5407
5408
|
"attempt",
|
|
5408
5409
|
"attempts",
|
|
@@ -25024,6 +25025,39 @@ CREATE TABLE usage_rollup (
|
|
|
25024
25025
|
-- cannot drift into disagreeing about which rows count.
|
|
25025
25026
|
`;
|
|
25026
25027
|
|
|
25028
|
+
// packages/store/src/sqlite/migrations/011_plugin_migrations.sql
|
|
25029
|
+
var _011_plugin_migrations_default = `-- The plugin migration ledger, kept deliberately apart from core's \`migrations\`.
|
|
25030
|
+
--
|
|
25031
|
+
-- Core's track is a single ascending integer owned by this repository. A plugin
|
|
25032
|
+
-- ships its own numbered migrations and knows nothing about ours, so the two
|
|
25033
|
+
-- cannot share a counter: if they did, installing a plugin would consume numbers
|
|
25034
|
+
-- that core's next migration then collides with, and the collision would surface
|
|
25035
|
+
-- as "migration already applied" on an upgrade rather than as anything legible.
|
|
25036
|
+
-- Core's next migration is 012 regardless of how many migrations any plugin
|
|
25037
|
+
-- ships, and a plugin's version 1 is its own version 1 forever.
|
|
25038
|
+
--
|
|
25039
|
+
-- The primary key is (plugin_id, version) rather than a surrogate id because
|
|
25040
|
+
-- that pair *is* the identity: applying the same version twice is the thing this
|
|
25041
|
+
-- table exists to prevent, and a uniqueness constraint states it once instead of
|
|
25042
|
+
-- leaving it to whichever code path happens to check first.
|
|
25043
|
+
--
|
|
25044
|
+
-- \`applied_at\` is milliseconds, matching every other timestamp in this schema.
|
|
25045
|
+
-- It is recorded for an operator reading the table during an incident, not read
|
|
25046
|
+
-- by the host: the host asks only whether a row exists.
|
|
25047
|
+
--
|
|
25048
|
+
-- Note what is *not* here: no row for the plugin's tables, and no cascade. A
|
|
25049
|
+
-- plugin's tables outlive its rows here on purpose \u2014 see \`PluginRepo.dropAll\`
|
|
25050
|
+
-- and \`orphanTables\`. Dropping a table because a plugin is currently absent
|
|
25051
|
+
-- destroys data irreversibly, and a restore is precisely the moment a plugin is
|
|
25052
|
+
-- most likely to be absent.
|
|
25053
|
+
CREATE TABLE plugin_migrations (
|
|
25054
|
+
plugin_id TEXT NOT NULL,
|
|
25055
|
+
version INTEGER NOT NULL,
|
|
25056
|
+
applied_at INTEGER NOT NULL,
|
|
25057
|
+
PRIMARY KEY (plugin_id, version)
|
|
25058
|
+
);
|
|
25059
|
+
`;
|
|
25060
|
+
|
|
25027
25061
|
// packages/store/src/sqlite/rollup.ts
|
|
25028
25062
|
var HOUR_MS = 3600000;
|
|
25029
25063
|
function hourOf(at) {
|
|
@@ -25258,7 +25292,8 @@ var MIGRATIONS = [
|
|
|
25258
25292
|
{ id: 7, sql: _007_quota_samples_default },
|
|
25259
25293
|
{ id: 8, sql: _008_body_logging_default },
|
|
25260
25294
|
{ id: 9, sql: _009_key_limits_default },
|
|
25261
|
-
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup }
|
|
25295
|
+
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
|
|
25296
|
+
{ id: 11, sql: _011_plugin_migrations_default }
|
|
25262
25297
|
];
|
|
25263
25298
|
function openDb(path) {
|
|
25264
25299
|
const db = new Database(path, { create: true });
|
|
@@ -25411,6 +25446,168 @@ function createMaintenanceRepo(db) {
|
|
|
25411
25446
|
}
|
|
25412
25447
|
};
|
|
25413
25448
|
}
|
|
25449
|
+
// packages/store/src/sqlite/plugins.ts
|
|
25450
|
+
var PLUGIN_ID = /^[a-z][a-z0-9-]{0,31}$/;
|
|
25451
|
+
var TABLE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
|
|
25452
|
+
var PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
|
|
25453
|
+
var CORE_TABLES = [
|
|
25454
|
+
"api_keys",
|
|
25455
|
+
"credential_health",
|
|
25456
|
+
"credentials",
|
|
25457
|
+
"migrations",
|
|
25458
|
+
"plugin_migrations",
|
|
25459
|
+
"quota_samples",
|
|
25460
|
+
"quota_windows",
|
|
25461
|
+
"request_bodies",
|
|
25462
|
+
"request_logs",
|
|
25463
|
+
"settings",
|
|
25464
|
+
"usage_daily",
|
|
25465
|
+
"usage_rollup",
|
|
25466
|
+
"virtual_models"
|
|
25467
|
+
];
|
|
25468
|
+
var CORE_TABLE_REFERENCE = new RegExp(`\\b(?:${CORE_TABLES.join("|")})\\b`);
|
|
25469
|
+
var PREFIX3 = "plugin_";
|
|
25470
|
+
function stripNoise(sql) {
|
|
25471
|
+
let out = "";
|
|
25472
|
+
let i = 0;
|
|
25473
|
+
while (i < sql.length) {
|
|
25474
|
+
const c = sql[i];
|
|
25475
|
+
const next = sql[i + 1];
|
|
25476
|
+
if (c === "'") {
|
|
25477
|
+
i += 1;
|
|
25478
|
+
while (i < sql.length) {
|
|
25479
|
+
if (sql[i] === "'") {
|
|
25480
|
+
if (sql[i + 1] === "'")
|
|
25481
|
+
i += 2;
|
|
25482
|
+
else
|
|
25483
|
+
break;
|
|
25484
|
+
} else
|
|
25485
|
+
i += 1;
|
|
25486
|
+
}
|
|
25487
|
+
i += 1;
|
|
25488
|
+
out += " ";
|
|
25489
|
+
} else if (c === "-" && next === "-") {
|
|
25490
|
+
while (i < sql.length && sql[i] !== `
|
|
25491
|
+
`)
|
|
25492
|
+
i += 1;
|
|
25493
|
+
out += " ";
|
|
25494
|
+
} else if (c === "/" && next === "*") {
|
|
25495
|
+
i += 2;
|
|
25496
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
|
|
25497
|
+
i += 1;
|
|
25498
|
+
i += 2;
|
|
25499
|
+
out += " ";
|
|
25500
|
+
} else {
|
|
25501
|
+
out += c;
|
|
25502
|
+
i += 1;
|
|
25503
|
+
}
|
|
25504
|
+
}
|
|
25505
|
+
return out;
|
|
25506
|
+
}
|
|
25507
|
+
function tableFor(pluginId, name) {
|
|
25508
|
+
return `"${PREFIX3}${pluginId}_${name}"`;
|
|
25509
|
+
}
|
|
25510
|
+
function prefixFor(pluginId) {
|
|
25511
|
+
return `${PREFIX3}${pluginId}_`;
|
|
25512
|
+
}
|
|
25513
|
+
function assertPluginId(pluginId) {
|
|
25514
|
+
if (!PLUGIN_ID.test(pluginId)) {
|
|
25515
|
+
throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
|
|
25516
|
+
}
|
|
25517
|
+
}
|
|
25518
|
+
function prepare(pluginId, sql) {
|
|
25519
|
+
assertPluginId(pluginId);
|
|
25520
|
+
const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
|
|
25521
|
+
if (!TABLE_NAME.test(name)) {
|
|
25522
|
+
throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
|
|
25523
|
+
}
|
|
25524
|
+
return tableFor(pluginId, name);
|
|
25525
|
+
});
|
|
25526
|
+
const found = CORE_TABLE_REFERENCE.exec(stripNoise(expanded));
|
|
25527
|
+
if (found !== null) {
|
|
25528
|
+
throw new Error(`plugin sql may not reference the core table ${found[0]}`);
|
|
25529
|
+
}
|
|
25530
|
+
return expanded;
|
|
25531
|
+
}
|
|
25532
|
+
function toBinding(value, index) {
|
|
25533
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
|
|
25534
|
+
return value;
|
|
25535
|
+
}
|
|
25536
|
+
throw new Error(`plugin sql parameter ${index} is not a bindable value`);
|
|
25537
|
+
}
|
|
25538
|
+
var toBindings = (params) => (params ?? []).map(toBinding);
|
|
25539
|
+
function createPluginRepo(db) {
|
|
25540
|
+
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");
|
|
25541
|
+
const tablesOf = (pluginId) => {
|
|
25542
|
+
const prefix = prefixFor(pluginId);
|
|
25543
|
+
return pluginTables().filter((name) => name.startsWith(prefix));
|
|
25544
|
+
};
|
|
25545
|
+
const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
|
|
25546
|
+
return {
|
|
25547
|
+
migrate(pluginId, migrations) {
|
|
25548
|
+
assertPluginId(pluginId);
|
|
25549
|
+
const done = appliedVersions(pluginId);
|
|
25550
|
+
const applied = [];
|
|
25551
|
+
const ordered = [...migrations].sort((a, b) => a.version - b.version);
|
|
25552
|
+
for (const migration of ordered) {
|
|
25553
|
+
if (done.has(migration.version))
|
|
25554
|
+
continue;
|
|
25555
|
+
try {
|
|
25556
|
+
if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
|
|
25557
|
+
throw new Error("migration version must be a positive integer");
|
|
25558
|
+
}
|
|
25559
|
+
const sql = prepare(pluginId, migration.sql);
|
|
25560
|
+
db.transaction(() => {
|
|
25561
|
+
db.run(sql);
|
|
25562
|
+
db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
|
|
25563
|
+
})();
|
|
25564
|
+
applied.push(migration.version);
|
|
25565
|
+
} catch (error51) {
|
|
25566
|
+
return {
|
|
25567
|
+
applied,
|
|
25568
|
+
failed: {
|
|
25569
|
+
version: migration.version,
|
|
25570
|
+
reason: error51 instanceof Error ? error51.message : String(error51)
|
|
25571
|
+
}
|
|
25572
|
+
};
|
|
25573
|
+
}
|
|
25574
|
+
}
|
|
25575
|
+
return { applied };
|
|
25576
|
+
},
|
|
25577
|
+
run(pluginId, sql, params) {
|
|
25578
|
+
db.run(prepare(pluginId, sql), toBindings(params));
|
|
25579
|
+
},
|
|
25580
|
+
all(pluginId, sql, params) {
|
|
25581
|
+
return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
|
|
25582
|
+
},
|
|
25583
|
+
get(pluginId, sql, params) {
|
|
25584
|
+
const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
|
|
25585
|
+
return row === null || row === undefined ? null : row;
|
|
25586
|
+
},
|
|
25587
|
+
transaction(pluginId, fn) {
|
|
25588
|
+
assertPluginId(pluginId);
|
|
25589
|
+
return db.transaction(fn)();
|
|
25590
|
+
},
|
|
25591
|
+
listTables(pluginId) {
|
|
25592
|
+
assertPluginId(pluginId);
|
|
25593
|
+
return tablesOf(pluginId);
|
|
25594
|
+
},
|
|
25595
|
+
dropAll(pluginId) {
|
|
25596
|
+
assertPluginId(pluginId);
|
|
25597
|
+
const tables = tablesOf(pluginId);
|
|
25598
|
+
db.transaction(() => {
|
|
25599
|
+
for (const table of tables)
|
|
25600
|
+
db.run(`DROP TABLE IF EXISTS "${table}"`);
|
|
25601
|
+
db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
|
|
25602
|
+
})();
|
|
25603
|
+
return tables.length;
|
|
25604
|
+
},
|
|
25605
|
+
orphanTables(installedIds) {
|
|
25606
|
+
const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
|
|
25607
|
+
return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
|
|
25608
|
+
}
|
|
25609
|
+
};
|
|
25610
|
+
}
|
|
25414
25611
|
// packages/rtk/src/catalog.ts
|
|
25415
25612
|
var RTK_FILTER_IDS = [
|
|
25416
25613
|
"git-diff",
|
|
@@ -25723,7 +25920,8 @@ async function createStore(opts) {
|
|
|
25723
25920
|
keys: createKeyRepo(db, logger2),
|
|
25724
25921
|
usage: createUsageRepo(db),
|
|
25725
25922
|
bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
25726
|
-
maintenance: createMaintenanceRepo(db)
|
|
25923
|
+
maintenance: createMaintenanceRepo(db),
|
|
25924
|
+
plugins: createPluginRepo(db)
|
|
25727
25925
|
};
|
|
25728
25926
|
};
|
|
25729
25927
|
let handle = open();
|
|
@@ -25796,6 +25994,22 @@ async function createStore(opts) {
|
|
|
25796
25994
|
snapshotTo: (path) => handle.maintenance.snapshotTo(path),
|
|
25797
25995
|
inspect: (path) => handle.maintenance.inspect(path)
|
|
25798
25996
|
},
|
|
25997
|
+
plugins: {
|
|
25998
|
+
migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
|
|
25999
|
+
run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
|
|
26000
|
+
all(pluginId, sql, params) {
|
|
26001
|
+
return handle.plugins.all(pluginId, sql, params);
|
|
26002
|
+
},
|
|
26003
|
+
get(pluginId, sql, params) {
|
|
26004
|
+
return handle.plugins.get(pluginId, sql, params);
|
|
26005
|
+
},
|
|
26006
|
+
transaction(pluginId, fn) {
|
|
26007
|
+
return handle.plugins.transaction(pluginId, fn);
|
|
26008
|
+
},
|
|
26009
|
+
listTables: (pluginId) => handle.plugins.listTables(pluginId),
|
|
26010
|
+
dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
|
|
26011
|
+
orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
|
|
26012
|
+
},
|
|
25799
26013
|
routing: {
|
|
25800
26014
|
version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
25801
26015
|
subscribe(listener) {
|
|
@@ -26211,6 +26425,13 @@ async function swapIn(deps, candidate) {
|
|
|
26211
26425
|
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
26212
26426
|
});
|
|
26213
26427
|
}
|
|
26428
|
+
try {
|
|
26429
|
+
await deps.reapplyPluginSchema?.();
|
|
26430
|
+
} catch (error51) {
|
|
26431
|
+
(deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
|
|
26432
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
26433
|
+
});
|
|
26434
|
+
}
|
|
26214
26435
|
return {
|
|
26215
26436
|
ok: true,
|
|
26216
26437
|
counts: inspection.counts,
|
|
@@ -26584,13 +26805,89 @@ import {
|
|
|
26584
26805
|
copyFileSync,
|
|
26585
26806
|
mkdirSync,
|
|
26586
26807
|
readdirSync,
|
|
26808
|
+
readFileSync,
|
|
26587
26809
|
realpathSync,
|
|
26588
26810
|
renameSync,
|
|
26589
26811
|
rmSync,
|
|
26590
26812
|
statfsSync,
|
|
26591
|
-
statSync
|
|
26813
|
+
statSync,
|
|
26814
|
+
writeFileSync
|
|
26592
26815
|
} from "fs";
|
|
26593
26816
|
import { join as join3 } from "path";
|
|
26817
|
+
|
|
26818
|
+
// packages/plugin-api/src/version.ts
|
|
26819
|
+
var PLUGIN_API_VERSION = 1;
|
|
26820
|
+
var DASHBOARD_SDK_VERSION = "0.1.0";
|
|
26821
|
+
// packages/plugin-api/src/manifest.ts
|
|
26822
|
+
var CAPABILITIES = [
|
|
26823
|
+
"storage",
|
|
26824
|
+
"files",
|
|
26825
|
+
"net:outbound",
|
|
26826
|
+
"events:request",
|
|
26827
|
+
"events:limit"
|
|
26828
|
+
];
|
|
26829
|
+
var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
26830
|
+
var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
|
|
26831
|
+
var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
|
|
26832
|
+
message: "entry must be a relative path with no '..' segment"
|
|
26833
|
+
});
|
|
26834
|
+
var originSchema = exports_external.string().refine((value) => {
|
|
26835
|
+
let url2;
|
|
26836
|
+
try {
|
|
26837
|
+
url2 = new URL(value);
|
|
26838
|
+
} catch {
|
|
26839
|
+
return false;
|
|
26840
|
+
}
|
|
26841
|
+
if (url2.protocol !== "https:" && url2.protocol !== "http:")
|
|
26842
|
+
return false;
|
|
26843
|
+
if (url2.hostname === "")
|
|
26844
|
+
return false;
|
|
26845
|
+
if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
|
|
26846
|
+
return false;
|
|
26847
|
+
return value === url2.origin || value === `${url2.origin}/`;
|
|
26848
|
+
}, { message: "origin must be scheme://host with no path, query or fragment" });
|
|
26849
|
+
var navSchema = exports_external.object({
|
|
26850
|
+
label: exports_external.string().min(1).max(32)
|
|
26851
|
+
}).strict();
|
|
26852
|
+
var manifestSchema = exports_external.object({
|
|
26853
|
+
id: idSchema,
|
|
26854
|
+
name: exports_external.string().min(1).max(64),
|
|
26855
|
+
version: exports_external.string().min(1).max(32),
|
|
26856
|
+
api: exports_external.number().int().nonnegative(),
|
|
26857
|
+
sdk: exports_external.string().min(1).max(32).optional(),
|
|
26858
|
+
server: entrySchema.optional(),
|
|
26859
|
+
ui: entrySchema.optional(),
|
|
26860
|
+
nav: navSchema.optional(),
|
|
26861
|
+
capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
|
|
26862
|
+
origins: exports_external.array(originSchema).min(1).optional(),
|
|
26863
|
+
defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
26864
|
+
}).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
|
|
26865
|
+
message: "manifest must declare a server entry, a ui entry, or both"
|
|
26866
|
+
}).refine((m) => m.ui === undefined || m.sdk !== undefined, {
|
|
26867
|
+
message: "a ui entry requires an sdk range"
|
|
26868
|
+
}).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
|
|
26869
|
+
message: "net:outbound requires origins"
|
|
26870
|
+
}).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
|
|
26871
|
+
message: "origins requires the net:outbound capability"
|
|
26872
|
+
});
|
|
26873
|
+
function safeParseManifest(input) {
|
|
26874
|
+
const parsed = manifestSchema.safeParse(input);
|
|
26875
|
+
if (parsed.success)
|
|
26876
|
+
return { ok: true, manifest: parsed.data };
|
|
26877
|
+
const first = parsed.error.issues[0];
|
|
26878
|
+
const path = first?.path.join(".") ?? "";
|
|
26879
|
+
const message = first?.message ?? "invalid manifest";
|
|
26880
|
+
return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
|
|
26881
|
+
}
|
|
26882
|
+
function isApiCompatible(manifest) {
|
|
26883
|
+
return manifest.api === PLUGIN_API_VERSION;
|
|
26884
|
+
}
|
|
26885
|
+
|
|
26886
|
+
// packages/control/src/plugins.ts
|
|
26887
|
+
var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
|
|
26888
|
+
var decoder2 = new TextDecoder;
|
|
26889
|
+
|
|
26890
|
+
// packages/control/src/nodeFs.ts
|
|
26594
26891
|
function nodeDatabaseFs() {
|
|
26595
26892
|
const dirBytes = (dir) => {
|
|
26596
26893
|
let entries;
|
|
@@ -27870,9 +28167,15 @@ function logLimit(requested) {
|
|
|
27870
28167
|
async function recentLogs(store, requested) {
|
|
27871
28168
|
return store.usage.recent(logLimit(requested));
|
|
27872
28169
|
}
|
|
28170
|
+
// packages/plugin-api/src/events.ts
|
|
28171
|
+
var WINDOW_MS2 = {
|
|
28172
|
+
"1m": 60000,
|
|
28173
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
28174
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
28175
|
+
};
|
|
27873
28176
|
// apps/gateway/src/app.ts
|
|
27874
|
-
import { existsSync, realpathSync as
|
|
27875
|
-
import { resolve as
|
|
28177
|
+
import { existsSync, realpathSync as realpathSync3 } from "fs";
|
|
28178
|
+
import { resolve as resolve3, sep as sep3 } from "path";
|
|
27876
28179
|
|
|
27877
28180
|
// node_modules/.bun/memoirist@0.4.0/node_modules/memoirist/dist/bun/index.js
|
|
27878
28181
|
var Y = (v, b) => {
|
|
@@ -36713,13 +37016,13 @@ async function* streamResponse(response) {
|
|
|
36713
37016
|
const body2 = response.body;
|
|
36714
37017
|
if (!body2)
|
|
36715
37018
|
return;
|
|
36716
|
-
const reader = body2.getReader(),
|
|
37019
|
+
const reader = body2.getReader(), decoder3 = new TextDecoder;
|
|
36717
37020
|
try {
|
|
36718
37021
|
for (;; ) {
|
|
36719
37022
|
const { done, value } = await reader.read();
|
|
36720
37023
|
if (done)
|
|
36721
37024
|
break;
|
|
36722
|
-
typeof value == "string" ? yield value : yield
|
|
37025
|
+
typeof value == "string" ? yield value : yield decoder3.decode(value);
|
|
36723
37026
|
}
|
|
36724
37027
|
} finally {
|
|
36725
37028
|
reader.releaseLock();
|
|
@@ -42237,12 +42540,12 @@ var _Elysia = class _Elysia2 {
|
|
|
42237
42540
|
}), this) : (typeof this.server?.reload == "function" && this.server.reload(this.server || {}), this._handle = composeGeneralHandler(this), this);
|
|
42238
42541
|
}
|
|
42239
42542
|
get fetch() {
|
|
42240
|
-
const
|
|
42543
|
+
const fetch2 = this.config.aot ? composeGeneralHandler(this) : createDynamicHandler(this);
|
|
42241
42544
|
return Object.defineProperty(this, "fetch", {
|
|
42242
|
-
value:
|
|
42545
|
+
value: fetch2,
|
|
42243
42546
|
configurable: true,
|
|
42244
42547
|
writable: true
|
|
42245
|
-
}),
|
|
42548
|
+
}), fetch2;
|
|
42246
42549
|
}
|
|
42247
42550
|
get modules() {
|
|
42248
42551
|
return this.promisedModules;
|
|
@@ -43070,58 +43373,6 @@ function openaiRateLimitHeaders(headroom, now) {
|
|
|
43070
43373
|
return headers;
|
|
43071
43374
|
}
|
|
43072
43375
|
|
|
43073
|
-
// apps/gateway/src/quiesce.ts
|
|
43074
|
-
function createQuiesceLatch() {
|
|
43075
|
-
let admitting = true;
|
|
43076
|
-
let inFlight = 0;
|
|
43077
|
-
let onIdle = null;
|
|
43078
|
-
const settle = () => {
|
|
43079
|
-
if (inFlight > 0 || onIdle === null)
|
|
43080
|
-
return;
|
|
43081
|
-
const resolve2 = onIdle;
|
|
43082
|
-
onIdle = null;
|
|
43083
|
-
resolve2();
|
|
43084
|
-
};
|
|
43085
|
-
return {
|
|
43086
|
-
enter() {
|
|
43087
|
-
if (!admitting)
|
|
43088
|
-
return null;
|
|
43089
|
-
inFlight++;
|
|
43090
|
-
let released = false;
|
|
43091
|
-
return () => {
|
|
43092
|
-
if (released)
|
|
43093
|
-
return;
|
|
43094
|
-
released = true;
|
|
43095
|
-
inFlight--;
|
|
43096
|
-
settle();
|
|
43097
|
-
};
|
|
43098
|
-
},
|
|
43099
|
-
isClosed: () => !admitting,
|
|
43100
|
-
open() {
|
|
43101
|
-
admitting = true;
|
|
43102
|
-
},
|
|
43103
|
-
async close(deadlineMs) {
|
|
43104
|
-
admitting = false;
|
|
43105
|
-
if (inFlight === 0)
|
|
43106
|
-
return { drained: true, inFlight: 0 };
|
|
43107
|
-
let timer;
|
|
43108
|
-
const idle = new Promise((resolve2) => {
|
|
43109
|
-
onIdle = resolve2;
|
|
43110
|
-
});
|
|
43111
|
-
const deadline = new Promise((resolve2) => {
|
|
43112
|
-
timer = setTimeout(resolve2, deadlineMs);
|
|
43113
|
-
});
|
|
43114
|
-
try {
|
|
43115
|
-
await Promise.race([idle, deadline]);
|
|
43116
|
-
} finally {
|
|
43117
|
-
clearTimeout(timer);
|
|
43118
|
-
onIdle = null;
|
|
43119
|
-
}
|
|
43120
|
-
return { drained: inFlight === 0, inFlight };
|
|
43121
|
-
}
|
|
43122
|
-
};
|
|
43123
|
-
}
|
|
43124
|
-
|
|
43125
43376
|
// apps/gateway/src/ingress/schemas.ts
|
|
43126
43377
|
var cacheControlSchema = exports_external.object({
|
|
43127
43378
|
type: exports_external.literal("ephemeral"),
|
|
@@ -43264,6 +43515,225 @@ function apiErrorHandler(input) {
|
|
|
43264
43515
|
return input.code === "NOT_FOUND" ? undefined : apiErrorResponse(input.error);
|
|
43265
43516
|
}
|
|
43266
43517
|
|
|
43518
|
+
// apps/gateway/src/plugins/routes.ts
|
|
43519
|
+
var MOUNT_PREFIX = "/api/plugins";
|
|
43520
|
+
var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
43521
|
+
var SEGMENT_PATTERN = /^[A-Za-z0-9._~-]+$/;
|
|
43522
|
+
var PARAM_PATTERN = /^:[A-Za-z][A-Za-z0-9_]*$/;
|
|
43523
|
+
function mountPath(id, path) {
|
|
43524
|
+
if (!ID_PATTERN2.test(id))
|
|
43525
|
+
return null;
|
|
43526
|
+
if (!path.startsWith("/"))
|
|
43527
|
+
return null;
|
|
43528
|
+
const base = `${MOUNT_PREFIX}/${id}`;
|
|
43529
|
+
if (path !== "/") {
|
|
43530
|
+
for (const segment of path.slice(1).split("/")) {
|
|
43531
|
+
if (segment === "." || segment === "..")
|
|
43532
|
+
return null;
|
|
43533
|
+
if (!SEGMENT_PATTERN.test(segment) && !PARAM_PATTERN.test(segment))
|
|
43534
|
+
return null;
|
|
43535
|
+
}
|
|
43536
|
+
}
|
|
43537
|
+
const mounted = `${base}${path}`;
|
|
43538
|
+
if (mounted !== base && !mounted.startsWith(`${base}/`))
|
|
43539
|
+
return null;
|
|
43540
|
+
return mounted;
|
|
43541
|
+
}
|
|
43542
|
+
async function pluginRequest(request2, params) {
|
|
43543
|
+
const url2 = new URL(request2.url);
|
|
43544
|
+
const query = Object.fromEntries(url2.searchParams);
|
|
43545
|
+
let body2 = null;
|
|
43546
|
+
if (request2.method !== "GET") {
|
|
43547
|
+
const text = await request2.text();
|
|
43548
|
+
if (text.trim().length > 0) {
|
|
43549
|
+
try {
|
|
43550
|
+
body2 = JSON.parse(text);
|
|
43551
|
+
} catch {
|
|
43552
|
+
throw new GatewayError("BAD_REQUEST", "invalid JSON body");
|
|
43553
|
+
}
|
|
43554
|
+
}
|
|
43555
|
+
}
|
|
43556
|
+
return { params, query, body: body2 };
|
|
43557
|
+
}
|
|
43558
|
+
var CACHE_CONTROL_PATTERN = /^[A-Za-z0-9 ,=."'_-]{1,200}$/;
|
|
43559
|
+
function toResponse(result) {
|
|
43560
|
+
const status2 = result.status ?? 200;
|
|
43561
|
+
const headers = {};
|
|
43562
|
+
if (result.cacheControl !== undefined && CACHE_CONTROL_PATTERN.test(result.cacheControl)) {
|
|
43563
|
+
headers["cache-control"] = result.cacheControl;
|
|
43564
|
+
}
|
|
43565
|
+
if (result.bytes !== undefined) {
|
|
43566
|
+
headers["content-type"] = result.contentType ?? "application/octet-stream";
|
|
43567
|
+
return new Response(result.bytes, { status: status2, headers });
|
|
43568
|
+
}
|
|
43569
|
+
headers["content-type"] = "application/json";
|
|
43570
|
+
return new Response(JSON.stringify(result.json ?? null), { status: status2, headers });
|
|
43571
|
+
}
|
|
43572
|
+
function isPluginResponse(value) {
|
|
43573
|
+
return typeof value === "object" && value !== null;
|
|
43574
|
+
}
|
|
43575
|
+
function pluginRoutes(deps) {
|
|
43576
|
+
const logger2 = deps.logger ?? noopLogger;
|
|
43577
|
+
const app = new Elysia;
|
|
43578
|
+
app.onError(apiErrorHandler);
|
|
43579
|
+
for (const plugin of deps.plugins) {
|
|
43580
|
+
for (const route of plugin.routes) {
|
|
43581
|
+
const path = mountPath(plugin.id, route.path);
|
|
43582
|
+
if (path === null) {
|
|
43583
|
+
logger2.error("plugin route refused", { plugin: plugin.id });
|
|
43584
|
+
continue;
|
|
43585
|
+
}
|
|
43586
|
+
const handle = async (context) => {
|
|
43587
|
+
await requireAdmin(context.request, deps.admin);
|
|
43588
|
+
const input = await pluginRequest(context.request, context.params);
|
|
43589
|
+
try {
|
|
43590
|
+
const result = await route.handler(input);
|
|
43591
|
+
if (!isPluginResponse(result))
|
|
43592
|
+
throw new Error("handler returned no response");
|
|
43593
|
+
return toResponse(result);
|
|
43594
|
+
} catch {
|
|
43595
|
+
logger2.error("plugin route handler failed", { plugin: plugin.id });
|
|
43596
|
+
return apiErrorResponse(new GatewayError("INTERNAL", "plugin route failed"));
|
|
43597
|
+
}
|
|
43598
|
+
};
|
|
43599
|
+
switch (route.method) {
|
|
43600
|
+
case "GET":
|
|
43601
|
+
app.get(path, handle);
|
|
43602
|
+
break;
|
|
43603
|
+
case "POST":
|
|
43604
|
+
app.post(path, handle);
|
|
43605
|
+
break;
|
|
43606
|
+
case "PUT":
|
|
43607
|
+
app.put(path, handle);
|
|
43608
|
+
break;
|
|
43609
|
+
case "DELETE":
|
|
43610
|
+
app.delete(path, handle);
|
|
43611
|
+
break;
|
|
43612
|
+
default:
|
|
43613
|
+
logger2.error("plugin route method not supported", { plugin: plugin.id });
|
|
43614
|
+
break;
|
|
43615
|
+
}
|
|
43616
|
+
}
|
|
43617
|
+
}
|
|
43618
|
+
return app;
|
|
43619
|
+
}
|
|
43620
|
+
|
|
43621
|
+
// apps/gateway/src/plugins/ui.ts
|
|
43622
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
43623
|
+
import { join as join4, resolve as resolve2, sep as sep2 } from "path";
|
|
43624
|
+
var PLUGIN_ASSET_PREFIX = "/plugin-assets";
|
|
43625
|
+
function pluginCatalog(plugins) {
|
|
43626
|
+
return plugins.map((plugin) => ({
|
|
43627
|
+
id: plugin.id,
|
|
43628
|
+
name: plugin.manifest.name,
|
|
43629
|
+
version: plugin.manifest.version,
|
|
43630
|
+
nav: plugin.manifest.nav === undefined ? null : plugin.manifest.nav,
|
|
43631
|
+
ui: plugin.ui === undefined ? null : {
|
|
43632
|
+
entry: plugin.ui.compatible ? `${PLUGIN_ASSET_PREFIX}/${plugin.id}/${plugin.ui.entry}` : null,
|
|
43633
|
+
compatible: plugin.ui.compatible,
|
|
43634
|
+
...plugin.ui.reason === undefined ? {} : { reason: plugin.ui.reason }
|
|
43635
|
+
}
|
|
43636
|
+
}));
|
|
43637
|
+
}
|
|
43638
|
+
function pluginUiRoutes(deps) {
|
|
43639
|
+
const logger2 = deps.logger ?? noopLogger;
|
|
43640
|
+
const roots = new Map;
|
|
43641
|
+
for (const plugin of deps.plugins) {
|
|
43642
|
+
if (plugin.ui === undefined || !plugin.ui.compatible)
|
|
43643
|
+
continue;
|
|
43644
|
+
try {
|
|
43645
|
+
roots.set(plugin.id, realpathSync2(join4(plugin.ui.dir, "ui")));
|
|
43646
|
+
} catch {
|
|
43647
|
+
logger2.warn("plugin ui directory is missing", { plugin: plugin.id });
|
|
43648
|
+
}
|
|
43649
|
+
}
|
|
43650
|
+
return new Elysia().onError(apiErrorHandler).get("/api/plugins", async ({ request: request2 }) => {
|
|
43651
|
+
await requireAdmin(request2, deps.admin);
|
|
43652
|
+
return { plugins: pluginCatalog(deps.plugins) };
|
|
43653
|
+
}).get(`${PLUGIN_ASSET_PREFIX}/:id/*`, ({ params, set: set2 }) => {
|
|
43654
|
+
const notFound = () => {
|
|
43655
|
+
set2.status = 404;
|
|
43656
|
+
return { error: { code: "NOT_FOUND", message: "no such plugin asset" } };
|
|
43657
|
+
};
|
|
43658
|
+
const root = roots.get(params.id);
|
|
43659
|
+
if (root === undefined)
|
|
43660
|
+
return notFound();
|
|
43661
|
+
let rest;
|
|
43662
|
+
try {
|
|
43663
|
+
rest = decodeURIComponent(params["*"] ?? "");
|
|
43664
|
+
} catch {
|
|
43665
|
+
return notFound();
|
|
43666
|
+
}
|
|
43667
|
+
let real;
|
|
43668
|
+
try {
|
|
43669
|
+
real = realpathSync2(resolve2(root, `./${rest}`));
|
|
43670
|
+
} catch {
|
|
43671
|
+
return notFound();
|
|
43672
|
+
}
|
|
43673
|
+
if (!real.startsWith(`${root}${sep2}`))
|
|
43674
|
+
return notFound();
|
|
43675
|
+
const file3 = Bun.file(real);
|
|
43676
|
+
return new Response(file3, {
|
|
43677
|
+
headers: {
|
|
43678
|
+
"cache-control": "no-cache",
|
|
43679
|
+
"content-type": real.endsWith(".js") ? "text/javascript; charset=utf-8" : real.endsWith(".css") ? "text/css; charset=utf-8" : file3.type
|
|
43680
|
+
}
|
|
43681
|
+
});
|
|
43682
|
+
});
|
|
43683
|
+
}
|
|
43684
|
+
|
|
43685
|
+
// apps/gateway/src/quiesce.ts
|
|
43686
|
+
function createQuiesceLatch() {
|
|
43687
|
+
let admitting = true;
|
|
43688
|
+
let inFlight = 0;
|
|
43689
|
+
let onIdle = null;
|
|
43690
|
+
const settle = () => {
|
|
43691
|
+
if (inFlight > 0 || onIdle === null)
|
|
43692
|
+
return;
|
|
43693
|
+
const resolve3 = onIdle;
|
|
43694
|
+
onIdle = null;
|
|
43695
|
+
resolve3();
|
|
43696
|
+
};
|
|
43697
|
+
return {
|
|
43698
|
+
enter() {
|
|
43699
|
+
if (!admitting)
|
|
43700
|
+
return null;
|
|
43701
|
+
inFlight++;
|
|
43702
|
+
let released = false;
|
|
43703
|
+
return () => {
|
|
43704
|
+
if (released)
|
|
43705
|
+
return;
|
|
43706
|
+
released = true;
|
|
43707
|
+
inFlight--;
|
|
43708
|
+
settle();
|
|
43709
|
+
};
|
|
43710
|
+
},
|
|
43711
|
+
isClosed: () => !admitting,
|
|
43712
|
+
open() {
|
|
43713
|
+
admitting = true;
|
|
43714
|
+
},
|
|
43715
|
+
async close(deadlineMs) {
|
|
43716
|
+
admitting = false;
|
|
43717
|
+
if (inFlight === 0)
|
|
43718
|
+
return { drained: true, inFlight: 0 };
|
|
43719
|
+
let timer;
|
|
43720
|
+
const idle = new Promise((resolve3) => {
|
|
43721
|
+
onIdle = resolve3;
|
|
43722
|
+
});
|
|
43723
|
+
const deadline = new Promise((resolve3) => {
|
|
43724
|
+
timer = setTimeout(resolve3, deadlineMs);
|
|
43725
|
+
});
|
|
43726
|
+
try {
|
|
43727
|
+
await Promise.race([idle, deadline]);
|
|
43728
|
+
} finally {
|
|
43729
|
+
clearTimeout(timer);
|
|
43730
|
+
onIdle = null;
|
|
43731
|
+
}
|
|
43732
|
+
return { drained: inFlight === 0, inFlight };
|
|
43733
|
+
}
|
|
43734
|
+
};
|
|
43735
|
+
}
|
|
43736
|
+
|
|
43267
43737
|
// apps/gateway/src/routes/admin.ts
|
|
43268
43738
|
function adminRoutes(deps) {
|
|
43269
43739
|
const logger2 = deps.logger ?? noopLogger;
|
|
@@ -43698,7 +44168,7 @@ function createBodyCollector(options) {
|
|
|
43698
44168
|
const client = { request: null, response: null, truncated: false };
|
|
43699
44169
|
const drain = async (stream2, entry) => {
|
|
43700
44170
|
const reader = stream2.getReader();
|
|
43701
|
-
const
|
|
44171
|
+
const decoder3 = new TextDecoder;
|
|
43702
44172
|
let bytes = 0;
|
|
43703
44173
|
try {
|
|
43704
44174
|
while (true) {
|
|
@@ -43713,11 +44183,11 @@ function createBodyCollector(options) {
|
|
|
43713
44183
|
continue;
|
|
43714
44184
|
}
|
|
43715
44185
|
bytes += value.byteLength;
|
|
43716
|
-
entry.responseText +=
|
|
44186
|
+
entry.responseText += decoder3.decode(value, { stream: true });
|
|
43717
44187
|
if (bytes > MAX_CAPTURED_BODY_BYTES)
|
|
43718
44188
|
entry.truncated = true;
|
|
43719
44189
|
}
|
|
43720
|
-
entry.responseText +=
|
|
44190
|
+
entry.responseText += decoder3.decode();
|
|
43721
44191
|
} catch {
|
|
43722
44192
|
entry.truncated = true;
|
|
43723
44193
|
} finally {
|
|
@@ -45666,7 +46136,7 @@ async function routeLog(store, requestId, target, logger2 = noopLogger) {
|
|
|
45666
46136
|
report(logger2, "failed to record request route", requestId, error51);
|
|
45667
46137
|
}
|
|
45668
46138
|
}
|
|
45669
|
-
async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit) {
|
|
46139
|
+
async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit, emit) {
|
|
45670
46140
|
try {
|
|
45671
46141
|
await store.usage.append({ ...log, apiKeyId: keyId });
|
|
45672
46142
|
} catch (error51) {
|
|
@@ -45678,6 +46148,28 @@ async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit)
|
|
|
45678
46148
|
costUsd: log.costUsd
|
|
45679
46149
|
});
|
|
45680
46150
|
}
|
|
46151
|
+
if (keyId !== null && emit !== undefined) {
|
|
46152
|
+
try {
|
|
46153
|
+
emit({
|
|
46154
|
+
requestId: log.id,
|
|
46155
|
+
apiKeyId: keyId,
|
|
46156
|
+
provider: log.resolvedProvider,
|
|
46157
|
+
model: log.resolvedModel ?? log.requestedModel,
|
|
46158
|
+
tokens: {
|
|
46159
|
+
input: log.inputTokens,
|
|
46160
|
+
output: log.outputTokens,
|
|
46161
|
+
cacheRead: log.cacheReadTokens,
|
|
46162
|
+
cacheWrite: log.cacheWriteTokens
|
|
46163
|
+
},
|
|
46164
|
+
costUsd: log.costUsd,
|
|
46165
|
+
durationMs: log.durationMs,
|
|
46166
|
+
ok: log.errorCode === null && log.status < 400,
|
|
46167
|
+
at: log.at
|
|
46168
|
+
});
|
|
46169
|
+
} catch (error51) {
|
|
46170
|
+
report(logger2, "failed to emit plugin event", log.id, error51);
|
|
46171
|
+
}
|
|
46172
|
+
}
|
|
45681
46173
|
if (bodies === undefined)
|
|
45682
46174
|
return;
|
|
45683
46175
|
try {
|
|
@@ -46176,12 +46668,12 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
46176
46668
|
function waitForCancellation(promise2, signal) {
|
|
46177
46669
|
if (signal.aborted)
|
|
46178
46670
|
return Promise.reject(signal.reason);
|
|
46179
|
-
return new Promise((
|
|
46671
|
+
return new Promise((resolve3, reject) => {
|
|
46180
46672
|
const abort = () => reject(signal.reason);
|
|
46181
46673
|
signal.addEventListener("abort", abort, { once: true });
|
|
46182
46674
|
promise2.then((value) => {
|
|
46183
46675
|
signal.removeEventListener("abort", abort);
|
|
46184
|
-
|
|
46676
|
+
resolve3(value);
|
|
46185
46677
|
}, (error51) => {
|
|
46186
46678
|
signal.removeEventListener("abort", abort);
|
|
46187
46679
|
reject(error51);
|
|
@@ -47078,8 +47570,8 @@ var KEEPALIVE_MS = 1e4;
|
|
|
47078
47570
|
var KEEPALIVE = Symbol("keepalive");
|
|
47079
47571
|
function withKeepalive(pending, ms) {
|
|
47080
47572
|
let timer;
|
|
47081
|
-
const tick = new Promise((
|
|
47082
|
-
timer = setTimeout(() =>
|
|
47573
|
+
const tick = new Promise((resolve3) => {
|
|
47574
|
+
timer = setTimeout(() => resolve3(KEEPALIVE), ms);
|
|
47083
47575
|
});
|
|
47084
47576
|
return Promise.race([pending, tick]).finally(() => clearTimeout(timer));
|
|
47085
47577
|
}
|
|
@@ -47298,7 +47790,7 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
47298
47790
|
reportRejection(deps.logger, requestId, completed, gatewayError, surface);
|
|
47299
47791
|
}
|
|
47300
47792
|
logged = true;
|
|
47301
|
-
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit);
|
|
47793
|
+
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit);
|
|
47302
47794
|
};
|
|
47303
47795
|
if (chatRequest.stream) {
|
|
47304
47796
|
const frames = surface === "anthropic" ? anthropicStream(dispatched.events, requestId) : openaiStream(dispatched.events, requestId, Math.floor(deps.now() / 1000));
|
|
@@ -47339,7 +47831,7 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
47339
47831
|
if (collector !== null && !logged)
|
|
47340
47832
|
collector.client.response = rejection;
|
|
47341
47833
|
if (!logged)
|
|
47342
|
-
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit);
|
|
47834
|
+
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit);
|
|
47343
47835
|
if (!cancelled)
|
|
47344
47836
|
reportRejection(deps.logger, requestId, completed, gatewayError, surface);
|
|
47345
47837
|
return jsonResponse(rejection, HTTP_STATUS[gatewayError.code], {
|
|
@@ -47456,11 +47948,11 @@ function createApp(deps) {
|
|
|
47456
47948
|
now,
|
|
47457
47949
|
logger: logger2
|
|
47458
47950
|
});
|
|
47459
|
-
const staticDir = deps.staticDir ?
|
|
47951
|
+
const staticDir = deps.staticDir ? resolve3(deps.staticDir) : undefined;
|
|
47460
47952
|
let staticRoot;
|
|
47461
47953
|
if (staticDir) {
|
|
47462
47954
|
try {
|
|
47463
|
-
staticRoot =
|
|
47955
|
+
staticRoot = realpathSync3(staticDir);
|
|
47464
47956
|
} catch {}
|
|
47465
47957
|
}
|
|
47466
47958
|
const latch = deps.latch ?? createQuiesceLatch();
|
|
@@ -47489,7 +47981,8 @@ function createApp(deps) {
|
|
|
47489
47981
|
logger: logger2,
|
|
47490
47982
|
...deps.loadRegistry === undefined ? {} : { loadRegistry: deps.loadRegistry },
|
|
47491
47983
|
discoveryMirrors: deps.discoveryMirrors === true,
|
|
47492
|
-
bodyLoggingAllowed: deps.bodyLoggingAllowed === true
|
|
47984
|
+
bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
|
|
47985
|
+
...deps.emit === undefined ? {} : { emit: deps.emit }
|
|
47493
47986
|
})).use(adminRoutes({
|
|
47494
47987
|
store: deps.store,
|
|
47495
47988
|
admin,
|
|
@@ -47502,6 +47995,7 @@ function createApp(deps) {
|
|
|
47502
47995
|
...deps.console === undefined ? {} : { console: deps.console }
|
|
47503
47996
|
})).use(databaseRoutes({
|
|
47504
47997
|
store: deps.store,
|
|
47998
|
+
...deps.reapplyPluginSchema === undefined ? {} : { reapplyPluginSchema: deps.reapplyPluginSchema },
|
|
47505
47999
|
admin,
|
|
47506
48000
|
latch,
|
|
47507
48001
|
snapshots,
|
|
@@ -47516,6 +48010,14 @@ function createApp(deps) {
|
|
|
47516
48010
|
http,
|
|
47517
48011
|
now,
|
|
47518
48012
|
logger: logger2
|
|
48013
|
+
})).use(pluginRoutes({
|
|
48014
|
+
admin,
|
|
48015
|
+
plugins: deps.plugins ?? [],
|
|
48016
|
+
logger: logger2
|
|
48017
|
+
})).use(pluginUiRoutes({
|
|
48018
|
+
admin,
|
|
48019
|
+
plugins: deps.pluginUi ?? [],
|
|
48020
|
+
logger: logger2
|
|
47519
48021
|
})).get("/*", ({ path, request: request2, set: set2 }) => {
|
|
47520
48022
|
const rawPath = new URL(request2.url).pathname;
|
|
47521
48023
|
const error51 = () => {
|
|
@@ -47534,14 +48036,14 @@ function createApp(deps) {
|
|
|
47534
48036
|
if (protectedPrefix)
|
|
47535
48037
|
return error51();
|
|
47536
48038
|
const requestedPath = decodedPath === "/" ? "/index.html" : decodedPath;
|
|
47537
|
-
const filePath =
|
|
47538
|
-
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${
|
|
48039
|
+
const filePath = resolve3(staticRoot, `.${requestedPath}`);
|
|
48040
|
+
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${sep3}`))
|
|
47539
48041
|
return error51();
|
|
47540
48042
|
let resolvedFilePath;
|
|
47541
48043
|
try {
|
|
47542
|
-
resolvedFilePath =
|
|
48044
|
+
resolvedFilePath = realpathSync3(filePath);
|
|
47543
48045
|
} catch {}
|
|
47544
|
-
if (resolvedFilePath && resolvedFilePath !== staticRoot && !resolvedFilePath.startsWith(`${staticRoot}${
|
|
48046
|
+
if (resolvedFilePath && resolvedFilePath !== staticRoot && !resolvedFilePath.startsWith(`${staticRoot}${sep3}`)) {
|
|
47545
48047
|
return error51();
|
|
47546
48048
|
}
|
|
47547
48049
|
const file3 = resolvedFilePath ? Bun.file(resolvedFilePath) : undefined;
|
|
@@ -47557,7 +48059,7 @@ function createApp(deps) {
|
|
|
47557
48059
|
const isNavigation = request2.headers.get("accept")?.includes("text/html") ?? false;
|
|
47558
48060
|
if (decodedPath.startsWith("/assets/") || !isNavigation)
|
|
47559
48061
|
return error51();
|
|
47560
|
-
const indexPath =
|
|
48062
|
+
const indexPath = resolve3(staticRoot, "index.html");
|
|
47561
48063
|
const index = Bun.file(indexPath);
|
|
47562
48064
|
if (index.size === 0)
|
|
47563
48065
|
return error51();
|
|
@@ -47588,8 +48090,8 @@ function createShutdown(deps) {
|
|
|
47588
48090
|
for (const stop of deps.stopLoops)
|
|
47589
48091
|
stop();
|
|
47590
48092
|
let timer;
|
|
47591
|
-
const abandoned = new Promise((
|
|
47592
|
-
timer = setTimeout(() =>
|
|
48093
|
+
const abandoned = new Promise((resolve4) => {
|
|
48094
|
+
timer = setTimeout(() => resolve4("timeout"), deps.stopDeadlineMs ?? STOP_DEADLINE_MS);
|
|
47593
48095
|
timer.unref?.();
|
|
47594
48096
|
});
|
|
47595
48097
|
Promise.race([deps.stopServer(), abandoned]).then((outcome) => {
|
|
@@ -47717,6 +48219,334 @@ function startRefreshScheduler(deps) {
|
|
|
47717
48219
|
return () => clearInterval(timer);
|
|
47718
48220
|
}
|
|
47719
48221
|
|
|
48222
|
+
// apps/gateway/src/plugins/events.ts
|
|
48223
|
+
var DEFAULT_CAPACITY = 1000;
|
|
48224
|
+
var defaultScheduler = (run) => {
|
|
48225
|
+
const timer = setTimeout(run, 0);
|
|
48226
|
+
return () => clearTimeout(timer);
|
|
48227
|
+
};
|
|
48228
|
+
function createPluginEventBus(opts) {
|
|
48229
|
+
const logger2 = opts.logger ?? noopLogger;
|
|
48230
|
+
const capacity = opts.capacity ?? DEFAULT_CAPACITY;
|
|
48231
|
+
const scheduler = opts.scheduler ?? defaultScheduler;
|
|
48232
|
+
const requestHandlers = [];
|
|
48233
|
+
const limitHandlers = [];
|
|
48234
|
+
const queue = [];
|
|
48235
|
+
let dropped = 0;
|
|
48236
|
+
let handlerErrors = 0;
|
|
48237
|
+
let draining = false;
|
|
48238
|
+
let live = true;
|
|
48239
|
+
let cancelDrain;
|
|
48240
|
+
let droppedSinceReport = 0;
|
|
48241
|
+
const errorsSinceReport = new Map;
|
|
48242
|
+
const drain = () => {
|
|
48243
|
+
draining = false;
|
|
48244
|
+
cancelDrain = undefined;
|
|
48245
|
+
if (!live)
|
|
48246
|
+
return;
|
|
48247
|
+
const batch = queue.splice(0, queue.length);
|
|
48248
|
+
for (const item of batch) {
|
|
48249
|
+
const handlers = item.name === "request:completed" ? requestHandlers : limitHandlers;
|
|
48250
|
+
for (const handler of handlers) {
|
|
48251
|
+
try {
|
|
48252
|
+
handler.run(item.event);
|
|
48253
|
+
} catch {
|
|
48254
|
+
handlerErrors++;
|
|
48255
|
+
errorsSinceReport.set(handler.pluginId, (errorsSinceReport.get(handler.pluginId) ?? 0) + 1);
|
|
48256
|
+
}
|
|
48257
|
+
}
|
|
48258
|
+
}
|
|
48259
|
+
for (const [pluginId, count] of errorsSinceReport) {
|
|
48260
|
+
logger2.warn("plugin event handler failed", { plugin: pluginId, count });
|
|
48261
|
+
}
|
|
48262
|
+
errorsSinceReport.clear();
|
|
48263
|
+
if (droppedSinceReport > 0) {
|
|
48264
|
+
logger2.warn("plugin event queue overflowed", { count: droppedSinceReport });
|
|
48265
|
+
droppedSinceReport = 0;
|
|
48266
|
+
}
|
|
48267
|
+
};
|
|
48268
|
+
const schedule = () => {
|
|
48269
|
+
if (draining || !live)
|
|
48270
|
+
return;
|
|
48271
|
+
draining = true;
|
|
48272
|
+
cancelDrain = scheduler(drain);
|
|
48273
|
+
};
|
|
48274
|
+
const enqueue = (item, subscribers) => {
|
|
48275
|
+
if (!live || subscribers === 0)
|
|
48276
|
+
return;
|
|
48277
|
+
if (queue.length >= capacity) {
|
|
48278
|
+
dropped++;
|
|
48279
|
+
droppedSinceReport++;
|
|
48280
|
+
return;
|
|
48281
|
+
}
|
|
48282
|
+
queue.push(item);
|
|
48283
|
+
schedule();
|
|
48284
|
+
};
|
|
48285
|
+
return {
|
|
48286
|
+
onRequestCompleted(pluginId, run) {
|
|
48287
|
+
requestHandlers.push({ pluginId, run });
|
|
48288
|
+
},
|
|
48289
|
+
onLimitReached(pluginId, run) {
|
|
48290
|
+
limitHandlers.push({ pluginId, run });
|
|
48291
|
+
},
|
|
48292
|
+
emitRequestCompleted(event) {
|
|
48293
|
+
enqueue({ name: "request:completed", event }, requestHandlers.length);
|
|
48294
|
+
},
|
|
48295
|
+
emitLimitReached(event) {
|
|
48296
|
+
enqueue({ name: "limit:reached", event }, limitHandlers.length);
|
|
48297
|
+
},
|
|
48298
|
+
stats() {
|
|
48299
|
+
return { dropped, handlerErrors, queued: queue.length };
|
|
48300
|
+
},
|
|
48301
|
+
stop() {
|
|
48302
|
+
live = false;
|
|
48303
|
+
queue.length = 0;
|
|
48304
|
+
cancelDrain?.();
|
|
48305
|
+
cancelDrain = undefined;
|
|
48306
|
+
draining = false;
|
|
48307
|
+
}
|
|
48308
|
+
};
|
|
48309
|
+
}
|
|
48310
|
+
|
|
48311
|
+
// apps/gateway/src/plugins/loader.ts
|
|
48312
|
+
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
48313
|
+
import { join as join5, resolve as resolve5, sep as sep5 } from "path";
|
|
48314
|
+
|
|
48315
|
+
// apps/gateway/src/plugins/capabilities.ts
|
|
48316
|
+
import { mkdir as mkdir2, readFile as readFile2, realpath, writeFile as writeFile2 } from "fs/promises";
|
|
48317
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
|
|
48318
|
+
async function resolveInside(root, path) {
|
|
48319
|
+
const rootReal = await realpath(root);
|
|
48320
|
+
const target = resolve4(rootReal, path);
|
|
48321
|
+
const inside = (candidate) => candidate === rootReal || candidate.startsWith(`${rootReal}${sep4}`);
|
|
48322
|
+
if (!inside(target))
|
|
48323
|
+
throw new Error(`path is outside the plugin directory: ${path}`);
|
|
48324
|
+
let probe2 = target;
|
|
48325
|
+
for (;; ) {
|
|
48326
|
+
try {
|
|
48327
|
+
if (!inside(await realpath(probe2))) {
|
|
48328
|
+
throw new Error(`path is outside the plugin directory: ${path}`);
|
|
48329
|
+
}
|
|
48330
|
+
return target;
|
|
48331
|
+
} catch (error51) {
|
|
48332
|
+
if (error51 instanceof Error && error51.message.includes("outside"))
|
|
48333
|
+
throw error51;
|
|
48334
|
+
const parent = dirname4(probe2);
|
|
48335
|
+
if (parent === probe2 || probe2 === rootReal)
|
|
48336
|
+
return target;
|
|
48337
|
+
probe2 = parent;
|
|
48338
|
+
}
|
|
48339
|
+
}
|
|
48340
|
+
}
|
|
48341
|
+
function createPluginFiles(root) {
|
|
48342
|
+
return {
|
|
48343
|
+
async read(path) {
|
|
48344
|
+
const target = await ensureRootThen(root, () => resolveInside(root, path));
|
|
48345
|
+
try {
|
|
48346
|
+
const data = await readFile2(target);
|
|
48347
|
+
return new Uint8Array(data);
|
|
48348
|
+
} catch {
|
|
48349
|
+
return null;
|
|
48350
|
+
}
|
|
48351
|
+
},
|
|
48352
|
+
async write(path, data) {
|
|
48353
|
+
const target = await ensureRootThen(root, () => resolveInside(root, path));
|
|
48354
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
48355
|
+
await writeFile2(target, data);
|
|
48356
|
+
},
|
|
48357
|
+
async exists(path) {
|
|
48358
|
+
const target = await ensureRootThen(root, () => resolveInside(root, path));
|
|
48359
|
+
try {
|
|
48360
|
+
await readFile2(target);
|
|
48361
|
+
return true;
|
|
48362
|
+
} catch {
|
|
48363
|
+
return false;
|
|
48364
|
+
}
|
|
48365
|
+
}
|
|
48366
|
+
};
|
|
48367
|
+
}
|
|
48368
|
+
async function ensureRootThen(root, then) {
|
|
48369
|
+
await mkdir2(root, { recursive: true });
|
|
48370
|
+
return then();
|
|
48371
|
+
}
|
|
48372
|
+
var PLUGIN_FETCH_TIMEOUT_MS = 30000;
|
|
48373
|
+
function createPluginFetch(origins, deps = {}) {
|
|
48374
|
+
const fetchImpl = deps.fetchImpl ?? ((url2, init) => fetch(url2, init));
|
|
48375
|
+
const allowed = new Set(origins.map((origin) => new URL(origin).origin));
|
|
48376
|
+
const timeoutMs = deps.timeoutMs ?? PLUGIN_FETCH_TIMEOUT_MS;
|
|
48377
|
+
return async (url2, init) => {
|
|
48378
|
+
const parsed = new URL(url2);
|
|
48379
|
+
if (!allowed.has(parsed.origin)) {
|
|
48380
|
+
throw new Error(`origin ${parsed.origin} is not in the allowlist`);
|
|
48381
|
+
}
|
|
48382
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
48383
|
+
const signal = init?.signal === undefined || init.signal === null ? timeout : AbortSignal.any([init.signal, timeout]);
|
|
48384
|
+
return fetchImpl(url2, { ...init, redirect: "manual", signal });
|
|
48385
|
+
};
|
|
48386
|
+
}
|
|
48387
|
+
|
|
48388
|
+
// apps/gateway/src/plugins/loader.ts
|
|
48389
|
+
var EVENT_MAX = 64;
|
|
48390
|
+
function sanitizeEvent(value) {
|
|
48391
|
+
return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, EVENT_MAX);
|
|
48392
|
+
}
|
|
48393
|
+
function createPluginLogger(id, logger2) {
|
|
48394
|
+
const fields = (f) => ({
|
|
48395
|
+
plugin: id,
|
|
48396
|
+
...f?.event === undefined ? {} : { reason: sanitizeEvent(f.event) },
|
|
48397
|
+
...f?.count === undefined ? {} : { count: f.count },
|
|
48398
|
+
...f?.durationMs === undefined ? {} : { durationMs: f.durationMs }
|
|
48399
|
+
});
|
|
48400
|
+
return {
|
|
48401
|
+
debug: (message3, f) => logger2.debug(message3, fields(f)),
|
|
48402
|
+
info: (message3, f) => logger2.info(message3, fields(f)),
|
|
48403
|
+
warn: (message3, f) => logger2.warn(message3, fields(f)),
|
|
48404
|
+
error: (message3, f) => logger2.error(message3, fields(f))
|
|
48405
|
+
};
|
|
48406
|
+
}
|
|
48407
|
+
function createPluginStorage(store, id) {
|
|
48408
|
+
return {
|
|
48409
|
+
run: (sql, params) => store.plugins.run(id, sql, params === undefined ? [] : [...params]),
|
|
48410
|
+
all: (sql, params) => store.plugins.all(id, sql, params === undefined ? [] : [...params]),
|
|
48411
|
+
get: (sql, params) => store.plugins.get(id, sql, params === undefined ? [] : [...params]),
|
|
48412
|
+
transaction: (fn) => store.plugins.transaction(id, fn)
|
|
48413
|
+
};
|
|
48414
|
+
}
|
|
48415
|
+
function createPluginEvents(manifest, bus) {
|
|
48416
|
+
const wantsRequest = manifest.capabilities.includes("events:request");
|
|
48417
|
+
const wantsLimit = manifest.capabilities.includes("events:limit");
|
|
48418
|
+
if (!wantsRequest && !wantsLimit)
|
|
48419
|
+
return;
|
|
48420
|
+
return {
|
|
48421
|
+
...wantsRequest ? { onRequestCompleted: (handler) => bus.onRequestCompleted(manifest.id, handler) } : {},
|
|
48422
|
+
...wantsLimit ? { onLimitReached: (handler) => bus.onLimitReached(manifest.id, handler) } : {}
|
|
48423
|
+
};
|
|
48424
|
+
}
|
|
48425
|
+
function buildContext(deps) {
|
|
48426
|
+
const { manifest, home } = deps;
|
|
48427
|
+
const declared = manifest.capabilities;
|
|
48428
|
+
return {
|
|
48429
|
+
id: manifest.id,
|
|
48430
|
+
now: deps.now,
|
|
48431
|
+
logger: createPluginLogger(manifest.id, deps.logger),
|
|
48432
|
+
...declared.includes("storage") ? { storage: createPluginStorage(deps.store, manifest.id) } : {},
|
|
48433
|
+
...declared.includes("files") ? { files: createPluginFiles(join5(home, "data")) } : {},
|
|
48434
|
+
...declared.includes("net:outbound") ? { net: createPluginFetch(manifest.origins ?? []) } : {},
|
|
48435
|
+
...(() => {
|
|
48436
|
+
const events = createPluginEvents(manifest, deps.events);
|
|
48437
|
+
return events === undefined ? {} : { events };
|
|
48438
|
+
})(),
|
|
48439
|
+
config: manifest.defaults ?? {}
|
|
48440
|
+
};
|
|
48441
|
+
}
|
|
48442
|
+
function reason(error51) {
|
|
48443
|
+
return error51 instanceof Error ? error51.message : String(error51);
|
|
48444
|
+
}
|
|
48445
|
+
async function loadPlugins(deps) {
|
|
48446
|
+
const logger2 = deps.logger ?? noopLogger;
|
|
48447
|
+
const now = deps.now ?? Date.now;
|
|
48448
|
+
const plugins = [];
|
|
48449
|
+
const failures = [];
|
|
48450
|
+
let entries;
|
|
48451
|
+
try {
|
|
48452
|
+
const dirents = await readdir2(deps.root, { withFileTypes: true });
|
|
48453
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
48454
|
+
} catch {
|
|
48455
|
+
return { plugins, failures };
|
|
48456
|
+
}
|
|
48457
|
+
for (const id of entries.sort()) {
|
|
48458
|
+
const home = join5(deps.root, id);
|
|
48459
|
+
const fail2 = (why) => {
|
|
48460
|
+
failures.push({ id, reason: why });
|
|
48461
|
+
logger2.warn("plugin skipped", { plugin: id, reason: why });
|
|
48462
|
+
};
|
|
48463
|
+
let manifest;
|
|
48464
|
+
try {
|
|
48465
|
+
const raw = await readFile3(join5(home, "omni-plugin.json"), "utf8");
|
|
48466
|
+
const parsed = safeParseManifest(JSON.parse(raw));
|
|
48467
|
+
if (!parsed.ok) {
|
|
48468
|
+
fail2(parsed.reason);
|
|
48469
|
+
continue;
|
|
48470
|
+
}
|
|
48471
|
+
manifest = parsed.manifest;
|
|
48472
|
+
} catch (error51) {
|
|
48473
|
+
fail2(reason(error51));
|
|
48474
|
+
continue;
|
|
48475
|
+
}
|
|
48476
|
+
if (manifest.id !== id) {
|
|
48477
|
+
fail2(`manifest id ${manifest.id} does not match its directory ${id}`);
|
|
48478
|
+
continue;
|
|
48479
|
+
}
|
|
48480
|
+
if (!isApiCompatible(manifest)) {
|
|
48481
|
+
fail2(`plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`);
|
|
48482
|
+
continue;
|
|
48483
|
+
}
|
|
48484
|
+
let routes = [];
|
|
48485
|
+
let migrations = [];
|
|
48486
|
+
if (manifest.server !== undefined) {
|
|
48487
|
+
const entry = resolve5(home, manifest.server);
|
|
48488
|
+
if (entry !== home && !entry.startsWith(`${home}${sep5}`)) {
|
|
48489
|
+
fail2(`server entry resolves outside the plugin directory`);
|
|
48490
|
+
continue;
|
|
48491
|
+
}
|
|
48492
|
+
let definition;
|
|
48493
|
+
try {
|
|
48494
|
+
const module = await import(entry);
|
|
48495
|
+
const candidate = module.default;
|
|
48496
|
+
if (typeof candidate !== "object" || candidate === null || typeof candidate.setup !== "function") {
|
|
48497
|
+
fail2("server entry has no default export with a setup function");
|
|
48498
|
+
continue;
|
|
48499
|
+
}
|
|
48500
|
+
definition = candidate;
|
|
48501
|
+
} catch (error51) {
|
|
48502
|
+
fail2(reason(error51));
|
|
48503
|
+
continue;
|
|
48504
|
+
}
|
|
48505
|
+
migrations = definition.migrations ?? [];
|
|
48506
|
+
if (migrations.length > 0) {
|
|
48507
|
+
const applied = deps.store.plugins.migrate(id, migrations);
|
|
48508
|
+
if (applied.failed !== undefined) {
|
|
48509
|
+
fail2(`migration ${applied.failed.version} failed: ${applied.failed.reason}`);
|
|
48510
|
+
continue;
|
|
48511
|
+
}
|
|
48512
|
+
}
|
|
48513
|
+
try {
|
|
48514
|
+
const context = buildContext({
|
|
48515
|
+
manifest,
|
|
48516
|
+
home,
|
|
48517
|
+
store: deps.store,
|
|
48518
|
+
events: deps.events,
|
|
48519
|
+
logger: logger2,
|
|
48520
|
+
now
|
|
48521
|
+
});
|
|
48522
|
+
const result = await definition.setup(context);
|
|
48523
|
+
routes = result?.routes ?? [];
|
|
48524
|
+
} catch (error51) {
|
|
48525
|
+
fail2(reason(error51));
|
|
48526
|
+
continue;
|
|
48527
|
+
}
|
|
48528
|
+
}
|
|
48529
|
+
const loaded = { id, manifest, routes, migrations };
|
|
48530
|
+
if (manifest.ui !== undefined && manifest.sdk !== undefined) {
|
|
48531
|
+
const UI_DIR = "ui/";
|
|
48532
|
+
if (!manifest.ui.startsWith(UI_DIR)) {
|
|
48533
|
+
fail2(`ui entry must live under ${UI_DIR}, got ${manifest.ui}`);
|
|
48534
|
+
continue;
|
|
48535
|
+
}
|
|
48536
|
+
const compatible = Bun.semver.satisfies(deps.sdkVersion, manifest.sdk);
|
|
48537
|
+
loaded.ui = {
|
|
48538
|
+
dir: home,
|
|
48539
|
+
entry: manifest.ui.slice(UI_DIR.length),
|
|
48540
|
+
compatible,
|
|
48541
|
+
...compatible ? {} : { reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}` }
|
|
48542
|
+
};
|
|
48543
|
+
}
|
|
48544
|
+
plugins.push(loaded);
|
|
48545
|
+
logger2.info("plugin loaded", { plugin: id });
|
|
48546
|
+
}
|
|
48547
|
+
return { plugins, failures };
|
|
48548
|
+
}
|
|
48549
|
+
|
|
47720
48550
|
// apps/gateway/src/quota/poller.ts
|
|
47721
48551
|
async function startQuotaPoller(deps) {
|
|
47722
48552
|
const logger2 = deps.logger ?? noopLogger;
|
|
@@ -47790,8 +48620,8 @@ async function main() {
|
|
|
47790
48620
|
if (config2.staticDir !== null)
|
|
47791
48621
|
return config2.staticDir;
|
|
47792
48622
|
const candidates = [
|
|
47793
|
-
|
|
47794
|
-
|
|
48623
|
+
resolve6(import.meta.dir, "./public"),
|
|
48624
|
+
resolve6(import.meta.dir, "../../dashboard/dist")
|
|
47795
48625
|
];
|
|
47796
48626
|
return candidates.find((path) => existsSync2(path)) ?? candidates[1];
|
|
47797
48627
|
}
|
|
@@ -47813,10 +48643,25 @@ async function main() {
|
|
|
47813
48643
|
logger2.info(existsSync2(staticDir) ? "dashboard directory resolved" : "dashboard directory absent", {
|
|
47814
48644
|
path: staticDir
|
|
47815
48645
|
});
|
|
47816
|
-
let shutdown = (
|
|
47817
|
-
logger2.error("shutdown requested before the gateway was serving", { reason });
|
|
48646
|
+
let shutdown = (reason2) => {
|
|
48647
|
+
logger2.error("shutdown requested before the gateway was serving", { reason: reason2 });
|
|
47818
48648
|
process.exit(1);
|
|
47819
48649
|
};
|
|
48650
|
+
const installRoot = process.env.OMNI_ROOT ?? dirname5(config2.databasePath);
|
|
48651
|
+
const pluginRoot = join6(installRoot, "plugins");
|
|
48652
|
+
const pluginEvents = createPluginEventBus({ logger: logger2 });
|
|
48653
|
+
const loadedPlugins = await loadPlugins({
|
|
48654
|
+
root: pluginRoot,
|
|
48655
|
+
store,
|
|
48656
|
+
events: pluginEvents,
|
|
48657
|
+
sdkVersion: DASHBOARD_SDK_VERSION,
|
|
48658
|
+
logger: logger2,
|
|
48659
|
+
now
|
|
48660
|
+
});
|
|
48661
|
+
for (const failure of loadedPlugins.failures) {
|
|
48662
|
+
logger2.warn("plugin unavailable", { plugin: failure.id, reason: failure.reason });
|
|
48663
|
+
}
|
|
48664
|
+
logger2.info("plugins resolved", { count: loadedPlugins.plugins.length, path: pluginRoot });
|
|
47820
48665
|
const app = createApp({
|
|
47821
48666
|
store,
|
|
47822
48667
|
baseUrl: config2.baseUrl,
|
|
@@ -47828,11 +48673,24 @@ async function main() {
|
|
|
47828
48673
|
console: console2,
|
|
47829
48674
|
discoveryMirrors: config2.exposeClaudeCodeAliases,
|
|
47830
48675
|
bodyLoggingAllowed: config2.bodyLoggingAllowed,
|
|
48676
|
+
plugins: loadedPlugins.plugins.map((plugin) => ({ id: plugin.id, routes: plugin.routes })),
|
|
48677
|
+
pluginUi: loadedPlugins.plugins,
|
|
48678
|
+
...loadedPlugins.plugins.length === 0 ? {} : { emit: (event) => pluginEvents.emitRequestCompleted(event) },
|
|
48679
|
+
reapplyPluginSchema: async () => {
|
|
48680
|
+
for (const plugin of loadedPlugins.plugins) {
|
|
48681
|
+
if (plugin.migrations.length === 0)
|
|
48682
|
+
continue;
|
|
48683
|
+
const applied = store.plugins.migrate(plugin.id, plugin.migrations);
|
|
48684
|
+
if (applied.failed !== undefined) {
|
|
48685
|
+
throw new Error(`plugin ${plugin.id} migration ${applied.failed.version}: ${applied.failed.reason}`);
|
|
48686
|
+
}
|
|
48687
|
+
}
|
|
48688
|
+
},
|
|
47831
48689
|
lifecycle: {
|
|
47832
48690
|
env: process.env,
|
|
47833
48691
|
fileExists: (path) => existsSync2(path),
|
|
47834
48692
|
run: commandRunner,
|
|
47835
|
-
stop: createDeferredStop((
|
|
48693
|
+
stop: createDeferredStop((reason2, mode) => shutdown(reason2, mode))
|
|
47836
48694
|
}
|
|
47837
48695
|
});
|
|
47838
48696
|
const stopMaintenance = startMaintenance({ store, now, logger: logger2 });
|
|
@@ -47849,7 +48707,7 @@ async function main() {
|
|
|
47849
48707
|
logger2.info("omnigateway listening", { host: config2.host, port: config2.port });
|
|
47850
48708
|
shutdown = createShutdown({
|
|
47851
48709
|
logger: logger2,
|
|
47852
|
-
stopLoops: [stopMaintenance, stopRefreshScheduler, stopQuotaPoller],
|
|
48710
|
+
stopLoops: [stopMaintenance, stopRefreshScheduler, stopQuotaPoller, () => pluginEvents.stop()],
|
|
47853
48711
|
stopServer: async () => {
|
|
47854
48712
|
await app.stop();
|
|
47855
48713
|
},
|