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.
Files changed (72) hide show
  1. package/README.md +175 -1
  2. package/bin/omni.js +1155 -40
  3. package/gateway.js +985 -89
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-DbKk1ExR.js → Chip-BltJSw8A.js} +5 -5
  6. package/public/assets/Confirm-gd8Y6snu.js +4 -0
  7. package/public/assets/CopyValue-Dds-Ow9a.js +27 -0
  8. package/public/assets/{Field-B7GOsJf8.js → Field-D-14HF0x.js} +11 -11
  9. package/public/assets/Lamp-BDf7-dMa.js +25 -0
  10. package/public/assets/Match-CgNxphvS.js +1 -0
  11. package/public/assets/Meter-stpt3vQu.js +14 -0
  12. package/public/assets/Modal-Dj3LvMS9.js +82 -0
  13. package/public/assets/Rack-B4c0dbIb.js +180 -0
  14. package/public/assets/{Readout--CQnPjoO.js → Readout-Du15WYx0.js} +5 -5
  15. package/public/assets/{States-CQFkw1UZ.js → States-CxLDlCaw.js} +5 -5
  16. package/public/assets/{Table-Ba58Wg4-.js → Table-CVDZ69Zx.js} +1 -1
  17. package/public/assets/Toggle-DMk46RX2.js +39 -0
  18. package/public/assets/TokenBreakdown-BySwFK6Q.js +31 -0
  19. package/public/assets/_app-Dx_xnlud.js +1 -0
  20. package/public/assets/_app.accounts-DvBYnbfS.js +64 -0
  21. package/public/assets/_app.console-CoaRdkB1.js +44 -0
  22. package/public/assets/_app.database-CES5ec5g.js +39 -0
  23. package/public/assets/_app.index-DwOZe1A-.js +62 -0
  24. package/public/assets/_app.keys-B5HrAu2U.js +76 -0
  25. package/public/assets/_app.logs-NbfIexz0.js +68 -0
  26. package/public/assets/_app.models-B1MdFRtQ.js +148 -0
  27. package/public/assets/_app.plugins._pluginId-ByMziW8p.js +20 -0
  28. package/public/assets/_app.settings-BkDkGkKF.js +46 -0
  29. package/public/assets/_app.usage-DNpM8Nub.js +83 -0
  30. package/public/assets/dist-9ps0Qd15.js +1 -0
  31. package/public/assets/index-gMv8Qs7m.js +178 -0
  32. package/public/assets/login-cCXnl34r.js +36 -0
  33. package/public/assets/matchContext-BOenO3J2.js +1 -0
  34. package/public/assets/plus-fXfUTt6_.js +1 -0
  35. package/public/assets/queries-CIOPbRcE.js +142 -0
  36. package/public/assets/shared-B4o-2quR.js +84 -0
  37. package/public/assets/{trash-2-DTWWI3vA.js → trash-2-ZPh3619D.js} +1 -1
  38. package/public/index.html +7 -2
  39. package/public/shared/chunks/jsx-runtime-CznXPbDH.js +1 -0
  40. package/public/shared/chunks/react-GitiQ0dt.js +1 -0
  41. package/public/shared/chunks/react-dom-BTlfKTOo.js +1 -0
  42. package/public/shared/chunks/rolldown-runtime-CbXtAM7H.js +1 -0
  43. package/public/shared/jsx-runtime.js +1 -0
  44. package/public/shared/react-dom-client.js +9 -0
  45. package/public/shared/react-dom.js +1 -0
  46. package/public/shared/react-query.js +1 -0
  47. package/public/shared/react.js +1 -0
  48. package/public/shared/styled-components.js +7 -0
  49. package/public/assets/Confirm-C5DYoI93.js +0 -4
  50. package/public/assets/CopyValue-CXjietiZ.js +0 -27
  51. package/public/assets/Lamp-QRSv-SqM.js +0 -25
  52. package/public/assets/Meter-Dngwbpjo.js +0 -14
  53. package/public/assets/Modal-DfltaSCz.js +0 -82
  54. package/public/assets/Rack-D1zBw9SW.js +0 -151
  55. package/public/assets/Toggle-DWrWc9-9.js +0 -39
  56. package/public/assets/TokenBreakdown-CyHJv0O5.js +0 -31
  57. package/public/assets/_app-CTDgfstQ.js +0 -1
  58. package/public/assets/_app.accounts-DP6T8z7I.js +0 -64
  59. package/public/assets/_app.console-DYr7TCAl.js +0 -44
  60. package/public/assets/_app.database-BzO1S_VR.js +0 -39
  61. package/public/assets/_app.index-CBt6b-lA.js +0 -62
  62. package/public/assets/_app.keys-B8HGsPPS.js +0 -76
  63. package/public/assets/_app.logs-DKsxa2Su.js +0 -68
  64. package/public/assets/_app.models-CZD2UpM-.js +0 -148
  65. package/public/assets/_app.settings-B2C1LQIF.js +0 -46
  66. package/public/assets/_app.usage-CDWUXtZy.js +0 -83
  67. package/public/assets/dist-IkJ0qg-4.js +0 -1
  68. package/public/assets/index-ClU3Xqui.js +0 -186
  69. package/public/assets/login-E3gh2_Cl.js +0 -36
  70. package/public/assets/plus-DkO4vG-T.js +0 -1
  71. package/public/assets/queries-BJHk_wso.js +0 -144
  72. 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 resolve3 } from "path";
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 realpathSync2 } from "fs";
27875
- import { resolve as resolve2, sep as sep2 } from "path";
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(), decoder2 = new TextDecoder;
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 decoder2.decode(value);
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 fetch = this.config.aot ? composeGeneralHandler(this) : createDynamicHandler(this);
42581
+ const fetch2 = this.config.aot ? composeGeneralHandler(this) : createDynamicHandler(this);
42241
42582
  return Object.defineProperty(this, "fetch", {
42242
- value: fetch,
42583
+ value: fetch2,
42243
42584
  configurable: true,
42244
42585
  writable: true
42245
- }), fetch;
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 decoder2 = new TextDecoder;
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 += decoder2.decode(value, { stream: true });
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 += decoder2.decode();
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((resolve2, reject) => {
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
- resolve2(value);
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((resolve2) => {
47082
- timer = setTimeout(() => resolve2(KEEPALIVE), ms);
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 ? resolve2(deps.staticDir) : undefined;
47989
+ const staticDir = deps.staticDir ? resolve3(deps.staticDir) : undefined;
47460
47990
  let staticRoot;
47461
47991
  if (staticDir) {
47462
47992
  try {
47463
- staticRoot = realpathSync2(staticDir);
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 = resolve2(staticRoot, `.${requestedPath}`);
47538
- if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${sep2}`))
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 = realpathSync2(filePath);
48082
+ resolvedFilePath = realpathSync3(filePath);
47543
48083
  } catch {}
47544
- if (resolvedFilePath && resolvedFilePath !== staticRoot && !resolvedFilePath.startsWith(`${staticRoot}${sep2}`)) {
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 = resolve2(staticRoot, "index.html");
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((resolve3) => {
47592
- timer = setTimeout(() => resolve3("timeout"), deps.stopDeadlineMs ?? STOP_DEADLINE_MS);
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
- resolve3(import.meta.dir, "./public"),
47794
- resolve3(import.meta.dir, "../../dashboard/dist")
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 = (reason) => {
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((reason, mode) => shutdown(reason, mode))
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
  },