omnigateway 0.1.0 → 0.1.2

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 (27) hide show
  1. package/README.md +8 -8
  2. package/bin/omni.js +255 -118
  3. package/gateway.js +212 -45
  4. package/package.json +2 -2
  5. package/public/assets/{CopyValue-BwjFnqW2.js → CopyValue-bzKz2tiJ.js} +1 -1
  6. package/public/assets/{Lamp-CSv_5d5k.js → Lamp-WOotyOTd.js} +4 -4
  7. package/public/assets/{Modal-1tHwlj0o.js → Modal-Crr8uyWt.js} +1 -1
  8. package/public/assets/{Rack-DANVhi_U.js → Rack-uDxrjvaz.js} +1 -1
  9. package/public/assets/{Toggle-pWcL2A-N.js → Toggle-D0e4C94X.js} +1 -1
  10. package/public/assets/{_app-C5mYmzgk.js → _app-OSvB-Dv9.js} +1 -1
  11. package/public/assets/_app.accounts-BOE49WyS.js +51 -0
  12. package/public/assets/_app.index-CTRlqxOq.js +61 -0
  13. package/public/assets/{_app.keys-DTREQf0P.js → _app.keys-BNEOjtbu.js} +1 -1
  14. package/public/assets/{_app.logs-Co3QPpc1.js → _app.logs-BxxqeEhM.js} +1 -1
  15. package/public/assets/{_app.models-DPSKIyeq.js → _app.models-BJi-RGUO.js} +6 -6
  16. package/public/assets/{_app.settings-15ZMiM5O.js → _app.settings-C1gB4w7c.js} +1 -1
  17. package/public/assets/_app.usage-B0iHKzmI.js +163 -0
  18. package/public/assets/catalog-DVRPlBJJ.js +1 -0
  19. package/public/assets/{index-DnNTc35a.js → index--dA3FFSN.js} +2 -2
  20. package/public/assets/{login-DNvWG7Nv.js → login-D-FsWNX8.js} +1 -1
  21. package/public/assets/trash-2-Cep_aVLx.js +1 -0
  22. package/public/favicon.svg +25 -0
  23. package/public/index.html +3 -1
  24. package/public/assets/_app.accounts-e7Mbaxdc.js +0 -47
  25. package/public/assets/_app.index-CxunMQ_m.js +0 -57
  26. package/public/assets/_app.usage-BBffCDCD.js +0 -163
  27. package/public/assets/catalog-DmrbfYQe.js +0 -1
package/README.md CHANGED
@@ -77,7 +77,7 @@ Then set up and start:
77
77
  ```bash
78
78
  omni db migrate # create the database
79
79
  omni admin set-password # prompts; the console signs in with this
80
- omni start # serves the API and the console on 127.0.0.1:8787
80
+ omni start # serves the API and the console on 127.0.0.1:9000
81
81
  ```
82
82
 
83
83
  Connect an account. The CLI prints a URL to open, and waits:
@@ -103,14 +103,14 @@ omni keys create --label laptop
103
103
  Now use it:
104
104
 
105
105
  ```bash
106
- curl http://127.0.0.1:8787/v1/chat/completions \
106
+ curl http://127.0.0.1:9000/v1/chat/completions \
107
107
  -H 'content-type: application/json' \
108
108
  -H 'authorization: Bearer <gateway-key>' \
109
109
  -d '{"model": "fast", "messages": [{"role": "user", "content": "Hello"}]}'
110
110
  ```
111
111
 
112
112
  Everything above is also available in the browser at
113
- `http://127.0.0.1:8787`, which walks the same steps.
113
+ `http://127.0.0.1:9000`, which walks the same steps.
114
114
 
115
115
  ## Using it from a client
116
116
 
@@ -132,7 +132,7 @@ Ask for one of your virtual models by name. A bare provider model
132
132
  (`claude-sonnet-5`, `gpt-5`) also works if an account can serve it.
133
133
 
134
134
  Most tools that accept a custom base URL work unchanged: set it to
135
- `http://127.0.0.1:8787` and use a gateway key where the provider key goes.
135
+ `http://127.0.0.1:9000` and use a gateway key where the provider key goes.
136
136
 
137
137
  ## The CLI
138
138
 
@@ -186,7 +186,7 @@ Configuration is environment variables, read from the installation's `.env`:
186
186
  | --- | --- | --- | --- |
187
187
  | `OMNI_ENCRYPTION_KEY` | Yes | — | Encrypts provider credentials at rest; 16 characters minimum |
188
188
  | `OMNI_HOST` | No | `127.0.0.1` | Listener host |
189
- | `OMNI_PORT` | No | `8787` | Listener port |
189
+ | `OMNI_PORT` | No | `9000` | Listener port |
190
190
  | `OMNI_DB_PATH` | No | `./omnigateway.db` | SQLite database path |
191
191
  | `OMNI_BASE_URL` | No | derived from host and port | Public origin for OAuth callbacks; set this behind a reverse proxy |
192
192
  | `OMNI_STATIC_DIR` | No | the console shipped with the server | Serve a different console build |
@@ -203,13 +203,13 @@ client-identity overrides.
203
203
  ```bash
204
204
  docker build -t omnigateway .
205
205
  docker run --rm \
206
- -p 8787:8787 \
206
+ -p 9000:9000 \
207
207
  -e OMNI_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
208
208
  -v omnigateway-data:/data \
209
209
  omnigateway
210
210
  ```
211
211
 
212
- The container listens on `0.0.0.0:8787` and keeps its database at
212
+ The container listens on `0.0.0.0:9000` and keeps its database at
213
213
  `/data/omnigateway.db`. Note that **the image builds the gateway only**: it
214
214
  serves the APIs and returns 404 for the console. Use the CLI or the control API
215
215
  against it, or install the npm package if you want the console.
@@ -258,7 +258,7 @@ cd omnigateway
258
258
  bun install
259
259
  cp .env.example .env # then set OMNI_ENCRYPTION_KEY
260
260
  bun run build:dashboard # the gateway serves this build
261
- bun run dev # gateway on 8787, with file watching
261
+ bun run dev # gateway on 9000, with file watching
262
262
  ```
263
263
 
264
264
  Releases are published to npm from a `v*` tag, with provenance attesting which
package/bin/omni.js CHANGED
@@ -159,7 +159,7 @@ function loadConfig(env) {
159
159
  throw new Error(`OMNI_ENCRYPTION_KEY must be set to at least ${MIN_KEY_LENGTH} characters. ` + "Generate one with: openssl rand -base64 32");
160
160
  }
161
161
  const host = optionalText(env.OMNI_HOST, "127.0.0.1");
162
- const rawPort = env.OMNI_PORT ?? "8787";
162
+ const rawPort = env.OMNI_PORT ?? "9000";
163
163
  const port = Number(rawPort);
164
164
  if (!DECIMAL_INTEGER.test(rawPort) || !Number.isInteger(port) || port < 1 || port > 65535) {
165
165
  throw new Error(`OMNI_PORT must be an integer between 1 and 65535, got "${rawPort}"`);
@@ -16415,25 +16415,104 @@ function requireDimension(grain, dimension) {
16415
16415
  }
16416
16416
 
16417
16417
  // packages/control/src/credentials.ts
16418
+ function summarizeCredential(credential) {
16419
+ return {
16420
+ id: credential.id,
16421
+ provider: credential.provider,
16422
+ label: credential.label,
16423
+ authType: credential.authType,
16424
+ enabled: credential.enabled,
16425
+ tier: credential.tier,
16426
+ weight: credential.weight,
16427
+ expiresAt: credential.expiresAt,
16428
+ accountEmail: credential.accountEmail,
16429
+ providerData: credential.providerData,
16430
+ disabledReason: credential.disabledReason,
16431
+ disabledAt: credential.disabledAt,
16432
+ hasRefreshToken: credential.hasRefreshToken,
16433
+ createdAt: credential.createdAt,
16434
+ updatedAt: credential.updatedAt
16435
+ };
16436
+ }
16418
16437
  async function listCredentials(store) {
16419
- const credentials = await store.credentials.list();
16420
- return credentials.map((c) => ({
16421
- id: c.id,
16422
- provider: c.provider,
16423
- label: c.label,
16424
- authType: c.authType,
16425
- enabled: c.enabled,
16426
- tier: c.tier,
16427
- weight: c.weight,
16428
- expiresAt: c.expiresAt,
16429
- accountEmail: c.accountEmail,
16430
- providerData: c.providerData,
16431
- disabledReason: c.disabledReason,
16432
- disabledAt: c.disabledAt,
16433
- hasRefreshToken: c.hasRefreshToken,
16434
- createdAt: c.createdAt,
16435
- updatedAt: c.updatedAt
16436
- }));
16438
+ return (await store.credentials.list()).map(summarizeCredential);
16439
+ }
16440
+ async function getCredential(store, id) {
16441
+ const credential = await store.credentials.get(id);
16442
+ if (credential === null)
16443
+ throw new GatewayError("BAD_REQUEST", "no such credential");
16444
+ return summarizeCredential(credential);
16445
+ }
16446
+ async function createApiKeyCredential(store, input) {
16447
+ const provider = parseOrThrow(providerIdSchema, input.provider);
16448
+ if (typeof input.apiKey !== "string" || input.apiKey.trim().length === 0) {
16449
+ throw new GatewayError("BAD_REQUEST", "apiKey: must not be empty");
16450
+ }
16451
+ if (input.label !== undefined && typeof input.label !== "string") {
16452
+ throw new GatewayError("BAD_REQUEST", "label: must be a string");
16453
+ }
16454
+ const label = input.label?.trim() || `${provider} api key`;
16455
+ const created = await store.credentials.create({
16456
+ id: crypto.randomUUID(),
16457
+ provider,
16458
+ label,
16459
+ authType: "apiKey",
16460
+ enabled: true,
16461
+ tier: 1,
16462
+ weight: 1,
16463
+ expiresAt: null,
16464
+ accountEmail: null,
16465
+ providerData: {},
16466
+ disabledReason: null,
16467
+ disabledAt: null,
16468
+ accessToken: null,
16469
+ refreshToken: null,
16470
+ apiKey: input.apiKey,
16471
+ idToken: null
16472
+ });
16473
+ return summarizeCredential(created);
16474
+ }
16475
+ async function refreshCredential(deps, id) {
16476
+ const credential = await deps.store.credentials.get(id);
16477
+ if (credential === null)
16478
+ throw new GatewayError("BAD_REQUEST", "no such credential");
16479
+ if (credential.authType !== "oauth") {
16480
+ throw new GatewayError("BAD_REQUEST", `credential "${id}" is an api key and has nothing to refresh`);
16481
+ }
16482
+ await deps.refresh(credential);
16483
+ return getCredential(deps.store, id);
16484
+ }
16485
+ async function credentialHealth(store) {
16486
+ const [health, quota] = await Promise.all([
16487
+ store.credentials.listHealth(),
16488
+ store.credentials.listQuota()
16489
+ ]);
16490
+ return { health, quota };
16491
+ }
16492
+ async function credentialStatus(store, options) {
16493
+ const [credentials, quota, adminConfigured] = await Promise.all([
16494
+ listCredentials(store),
16495
+ store.credentials.listQuota(),
16496
+ createAdminAuth(store, { now: options.now, sessionTtlMs: 0 }).isConfigured()
16497
+ ]);
16498
+ const byCredential = new Map;
16499
+ for (const row of quota) {
16500
+ const rows = byCredential.get(row.credentialId);
16501
+ if (rows === undefined)
16502
+ byCredential.set(row.credentialId, [row]);
16503
+ else
16504
+ rows.push(row);
16505
+ }
16506
+ return {
16507
+ adminConfigured,
16508
+ credentials: credentials.map(({ id, provider, label, enabled }) => ({
16509
+ id,
16510
+ provider,
16511
+ label,
16512
+ enabled,
16513
+ quota: byCredential.get(id) ?? []
16514
+ }))
16515
+ };
16437
16516
  }
16438
16517
  async function patchCredential(deps, id, input) {
16439
16518
  const patch = parseOrThrow(credentialPatchSchema, input);
@@ -16461,7 +16540,7 @@ function healthKey(credentialId, model) {
16461
16540
  }
16462
16541
  async function buildSnapshot(store, now) {
16463
16542
  const [credentials, healthRows, quotaRows, models, settings] = await Promise.all([
16464
- store.credentials.list(),
16543
+ store.credentials.listRouting(),
16465
16544
  store.credentials.listHealth(),
16466
16545
  store.credentials.listQuota(),
16467
16546
  store.config.listModels(),
@@ -16775,7 +16854,7 @@ var DEFAULT_SETTINGS = {
16775
16854
  // packages/store/src/sqlite/config.ts
16776
16855
  var SETTINGS_KEY = "settings";
16777
16856
  var ADMIN_HASH_KEY = "adminPasswordHash";
16778
- function createConfigRepo(db) {
16857
+ function createConfigRepo(db, emit2 = () => {}) {
16779
16858
  const readRaw = (key) => db.query("SELECT value FROM settings WHERE key = ?").get(key)?.value ?? null;
16780
16859
  const writeRaw = (key, value) => {
16781
16860
  db.run("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value", [key, value]);
@@ -16795,9 +16874,11 @@ function createConfigRepo(db) {
16795
16874
  targets = excluded.targets,
16796
16875
  strategy = excluded.strategy,
16797
16876
  is_alias = excluded.is_alias`, [model.id, JSON.stringify(model.targets), model.strategy, model.isAlias ? 1 : 0]);
16877
+ emit2({ type: "modelsChanged" });
16798
16878
  },
16799
16879
  async removeModel(id) {
16800
16880
  db.run("DELETE FROM virtual_models WHERE id = ?", [id]);
16881
+ emit2({ type: "modelsChanged" });
16801
16882
  },
16802
16883
  async getSettings() {
16803
16884
  const raw = readRaw(SETTINGS_KEY);
@@ -16818,6 +16899,7 @@ function createConfigRepo(db) {
16818
16899
  weights: { ...current.weights, ...patch.weights }
16819
16900
  };
16820
16901
  writeRaw(SETTINGS_KEY, JSON.stringify(next));
16902
+ emit2({ type: "settingsChanged" });
16821
16903
  return next;
16822
16904
  },
16823
16905
  async getAdminPasswordHash() {
@@ -16833,8 +16915,40 @@ function createConfigRepo(db) {
16833
16915
  };
16834
16916
  }
16835
16917
  // packages/store/src/sqlite/credentials.ts
16836
- function createCredentialRepo(db, key) {
16837
- const view = (row) => ({
16918
+ function createCredentialRepo(db, key, emit2 = () => {}) {
16919
+ const open = async (v) => v === null ? null : decrypt(key, v);
16920
+ const secretsFrom = async (row) => ({
16921
+ accessToken: await open(row.access_token),
16922
+ refreshToken: await open(row.refresh_token),
16923
+ apiKey: await open(row.api_key),
16924
+ idToken: await open(row.id_token)
16925
+ });
16926
+ const requiredRow = (row, id) => {
16927
+ if (row === null)
16928
+ throw new Error(`credential ${id} no longer exists`);
16929
+ return row;
16930
+ };
16931
+ const currentSecrets = async (id) => {
16932
+ const row = requiredRow(db.query("SELECT access_token, refresh_token, api_key, id_token FROM credentials WHERE id = ?").get(id), id);
16933
+ return secretsFrom(row);
16934
+ };
16935
+ const currentInferenceSecrets = async (id, authType) => {
16936
+ if (authType === "oauth") {
16937
+ const row2 = requiredRow(db.query("SELECT access_token FROM credentials WHERE id = ?").get(id), id);
16938
+ return { accessToken: await open(row2.access_token), apiKey: null };
16939
+ }
16940
+ const row = requiredRow(db.query("SELECT api_key FROM credentials WHERE id = ?").get(id), id);
16941
+ return { accessToken: null, apiKey: await open(row.api_key) };
16942
+ };
16943
+ const currentRefreshSecrets = async (id) => {
16944
+ const row = requiredRow(db.query("SELECT refresh_token FROM credentials WHERE id = ?").get(id), id);
16945
+ return { refreshToken: await open(row.refresh_token) };
16946
+ };
16947
+ const currentUsageSecrets = async (id) => {
16948
+ const row = requiredRow(db.query("SELECT access_token FROM credentials WHERE id = ?").get(id), id);
16949
+ return { accessToken: await open(row.access_token) };
16950
+ };
16951
+ const view = (row, loadCurrentSecrets = false) => ({
16838
16952
  id: row.id,
16839
16953
  provider: row.provider,
16840
16954
  label: row.label,
@@ -16850,18 +16964,31 @@ function createCredentialRepo(db, key) {
16850
16964
  hasRefreshToken: row.refresh_token !== null,
16851
16965
  createdAt: row.created_at,
16852
16966
  updatedAt: row.updated_at,
16853
- secrets: async () => ({
16854
- accessToken: await open(row.access_token),
16855
- refreshToken: await open(row.refresh_token),
16856
- apiKey: await open(row.api_key),
16857
- idToken: await open(row.id_token)
16858
- })
16967
+ secrets: () => loadCurrentSecrets ? currentSecrets(row.id) : secretsFrom(row),
16968
+ openForInference: async () => {
16969
+ if (loadCurrentSecrets)
16970
+ return currentInferenceSecrets(row.id, row.auth_type);
16971
+ if (row.auth_type === "oauth") {
16972
+ return { accessToken: await open(row.access_token), apiKey: null };
16973
+ }
16974
+ return { accessToken: null, apiKey: await open(row.api_key) };
16975
+ },
16976
+ openForRefresh: async () => loadCurrentSecrets ? currentRefreshSecrets(row.id) : { refreshToken: await open(row.refresh_token) },
16977
+ openForUsage: async () => loadCurrentSecrets ? currentUsageSecrets(row.id) : { accessToken: await open(row.access_token) }
16859
16978
  });
16860
- const open = async (v) => v === null ? null : decrypt(key, v);
16861
16979
  const seal = async (v) => v === null || v === undefined ? null : encrypt(key, v);
16862
16980
  return {
16863
16981
  async list() {
16864
- return db.query("SELECT * FROM credentials ORDER BY tier, label").all().map(view);
16982
+ return db.query("SELECT * FROM credentials ORDER BY tier, label").all().map((row) => view(row));
16983
+ },
16984
+ async listRouting() {
16985
+ return db.query(`SELECT id, provider, label, auth_type, enabled, tier, weight, expires_at,
16986
+ account_email, provider_data, disabled_reason, disabled_at,
16987
+ NULL AS access_token,
16988
+ CASE WHEN refresh_token IS NULL THEN NULL ELSE 'present' END AS refresh_token,
16989
+ NULL AS api_key, NULL AS id_token, created_at, updated_at
16990
+ FROM credentials
16991
+ ORDER BY tier, label`).all().map((row) => view(row, true));
16865
16992
  },
16866
16993
  async get(id) {
16867
16994
  const row = db.query("SELECT * FROM credentials WHERE id = ?").get(id);
@@ -16894,6 +17021,7 @@ function createCredentialRepo(db, key) {
16894
17021
  now
16895
17022
  ]);
16896
17023
  const { accessToken, refreshToken, apiKey, idToken, ...meta3 } = input;
17024
+ emit2({ type: "credentialsChanged" });
16897
17025
  return {
16898
17026
  ...meta3,
16899
17027
  hasRefreshToken: refreshToken != null,
@@ -16930,6 +17058,7 @@ function createCredentialRepo(db, key) {
16930
17058
  return;
16931
17059
  put("updated_at", Date.now());
16932
17060
  db.run(`UPDATE credentials SET ${sets.join(", ")} WHERE id = ?`, [...vals, id]);
17061
+ emit2({ type: "credentialsChanged" });
16933
17062
  },
16934
17063
  async updateSecrets(id, secrets, expiresAt) {
16935
17064
  const sets = [];
@@ -16953,9 +17082,11 @@ function createCredentialRepo(db, key) {
16953
17082
  sets.push("expires_at = ?", "updated_at = ?");
16954
17083
  vals.push(expiresAt, Date.now());
16955
17084
  db.run(`UPDATE credentials SET ${sets.join(", ")} WHERE id = ?`, [...vals, id]);
17085
+ emit2({ type: "credentialsChanged" });
16956
17086
  },
16957
17087
  async remove(id) {
16958
17088
  db.run("DELETE FROM credentials WHERE id = ?", [id]);
17089
+ emit2({ type: "credentialsChanged" });
16959
17090
  },
16960
17091
  async listHealth() {
16961
17092
  return db.query("SELECT * FROM credential_health").all().map((r) => ({
@@ -16986,6 +17117,7 @@ function createCredentialRepo(db, key) {
16986
17117
  stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
16987
17118
  }
16988
17119
  })();
17120
+ emit2({ type: "healthSaved", rows });
16989
17121
  },
16990
17122
  async listQuota() {
16991
17123
  return db.query("SELECT * FROM quota_windows").all().map((r) => ({
@@ -17008,11 +17140,24 @@ function createCredentialRepo(db, key) {
17008
17140
  limit_value = excluded.limit_value,
17009
17141
  resets_at = excluded.resets_at,
17010
17142
  observed_at = excluded.observed_at`);
17143
+ const prune = db.prepare("DELETE FROM quota_windows WHERE credential_id = ? AND window_type NOT IN (SELECT value FROM json_each(?))");
17144
+ const kept = new Map;
17145
+ for (const r of rows) {
17146
+ const types2 = kept.get(r.credentialId);
17147
+ if (types2 === undefined)
17148
+ kept.set(r.credentialId, [r.windowType]);
17149
+ else
17150
+ types2.push(r.windowType);
17151
+ }
17011
17152
  db.transaction(() => {
17012
17153
  for (const r of rows) {
17013
17154
  stmt.run(r.credentialId, r.windowType, r.startsAt, r.used, r.limit, r.resetsAt, r.observedAt);
17014
17155
  }
17156
+ for (const [credentialId, types2] of kept) {
17157
+ prune.run(credentialId, JSON.stringify(types2));
17158
+ }
17015
17159
  })();
17160
+ emit2({ type: "quotaSaved", rows });
17016
17161
  }
17017
17162
  };
17018
17163
  }
@@ -17485,11 +17630,26 @@ function createUsageRepo(db) {
17485
17630
  // packages/store/src/sqlite/store.ts
17486
17631
  async function createStore(opts) {
17487
17632
  const db = openDb(opts.path);
17633
+ const listeners = new Set;
17634
+ const emit2 = (change) => {
17635
+ for (const listener of listeners) {
17636
+ try {
17637
+ listener(change);
17638
+ } catch {}
17639
+ }
17640
+ };
17488
17641
  return {
17489
- credentials: createCredentialRepo(db, opts.encryptionKey),
17490
- config: createConfigRepo(db),
17642
+ credentials: createCredentialRepo(db, opts.encryptionKey, emit2),
17643
+ config: createConfigRepo(db, emit2),
17491
17644
  keys: createKeyRepo(db),
17492
17645
  usage: createUsageRepo(db),
17646
+ routing: {
17647
+ version: () => db.query("PRAGMA data_version").get()?.data_version ?? 0,
17648
+ subscribe(listener) {
17649
+ listeners.add(listener);
17650
+ return () => listeners.delete(listener);
17651
+ }
17652
+ },
17493
17653
  close: () => db.close()
17494
17654
  };
17495
17655
  }
@@ -17725,14 +17885,34 @@ function toResult3(token, fallbackRefresh, deps, fallbackAccountId = null) {
17725
17885
  providerData: { accountId: claims.accountId ?? fallbackAccountId }
17726
17886
  };
17727
17887
  }
17888
+ function windowTypeOf(value, fallback) {
17889
+ const record2 = recordOf(value);
17890
+ if (record2 === null)
17891
+ return fallback;
17892
+ const seconds = numberOf(record2, [
17893
+ "limit_window_seconds",
17894
+ "limitWindowSeconds",
17895
+ "window_seconds",
17896
+ "windowSeconds"
17897
+ ]);
17898
+ if (seconds === null || seconds <= 0)
17899
+ return fallback;
17900
+ if (seconds <= 6 * 60 * 60)
17901
+ return "fiveHour";
17902
+ if (seconds <= 36 * 60 * 60)
17903
+ return "daily";
17904
+ return "weekly";
17905
+ }
17728
17906
  function parseOpenAIUsage(value, now) {
17729
17907
  const root = recordOf(value);
17730
17908
  if (root === null)
17731
17909
  return null;
17732
17910
  const rateLimit = nestedOf(root, ["rate_limit", "rateLimit"]) ?? root;
17911
+ const primary = rateLimit.primary_window ?? rateLimit.primaryWindow;
17912
+ const secondary = rateLimit.secondary_window ?? rateLimit.secondaryWindow;
17733
17913
  return reportFrom([
17734
- windowFrom(rateLimit.primary_window ?? rateLimit.primaryWindow, "fiveHour", now),
17735
- windowFrom(rateLimit.secondary_window ?? rateLimit.secondaryWindow, "weekly", now)
17914
+ windowFrom(primary, windowTypeOf(primary, "fiveHour"), now),
17915
+ windowFrom(secondary, windowTypeOf(secondary, "weekly"), now)
17736
17916
  ]);
17737
17917
  }
17738
17918
  var openaiOAuth = {
@@ -17797,7 +17977,7 @@ var OAUTH_PROVIDERS = {
17797
17977
  function createRefresher(deps) {
17798
17978
  const inFlight = new Map;
17799
17979
  async function run(credential) {
17800
- const secrets = await credential.secrets();
17980
+ const secrets = await credential.openForRefresh();
17801
17981
  if (secrets.refreshToken === null) {
17802
17982
  throw new GatewayError("AUTH", `credential ${credential.id} has no refresh token`);
17803
17983
  }
@@ -18134,10 +18314,7 @@ var credentialsList = {
18134
18314
  }
18135
18315
  };
18136
18316
  async function findCredential(ctx, id) {
18137
- const credential = await (await ctx.store()).credentials.get(id) ?? null;
18138
- if (credential === null)
18139
- throw new CliError(`no credential "${id}"`);
18140
- return credential;
18317
+ return getCredential(await ctx.store(), id);
18141
18318
  }
18142
18319
  var credentialsShow = {
18143
18320
  usage: "credentials show <id>",
@@ -18236,12 +18413,7 @@ var credentialsRefresh = {
18236
18413
  async run(args, { ctx, writer }) {
18237
18414
  const id = requirePositional(args, 0, "credential id");
18238
18415
  const store = await ctx.store();
18239
- const credential = await store.credentials.get(id);
18240
- if (credential === null)
18241
- throw new CliError(`no credential "${id}"`);
18242
- if (credential.authType !== "oauth") {
18243
- throw new CliError(`credential "${id}" is an api key and has nothing to refresh`);
18244
- }
18416
+ const credential = await findCredential(ctx, id);
18245
18417
  const refresh = createRefresher({
18246
18418
  store,
18247
18419
  providers: OAUTH_PROVIDERS,
@@ -18249,9 +18421,8 @@ var credentialsRefresh = {
18249
18421
  now: ctx.now
18250
18422
  });
18251
18423
  note(ctx, writer, `refreshing ${credential.provider} credential ${id}\u2026`);
18252
- await refresh(credential);
18253
- const updated = await store.credentials.get(id);
18254
- emit(ctx, writer, { id, expiresAt: updated?.expiresAt ?? null }, () => `${id} refreshed; expires ${formatTime(updated?.expiresAt ?? null)}`);
18424
+ const updated = await refreshCredential({ store, refresh }, id);
18425
+ emit(ctx, writer, { id, expiresAt: updated.expiresAt }, () => `${id} refreshed; expires ${formatTime(updated.expiresAt)}`);
18255
18426
  }
18256
18427
  };
18257
18428
  var credentialsAddKey = {
@@ -18266,27 +18437,12 @@ var credentialsAddKey = {
18266
18437
  const key = await prompt.secret(`${providerId} API key: `);
18267
18438
  if (key.length === 0)
18268
18439
  throw new CliError("no API key given");
18269
- const store = await ctx.store();
18270
- const id = crypto.randomUUID();
18271
- await store.credentials.create({
18272
- id,
18440
+ const created = await createApiKeyCredential(await ctx.store(), {
18273
18441
  provider: providerId,
18274
- label: stringFlag(args.values, "label") ?? `${providerId} api key`,
18275
- authType: "apiKey",
18276
- enabled: true,
18277
- tier: 1,
18278
- weight: 1,
18279
- expiresAt: null,
18280
- accountEmail: null,
18281
- providerData: {},
18282
- disabledReason: null,
18283
- disabledAt: null,
18284
- accessToken: null,
18285
- refreshToken: null,
18286
18442
  apiKey: key,
18287
- idToken: null
18443
+ label: stringFlag(args.values, "label")
18288
18444
  });
18289
- emit(ctx, writer, { id, provider: providerId }, () => `stored ${providerId} api key as ${id}`);
18445
+ emit(ctx, writer, { id: created.id, provider: created.provider }, () => `stored ${created.provider} api key as ${created.id}`);
18290
18446
  }
18291
18447
  };
18292
18448
  var credentialsHealth = {
@@ -18295,8 +18451,8 @@ var credentialsHealth = {
18295
18451
  options: { all: { type: "boolean" } },
18296
18452
  async run(args, { ctx, writer }) {
18297
18453
  const store = await ctx.store();
18298
- const [rows, credentials] = await Promise.all([
18299
- store.credentials.listHealth(),
18454
+ const [{ health: rows }, credentials] = await Promise.all([
18455
+ credentialHealth(store),
18300
18456
  listCredentials(store)
18301
18457
  ]);
18302
18458
  const labels = new Map(credentials.map((c) => [c.id, c.label]));
@@ -19098,24 +19254,29 @@ var settingsSet = {
19098
19254
  };
19099
19255
 
19100
19256
  // apps/cli/src/commands/status.ts
19101
- function tightest(windows) {
19102
- let worst = null;
19103
- for (const window of windows) {
19104
- if (window.limit === null || window.limit <= 0)
19105
- continue;
19106
- if (worst === null || window.used / window.limit > worst.used / (worst.limit ?? 1)) {
19107
- worst = window;
19108
- }
19109
- }
19110
- return worst;
19257
+ var WINDOW_ORDER = {
19258
+ fiveHour: 0,
19259
+ daily: 1,
19260
+ weekly: 2
19261
+ };
19262
+ var WINDOW_LABEL = {
19263
+ fiveHour: "5h",
19264
+ daily: "24h",
19265
+ weekly: "7d"
19266
+ };
19267
+ function reportedWindows(windows) {
19268
+ return windows.filter((window) => window.limit !== null && window.limit > 0).sort((a, b) => WINDOW_ORDER[a.windowType] - WINDOW_ORDER[b.windowType]);
19111
19269
  }
19112
- function quotaCell(ctx, windows, headroom, now) {
19113
- const window = tightest(windows);
19114
- if (window === null || window.limit === null)
19270
+ function quotaCell(ctx, windows, now) {
19271
+ const reported = reportedWindows(windows);
19272
+ if (reported.length === 0)
19115
19273
  return paint(ctx, "dim", "unknown");
19116
- const used = Math.round(window.used / window.limit * 100);
19117
- const age = formatAge(window.observedAt, now);
19118
- return `${state(ctx, headroom >= 0.5, `${used}% used`)} ${paint(ctx, "dim", `(${window.windowType}, ${age} ago)`)}`;
19274
+ const parts = reported.map((window) => {
19275
+ const used = Math.round(window.used / window.limit * 100);
19276
+ return state(ctx, used < 90, `${WINDOW_LABEL[window.windowType]} ${used}%`);
19277
+ });
19278
+ const observedAt = Math.min(...reported.map((window) => window.observedAt));
19279
+ return `${parts.join(" \xB7 ")} ${paint(ctx, "dim", `(${formatAge(observedAt, now)} ago)`)}`;
19119
19280
  }
19120
19281
  var status2 = {
19121
19282
  usage: "status",
@@ -19130,31 +19291,11 @@ var status2 = {
19130
19291
  } catch (error51) {
19131
19292
  storeError = error51 instanceof Error ? error51.message : "could not open the database";
19132
19293
  }
19133
- const credentials = store === null ? [] : await store.credentials.list();
19134
- const quotaRows = store === null ? [] : await store.credentials.listQuota();
19135
- const settings = store === null ? null : await store.config.getSettings();
19136
- const configured = store === null ? false : await createAdminAuth(store, {
19137
- now: ctx.now,
19138
- sessionTtlMs: 0
19139
- }).isConfigured();
19140
- const byCredential = new Map;
19141
- for (const row of quotaRows) {
19142
- const list = byCredential.get(row.credentialId);
19143
- if (list === undefined)
19144
- byCredential.set(row.credentialId, [row]);
19145
- else
19146
- list.push(row);
19147
- }
19294
+ const persistent = store === null ? { adminConfigured: false, credentials: [] } : await credentialStatus(store, { now: ctx.now });
19295
+ const { adminConfigured: configured, credentials } = persistent;
19148
19296
  const data = {
19149
19297
  process: process3,
19150
- adminConfigured: configured,
19151
- credentials: credentials.map((credential) => ({
19152
- id: credential.id,
19153
- provider: credential.provider,
19154
- label: credential.label,
19155
- enabled: credential.enabled,
19156
- quota: byCredential.get(credential.id) ?? []
19157
- })),
19298
+ ...persistent,
19158
19299
  storeError
19159
19300
  };
19160
19301
  emit(ctx, writer, data, () => {
@@ -19176,16 +19317,12 @@ ${state(ctx, false, storeError)}`;
19176
19317
 
19177
19318
  no credentials; add one with: omni connect <provider>`;
19178
19319
  }
19179
- const rows = credentials.map((credential) => {
19180
- const windows = byCredential.get(credential.id) ?? [];
19181
- const headroom = quotaHeadroom(credential, windows, ctx.now(), settings?.quotaPollIntervalMs ?? 0);
19182
- return [
19183
- credential.label,
19184
- provider(ctx, credential.provider),
19185
- state(ctx, credential.enabled, credential.enabled ? "enabled" : "disabled"),
19186
- quotaCell(ctx, windows, headroom, ctx.now())
19187
- ];
19188
- });
19320
+ const rows = credentials.map((credential) => [
19321
+ credential.label,
19322
+ provider(ctx, credential.provider),
19323
+ state(ctx, credential.enabled, credential.enabled ? "enabled" : "disabled"),
19324
+ quotaCell(ctx, credential.quota, ctx.now())
19325
+ ]);
19189
19326
  return `${header}
19190
19327
 
19191
19328
  ${table([{ header: "ACCOUNT" }, { header: "PROVIDER" }, { header: "STATE" }, { header: "QUOTA" }], rows)}`;