omnigateway 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -1
- package/bin/omni.js +1155 -40
- package/gateway.js +985 -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,206 @@ 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`, "i");
|
|
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 === '"' || c === "`" || c === "[") {
|
|
25477
|
+
const close = c === "[" ? "]" : c;
|
|
25478
|
+
out += c;
|
|
25479
|
+
i += 1;
|
|
25480
|
+
while (i < sql.length) {
|
|
25481
|
+
if (sql[i] === close) {
|
|
25482
|
+
if (close !== "]" && sql[i + 1] === close) {
|
|
25483
|
+
out += close + close;
|
|
25484
|
+
i += 2;
|
|
25485
|
+
continue;
|
|
25486
|
+
}
|
|
25487
|
+
break;
|
|
25488
|
+
}
|
|
25489
|
+
out += sql[i];
|
|
25490
|
+
i += 1;
|
|
25491
|
+
}
|
|
25492
|
+
out += close;
|
|
25493
|
+
i += 1;
|
|
25494
|
+
} else if (c === "'") {
|
|
25495
|
+
i += 1;
|
|
25496
|
+
while (i < sql.length) {
|
|
25497
|
+
if (sql[i] === "'") {
|
|
25498
|
+
if (sql[i + 1] === "'")
|
|
25499
|
+
i += 2;
|
|
25500
|
+
else
|
|
25501
|
+
break;
|
|
25502
|
+
} else
|
|
25503
|
+
i += 1;
|
|
25504
|
+
}
|
|
25505
|
+
i += 1;
|
|
25506
|
+
out += " ";
|
|
25507
|
+
} else if (c === "-" && next === "-") {
|
|
25508
|
+
while (i < sql.length && sql[i] !== `
|
|
25509
|
+
`)
|
|
25510
|
+
i += 1;
|
|
25511
|
+
out += " ";
|
|
25512
|
+
} else if (c === "/" && next === "*") {
|
|
25513
|
+
i += 2;
|
|
25514
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
|
|
25515
|
+
i += 1;
|
|
25516
|
+
i += 2;
|
|
25517
|
+
out += " ";
|
|
25518
|
+
} else {
|
|
25519
|
+
out += c;
|
|
25520
|
+
i += 1;
|
|
25521
|
+
}
|
|
25522
|
+
}
|
|
25523
|
+
return out;
|
|
25524
|
+
}
|
|
25525
|
+
function tableFor(pluginId, name) {
|
|
25526
|
+
return `"${PREFIX3}${pluginId}_${name}"`;
|
|
25527
|
+
}
|
|
25528
|
+
function prefixFor(pluginId) {
|
|
25529
|
+
return `${PREFIX3}${pluginId}_`;
|
|
25530
|
+
}
|
|
25531
|
+
function assertPluginId(pluginId) {
|
|
25532
|
+
if (!PLUGIN_ID.test(pluginId)) {
|
|
25533
|
+
throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
|
|
25534
|
+
}
|
|
25535
|
+
}
|
|
25536
|
+
var CONNECTION_STATEMENT = /^\s*(pragma|attach|detach|vacuum)\b/i;
|
|
25537
|
+
function prepare(pluginId, sql) {
|
|
25538
|
+
assertPluginId(pluginId);
|
|
25539
|
+
const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
|
|
25540
|
+
if (!TABLE_NAME.test(name)) {
|
|
25541
|
+
throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
|
|
25542
|
+
}
|
|
25543
|
+
return tableFor(pluginId, name);
|
|
25544
|
+
});
|
|
25545
|
+
const stripped = stripNoise(expanded);
|
|
25546
|
+
const connection = CONNECTION_STATEMENT.exec(stripped);
|
|
25547
|
+
if (connection !== null) {
|
|
25548
|
+
throw new Error(`plugin sql may not ${connection[1]?.toUpperCase()}: the database handle is the gateway's, ` + "and this would reconfigure it for everything sharing it");
|
|
25549
|
+
}
|
|
25550
|
+
const found = CORE_TABLE_REFERENCE.exec(stripped);
|
|
25551
|
+
if (found !== null) {
|
|
25552
|
+
throw new Error(`plugin sql may not reference the core table ${found[0]}`);
|
|
25553
|
+
}
|
|
25554
|
+
return expanded;
|
|
25555
|
+
}
|
|
25556
|
+
function schemaObjects(db) {
|
|
25557
|
+
const rows = db.query("SELECT name FROM sqlite_master").all();
|
|
25558
|
+
return new Set(rows.map((row) => row.name));
|
|
25559
|
+
}
|
|
25560
|
+
function assertOwnNamespace(db, pluginId, before) {
|
|
25561
|
+
const prefix = `${PREFIX3}${pluginId}_`;
|
|
25562
|
+
for (const name of schemaObjects(db)) {
|
|
25563
|
+
if (before.has(name) || name.startsWith(prefix) || name.startsWith("sqlite_"))
|
|
25564
|
+
continue;
|
|
25565
|
+
throw new Error(`plugin migration created ${name}, which is outside this plugin's namespace \u2014 ` + `name it {{\u2026}} so it becomes ${prefix}\u2026`);
|
|
25566
|
+
}
|
|
25567
|
+
}
|
|
25568
|
+
function toBinding(value, index) {
|
|
25569
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
|
|
25570
|
+
return value;
|
|
25571
|
+
}
|
|
25572
|
+
throw new Error(`plugin sql parameter ${index} is not a bindable value`);
|
|
25573
|
+
}
|
|
25574
|
+
var toBindings = (params) => (params ?? []).map(toBinding);
|
|
25575
|
+
function createPluginRepo(db) {
|
|
25576
|
+
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");
|
|
25577
|
+
const tablesOf = (pluginId) => {
|
|
25578
|
+
const prefix = prefixFor(pluginId);
|
|
25579
|
+
return pluginTables().filter((name) => name.startsWith(prefix));
|
|
25580
|
+
};
|
|
25581
|
+
const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
|
|
25582
|
+
return {
|
|
25583
|
+
migrate(pluginId, migrations) {
|
|
25584
|
+
assertPluginId(pluginId);
|
|
25585
|
+
const done = appliedVersions(pluginId);
|
|
25586
|
+
const applied = [];
|
|
25587
|
+
const ordered = [...migrations].sort((a, b) => a.version - b.version);
|
|
25588
|
+
for (const migration of ordered) {
|
|
25589
|
+
if (done.has(migration.version))
|
|
25590
|
+
continue;
|
|
25591
|
+
try {
|
|
25592
|
+
if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
|
|
25593
|
+
throw new Error("migration version must be a positive integer");
|
|
25594
|
+
}
|
|
25595
|
+
const sql = prepare(pluginId, migration.sql);
|
|
25596
|
+
const before = schemaObjects(db);
|
|
25597
|
+
db.transaction(() => {
|
|
25598
|
+
db.run(sql);
|
|
25599
|
+
assertOwnNamespace(db, pluginId, before);
|
|
25600
|
+
db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
|
|
25601
|
+
})();
|
|
25602
|
+
applied.push(migration.version);
|
|
25603
|
+
} catch (error51) {
|
|
25604
|
+
return {
|
|
25605
|
+
applied,
|
|
25606
|
+
failed: {
|
|
25607
|
+
version: migration.version,
|
|
25608
|
+
reason: error51 instanceof Error ? error51.message : String(error51)
|
|
25609
|
+
}
|
|
25610
|
+
};
|
|
25611
|
+
}
|
|
25612
|
+
}
|
|
25613
|
+
return { applied };
|
|
25614
|
+
},
|
|
25615
|
+
run(pluginId, sql, params) {
|
|
25616
|
+
db.run(prepare(pluginId, sql), toBindings(params));
|
|
25617
|
+
},
|
|
25618
|
+
all(pluginId, sql, params) {
|
|
25619
|
+
return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
|
|
25620
|
+
},
|
|
25621
|
+
get(pluginId, sql, params) {
|
|
25622
|
+
const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
|
|
25623
|
+
return row === null || row === undefined ? null : row;
|
|
25624
|
+
},
|
|
25625
|
+
transaction(pluginId, fn) {
|
|
25626
|
+
assertPluginId(pluginId);
|
|
25627
|
+
return db.transaction(fn)();
|
|
25628
|
+
},
|
|
25629
|
+
listTables(pluginId) {
|
|
25630
|
+
assertPluginId(pluginId);
|
|
25631
|
+
return tablesOf(pluginId);
|
|
25632
|
+
},
|
|
25633
|
+
dropAll(pluginId) {
|
|
25634
|
+
assertPluginId(pluginId);
|
|
25635
|
+
const tables = tablesOf(pluginId);
|
|
25636
|
+
db.transaction(() => {
|
|
25637
|
+
for (const table of tables)
|
|
25638
|
+
db.run(`DROP TABLE IF EXISTS "${table}"`);
|
|
25639
|
+
db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
|
|
25640
|
+
})();
|
|
25641
|
+
return tables.length;
|
|
25642
|
+
},
|
|
25643
|
+
orphanTables(installedIds) {
|
|
25644
|
+
const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
|
|
25645
|
+
return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
|
|
25646
|
+
}
|
|
25647
|
+
};
|
|
25648
|
+
}
|
|
25414
25649
|
// packages/rtk/src/catalog.ts
|
|
25415
25650
|
var RTK_FILTER_IDS = [
|
|
25416
25651
|
"git-diff",
|
|
@@ -25723,7 +25958,8 @@ async function createStore(opts) {
|
|
|
25723
25958
|
keys: createKeyRepo(db, logger2),
|
|
25724
25959
|
usage: createUsageRepo(db),
|
|
25725
25960
|
bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
25726
|
-
maintenance: createMaintenanceRepo(db)
|
|
25961
|
+
maintenance: createMaintenanceRepo(db),
|
|
25962
|
+
plugins: createPluginRepo(db)
|
|
25727
25963
|
};
|
|
25728
25964
|
};
|
|
25729
25965
|
let handle = open();
|
|
@@ -25796,6 +26032,22 @@ async function createStore(opts) {
|
|
|
25796
26032
|
snapshotTo: (path) => handle.maintenance.snapshotTo(path),
|
|
25797
26033
|
inspect: (path) => handle.maintenance.inspect(path)
|
|
25798
26034
|
},
|
|
26035
|
+
plugins: {
|
|
26036
|
+
migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
|
|
26037
|
+
run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
|
|
26038
|
+
all(pluginId, sql, params) {
|
|
26039
|
+
return handle.plugins.all(pluginId, sql, params);
|
|
26040
|
+
},
|
|
26041
|
+
get(pluginId, sql, params) {
|
|
26042
|
+
return handle.plugins.get(pluginId, sql, params);
|
|
26043
|
+
},
|
|
26044
|
+
transaction(pluginId, fn) {
|
|
26045
|
+
return handle.plugins.transaction(pluginId, fn);
|
|
26046
|
+
},
|
|
26047
|
+
listTables: (pluginId) => handle.plugins.listTables(pluginId),
|
|
26048
|
+
dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
|
|
26049
|
+
orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
|
|
26050
|
+
},
|
|
25799
26051
|
routing: {
|
|
25800
26052
|
version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
25801
26053
|
subscribe(listener) {
|
|
@@ -26211,6 +26463,13 @@ async function swapIn(deps, candidate) {
|
|
|
26211
26463
|
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
26212
26464
|
});
|
|
26213
26465
|
}
|
|
26466
|
+
try {
|
|
26467
|
+
await deps.reapplyPluginSchema?.();
|
|
26468
|
+
} catch (error51) {
|
|
26469
|
+
(deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
|
|
26470
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
26471
|
+
});
|
|
26472
|
+
}
|
|
26214
26473
|
return {
|
|
26215
26474
|
ok: true,
|
|
26216
26475
|
counts: inspection.counts,
|
|
@@ -26584,13 +26843,89 @@ import {
|
|
|
26584
26843
|
copyFileSync,
|
|
26585
26844
|
mkdirSync,
|
|
26586
26845
|
readdirSync,
|
|
26846
|
+
readFileSync,
|
|
26587
26847
|
realpathSync,
|
|
26588
26848
|
renameSync,
|
|
26589
26849
|
rmSync,
|
|
26590
26850
|
statfsSync,
|
|
26591
|
-
statSync
|
|
26851
|
+
statSync,
|
|
26852
|
+
writeFileSync
|
|
26592
26853
|
} from "fs";
|
|
26593
26854
|
import { join as join3 } from "path";
|
|
26855
|
+
|
|
26856
|
+
// packages/plugin-api/src/version.ts
|
|
26857
|
+
var PLUGIN_API_VERSION = 1;
|
|
26858
|
+
var DASHBOARD_SDK_VERSION = "0.1.0";
|
|
26859
|
+
// packages/plugin-api/src/manifest.ts
|
|
26860
|
+
var CAPABILITIES = [
|
|
26861
|
+
"storage",
|
|
26862
|
+
"files",
|
|
26863
|
+
"net:outbound",
|
|
26864
|
+
"events:request",
|
|
26865
|
+
"events:limit"
|
|
26866
|
+
];
|
|
26867
|
+
var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
26868
|
+
var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
|
|
26869
|
+
var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
|
|
26870
|
+
message: "entry must be a relative path with no '..' segment"
|
|
26871
|
+
});
|
|
26872
|
+
var originSchema = exports_external.string().refine((value) => {
|
|
26873
|
+
let url2;
|
|
26874
|
+
try {
|
|
26875
|
+
url2 = new URL(value);
|
|
26876
|
+
} catch {
|
|
26877
|
+
return false;
|
|
26878
|
+
}
|
|
26879
|
+
if (url2.protocol !== "https:" && url2.protocol !== "http:")
|
|
26880
|
+
return false;
|
|
26881
|
+
if (url2.hostname === "")
|
|
26882
|
+
return false;
|
|
26883
|
+
if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
|
|
26884
|
+
return false;
|
|
26885
|
+
return value === url2.origin || value === `${url2.origin}/`;
|
|
26886
|
+
}, { message: "origin must be scheme://host with no path, query or fragment" });
|
|
26887
|
+
var navSchema = exports_external.object({
|
|
26888
|
+
label: exports_external.string().min(1).max(32)
|
|
26889
|
+
}).strict();
|
|
26890
|
+
var manifestSchema = exports_external.object({
|
|
26891
|
+
id: idSchema,
|
|
26892
|
+
name: exports_external.string().min(1).max(64),
|
|
26893
|
+
version: exports_external.string().min(1).max(32),
|
|
26894
|
+
api: exports_external.number().int().nonnegative(),
|
|
26895
|
+
sdk: exports_external.string().min(1).max(32).optional(),
|
|
26896
|
+
server: entrySchema.optional(),
|
|
26897
|
+
ui: entrySchema.optional(),
|
|
26898
|
+
nav: navSchema.optional(),
|
|
26899
|
+
capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
|
|
26900
|
+
origins: exports_external.array(originSchema).min(1).optional(),
|
|
26901
|
+
defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
26902
|
+
}).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
|
|
26903
|
+
message: "manifest must declare a server entry, a ui entry, or both"
|
|
26904
|
+
}).refine((m) => m.ui === undefined || m.sdk !== undefined, {
|
|
26905
|
+
message: "a ui entry requires an sdk range"
|
|
26906
|
+
}).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
|
|
26907
|
+
message: "net:outbound requires origins"
|
|
26908
|
+
}).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
|
|
26909
|
+
message: "origins requires the net:outbound capability"
|
|
26910
|
+
});
|
|
26911
|
+
function safeParseManifest(input) {
|
|
26912
|
+
const parsed = manifestSchema.safeParse(input);
|
|
26913
|
+
if (parsed.success)
|
|
26914
|
+
return { ok: true, manifest: parsed.data };
|
|
26915
|
+
const first = parsed.error.issues[0];
|
|
26916
|
+
const path = first?.path.join(".") ?? "";
|
|
26917
|
+
const message = first?.message ?? "invalid manifest";
|
|
26918
|
+
return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
|
|
26919
|
+
}
|
|
26920
|
+
function isApiCompatible(manifest) {
|
|
26921
|
+
return manifest.api === PLUGIN_API_VERSION;
|
|
26922
|
+
}
|
|
26923
|
+
|
|
26924
|
+
// packages/control/src/plugins.ts
|
|
26925
|
+
var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
|
|
26926
|
+
var decoder2 = new TextDecoder;
|
|
26927
|
+
|
|
26928
|
+
// packages/control/src/nodeFs.ts
|
|
26594
26929
|
function nodeDatabaseFs() {
|
|
26595
26930
|
const dirBytes = (dir) => {
|
|
26596
26931
|
let entries;
|
|
@@ -27870,9 +28205,15 @@ function logLimit(requested) {
|
|
|
27870
28205
|
async function recentLogs(store, requested) {
|
|
27871
28206
|
return store.usage.recent(logLimit(requested));
|
|
27872
28207
|
}
|
|
28208
|
+
// packages/plugin-api/src/events.ts
|
|
28209
|
+
var WINDOW_MS2 = {
|
|
28210
|
+
"1m": 60000,
|
|
28211
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
28212
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
28213
|
+
};
|
|
27873
28214
|
// apps/gateway/src/app.ts
|
|
27874
|
-
import { existsSync, realpathSync as
|
|
27875
|
-
import { resolve as
|
|
28215
|
+
import { existsSync, realpathSync as realpathSync3 } from "fs";
|
|
28216
|
+
import { resolve as resolve3, sep as sep3 } from "path";
|
|
27876
28217
|
|
|
27877
28218
|
// node_modules/.bun/memoirist@0.4.0/node_modules/memoirist/dist/bun/index.js
|
|
27878
28219
|
var Y = (v, b) => {
|
|
@@ -36713,13 +37054,13 @@ async function* streamResponse(response) {
|
|
|
36713
37054
|
const body2 = response.body;
|
|
36714
37055
|
if (!body2)
|
|
36715
37056
|
return;
|
|
36716
|
-
const reader = body2.getReader(),
|
|
37057
|
+
const reader = body2.getReader(), decoder3 = new TextDecoder;
|
|
36717
37058
|
try {
|
|
36718
37059
|
for (;; ) {
|
|
36719
37060
|
const { done, value } = await reader.read();
|
|
36720
37061
|
if (done)
|
|
36721
37062
|
break;
|
|
36722
|
-
typeof value == "string" ? yield value : yield
|
|
37063
|
+
typeof value == "string" ? yield value : yield decoder3.decode(value);
|
|
36723
37064
|
}
|
|
36724
37065
|
} finally {
|
|
36725
37066
|
reader.releaseLock();
|
|
@@ -42237,12 +42578,12 @@ var _Elysia = class _Elysia2 {
|
|
|
42237
42578
|
}), this) : (typeof this.server?.reload == "function" && this.server.reload(this.server || {}), this._handle = composeGeneralHandler(this), this);
|
|
42238
42579
|
}
|
|
42239
42580
|
get fetch() {
|
|
42240
|
-
const
|
|
42581
|
+
const fetch2 = this.config.aot ? composeGeneralHandler(this) : createDynamicHandler(this);
|
|
42241
42582
|
return Object.defineProperty(this, "fetch", {
|
|
42242
|
-
value:
|
|
42583
|
+
value: fetch2,
|
|
42243
42584
|
configurable: true,
|
|
42244
42585
|
writable: true
|
|
42245
|
-
}),
|
|
42586
|
+
}), fetch2;
|
|
42246
42587
|
}
|
|
42247
42588
|
get modules() {
|
|
42248
42589
|
return this.promisedModules;
|
|
@@ -43070,58 +43411,6 @@ function openaiRateLimitHeaders(headroom, now) {
|
|
|
43070
43411
|
return headers;
|
|
43071
43412
|
}
|
|
43072
43413
|
|
|
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
43414
|
// apps/gateway/src/ingress/schemas.ts
|
|
43126
43415
|
var cacheControlSchema = exports_external.object({
|
|
43127
43416
|
type: exports_external.literal("ephemeral"),
|
|
@@ -43264,6 +43553,225 @@ function apiErrorHandler(input) {
|
|
|
43264
43553
|
return input.code === "NOT_FOUND" ? undefined : apiErrorResponse(input.error);
|
|
43265
43554
|
}
|
|
43266
43555
|
|
|
43556
|
+
// apps/gateway/src/plugins/routes.ts
|
|
43557
|
+
var MOUNT_PREFIX = "/api/plugins";
|
|
43558
|
+
var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
43559
|
+
var SEGMENT_PATTERN = /^[A-Za-z0-9._~-]+$/;
|
|
43560
|
+
var PARAM_PATTERN = /^:[A-Za-z][A-Za-z0-9_]*$/;
|
|
43561
|
+
function mountPath(id, path) {
|
|
43562
|
+
if (!ID_PATTERN2.test(id))
|
|
43563
|
+
return null;
|
|
43564
|
+
if (!path.startsWith("/"))
|
|
43565
|
+
return null;
|
|
43566
|
+
const base = `${MOUNT_PREFIX}/${id}`;
|
|
43567
|
+
if (path !== "/") {
|
|
43568
|
+
for (const segment of path.slice(1).split("/")) {
|
|
43569
|
+
if (segment === "." || segment === "..")
|
|
43570
|
+
return null;
|
|
43571
|
+
if (!SEGMENT_PATTERN.test(segment) && !PARAM_PATTERN.test(segment))
|
|
43572
|
+
return null;
|
|
43573
|
+
}
|
|
43574
|
+
}
|
|
43575
|
+
const mounted = `${base}${path}`;
|
|
43576
|
+
if (mounted !== base && !mounted.startsWith(`${base}/`))
|
|
43577
|
+
return null;
|
|
43578
|
+
return mounted;
|
|
43579
|
+
}
|
|
43580
|
+
async function pluginRequest(request2, params) {
|
|
43581
|
+
const url2 = new URL(request2.url);
|
|
43582
|
+
const query = Object.fromEntries(url2.searchParams);
|
|
43583
|
+
let body2 = null;
|
|
43584
|
+
if (request2.method !== "GET") {
|
|
43585
|
+
const text = await request2.text();
|
|
43586
|
+
if (text.trim().length > 0) {
|
|
43587
|
+
try {
|
|
43588
|
+
body2 = JSON.parse(text);
|
|
43589
|
+
} catch {
|
|
43590
|
+
throw new GatewayError("BAD_REQUEST", "invalid JSON body");
|
|
43591
|
+
}
|
|
43592
|
+
}
|
|
43593
|
+
}
|
|
43594
|
+
return { params, query, body: body2 };
|
|
43595
|
+
}
|
|
43596
|
+
var CACHE_CONTROL_PATTERN = /^[A-Za-z0-9 ,=."'_-]{1,200}$/;
|
|
43597
|
+
function toResponse(result) {
|
|
43598
|
+
const status2 = result.status ?? 200;
|
|
43599
|
+
const headers = {};
|
|
43600
|
+
if (result.cacheControl !== undefined && CACHE_CONTROL_PATTERN.test(result.cacheControl)) {
|
|
43601
|
+
headers["cache-control"] = result.cacheControl;
|
|
43602
|
+
}
|
|
43603
|
+
if (result.bytes !== undefined) {
|
|
43604
|
+
headers["content-type"] = result.contentType ?? "application/octet-stream";
|
|
43605
|
+
return new Response(result.bytes, { status: status2, headers });
|
|
43606
|
+
}
|
|
43607
|
+
headers["content-type"] = "application/json";
|
|
43608
|
+
return new Response(JSON.stringify(result.json ?? null), { status: status2, headers });
|
|
43609
|
+
}
|
|
43610
|
+
function isPluginResponse(value) {
|
|
43611
|
+
return typeof value === "object" && value !== null;
|
|
43612
|
+
}
|
|
43613
|
+
function pluginRoutes(deps) {
|
|
43614
|
+
const logger2 = deps.logger ?? noopLogger;
|
|
43615
|
+
const app = new Elysia;
|
|
43616
|
+
app.onError(apiErrorHandler);
|
|
43617
|
+
for (const plugin of deps.plugins) {
|
|
43618
|
+
for (const route of plugin.routes) {
|
|
43619
|
+
const path = mountPath(plugin.id, route.path);
|
|
43620
|
+
if (path === null) {
|
|
43621
|
+
logger2.error("plugin route refused", { plugin: plugin.id });
|
|
43622
|
+
continue;
|
|
43623
|
+
}
|
|
43624
|
+
const handle = async (context) => {
|
|
43625
|
+
await requireAdmin(context.request, deps.admin);
|
|
43626
|
+
const input = await pluginRequest(context.request, context.params);
|
|
43627
|
+
try {
|
|
43628
|
+
const result = await route.handler(input);
|
|
43629
|
+
if (!isPluginResponse(result))
|
|
43630
|
+
throw new Error("handler returned no response");
|
|
43631
|
+
return toResponse(result);
|
|
43632
|
+
} catch {
|
|
43633
|
+
logger2.error("plugin route handler failed", { plugin: plugin.id });
|
|
43634
|
+
return apiErrorResponse(new GatewayError("INTERNAL", "plugin route failed"));
|
|
43635
|
+
}
|
|
43636
|
+
};
|
|
43637
|
+
switch (route.method) {
|
|
43638
|
+
case "GET":
|
|
43639
|
+
app.get(path, handle);
|
|
43640
|
+
break;
|
|
43641
|
+
case "POST":
|
|
43642
|
+
app.post(path, handle);
|
|
43643
|
+
break;
|
|
43644
|
+
case "PUT":
|
|
43645
|
+
app.put(path, handle);
|
|
43646
|
+
break;
|
|
43647
|
+
case "DELETE":
|
|
43648
|
+
app.delete(path, handle);
|
|
43649
|
+
break;
|
|
43650
|
+
default:
|
|
43651
|
+
logger2.error("plugin route method not supported", { plugin: plugin.id });
|
|
43652
|
+
break;
|
|
43653
|
+
}
|
|
43654
|
+
}
|
|
43655
|
+
}
|
|
43656
|
+
return app;
|
|
43657
|
+
}
|
|
43658
|
+
|
|
43659
|
+
// apps/gateway/src/plugins/ui.ts
|
|
43660
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
43661
|
+
import { join as join4, resolve as resolve2, sep as sep2 } from "path";
|
|
43662
|
+
var PLUGIN_ASSET_PREFIX = "/plugin-assets";
|
|
43663
|
+
function pluginCatalog(plugins) {
|
|
43664
|
+
return plugins.map((plugin) => ({
|
|
43665
|
+
id: plugin.id,
|
|
43666
|
+
name: plugin.manifest.name,
|
|
43667
|
+
version: plugin.manifest.version,
|
|
43668
|
+
nav: plugin.manifest.nav === undefined ? null : plugin.manifest.nav,
|
|
43669
|
+
ui: plugin.ui === undefined ? null : {
|
|
43670
|
+
entry: plugin.ui.compatible ? `${PLUGIN_ASSET_PREFIX}/${plugin.id}/${plugin.ui.entry}` : null,
|
|
43671
|
+
compatible: plugin.ui.compatible,
|
|
43672
|
+
...plugin.ui.reason === undefined ? {} : { reason: plugin.ui.reason }
|
|
43673
|
+
}
|
|
43674
|
+
}));
|
|
43675
|
+
}
|
|
43676
|
+
function pluginUiRoutes(deps) {
|
|
43677
|
+
const logger2 = deps.logger ?? noopLogger;
|
|
43678
|
+
const roots = new Map;
|
|
43679
|
+
for (const plugin of deps.plugins) {
|
|
43680
|
+
if (plugin.ui === undefined || !plugin.ui.compatible)
|
|
43681
|
+
continue;
|
|
43682
|
+
try {
|
|
43683
|
+
roots.set(plugin.id, realpathSync2(join4(plugin.ui.dir, "ui")));
|
|
43684
|
+
} catch {
|
|
43685
|
+
logger2.warn("plugin ui directory is missing", { plugin: plugin.id });
|
|
43686
|
+
}
|
|
43687
|
+
}
|
|
43688
|
+
return new Elysia().onError(apiErrorHandler).get("/api/plugins", async ({ request: request2 }) => {
|
|
43689
|
+
await requireAdmin(request2, deps.admin);
|
|
43690
|
+
return { plugins: pluginCatalog(deps.plugins) };
|
|
43691
|
+
}).get(`${PLUGIN_ASSET_PREFIX}/:id/*`, ({ params, set: set2 }) => {
|
|
43692
|
+
const notFound = () => {
|
|
43693
|
+
set2.status = 404;
|
|
43694
|
+
return { error: { code: "NOT_FOUND", message: "no such plugin asset" } };
|
|
43695
|
+
};
|
|
43696
|
+
const root = roots.get(params.id);
|
|
43697
|
+
if (root === undefined)
|
|
43698
|
+
return notFound();
|
|
43699
|
+
let rest;
|
|
43700
|
+
try {
|
|
43701
|
+
rest = decodeURIComponent(params["*"] ?? "");
|
|
43702
|
+
} catch {
|
|
43703
|
+
return notFound();
|
|
43704
|
+
}
|
|
43705
|
+
let real;
|
|
43706
|
+
try {
|
|
43707
|
+
real = realpathSync2(resolve2(root, `./${rest}`));
|
|
43708
|
+
} catch {
|
|
43709
|
+
return notFound();
|
|
43710
|
+
}
|
|
43711
|
+
if (!real.startsWith(`${root}${sep2}`))
|
|
43712
|
+
return notFound();
|
|
43713
|
+
const file3 = Bun.file(real);
|
|
43714
|
+
return new Response(file3, {
|
|
43715
|
+
headers: {
|
|
43716
|
+
"cache-control": "no-cache",
|
|
43717
|
+
"content-type": real.endsWith(".js") ? "text/javascript; charset=utf-8" : real.endsWith(".css") ? "text/css; charset=utf-8" : file3.type
|
|
43718
|
+
}
|
|
43719
|
+
});
|
|
43720
|
+
});
|
|
43721
|
+
}
|
|
43722
|
+
|
|
43723
|
+
// apps/gateway/src/quiesce.ts
|
|
43724
|
+
function createQuiesceLatch() {
|
|
43725
|
+
let admitting = true;
|
|
43726
|
+
let inFlight = 0;
|
|
43727
|
+
let onIdle = null;
|
|
43728
|
+
const settle = () => {
|
|
43729
|
+
if (inFlight > 0 || onIdle === null)
|
|
43730
|
+
return;
|
|
43731
|
+
const resolve3 = onIdle;
|
|
43732
|
+
onIdle = null;
|
|
43733
|
+
resolve3();
|
|
43734
|
+
};
|
|
43735
|
+
return {
|
|
43736
|
+
enter() {
|
|
43737
|
+
if (!admitting)
|
|
43738
|
+
return null;
|
|
43739
|
+
inFlight++;
|
|
43740
|
+
let released = false;
|
|
43741
|
+
return () => {
|
|
43742
|
+
if (released)
|
|
43743
|
+
return;
|
|
43744
|
+
released = true;
|
|
43745
|
+
inFlight--;
|
|
43746
|
+
settle();
|
|
43747
|
+
};
|
|
43748
|
+
},
|
|
43749
|
+
isClosed: () => !admitting,
|
|
43750
|
+
open() {
|
|
43751
|
+
admitting = true;
|
|
43752
|
+
},
|
|
43753
|
+
async close(deadlineMs) {
|
|
43754
|
+
admitting = false;
|
|
43755
|
+
if (inFlight === 0)
|
|
43756
|
+
return { drained: true, inFlight: 0 };
|
|
43757
|
+
let timer;
|
|
43758
|
+
const idle = new Promise((resolve3) => {
|
|
43759
|
+
onIdle = resolve3;
|
|
43760
|
+
});
|
|
43761
|
+
const deadline = new Promise((resolve3) => {
|
|
43762
|
+
timer = setTimeout(resolve3, deadlineMs);
|
|
43763
|
+
});
|
|
43764
|
+
try {
|
|
43765
|
+
await Promise.race([idle, deadline]);
|
|
43766
|
+
} finally {
|
|
43767
|
+
clearTimeout(timer);
|
|
43768
|
+
onIdle = null;
|
|
43769
|
+
}
|
|
43770
|
+
return { drained: inFlight === 0, inFlight };
|
|
43771
|
+
}
|
|
43772
|
+
};
|
|
43773
|
+
}
|
|
43774
|
+
|
|
43267
43775
|
// apps/gateway/src/routes/admin.ts
|
|
43268
43776
|
function adminRoutes(deps) {
|
|
43269
43777
|
const logger2 = deps.logger ?? noopLogger;
|
|
@@ -43698,7 +44206,7 @@ function createBodyCollector(options) {
|
|
|
43698
44206
|
const client = { request: null, response: null, truncated: false };
|
|
43699
44207
|
const drain = async (stream2, entry) => {
|
|
43700
44208
|
const reader = stream2.getReader();
|
|
43701
|
-
const
|
|
44209
|
+
const decoder3 = new TextDecoder;
|
|
43702
44210
|
let bytes = 0;
|
|
43703
44211
|
try {
|
|
43704
44212
|
while (true) {
|
|
@@ -43713,11 +44221,11 @@ function createBodyCollector(options) {
|
|
|
43713
44221
|
continue;
|
|
43714
44222
|
}
|
|
43715
44223
|
bytes += value.byteLength;
|
|
43716
|
-
entry.responseText +=
|
|
44224
|
+
entry.responseText += decoder3.decode(value, { stream: true });
|
|
43717
44225
|
if (bytes > MAX_CAPTURED_BODY_BYTES)
|
|
43718
44226
|
entry.truncated = true;
|
|
43719
44227
|
}
|
|
43720
|
-
entry.responseText +=
|
|
44228
|
+
entry.responseText += decoder3.decode();
|
|
43721
44229
|
} catch {
|
|
43722
44230
|
entry.truncated = true;
|
|
43723
44231
|
} finally {
|
|
@@ -45666,7 +46174,7 @@ async function routeLog(store, requestId, target, logger2 = noopLogger) {
|
|
|
45666
46174
|
report(logger2, "failed to record request route", requestId, error51);
|
|
45667
46175
|
}
|
|
45668
46176
|
}
|
|
45669
|
-
async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit) {
|
|
46177
|
+
async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit, emit) {
|
|
45670
46178
|
try {
|
|
45671
46179
|
await store.usage.append({ ...log, apiKeyId: keyId });
|
|
45672
46180
|
} catch (error51) {
|
|
@@ -45678,6 +46186,28 @@ async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit)
|
|
|
45678
46186
|
costUsd: log.costUsd
|
|
45679
46187
|
});
|
|
45680
46188
|
}
|
|
46189
|
+
if (keyId !== null && emit !== undefined) {
|
|
46190
|
+
try {
|
|
46191
|
+
emit({
|
|
46192
|
+
requestId: log.id,
|
|
46193
|
+
apiKeyId: keyId,
|
|
46194
|
+
provider: log.resolvedProvider,
|
|
46195
|
+
model: log.resolvedModel ?? log.requestedModel,
|
|
46196
|
+
tokens: {
|
|
46197
|
+
input: log.inputTokens,
|
|
46198
|
+
output: log.outputTokens,
|
|
46199
|
+
cacheRead: log.cacheReadTokens,
|
|
46200
|
+
cacheWrite: log.cacheWriteTokens
|
|
46201
|
+
},
|
|
46202
|
+
costUsd: log.costUsd,
|
|
46203
|
+
durationMs: log.durationMs,
|
|
46204
|
+
ok: log.errorCode === null && log.status < 400,
|
|
46205
|
+
at: log.at
|
|
46206
|
+
});
|
|
46207
|
+
} catch (error51) {
|
|
46208
|
+
report(logger2, "failed to emit plugin event", log.id, error51);
|
|
46209
|
+
}
|
|
46210
|
+
}
|
|
45681
46211
|
if (bodies === undefined)
|
|
45682
46212
|
return;
|
|
45683
46213
|
try {
|
|
@@ -46176,12 +46706,12 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
46176
46706
|
function waitForCancellation(promise2, signal) {
|
|
46177
46707
|
if (signal.aborted)
|
|
46178
46708
|
return Promise.reject(signal.reason);
|
|
46179
|
-
return new Promise((
|
|
46709
|
+
return new Promise((resolve3, reject) => {
|
|
46180
46710
|
const abort = () => reject(signal.reason);
|
|
46181
46711
|
signal.addEventListener("abort", abort, { once: true });
|
|
46182
46712
|
promise2.then((value) => {
|
|
46183
46713
|
signal.removeEventListener("abort", abort);
|
|
46184
|
-
|
|
46714
|
+
resolve3(value);
|
|
46185
46715
|
}, (error51) => {
|
|
46186
46716
|
signal.removeEventListener("abort", abort);
|
|
46187
46717
|
reject(error51);
|
|
@@ -47078,8 +47608,8 @@ var KEEPALIVE_MS = 1e4;
|
|
|
47078
47608
|
var KEEPALIVE = Symbol("keepalive");
|
|
47079
47609
|
function withKeepalive(pending, ms) {
|
|
47080
47610
|
let timer;
|
|
47081
|
-
const tick = new Promise((
|
|
47082
|
-
timer = setTimeout(() =>
|
|
47611
|
+
const tick = new Promise((resolve3) => {
|
|
47612
|
+
timer = setTimeout(() => resolve3(KEEPALIVE), ms);
|
|
47083
47613
|
});
|
|
47084
47614
|
return Promise.race([pending, tick]).finally(() => clearTimeout(timer));
|
|
47085
47615
|
}
|
|
@@ -47298,7 +47828,7 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
47298
47828
|
reportRejection(deps.logger, requestId, completed, gatewayError, surface);
|
|
47299
47829
|
}
|
|
47300
47830
|
logged = true;
|
|
47301
|
-
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit);
|
|
47831
|
+
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit);
|
|
47302
47832
|
};
|
|
47303
47833
|
if (chatRequest.stream) {
|
|
47304
47834
|
const frames = surface === "anthropic" ? anthropicStream(dispatched.events, requestId) : openaiStream(dispatched.events, requestId, Math.floor(deps.now() / 1000));
|
|
@@ -47339,7 +47869,7 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
47339
47869
|
if (collector !== null && !logged)
|
|
47340
47870
|
collector.client.response = rejection;
|
|
47341
47871
|
if (!logged)
|
|
47342
|
-
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit);
|
|
47872
|
+
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit);
|
|
47343
47873
|
if (!cancelled)
|
|
47344
47874
|
reportRejection(deps.logger, requestId, completed, gatewayError, surface);
|
|
47345
47875
|
return jsonResponse(rejection, HTTP_STATUS[gatewayError.code], {
|
|
@@ -47456,11 +47986,11 @@ function createApp(deps) {
|
|
|
47456
47986
|
now,
|
|
47457
47987
|
logger: logger2
|
|
47458
47988
|
});
|
|
47459
|
-
const staticDir = deps.staticDir ?
|
|
47989
|
+
const staticDir = deps.staticDir ? resolve3(deps.staticDir) : undefined;
|
|
47460
47990
|
let staticRoot;
|
|
47461
47991
|
if (staticDir) {
|
|
47462
47992
|
try {
|
|
47463
|
-
staticRoot =
|
|
47993
|
+
staticRoot = realpathSync3(staticDir);
|
|
47464
47994
|
} catch {}
|
|
47465
47995
|
}
|
|
47466
47996
|
const latch = deps.latch ?? createQuiesceLatch();
|
|
@@ -47489,7 +48019,8 @@ function createApp(deps) {
|
|
|
47489
48019
|
logger: logger2,
|
|
47490
48020
|
...deps.loadRegistry === undefined ? {} : { loadRegistry: deps.loadRegistry },
|
|
47491
48021
|
discoveryMirrors: deps.discoveryMirrors === true,
|
|
47492
|
-
bodyLoggingAllowed: deps.bodyLoggingAllowed === true
|
|
48022
|
+
bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
|
|
48023
|
+
...deps.emit === undefined ? {} : { emit: deps.emit }
|
|
47493
48024
|
})).use(adminRoutes({
|
|
47494
48025
|
store: deps.store,
|
|
47495
48026
|
admin,
|
|
@@ -47502,6 +48033,7 @@ function createApp(deps) {
|
|
|
47502
48033
|
...deps.console === undefined ? {} : { console: deps.console }
|
|
47503
48034
|
})).use(databaseRoutes({
|
|
47504
48035
|
store: deps.store,
|
|
48036
|
+
...deps.reapplyPluginSchema === undefined ? {} : { reapplyPluginSchema: deps.reapplyPluginSchema },
|
|
47505
48037
|
admin,
|
|
47506
48038
|
latch,
|
|
47507
48039
|
snapshots,
|
|
@@ -47516,6 +48048,14 @@ function createApp(deps) {
|
|
|
47516
48048
|
http,
|
|
47517
48049
|
now,
|
|
47518
48050
|
logger: logger2
|
|
48051
|
+
})).use(pluginRoutes({
|
|
48052
|
+
admin,
|
|
48053
|
+
plugins: deps.plugins ?? [],
|
|
48054
|
+
logger: logger2
|
|
48055
|
+
})).use(pluginUiRoutes({
|
|
48056
|
+
admin,
|
|
48057
|
+
plugins: deps.pluginUi ?? [],
|
|
48058
|
+
logger: logger2
|
|
47519
48059
|
})).get("/*", ({ path, request: request2, set: set2 }) => {
|
|
47520
48060
|
const rawPath = new URL(request2.url).pathname;
|
|
47521
48061
|
const error51 = () => {
|
|
@@ -47534,14 +48074,14 @@ function createApp(deps) {
|
|
|
47534
48074
|
if (protectedPrefix)
|
|
47535
48075
|
return error51();
|
|
47536
48076
|
const requestedPath = decodedPath === "/" ? "/index.html" : decodedPath;
|
|
47537
|
-
const filePath =
|
|
47538
|
-
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${
|
|
48077
|
+
const filePath = resolve3(staticRoot, `.${requestedPath}`);
|
|
48078
|
+
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${sep3}`))
|
|
47539
48079
|
return error51();
|
|
47540
48080
|
let resolvedFilePath;
|
|
47541
48081
|
try {
|
|
47542
|
-
resolvedFilePath =
|
|
48082
|
+
resolvedFilePath = realpathSync3(filePath);
|
|
47543
48083
|
} catch {}
|
|
47544
|
-
if (resolvedFilePath && resolvedFilePath !== staticRoot && !resolvedFilePath.startsWith(`${staticRoot}${
|
|
48084
|
+
if (resolvedFilePath && resolvedFilePath !== staticRoot && !resolvedFilePath.startsWith(`${staticRoot}${sep3}`)) {
|
|
47545
48085
|
return error51();
|
|
47546
48086
|
}
|
|
47547
48087
|
const file3 = resolvedFilePath ? Bun.file(resolvedFilePath) : undefined;
|
|
@@ -47557,7 +48097,7 @@ function createApp(deps) {
|
|
|
47557
48097
|
const isNavigation = request2.headers.get("accept")?.includes("text/html") ?? false;
|
|
47558
48098
|
if (decodedPath.startsWith("/assets/") || !isNavigation)
|
|
47559
48099
|
return error51();
|
|
47560
|
-
const indexPath =
|
|
48100
|
+
const indexPath = resolve3(staticRoot, "index.html");
|
|
47561
48101
|
const index = Bun.file(indexPath);
|
|
47562
48102
|
if (index.size === 0)
|
|
47563
48103
|
return error51();
|
|
@@ -47588,8 +48128,8 @@ function createShutdown(deps) {
|
|
|
47588
48128
|
for (const stop of deps.stopLoops)
|
|
47589
48129
|
stop();
|
|
47590
48130
|
let timer;
|
|
47591
|
-
const abandoned = new Promise((
|
|
47592
|
-
timer = setTimeout(() =>
|
|
48131
|
+
const abandoned = new Promise((resolve4) => {
|
|
48132
|
+
timer = setTimeout(() => resolve4("timeout"), deps.stopDeadlineMs ?? STOP_DEADLINE_MS);
|
|
47593
48133
|
timer.unref?.();
|
|
47594
48134
|
});
|
|
47595
48135
|
Promise.race([deps.stopServer(), abandoned]).then((outcome) => {
|
|
@@ -47717,6 +48257,334 @@ function startRefreshScheduler(deps) {
|
|
|
47717
48257
|
return () => clearInterval(timer);
|
|
47718
48258
|
}
|
|
47719
48259
|
|
|
48260
|
+
// apps/gateway/src/plugins/events.ts
|
|
48261
|
+
var DEFAULT_CAPACITY = 1000;
|
|
48262
|
+
var defaultScheduler = (run) => {
|
|
48263
|
+
const timer = setTimeout(run, 0);
|
|
48264
|
+
return () => clearTimeout(timer);
|
|
48265
|
+
};
|
|
48266
|
+
function createPluginEventBus(opts) {
|
|
48267
|
+
const logger2 = opts.logger ?? noopLogger;
|
|
48268
|
+
const capacity = opts.capacity ?? DEFAULT_CAPACITY;
|
|
48269
|
+
const scheduler = opts.scheduler ?? defaultScheduler;
|
|
48270
|
+
const requestHandlers = [];
|
|
48271
|
+
const limitHandlers = [];
|
|
48272
|
+
const queue = [];
|
|
48273
|
+
let dropped = 0;
|
|
48274
|
+
let handlerErrors = 0;
|
|
48275
|
+
let draining = false;
|
|
48276
|
+
let live = true;
|
|
48277
|
+
let cancelDrain;
|
|
48278
|
+
let droppedSinceReport = 0;
|
|
48279
|
+
const errorsSinceReport = new Map;
|
|
48280
|
+
const drain = () => {
|
|
48281
|
+
draining = false;
|
|
48282
|
+
cancelDrain = undefined;
|
|
48283
|
+
if (!live)
|
|
48284
|
+
return;
|
|
48285
|
+
const batch = queue.splice(0, queue.length);
|
|
48286
|
+
for (const item of batch) {
|
|
48287
|
+
const handlers = item.name === "request:completed" ? requestHandlers : limitHandlers;
|
|
48288
|
+
for (const handler of handlers) {
|
|
48289
|
+
try {
|
|
48290
|
+
handler.run(item.event);
|
|
48291
|
+
} catch {
|
|
48292
|
+
handlerErrors++;
|
|
48293
|
+
errorsSinceReport.set(handler.pluginId, (errorsSinceReport.get(handler.pluginId) ?? 0) + 1);
|
|
48294
|
+
}
|
|
48295
|
+
}
|
|
48296
|
+
}
|
|
48297
|
+
for (const [pluginId, count] of errorsSinceReport) {
|
|
48298
|
+
logger2.warn("plugin event handler failed", { plugin: pluginId, count });
|
|
48299
|
+
}
|
|
48300
|
+
errorsSinceReport.clear();
|
|
48301
|
+
if (droppedSinceReport > 0) {
|
|
48302
|
+
logger2.warn("plugin event queue overflowed", { count: droppedSinceReport });
|
|
48303
|
+
droppedSinceReport = 0;
|
|
48304
|
+
}
|
|
48305
|
+
};
|
|
48306
|
+
const schedule = () => {
|
|
48307
|
+
if (draining || !live)
|
|
48308
|
+
return;
|
|
48309
|
+
draining = true;
|
|
48310
|
+
cancelDrain = scheduler(drain);
|
|
48311
|
+
};
|
|
48312
|
+
const enqueue = (item, subscribers) => {
|
|
48313
|
+
if (!live || subscribers === 0)
|
|
48314
|
+
return;
|
|
48315
|
+
if (queue.length >= capacity) {
|
|
48316
|
+
dropped++;
|
|
48317
|
+
droppedSinceReport++;
|
|
48318
|
+
return;
|
|
48319
|
+
}
|
|
48320
|
+
queue.push(item);
|
|
48321
|
+
schedule();
|
|
48322
|
+
};
|
|
48323
|
+
return {
|
|
48324
|
+
onRequestCompleted(pluginId, run) {
|
|
48325
|
+
requestHandlers.push({ pluginId, run });
|
|
48326
|
+
},
|
|
48327
|
+
onLimitReached(pluginId, run) {
|
|
48328
|
+
limitHandlers.push({ pluginId, run });
|
|
48329
|
+
},
|
|
48330
|
+
emitRequestCompleted(event) {
|
|
48331
|
+
enqueue({ name: "request:completed", event }, requestHandlers.length);
|
|
48332
|
+
},
|
|
48333
|
+
emitLimitReached(event) {
|
|
48334
|
+
enqueue({ name: "limit:reached", event }, limitHandlers.length);
|
|
48335
|
+
},
|
|
48336
|
+
stats() {
|
|
48337
|
+
return { dropped, handlerErrors, queued: queue.length };
|
|
48338
|
+
},
|
|
48339
|
+
stop() {
|
|
48340
|
+
live = false;
|
|
48341
|
+
queue.length = 0;
|
|
48342
|
+
cancelDrain?.();
|
|
48343
|
+
cancelDrain = undefined;
|
|
48344
|
+
draining = false;
|
|
48345
|
+
}
|
|
48346
|
+
};
|
|
48347
|
+
}
|
|
48348
|
+
|
|
48349
|
+
// apps/gateway/src/plugins/loader.ts
|
|
48350
|
+
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
48351
|
+
import { join as join5, resolve as resolve5, sep as sep5 } from "path";
|
|
48352
|
+
|
|
48353
|
+
// apps/gateway/src/plugins/capabilities.ts
|
|
48354
|
+
import { mkdir as mkdir2, readFile as readFile2, realpath, writeFile as writeFile2 } from "fs/promises";
|
|
48355
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
|
|
48356
|
+
async function resolveInside(root, path) {
|
|
48357
|
+
const rootReal = await realpath(root);
|
|
48358
|
+
const target = resolve4(rootReal, path);
|
|
48359
|
+
const inside = (candidate) => candidate === rootReal || candidate.startsWith(`${rootReal}${sep4}`);
|
|
48360
|
+
if (!inside(target))
|
|
48361
|
+
throw new Error(`path is outside the plugin directory: ${path}`);
|
|
48362
|
+
let probe2 = target;
|
|
48363
|
+
for (;; ) {
|
|
48364
|
+
try {
|
|
48365
|
+
if (!inside(await realpath(probe2))) {
|
|
48366
|
+
throw new Error(`path is outside the plugin directory: ${path}`);
|
|
48367
|
+
}
|
|
48368
|
+
return target;
|
|
48369
|
+
} catch (error51) {
|
|
48370
|
+
if (error51 instanceof Error && error51.message.includes("outside"))
|
|
48371
|
+
throw error51;
|
|
48372
|
+
const parent = dirname4(probe2);
|
|
48373
|
+
if (parent === probe2 || probe2 === rootReal)
|
|
48374
|
+
return target;
|
|
48375
|
+
probe2 = parent;
|
|
48376
|
+
}
|
|
48377
|
+
}
|
|
48378
|
+
}
|
|
48379
|
+
function createPluginFiles(root) {
|
|
48380
|
+
return {
|
|
48381
|
+
async read(path) {
|
|
48382
|
+
const target = await ensureRootThen(root, () => resolveInside(root, path));
|
|
48383
|
+
try {
|
|
48384
|
+
const data = await readFile2(target);
|
|
48385
|
+
return new Uint8Array(data);
|
|
48386
|
+
} catch {
|
|
48387
|
+
return null;
|
|
48388
|
+
}
|
|
48389
|
+
},
|
|
48390
|
+
async write(path, data) {
|
|
48391
|
+
const target = await ensureRootThen(root, () => resolveInside(root, path));
|
|
48392
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
48393
|
+
await writeFile2(target, data);
|
|
48394
|
+
},
|
|
48395
|
+
async exists(path) {
|
|
48396
|
+
const target = await ensureRootThen(root, () => resolveInside(root, path));
|
|
48397
|
+
try {
|
|
48398
|
+
await readFile2(target);
|
|
48399
|
+
return true;
|
|
48400
|
+
} catch {
|
|
48401
|
+
return false;
|
|
48402
|
+
}
|
|
48403
|
+
}
|
|
48404
|
+
};
|
|
48405
|
+
}
|
|
48406
|
+
async function ensureRootThen(root, then) {
|
|
48407
|
+
await mkdir2(root, { recursive: true });
|
|
48408
|
+
return then();
|
|
48409
|
+
}
|
|
48410
|
+
var PLUGIN_FETCH_TIMEOUT_MS = 30000;
|
|
48411
|
+
function createPluginFetch(origins, deps = {}) {
|
|
48412
|
+
const fetchImpl = deps.fetchImpl ?? ((url2, init) => fetch(url2, init));
|
|
48413
|
+
const allowed = new Set(origins.map((origin) => new URL(origin).origin));
|
|
48414
|
+
const timeoutMs = deps.timeoutMs ?? PLUGIN_FETCH_TIMEOUT_MS;
|
|
48415
|
+
return async (url2, init) => {
|
|
48416
|
+
const parsed = new URL(url2);
|
|
48417
|
+
if (!allowed.has(parsed.origin)) {
|
|
48418
|
+
throw new Error(`origin ${parsed.origin} is not in the allowlist`);
|
|
48419
|
+
}
|
|
48420
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
48421
|
+
const signal = init?.signal === undefined || init.signal === null ? timeout : AbortSignal.any([init.signal, timeout]);
|
|
48422
|
+
return fetchImpl(url2, { ...init, redirect: "manual", signal });
|
|
48423
|
+
};
|
|
48424
|
+
}
|
|
48425
|
+
|
|
48426
|
+
// apps/gateway/src/plugins/loader.ts
|
|
48427
|
+
var EVENT_MAX = 64;
|
|
48428
|
+
function sanitizeEvent(value) {
|
|
48429
|
+
return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, EVENT_MAX);
|
|
48430
|
+
}
|
|
48431
|
+
function createPluginLogger(id, logger2) {
|
|
48432
|
+
const fields = (f) => ({
|
|
48433
|
+
plugin: id,
|
|
48434
|
+
...f?.event === undefined ? {} : { reason: sanitizeEvent(f.event) },
|
|
48435
|
+
...f?.count === undefined ? {} : { count: f.count },
|
|
48436
|
+
...f?.durationMs === undefined ? {} : { durationMs: f.durationMs }
|
|
48437
|
+
});
|
|
48438
|
+
return {
|
|
48439
|
+
debug: (message3, f) => logger2.debug(message3, fields(f)),
|
|
48440
|
+
info: (message3, f) => logger2.info(message3, fields(f)),
|
|
48441
|
+
warn: (message3, f) => logger2.warn(message3, fields(f)),
|
|
48442
|
+
error: (message3, f) => logger2.error(message3, fields(f))
|
|
48443
|
+
};
|
|
48444
|
+
}
|
|
48445
|
+
function createPluginStorage(store, id) {
|
|
48446
|
+
return {
|
|
48447
|
+
run: (sql, params) => store.plugins.run(id, sql, params === undefined ? [] : [...params]),
|
|
48448
|
+
all: (sql, params) => store.plugins.all(id, sql, params === undefined ? [] : [...params]),
|
|
48449
|
+
get: (sql, params) => store.plugins.get(id, sql, params === undefined ? [] : [...params]),
|
|
48450
|
+
transaction: (fn) => store.plugins.transaction(id, fn)
|
|
48451
|
+
};
|
|
48452
|
+
}
|
|
48453
|
+
function createPluginEvents(manifest, bus) {
|
|
48454
|
+
const wantsRequest = manifest.capabilities.includes("events:request");
|
|
48455
|
+
const wantsLimit = manifest.capabilities.includes("events:limit");
|
|
48456
|
+
if (!wantsRequest && !wantsLimit)
|
|
48457
|
+
return;
|
|
48458
|
+
return {
|
|
48459
|
+
...wantsRequest ? { onRequestCompleted: (handler) => bus.onRequestCompleted(manifest.id, handler) } : {},
|
|
48460
|
+
...wantsLimit ? { onLimitReached: (handler) => bus.onLimitReached(manifest.id, handler) } : {}
|
|
48461
|
+
};
|
|
48462
|
+
}
|
|
48463
|
+
function buildContext(deps) {
|
|
48464
|
+
const { manifest, home } = deps;
|
|
48465
|
+
const declared = manifest.capabilities;
|
|
48466
|
+
return {
|
|
48467
|
+
id: manifest.id,
|
|
48468
|
+
now: deps.now,
|
|
48469
|
+
logger: createPluginLogger(manifest.id, deps.logger),
|
|
48470
|
+
...declared.includes("storage") ? { storage: createPluginStorage(deps.store, manifest.id) } : {},
|
|
48471
|
+
...declared.includes("files") ? { files: createPluginFiles(join5(home, "data")) } : {},
|
|
48472
|
+
...declared.includes("net:outbound") ? { net: createPluginFetch(manifest.origins ?? []) } : {},
|
|
48473
|
+
...(() => {
|
|
48474
|
+
const events = createPluginEvents(manifest, deps.events);
|
|
48475
|
+
return events === undefined ? {} : { events };
|
|
48476
|
+
})(),
|
|
48477
|
+
config: manifest.defaults ?? {}
|
|
48478
|
+
};
|
|
48479
|
+
}
|
|
48480
|
+
function reason(error51) {
|
|
48481
|
+
return error51 instanceof Error ? error51.message : String(error51);
|
|
48482
|
+
}
|
|
48483
|
+
async function loadPlugins(deps) {
|
|
48484
|
+
const logger2 = deps.logger ?? noopLogger;
|
|
48485
|
+
const now = deps.now ?? Date.now;
|
|
48486
|
+
const plugins = [];
|
|
48487
|
+
const failures = [];
|
|
48488
|
+
let entries;
|
|
48489
|
+
try {
|
|
48490
|
+
const dirents = await readdir2(deps.root, { withFileTypes: true });
|
|
48491
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
48492
|
+
} catch {
|
|
48493
|
+
return { plugins, failures };
|
|
48494
|
+
}
|
|
48495
|
+
for (const id of entries.sort()) {
|
|
48496
|
+
const home = join5(deps.root, id);
|
|
48497
|
+
const fail2 = (why) => {
|
|
48498
|
+
failures.push({ id, reason: why });
|
|
48499
|
+
logger2.warn("plugin skipped", { plugin: id, reason: why });
|
|
48500
|
+
};
|
|
48501
|
+
let manifest;
|
|
48502
|
+
try {
|
|
48503
|
+
const raw = await readFile3(join5(home, "omni-plugin.json"), "utf8");
|
|
48504
|
+
const parsed = safeParseManifest(JSON.parse(raw));
|
|
48505
|
+
if (!parsed.ok) {
|
|
48506
|
+
fail2(parsed.reason);
|
|
48507
|
+
continue;
|
|
48508
|
+
}
|
|
48509
|
+
manifest = parsed.manifest;
|
|
48510
|
+
} catch (error51) {
|
|
48511
|
+
fail2(reason(error51));
|
|
48512
|
+
continue;
|
|
48513
|
+
}
|
|
48514
|
+
if (manifest.id !== id) {
|
|
48515
|
+
fail2(`manifest id ${manifest.id} does not match its directory ${id}`);
|
|
48516
|
+
continue;
|
|
48517
|
+
}
|
|
48518
|
+
if (!isApiCompatible(manifest)) {
|
|
48519
|
+
fail2(`plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`);
|
|
48520
|
+
continue;
|
|
48521
|
+
}
|
|
48522
|
+
let routes = [];
|
|
48523
|
+
let migrations = [];
|
|
48524
|
+
if (manifest.server !== undefined) {
|
|
48525
|
+
const entry = resolve5(home, manifest.server);
|
|
48526
|
+
if (entry !== home && !entry.startsWith(`${home}${sep5}`)) {
|
|
48527
|
+
fail2(`server entry resolves outside the plugin directory`);
|
|
48528
|
+
continue;
|
|
48529
|
+
}
|
|
48530
|
+
let definition;
|
|
48531
|
+
try {
|
|
48532
|
+
const module = await import(entry);
|
|
48533
|
+
const candidate = module.default;
|
|
48534
|
+
if (typeof candidate !== "object" || candidate === null || typeof candidate.setup !== "function") {
|
|
48535
|
+
fail2("server entry has no default export with a setup function");
|
|
48536
|
+
continue;
|
|
48537
|
+
}
|
|
48538
|
+
definition = candidate;
|
|
48539
|
+
} catch (error51) {
|
|
48540
|
+
fail2(reason(error51));
|
|
48541
|
+
continue;
|
|
48542
|
+
}
|
|
48543
|
+
migrations = definition.migrations ?? [];
|
|
48544
|
+
if (migrations.length > 0) {
|
|
48545
|
+
const applied = deps.store.plugins.migrate(id, migrations);
|
|
48546
|
+
if (applied.failed !== undefined) {
|
|
48547
|
+
fail2(`migration ${applied.failed.version} failed: ${applied.failed.reason}`);
|
|
48548
|
+
continue;
|
|
48549
|
+
}
|
|
48550
|
+
}
|
|
48551
|
+
try {
|
|
48552
|
+
const context = buildContext({
|
|
48553
|
+
manifest,
|
|
48554
|
+
home,
|
|
48555
|
+
store: deps.store,
|
|
48556
|
+
events: deps.events,
|
|
48557
|
+
logger: logger2,
|
|
48558
|
+
now
|
|
48559
|
+
});
|
|
48560
|
+
const result = await definition.setup(context);
|
|
48561
|
+
routes = result?.routes ?? [];
|
|
48562
|
+
} catch (error51) {
|
|
48563
|
+
fail2(reason(error51));
|
|
48564
|
+
continue;
|
|
48565
|
+
}
|
|
48566
|
+
}
|
|
48567
|
+
const loaded = { id, manifest, routes, migrations };
|
|
48568
|
+
if (manifest.ui !== undefined && manifest.sdk !== undefined) {
|
|
48569
|
+
const UI_DIR = "ui/";
|
|
48570
|
+
if (!manifest.ui.startsWith(UI_DIR)) {
|
|
48571
|
+
fail2(`ui entry must live under ${UI_DIR}, got ${manifest.ui}`);
|
|
48572
|
+
continue;
|
|
48573
|
+
}
|
|
48574
|
+
const compatible = Bun.semver.satisfies(deps.sdkVersion, manifest.sdk);
|
|
48575
|
+
loaded.ui = {
|
|
48576
|
+
dir: home,
|
|
48577
|
+
entry: manifest.ui.slice(UI_DIR.length),
|
|
48578
|
+
compatible,
|
|
48579
|
+
...compatible ? {} : { reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}` }
|
|
48580
|
+
};
|
|
48581
|
+
}
|
|
48582
|
+
plugins.push(loaded);
|
|
48583
|
+
logger2.info("plugin loaded", { plugin: id });
|
|
48584
|
+
}
|
|
48585
|
+
return { plugins, failures };
|
|
48586
|
+
}
|
|
48587
|
+
|
|
47720
48588
|
// apps/gateway/src/quota/poller.ts
|
|
47721
48589
|
async function startQuotaPoller(deps) {
|
|
47722
48590
|
const logger2 = deps.logger ?? noopLogger;
|
|
@@ -47790,8 +48658,8 @@ async function main() {
|
|
|
47790
48658
|
if (config2.staticDir !== null)
|
|
47791
48659
|
return config2.staticDir;
|
|
47792
48660
|
const candidates = [
|
|
47793
|
-
|
|
47794
|
-
|
|
48661
|
+
resolve6(import.meta.dir, "./public"),
|
|
48662
|
+
resolve6(import.meta.dir, "../../dashboard/dist")
|
|
47795
48663
|
];
|
|
47796
48664
|
return candidates.find((path) => existsSync2(path)) ?? candidates[1];
|
|
47797
48665
|
}
|
|
@@ -47813,10 +48681,25 @@ async function main() {
|
|
|
47813
48681
|
logger2.info(existsSync2(staticDir) ? "dashboard directory resolved" : "dashboard directory absent", {
|
|
47814
48682
|
path: staticDir
|
|
47815
48683
|
});
|
|
47816
|
-
let shutdown = (
|
|
47817
|
-
logger2.error("shutdown requested before the gateway was serving", { reason });
|
|
48684
|
+
let shutdown = (reason2) => {
|
|
48685
|
+
logger2.error("shutdown requested before the gateway was serving", { reason: reason2 });
|
|
47818
48686
|
process.exit(1);
|
|
47819
48687
|
};
|
|
48688
|
+
const installRoot = process.env.OMNI_ROOT ?? dirname5(config2.databasePath);
|
|
48689
|
+
const pluginRoot = join6(installRoot, "plugins");
|
|
48690
|
+
const pluginEvents = createPluginEventBus({ logger: logger2 });
|
|
48691
|
+
const loadedPlugins = await loadPlugins({
|
|
48692
|
+
root: pluginRoot,
|
|
48693
|
+
store,
|
|
48694
|
+
events: pluginEvents,
|
|
48695
|
+
sdkVersion: DASHBOARD_SDK_VERSION,
|
|
48696
|
+
logger: logger2,
|
|
48697
|
+
now
|
|
48698
|
+
});
|
|
48699
|
+
for (const failure of loadedPlugins.failures) {
|
|
48700
|
+
logger2.warn("plugin unavailable", { plugin: failure.id, reason: failure.reason });
|
|
48701
|
+
}
|
|
48702
|
+
logger2.info("plugins resolved", { count: loadedPlugins.plugins.length, path: pluginRoot });
|
|
47820
48703
|
const app = createApp({
|
|
47821
48704
|
store,
|
|
47822
48705
|
baseUrl: config2.baseUrl,
|
|
@@ -47828,11 +48711,24 @@ async function main() {
|
|
|
47828
48711
|
console: console2,
|
|
47829
48712
|
discoveryMirrors: config2.exposeClaudeCodeAliases,
|
|
47830
48713
|
bodyLoggingAllowed: config2.bodyLoggingAllowed,
|
|
48714
|
+
plugins: loadedPlugins.plugins.map((plugin) => ({ id: plugin.id, routes: plugin.routes })),
|
|
48715
|
+
pluginUi: loadedPlugins.plugins,
|
|
48716
|
+
...loadedPlugins.plugins.length === 0 ? {} : { emit: (event) => pluginEvents.emitRequestCompleted(event) },
|
|
48717
|
+
reapplyPluginSchema: async () => {
|
|
48718
|
+
for (const plugin of loadedPlugins.plugins) {
|
|
48719
|
+
if (plugin.migrations.length === 0)
|
|
48720
|
+
continue;
|
|
48721
|
+
const applied = store.plugins.migrate(plugin.id, plugin.migrations);
|
|
48722
|
+
if (applied.failed !== undefined) {
|
|
48723
|
+
throw new Error(`plugin ${plugin.id} migration ${applied.failed.version}: ${applied.failed.reason}`);
|
|
48724
|
+
}
|
|
48725
|
+
}
|
|
48726
|
+
},
|
|
47831
48727
|
lifecycle: {
|
|
47832
48728
|
env: process.env,
|
|
47833
48729
|
fileExists: (path) => existsSync2(path),
|
|
47834
48730
|
run: commandRunner,
|
|
47835
|
-
stop: createDeferredStop((
|
|
48731
|
+
stop: createDeferredStop((reason2, mode) => shutdown(reason2, mode))
|
|
47836
48732
|
}
|
|
47837
48733
|
});
|
|
47838
48734
|
const stopMaintenance = startMaintenance({ store, now, logger: logger2 });
|
|
@@ -47849,7 +48745,7 @@ async function main() {
|
|
|
47849
48745
|
logger2.info("omnigateway listening", { host: config2.host, port: config2.port });
|
|
47850
48746
|
shutdown = createShutdown({
|
|
47851
48747
|
logger: logger2,
|
|
47852
|
-
stopLoops: [stopMaintenance, stopRefreshScheduler, stopQuotaPoller],
|
|
48748
|
+
stopLoops: [stopMaintenance, stopRefreshScheduler, stopQuotaPoller, () => pluginEvents.stop()],
|
|
47853
48749
|
stopServer: async () => {
|
|
47854
48750
|
await app.stop();
|
|
47855
48751
|
},
|