omnigateway 0.4.13 → 0.5.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 (47) hide show
  1. package/README.md +52 -1
  2. package/bin/omni.js +176 -72
  3. package/gateway.js +1145 -126
  4. package/package.json +1 -1
  5. package/public/assets/{CopyValue-C2DkO9Yz.js → CopyValue-dQWAaTxf.js} +3 -3
  6. package/public/assets/{Field-B79Yxdit.js → Field-DTQ2LPzA.js} +1 -1
  7. package/public/assets/{Match-BWa6L0L1.js → Match-C85GObiU.js} +1 -1
  8. package/public/assets/{Panel-CoulMx6R.js → Panel-CQVz9yeR.js} +2 -2
  9. package/public/assets/Rack-V32CHmN6.js +316 -0
  10. package/public/assets/{Readout-D1XEfywe.js → Readout-DJR1qKja.js} +1 -1
  11. package/public/assets/States-CauIsA0N.js +54 -0
  12. package/public/assets/{Toggle-CRrH9nDt.js → Toggle-BQI8jz09.js} +1 -1
  13. package/public/assets/{TokenBreakdown-C2NsJnqO.js → TokenBreakdown-B2_gfuqt.js} +1 -1
  14. package/public/assets/_app-DzzXdoyU.js +1 -0
  15. package/public/assets/_app.accounts-owsnxlbn.js +64 -0
  16. package/public/assets/_app.console-bzKNP_8Y.js +44 -0
  17. package/public/assets/_app.database-CsJbkedE.js +24 -0
  18. package/public/assets/_app.index-D4ydl2mY.js +62 -0
  19. package/public/assets/_app.keys-DKX0smty.js +82 -0
  20. package/public/assets/_app.logs-CFFsj2zR.js +68 -0
  21. package/public/assets/_app.models-B0ZxWGHV.js +148 -0
  22. package/public/assets/{_app.plugins._pluginId-BOXRBToU.js → _app.plugins._pluginId-C5X4B1cE.js} +3 -3
  23. package/public/assets/{_app.settings-DoaOUmc1.js → _app.settings-Yg_6az-L.js} +4 -4
  24. package/public/assets/_app.usage-BRIz_GCn.js +83 -0
  25. package/public/assets/index-DiCU8bLB.js +178 -0
  26. package/public/assets/{login-B9CJqU--.js → login-B4LoiALI.js} +3 -3
  27. package/public/assets/plus-xu9reaJp.js +1 -0
  28. package/public/assets/queries-DuSqKybM.js +145 -0
  29. package/public/assets/{shared-Cct_4xwp.js → shared-DYkFSPbd.js} +4 -4
  30. package/public/assets/stream-Dz5HjHBh.js +1 -0
  31. package/public/assets/{trash-2-DwMK-JrZ.js → trash-2-DbutVvFY.js} +1 -1
  32. package/public/index.html +3 -3
  33. package/public/shared/dashboard-sdk.js +1 -1
  34. package/public/assets/Rack-HM36SprU.js +0 -314
  35. package/public/assets/States-DtkkMzEu.js +0 -54
  36. package/public/assets/_app-DVpIsUid.js +0 -1
  37. package/public/assets/_app.accounts-64d_cxir.js +0 -64
  38. package/public/assets/_app.console-BBuBmLRI.js +0 -44
  39. package/public/assets/_app.database-J9Ujio8l.js +0 -24
  40. package/public/assets/_app.index-BX4yKEXO.js +0 -62
  41. package/public/assets/_app.keys-2sKwhZJc.js +0 -76
  42. package/public/assets/_app.logs-DuHXYrZU.js +0 -68
  43. package/public/assets/_app.models-B0kTfPtn.js +0 -148
  44. package/public/assets/_app.usage-B5sxB40E.js +0 -83
  45. package/public/assets/index-iCfSaRSG.js +0 -178
  46. package/public/assets/plus-DKapAGoR.js +0 -1
  47. package/public/assets/queries-CX9mStps.js +0 -145
package/gateway.js CHANGED
@@ -2763,8 +2763,8 @@ function checkBytes(buffer, headers, options) {
2763
2763
  return true;
2764
2764
  }
2765
2765
  function hasUnknownFileSize(tokenizer) {
2766
- const fileSize = tokenizer.fileInfo.size;
2767
- return !Number.isFinite(fileSize) || fileSize === Number.MAX_SAFE_INTEGER;
2766
+ const fileSize2 = tokenizer.fileInfo.size;
2767
+ return !Number.isFinite(fileSize2) || fileSize2 === Number.MAX_SAFE_INTEGER;
2768
2768
  }
2769
2769
  function hasExceededUnknownSizeScanBudget(tokenizer, startOffset, maximumBytes) {
2770
2770
  return hasUnknownFileSize(tokenizer) && tokenizer.position - startOffset > maximumBytes;
@@ -2822,8 +2822,8 @@ function mergeByteChunks(chunks, totalLength) {
2822
2822
  return merged;
2823
2823
  }
2824
2824
  function getMaximumZipBufferedReadLength(tokenizer) {
2825
- const fileSize = tokenizer.fileInfo.size;
2826
- const remainingBytes = Number.isFinite(fileSize) ? Math.max(0, fileSize - tokenizer.position) : Number.MAX_SAFE_INTEGER;
2825
+ const fileSize2 = tokenizer.fileInfo.size;
2826
+ const remainingBytes = Number.isFinite(fileSize2) ? Math.max(0, fileSize2 - tokenizer.position) : Number.MAX_SAFE_INTEGER;
2827
2827
  return Math.min(remainingBytes, maximumZipBufferedReadSizeInBytes);
2828
2828
  }
2829
2829
  function isRecoverableZipError(error51) {
@@ -3561,11 +3561,11 @@ function normalizeMpegOffsetTolerance(mpegOffsetTolerance) {
3561
3561
  }
3562
3562
  return Math.max(0, Math.min(maximumMpegOffsetTolerance, Math.trunc(mpegOffsetTolerance)));
3563
3563
  }
3564
- function getKnownFileSizeOrMaximum(fileSize) {
3565
- if (!Number.isFinite(fileSize)) {
3564
+ function getKnownFileSizeOrMaximum(fileSize2) {
3565
+ if (!Number.isFinite(fileSize2)) {
3566
3566
  return Number.MAX_SAFE_INTEGER;
3567
3567
  }
3568
- return Math.max(0, fileSize);
3568
+ return Math.max(0, fileSize2);
3569
3569
  }
3570
3570
  function importAtRuntime(specifier) {
3571
3571
  return import(specifier);
@@ -4804,8 +4804,8 @@ class FileTypeParser {
4804
4804
  };
4805
4805
  detectImprecise = async (tokenizer) => {
4806
4806
  this.buffer = new Uint8Array(reasonableDetectionSizeInBytes);
4807
- const fileSize = getKnownFileSizeOrMaximum(tokenizer.fileInfo.size);
4808
- await tokenizer.peekBuffer(this.buffer, { length: Math.min(8, fileSize), mayBeLess: true });
4807
+ const fileSize2 = getKnownFileSizeOrMaximum(tokenizer.fileInfo.size);
4808
+ await tokenizer.peekBuffer(this.buffer, { length: Math.min(8, fileSize2), mayBeLess: true });
4809
4809
  if (this.check([0, 0, 1, 186]) || this.check([0, 0, 1, 179])) {
4810
4810
  return {
4811
4811
  ext: "mpg",
@@ -4830,7 +4830,7 @@ class FileTypeParser {
4830
4830
  mime: "image/x-icon"
4831
4831
  };
4832
4832
  }
4833
- await tokenizer.peekBuffer(this.buffer, { length: Math.min(2 + this.options.mpegOffsetTolerance, fileSize), mayBeLess: true });
4833
+ await tokenizer.peekBuffer(this.buffer, { length: Math.min(2 + this.options.mpegOffsetTolerance, fileSize2), mayBeLess: true });
4834
4834
  if (this.buffer.length >= 2 + this.options.mpegOffsetTolerance) {
4835
4835
  for (let depth = 0;depth <= this.options.mpegOffsetTolerance; ++depth) {
4836
4836
  const type = this.scanMpeg(depth);
@@ -5760,7 +5760,6 @@ function loadConfig(env) {
5760
5760
  const baseUrl = optionalText(env.OMNI_BASE_URL, derivedBaseUrl).replace(/\/+$/, "") || derivedBaseUrl;
5761
5761
  const staticDir = env.OMNI_STATIC_DIR?.trim();
5762
5762
  const logFile = env.OMNI_LOG_FILE?.trim();
5763
- const exposeClaudeCodeAliases = TRUTHY.has((env.OMNI_EXPOSE_CLAUDE_CODE_ALIASES ?? "").trim().toLowerCase());
5764
5763
  const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
5765
5764
  const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
5766
5765
  const logLevel = parseLogLevel(rawLogLevel);
@@ -5773,7 +5772,6 @@ function loadConfig(env) {
5773
5772
  encryptionKey,
5774
5773
  baseUrl,
5775
5774
  staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir,
5776
- exposeClaudeCodeAliases,
5777
5775
  bodyLoggingAllowed,
5778
5776
  logFile: logFile === undefined || logFile.length === 0 ? null : logFile
5779
5777
  };
@@ -23973,6 +23971,7 @@ var targetSchema = exports_external.discriminatedUnion("provider", [
23973
23971
  }),
23974
23972
  contextWindow: exports_external.number().int().positive().optional(),
23975
23973
  maxOutputTokens: exports_external.number().int().positive().optional(),
23974
+ credentialId: exports_external.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/, "credentialId must be an account id").optional(),
23976
23975
  capabilities: exports_external.object({
23977
23976
  tools: exports_external.boolean(),
23978
23977
  images: exports_external.boolean(),
@@ -23994,6 +23993,7 @@ var targetSchema = exports_external.discriminatedUnion("provider", [
23994
23993
  }),
23995
23994
  contextWindow: exports_external.number().int().positive().optional(),
23996
23995
  maxOutputTokens: exports_external.number().int().positive().optional(),
23996
+ credentialId: exports_external.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/, "credentialId must be an account id").optional(),
23997
23997
  capabilities: exports_external.object({
23998
23998
  tools: exports_external.boolean(),
23999
23999
  images: exports_external.boolean(),
@@ -24002,9 +24002,7 @@ var targetSchema = exports_external.discriminatedUnion("provider", [
24002
24002
  }).strict()
24003
24003
  ]);
24004
24004
  var modelSchema = exports_external.object({
24005
- id: exports_external.string().min(1).refine((value) => !value.toLowerCase().startsWith("claude/"), {
24006
- message: 'model id must not start with "claude/": that prefix is reserved for discovery mirrors'
24007
- }),
24005
+ id: exports_external.string().min(1),
24008
24006
  strategy: exports_external.enum(["score", "priority", "roundRobin", "weighted"]),
24009
24007
  isAlias: exports_external.boolean(),
24010
24008
  targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
@@ -24016,6 +24014,9 @@ var keyCreateSchema = exports_external.object({
24016
24014
  bodyLoggingOptOut: exports_external.boolean().default(false)
24017
24015
  }).strict();
24018
24016
  var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
24017
+ var keyModelsSchema = exports_external.object({
24018
+ modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
24019
+ }).strict();
24019
24020
  var retentionSchema = exports_external.object({
24020
24021
  keepLatest: exports_external.number().int().min(1).max(100),
24021
24022
  maxAgeDays: exports_external.number().int().min(1).max(3650)
@@ -24113,13 +24114,65 @@ async function readSource(deps, source, lines) {
24113
24114
  ]);
24114
24115
  return result.code === 0 ? result.stdout : "";
24115
24116
  }
24117
+ function parseConsoleLines(text, query) {
24118
+ const lines = consoleLimit(query.lines);
24119
+ return text.split(`
24120
+ `).filter((raw) => raw.trim().length > 0).map(parseLine).filter((line) => keep(line, query)).slice(-lines);
24121
+ }
24116
24122
  async function readConsole(deps, source, query) {
24117
24123
  const limited = { ...query, lines: consoleLimit(query.lines) };
24118
24124
  const text = await readSource(deps, source, scanWidth(limited));
24119
- const lines = text.split(`
24120
- `).filter((raw) => raw.trim().length > 0).map(parseLine).filter((line) => keep(line, limited)).slice(-limited.lines);
24125
+ const lines = parseConsoleLines(text, limited);
24121
24126
  return source.kind === "file" ? { source: "file", path: source.path, lines } : { source: source.kind, lines };
24122
24127
  }
24128
+ // packages/store/src/types.ts
24129
+ var WINDOW_DURATION_MS = {
24130
+ fiveHour: 5 * 60 * 60 * 1000,
24131
+ daily: 24 * 60 * 60 * 1000,
24132
+ weekly: 7 * 24 * 60 * 60 * 1000
24133
+ };
24134
+ function durationFor(windowType, windowMs) {
24135
+ return windowMs !== null && windowMs > 0 ? windowMs : WINDOW_DURATION_MS[windowType];
24136
+ }
24137
+ var SAME_WINDOW_TOLERANCE_MS = 60000;
24138
+ function sameWindow(a, b) {
24139
+ if (a === null || b === null)
24140
+ return a === b;
24141
+ return Math.abs(a - b) <= SAME_WINDOW_TOLERANCE_MS;
24142
+ }
24143
+ var READ_OVER_INPUT = 0.1;
24144
+ function cacheReadRate(prices) {
24145
+ return prices.cacheRead ?? prices.input * READ_OVER_INPUT;
24146
+ }
24147
+ function servesTarget(target, account) {
24148
+ if (account.provider !== target.provider)
24149
+ return false;
24150
+ if (target.provider === "custom" && account.providerData.endpointId !== target.endpointId) {
24151
+ return false;
24152
+ }
24153
+ return target.credentialId === undefined || account.id === target.credentialId;
24154
+ }
24155
+ function resolvePin(target, accounts) {
24156
+ if (target.credentialId === undefined)
24157
+ return;
24158
+ return accounts.find((account) => servesTarget(target, account));
24159
+ }
24160
+ var DEFAULT_SETTINGS = {
24161
+ weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
24162
+ maxAttempts: 3,
24163
+ requestDeadlineMs: 120000,
24164
+ breakerThreshold: 3,
24165
+ breakerCooldownMs: 30000,
24166
+ logRetentionDays: 30,
24167
+ quotaPollIntervalMs: 300000,
24168
+ rtkEnabled: false,
24169
+ autoCacheEnabled: true,
24170
+ bodyLoggingEnabled: false,
24171
+ bodyLoggingCaptureStreamChunks: false,
24172
+ snapshotKeepLatest: 5,
24173
+ snapshotMaxAgeDays: 30
24174
+ };
24175
+
24123
24176
  // packages/router/src/snapshot.ts
24124
24177
  var HEALTH_KEY_SEP = "::";
24125
24178
  function healthKey(credentialId, model) {
@@ -24179,12 +24232,11 @@ function eligible(input) {
24179
24232
  const excluded = [];
24180
24233
  for (const target of model.targets) {
24181
24234
  const missing = needNative && !ANTHROPIC_NATIVE_TOOLS[target.provider] ? "anthropicTools" : ["tools", "images", "reasoning"].find((cap) => need[cap] && !target.capabilities[cap]);
24235
+ let pinSeen = false;
24182
24236
  for (const credential of snapshot.credentials) {
24183
- if (credential.provider !== target.provider)
24237
+ if (!servesTarget(target, credential))
24184
24238
  continue;
24185
- if (target.provider === "custom" && credential.providerData.endpointId !== target.endpointId) {
24186
- continue;
24187
- }
24239
+ pinSeen = target.credentialId !== undefined;
24188
24240
  const drop = (reason) => {
24189
24241
  excluded.push({ credentialId: credential.id, model: target.model, reason });
24190
24242
  };
@@ -24221,45 +24273,17 @@ function eligible(input) {
24221
24273
  }
24222
24274
  pairs.push({ credential, target });
24223
24275
  }
24276
+ if (target.credentialId !== undefined && !pinSeen) {
24277
+ excluded.push({
24278
+ credentialId: target.credentialId,
24279
+ model: target.model,
24280
+ reason: "pin:missing"
24281
+ });
24282
+ }
24224
24283
  }
24225
24284
  return { pairs, excluded };
24226
24285
  }
24227
24286
 
24228
- // packages/store/src/types.ts
24229
- var WINDOW_DURATION_MS = {
24230
- fiveHour: 5 * 60 * 60 * 1000,
24231
- daily: 24 * 60 * 60 * 1000,
24232
- weekly: 7 * 24 * 60 * 60 * 1000
24233
- };
24234
- function durationFor(windowType, windowMs) {
24235
- return windowMs !== null && windowMs > 0 ? windowMs : WINDOW_DURATION_MS[windowType];
24236
- }
24237
- var SAME_WINDOW_TOLERANCE_MS = 60000;
24238
- function sameWindow(a, b) {
24239
- if (a === null || b === null)
24240
- return a === b;
24241
- return Math.abs(a - b) <= SAME_WINDOW_TOLERANCE_MS;
24242
- }
24243
- var READ_OVER_INPUT = 0.1;
24244
- function cacheReadRate(prices) {
24245
- return prices.cacheRead ?? prices.input * READ_OVER_INPUT;
24246
- }
24247
- var DEFAULT_SETTINGS = {
24248
- weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
24249
- maxAttempts: 3,
24250
- requestDeadlineMs: 120000,
24251
- breakerThreshold: 3,
24252
- breakerCooldownMs: 30000,
24253
- logRetentionDays: 30,
24254
- quotaPollIntervalMs: 300000,
24255
- rtkEnabled: false,
24256
- autoCacheEnabled: true,
24257
- bodyLoggingEnabled: false,
24258
- bodyLoggingCaptureStreamChunks: false,
24259
- snapshotKeepLatest: 5,
24260
- snapshotMaxAgeDays: 30
24261
- };
24262
-
24263
24287
  // packages/router/src/quota.ts
24264
24288
  var UNKNOWN_QUOTA = 0.5;
24265
24289
  var QUOTA_FLOOR = 0.1;
@@ -26055,6 +26079,12 @@ function createKeyRepo(db, logger2 = noopLogger) {
26055
26079
  id
26056
26080
  ]);
26057
26081
  },
26082
+ async setModelAllowlist(id, modelAllowlist) {
26083
+ db.run("UPDATE api_keys SET model_allowlist = ? WHERE id = ?", [
26084
+ modelAllowlist === null ? null : JSON.stringify(modelAllowlist),
26085
+ id
26086
+ ]);
26087
+ },
26058
26088
  async revoke(id) {
26059
26089
  db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
26060
26090
  }
@@ -26683,6 +26713,7 @@ async function createStore(opts) {
26683
26713
  findByHash: (hash3) => handle.keys.findByHash(hash3),
26684
26714
  create: (input) => handle.keys.create(input),
26685
26715
  setLimits: (id, limits) => handle.keys.setLimits(id, limits),
26716
+ setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
26686
26717
  revoke: (id) => handle.keys.revoke(id)
26687
26718
  },
26688
26719
  usage: {
@@ -27352,6 +27383,14 @@ async function setKeyLimits(store, id, input, now = Date.now()) {
27352
27383
  await store.keys.setLimits(id, body2.limits);
27353
27384
  return toSummary(store, { ...key, limits: body2.limits }, now);
27354
27385
  }
27386
+ async function setKeyModels(store, id, input, now = Date.now()) {
27387
+ const body2 = parseOrThrow(keyModelsSchema, input);
27388
+ const key = (await store.keys.list()).find((entry) => entry.id === id);
27389
+ if (key === undefined)
27390
+ throw new GatewayError("BAD_REQUEST", "no such api key");
27391
+ await store.keys.setModelAllowlist(id, body2.modelAllowlist);
27392
+ return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
27393
+ }
27355
27394
  async function revokeKey(store, id) {
27356
27395
  await store.keys.revoke(id);
27357
27396
  }
@@ -27412,8 +27451,8 @@ function narrower(a, b) {
27412
27451
  ...output.length === 0 ? {} : { maxOutputTokens: Math.min(...output) }
27413
27452
  };
27414
27453
  }
27415
- function targetLimits(target, auths) {
27416
- const ways = auths.size === 0 ? ["apiKey"] : [...auths];
27454
+ function targetLimits(target, auths, pinned) {
27455
+ const ways = pinned !== undefined ? [pinned.authType] : auths.size === 0 ? ["apiKey"] : [...auths];
27417
27456
  let listed = {};
27418
27457
  for (const auth of ways) {
27419
27458
  const entry = catalogLimits(target.provider, target.model, auth);
@@ -27433,20 +27472,22 @@ function targetLimits(target, auths) {
27433
27472
  }
27434
27473
  function servingAuths(credentials) {
27435
27474
  const byProvider = new Map;
27475
+ const routable = [];
27436
27476
  for (const credential of credentials) {
27437
27477
  if (!credential.enabled)
27438
27478
  continue;
27439
27479
  const ways = byProvider.get(credential.provider) ?? new Set;
27440
27480
  ways.add(credential.authType);
27441
27481
  byProvider.set(credential.provider, ways);
27482
+ routable.push(credential);
27442
27483
  }
27443
- return byProvider;
27484
+ return { byProvider, routable };
27444
27485
  }
27445
27486
  function resolveModelLimits(model, credentials) {
27446
- const auths = servingAuths(credentials);
27487
+ const { byProvider, routable } = servingAuths(credentials);
27447
27488
  let limits = {};
27448
27489
  for (const target of model.targets) {
27449
- limits = narrower(limits, targetLimits(target, auths.get(target.provider) ?? new Set));
27490
+ limits = narrower(limits, targetLimits(target, byProvider.get(target.provider) ?? new Set, resolvePin(target, routable)));
27450
27491
  }
27451
27492
  return limits;
27452
27493
  }
@@ -27484,15 +27525,18 @@ function unreachable(model, credentials, stored) {
27484
27525
  const held = heldAuths(credentials);
27485
27526
  const grandfathered = new Set((stored?.targets ?? []).map(pairOf));
27486
27527
  for (const target of model.targets) {
27487
- if (grandfathered.has(pairOf(target)))
27528
+ const pinned = resolvePin(target, credentials);
27529
+ if (pinned === undefined && grandfathered.has(pairOf(target)))
27488
27530
  continue;
27489
- const have = held.get(target.provider);
27531
+ const have = pinned === undefined ? held.get(target.provider) : new Set([pinned.authType]);
27490
27532
  if (have === undefined)
27491
27533
  continue;
27492
27534
  const reach = catalogModelAuths(target.provider, target.model);
27493
27535
  if (reach.some((auth) => have.has(auth)))
27494
27536
  continue;
27495
- return new GatewayError("BAD_REQUEST", `${target.provider} serves "${target.model}" to ${phrase(reach)} credentials only, ` + `and every ${target.provider} credential here is ${phrase([...have])} \u2014 ` + `connect the other kind, or pick a model this one can reach`);
27537
+ const holds = pinned === undefined ? `every ${target.provider} credential here is ${phrase([...have])}` : `this target is pinned to "${pinned.label}", which is ${phrase([...have])}`;
27538
+ const fix = pinned === undefined ? "connect the other kind, or pick a model this one can reach" : "pin it to the other kind, or pick a model this account can reach";
27539
+ return new GatewayError("BAD_REQUEST", `${target.provider} serves "${target.model}" to ${phrase(reach)} credentials only, ` + `and ${holds} \u2014 ${fix}`);
27496
27540
  }
27497
27541
  return null;
27498
27542
  }
@@ -27532,14 +27576,15 @@ import { join as join3 } from "path";
27532
27576
 
27533
27577
  // packages/plugin-api/src/version.ts
27534
27578
  var PLUGIN_API_VERSION = 1;
27535
- var DASHBOARD_SDK_VERSION = "0.1.1";
27579
+ var DASHBOARD_SDK_VERSION = "0.1.2";
27536
27580
  // packages/plugin-api/src/manifest.ts
27537
27581
  var CAPABILITIES = [
27538
27582
  "storage",
27539
27583
  "files",
27540
27584
  "net:outbound",
27541
27585
  "events:request",
27542
- "events:limit"
27586
+ "events:limit",
27587
+ "channels"
27543
27588
  ];
27544
27589
  var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
27545
27590
  var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
@@ -28713,9 +28758,11 @@ var KEY_PLACEHOLDER = "<your OmniGateway key>";
28713
28758
  async function describeModelsForSetup(store) {
28714
28759
  const models = await listModels(store);
28715
28760
  const credentials = (await listCredentials(store)).map((credential) => ({
28761
+ id: credential.id,
28716
28762
  provider: credential.provider,
28717
28763
  authType: credential.authType,
28718
- enabled: credential.enabled
28764
+ enabled: credential.enabled,
28765
+ providerData: credential.providerData
28719
28766
  }));
28720
28767
  return models.map((model) => ({
28721
28768
  model,
@@ -28751,8 +28798,7 @@ function claudeSettings(described, input, mapping, existing) {
28751
28798
  const visibleId = (slot, id) => {
28752
28799
  if (!ids.has(id))
28753
28800
  throw new Error(`${slot} names unknown virtual model "${id}"`);
28754
- const useMirror = input.discoveryMirrors === true && !/^(?:claude|anthropic)/i.test(id);
28755
- return useMirror ? `claude/${id}` : id;
28801
+ return id;
28756
28802
  };
28757
28803
  const settings = settingsObject(existing);
28758
28804
  const currentEnv = settings.env;
@@ -28861,6 +28907,43 @@ function tailFile(path, lines) {
28861
28907
  closeSync(fd);
28862
28908
  }
28863
28909
  }
28910
+ function fileSize(path) {
28911
+ try {
28912
+ const stat = statSync2(path);
28913
+ return stat.isFile() ? stat.size : null;
28914
+ } catch {
28915
+ return null;
28916
+ }
28917
+ }
28918
+ function readFrom(path, offset) {
28919
+ let fd;
28920
+ try {
28921
+ fd = openSync(path, "r");
28922
+ } catch {
28923
+ return null;
28924
+ }
28925
+ try {
28926
+ const size = fstatSync(fd).size;
28927
+ const shrank = size < offset;
28928
+ const from = shrank ? 0 : offset;
28929
+ const available = size - from;
28930
+ if (available <= 0)
28931
+ return { text: "", offset: size, gap: shrank };
28932
+ const span = Math.min(available, MAX_BYTES);
28933
+ const start = from + (available - span);
28934
+ const buffer = Buffer.allocUnsafe(span);
28935
+ const read = readSync(fd, buffer, 0, span, start);
28936
+ return {
28937
+ text: buffer.subarray(0, read).toString("utf8"),
28938
+ offset: start + read,
28939
+ gap: shrank || span < available
28940
+ };
28941
+ } catch {
28942
+ return null;
28943
+ } finally {
28944
+ closeSync(fd);
28945
+ }
28946
+ }
28864
28947
  // packages/control/src/usage.ts
28865
28948
  var MAX_LOG_LIMIT = 500;
28866
28949
  var DEFAULT_LOG_LIMIT = 100;
@@ -44472,6 +44555,7 @@ function createQuiesceLatch() {
44472
44555
  // apps/gateway/src/routes/admin.ts
44473
44556
  function adminRoutes(deps) {
44474
44557
  const logger2 = deps.logger ?? noopLogger;
44558
+ const changed = (topic) => deps.broadcaster?.invalidate(topic);
44475
44559
  return new Elysia().onError(apiErrorHandler).get("/api/status", async ({ request: request2 }) => {
44476
44560
  const token = readCookie(request2, ADMIN_COOKIE);
44477
44561
  return {
@@ -44531,9 +44615,9 @@ function adminRoutes(deps) {
44531
44615
  const body2 = await readJsonRecord(request2);
44532
44616
  if (body2 === null)
44533
44617
  throw new GatewayError("BAD_REQUEST", "credential body is required");
44534
- return {
44535
- credential: await createApiKeyCredential(deps.store, { provider: body2.provider, apiKey: body2.apiKey, ...body2 }, logger2)
44536
- };
44618
+ const credential = await createApiKeyCredential(deps.store, { provider: body2.provider, apiKey: body2.apiKey, ...body2 }, logger2);
44619
+ changed("res:credentials");
44620
+ return { credential };
44537
44621
  }).get("/api/credentials/health", async ({ request: request2 }) => {
44538
44622
  await requireAdmin(request2, deps.admin);
44539
44623
  return credentialHealth(deps);
@@ -44547,10 +44631,12 @@ function adminRoutes(deps) {
44547
44631
  }).patch("/api/credentials/:id", async ({ request: request2, params }) => {
44548
44632
  await requireAdmin(request2, deps.admin);
44549
44633
  await patchCredential(deps, params.id, await readJson(request2));
44634
+ changed("res:credentials");
44550
44635
  return { ok: true };
44551
44636
  }).delete("/api/credentials/:id", async ({ request: request2, params }) => {
44552
44637
  await requireAdmin(request2, deps.admin);
44553
44638
  await removeCredential(deps.store, params.id);
44639
+ changed("res:credentials");
44554
44640
  return { ok: true };
44555
44641
  }).get("/api/models", async ({ request: request2 }) => {
44556
44642
  await requireAdmin(request2, deps.admin);
@@ -44558,10 +44644,12 @@ function adminRoutes(deps) {
44558
44644
  }).put("/api/models/:id", async ({ request: request2, params }) => {
44559
44645
  await requireAdmin(request2, deps.admin);
44560
44646
  await putModel(deps.store, params.id, await readJson(request2));
44647
+ changed("res:models");
44561
44648
  return { ok: true };
44562
44649
  }).delete("/api/models/:id", async ({ request: request2, params }) => {
44563
44650
  await requireAdmin(request2, deps.admin);
44564
44651
  await removeModel(deps.store, params.id);
44652
+ changed("res:models");
44565
44653
  return { ok: true };
44566
44654
  }).get("/api/agent-setup", async ({ request: request2, query }) => {
44567
44655
  await requireAdmin(request2, deps.admin);
@@ -44573,10 +44661,7 @@ function adminRoutes(deps) {
44573
44661
  try {
44574
44662
  return {
44575
44663
  client,
44576
- files: await setupFiles(deps.store, client, {
44577
- baseUrl: deps.baseUrl,
44578
- discoveryMirrors: deps.discoveryMirrors === true
44579
- }, {
44664
+ files: await setupFiles(deps.store, client, { baseUrl: deps.baseUrl }, {
44580
44665
  defaultModel,
44581
44666
  ...query.fableModel ? { fableModel: query.fableModel } : {},
44582
44667
  ...query.opusModel ? { opusModel: query.opusModel } : {},
@@ -44592,13 +44677,23 @@ function adminRoutes(deps) {
44592
44677
  return { keys: await listKeys(deps.store) };
44593
44678
  }).post("/api/keys", async ({ request: request2 }) => {
44594
44679
  await requireAdmin(request2, deps.admin);
44595
- return createKey(deps.store, await readJson(request2));
44680
+ const created = await createKey(deps.store, await readJson(request2));
44681
+ changed("res:keys");
44682
+ return created;
44596
44683
  }).put("/api/keys/:id/limits", async ({ request: request2, params }) => {
44597
44684
  await requireAdmin(request2, deps.admin);
44598
- return setKeyLimits(deps.store, params.id, await readJson(request2));
44685
+ const key = await setKeyLimits(deps.store, params.id, await readJson(request2));
44686
+ changed("res:keys");
44687
+ return key;
44688
+ }).put("/api/keys/:id/models", async ({ request: request2, params }) => {
44689
+ await requireAdmin(request2, deps.admin);
44690
+ const key = await setKeyModels(deps.store, params.id, await readJson(request2));
44691
+ changed("res:keys");
44692
+ return key;
44599
44693
  }).delete("/api/keys/:id", async ({ request: request2, params }) => {
44600
44694
  await requireAdmin(request2, deps.admin);
44601
44695
  await revokeKey(deps.store, params.id);
44696
+ changed("res:keys");
44602
44697
  return { ok: true };
44603
44698
  }).get("/api/settings", async ({ request: request2 }) => {
44604
44699
  await requireAdmin(request2, deps.admin);
@@ -44609,6 +44704,7 @@ function adminRoutes(deps) {
44609
44704
  }).put("/api/settings", async ({ request: request2 }) => {
44610
44705
  await requireAdmin(request2, deps.admin);
44611
44706
  await putSettings(deps.store, await readJson(request2));
44707
+ changed("res:settings");
44612
44708
  return { ok: true };
44613
44709
  }).post("/api/models/:id/dry-run", async ({ request: request2, params }) => {
44614
44710
  await requireAdmin(request2, deps.admin);
@@ -44693,6 +44789,7 @@ async function swap(deps, logger2, label2, operation) {
44693
44789
  reason: label2
44694
44790
  });
44695
44791
  }
44792
+ deps.broadcaster?.invalidateAll();
44696
44793
  return result;
44697
44794
  } catch (error51) {
44698
44795
  if (error51 instanceof SwapFailedError) {
@@ -46857,24 +46954,44 @@ function report(logger2, what, requestId, error51) {
46857
46954
  reason: describeError(error51, "unknown")
46858
46955
  });
46859
46956
  }
46860
- async function beginLog(store, log, keyId, logger2 = noopLogger) {
46957
+ async function beginLog(store, log, keyId, logger2 = noopLogger, broadcast) {
46958
+ let began = false;
46861
46959
  try {
46862
46960
  await store.usage.begin({ ...log, state: "pending", apiKeyId: keyId });
46961
+ began = true;
46863
46962
  } catch (error51) {
46864
46963
  report(logger2, "failed to record request start", log.id, error51);
46865
46964
  }
46965
+ if (began && broadcast !== undefined) {
46966
+ try {
46967
+ broadcast.invalidate("res:logs");
46968
+ } catch (error51) {
46969
+ report(logger2, "failed to publish a resource invalidation", log.id, error51);
46970
+ }
46971
+ }
46866
46972
  }
46867
- async function routeLog(store, requestId, target, logger2 = noopLogger) {
46973
+ async function routeLog(store, requestId, target, logger2 = noopLogger, broadcast) {
46974
+ let routed = false;
46868
46975
  try {
46869
46976
  await store.usage.route(requestId, target);
46977
+ routed = true;
46870
46978
  } catch (error51) {
46871
46979
  report(logger2, "failed to record request route", requestId, error51);
46872
46980
  }
46981
+ if (routed && broadcast !== undefined) {
46982
+ try {
46983
+ broadcast.invalidate("res:logs");
46984
+ } catch (error51) {
46985
+ report(logger2, "failed to publish a resource invalidation", requestId, error51);
46986
+ }
46987
+ }
46873
46988
  }
46874
- async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit, emit) {
46989
+ async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit, emit, broadcast) {
46990
+ let appended = true;
46875
46991
  try {
46876
46992
  await store.usage.append({ ...log, apiKeyId: keyId });
46877
46993
  } catch (error51) {
46994
+ appended = false;
46878
46995
  report(logger2, "failed to persist request log", log.id, error51);
46879
46996
  }
46880
46997
  if (keyId !== null && debit !== undefined) {
@@ -46905,6 +47022,14 @@ async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit,
46905
47022
  report(logger2, "failed to emit plugin event", log.id, error51);
46906
47023
  }
46907
47024
  }
47025
+ if (appended && broadcast !== undefined) {
47026
+ try {
47027
+ broadcast.invalidate("res:usage");
47028
+ broadcast.invalidate("res:logs");
47029
+ } catch (error51) {
47030
+ report(logger2, "failed to publish a resource invalidation", log.id, error51);
47031
+ }
47032
+ }
46908
47033
  if (bodies === undefined)
46909
47034
  return;
46910
47035
  try {
@@ -47631,7 +47756,6 @@ function mcpServerNames(body2) {
47631
47756
  }
47632
47757
 
47633
47758
  // apps/gateway/src/ingress/model.ts
47634
- var DISCOVERY_PREFIX = "claude/";
47635
47759
  var ONE_M_SUFFIX2 = "[1m]";
47636
47760
  function normalizeClientModel(raw, betas2 = []) {
47637
47761
  let model = raw.trim();
@@ -47643,11 +47767,6 @@ function normalizeClientModel(raw, betas2 = []) {
47643
47767
  wantsOneM = true;
47644
47768
  }
47645
47769
  }
47646
- if (model.toLowerCase().startsWith(DISCOVERY_PREFIX)) {
47647
- const stripped = model.slice(DISCOVERY_PREFIX.length);
47648
- if (stripped.length > 0)
47649
- model = stripped;
47650
- }
47651
47770
  const merged = [...betas2];
47652
47771
  if (wantsOneM && !merged.includes(CONTEXT_1M_BETA))
47653
47772
  merged.push(CONTEXT_1M_BETA);
@@ -48294,29 +48413,8 @@ function describeModel(model, credentials) {
48294
48413
  ...limits.maxOutputTokens === undefined ? {} : { max_tokens: limits.maxOutputTokens }
48295
48414
  };
48296
48415
  }
48297
- var ALREADY_CLAUDE = /^(?:claude|anthropic)/i;
48298
- var MIRROR_PREFIX = "claude/";
48299
- function discoveryMirrors(data) {
48300
- const taken = new Set(data.map((entry) => entry.id));
48301
- const mirrors = [];
48302
- for (const entry of data) {
48303
- if (ALREADY_CLAUDE.test(entry.id))
48304
- continue;
48305
- const id = `${MIRROR_PREFIX}${entry.id}`;
48306
- if (taken.has(id))
48307
- continue;
48308
- mirrors.push({
48309
- ...entry,
48310
- id,
48311
- root: entry.id,
48312
- display_name: `${entry.display_name} (OmniGateway)`
48313
- });
48314
- }
48315
- return mirrors;
48316
- }
48317
- function modelListBody(models, credentials, options = {}) {
48318
- const described = models.map((model) => describeModel(model, credentials));
48319
- const data = options.discoveryMirrors === true ? [...described, ...discoveryMirrors(described)] : described;
48416
+ function modelListBody(models, credentials) {
48417
+ const data = models.map((model) => describeModel(model, credentials));
48320
48418
  return {
48321
48419
  object: "list",
48322
48420
  data,
@@ -48516,7 +48614,7 @@ async function handle(deps, rateLimiter, surface, request2) {
48516
48614
  ...captured === null ? {} : { http: captured.wrap(deps.http) },
48517
48615
  async onRoute(target) {
48518
48616
  if (began) {
48519
- await routeLog(deps.store, requestId, target, deps.logger);
48617
+ await routeLog(deps.store, requestId, target, deps.logger, deps.broadcaster);
48520
48618
  return;
48521
48619
  }
48522
48620
  began = true;
@@ -48528,7 +48626,7 @@ async function handle(deps, rateLimiter, surface, request2) {
48528
48626
  resolvedModel: target.model,
48529
48627
  credentialId: target.credentialId
48530
48628
  });
48531
- await beginLog(deps.store, log2, keyId, deps.logger);
48629
+ await beginLog(deps.store, log2, keyId, deps.logger, deps.broadcaster);
48532
48630
  }
48533
48631
  }, request2.signal, requestId);
48534
48632
  outcome = dispatched;
@@ -48551,7 +48649,7 @@ async function handle(deps, rateLimiter, surface, request2) {
48551
48649
  reportRejection(deps.logger, requestId, completed, gatewayError, surface);
48552
48650
  }
48553
48651
  logged = true;
48554
- await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit);
48652
+ await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit, deps.broadcaster);
48555
48653
  };
48556
48654
  if (chatRequest.stream) {
48557
48655
  const frames = surface === "anthropic" ? anthropicStream(dispatched.events, requestId) : openaiStream(dispatched.events, requestId, Math.floor(deps.now() / 1000));
@@ -48592,7 +48690,7 @@ async function handle(deps, rateLimiter, surface, request2) {
48592
48690
  if (collector !== null && !logged)
48593
48691
  collector.client.response = rejection;
48594
48692
  if (!logged)
48595
- await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit);
48693
+ await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit, deps.broadcaster);
48596
48694
  if (!cancelled)
48597
48695
  reportRejection(deps.logger, requestId, completed, gatewayError, surface);
48598
48696
  return jsonResponse(rejection, HTTP_STATUS[gatewayError.code], {
@@ -48626,9 +48724,7 @@ function proxyRoutes(deps) {
48626
48724
  const snapshot = await dispatchDeps.snapshots.get(deps.now());
48627
48725
  const models = [...snapshot.models.values()];
48628
48726
  const visibleModels = key.modelAllowlist === null ? models : models.filter((model) => key.modelAllowlist?.includes(model.id));
48629
- return Response.json(modelListBody(visibleModels, snapshot.credentials, {
48630
- discoveryMirrors: deps.discoveryMirrors === true
48631
- }));
48727
+ return Response.json(modelListBody(visibleModels, snapshot.credentials));
48632
48728
  } catch (error51) {
48633
48729
  const gatewayError = asGatewayError(error51);
48634
48730
  logger2.error("model listing failed", {
@@ -48668,6 +48764,684 @@ function proxyRoutes(deps) {
48668
48764
  });
48669
48765
  }
48670
48766
 
48767
+ // apps/gateway/src/stream/broadcaster.ts
48768
+ var INVALIDATION_FLOORS = {
48769
+ "res:usage": 1000,
48770
+ "res:logs": 1000,
48771
+ "res:quota": 5000,
48772
+ "res:credentials": 5000
48773
+ };
48774
+ var DEFAULT_FLOOR_MS = 1000;
48775
+ var GLOBAL_INVALIDATE = "res:*";
48776
+ function createBroadcaster(deps) {
48777
+ const streams = new Set;
48778
+ return {
48779
+ invalidate(topic, keys) {
48780
+ deps.coalescer.emit(topic, keys === undefined ? undefined : { keys });
48781
+ },
48782
+ invalidateAll() {
48783
+ deps.registry.publish(GLOBAL_INVALIDATE, {
48784
+ type: "event",
48785
+ topic: GLOBAL_INVALIDATE
48786
+ });
48787
+ },
48788
+ declareStream(topic) {
48789
+ streams.add(topic);
48790
+ },
48791
+ declared(topic) {
48792
+ return streams.has(topic);
48793
+ },
48794
+ stream(topic, payload) {
48795
+ const seq = deps.ring.push(topic, payload);
48796
+ deps.registry.publish(topic, { type: "event", topic, seq, payload });
48797
+ },
48798
+ resetStream(topic) {
48799
+ deps.ring.reset(topic);
48800
+ },
48801
+ stop() {
48802
+ deps.coalescer.stop();
48803
+ }
48804
+ };
48805
+ }
48806
+
48807
+ // apps/gateway/src/stream/protocol.ts
48808
+ function topicClass(topic) {
48809
+ if (topic.startsWith("res:") && topic.length > 4)
48810
+ return "res";
48811
+ if (topic.startsWith("stream:") && topic.length > 7)
48812
+ return "stream";
48813
+ if (topic.startsWith("plugin:")) {
48814
+ const rest = topic.slice(7);
48815
+ const colon = rest.indexOf(":");
48816
+ return colon > 0 && colon < rest.length - 1 ? "plugin" : null;
48817
+ }
48818
+ return null;
48819
+ }
48820
+ var MAX_TOPIC = 128;
48821
+ function isRecord7(value) {
48822
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48823
+ }
48824
+ function parseClientFrame(raw) {
48825
+ if (!isRecord7(raw))
48826
+ return null;
48827
+ const { id, type, topic } = raw;
48828
+ if (id !== undefined && typeof id !== "string")
48829
+ return null;
48830
+ if (typeof topic !== "string" || topic.length === 0 || topic.length > MAX_TOPIC)
48831
+ return null;
48832
+ if (topicClass(topic) === null)
48833
+ return null;
48834
+ const head = id === undefined ? { topic } : { id, topic };
48835
+ if (type === "unsubscribe")
48836
+ return { ...head, type: "unsubscribe" };
48837
+ if (type === "send") {
48838
+ return "payload" in raw ? { ...head, type: "send", payload: raw.payload } : { ...head, type: "send" };
48839
+ }
48840
+ if (type === "subscribe") {
48841
+ const { sinceSeq } = raw;
48842
+ if (sinceSeq === undefined)
48843
+ return { ...head, type: "subscribe" };
48844
+ if (typeof sinceSeq !== "number" || !Number.isInteger(sinceSeq) || sinceSeq < 0)
48845
+ return null;
48846
+ return { ...head, type: "subscribe", sinceSeq };
48847
+ }
48848
+ return null;
48849
+ }
48850
+
48851
+ // apps/gateway/src/routes/stream.ts
48852
+ function authorised(channels, principal, topic) {
48853
+ const kind = topicClass(topic);
48854
+ if (kind === null)
48855
+ return false;
48856
+ if (kind === "plugin") {
48857
+ if (!channels.opened(topic))
48858
+ return false;
48859
+ if (principal.kind === "admin")
48860
+ return true;
48861
+ return topic.startsWith(`plugin:${principal.pluginId}:`);
48862
+ }
48863
+ return principal.kind === "admin";
48864
+ }
48865
+ function streamRoutes(deps) {
48866
+ const logger2 = deps.logger ?? noopLogger;
48867
+ const nextId = deps.connectionId ?? (() => `ws_${crypto.randomUUID()}`);
48868
+ const resolved = new WeakMap;
48869
+ const ids = new WeakMap;
48870
+ const send = (ws, frame2) => {
48871
+ ws.send(JSON.stringify(frame2));
48872
+ };
48873
+ return new Elysia().onError(apiErrorHandler).ws("/api/stream", {
48874
+ beforeHandle: async ({ request: request2 }) => {
48875
+ if (resolved.has(request2))
48876
+ return;
48877
+ const bearer = request2.headers.get("authorization");
48878
+ const cookie = readCookie(request2, ADMIN_COOKIE);
48879
+ if (bearer !== null && cookie !== null) {
48880
+ throw new GatewayError("AUTH", "present one credential, not two");
48881
+ }
48882
+ await requireAdmin(request2, deps.admin);
48883
+ if (cookie === null)
48884
+ throw new GatewayError("AUTH", "admin session required");
48885
+ resolved.set(request2, {
48886
+ principal: { kind: "admin" },
48887
+ revalidate: () => deps.admin.verify(cookie)
48888
+ });
48889
+ },
48890
+ open(ws) {
48891
+ const credential = resolved.get(ws.data.request);
48892
+ if (credential === undefined) {
48893
+ ws.close(1011, "unauthenticated");
48894
+ return;
48895
+ }
48896
+ const id = nextId();
48897
+ ids.set(ws.raw, id);
48898
+ deps.registry.add(id, ws.raw, credential);
48899
+ deps.registry.subscribe(id, GLOBAL_INVALIDATE);
48900
+ },
48901
+ message(ws, raw) {
48902
+ const id = ids.get(ws.raw);
48903
+ if (id === undefined)
48904
+ return;
48905
+ const frame2 = parseClientFrame(raw);
48906
+ if (frame2 === null) {
48907
+ send(ws, { type: "error", message: "malformed frame" });
48908
+ return;
48909
+ }
48910
+ const principal = deps.registry.principal(id);
48911
+ if (principal === null)
48912
+ return;
48913
+ const head = frame2.id === undefined ? {} : { id: frame2.id };
48914
+ if (!authorised(deps.channels, principal, frame2.topic)) {
48915
+ send(ws, { ...head, type: "error", topic: frame2.topic, message: "not permitted" });
48916
+ return;
48917
+ }
48918
+ if (frame2.type === "unsubscribe") {
48919
+ deps.registry.unsubscribe(id, frame2.topic);
48920
+ send(ws, { ...head, type: "ack", topic: frame2.topic });
48921
+ return;
48922
+ }
48923
+ if (frame2.type === "send") {
48924
+ if (topicClass(frame2.topic) !== "plugin") {
48925
+ send(ws, { ...head, type: "error", topic: frame2.topic, message: "topic is read-only" });
48926
+ return;
48927
+ }
48928
+ if (!deps.channels.deliver(frame2.topic, id, frame2.payload)) {
48929
+ send(ws, {
48930
+ ...head,
48931
+ type: "error",
48932
+ topic: frame2.topic,
48933
+ message: "subscribe before sending"
48934
+ });
48935
+ return;
48936
+ }
48937
+ send(ws, { ...head, type: "ack", topic: frame2.topic });
48938
+ return;
48939
+ }
48940
+ if (topicClass(frame2.topic) === "stream" && !deps.broadcaster.declared(frame2.topic)) {
48941
+ send(ws, { ...head, type: "error", topic: frame2.topic, message: "no source" });
48942
+ return;
48943
+ }
48944
+ deps.registry.subscribe(id, frame2.topic);
48945
+ send(ws, { ...head, type: "ack", topic: frame2.topic });
48946
+ if (frame2.sinceSeq !== undefined) {
48947
+ const slice = deps.ring.since(frame2.topic, frame2.sinceSeq);
48948
+ if (slice.kind === "gap") {
48949
+ send(ws, { type: "gap", topic: frame2.topic, seq: slice.seq });
48950
+ } else {
48951
+ for (const item of slice.frames) {
48952
+ send(ws, { type: "event", topic: frame2.topic, seq: item.seq, payload: item.payload });
48953
+ }
48954
+ }
48955
+ }
48956
+ },
48957
+ pong(ws) {
48958
+ const id = ids.get(ws.raw);
48959
+ if (id !== undefined)
48960
+ deps.registry.pong(id);
48961
+ },
48962
+ drain(ws) {
48963
+ const id = ids.get(ws.raw);
48964
+ if (id !== undefined)
48965
+ deps.registry.drain(id);
48966
+ },
48967
+ close(ws) {
48968
+ const id = ids.get(ws.raw);
48969
+ if (id === undefined)
48970
+ return;
48971
+ deps.channels.closed(id, deps.registry.topics(id));
48972
+ deps.registry.remove(id);
48973
+ ids.delete(ws.raw);
48974
+ logger2.debug("stream closed");
48975
+ }
48976
+ }).get("/api/stream", ({ set: set2 }) => {
48977
+ set2.status = 426;
48978
+ return { error: { code: "BAD_REQUEST", message: "websocket upgrade required" } };
48979
+ });
48980
+ }
48981
+
48982
+ // apps/gateway/src/stream/channels.ts
48983
+ var NAME_PATTERN = /^[a-z0-9][a-z0-9:._-]{0,63}$/;
48984
+ var defaultScheduler = (run) => {
48985
+ const timer = setTimeout(run, 0);
48986
+ timer.unref?.();
48987
+ return () => clearTimeout(timer);
48988
+ };
48989
+ function createChannelRegistry(deps) {
48990
+ const logger2 = deps.logger ?? noopLogger;
48991
+ const scheduler = deps.scheduler ?? defaultScheduler;
48992
+ const channels = new Map;
48993
+ let handlerErrors = 0;
48994
+ let live = true;
48995
+ let cancelReport;
48996
+ const errorsSinceReport = new Map;
48997
+ const report2 = () => {
48998
+ cancelReport = undefined;
48999
+ if (!live)
49000
+ return;
49001
+ for (const [pluginId, count] of errorsSinceReport) {
49002
+ logger2.warn("plugin channel handler failed", { plugin: pluginId, count });
49003
+ }
49004
+ errorsSinceReport.clear();
49005
+ };
49006
+ const countFailure = (pluginId) => {
49007
+ handlerErrors++;
49008
+ errorsSinceReport.set(pluginId, (errorsSinceReport.get(pluginId) ?? 0) + 1);
49009
+ if (cancelReport !== undefined || !live)
49010
+ return;
49011
+ cancelReport = scheduler(report2);
49012
+ };
49013
+ const holds = (connectionId, topic) => deps.sockets.has(connectionId, topic);
49014
+ const openChannel = (pluginId, name) => {
49015
+ if (!NAME_PATTERN.test(name)) {
49016
+ throw new Error(`channel name must match ${String(NAME_PATTERN)}, got ${name}`);
49017
+ }
49018
+ const topic = `plugin:${pluginId}:${name}`;
49019
+ const existing = channels.get(topic);
49020
+ if (existing !== undefined)
49021
+ return existing.facade;
49022
+ const channel = {
49023
+ pluginId,
49024
+ topic,
49025
+ onMessage: [],
49026
+ onClose: [],
49027
+ facade: { onMessage: () => {}, send: () => {}, onClose: () => {} }
49028
+ };
49029
+ channel.facade = {
49030
+ onMessage(handler) {
49031
+ channel.onMessage.push(handler);
49032
+ },
49033
+ onClose(handler) {
49034
+ channel.onClose.push(handler);
49035
+ },
49036
+ send(connectionId, payload) {
49037
+ if (!live)
49038
+ return;
49039
+ if (!holds(connectionId, topic))
49040
+ return;
49041
+ deps.sockets.sendTo(connectionId, { type: "event", topic, payload });
49042
+ }
49043
+ };
49044
+ channels.set(topic, channel);
49045
+ return channel.facade;
49046
+ };
49047
+ return {
49048
+ for(pluginId) {
49049
+ return { open: (name) => openChannel(pluginId, name) };
49050
+ },
49051
+ opened(topic) {
49052
+ return channels.has(topic);
49053
+ },
49054
+ deliver(topic, connectionId, payload) {
49055
+ if (!live)
49056
+ return false;
49057
+ const channel = channels.get(topic);
49058
+ if (channel === undefined)
49059
+ return false;
49060
+ if (!holds(connectionId, topic))
49061
+ return false;
49062
+ for (const handler of channel.onMessage) {
49063
+ try {
49064
+ handler({ connectionId, payload });
49065
+ } catch {
49066
+ countFailure(channel.pluginId);
49067
+ }
49068
+ }
49069
+ return true;
49070
+ },
49071
+ closed(connectionId, topics) {
49072
+ if (!live)
49073
+ return;
49074
+ for (const topic of topics) {
49075
+ const channel = channels.get(topic);
49076
+ if (channel === undefined)
49077
+ continue;
49078
+ for (const handler of channel.onClose) {
49079
+ try {
49080
+ handler(connectionId);
49081
+ } catch {
49082
+ countFailure(channel.pluginId);
49083
+ }
49084
+ }
49085
+ }
49086
+ },
49087
+ stats() {
49088
+ return { channels: channels.size, handlerErrors };
49089
+ },
49090
+ stop() {
49091
+ live = false;
49092
+ channels.clear();
49093
+ cancelReport?.();
49094
+ cancelReport = undefined;
49095
+ errorsSinceReport.clear();
49096
+ }
49097
+ };
49098
+ }
49099
+
49100
+ // apps/gateway/src/stream/coalescer.ts
49101
+ var defaultSchedule = (run, ms) => {
49102
+ const timer = setTimeout(run, ms);
49103
+ return () => clearTimeout(timer);
49104
+ };
49105
+ function createCoalescer(deps) {
49106
+ const now = deps.now ?? Date.now;
49107
+ const schedule = deps.schedule ?? defaultSchedule;
49108
+ const lastSent = new Map;
49109
+ const pending = new Map;
49110
+ const floorFor = (topic) => deps.floors[topic] ?? deps.defaultFloorMs;
49111
+ const send = (topic, payload) => {
49112
+ lastSent.set(topic, now());
49113
+ deps.sink(topic, payload);
49114
+ };
49115
+ const fire = (topic) => {
49116
+ const item = pending.get(topic);
49117
+ if (item === undefined)
49118
+ return;
49119
+ pending.delete(topic);
49120
+ send(topic, item.payload);
49121
+ };
49122
+ return {
49123
+ emit(topic, payload) {
49124
+ const floor = floorFor(topic);
49125
+ const previous = lastSent.get(topic);
49126
+ const elapsed = previous === undefined ? Number.POSITIVE_INFINITY : now() - previous;
49127
+ const waiting = pending.get(topic);
49128
+ if (waiting !== undefined) {
49129
+ waiting.payload = payload;
49130
+ return;
49131
+ }
49132
+ if (elapsed >= floor) {
49133
+ send(topic, payload);
49134
+ return;
49135
+ }
49136
+ const cancel = schedule(() => fire(topic), floor - elapsed);
49137
+ pending.set(topic, { payload, cancel });
49138
+ },
49139
+ flush() {
49140
+ for (const [topic, item] of [...pending]) {
49141
+ item.cancel();
49142
+ pending.delete(topic);
49143
+ send(topic, item.payload);
49144
+ }
49145
+ },
49146
+ stop() {
49147
+ for (const item of pending.values())
49148
+ item.cancel();
49149
+ pending.clear();
49150
+ lastSent.clear();
49151
+ }
49152
+ };
49153
+ }
49154
+
49155
+ // apps/gateway/src/stream/registry.ts
49156
+ var DEFAULT_QUEUE_CAPACITY = 1000;
49157
+ var MAX_TOPICS_PER_CONNECTION = 256;
49158
+ var HIGH_WATER_BYTES = 512 * 1024;
49159
+ var HEARTBEAT_MS = 20000;
49160
+ var PONG_DEADLINE_MS = 60000;
49161
+ var CHECK_STALE_MS = 60000;
49162
+ var CLOSE_UNAUTHENTICATED = 4401;
49163
+ function createSocketRegistry(deps = {}) {
49164
+ const logger2 = deps.logger ?? noopLogger;
49165
+ const onDetach = deps.onDetach;
49166
+ const now = deps.now ?? Date.now;
49167
+ const schedule = deps.schedule ?? ((run, ms) => {
49168
+ const timer = setTimeout(run, ms);
49169
+ timer.unref?.();
49170
+ return () => clearTimeout(timer);
49171
+ });
49172
+ const capacity = deps.queueCapacity ?? DEFAULT_QUEUE_CAPACITY;
49173
+ const heartbeatMs = deps.heartbeatMs ?? HEARTBEAT_MS;
49174
+ const pongDeadlineMs = deps.pongDeadlineMs ?? PONG_DEADLINE_MS;
49175
+ const checkStaleMs = deps.checkStaleMs ?? CHECK_STALE_MS;
49176
+ const connections = new Map;
49177
+ const index = new Map;
49178
+ let dropped = 0;
49179
+ let live = true;
49180
+ let cancelTick;
49181
+ let droppedSinceReport = 0;
49182
+ const flush = (connection) => {
49183
+ while (connection.queue.length > 0) {
49184
+ if ((connection.socket.getBufferedAmount?.() ?? 0) >= HIGH_WATER_BYTES)
49185
+ return;
49186
+ const frame2 = connection.queue[0];
49187
+ if (frame2 === undefined)
49188
+ break;
49189
+ let status2;
49190
+ try {
49191
+ status2 = connection.socket.send(JSON.stringify(frame2));
49192
+ } catch {
49193
+ return;
49194
+ }
49195
+ if (status2 === 0)
49196
+ return;
49197
+ connection.queue.shift();
49198
+ }
49199
+ };
49200
+ const deliver = (connection, frame2) => {
49201
+ if (connection.queue.length >= capacity) {
49202
+ connection.queue.shift();
49203
+ dropped++;
49204
+ droppedSinceReport++;
49205
+ }
49206
+ connection.queue.push(frame2);
49207
+ flush(connection);
49208
+ };
49209
+ const detach = (connection) => {
49210
+ for (const topic of connection.topics) {
49211
+ const subscribers = index.get(topic);
49212
+ if (subscribers === undefined)
49213
+ continue;
49214
+ subscribers.delete(connection.id);
49215
+ if (subscribers.size === 0)
49216
+ index.delete(topic);
49217
+ }
49218
+ connections.delete(connection.id);
49219
+ };
49220
+ const closeOne = (connection, code, reason) => {
49221
+ onDetach?.(connection.id, [...connection.topics]);
49222
+ detach(connection);
49223
+ try {
49224
+ connection.socket.close(code, reason);
49225
+ } catch {}
49226
+ };
49227
+ const tick = () => {
49228
+ cancelTick = undefined;
49229
+ if (!live)
49230
+ return;
49231
+ const at = now();
49232
+ for (const connection of [...connections.values()]) {
49233
+ if (at - connection.lastPongAt > pongDeadlineMs) {
49234
+ closeOne(connection, 1001, "pong deadline");
49235
+ continue;
49236
+ }
49237
+ try {
49238
+ connection.socket.ping?.();
49239
+ } catch {
49240
+ closeOne(connection, 1001, "ping failed");
49241
+ continue;
49242
+ }
49243
+ if (connection.checking) {
49244
+ if (at - connection.checkingSince < checkStaleMs)
49245
+ continue;
49246
+ connection.generation += 1;
49247
+ connection.checking = false;
49248
+ }
49249
+ connection.checking = true;
49250
+ connection.checkingSince = at;
49251
+ const generation = connection.generation;
49252
+ connection.credential.revalidate().then((valid) => {
49253
+ if (connection.generation !== generation)
49254
+ return;
49255
+ connection.checking = false;
49256
+ if (!valid && connections.has(connection.id)) {
49257
+ closeOne(connection, CLOSE_UNAUTHENTICATED, "session expired");
49258
+ }
49259
+ }).catch(() => {
49260
+ if (connection.generation !== generation)
49261
+ return;
49262
+ connection.checking = false;
49263
+ });
49264
+ }
49265
+ if (droppedSinceReport > 0) {
49266
+ logger2.warn("stream queue overflowed", { count: droppedSinceReport });
49267
+ droppedSinceReport = 0;
49268
+ }
49269
+ arm();
49270
+ };
49271
+ const arm = () => {
49272
+ if (!live || cancelTick !== undefined)
49273
+ return;
49274
+ cancelTick = schedule(tick, heartbeatMs);
49275
+ };
49276
+ arm();
49277
+ return {
49278
+ add(id, socket, credential) {
49279
+ if (!live) {
49280
+ socket.close(1001, "shutting down");
49281
+ return;
49282
+ }
49283
+ connections.set(id, {
49284
+ id,
49285
+ socket,
49286
+ credential,
49287
+ topics: new Set,
49288
+ queue: [],
49289
+ lastPongAt: now(),
49290
+ checking: false,
49291
+ checkingSince: 0,
49292
+ generation: 0
49293
+ });
49294
+ },
49295
+ remove(id) {
49296
+ const connection = connections.get(id);
49297
+ if (connection !== undefined)
49298
+ detach(connection);
49299
+ },
49300
+ subscribe(id, topic) {
49301
+ const connection = connections.get(id);
49302
+ if (connection === undefined)
49303
+ return;
49304
+ if (connection.topics.size >= MAX_TOPICS_PER_CONNECTION && !connection.topics.has(topic)) {
49305
+ return;
49306
+ }
49307
+ connection.topics.add(topic);
49308
+ const subscribers = index.get(topic) ?? new Set;
49309
+ subscribers.add(id);
49310
+ index.set(topic, subscribers);
49311
+ },
49312
+ unsubscribe(id, topic) {
49313
+ const connection = connections.get(id);
49314
+ if (connection === undefined)
49315
+ return;
49316
+ connection.topics.delete(topic);
49317
+ const subscribers = index.get(topic);
49318
+ if (subscribers === undefined)
49319
+ return;
49320
+ subscribers.delete(id);
49321
+ if (subscribers.size === 0)
49322
+ index.delete(topic);
49323
+ },
49324
+ topics(id) {
49325
+ return [...connections.get(id)?.topics ?? []];
49326
+ },
49327
+ has(id, topic) {
49328
+ return connections.get(id)?.topics.has(topic) ?? false;
49329
+ },
49330
+ principal(id) {
49331
+ return connections.get(id)?.credential.principal ?? null;
49332
+ },
49333
+ pong(id) {
49334
+ const connection = connections.get(id);
49335
+ if (connection !== undefined)
49336
+ connection.lastPongAt = now();
49337
+ },
49338
+ drain(id) {
49339
+ const connection = connections.get(id);
49340
+ if (connection !== undefined)
49341
+ flush(connection);
49342
+ },
49343
+ publish(topic, frame2) {
49344
+ const subscribers = index.get(topic);
49345
+ if (subscribers === undefined)
49346
+ return;
49347
+ for (const id of subscribers) {
49348
+ const connection = connections.get(id);
49349
+ if (connection !== undefined)
49350
+ deliver(connection, frame2);
49351
+ }
49352
+ },
49353
+ sendTo(id, frame2) {
49354
+ const connection = connections.get(id);
49355
+ if (connection !== undefined)
49356
+ deliver(connection, frame2);
49357
+ },
49358
+ closeAll(code, reason) {
49359
+ for (const connection of [...connections.values()])
49360
+ closeOne(connection, code, reason);
49361
+ index.clear();
49362
+ },
49363
+ stats() {
49364
+ let queued = 0;
49365
+ for (const connection of connections.values())
49366
+ queued += connection.queue.length;
49367
+ return { connections: connections.size, dropped, queued };
49368
+ },
49369
+ stop() {
49370
+ live = false;
49371
+ cancelTick?.();
49372
+ cancelTick = undefined;
49373
+ }
49374
+ };
49375
+ }
49376
+
49377
+ // apps/gateway/src/stream/ring.ts
49378
+ function sizeOf(payload) {
49379
+ try {
49380
+ return JSON.stringify(payload)?.length ?? 0;
49381
+ } catch {
49382
+ return 0;
49383
+ }
49384
+ }
49385
+ function createRing(limits) {
49386
+ const topics = new Map;
49387
+ const stateFor = (topic) => {
49388
+ const existing = topics.get(topic);
49389
+ if (existing !== undefined)
49390
+ return existing;
49391
+ const created = { entries: [], bytes: 0, seq: 0 };
49392
+ topics.set(topic, created);
49393
+ return created;
49394
+ };
49395
+ return {
49396
+ push(topic, payload) {
49397
+ const state = stateFor(topic);
49398
+ state.seq += 1;
49399
+ const bytes = sizeOf(payload);
49400
+ state.entries.push({ seq: state.seq, payload, bytes });
49401
+ state.bytes += bytes;
49402
+ while (state.entries.length > limits.frames || state.bytes > limits.bytes && state.entries.length > 1) {
49403
+ const evicted = state.entries.shift();
49404
+ if (evicted === undefined)
49405
+ break;
49406
+ state.bytes -= evicted.bytes;
49407
+ }
49408
+ return state.seq;
49409
+ },
49410
+ since(topic, sinceSeq) {
49411
+ const state = topics.get(topic);
49412
+ if (state === undefined) {
49413
+ return sinceSeq === 0 ? { kind: "frames", frames: [] } : { kind: "gap", seq: 0 };
49414
+ }
49415
+ if (sinceSeq > state.seq)
49416
+ return { kind: "gap", seq: state.seq };
49417
+ if (sinceSeq === state.seq)
49418
+ return { kind: "frames", frames: [] };
49419
+ const oldest = state.entries[0];
49420
+ if (oldest === undefined)
49421
+ return { kind: "gap", seq: state.seq };
49422
+ if (oldest.seq > sinceSeq + 1)
49423
+ return { kind: "gap", seq: state.seq };
49424
+ return {
49425
+ kind: "frames",
49426
+ frames: state.entries.filter((entry) => entry.seq > sinceSeq).map(({ seq, payload }) => ({ seq, payload }))
49427
+ };
49428
+ },
49429
+ head(topic) {
49430
+ return topics.get(topic)?.seq ?? 0;
49431
+ },
49432
+ reset(topic) {
49433
+ const state = topics.get(topic);
49434
+ if (state === undefined)
49435
+ return;
49436
+ state.entries = [];
49437
+ state.bytes = 0;
49438
+ },
49439
+ drop(topic) {
49440
+ topics.delete(topic);
49441
+ }
49442
+ };
49443
+ }
49444
+
48671
49445
  // apps/gateway/src/app.ts
48672
49446
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
48673
49447
  var QUIESCE_MESSAGE = "the gateway is briefly quiesced for database maintenance";
@@ -48717,6 +49491,29 @@ function createApp(deps) {
48717
49491
  } catch {}
48718
49492
  }
48719
49493
  const latch = deps.latch ?? createQuiesceLatch();
49494
+ let channelsRef;
49495
+ const registry2 = deps.registry ?? createSocketRegistry({
49496
+ logger: logger2,
49497
+ now,
49498
+ onDetach: (id, topics) => channelsRef?.closed(id, topics)
49499
+ });
49500
+ const channels = deps.channels ?? createChannelRegistry({ sockets: registry2, logger: logger2 });
49501
+ channelsRef = channels;
49502
+ const ring = deps.ring ?? createRing({ frames: 500, bytes: 2 * 1024 * 1024 });
49503
+ const broadcaster = deps.broadcaster ?? createBroadcaster({
49504
+ registry: registry2,
49505
+ ring,
49506
+ coalescer: createCoalescer({
49507
+ floors: INVALIDATION_FLOORS,
49508
+ defaultFloorMs: DEFAULT_FLOOR_MS,
49509
+ now,
49510
+ sink: (topic, payload) => registry2.publish(topic, {
49511
+ type: "event",
49512
+ topic,
49513
+ ...payload === undefined ? {} : { payload }
49514
+ })
49515
+ })
49516
+ });
48720
49517
  const admitted = new WeakMap;
48721
49518
  return new Elysia().onRequest(({ request: request2 }) => {
48722
49519
  const path = new URL(request2.url).pathname;
@@ -48741,18 +49538,18 @@ function createApp(deps) {
48741
49538
  rateLimiter,
48742
49539
  logger: logger2,
48743
49540
  ...deps.loadRegistry === undefined ? {} : { loadRegistry: deps.loadRegistry },
48744
- discoveryMirrors: deps.discoveryMirrors === true,
48745
49541
  bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
48746
- ...deps.emit === undefined ? {} : { emit: deps.emit }
49542
+ ...deps.emit === undefined ? {} : { emit: deps.emit },
49543
+ broadcaster
48747
49544
  })).use(adminRoutes({
48748
49545
  store: deps.store,
48749
49546
  admin,
48750
49547
  baseUrl: deps.baseUrl,
48751
- discoveryMirrors: deps.discoveryMirrors === true,
48752
49548
  bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
48753
49549
  now,
48754
49550
  sessionTtlMs: ADMIN_SESSION_TTL_MS,
48755
49551
  logger: logger2,
49552
+ broadcaster,
48756
49553
  ...deps.console === undefined ? {} : { console: deps.console }
48757
49554
  })).use(databaseRoutes({
48758
49555
  store: deps.store,
@@ -48760,6 +49557,7 @@ function createApp(deps) {
48760
49557
  admin,
48761
49558
  latch,
48762
49559
  snapshots,
49560
+ broadcaster,
48763
49561
  fs: deps.databaseFs ?? nodeDatabaseFs(),
48764
49562
  now,
48765
49563
  logger: logger2,
@@ -48779,6 +49577,13 @@ function createApp(deps) {
48779
49577
  admin,
48780
49578
  plugins: deps.pluginUi ?? [],
48781
49579
  logger: logger2
49580
+ })).use(streamRoutes({
49581
+ admin,
49582
+ registry: registry2,
49583
+ broadcaster,
49584
+ ring,
49585
+ channels,
49586
+ logger: logger2
48782
49587
  })).get("/*", ({ path, request: request2, set: set2 }) => {
48783
49588
  const rawPath = new URL(request2.url).pathname;
48784
49589
  const error51 = () => {
@@ -48935,6 +49740,7 @@ async function sweep(deps) {
48935
49740
  const logger2 = deps.logger ?? noopLogger;
48936
49741
  const credentials = await deps.store.credentials.list();
48937
49742
  let refreshed = 0;
49743
+ let touched = 0;
48938
49744
  for (const credential of due(credentials, deps.now())) {
48939
49745
  if (!credential.hasRefreshToken) {
48940
49746
  if (credential.expiresAt !== null && credential.expiresAt <= deps.now()) {
@@ -48943,14 +49749,18 @@ async function sweep(deps) {
48943
49749
  disabledReason: "expiredNoRefresh",
48944
49750
  disabledAt: deps.now()
48945
49751
  });
49752
+ touched += 1;
48946
49753
  }
48947
49754
  continue;
48948
49755
  }
48949
49756
  try {
48950
49757
  await deps.refresh(credential);
48951
49758
  refreshed += 1;
49759
+ touched += 1;
48952
49760
  } catch (error51) {
48953
49761
  const code = error51 instanceof GatewayError ? error51.code : "INTERNAL";
49762
+ if (code === "AUTH")
49763
+ touched += 1;
48954
49764
  logger2.warn("scheduled token refresh failed", {
48955
49765
  provider: credential.provider,
48956
49766
  credentialId: credential.id,
@@ -48959,6 +49769,8 @@ async function sweep(deps) {
48959
49769
  });
48960
49770
  }
48961
49771
  }
49772
+ if (touched > 0)
49773
+ deps.broadcaster?.invalidate("res:credentials");
48962
49774
  return refreshed;
48963
49775
  }
48964
49776
  function startRefreshScheduler(deps) {
@@ -48982,14 +49794,14 @@ function startRefreshScheduler(deps) {
48982
49794
 
48983
49795
  // apps/gateway/src/plugins/events.ts
48984
49796
  var DEFAULT_CAPACITY = 1000;
48985
- var defaultScheduler = (run) => {
49797
+ var defaultScheduler2 = (run) => {
48986
49798
  const timer = setTimeout(run, 0);
48987
49799
  return () => clearTimeout(timer);
48988
49800
  };
48989
49801
  function createPluginEventBus(opts) {
48990
49802
  const logger2 = opts.logger ?? noopLogger;
48991
49803
  const capacity = opts.capacity ?? DEFAULT_CAPACITY;
48992
- const scheduler = opts.scheduler ?? defaultScheduler;
49804
+ const scheduler = opts.scheduler ?? defaultScheduler2;
48993
49805
  const requestHandlers = [];
48994
49806
  const limitHandlers = [];
48995
49807
  const queue = [];
@@ -49197,6 +50009,7 @@ function buildContext(deps) {
49197
50009
  const events = createPluginEvents(manifest, deps.events);
49198
50010
  return events === undefined ? {} : { events };
49199
50011
  })(),
50012
+ ...declared.includes("channels") ? { channels: deps.channels.for(manifest.id) } : {},
49200
50013
  config: manifest.defaults ?? {}
49201
50014
  };
49202
50015
  }
@@ -49277,6 +50090,7 @@ async function loadPlugins(deps) {
49277
50090
  home,
49278
50091
  store: deps.store,
49279
50092
  events: deps.events,
50093
+ channels: deps.channels,
49280
50094
  logger: logger2,
49281
50095
  now
49282
50096
  });
@@ -49325,6 +50139,7 @@ async function startQuotaPoller(deps) {
49325
50139
  });
49326
50140
  }).finally(() => {
49327
50141
  running = false;
50142
+ deps.broadcaster?.invalidate("res:quota");
49328
50143
  });
49329
50144
  };
49330
50145
  pass();
@@ -49333,6 +50148,169 @@ async function startQuotaPoller(deps) {
49333
50148
  return () => clearInterval(timer);
49334
50149
  }
49335
50150
 
50151
+ // apps/gateway/src/stream/console.ts
50152
+ import { watch } from "fs";
50153
+ var CONSOLE_TOPIC = "stream:console";
50154
+ var CONSOLE_FLOOR_MS = 250;
50155
+ var JOURNAL_POLL_MS = 1000;
50156
+ var REWATCH_ATTEMPTS = 5;
50157
+ var defaultSchedule2 = (run, ms) => {
50158
+ const timer = setTimeout(run, ms);
50159
+ return () => clearTimeout(timer);
50160
+ };
50161
+ function startConsoleStream(deps) {
50162
+ const logger2 = deps.logger ?? noopLogger;
50163
+ const { source } = deps.console;
50164
+ if (source.kind === "none")
50165
+ return () => {};
50166
+ if (source.kind === "journal")
50167
+ return startJournalPoll(deps, source, logger2);
50168
+ return startFileWatch(deps, source, logger2);
50169
+ }
50170
+ function startFileWatch(deps, source, logger2) {
50171
+ const { broadcaster } = deps;
50172
+ const read = deps.read ?? readFrom;
50173
+ const size = deps.size ?? fileSize;
50174
+ const floorMs = deps.floorMs ?? CONSOLE_FLOOR_MS;
50175
+ let offset = size(source.path) ?? 0;
50176
+ let stopped = false;
50177
+ let watcher = null;
50178
+ let rotated = false;
50179
+ let rewatchAttempts = 0;
50180
+ let cancelRewatch = null;
50181
+ const rewatchSchedule = deps.schedule ?? defaultSchedule2;
50182
+ const pump = () => {
50183
+ const delta2 = read(source.path, offset);
50184
+ if (delta2 === null) {
50185
+ rotate();
50186
+ return;
50187
+ }
50188
+ offset = delta2.offset;
50189
+ const lines = parseConsoleLines(delta2.text, { lines: MAX_CONSOLE_LINES });
50190
+ if (lines.length > 0)
50191
+ broadcaster.stream(CONSOLE_TOPIC, { lines });
50192
+ if (delta2.gap || rotated) {
50193
+ rotated = false;
50194
+ broadcaster.resetStream(CONSOLE_TOPIC);
50195
+ }
50196
+ };
50197
+ const coalescer = createCoalescer({
50198
+ floors: { [CONSOLE_TOPIC]: floorMs },
50199
+ defaultFloorMs: floorMs,
50200
+ ...deps.now === undefined ? {} : { now: deps.now },
50201
+ ...deps.schedule === undefined ? {} : { schedule: deps.schedule },
50202
+ sink: () => pump()
50203
+ });
50204
+ function rotate() {
50205
+ if (stopped)
50206
+ return;
50207
+ offset = 0;
50208
+ rotated = true;
50209
+ watcher?.close();
50210
+ watcher = startWatcher(deps, source.path, onWatchEvent, logger2);
50211
+ if (watcher === null) {
50212
+ if (rewatchAttempts >= REWATCH_ATTEMPTS) {
50213
+ logger2.warn("console stream ended", { path: source.path });
50214
+ return;
50215
+ }
50216
+ rewatchAttempts += 1;
50217
+ cancelRewatch?.();
50218
+ cancelRewatch = rewatchSchedule(() => rotate(), floorMs);
50219
+ return;
50220
+ }
50221
+ rewatchAttempts = 0;
50222
+ coalescer.emit(CONSOLE_TOPIC);
50223
+ }
50224
+ function onWatchEvent(event) {
50225
+ if (event === "rename") {
50226
+ rotate();
50227
+ return;
50228
+ }
50229
+ coalescer.emit(CONSOLE_TOPIC);
50230
+ }
50231
+ watcher = startWatcher(deps, source.path, onWatchEvent, logger2);
50232
+ if (watcher === null) {
50233
+ coalescer.stop();
50234
+ return () => {};
50235
+ }
50236
+ broadcaster.declareStream(CONSOLE_TOPIC);
50237
+ logger2.info("console stream watching", { path: source.path });
50238
+ return () => {
50239
+ stopped = true;
50240
+ cancelRewatch?.();
50241
+ watcher?.close();
50242
+ coalescer.stop();
50243
+ };
50244
+ }
50245
+ function startWatcher(deps, path, onChange, logger2) {
50246
+ const create2 = deps.watch ?? ((target, changed) => {
50247
+ const watcher = watch(target, (event) => changed(event));
50248
+ watcher.on("error", (error51) => {
50249
+ logger2.error("console watch failed", { reason: describeError(error51, "unknown") });
50250
+ });
50251
+ return watcher;
50252
+ });
50253
+ try {
50254
+ return create2(path, onChange);
50255
+ } catch (error51) {
50256
+ logger2.warn("console stream unavailable", {
50257
+ path,
50258
+ reason: describeError(error51, "unknown")
50259
+ });
50260
+ return null;
50261
+ }
50262
+ }
50263
+ function startJournalPoll(deps, source, logger2) {
50264
+ const { broadcaster } = deps;
50265
+ const now = deps.now ?? Date.now;
50266
+ const schedule = deps.schedule ?? defaultSchedule2;
50267
+ const pollMs = deps.pollMs ?? JOURNAL_POLL_MS;
50268
+ let cursor = now();
50269
+ let stopped = false;
50270
+ let cancel = null;
50271
+ let publishedAtCursor = new Set;
50272
+ const arm = () => {
50273
+ if (stopped)
50274
+ return;
50275
+ cancel = schedule(() => void tick(), pollMs);
50276
+ };
50277
+ const tick = async () => {
50278
+ try {
50279
+ const read = await readConsole(deps.console.deps, source, {
50280
+ lines: MAX_CONSOLE_LINES,
50281
+ since: cursor - 1
50282
+ });
50283
+ const fresh = read.lines.filter((line) => line.at !== cursor || !publishedAtCursor.has(line.raw));
50284
+ if (fresh.length > 0) {
50285
+ let newest = cursor;
50286
+ for (const line of fresh) {
50287
+ if (line.at !== null && line.at > newest)
50288
+ newest = line.at;
50289
+ }
50290
+ if (newest > cursor) {
50291
+ cursor = newest;
50292
+ publishedAtCursor = new Set;
50293
+ }
50294
+ for (const line of fresh) {
50295
+ if (line.at === cursor)
50296
+ publishedAtCursor.add(line.raw);
50297
+ }
50298
+ broadcaster.stream(CONSOLE_TOPIC, { lines: fresh });
50299
+ }
50300
+ } catch (error51) {
50301
+ logger2.error("console poll failed", { reason: describeError(error51, "unknown") });
50302
+ } finally {
50303
+ arm();
50304
+ }
50305
+ };
50306
+ broadcaster.declareStream(CONSOLE_TOPIC);
50307
+ arm();
50308
+ return () => {
50309
+ stopped = true;
50310
+ cancel?.();
50311
+ };
50312
+ }
50313
+
49336
50314
  // apps/gateway/src/index.ts
49337
50315
  function stdoutLogger(level) {
49338
50316
  return createLogger({
@@ -49411,10 +50389,19 @@ async function main() {
49411
50389
  const installRoot = process.env.OMNI_ROOT ?? dirname5(config2.databasePath);
49412
50390
  const pluginRoot = resolve6(installRoot, "plugins");
49413
50391
  const pluginEvents = createPluginEventBus({ logger: logger2 });
50392
+ let pluginChannelsRef;
50393
+ const streamRegistry = createSocketRegistry({
50394
+ logger: logger2,
50395
+ now,
50396
+ onDetach: (id, topics) => pluginChannelsRef?.closed(id, topics)
50397
+ });
50398
+ const pluginChannels = createChannelRegistry({ sockets: streamRegistry, logger: logger2 });
50399
+ pluginChannelsRef = pluginChannels;
49414
50400
  const loadedPlugins = await loadPlugins({
49415
50401
  root: pluginRoot,
49416
50402
  store,
49417
50403
  events: pluginEvents,
50404
+ channels: pluginChannels,
49418
50405
  sdkVersion: DASHBOARD_SDK_VERSION,
49419
50406
  logger: logger2,
49420
50407
  now
@@ -49423,6 +50410,21 @@ async function main() {
49423
50410
  logger2.warn("plugin unavailable", { plugin: failure.id, reason: failure.reason });
49424
50411
  }
49425
50412
  logger2.info("plugins resolved", { count: loadedPlugins.plugins.length, path: pluginRoot });
50413
+ const streamRing = createRing({ frames: 500, bytes: 2 * 1024 * 1024 });
50414
+ const broadcaster = createBroadcaster({
50415
+ registry: streamRegistry,
50416
+ ring: streamRing,
50417
+ coalescer: createCoalescer({
50418
+ floors: INVALIDATION_FLOORS,
50419
+ defaultFloorMs: DEFAULT_FLOOR_MS,
50420
+ now,
50421
+ sink: (topic, payload) => streamRegistry.publish(topic, {
50422
+ type: "event",
50423
+ topic,
50424
+ ...payload === undefined ? {} : { payload }
50425
+ })
50426
+ })
50427
+ });
49426
50428
  const app = createApp({
49427
50429
  store,
49428
50430
  baseUrl: config2.baseUrl,
@@ -49432,7 +50434,10 @@ async function main() {
49432
50434
  staticDir,
49433
50435
  logger: logger2,
49434
50436
  console: console2,
49435
- discoveryMirrors: config2.exposeClaudeCodeAliases,
50437
+ registry: streamRegistry,
50438
+ broadcaster,
50439
+ ring: streamRing,
50440
+ channels: pluginChannels,
49436
50441
  bodyLoggingAllowed: config2.bodyLoggingAllowed,
49437
50442
  plugins: loadedPlugins.plugins.map((plugin) => ({ id: plugin.id, routes: plugin.routes })),
49438
50443
  pluginUi: loadedPlugins.plugins,
@@ -49455,20 +50460,34 @@ async function main() {
49455
50460
  }
49456
50461
  });
49457
50462
  const stopMaintenance = startMaintenance({ store, now, logger: logger2 });
49458
- const stopRefreshScheduler = startRefreshScheduler({ store, refresh, now, logger: logger2 });
50463
+ const stopRefreshScheduler = startRefreshScheduler({ store, refresh, now, logger: logger2, broadcaster });
49459
50464
  const stopQuotaPoller = await startQuotaPoller({
49460
50465
  store,
49461
50466
  providers: OAUTH_PROVIDERS,
49462
50467
  http,
49463
50468
  refresh,
49464
50469
  now,
49465
- logger: logger2
50470
+ logger: logger2,
50471
+ broadcaster
49466
50472
  });
50473
+ const stopConsoleStream = startConsoleStream({ console: console2, broadcaster, logger: logger2, now });
49467
50474
  app.listen({ port: config2.port, hostname: config2.host, idleTimeout: 255 });
49468
50475
  logger2.info("omnigateway listening", { host: config2.host, port: config2.port });
49469
50476
  shutdown = createShutdown({
49470
50477
  logger: logger2,
49471
- stopLoops: [stopMaintenance, stopRefreshScheduler, stopQuotaPoller, () => pluginEvents.stop()],
50478
+ stopLoops: [
50479
+ stopMaintenance,
50480
+ stopRefreshScheduler,
50481
+ stopQuotaPoller,
50482
+ stopConsoleStream,
50483
+ () => pluginEvents.stop(),
50484
+ () => {
50485
+ streamRegistry.closeAll(1001, "restart");
50486
+ streamRegistry.stop();
50487
+ broadcaster.stop();
50488
+ pluginChannels.stop();
50489
+ }
50490
+ ],
49472
50491
  stopServer: async () => {
49473
50492
  await app.stop();
49474
50493
  },