omnigateway 0.11.0 → 0.11.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.
- package/README.md +16 -17
- package/bin/omni.js +184 -35
- package/gateway.js +139 -40
- package/package.json +2 -2
- package/public/assets/{Chip-CmL-9rw7.js → Chip-77Q04h7S.js} +2 -1
- package/public/assets/{CopyValue-CTVbU0O_.js → CopyValue-DDJId-Zo.js} +1 -1
- package/public/assets/{Field-DcNu4yw-.js → Field-56pmSGrh.js} +1 -1
- package/public/assets/{Lamp-DtHKJH6P.js → Lamp-Drd1fFwQ.js} +7 -7
- package/public/assets/{Panel-BlNr9j6-.js → Panel-BV18WuL1.js} +1 -0
- package/public/assets/Rack-D3iFlR6e.js +482 -0
- package/public/assets/{Readout-D8Ofl3Gz.js → Readout-Dxwka1JI.js} +1 -1
- package/public/assets/RequestTable-I395emqH.js +11 -0
- package/public/assets/SummaryDeck-pAZ93jBW.js +15 -0
- package/public/assets/{Toggle-ChjN-tw1.js → Toggle-y17mtXCV.js} +1 -1
- package/public/assets/{TokenBreakdown-BPoXM3JM.js → TokenBreakdown-BIwuEVs8.js} +2 -1
- package/public/assets/{WindowChart-BPEsEGFL.js → WindowChart-DWZtz4Ps.js} +2 -2
- package/public/assets/{_app-CWXzHdhc.js → _app-0fHLZIC7.js} +1 -1
- package/public/assets/_app.accounts-DBlImnPw.js +59 -0
- package/public/assets/{_app.console-Bkdallia.js → _app.console-in_iVnep.js} +2 -2
- package/public/assets/{_app.database-DxqN1395.js → _app.database-CPj-Juag.js} +4 -4
- package/public/assets/_app.index-DGz9njXx.js +67 -0
- package/public/assets/_app.keys-C-Fk39-U.js +92 -0
- package/public/assets/{_app.logs-DGQD8ZAq.js → _app.logs-C7BhBRjg.js} +3 -3
- package/public/assets/{_app.models-D6SV_GVk.js → _app.models-BuiRrOrz.js} +4 -4
- package/public/assets/{_app.plugins._pluginId-CJ7ObXXe.js → _app.plugins._pluginId-Bx1LgcDj.js} +1 -1
- package/public/assets/_app.settings-s6K98CA2.js +88 -0
- package/public/assets/_app.usage-Bp45JtBe.js +76 -0
- package/public/assets/{chevron-right-BfnK63GB.js → chevron-right-BE-3EQR5.js} +1 -1
- package/public/assets/client-Bl_FA5jI.js +39 -0
- package/public/assets/index-nvJxJ9Gp.js +13 -0
- package/public/assets/{login-CrJK9wRA.js → login-Qc9iIepG.js} +2 -2
- package/public/assets/plus-DhyoE7aH.js +1 -0
- package/public/assets/{preload-helper-BknjH67x.js → preload-helper-DYShfVEU.js} +1 -1
- package/public/assets/{shared-oTMOLWMT.js → shared-ChaZCaBz.js} +12 -12
- package/public/assets/{trash-2-CSkf6ME_.js → trash-2-zGGJ7VE_.js} +1 -1
- package/public/index.html +5 -5
- package/public/assets/Rack-_A_3zqxC.js +0 -476
- package/public/assets/RequestTable-Cokk7wKP.js +0 -11
- package/public/assets/SummaryDeck-C0IbqnLz.js +0 -15
- package/public/assets/_app.accounts-DKmxz7Tl.js +0 -59
- package/public/assets/_app.index-BUqor-qH.js +0 -62
- package/public/assets/_app.keys-DMCPEj1g.js +0 -82
- package/public/assets/_app.settings-BVimpngR.js +0 -88
- package/public/assets/_app.usage-Y2Nkaw0t.js +0 -76
- package/public/assets/client-WgQ5A2PD.js +0 -39
- package/public/assets/index-BTE8vyBz.js +0 -13
- package/public/assets/plus-Cv51cK5k.js +0 -1
package/README.md
CHANGED
|
@@ -174,6 +174,22 @@ pair is unlimited. Windows slide, `tokens` and `spend` are debited on
|
|
|
174
174
|
completion, and limits are editable afterwards with `omni keys limits`; the
|
|
175
175
|
semantics are in [docs/operations.md](docs/operations.md#key-limits).
|
|
176
176
|
|
|
177
|
+
A key can also carry an expiry, which is optional and off by default — a key
|
|
178
|
+
with none works until it is revoked, and no upgrade ever attaches one:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
omni keys create --label contractor --expires 2027-01-01
|
|
182
|
+
omni keys expiry <id> --at 2027-06-01T12:00:00Z # move it
|
|
183
|
+
omni keys expiry <id> --never # take it away
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Past the instant, every request the key makes is refused with the same answer a
|
|
187
|
+
revoked or unknown key gets — deliberately indistinguishable. Unlike
|
|
188
|
+
`omni keys revoke` this is reversible: a date already behind the clock is
|
|
189
|
+
accepted and means "stop it now", and `--never` brings the key back. Expired
|
|
190
|
+
keys stay listed, with their own state in `omni keys list` and on the console's
|
|
191
|
+
keys board, so a client that stopped working can be found rather than guessed at.
|
|
192
|
+
|
|
177
193
|
Now use it:
|
|
178
194
|
|
|
179
195
|
```bash
|
|
@@ -265,23 +281,6 @@ Several replicas behind a load balancer are **cluster mode** on Postgres and
|
|
|
265
281
|
Redis. All of it, with the nginx block and the Kubernetes base, is in
|
|
266
282
|
[docs/deploying.md](docs/deploying.md).
|
|
267
283
|
|
|
268
|
-
### Upgrading past v0.10.3 — action required
|
|
269
|
-
|
|
270
|
-
The first release after v0.10.3 drops two columns from `credential_health`
|
|
271
|
-
that every earlier gateway writes on **every successful request**. The moment
|
|
272
|
-
the new version migrates the database, every older gateway still running fails
|
|
273
|
-
each request *after* the upstream call has completed and been billed: the
|
|
274
|
-
client gets its content and then an aborted connection with no terminal frame.
|
|
275
|
-
|
|
276
|
-
- **Cluster mode:** a rolling deploy does not protect you — the surviving old
|
|
277
|
-
replica is the one that breaks. Scale to **one replica** before deploying
|
|
278
|
-
this release, then scale back up, or accept that every old replica serves
|
|
279
|
-
only failures until it is replaced.
|
|
280
|
-
- **Single process (SQLite):** the upgrade itself is safe; the rollback is not.
|
|
281
|
-
- **Rollback is unsafe on both.** An older version cannot re-add the columns
|
|
282
|
-
and fails the same way against the new schema. Take a snapshot **before** the
|
|
283
|
-
deploy; restoring it is the only way back.
|
|
284
|
-
|
|
285
284
|
## Configuration
|
|
286
285
|
|
|
287
286
|
Configuration is environment variables, read from the installation's `.env`:
|
package/bin/omni.js
CHANGED
|
@@ -75,7 +75,10 @@ function note(ctx, writer, message) {
|
|
|
75
75
|
function formatTime(at) {
|
|
76
76
|
if (at === null || !Number.isFinite(at))
|
|
77
77
|
return "\u2014";
|
|
78
|
-
|
|
78
|
+
const date = new Date(at);
|
|
79
|
+
if (Number.isNaN(date.getTime()))
|
|
80
|
+
return "out of range";
|
|
81
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
79
82
|
}
|
|
80
83
|
function formatAge(from, now) {
|
|
81
84
|
if (from === null || !Number.isFinite(from))
|
|
@@ -20124,13 +20127,34 @@ CREATE TRIGGER credential_health_config_version_del
|
|
|
20124
20127
|
FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
|
|
20125
20128
|
`;
|
|
20126
20129
|
|
|
20130
|
+
// packages/store/src/postgres/migrations/003_key_expiry.sql
|
|
20131
|
+
var _003_key_expiry_default = `-- The Postgres half of the sqlite migration of the same name. \`BIGINT\`, like
|
|
20132
|
+
-- every other epoch-ms column in this schema, and read back through \`numOrNull\`.
|
|
20133
|
+
--
|
|
20134
|
+
-- Nullable rather than \`NOT NULL DEFAULT\`: the column is a deadline, and every
|
|
20135
|
+
-- finite default is a date on which somebody's client stops working. NULL is
|
|
20136
|
+
-- the only value that says nothing was decided, and every reader maps it to
|
|
20137
|
+
-- "never expires".
|
|
20138
|
+
--
|
|
20139
|
+
-- No backfill, deliberately. Every key on every existing installation was
|
|
20140
|
+
-- minted on the promise that it works until revoked, so adding the column and
|
|
20141
|
+
-- stopping is what keeps that promise. The absence of an \`UPDATE\` here is the
|
|
20142
|
+
-- substance of the migration, not an omission.
|
|
20143
|
+
--
|
|
20144
|
+
-- Safe to apply under a rolling deploy, unlike migration 2: this adds a
|
|
20145
|
+
-- nullable column and drops nothing, so a replica running the previous image
|
|
20146
|
+
-- keeps serving \u2014 \`SELECT *\` gains a field its mapper ignores.
|
|
20147
|
+
ALTER TABLE api_keys ADD COLUMN expires_at BIGINT;
|
|
20148
|
+
`;
|
|
20149
|
+
|
|
20127
20150
|
// packages/store/src/postgres/db.ts
|
|
20128
20151
|
var num = (v) => Number(v);
|
|
20129
20152
|
var numOrNull = (v) => v === null ? null : Number(v);
|
|
20130
20153
|
var MIGRATION_LOCK = 7140641;
|
|
20131
20154
|
var MIGRATIONS = [
|
|
20132
20155
|
{ id: 1, sql: _001_init_default },
|
|
20133
|
-
{ id: 2, sql: _002_health_measurements_default }
|
|
20156
|
+
{ id: 2, sql: _002_health_measurements_default },
|
|
20157
|
+
{ id: 3, sql: _003_key_expiry_default }
|
|
20134
20158
|
];
|
|
20135
20159
|
async function openPg(url2) {
|
|
20136
20160
|
const sql = new SQL({ url: url2 });
|
|
@@ -20328,6 +20352,9 @@ function resolvePin(target, accounts) {
|
|
|
20328
20352
|
return;
|
|
20329
20353
|
return accounts.find((account) => servesTarget(target, account));
|
|
20330
20354
|
}
|
|
20355
|
+
var keyUsable = (key, now) => key.revokedAt === null && (key.expiresAt === null || key.expiresAt > now);
|
|
20356
|
+
var credentialPastExpiry = (credential, now) => credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now;
|
|
20357
|
+
var credentialExpired = (credential, now) => credentialPastExpiry(credential, now) && !credential.hasRefreshToken;
|
|
20331
20358
|
var NODE_GRACE_MS = 60000;
|
|
20332
20359
|
var DEFAULT_SETTINGS = {
|
|
20333
20360
|
weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
|
|
@@ -20807,7 +20834,8 @@ var toKey = (r, logger2) => ({
|
|
|
20807
20834
|
limits: parseLimits(r.id, r.limits, logger2),
|
|
20808
20835
|
bodyLoggingOptOut: r.body_logging_opt_out,
|
|
20809
20836
|
createdAt: num(r.created_at),
|
|
20810
|
-
revokedAt: numOrNull(r.revoked_at)
|
|
20837
|
+
revokedAt: numOrNull(r.revoked_at),
|
|
20838
|
+
expiresAt: numOrNull(r.expires_at)
|
|
20811
20839
|
});
|
|
20812
20840
|
function createKeyRepo(sql, logger2) {
|
|
20813
20841
|
const one = async (where, arg) => {
|
|
@@ -20825,8 +20853,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20825
20853
|
async create(input2) {
|
|
20826
20854
|
const now = Date.now();
|
|
20827
20855
|
await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
20828
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
20829
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL)`, [
|
|
20856
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
20857
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL,$9)`, [
|
|
20830
20858
|
input2.id,
|
|
20831
20859
|
input2.label,
|
|
20832
20860
|
input2.prefix,
|
|
@@ -20834,7 +20862,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20834
20862
|
input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
|
|
20835
20863
|
JSON.stringify(parseLimitConfig(input2.limits)),
|
|
20836
20864
|
input2.bodyLoggingOptOut,
|
|
20837
|
-
now
|
|
20865
|
+
now,
|
|
20866
|
+
input2.expiresAt
|
|
20838
20867
|
]);
|
|
20839
20868
|
return { ...input2, createdAt: now, revokedAt: null };
|
|
20840
20869
|
},
|
|
@@ -20842,8 +20871,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20842
20871
|
if (row.limits === null)
|
|
20843
20872
|
throw new Error(`api key ${row.id} has unreadable limits`);
|
|
20844
20873
|
await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
20845
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
20846
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, [
|
|
20874
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
20875
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, [
|
|
20847
20876
|
row.id,
|
|
20848
20877
|
row.label,
|
|
20849
20878
|
row.prefix,
|
|
@@ -20852,7 +20881,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20852
20881
|
JSON.stringify(parseLimitConfig(row.limits)),
|
|
20853
20882
|
row.bodyLoggingOptOut,
|
|
20854
20883
|
row.createdAt,
|
|
20855
|
-
row.revokedAt
|
|
20884
|
+
row.revokedAt,
|
|
20885
|
+
row.expiresAt
|
|
20856
20886
|
]);
|
|
20857
20887
|
},
|
|
20858
20888
|
async setLimits(id, limits) {
|
|
@@ -20867,6 +20897,9 @@ function createKeyRepo(sql, logger2) {
|
|
|
20867
20897
|
id
|
|
20868
20898
|
]);
|
|
20869
20899
|
},
|
|
20900
|
+
async setExpiry(id, expiresAt) {
|
|
20901
|
+
await sql.unsafe("UPDATE api_keys SET expires_at = $1 WHERE id = $2", [expiresAt, id]);
|
|
20902
|
+
},
|
|
20870
20903
|
async revoke(id) {
|
|
20871
20904
|
await sql.unsafe("UPDATE api_keys SET revoked_at = $1 WHERE id = $2", [Date.now(), id]);
|
|
20872
20905
|
}
|
|
@@ -22671,6 +22704,35 @@ ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
|
|
|
22671
22704
|
ALTER TABLE credential_health DROP COLUMN last_used_at;
|
|
22672
22705
|
`;
|
|
22673
22706
|
|
|
22707
|
+
// packages/store/src/sqlite/migrations/014_key_expiry.sql
|
|
22708
|
+
var _014_key_expiry_default = `-- An optional expiry on a gateway API key: an absolute epoch-ms instant after
|
|
22709
|
+
-- which the key is refused, spelled exactly as \`credentials.expires_at\` is.
|
|
22710
|
+
--
|
|
22711
|
+
-- Nullable rather than \`NOT NULL DEFAULT\`, which is the opposite of the choice
|
|
22712
|
+
-- migration 9 made for \`limits\`, and for the opposite reason. There, every
|
|
22713
|
+
-- possible value is a policy and \`{}\` is the one that means "no ceiling", so a
|
|
22714
|
+
-- default gives "unlimited" a single spelling. Here the column is a deadline,
|
|
22715
|
+
-- and no number means "no deadline" \u2014 every finite default is a date on which
|
|
22716
|
+
-- somebody's client stops working. NULL is the only value that says nothing was
|
|
22717
|
+
-- decided, so it is the column's own default and every reader maps it to
|
|
22718
|
+
-- "never expires".
|
|
22719
|
+
--
|
|
22720
|
+
-- No backfill, deliberately, and this is the whole substance of the migration.
|
|
22721
|
+
-- Every key on every existing installation was minted on the promise that it
|
|
22722
|
+
-- works until revoked; an upgrade that attaches an expiry to those rows breaks
|
|
22723
|
+
-- clients on a date the operator never chose, and the failure surfaces days
|
|
22724
|
+
-- later as an authentication error with nothing in it that names this change.
|
|
22725
|
+
-- Adding the column and stopping is what keeps that promise, so the absence of
|
|
22726
|
+
-- an \`UPDATE\` here is load-bearing rather than an omission \u2014
|
|
22727
|
+
-- \`packages/store/test/migrations.test.ts\` pins it.
|
|
22728
|
+
--
|
|
22729
|
+
-- No index. The column is read only after a row has already been found by
|
|
22730
|
+
-- \`hash\` or by \`id\`, both of which are unique, so there is no scan for an index
|
|
22731
|
+
-- to help; and nothing sweeps expired keys, because an expired key stays listed
|
|
22732
|
+
-- exactly as a revoked one does so its usage history keeps its attribution.
|
|
22733
|
+
ALTER TABLE api_keys ADD COLUMN expires_at INTEGER;
|
|
22734
|
+
`;
|
|
22735
|
+
|
|
22674
22736
|
// packages/store/src/sqlite/db.ts
|
|
22675
22737
|
var MIGRATIONS2 = [
|
|
22676
22738
|
{ id: 1, sql: _001_init_default2 },
|
|
@@ -22685,7 +22747,8 @@ var MIGRATIONS2 = [
|
|
|
22685
22747
|
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
|
|
22686
22748
|
{ id: 11, sql: _011_plugin_migrations_default },
|
|
22687
22749
|
{ id: 12, sql: _012_nodes_default },
|
|
22688
|
-
{ id: 13, sql: _013_health_measurements_default }
|
|
22750
|
+
{ id: 13, sql: _013_health_measurements_default },
|
|
22751
|
+
{ id: 14, sql: _014_key_expiry_default }
|
|
22689
22752
|
];
|
|
22690
22753
|
function statements(sql) {
|
|
22691
22754
|
return sql.split(`
|
|
@@ -22741,7 +22804,8 @@ var toKey2 = (r, logger2) => ({
|
|
|
22741
22804
|
limits: parseLimits2(r.id, r.limits, logger2),
|
|
22742
22805
|
bodyLoggingOptOut: r.body_logging_opt_out === 1,
|
|
22743
22806
|
createdAt: r.created_at,
|
|
22744
|
-
revokedAt: r.revoked_at
|
|
22807
|
+
revokedAt: r.revoked_at,
|
|
22808
|
+
expiresAt: r.expires_at
|
|
22745
22809
|
});
|
|
22746
22810
|
function createKeyRepo2(db, logger2 = noopLogger) {
|
|
22747
22811
|
return {
|
|
@@ -22759,8 +22823,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22759
22823
|
async create(input2) {
|
|
22760
22824
|
const now = Date.now();
|
|
22761
22825
|
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
22762
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
22763
|
-
VALUES (?,?,?,?,?,?,?,?,NULL)`, [
|
|
22826
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
22827
|
+
VALUES (?,?,?,?,?,?,?,?,NULL,?)`, [
|
|
22764
22828
|
input2.id,
|
|
22765
22829
|
input2.label,
|
|
22766
22830
|
input2.prefix,
|
|
@@ -22768,7 +22832,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22768
22832
|
input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
|
|
22769
22833
|
JSON.stringify(parseLimitConfig(input2.limits)),
|
|
22770
22834
|
input2.bodyLoggingOptOut ? 1 : 0,
|
|
22771
|
-
now
|
|
22835
|
+
now,
|
|
22836
|
+
input2.expiresAt
|
|
22772
22837
|
]);
|
|
22773
22838
|
return { ...input2, createdAt: now, revokedAt: null };
|
|
22774
22839
|
},
|
|
@@ -22776,8 +22841,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22776
22841
|
if (row.limits === null)
|
|
22777
22842
|
throw new Error(`api key ${row.id} has unreadable limits`);
|
|
22778
22843
|
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
22779
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
22780
|
-
VALUES (
|
|
22844
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
22845
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)`, [
|
|
22781
22846
|
row.id,
|
|
22782
22847
|
row.label,
|
|
22783
22848
|
row.prefix,
|
|
@@ -22786,7 +22851,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22786
22851
|
JSON.stringify(parseLimitConfig(row.limits)),
|
|
22787
22852
|
row.bodyLoggingOptOut ? 1 : 0,
|
|
22788
22853
|
row.createdAt,
|
|
22789
|
-
row.revokedAt
|
|
22854
|
+
row.revokedAt,
|
|
22855
|
+
row.expiresAt
|
|
22790
22856
|
]);
|
|
22791
22857
|
},
|
|
22792
22858
|
async setLimits(id, limits) {
|
|
@@ -22801,6 +22867,9 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22801
22867
|
id
|
|
22802
22868
|
]);
|
|
22803
22869
|
},
|
|
22870
|
+
async setExpiry(id, expiresAt) {
|
|
22871
|
+
db.run("UPDATE api_keys SET expires_at = ? WHERE id = ?", [expiresAt, id]);
|
|
22872
|
+
},
|
|
22804
22873
|
async revoke(id) {
|
|
22805
22874
|
db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
|
|
22806
22875
|
}
|
|
@@ -23365,6 +23434,7 @@ async function createStore(opts) {
|
|
|
23365
23434
|
importRow: (row) => handle.keys.importRow(row),
|
|
23366
23435
|
setLimits: (id, limits) => handle.keys.setLimits(id, limits),
|
|
23367
23436
|
setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
|
|
23437
|
+
setExpiry: (id, expiresAt) => handle.keys.setExpiry(id, expiresAt),
|
|
23368
23438
|
revoke: (id) => handle.keys.revoke(id)
|
|
23369
23439
|
},
|
|
23370
23440
|
usage: {
|
|
@@ -23467,7 +23537,7 @@ function createAdminAuth(store, opts) {
|
|
|
23467
23537
|
};
|
|
23468
23538
|
const keyStillValid = async (apiKeyId) => {
|
|
23469
23539
|
const key = await store.keys.get(apiKeyId);
|
|
23470
|
-
return key !== null && key.
|
|
23540
|
+
return key !== null && keyUsable(key, opts.now());
|
|
23471
23541
|
};
|
|
23472
23542
|
const applyPassword = async (password) => {
|
|
23473
23543
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
@@ -23521,7 +23591,7 @@ function createAdminAuth(store, opts) {
|
|
|
23521
23591
|
},
|
|
23522
23592
|
async loginClient(rawKey) {
|
|
23523
23593
|
const key = await store.keys.findByHash(await hashApiKey(rawKey));
|
|
23524
|
-
if (key === null || key.
|
|
23594
|
+
if (key === null || !keyUsable(key, opts.now()))
|
|
23525
23595
|
return null;
|
|
23526
23596
|
return issue2({ kind: "client", apiKeyId: key.id });
|
|
23527
23597
|
},
|
|
@@ -30996,16 +31066,19 @@ var modelSchema = exports_external.object({
|
|
|
30996
31066
|
isAlias: exports_external.boolean(),
|
|
30997
31067
|
targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
|
|
30998
31068
|
});
|
|
31069
|
+
var MAX_INSTANT = 8640000000000000;
|
|
30999
31070
|
var keyCreateSchema = exports_external.object({
|
|
31000
31071
|
label: exports_external.string().min(1).default("api key"),
|
|
31001
31072
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
|
|
31002
31073
|
limits: limitConfigSchema.default({}),
|
|
31003
|
-
bodyLoggingOptOut: exports_external.boolean().default(false)
|
|
31074
|
+
bodyLoggingOptOut: exports_external.boolean().default(false),
|
|
31075
|
+
expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable().default(null)
|
|
31004
31076
|
}).strict();
|
|
31005
31077
|
var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
|
|
31006
31078
|
var keyModelsSchema = exports_external.object({
|
|
31007
31079
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
|
|
31008
31080
|
}).strict();
|
|
31081
|
+
var keyExpirySchema = exports_external.object({ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable() }).strict();
|
|
31009
31082
|
var retentionSchema = exports_external.object({
|
|
31010
31083
|
keepLatest: exports_external.number().int().min(1).max(100),
|
|
31011
31084
|
maxAgeDays: exports_external.number().int().min(1).max(3650)
|
|
@@ -31303,7 +31376,7 @@ function eligible(input2) {
|
|
|
31303
31376
|
drop("disabled");
|
|
31304
31377
|
continue;
|
|
31305
31378
|
}
|
|
31306
|
-
if (credential
|
|
31379
|
+
if (credentialExpired(credential, now)) {
|
|
31307
31380
|
drop("expired");
|
|
31308
31381
|
continue;
|
|
31309
31382
|
}
|
|
@@ -32107,7 +32180,8 @@ async function toSummary(store, key, now) {
|
|
|
32107
32180
|
limitUsage: await readLimitUsage(store, key.id, key.limits, now),
|
|
32108
32181
|
bodyLoggingOptOut: key.bodyLoggingOptOut,
|
|
32109
32182
|
createdAt: key.createdAt,
|
|
32110
|
-
revokedAt: key.revokedAt
|
|
32183
|
+
revokedAt: key.revokedAt,
|
|
32184
|
+
expiresAt: key.expiresAt
|
|
32111
32185
|
};
|
|
32112
32186
|
}
|
|
32113
32187
|
async function listKeys(store, now = Date.now()) {
|
|
@@ -32124,7 +32198,8 @@ async function createKey(store, input2) {
|
|
|
32124
32198
|
hash: await hashApiKey(raw),
|
|
32125
32199
|
modelAllowlist: body2.modelAllowlist,
|
|
32126
32200
|
limits: body2.limits,
|
|
32127
|
-
bodyLoggingOptOut: body2.bodyLoggingOptOut
|
|
32201
|
+
bodyLoggingOptOut: body2.bodyLoggingOptOut,
|
|
32202
|
+
expiresAt: body2.expiresAt
|
|
32128
32203
|
});
|
|
32129
32204
|
return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
|
|
32130
32205
|
}
|
|
@@ -32144,6 +32219,14 @@ async function setKeyModels(store, id, input2, now = Date.now()) {
|
|
|
32144
32219
|
await store.keys.setModelAllowlist(id, body2.modelAllowlist);
|
|
32145
32220
|
return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
|
|
32146
32221
|
}
|
|
32222
|
+
async function setKeyExpiry(store, id, input2, now = Date.now()) {
|
|
32223
|
+
const body2 = parseOrThrow(keyExpirySchema, input2);
|
|
32224
|
+
const key = await store.keys.get(id);
|
|
32225
|
+
if (key === null)
|
|
32226
|
+
throw new GatewayError("BAD_REQUEST", "no such api key");
|
|
32227
|
+
await store.keys.setExpiry(id, body2.expiresAt);
|
|
32228
|
+
return toSummary(store, { ...key, expiresAt: body2.expiresAt }, now);
|
|
32229
|
+
}
|
|
32147
32230
|
async function revokeKey(store, id) {
|
|
32148
32231
|
await store.keys.revoke(id);
|
|
32149
32232
|
}
|
|
@@ -33614,6 +33697,12 @@ function readsNothing(scope) {
|
|
|
33614
33697
|
// packages/control/src/quota/history.ts
|
|
33615
33698
|
var DAY_MS3 = 24 * 60 * 60 * 1000;
|
|
33616
33699
|
var HOUR_MS3 = 3600000;
|
|
33700
|
+
var MAX_SAMPLES = 50000;
|
|
33701
|
+
var SAMPLE_QUERY_LIMIT = MAX_SAMPLES + 1;
|
|
33702
|
+
function pageSamples(samples) {
|
|
33703
|
+
const truncated = samples.length > MAX_SAMPLES;
|
|
33704
|
+
return { page: truncated ? samples.slice(0, MAX_SAMPLES) : samples, truncated };
|
|
33705
|
+
}
|
|
33617
33706
|
async function retainedSpan(deps, input2, maxSpanMs) {
|
|
33618
33707
|
const now = deps.now();
|
|
33619
33708
|
const settings = await deps.store.config.getSettings();
|
|
@@ -33660,12 +33749,14 @@ async function quotaHistory(deps, input2) {
|
|
|
33660
33749
|
deps.store.credentials.listQuotaSamples({
|
|
33661
33750
|
since,
|
|
33662
33751
|
until,
|
|
33752
|
+
limit: SAMPLE_QUERY_LIMIT,
|
|
33663
33753
|
...credentialId === undefined ? {} : { credentialId }
|
|
33664
33754
|
}),
|
|
33665
33755
|
deps.store.credentials.listQuota()
|
|
33666
33756
|
]);
|
|
33667
33757
|
const scoped = credentialId === undefined ? quota : quota.filter((w) => w.credentialId === credentialId);
|
|
33668
|
-
|
|
33758
|
+
const { page, truncated } = pageSamples(samples);
|
|
33759
|
+
return { samples: page, truncated, gatewayRates: await gatewayRatesFor(deps.store, scoped) };
|
|
33669
33760
|
}
|
|
33670
33761
|
|
|
33671
33762
|
// packages/control/src/quota/clientHistory.ts
|
|
@@ -34715,12 +34806,13 @@ var console_ = {
|
|
|
34715
34806
|
};
|
|
34716
34807
|
|
|
34717
34808
|
// apps/cli/src/commands/credentials.ts
|
|
34718
|
-
function condition(credential) {
|
|
34809
|
+
function condition(credential, now) {
|
|
34719
34810
|
if (!credential.enabled)
|
|
34720
34811
|
return { ok: false, text: credential.disabledReason ?? "disabled" };
|
|
34721
|
-
if (credential
|
|
34722
|
-
return
|
|
34723
|
-
|
|
34812
|
+
if (credentialExpired(credential, now))
|
|
34813
|
+
return { ok: false, text: "expired" };
|
|
34814
|
+
if (credentialPastExpiry(credential, now))
|
|
34815
|
+
return { ok: true, text: "expired (refreshable)" };
|
|
34724
34816
|
return { ok: true, text: "enabled" };
|
|
34725
34817
|
}
|
|
34726
34818
|
var credentialsList = {
|
|
@@ -34741,7 +34833,7 @@ var credentialsList = {
|
|
|
34741
34833
|
{ header: "STATE" },
|
|
34742
34834
|
{ header: "EXPIRES" }
|
|
34743
34835
|
], credentials.map((c) => {
|
|
34744
|
-
const status2 = condition(c);
|
|
34836
|
+
const status2 = condition(c, ctx.now());
|
|
34745
34837
|
return [
|
|
34746
34838
|
c.id,
|
|
34747
34839
|
provider(ctx, c.provider),
|
|
@@ -34765,7 +34857,7 @@ var credentialsShow = {
|
|
|
34765
34857
|
async run(args, { ctx, writer }) {
|
|
34766
34858
|
const id = requirePositional(args, 0, "credential id");
|
|
34767
34859
|
const credential = await findCredential(ctx, id);
|
|
34768
|
-
const status2 = condition(credential);
|
|
34860
|
+
const status2 = condition(credential, ctx.now());
|
|
34769
34861
|
const data = {
|
|
34770
34862
|
id: credential.id,
|
|
34771
34863
|
provider: credential.provider,
|
|
@@ -35287,11 +35379,24 @@ function summarizeLimits(limits) {
|
|
|
35287
35379
|
}
|
|
35288
35380
|
return parts.length === 0 ? "\u2014" : parts.join(" ");
|
|
35289
35381
|
}
|
|
35382
|
+
function parseWhen(flag, raw) {
|
|
35383
|
+
const at = Date.parse(raw);
|
|
35384
|
+
if (!Number.isFinite(at)) {
|
|
35385
|
+
throw new UsageError(`${flag} must be a date, e.g. 2030-01-01, got "${raw}"`);
|
|
35386
|
+
}
|
|
35387
|
+
return at;
|
|
35388
|
+
}
|
|
35389
|
+
function keyState(key, now) {
|
|
35390
|
+
if (key.revokedAt !== null)
|
|
35391
|
+
return "revoked";
|
|
35392
|
+
return keyUsable(key, now) ? "active" : "expired";
|
|
35393
|
+
}
|
|
35290
35394
|
var keysList = {
|
|
35291
35395
|
usage: "keys list",
|
|
35292
35396
|
summary: "List gateway API keys",
|
|
35293
35397
|
async run(_args, { ctx, writer }) {
|
|
35294
35398
|
const keys = await listKeys(await ctx.store());
|
|
35399
|
+
const now = Date.now();
|
|
35295
35400
|
emit(ctx, writer, { keys }, () => {
|
|
35296
35401
|
if (keys.length === 0)
|
|
35297
35402
|
return "no api keys; create one with: omni keys create";
|
|
@@ -35303,6 +35408,7 @@ var keysList = {
|
|
|
35303
35408
|
{ header: "LIMITS" },
|
|
35304
35409
|
{ header: "BODY CAPTURE" },
|
|
35305
35410
|
{ header: "STATE" },
|
|
35411
|
+
{ header: "EXPIRES" },
|
|
35306
35412
|
{ header: "CREATED" }
|
|
35307
35413
|
], keys.map((key) => [
|
|
35308
35414
|
key.id,
|
|
@@ -35311,28 +35417,32 @@ var keysList = {
|
|
|
35311
35417
|
key.modelAllowlist === null ? "all" : key.modelAllowlist.length === 0 ? "none" : key.modelAllowlist.join(","),
|
|
35312
35418
|
key.limits === null ? paint(ctx, "red", "unreadable") : summarizeLimits(key.limits),
|
|
35313
35419
|
key.bodyLoggingOptOut ? "no bodies" : "\u2014",
|
|
35314
|
-
state(ctx,
|
|
35420
|
+
((verdict) => state(ctx, verdict === "active", verdict))(keyState(key, now)),
|
|
35421
|
+
formatTime(key.expiresAt),
|
|
35315
35422
|
formatTime(key.createdAt)
|
|
35316
35423
|
]));
|
|
35317
35424
|
});
|
|
35318
35425
|
}
|
|
35319
35426
|
};
|
|
35320
35427
|
var keysCreate = {
|
|
35321
|
-
usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--no-bodies]",
|
|
35428
|
+
usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--expires <when>] [--no-bodies]",
|
|
35322
35429
|
summary: "Mint a gateway API key, printed once",
|
|
35323
35430
|
options: {
|
|
35324
35431
|
label: { type: "string" },
|
|
35325
35432
|
allow: { type: "string", multiple: true },
|
|
35326
35433
|
limit: { type: "string", multiple: true },
|
|
35434
|
+
expires: { type: "string" },
|
|
35327
35435
|
"no-bodies": { type: "boolean" }
|
|
35328
35436
|
},
|
|
35329
35437
|
async run(args, { ctx, writer }) {
|
|
35330
35438
|
const allow = listFlag(args.values, "allow");
|
|
35439
|
+
const expires = stringFlag(args.values, "expires");
|
|
35331
35440
|
const created = await createKey(await ctx.store(), {
|
|
35332
35441
|
...stringFlag(args.values, "label") === undefined ? {} : { label: stringFlag(args.values, "label") },
|
|
35333
35442
|
modelAllowlist: allow ?? null,
|
|
35334
35443
|
limits: parseLimitFlags(listFlag(args.values, "limit") ?? [], "--limit"),
|
|
35335
|
-
bodyLoggingOptOut: boolFlag(args.values, "no-bodies")
|
|
35444
|
+
bodyLoggingOptOut: boolFlag(args.values, "no-bodies"),
|
|
35445
|
+
expiresAt: expires === undefined ? null : parseWhen("--expires", expires)
|
|
35336
35446
|
});
|
|
35337
35447
|
emit(ctx, writer, created, () => {
|
|
35338
35448
|
note(ctx, writer, paint(ctx, "yellow", "this key is shown once and stored only as a hash"));
|
|
@@ -35399,6 +35509,44 @@ ${paint(ctx, "dim", "usage is counted from completed requests still inside each
|
|
|
35399
35509
|
});
|
|
35400
35510
|
}
|
|
35401
35511
|
};
|
|
35512
|
+
var keysExpiry = {
|
|
35513
|
+
usage: "keys expiry <id> [--at <when>] [--never]",
|
|
35514
|
+
summary: "Show or replace one key's expiry",
|
|
35515
|
+
options: {
|
|
35516
|
+
at: { type: "string" },
|
|
35517
|
+
never: { type: "boolean" }
|
|
35518
|
+
},
|
|
35519
|
+
async run(args, { ctx, writer }) {
|
|
35520
|
+
const id = requirePositional(args, 0, "key id");
|
|
35521
|
+
const at = stringFlag(args.values, "at");
|
|
35522
|
+
const never2 = boolFlag(args.values, "never");
|
|
35523
|
+
const store = await ctx.store();
|
|
35524
|
+
const existing = (await listKeys(store)).find((entry) => entry.id === id);
|
|
35525
|
+
if (existing === undefined)
|
|
35526
|
+
throw new CliError(`no api key "${id}"`);
|
|
35527
|
+
let key = existing;
|
|
35528
|
+
if (never2 || at !== undefined) {
|
|
35529
|
+
if (never2 && at !== undefined) {
|
|
35530
|
+
throw new UsageError("--at and --never cannot be combined");
|
|
35531
|
+
}
|
|
35532
|
+
const next = never2 ? null : parseWhen("--at", at);
|
|
35533
|
+
key = await setKeyExpiry(store, id, { expiresAt: next });
|
|
35534
|
+
}
|
|
35535
|
+
emit(ctx, writer, key, () => {
|
|
35536
|
+
const head = fields([
|
|
35537
|
+
["id", key.id],
|
|
35538
|
+
["label", key.label],
|
|
35539
|
+
["prefix", `${key.prefix}\u2026`]
|
|
35540
|
+
]);
|
|
35541
|
+
if (key.expiresAt === null)
|
|
35542
|
+
return `${head}
|
|
35543
|
+
expires: never`;
|
|
35544
|
+
const verdict = keyState(key, Date.now());
|
|
35545
|
+
return `${head}
|
|
35546
|
+
expires: ${formatTime(key.expiresAt)}${verdict === "expired" ? ` ${paint(ctx, "red", "(expired)")}` : ""}`;
|
|
35547
|
+
});
|
|
35548
|
+
}
|
|
35549
|
+
};
|
|
35402
35550
|
var keysRevoke = {
|
|
35403
35551
|
usage: "keys revoke <id>",
|
|
35404
35552
|
summary: "Revoke a gateway API key, keeping its usage history",
|
|
@@ -35894,7 +36042,7 @@ function createServiceDeps(input2) {
|
|
|
35894
36042
|
}
|
|
35895
36043
|
|
|
35896
36044
|
// apps/cli/src/version.ts
|
|
35897
|
-
var VERSION = "0.11.
|
|
36045
|
+
var VERSION = "0.11.2";
|
|
35898
36046
|
|
|
35899
36047
|
// apps/cli/src/commands/service.ts
|
|
35900
36048
|
function gatewayArgv(root) {
|
|
@@ -36494,7 +36642,7 @@ var logs = {
|
|
|
36494
36642
|
return;
|
|
36495
36643
|
let seen = first[0]?.at ?? 0;
|
|
36496
36644
|
for (;; ) {
|
|
36497
|
-
await Bun.sleep(
|
|
36645
|
+
await Bun.sleep(FOLLOW_INTERVAL_MS);
|
|
36498
36646
|
const next = (await recentLogs(store, limit)).filter((row) => row.at > seen);
|
|
36499
36647
|
if (next.length === 0)
|
|
36500
36648
|
continue;
|
|
@@ -36538,6 +36686,7 @@ var COMMANDS = {
|
|
|
36538
36686
|
"keys create": keysCreate,
|
|
36539
36687
|
"keys limits": keysLimits,
|
|
36540
36688
|
"keys models": keysModels,
|
|
36689
|
+
"keys expiry": keysExpiry,
|
|
36541
36690
|
"keys revoke": keysRevoke,
|
|
36542
36691
|
"plugin list": pluginList,
|
|
36543
36692
|
"plugin verify": pluginVerify,
|