omnigateway 0.3.0 → 0.4.0

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