@linxiraos/pi-ai 1.1.26 → 1.2.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.
- package/CHANGELOG.md +9 -8
- package/README.md +15 -0
- package/dist/types/auth/affinity.d.ts +33 -6
- package/dist/types/auth/cascade.d.ts +44 -7
- package/dist/types/auth/oauth.d.ts +9 -6
- package/dist/types/auth/policy.d.ts +1 -0
- package/dist/types/auth/refresh.d.ts +1 -2
- package/dist/types/auth/select.d.ts +6 -4
- package/dist/types/auth/sqlite-credential-store.d.ts +21 -1
- package/dist/types/auth/store.d.ts +8 -2
- package/dist/types/auth/types.d.ts +99 -15
- package/dist/types/auth-broker/client.d.ts +3 -3
- package/dist/types/auth-broker/discover.d.ts +1 -1
- package/dist/types/auth-broker/refresher.d.ts +6 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -4
- package/dist/types/auth-broker/server.d.ts +2 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
- package/dist/types/auth-gateway/dispatch.d.ts +13 -5
- package/dist/types/auth-gateway/http.d.ts +0 -2
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/routes/embeddings.d.ts +2 -2
- package/dist/types/auth-gateway/routes/images.d.ts +3 -3
- package/dist/types/auth-gateway/routes/rerank.d.ts +2 -2
- package/dist/types/auth-gateway/routes/speech.d.ts +2 -2
- package/dist/types/auth-gateway/routes/systemone.d.ts +2 -2
- package/dist/types/auth-gateway/routes/transcriptions.d.ts +2 -2
- package/dist/types/auth-gateway/routes/video.d.ts +4 -4
- package/dist/types/auth-gateway/server.d.ts +22 -5
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/stdio.d.ts +12 -0
- package/dist/types/auth-storage.d.ts +7 -6
- package/dist/types/dialect/terminator-wait.d.ts +19 -0
- package/dist/types/env-api-key.d.ts +21 -0
- package/dist/types/error/auth.d.ts +10 -0
- package/dist/types/error/flags.d.ts +38 -0
- package/dist/types/images/index.d.ts +2 -3
- package/dist/types/images/shared.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/judgment/index.d.ts +2 -1
- package/dist/types/judgment/native.d.ts +80 -0
- package/dist/types/judgment/openai-decisions.d.ts +3 -0
- package/dist/types/judgment/question-file.d.ts +52 -0
- package/dist/types/judgment/systemone.d.ts +7 -0
- package/dist/types/judgment/types.d.ts +2 -2
- package/dist/types/judgment/wire.d.ts +36 -0
- package/dist/types/provider-details.d.ts +1 -1
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-client.d.ts +10 -0
- package/dist/types/providers/anthropic-state.d.ts +2 -0
- package/dist/types/providers/anthropic.d.ts +38 -3
- package/dist/types/providers/aws-credentials.d.ts +4 -8
- package/dist/types/providers/connect-error-detail.d.ts +9 -0
- package/dist/types/providers/connect-frame.d.ts +44 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
- package/dist/types/providers/cursor.d.ts +8 -8
- package/dist/types/providers/google-auth.d.ts +4 -1
- package/dist/types/providers/google-shared.d.ts +24 -4
- package/dist/types/providers/inference-headers.d.ts +6 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/openai-configuration-update.d.ts +20 -11
- package/dist/types/providers/openai-responses-wire.d.ts +1 -55
- package/dist/types/providers/openai-responses.d.ts +40 -3
- package/dist/types/providers/openai-shared.d.ts +30 -5
- package/dist/types/providers/pi-native-client.d.ts +1 -1
- package/dist/types/providers/transform-messages.d.ts +25 -0
- package/dist/types/providers/vision-guard.d.ts +1 -1
- package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
- package/dist/types/registry/oauth/cursor.d.ts +10 -2
- package/dist/types/registry/oauth/index.d.ts +2 -0
- package/dist/types/registry/oauth/snowflake.d.ts +3 -0
- package/dist/types/registry/snowflake.d.ts +4 -0
- package/dist/types/stream.d.ts +0 -21
- package/dist/types/transcription/types.d.ts +2 -1
- package/dist/types/types.d.ts +116 -14
- package/dist/types/usage.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +21 -7
- package/dist/types/utils/aws-profile.d.ts +5 -0
- package/dist/types/utils/block-symbols.d.ts +14 -0
- package/dist/types/utils/dsml-leak.d.ts +29 -0
- package/dist/types/utils/glyph-codec.d.ts +1 -1
- package/dist/types/utils/idle-iterator.d.ts +11 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
- package/dist/types/utils/resolve-peer.d.ts +2 -0
- package/dist/types/utils/retry-wait.d.ts +9 -0
- package/dist/types/utils/schema/normalize.d.ts +8 -3
- package/dist/types/utils/stream-markup-healing.d.ts +4 -4
- package/dist/types/utils/tool-call-arguments.d.ts +8 -0
- package/dist/types/utils.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/affinity.ts +130 -37
- package/src/auth/blocks.ts +94 -15
- package/src/auth/cascade.ts +112 -17
- package/src/auth/health.ts +3 -2
- package/src/auth/oauth.ts +25 -10
- package/src/auth/policy.ts +10 -1
- package/src/auth/pool.ts +0 -5
- package/src/auth/refresh.ts +17 -12
- package/src/auth/resets.ts +1 -1
- package/src/auth/rotation.ts +8 -2
- package/src/auth/select.ts +102 -30
- package/src/auth/sqlite-credential-store.ts +119 -49
- package/src/auth/store.ts +8 -2
- package/src/auth/types.ts +108 -17
- package/src/auth/usage-cache.ts +16 -5
- package/src/auth/usage.ts +2 -2
- package/src/auth-broker/client.ts +7 -4
- package/src/auth-broker/discover.ts +6 -5
- package/src/auth-broker/refresher.ts +8 -1
- package/src/auth-broker/remote-store.ts +200 -62
- package/src/auth-broker/server.ts +316 -130
- package/src/auth-broker/snapshot-cache.ts +142 -2
- package/src/auth-gateway/dispatch.ts +15 -7
- package/src/auth-gateway/http.ts +0 -8
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +2 -2
- package/src/auth-gateway/routes/images.ts +4 -4
- package/src/auth-gateway/routes/rerank.ts +2 -2
- package/src/auth-gateway/routes/speech.ts +2 -2
- package/src/auth-gateway/routes/systemone.ts +22 -14
- package/src/auth-gateway/routes/transcriptions.ts +2 -2
- package/src/auth-gateway/routes/video.ts +7 -7
- package/src/auth-gateway/server.ts +175 -134
- package/src/auth-gateway/session-state.ts +51 -9
- package/src/auth-gateway/stdio.ts +132 -0
- package/src/auth-storage.ts +23 -9
- package/src/dialect/deepseek.ts +67 -8
- package/src/dialect/gemini.ts +8 -2
- package/src/dialect/hermes.ts +8 -2
- package/src/dialect/kimi.ts +8 -2
- package/src/dialect/qwen-xml.ts +8 -2
- package/src/dialect/qwen3.ts +8 -2
- package/src/dialect/terminator-wait.ts +42 -0
- package/src/env-api-key.ts +78 -0
- package/src/error/auth.ts +20 -0
- package/src/error/flags.ts +143 -24
- package/src/error/rate-limit.ts +8 -6
- package/src/images/google-antigravity.ts +0 -1
- package/src/images/index.ts +3 -17
- package/src/images/openai-hosted.ts +13 -4
- package/src/images/openai-images.ts +19 -13
- package/src/images/shared.ts +10 -2
- package/src/index.ts +2 -1
- package/src/judgment/index.ts +2 -1
- package/src/judgment/native.ts +333 -0
- package/src/judgment/openai-decisions-predicate.md +9 -0
- package/src/judgment/openai-decisions.ts +129 -0
- package/src/judgment/question-file.ts +90 -0
- package/src/judgment/systemone.ts +65 -0
- package/src/judgment/text.ts +9 -1
- package/src/judgment/types.ts +2 -2
- package/src/judgment/wire.ts +42 -0
- package/src/provider-details.ts +1 -1
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +19 -8
- package/src/providers/anthropic-client.ts +16 -1
- package/src/providers/anthropic-state.ts +6 -0
- package/src/providers/anthropic.ts +962 -238
- package/src/providers/apple-foundation-models.ts +10 -4
- package/src/providers/aws-credentials.ts +65 -5
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/connect-error-detail.ts +21 -0
- package/src/providers/connect-frame.ts +107 -0
- package/src/providers/cursor/interaction-query.ts +1 -9
- package/src/providers/cursor.ts +590 -133
- package/src/providers/devin.ts +68 -58
- package/src/providers/factory-droid/gemini.ts +38 -51
- package/src/providers/factory-droid.ts +2 -0
- package/src/providers/gitlab-duo-workflow.ts +5 -2
- package/src/providers/gitlab-duo.ts +1 -0
- package/src/providers/google-auth.ts +10 -3
- package/src/providers/google-gemini-cli.ts +4 -0
- package/src/providers/google-shared.ts +134 -105
- package/src/providers/google-vertex.ts +0 -1
- package/src/providers/google.ts +1 -1
- package/src/providers/inference-headers.ts +19 -0
- package/src/providers/mock.ts +4 -0
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-codex/request-transformer.ts +16 -11
- package/src/providers/openai-codex-responses.ts +201 -76
- package/src/providers/openai-completions.ts +12 -2
- package/src/providers/openai-configuration-update.ts +54 -15
- package/src/providers/openai-responses-wire.ts +1 -55
- package/src/providers/openai-responses.ts +437 -33
- package/src/providers/openai-shared.ts +267 -72
- package/src/providers/pi-native-client.ts +2 -2
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +1 -1
- package/src/providers/transcriptions-server.ts +1 -1
- package/src/providers/transform-messages.ts +240 -84
- package/src/providers/vision-guard.ts +1 -1
- package/src/registry/aws.ts +51 -14
- package/src/registry/cloudflare-ai-gateway.ts +26 -3
- package/src/registry/hooks/api-key.ts +1 -0
- package/src/registry/hooks/custom.ts +2 -0
- package/src/registry/hooks/env.ts +2 -5
- package/src/registry/oauth/callback-server.ts +6 -1
- package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
- package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
- package/src/registry/oauth/cursor.ts +80 -17
- package/src/registry/oauth/index.ts +5 -0
- package/src/registry/oauth/snowflake.ts +183 -0
- package/src/registry/registry.ts +2 -0
- package/src/registry/snowflake.ts +149 -0
- package/src/stream.ts +85 -107
- package/src/transcription/types.ts +2 -1
- package/src/types.ts +142 -15
- package/src/usage/claude.ts +19 -12
- package/src/usage/commandcode.ts +1 -1
- package/src/usage/cursor.ts +14 -25
- package/src/usage/github-copilot.ts +1 -1
- package/src/usage/kimi.ts +1 -1
- package/src/usage/minimax-code.ts +8 -1
- package/src/usage.ts +8 -0
- package/src/utils/anthropic-auth.ts +34 -10
- package/src/utils/aws-profile.ts +10 -4
- package/src/utils/block-symbols.ts +17 -0
- package/src/utils/dsml-leak.ts +109 -0
- package/src/utils/empty-completion-retry.ts +2 -3
- package/src/utils/glyph-codec.ts +200 -7
- package/src/utils/idle-iterator.ts +80 -35
- package/src/utils/leaked-thinking-stream.ts +47 -25
- package/src/utils/resolve-peer.ts +7 -0
- package/src/utils/retry-wait.ts +19 -0
- package/src/utils/schema/CONSTRAINTS.md +9 -7
- package/src/utils/schema/fields.ts +20 -0
- package/src/utils/schema/normalize.ts +65 -23
- package/src/utils/schema/wire.ts +3 -3
- package/src/utils/stream-markup-healing.ts +4 -4
- package/src/utils/thinking-loop.ts +36 -13
- package/src/utils/tool-call-arguments.ts +16 -0
- package/src/utils/transport-fetch.ts +1 -1
- package/src/utils.ts +41 -10
- package/dist/types/judgment/typesafe.d.ts +0 -49
- package/src/judgment/typesafe.ts +0 -183
|
@@ -9,7 +9,7 @@ import * as fs from "node:fs/promises";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { authPolicyFor } from "@linxiraos/pi-catalog/compat/auth";
|
|
11
11
|
import { parseAlibabaTokenPlanCredential } from "@linxiraos/pi-catalog/wire/alibaba-token-plan";
|
|
12
|
-
import {
|
|
12
|
+
import { parseCloudflareCredential } from "@linxiraos/pi-catalog/wire/cloudflare";
|
|
13
13
|
import {
|
|
14
14
|
getAgentDbPath,
|
|
15
15
|
getDbBusyTimeoutMs,
|
|
@@ -110,6 +110,12 @@ const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
|
110
110
|
const LEGACY_CODEX_BLOCK_PROVIDER_KEY = "openai-codex:oauth";
|
|
111
111
|
const LEGACY_CODEX_BLOCK_SCOPE = "shared";
|
|
112
112
|
const CODEX_METER_BLOCK_SCOPES = ["chat", "spark"] as const;
|
|
113
|
+
/**
|
|
114
|
+
* Minimum spacing between read-path sweeps of expired block rows. Reads already
|
|
115
|
+
* filter `blocked_until_ms > now`; the sweep only bounds table growth, so it
|
|
116
|
+
* must not take the SQLite write lock on every selection.
|
|
117
|
+
*/
|
|
118
|
+
const CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS = 60_000;
|
|
113
119
|
|
|
114
120
|
// SQLite error classifiers live in pi-utils so the credential store and the
|
|
115
121
|
// model cache share one implementation; re-exported here to preserve the
|
|
@@ -211,6 +217,12 @@ function resolveProviderCredentialIdentityKey(provider: string, identifiers: str
|
|
|
211
217
|
return null;
|
|
212
218
|
}
|
|
213
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Identity key of a credential: the `identityKey` that broker snapshots carry
|
|
222
|
+
* and that broker account pools and `sessions.restrict` match, such as
|
|
223
|
+
* `email:<address>|org:<id>` for org-scoped providers. `null` for an API key or
|
|
224
|
+
* an OAuth credential without any account, email, project, or org identity.
|
|
225
|
+
*/
|
|
214
226
|
export function resolveCredentialIdentityKey(provider: string, credential: AuthCredential): string | null {
|
|
215
227
|
if (credential.type === "api_key") return null;
|
|
216
228
|
return resolveProviderCredentialIdentityKey(provider, extractOAuthCredentialIdentifiers(credential));
|
|
@@ -223,7 +235,11 @@ function resolveRowCredentialIdentityKey(provider: string, row: AuthRow): string
|
|
|
223
235
|
return credential?.type === "oauth" ? resolveCredentialIdentityKey(provider, credential) : null;
|
|
224
236
|
}
|
|
225
237
|
|
|
226
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Whether upserting `incoming` replaces the active row holding `existing` (stored
|
|
240
|
+
* under `existingIdentityKey`) instead of inserting a new row beside it.
|
|
241
|
+
*/
|
|
242
|
+
export function matchesReplacementCredential(
|
|
227
243
|
provider: string,
|
|
228
244
|
existing: AuthCredential | null,
|
|
229
245
|
existingIdentityKey: string | null,
|
|
@@ -238,9 +254,9 @@ function matchesReplacementCredential(
|
|
|
238
254
|
const incomingToken = parseAlibabaTokenPlanCredential(incoming.key)?.token;
|
|
239
255
|
return existingToken !== undefined && existingToken === incomingToken;
|
|
240
256
|
}
|
|
241
|
-
if (provider === "cloudflare-ai-gateway") {
|
|
242
|
-
const existingToken =
|
|
243
|
-
const incomingToken =
|
|
257
|
+
if (provider === "cloudflare-ai-gateway" || provider === "cloudflare-workers-ai") {
|
|
258
|
+
const existingToken = parseCloudflareCredential(existing.key)?.token;
|
|
259
|
+
const incomingToken = parseCloudflareCredential(incoming.key)?.token;
|
|
244
260
|
return existingToken !== undefined && existingToken === incomingToken;
|
|
245
261
|
}
|
|
246
262
|
return false;
|
|
@@ -294,6 +310,23 @@ function matchesReplacementCredential(
|
|
|
294
310
|
return false;
|
|
295
311
|
}
|
|
296
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Whether two OAuth credentials belong to the same account of the same organization,
|
|
315
|
+
* whichever identity key each is stored under. The email decides when both carry one;
|
|
316
|
+
* otherwise a matching account id does, unless it is the organization's own id (a
|
|
317
|
+
* ChatGPT workspace id shared by every member). Project ids are shared between users
|
|
318
|
+
* (every consumer Antigravity login has the same one) and never count.
|
|
319
|
+
*/
|
|
320
|
+
export function isSameOAuthAccount(a: OAuthCredential, b: OAuthCredential): boolean {
|
|
321
|
+
const orgId = normalizeStoredAccountId(a.orgId);
|
|
322
|
+
if (orgId !== normalizeStoredAccountId(b.orgId)) return false;
|
|
323
|
+
const emailA = normalizeStoredEmail(a.email);
|
|
324
|
+
const emailB = normalizeStoredEmail(b.email);
|
|
325
|
+
if (emailA !== null && emailB !== null) return emailA === emailB;
|
|
326
|
+
const accountId = normalizeStoredAccountId(a.accountId);
|
|
327
|
+
return accountId !== null && accountId !== orgId && accountId === normalizeStoredAccountId(b.accountId);
|
|
328
|
+
}
|
|
329
|
+
|
|
297
330
|
function extractOAuthCredentialIdentifiers(credential: OAuthCredential): string[] {
|
|
298
331
|
const identifiers = new Set<string>();
|
|
299
332
|
const accountId = normalizeStoredAccountId(credential.accountId);
|
|
@@ -386,11 +419,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
386
419
|
#updateIfMatchesWithLeaseStmt: Statement;
|
|
387
420
|
#deleteIfMatchesWithLeaseStmt: Statement;
|
|
388
421
|
#getCredentialBlockStmt: Statement;
|
|
389
|
-
#
|
|
422
|
+
#listCredentialBlockScopesStmt: Statement;
|
|
423
|
+
#listCredentialBlocksStmt: Statement;
|
|
390
424
|
#upsertCredentialBlockStmt: Statement;
|
|
391
425
|
#deleteCredentialBlocksStmt: Statement;
|
|
392
426
|
#deleteCredentialBlockStmt: Statement;
|
|
393
427
|
#deleteExpiredCredentialBlocksStmt: Statement;
|
|
428
|
+
/** Wall-clock time of the last expired-block sweep; throttles read-path sweeps. */
|
|
429
|
+
#lastCredentialBlockSweepMs = Number.NEGATIVE_INFINITY;
|
|
394
430
|
#acquireCredentialRefreshLeaseStmt: Statement;
|
|
395
431
|
#getCredentialRefreshLeaseStmt: Statement;
|
|
396
432
|
#renewCredentialRefreshLeaseStmt: Statement;
|
|
@@ -475,12 +511,18 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
475
511
|
this.#getCredentialBlockStmt = this.#db.prepare(
|
|
476
512
|
"SELECT blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
477
513
|
);
|
|
478
|
-
this.#
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
514
|
+
this.#listCredentialBlockScopesStmt = this.#db.prepare(
|
|
515
|
+
"SELECT block_scope, blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND blocked_until_ms > ?",
|
|
516
|
+
);
|
|
517
|
+
// One statement for any number of ids: `json_each` keeps the caller's id
|
|
518
|
+
// order (`ids.key`) and the per-credential provider/scope order.
|
|
519
|
+
this.#listCredentialBlocksStmt = this.#db.prepare(
|
|
520
|
+
`SELECT b.credential_id, b.provider_key, b.block_scope, b.blocked_until_ms, b.updated_at
|
|
521
|
+
FROM json_each(?) AS ids
|
|
522
|
+
JOIN auth_credential_blocks AS b ON b.credential_id = ids.value
|
|
523
|
+
WHERE b.blocked_until_ms > ?
|
|
524
|
+
AND NOT (b.provider_key = ? AND b.block_scope = ?)
|
|
525
|
+
ORDER BY ids.key ASC, b.provider_key ASC, b.block_scope ASC`,
|
|
484
526
|
);
|
|
485
527
|
this.#upsertCredentialBlockStmt = this.#db.prepare(
|
|
486
528
|
`INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
|
|
@@ -576,7 +618,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
576
618
|
* Install the per-connection busy handler so lock-taking statements wait for
|
|
577
619
|
* a contended writer instead of failing immediately (Bun defaults
|
|
578
620
|
* `busy_timeout` to 0). MUST run before the first lock-taking statement on
|
|
579
|
-
* the connection: concurrent
|
|
621
|
+
* the connection: concurrent omp startups race WAL recovery and the leases
|
|
580
622
|
* DDL. Uses the centralized timeout so headless hosts keep their bounded
|
|
581
623
|
* busy wait instead of the interactive 5s value. See issues #2421, #7298.
|
|
582
624
|
*/
|
|
@@ -587,7 +629,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
587
629
|
#initializeSchema(): void {
|
|
588
630
|
// Install the busy handler BEFORE any lock-taking statement (incl.
|
|
589
631
|
// `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
|
|
590
|
-
// recovery). Without this, concurrent
|
|
632
|
+
// recovery). Without this, concurrent omp startups can crash here with
|
|
591
633
|
// `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. Re-setting when opened via
|
|
592
634
|
// `open()` (which already installed it) is idempotent. See issue #2421.
|
|
593
635
|
SqliteAuthCredentialStore.#installBusyTimeout(this.#db);
|
|
@@ -1585,27 +1627,38 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1585
1627
|
}
|
|
1586
1628
|
|
|
1587
1629
|
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
1588
|
-
const nowMs = Date.now();
|
|
1589
|
-
const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1590
1630
|
// Current callers use meter scopes. The physical shared row exists only
|
|
1591
1631
|
// for direct SQLite readers from pre-meter releases.
|
|
1592
|
-
if (
|
|
1632
|
+
if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1593
1633
|
return undefined;
|
|
1594
1634
|
}
|
|
1595
|
-
|
|
1596
|
-
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope,
|
|
1635
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1636
|
+
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, Date.now()) as
|
|
1597
1637
|
| { blocked_until_ms?: number; updated_at?: number }
|
|
1598
1638
|
| undefined;
|
|
1599
1639
|
return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
|
|
1600
1640
|
}
|
|
1601
1641
|
|
|
1642
|
+
getCredentialBlockScopes(credentialId: number, providerKey: string): Map<string, number> {
|
|
1643
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1644
|
+
const rows = this.#listCredentialBlockScopesStmt.all(credentialId, providerKey, Date.now()) as Array<
|
|
1645
|
+
Pick<CredentialBlockRow, "block_scope" | "blocked_until_ms">
|
|
1646
|
+
>;
|
|
1647
|
+
const hidesLegacyShared = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1648
|
+
const scopes = new Map<string, number>();
|
|
1649
|
+
for (const row of rows) {
|
|
1650
|
+
if (hidesLegacyShared && row.block_scope === LEGACY_CODEX_BLOCK_SCOPE) continue;
|
|
1651
|
+
scopes.set(row.block_scope, row.blocked_until_ms);
|
|
1652
|
+
}
|
|
1653
|
+
return scopes;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1602
1656
|
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
1603
|
-
|
|
1604
|
-
const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1605
|
-
if (isCodexBlock && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1657
|
+
if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1606
1658
|
return undefined;
|
|
1607
1659
|
}
|
|
1608
|
-
|
|
1660
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1661
|
+
const nowMs = Date.now();
|
|
1609
1662
|
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
1610
1663
|
| { blocked_until_ms?: number; updated_at?: number }
|
|
1611
1664
|
| undefined;
|
|
@@ -1622,7 +1675,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1622
1675
|
const isLegacyCodexBlock =
|
|
1623
1676
|
block.providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && block.blockScope === LEGACY_CODEX_BLOCK_SCOPE;
|
|
1624
1677
|
const blockScopes = isLegacyCodexBlock ? CODEX_METER_BLOCK_SCOPES : [block.blockScope];
|
|
1678
|
+
const nowMs = Date.now();
|
|
1625
1679
|
const upsert = this.#db.transaction(() => {
|
|
1680
|
+
// A mark already holds the write lock: sweep expired rows in the same transaction.
|
|
1681
|
+
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
1626
1682
|
for (const blockScope of blockScopes) {
|
|
1627
1683
|
this.#upsertCredentialBlockStmt.run(
|
|
1628
1684
|
block.credentialId,
|
|
@@ -1633,8 +1689,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1633
1689
|
}
|
|
1634
1690
|
});
|
|
1635
1691
|
upsert.immediate();
|
|
1692
|
+
this.#lastCredentialBlockSweepMs = nowMs;
|
|
1693
|
+
this.#pruneCredentialBlockReconcileAfter(nowMs);
|
|
1636
1694
|
|
|
1637
|
-
const reconcileAfterMs = Math.min(block.blockedUntilMs,
|
|
1695
|
+
const reconcileAfterMs = Math.min(block.blockedUntilMs, nowMs + USAGE_REPORT_TTL_MS);
|
|
1638
1696
|
for (const blockScope of blockScopes) {
|
|
1639
1697
|
this.#credentialBlockReconcileAfter.set(
|
|
1640
1698
|
`${block.credentialId}\0${block.providerKey}\0${blockScope}`,
|
|
@@ -1661,38 +1719,49 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1661
1719
|
}
|
|
1662
1720
|
|
|
1663
1721
|
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
1722
|
+
// Stamp wall-clock time first: callers may pass a future `nowMs`, and a
|
|
1723
|
+
// failed sweep must not be retried by every following read.
|
|
1724
|
+
this.#lastCredentialBlockSweepMs = Date.now();
|
|
1664
1725
|
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
1726
|
+
this.#pruneCredentialBlockReconcileAfter(nowMs);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
#pruneCredentialBlockReconcileAfter(nowMs: number): void {
|
|
1665
1730
|
for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
|
|
1666
1731
|
if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
|
|
1667
1732
|
}
|
|
1668
1733
|
}
|
|
1669
1734
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1735
|
+
/**
|
|
1736
|
+
* Read-path expiry sweep, at most once per {@link CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS}.
|
|
1737
|
+
* A busy write lock only postpones housekeeping; it never fails the read.
|
|
1738
|
+
*/
|
|
1739
|
+
#sweepExpiredCredentialBlocks(): void {
|
|
1672
1740
|
const nowMs = Date.now();
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
if (
|
|
1678
|
-
seenCredentialIds.add(credentialId);
|
|
1679
|
-
const rows = this.#listCredentialBlocksByCredentialStmt.all(
|
|
1680
|
-
credentialId,
|
|
1681
|
-
nowMs,
|
|
1682
|
-
LEGACY_CODEX_BLOCK_PROVIDER_KEY,
|
|
1683
|
-
LEGACY_CODEX_BLOCK_SCOPE,
|
|
1684
|
-
) as CredentialBlockRow[];
|
|
1685
|
-
for (const row of rows) {
|
|
1686
|
-
blocks.push({
|
|
1687
|
-
credentialId: row.credential_id,
|
|
1688
|
-
providerKey: row.provider_key,
|
|
1689
|
-
blockScope: row.block_scope,
|
|
1690
|
-
blockedUntilMs: row.blocked_until_ms,
|
|
1691
|
-
updatedAtMs: row.updated_at * 1000,
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1741
|
+
if (nowMs - this.#lastCredentialBlockSweepMs < CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS) return;
|
|
1742
|
+
try {
|
|
1743
|
+
this.cleanExpiredCredentialBlocks(nowMs);
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
if (!isSqliteBusyError(err)) throw err;
|
|
1694
1746
|
}
|
|
1695
|
-
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
1750
|
+
if (credentialIds.length === 0) return [];
|
|
1751
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1752
|
+
const rows = this.#listCredentialBlocksStmt.all(
|
|
1753
|
+
JSON.stringify([...new Set(credentialIds)]),
|
|
1754
|
+
Date.now(),
|
|
1755
|
+
LEGACY_CODEX_BLOCK_PROVIDER_KEY,
|
|
1756
|
+
LEGACY_CODEX_BLOCK_SCOPE,
|
|
1757
|
+
) as CredentialBlockRow[];
|
|
1758
|
+
return rows.map(row => ({
|
|
1759
|
+
credentialId: row.credential_id,
|
|
1760
|
+
providerKey: row.provider_key,
|
|
1761
|
+
blockScope: row.block_scope,
|
|
1762
|
+
blockedUntilMs: row.blocked_until_ms,
|
|
1763
|
+
updatedAtMs: row.updated_at * 1000,
|
|
1764
|
+
}));
|
|
1696
1765
|
}
|
|
1697
1766
|
|
|
1698
1767
|
tryAcquireCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean {
|
|
@@ -2050,7 +2119,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
2050
2119
|
this.#upsertCacheStmt.finalize();
|
|
2051
2120
|
this.#deleteExpiredCacheStmt.finalize();
|
|
2052
2121
|
this.#getCredentialBlockStmt.finalize();
|
|
2053
|
-
this.#
|
|
2122
|
+
this.#listCredentialBlockScopesStmt.finalize();
|
|
2123
|
+
this.#listCredentialBlocksStmt.finalize();
|
|
2054
2124
|
this.#upsertCredentialBlockStmt.finalize();
|
|
2055
2125
|
this.#deleteCredentialBlocksStmt.finalize();
|
|
2056
2126
|
this.#deleteCredentialBlockStmt.finalize();
|
package/src/auth/store.ts
CHANGED
|
@@ -98,6 +98,12 @@ export interface CredentialCacheStore {
|
|
|
98
98
|
export interface CredentialBlockStore {
|
|
99
99
|
/** Non-expired block for one (credential, providerKey, scope) key, or undefined. */
|
|
100
100
|
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Non-expired blocks for one credential and providerKey, keyed by block
|
|
103
|
+
* scope (`""` is unscoped), in one read. Optional: selectors fall back to
|
|
104
|
+
* {@link CredentialBlockStore.getCredentialBlock} per scope when absent.
|
|
105
|
+
*/
|
|
106
|
+
getCredentialBlockScopes?(credentialId: number, providerKey: string): ReadonlyMap<string, number>;
|
|
101
107
|
/** Earliest time a shared-store block should be eligible for live-usage reconciliation. */
|
|
102
108
|
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
103
109
|
/** Upsert with MAX semantics: keep the later blockedUntilMs on conflict. */
|
|
@@ -146,8 +152,8 @@ export interface UsageLedgerStore {
|
|
|
146
152
|
|
|
147
153
|
/** Broker-delegated OAuth refresh and usage-report operations. */
|
|
148
154
|
export interface CredentialUpstream {
|
|
149
|
-
/** Optional hook to notify the underlying store that usage
|
|
150
|
-
invalidateUsageCache(signal?: AbortSignal): Promise<void>;
|
|
155
|
+
/** Optional hook to notify the underlying store that usage reports (one provider's, when given) are stale. */
|
|
156
|
+
invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void>;
|
|
151
157
|
/**
|
|
152
158
|
* Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
|
|
153
159
|
* it before the per-provider local refresh path. `RemoteAuthCredentialStore`
|
package/src/auth/types.ts
CHANGED
|
@@ -257,6 +257,8 @@ export interface CheckCredentialsOptions {
|
|
|
257
257
|
completionProbe?: CompletionProbe;
|
|
258
258
|
/** Per-credential completion probe timeout (ms). Defaults to `timeoutMs`. */
|
|
259
259
|
completionTimeoutMs?: number;
|
|
260
|
+
/** Providers whose credentials are skipped entirely (not probed, not reported). */
|
|
261
|
+
excludeProviders?: ReadonlySet<string>;
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -371,6 +373,12 @@ export type AuthStorageOptions = {
|
|
|
371
373
|
signal?: AbortSignal,
|
|
372
374
|
reason?: OAuthRefreshReason,
|
|
373
375
|
) => Promise<OAuthCredentials>;
|
|
376
|
+
/**
|
|
377
|
+
* Set when {@link refreshOAuthCredential} exchanges the refresh token with the
|
|
378
|
+
* provider itself instead of delegating (e.g. to a broker), so its tokens count
|
|
379
|
+
* as this process's mints for auth-recovery reuse.
|
|
380
|
+
*/
|
|
381
|
+
refreshOAuthCredentialMints?: boolean;
|
|
374
382
|
/**
|
|
375
383
|
* Human-readable description of the credential store backing this
|
|
376
384
|
* AuthStorage instance. Surfaced through {@link AuthStorage.keys.describe}
|
|
@@ -470,7 +478,7 @@ export interface ModelUsageHealthOptions {
|
|
|
470
478
|
export type AuthApiKeyOptions = {
|
|
471
479
|
baseUrl?: string;
|
|
472
480
|
modelId?: string;
|
|
473
|
-
/**
|
|
481
|
+
/** {@link oauthAccountKey}s of accounts known to serve `modelId` from multi-account discovery; OAuth selection prefers them and tries other accounts only as a last resort. */
|
|
474
482
|
accountIds?: readonly string[];
|
|
475
483
|
/**
|
|
476
484
|
* Caller's cancel signal. Threaded into any broker-bound OAuth refresh so
|
|
@@ -487,8 +495,26 @@ export type AuthApiKeyOptions = {
|
|
|
487
495
|
forceRefresh?: boolean;
|
|
488
496
|
/** Explicit provider-401 recovery; generic force refreshes leave this unset. */
|
|
489
497
|
refreshReason?: OAuthRefreshReason;
|
|
498
|
+
/** When false, select as `sessionId` would without recording the choice as that session's sticky credential. */
|
|
499
|
+
recordAffinity?: boolean;
|
|
490
500
|
};
|
|
491
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Identity that multi-account discovery records in `Model.accountAccess` and
|
|
504
|
+
* that {@link AuthApiKeyOptions.accountIds} selection matches: the provider
|
|
505
|
+
* account id (Codex `chatgpt_account_id`), else the login email for providers
|
|
506
|
+
* whose credentials carry no account id (Antigravity), else the project id for
|
|
507
|
+
* a login whose email lookup failed. Same precedence as the credential store's
|
|
508
|
+
* identity key.
|
|
509
|
+
*/
|
|
510
|
+
export function oauthAccountKey(identity: {
|
|
511
|
+
accountId?: string;
|
|
512
|
+
email?: string;
|
|
513
|
+
projectId?: string;
|
|
514
|
+
}): string | undefined {
|
|
515
|
+
return identity.accountId ?? identity.email ?? identity.projectId;
|
|
516
|
+
}
|
|
517
|
+
|
|
492
518
|
/** Non-secret identity bound to the OAuth credential selected for one request attempt. */
|
|
493
519
|
export interface OAuthRequestIdentity {
|
|
494
520
|
orgId?: string;
|
|
@@ -890,9 +916,22 @@ export interface KeysApi {
|
|
|
890
916
|
* 4. API key persisted by a successful `/login`
|
|
891
917
|
* 5. Environment variable
|
|
892
918
|
* 6. Stored API key (e.g. a broker-migrated copy) — last resort, so an explicit env var wins
|
|
919
|
+
* 7. OMP compatibility mirror — below every stored credential, so a key the
|
|
920
|
+
* user saves locally always wins over the upstream fallback
|
|
921
|
+
*
|
|
922
|
+
* Resolves `undefined` when no permitted credential can serve, including when
|
|
923
|
+
* OAuth refresh failed transiently (network, timeout, 5xx), so availability
|
|
924
|
+
* probes move on to their next candidate. Request paths use
|
|
925
|
+
* {@link KeysApi.getWithCredential}, which surfaces that failure.
|
|
893
926
|
*/
|
|
894
927
|
get(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
|
|
895
|
-
/**
|
|
928
|
+
/**
|
|
929
|
+
* Resolve a bearer together with its durable stored credential row id, when known.
|
|
930
|
+
* Same precedence as {@link KeysApi.get}, but rejects with
|
|
931
|
+
* `OAuthRefreshUnavailableError` (transient, retryable) when OAuth refresh failed
|
|
932
|
+
* with a retryable error and no other permitted source could serve the request;
|
|
933
|
+
* the stored credential is kept.
|
|
934
|
+
*/
|
|
896
935
|
getWithCredential(
|
|
897
936
|
provider: string,
|
|
898
937
|
sessionId?: string,
|
|
@@ -951,10 +990,14 @@ export interface KeysApi {
|
|
|
951
990
|
* `fallback: true` ranks the value below stored OAuth and `/login`
|
|
952
991
|
* credentials instead, so a provider's default key reference cannot shadow
|
|
953
992
|
* a key the user logged in with.
|
|
993
|
+
*
|
|
994
|
+
* `mirror: true` ranks the value below everything else, under stored
|
|
995
|
+
* api_key credentials — a read-only compatibility mirror (upstream OMP
|
|
996
|
+
* agent.db key) that must never shadow a locally stored key.
|
|
954
997
|
*/
|
|
955
|
-
setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean }): void;
|
|
998
|
+
setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean; mirror?: boolean }): void;
|
|
956
999
|
/**
|
|
957
|
-
* Remove a single config-sourced API key (override or
|
|
1000
|
+
* Remove a single config-sourced API key (override, fallback, or mirror).
|
|
958
1001
|
*/
|
|
959
1002
|
removeConfig(provider: string): void;
|
|
960
1003
|
/**
|
|
@@ -983,7 +1026,7 @@ export interface KeysApi {
|
|
|
983
1026
|
resolver(provider: string, options?: { sessionId?: string; baseUrl?: string; modelId?: string }): ApiKeyResolver;
|
|
984
1027
|
}
|
|
985
1028
|
|
|
986
|
-
/** Controls whether a row-id refresh may reuse a
|
|
1029
|
+
/** Controls whether a row-id refresh may reuse a recently minted token. */
|
|
987
1030
|
export interface OAuthRefreshByIdOptions {
|
|
988
1031
|
/**
|
|
989
1032
|
* Return the stored credential when it still holds a fresh access token this
|
|
@@ -991,6 +1034,12 @@ export interface OAuthRefreshByIdOptions {
|
|
|
991
1034
|
* refresh tokens repeatedly when a provider rejects every valid bearer.
|
|
992
1035
|
*/
|
|
993
1036
|
reuseRecentMint?: boolean;
|
|
1037
|
+
/**
|
|
1038
|
+
* Provider-401 recovery intent. Forwarded to a delegated refresh (an auth
|
|
1039
|
+
* broker), which may then return a token it minted in the last five minutes.
|
|
1040
|
+
* Local reuse still requires {@link reuseRecentMint}.
|
|
1041
|
+
*/
|
|
1042
|
+
reason?: OAuthRefreshReason;
|
|
994
1043
|
}
|
|
995
1044
|
|
|
996
1045
|
/** OAuth login, access, account identity, and refresh operations. */
|
|
@@ -1013,9 +1062,11 @@ export interface OAuthApi {
|
|
|
1013
1062
|
* `enterpriseUrl`). For pure "give me the bytes for `Authorization`"
|
|
1014
1063
|
* scenarios, prefer {@link AuthStorage.keys.get}.
|
|
1015
1064
|
*
|
|
1016
|
-
* Returns `undefined` when no OAuth credential is available
|
|
1017
|
-
*
|
|
1018
|
-
* OAuth with an explicit API key.
|
|
1065
|
+
* Returns `undefined` when no usable OAuth credential is available (none
|
|
1066
|
+
* stored, or every one definitively failed to refresh) or runtime/config
|
|
1067
|
+
* overrides have replaced OAuth with an explicit API key. Rejects with
|
|
1068
|
+
* `OAuthRefreshUnavailableError` (transient, retryable) when a retryable
|
|
1069
|
+
* refresh failure left no usable credential.
|
|
1019
1070
|
*/
|
|
1020
1071
|
access(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined>;
|
|
1021
1072
|
/**
|
|
@@ -1031,9 +1082,10 @@ export interface OAuthApi {
|
|
|
1031
1082
|
* Resolve one stored OAuth credential by its durable storage row id.
|
|
1032
1083
|
*
|
|
1033
1084
|
* Unlike the normal session resolver, this method never ranks, rotates, or
|
|
1034
|
-
* falls back to sibling credentials. A forced refresh re-mints only the
|
|
1035
|
-
* requested row
|
|
1036
|
-
*
|
|
1085
|
+
* falls back to sibling credentials. A generic forced refresh re-mints only the
|
|
1086
|
+
* requested row; with `refreshReason: "auth-recovery"` it may instead reuse a
|
|
1087
|
+
* still-usable recent mint of that row. Either way exact-account affinity holds
|
|
1088
|
+
* for operations whose provenance and policy boundary are tied to one workspace.
|
|
1037
1089
|
*
|
|
1038
1090
|
* Returns `undefined` when the row does not exist for `provider` or an
|
|
1039
1091
|
* explicit runtime/config API-key override suppresses OAuth.
|
|
@@ -1071,8 +1123,9 @@ export interface OAuthApi {
|
|
|
1071
1123
|
* Refresh the OAuth credential with the given id through a per-credential
|
|
1072
1124
|
* single-flight. Concurrent callers for the same row await the same upstream
|
|
1073
1125
|
* refresh attempt, which is required for providers that rotate refresh tokens
|
|
1074
|
-
* on every successful refresh. Mints
|
|
1075
|
-
*
|
|
1126
|
+
* on every successful refresh. Mints unless {@link OAuthRefreshByIdOptions.reuseRecentMint}
|
|
1127
|
+
* finds a recent local mint, or a delegated refresh given
|
|
1128
|
+
* {@link OAuthRefreshByIdOptions.reason} `"auth-recovery"` returns its recent mint.
|
|
1076
1129
|
*/
|
|
1077
1130
|
refresh(id: number, signal?: AbortSignal, options?: OAuthRefreshByIdOptions): Promise<AuthCredentialSnapshotEntry>;
|
|
1078
1131
|
/**
|
|
@@ -1084,6 +1137,12 @@ export interface OAuthApi {
|
|
|
1084
1137
|
): Promise<StoredOAuthRefreshResult<T>>;
|
|
1085
1138
|
}
|
|
1086
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Handle for one {@link SessionsApi.restrict} call. {@link SessionsApi.unrestrict}
|
|
1142
|
+
* lifts a restriction only with the lease of the call that installed it.
|
|
1143
|
+
*/
|
|
1144
|
+
export type SessionRestrictionLease = symbol;
|
|
1145
|
+
|
|
1087
1146
|
/** Session credential affinity operations. */
|
|
1088
1147
|
export interface SessionsApi {
|
|
1089
1148
|
/**
|
|
@@ -1101,14 +1160,46 @@ export interface SessionsApi {
|
|
|
1101
1160
|
*/
|
|
1102
1161
|
pin(provider: string, sessionId: string, credentialId: number, options?: { restoredAtMs?: number }): boolean;
|
|
1103
1162
|
/**
|
|
1104
|
-
* Copy
|
|
1163
|
+
* Copy stored credential affinities from one live session to another.
|
|
1105
1164
|
*
|
|
1106
1165
|
* The target receives its own sticky entries, so request resolution, usage
|
|
1107
1166
|
* blocking, credential rotation, metadata, and persisted pins all continue
|
|
1108
1167
|
* through the target session id without retaining a live dependency on the
|
|
1109
|
-
* source session.
|
|
1110
|
-
|
|
1111
|
-
|
|
1168
|
+
* source session. `include` limits the copy to the providers it accepts,
|
|
1169
|
+
* given whether the source's affinity is an explicit user pin.
|
|
1170
|
+
*/
|
|
1171
|
+
inherit(
|
|
1172
|
+
sourceSessionId: string,
|
|
1173
|
+
targetSessionId: string,
|
|
1174
|
+
include?: (provider: string, explicit: boolean) => boolean,
|
|
1175
|
+
): number;
|
|
1176
|
+
/**
|
|
1177
|
+
* Restrict one session's credentials for `provider` to the OAuth accounts
|
|
1178
|
+
* whose identity key (`email:<address>|org:<id>` for org-scoped providers;
|
|
1179
|
+
* otherwise `account:`, `email:`, or `project:` — the keys broker account
|
|
1180
|
+
* pools use) is listed. Selection, session pins, inherited affinity,
|
|
1181
|
+
* fallback passes, and rotation never leave the list, and runtime, config,
|
|
1182
|
+
* environment, and stored API keys are not used for that session; when no
|
|
1183
|
+
* listed account can serve, key resolution fails instead of borrowing
|
|
1184
|
+
* another account. An empty list allows no account. Replaces any earlier
|
|
1185
|
+
* restriction for the same provider and session; {@link inherit} does not
|
|
1186
|
+
* copy restrictions. Restrictions are never evicted, because that would
|
|
1187
|
+
* widen a live session; the owner passes the returned lease to
|
|
1188
|
+
* {@link unrestrict} when the session ends.
|
|
1189
|
+
*/
|
|
1190
|
+
restrict(provider: string, sessionId: string, identityKeys: readonly string[]): SessionRestrictionLease;
|
|
1191
|
+
/**
|
|
1192
|
+
* Lift the restriction that `lease` installed. Once a later {@link restrict}
|
|
1193
|
+
* call has replaced it for the same provider and session, this does nothing,
|
|
1194
|
+
* so a stale owner never lifts the restriction a newer owner relies on.
|
|
1195
|
+
*/
|
|
1196
|
+
unrestrict(provider: string, sessionId: string, lease: SessionRestrictionLease): void;
|
|
1197
|
+
/**
|
|
1198
|
+
* Whether stored credential `credentialId` may serve `sessionId`: always for
|
|
1199
|
+
* an unrestricted session, otherwise only an OAuth account its
|
|
1200
|
+
* {@link restrict} list names.
|
|
1201
|
+
*/
|
|
1202
|
+
permits(provider: string, sessionId: string, credentialId: number): boolean;
|
|
1112
1203
|
/**
|
|
1113
1204
|
* Release a session's sticky credential so its next {@link getApiKey} call
|
|
1114
1205
|
* re-runs native pool ranking. This never blocks or penalizes the released
|
package/src/auth/usage-cache.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { UsageCredential, UsageProvider, UsageReport } from "../usage";
|
|
|
6
6
|
|
|
7
7
|
const USAGE_CACHE_PREFIX = "usage_cache:";
|
|
8
8
|
const USAGE_FORCE_REFRESH_CACHE_PREFIX = "force-refresh:";
|
|
9
|
+
const USAGE_REPORT_KEY_PREFIX = "report:";
|
|
9
10
|
/** Minimum interval between non-exhausted header snapshots; used by UsageService. */
|
|
10
11
|
export const USAGE_HEADER_INGEST_INTERVAL_MS = 60_000;
|
|
11
12
|
const USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000;
|
|
@@ -45,6 +46,13 @@ function parseUsageCacheEntry<T>(raw: string): UsageCacheEntry<T> | undefined {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Cached reports drop the provider's upstream `raw` payload; only a fresh fetch carries it. */
|
|
50
|
+
function withoutRawPayload(value: unknown): unknown {
|
|
51
|
+
if (value === null || typeof value !== "object" || !("raw" in value)) return value;
|
|
52
|
+
const { raw: _raw, ...rest } = value;
|
|
53
|
+
return rest;
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
/** Convert a stored credential for usage providers; used by usage and health services. */
|
|
49
57
|
export function buildUsageCredential(credential: AuthCredential): UsageCredential {
|
|
50
58
|
if (credential.type === "api_key") {
|
|
@@ -196,12 +204,13 @@ export class UsageCache {
|
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
set<T>(key: string, entry: UsageCacheEntry<T>): void {
|
|
207
|
+
const value = withoutRawPayload(entry.value);
|
|
199
208
|
const payload = JSON.stringify({
|
|
200
|
-
value
|
|
209
|
+
value,
|
|
201
210
|
expiresAt: entry.expiresAt,
|
|
202
211
|
});
|
|
203
212
|
const durableExpiresAt =
|
|
204
|
-
|
|
213
|
+
value === null ? entry.expiresAt : Math.max(entry.expiresAt, Date.now() + USAGE_LAST_GOOD_RETENTION_MS);
|
|
205
214
|
this.#store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
|
|
206
215
|
}
|
|
207
216
|
|
|
@@ -272,7 +281,7 @@ export class UsageCache {
|
|
|
272
281
|
const baseUrl = this.#normalizeUsageBaseUrl(request.baseUrl) || "default";
|
|
273
282
|
const identity = usageCacheIdentity(request.credential);
|
|
274
283
|
const providerKey = this.#usageCacheProviderKey(request.provider);
|
|
275
|
-
const cacheKey =
|
|
284
|
+
const cacheKey = `${USAGE_REPORT_KEY_PREFIX}${providerKey}:${baseUrl}:${identity}`;
|
|
276
285
|
const cacheKeys = this.#usageReportCacheKeysByProvider.get(request.provider) ?? new Set<string>();
|
|
277
286
|
cacheKeys.add(cacheKey);
|
|
278
287
|
this.#usageReportCacheKeysByProvider.set(request.provider, cacheKeys);
|
|
@@ -334,7 +343,7 @@ export class UsageCache {
|
|
|
334
343
|
this.#epoch += 1;
|
|
335
344
|
this.#bumpRefreshEpoch(provider);
|
|
336
345
|
const expired = Date.now() - 1;
|
|
337
|
-
const prefix =
|
|
346
|
+
const prefix = `${USAGE_REPORT_KEY_PREFIX}${this.#usageCacheProviderKey(provider)}:`;
|
|
338
347
|
if (this.deletePrefix(prefix)) return;
|
|
339
348
|
const cacheKeys = new Set(this.#usageReportCacheKeysByProvider.get(provider));
|
|
340
349
|
for (const entry of this.#pool.entries(provider)) {
|
|
@@ -361,7 +370,9 @@ export class UsageCache {
|
|
|
361
370
|
): Promise<void> {
|
|
362
371
|
this.#epoch += 1;
|
|
363
372
|
this.#bumpRefreshEpoch(provider);
|
|
364
|
-
const prefix = provider
|
|
373
|
+
const prefix = provider
|
|
374
|
+
? `${USAGE_REPORT_KEY_PREFIX}${this.#usageCacheProviderKey(provider)}:`
|
|
375
|
+
: USAGE_REPORT_KEY_PREFIX;
|
|
365
376
|
if (!this.deletePrefix(prefix)) {
|
|
366
377
|
// Third-party stores may not support prefix deletion. Clear every active
|
|
367
378
|
// request key instead, including API-key and environment credentials.
|
package/src/auth/usage.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authPolicyFor } from "@linxiraos/pi-catalog/compat/auth";
|
|
2
2
|
import { $pickenv, logger } from "@linxiraos/pi-utils";
|
|
3
|
+
import { getEnvApiKey } from "../env-api-key";
|
|
3
4
|
import * as AIError from "../error";
|
|
4
|
-
import { getEnvApiKey } from "../stream";
|
|
5
5
|
import type { OAuthCredentials } from "../registry/oauth/types";
|
|
6
6
|
import type { Provider } from "../types";
|
|
7
7
|
import { resolveUsedFraction } from "../usage";
|
|
@@ -871,7 +871,7 @@ export class UsageService implements UsageApi {
|
|
|
871
871
|
await this.#deps.cache.clearReports(provider, () => this.#collectUsageRequests());
|
|
872
872
|
|
|
873
873
|
if (this.#deps.store.invalidateUsageCache) {
|
|
874
|
-
await this.#deps.store.invalidateUsageCache(signal).catch(err => {
|
|
874
|
+
await this.#deps.store.invalidateUsageCache(provider, signal).catch(err => {
|
|
875
875
|
logger.debug("Failed to notify store of stale usage", { err });
|
|
876
876
|
});
|
|
877
877
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP client for the
|
|
2
|
+
* HTTP client for the zetacode auth-broker server.
|
|
3
3
|
*
|
|
4
4
|
* Used by {@link RemoteAuthCredentialStore} (snapshot pulls) and by
|
|
5
|
-
* `
|
|
5
|
+
* `zetacode auth-broker status` (liveness checks). All endpoints except
|
|
6
6
|
* `/v1/healthz` require a bearer token.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -309,8 +309,11 @@ export class AuthBrokerClient {
|
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
notifyUsageStale(signal?: AbortSignal): Promise<UsageStaleResponse> {
|
|
313
|
-
|
|
312
|
+
notifyUsageStale(provider?: string, signal?: AbortSignal): Promise<UsageStaleResponse> {
|
|
313
|
+
const params = new URLSearchParams();
|
|
314
|
+
if (provider) params.set("provider", provider);
|
|
315
|
+
const path = `/v1/usage/stale${params.size > 0 ? `?${params.toString()}` : ""}`;
|
|
316
|
+
return this.#request<UsageStaleResponse>("POST", path, {
|
|
314
317
|
schema: "usageStaleResponseSchema",
|
|
315
318
|
signal,
|
|
316
319
|
});
|