@oh-my-pi/pi-ai 18.6.3 → 18.8.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 +34 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +1 -0
- package/dist/types/auth/store.d.ts +6 -0
- package/dist/types/auth-broker/remote-store.d.ts +1 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/dialect/terminator-wait.d.ts +19 -0
- package/dist/types/env-api-key.d.ts +21 -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 +1 -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/google-auth.d.ts +4 -1
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-responses-wire.d.ts +0 -54
- package/dist/types/providers/openai-shared.d.ts +4 -1
- package/dist/types/registry/oauth/cursor.d.ts +9 -1
- package/dist/types/registry/oauth/index.d.ts +2 -0
- package/dist/types/stream.d.ts +0 -21
- package/dist/types/transcription/types.d.ts +2 -1
- package/dist/types/types.d.ts +32 -10
- package/dist/types/utils/aws-profile.d.ts +5 -0
- package/package.json +6 -6
- package/src/auth/affinity.ts +33 -24
- package/src/auth/blocks.ts +90 -14
- package/src/auth/cascade.ts +1 -1
- package/src/auth/select.ts +27 -2
- package/src/auth/sqlite-credential-store.ts +85 -42
- package/src/auth/store.ts +6 -0
- package/src/auth/usage-cache.ts +16 -5
- package/src/auth/usage.ts +1 -1
- package/src/auth-broker/discover.ts +5 -4
- package/src/auth-broker/remote-store.ts +117 -46
- package/src/auth-broker/server.ts +283 -123
- package/src/auth-broker/snapshot-cache.ts +142 -2
- package/src/auth-gateway/server.ts +19 -5
- package/src/auth-gateway/session-state.ts +51 -9
- package/src/dialect/deepseek.ts +10 -2
- 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/images/index.ts +3 -17
- package/src/images/openai-hosted.ts +11 -4
- package/src/images/openai-images.ts +19 -13
- package/src/images/shared.ts +10 -2
- package/src/index.ts +1 -0
- package/src/providers/anthropic.ts +39 -13
- package/src/providers/apple-foundation-models.ts +10 -4
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/connect-frame.ts +107 -0
- package/src/providers/cursor/interaction-query.ts +1 -9
- package/src/providers/cursor.ts +87 -47
- package/src/providers/devin.ts +32 -41
- package/src/providers/gitlab-duo-workflow.ts +5 -2
- package/src/providers/google-auth.ts +10 -3
- package/src/providers/google.ts +1 -1
- package/src/providers/mock.ts +4 -0
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-codex-responses.ts +49 -45
- package/src/providers/openai-completions.ts +1 -1
- package/src/providers/openai-responses-wire.ts +0 -54
- package/src/providers/openai-responses.ts +5 -4
- package/src/providers/openai-shared.ts +73 -27
- package/src/providers/transcriptions-server.ts +1 -1
- package/src/providers/transform-messages.ts +17 -13
- package/src/registry/aws.ts +51 -14
- package/src/registry/cloudflare-ai-gateway.ts +24 -1
- package/src/registry/hooks/env.ts +2 -5
- package/src/registry/oauth/cursor.ts +57 -3
- package/src/registry/oauth/index.ts +5 -0
- package/src/stream.ts +3 -72
- package/src/transcription/types.ts +2 -1
- package/src/types.ts +52 -11
- package/src/usage/cursor.ts +14 -25
- package/src/usage/minimax-code.ts +8 -1
- package/src/utils/aws-profile.ts +10 -4
- package/src/utils/thinking-loop.ts +18 -3
- package/src/utils.ts +22 -6
package/src/auth/affinity.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import {
|
|
2
|
+
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
3
|
+
import { getEnvApiKey } from "../env-api-key";
|
|
3
4
|
import type { AuthCredential, OAuthCredential, SessionsApi } from "./types";
|
|
4
5
|
import type { AuthCredentialStore } from "./store";
|
|
5
6
|
import type { CredentialPool } from "./pool";
|
|
@@ -14,6 +15,11 @@ const SESSION_STICKY_TTL_SEC = 30 * 24 * 60 * 60;
|
|
|
14
15
|
* in-memory sticky stays exact; only the copy other processes resume from lags.
|
|
15
16
|
*/
|
|
16
17
|
const SESSION_STICKY_PERSIST_INTERVAL_MS = 60_000;
|
|
18
|
+
/**
|
|
19
|
+
* In-memory pins kept per provider. Long-lived gateways mint a session id per
|
|
20
|
+
* conversation; evicted sessions resume from their persisted sticky row.
|
|
21
|
+
*/
|
|
22
|
+
const SESSION_AFFINITY_MAX_SESSIONS_PER_PROVIDER = 256;
|
|
17
23
|
|
|
18
24
|
/** What this process last wrote (or read) for one persisted sticky row. */
|
|
19
25
|
type PersistedSticky = {
|
|
@@ -36,9 +42,9 @@ export type SessionCredential = {
|
|
|
36
42
|
/** Session → credential affinity (pins), persisted in the store cache. */
|
|
37
43
|
export class SessionAffinity implements SessionsApi {
|
|
38
44
|
/** Tracks the last used credential per provider for a session (used for rate-limit switching). */
|
|
39
|
-
#sessionLastCredential: Map<string,
|
|
40
|
-
/** Persisted sticky rows keyed by
|
|
41
|
-
#persistedSticky: Map<string, PersistedSticky
|
|
45
|
+
#sessionLastCredential: Map<string, LRUCache<string, SessionCredential>> = new Map();
|
|
46
|
+
/** Persisted sticky rows per provider, keyed by session id, so unchanged re-records skip the write. */
|
|
47
|
+
#persistedSticky: Map<string, LRUCache<string, PersistedSticky>> = new Map();
|
|
42
48
|
#store: AuthCredentialStore;
|
|
43
49
|
#pool: CredentialPool;
|
|
44
50
|
#overrides: KeyOverrides;
|
|
@@ -49,13 +55,21 @@ export class SessionAffinity implements SessionsApi {
|
|
|
49
55
|
this.#overrides = overrides;
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
/** Bounded per-provider session map, created on first use. */
|
|
59
|
+
static #sessionsFor<V>(maps: Map<string, LRUCache<string, V>>, provider: string): LRUCache<string, V> {
|
|
60
|
+
let sessions = maps.get(provider);
|
|
61
|
+
if (!sessions) {
|
|
62
|
+
sessions = new LRUCache<string, V>({ max: SESSION_AFFINITY_MAX_SESSIONS_PER_PROVIDER });
|
|
63
|
+
maps.set(provider, sessions);
|
|
64
|
+
}
|
|
65
|
+
return sessions;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
/** Drop every pin for a provider, both in memory and in the persisted cache. */
|
|
53
69
|
clearProvider(provider: string): void {
|
|
54
70
|
this.#sessionLastCredential.delete(provider);
|
|
71
|
+
this.#persistedSticky.delete(provider);
|
|
55
72
|
const prefix = `${SESSION_STICKY_CACHE_PREFIX}${provider}:`;
|
|
56
|
-
for (const cacheKey of this.#persistedSticky.keys()) {
|
|
57
|
-
if (cacheKey.startsWith(prefix)) this.#persistedSticky.delete(cacheKey);
|
|
58
|
-
}
|
|
59
73
|
try {
|
|
60
74
|
this.#store.deleteCachePrefix?.(prefix);
|
|
61
75
|
} catch (err) {
|
|
@@ -90,7 +104,7 @@ export class SessionAffinity implements SessionsApi {
|
|
|
90
104
|
if (!sessionId) return;
|
|
91
105
|
const nowMs = lastUsedAtMs ?? Date.now();
|
|
92
106
|
const credentialId = this.#pool.entries(provider)[index]?.id;
|
|
93
|
-
const sessionMap = this.#sessionLastCredential
|
|
107
|
+
const sessionMap = SessionAffinity.#sessionsFor(this.#sessionLastCredential, provider);
|
|
94
108
|
const previous = sessionMap.get(sessionId);
|
|
95
109
|
const sameCredential =
|
|
96
110
|
previous?.type === type &&
|
|
@@ -104,12 +118,12 @@ export class SessionAffinity implements SessionsApi {
|
|
|
104
118
|
...(isExplicit ? { explicit: true as const } : {}),
|
|
105
119
|
};
|
|
106
120
|
sessionMap.set(sessionId, sessionCredential);
|
|
107
|
-
this.#sessionLastCredential.set(provider, sessionMap);
|
|
108
121
|
|
|
109
122
|
if (credentialId === undefined) return;
|
|
110
123
|
const cacheKey = `${SESSION_STICKY_CACHE_PREFIX}${provider}:${sessionId}`;
|
|
111
124
|
const expiresAtSec = Math.floor(nowMs / 1000) + SESSION_STICKY_TTL_SEC;
|
|
112
|
-
const
|
|
125
|
+
const persistedSessions = SessionAffinity.#sessionsFor(this.#persistedSticky, provider);
|
|
126
|
+
const persisted = persistedSessions.get(sessionId);
|
|
113
127
|
if (
|
|
114
128
|
persisted &&
|
|
115
129
|
persisted.type === type &&
|
|
@@ -121,14 +135,14 @@ export class SessionAffinity implements SessionsApi {
|
|
|
121
135
|
}
|
|
122
136
|
try {
|
|
123
137
|
this.#store.setCache(cacheKey, JSON.stringify(sessionCredential), expiresAtSec);
|
|
124
|
-
|
|
138
|
+
persistedSessions.set(sessionId, {
|
|
125
139
|
type,
|
|
126
140
|
credentialId,
|
|
127
141
|
explicit: isExplicit,
|
|
128
142
|
lastUsedAtMs: nowMs,
|
|
129
143
|
});
|
|
130
144
|
} catch (err) {
|
|
131
|
-
|
|
145
|
+
persistedSessions.delete(sessionId);
|
|
132
146
|
logger.debug("Failed to write session sticky credential to persistent store cache", { err });
|
|
133
147
|
}
|
|
134
148
|
}
|
|
@@ -136,8 +150,7 @@ export class SessionAffinity implements SessionsApi {
|
|
|
136
150
|
/** Retrieves the last credential used by a session. */
|
|
137
151
|
get(provider: string, sessionId: string | undefined): SessionCredential | undefined {
|
|
138
152
|
if (!sessionId) return undefined;
|
|
139
|
-
|
|
140
|
-
const live = sessionMap?.get(sessionId);
|
|
153
|
+
const live = this.#sessionLastCredential.get(provider)?.get(sessionId);
|
|
141
154
|
if (live) {
|
|
142
155
|
// Another process can add or drop rows mid-session and the pool is an
|
|
143
156
|
// index-ordered snapshot, so re-resolve the pin through its durable row
|
|
@@ -147,7 +160,7 @@ export class SessionAffinity implements SessionsApi {
|
|
|
147
160
|
const stored = this.#pool.entries(provider);
|
|
148
161
|
const actualIndex = stored.findIndex(entry => entry.id === live.credentialId);
|
|
149
162
|
if (actualIndex === -1 || stored[actualIndex]?.credential.type !== live.type) {
|
|
150
|
-
|
|
163
|
+
this.#sessionLastCredential.get(provider)?.delete(sessionId);
|
|
151
164
|
return undefined;
|
|
152
165
|
}
|
|
153
166
|
live.index = actualIndex;
|
|
@@ -163,22 +176,18 @@ export class SessionAffinity implements SessionsApi {
|
|
|
163
176
|
const stored = this.#pool.entries(provider);
|
|
164
177
|
const actualIndex = stored.findIndex(entry => entry.id === val.credentialId);
|
|
165
178
|
if (actualIndex === -1 || stored[actualIndex]?.credential.type !== val.type) {
|
|
166
|
-
this.#persistedSticky.delete(
|
|
179
|
+
this.#persistedSticky.get(provider)?.delete(sessionId);
|
|
167
180
|
this.#store.setCache(cacheKey, "", 0);
|
|
168
181
|
return undefined;
|
|
169
182
|
}
|
|
170
183
|
val.index = actualIndex;
|
|
171
184
|
} else {
|
|
172
185
|
// Fallback: drop unsafe index-only cache rows to prevent wrong-account routing
|
|
173
|
-
this.#persistedSticky.delete(
|
|
186
|
+
this.#persistedSticky.get(provider)?.delete(sessionId);
|
|
174
187
|
this.#store.setCache(cacheKey, "", 0);
|
|
175
188
|
return undefined;
|
|
176
189
|
}
|
|
177
190
|
|
|
178
|
-
if (!sessionMap) {
|
|
179
|
-
sessionMap = new Map();
|
|
180
|
-
this.#sessionLastCredential.set(provider, sessionMap);
|
|
181
|
-
}
|
|
182
191
|
const sessionVal: SessionCredential = {
|
|
183
192
|
type: val.type,
|
|
184
193
|
index: val.index,
|
|
@@ -187,14 +196,14 @@ export class SessionAffinity implements SessionsApi {
|
|
|
187
196
|
...(val.explicit === true ? { explicit: true } : {}),
|
|
188
197
|
};
|
|
189
198
|
if (typeof val.lastUsedAtMs === "number") {
|
|
190
|
-
this.#persistedSticky.set(
|
|
199
|
+
SessionAffinity.#sessionsFor(this.#persistedSticky, provider).set(sessionId, {
|
|
191
200
|
type: val.type,
|
|
192
201
|
credentialId: val.credentialId,
|
|
193
202
|
explicit: val.explicit === true,
|
|
194
203
|
lastUsedAtMs: val.lastUsedAtMs,
|
|
195
204
|
});
|
|
196
205
|
}
|
|
197
|
-
|
|
206
|
+
SessionAffinity.#sessionsFor(this.#sessionLastCredential, provider).set(sessionId, sessionVal);
|
|
198
207
|
return sessionVal;
|
|
199
208
|
}
|
|
200
209
|
} catch (err) {
|
|
@@ -214,7 +223,7 @@ export class SessionAffinity implements SessionsApi {
|
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
const cacheKey = `${SESSION_STICKY_CACHE_PREFIX}${provider}:${sessionId}`;
|
|
217
|
-
this.#persistedSticky.delete(
|
|
226
|
+
this.#persistedSticky.get(provider)?.delete(sessionId);
|
|
218
227
|
try {
|
|
219
228
|
this.#store.setCache(cacheKey, "", 0);
|
|
220
229
|
} catch (err) {
|
package/src/auth/blocks.ts
CHANGED
|
@@ -64,6 +64,14 @@ export function credentialBlockScopesForRequest(
|
|
|
64
64
|
*/
|
|
65
65
|
type CredentialBackoff = { until: number; timed: boolean; probeAfter: number; persisted?: boolean };
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* One read of a credential's persisted blocks under a providerKey, keyed by
|
|
69
|
+
* scope (`""` is unscoped). `undefined`: the store has no bulk read, so use
|
|
70
|
+
* per-scope point reads. `null`: unreadable (damaged store or failed read), so
|
|
71
|
+
* it neither contributes deadlines nor retires mirrored in-memory blocks.
|
|
72
|
+
*/
|
|
73
|
+
type PersistedBlockScopes = ReadonlyMap<string, number> | null | undefined;
|
|
74
|
+
|
|
67
75
|
/** Latch for unrecoverably corrupt persisted block stores, shared with the credential pool. */
|
|
68
76
|
export class BlockStoreHealth {
|
|
69
77
|
readonly #sourceLabel: string | undefined;
|
|
@@ -134,8 +142,16 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Returns in-memory block expiry timestamp for a credential/key pair, cleaning up expired entries.
|
|
147
|
+
* `persistedScopes` is a bulk read for the key's providerKey; omitted, the store is point-read.
|
|
148
|
+
*/
|
|
149
|
+
#getCredentialBlockedUntilForKey(
|
|
150
|
+
backoffKey: string,
|
|
151
|
+
credentialId: number,
|
|
152
|
+
nowMs: number,
|
|
153
|
+
persistedScopes: PersistedBlockScopes = undefined,
|
|
154
|
+
): number | undefined {
|
|
139
155
|
const block = this.#credentialBackoff.get(backoffKey)?.get(credentialId);
|
|
140
156
|
if (!block) return undefined;
|
|
141
157
|
// Once mirrored successfully, the store is authoritative for cross-process
|
|
@@ -143,12 +159,19 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
143
159
|
// Upserts are longest-wins, so a persisted row shorter than the deadline this
|
|
144
160
|
// process mirrored means the row was deleted and replaced: the old deadline
|
|
145
161
|
// was superseded and must not win the next longest-wins merge.
|
|
146
|
-
if (
|
|
162
|
+
if (
|
|
163
|
+
block.persisted &&
|
|
164
|
+
!this.#deps.health.damaged &&
|
|
165
|
+
persistedScopes !== null &&
|
|
166
|
+
(persistedScopes !== undefined || this.#deps.store.getCredentialBlock)
|
|
167
|
+
) {
|
|
147
168
|
const separator = backoffKey.indexOf("\0");
|
|
148
169
|
const providerKey = separator < 0 ? backoffKey : backoffKey.slice(0, separator);
|
|
149
170
|
const scope = separator < 0 ? "" : backoffKey.slice(separator + 1);
|
|
150
171
|
try {
|
|
151
|
-
const persisted =
|
|
172
|
+
const persisted = persistedScopes
|
|
173
|
+
? persistedScopes.get(scope)
|
|
174
|
+
: this.#deps.store.getCredentialBlock?.(credentialId, providerKey, scope);
|
|
152
175
|
if (persisted === undefined || persisted < block.until) {
|
|
153
176
|
this.#deleteCredentialBackoff(backoffKey, credentialId);
|
|
154
177
|
return undefined;
|
|
@@ -175,7 +198,9 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
175
198
|
credentialId: number,
|
|
176
199
|
providerKey: string,
|
|
177
200
|
blockScope: string | undefined,
|
|
201
|
+
persistedScopes: PersistedBlockScopes = undefined,
|
|
178
202
|
): number | undefined {
|
|
203
|
+
if (persistedScopes !== undefined) return persistedScopes?.get(blockScope ?? "");
|
|
179
204
|
if (this.#deps.health.damaged) return undefined;
|
|
180
205
|
const getCredentialBlock = this.#deps.store.getCredentialBlock?.bind(this.#deps.store);
|
|
181
206
|
if (!getCredentialBlock) return undefined;
|
|
@@ -193,6 +218,20 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
|
|
221
|
+
/** Every persisted scope of one credential/providerKey in a single store read, when supported. */
|
|
222
|
+
#readPersistedCredentialBlockScopes(credentialId: number, providerKey: string): PersistedBlockScopes {
|
|
223
|
+
const store = this.#deps.store;
|
|
224
|
+
if (!store.getCredentialBlockScopes) return undefined;
|
|
225
|
+
if (this.#deps.health.damaged) return null;
|
|
226
|
+
try {
|
|
227
|
+
return store.getCredentialBlockScopes(credentialId, providerKey);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (this.#deps.health.handle(err)) return null;
|
|
230
|
+
logger.debug("Failed to read credential blocks from persistent store", { err, credentialId, providerKey });
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
196
235
|
#readPersistedCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number {
|
|
197
236
|
if (this.#deps.health.damaged) return 0;
|
|
198
237
|
const getCredentialBlockReconcileAfter = this.#deps.store.getCredentialBlockReconcileAfter?.bind(
|
|
@@ -215,13 +254,19 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
215
254
|
}
|
|
216
255
|
}
|
|
217
256
|
|
|
218
|
-
#exactBlockedUntil(
|
|
257
|
+
#exactBlockedUntil(
|
|
258
|
+
credentialId: number,
|
|
259
|
+
providerKey: string,
|
|
260
|
+
scope: string | undefined,
|
|
261
|
+
persistedScopes: PersistedBlockScopes = this.#readPersistedCredentialBlockScopes(credentialId, providerKey),
|
|
262
|
+
): number | undefined {
|
|
219
263
|
const memory = this.#getCredentialBlockedUntilForKey(
|
|
220
264
|
scopedBackoffKey(providerKey, scope),
|
|
221
265
|
credentialId,
|
|
222
266
|
Date.now(),
|
|
267
|
+
persistedScopes,
|
|
223
268
|
);
|
|
224
|
-
const persisted = this.#readPersistedCredentialBlock(credentialId, providerKey, scope);
|
|
269
|
+
const persisted = this.#readPersistedCredentialBlock(credentialId, providerKey, scope, persistedScopes);
|
|
225
270
|
return memory === undefined ? persisted : persisted === undefined ? memory : Math.max(memory, persisted);
|
|
226
271
|
}
|
|
227
272
|
|
|
@@ -231,6 +276,22 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
231
276
|
providerKey: string,
|
|
232
277
|
credentialIndex: number,
|
|
233
278
|
blockScopeOrScopes: string | readonly string[] | undefined = undefined,
|
|
279
|
+
): number | undefined {
|
|
280
|
+
const credentialId = this.#deps.pool.entries(provider)[credentialIndex]?.id;
|
|
281
|
+
if (credentialId === undefined) return undefined;
|
|
282
|
+
return this.#blockedUntilById(
|
|
283
|
+
credentialId,
|
|
284
|
+
providerKey,
|
|
285
|
+
blockScopeOrScopes,
|
|
286
|
+
this.#readPersistedCredentialBlockScopes(credentialId, providerKey),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#blockedUntilById(
|
|
291
|
+
credentialId: number,
|
|
292
|
+
providerKey: string,
|
|
293
|
+
blockScopeOrScopes: string | readonly string[] | undefined,
|
|
294
|
+
persistedScopes: PersistedBlockScopes,
|
|
234
295
|
): number | undefined {
|
|
235
296
|
const nowMs = Date.now();
|
|
236
297
|
// A request honours its own scope plus any legacy catch-all scope, so a
|
|
@@ -239,21 +300,25 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
239
300
|
...PROTECTED_BLOCK_SCOPES,
|
|
240
301
|
...(typeof blockScopeOrScopes === "string" ? [blockScopeOrScopes] : (blockScopeOrScopes ?? [])),
|
|
241
302
|
].filter(scope => scope.length > 0);
|
|
242
|
-
|
|
243
|
-
if (credentialId === undefined) return undefined;
|
|
244
|
-
let blockedUntil = this.#getCredentialBlockedUntilForKey(providerKey, credentialId, nowMs);
|
|
303
|
+
let blockedUntil = this.#getCredentialBlockedUntilForKey(providerKey, credentialId, nowMs, persistedScopes);
|
|
245
304
|
for (const blockScope of scopes) {
|
|
246
305
|
const scopedBlockedUntil = this.#getCredentialBlockedUntilForKey(
|
|
247
306
|
scopedBackoffKey(providerKey, blockScope),
|
|
248
307
|
credentialId,
|
|
249
308
|
nowMs,
|
|
309
|
+
persistedScopes,
|
|
250
310
|
);
|
|
251
311
|
if (scopedBlockedUntil !== undefined && (blockedUntil === undefined || scopedBlockedUntil > blockedUntil)) {
|
|
252
312
|
blockedUntil = scopedBlockedUntil;
|
|
253
313
|
}
|
|
254
314
|
}
|
|
255
315
|
|
|
256
|
-
const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(
|
|
316
|
+
const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(
|
|
317
|
+
credentialId,
|
|
318
|
+
providerKey,
|
|
319
|
+
"",
|
|
320
|
+
persistedScopes,
|
|
321
|
+
);
|
|
257
322
|
if (
|
|
258
323
|
persistedGlobalBlockedUntil !== undefined &&
|
|
259
324
|
(blockedUntil === undefined || persistedGlobalBlockedUntil > blockedUntil)
|
|
@@ -261,7 +326,12 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
261
326
|
blockedUntil = persistedGlobalBlockedUntil;
|
|
262
327
|
}
|
|
263
328
|
for (const blockScope of scopes) {
|
|
264
|
-
const persistedScopedBlockedUntil = this.#readPersistedCredentialBlock(
|
|
329
|
+
const persistedScopedBlockedUntil = this.#readPersistedCredentialBlock(
|
|
330
|
+
credentialId,
|
|
331
|
+
providerKey,
|
|
332
|
+
blockScope,
|
|
333
|
+
persistedScopes,
|
|
334
|
+
);
|
|
265
335
|
if (
|
|
266
336
|
persistedScopedBlockedUntil !== undefined &&
|
|
267
337
|
(blockedUntil === undefined || persistedScopedBlockedUntil > blockedUntil)
|
|
@@ -440,14 +510,20 @@ export class CredentialBlocks implements BlocksApi {
|
|
|
440
510
|
if (!this.supportsHealing(provider)) return false;
|
|
441
511
|
const credentialId = this.#deps.pool.entries(provider)[credentialIndex]?.id;
|
|
442
512
|
if (credentialId === undefined) return false;
|
|
443
|
-
|
|
513
|
+
// One persisted read serves the protected-scope, global, and requested checks.
|
|
514
|
+
const persistedScopes = this.#readPersistedCredentialBlockScopes(credentialId, providerKey);
|
|
515
|
+
if (
|
|
516
|
+
PROTECTED_BLOCK_SCOPES.some(
|
|
517
|
+
scope => this.#exactBlockedUntil(credentialId, providerKey, scope, persistedScopes) !== undefined,
|
|
518
|
+
)
|
|
519
|
+
)
|
|
444
520
|
return false;
|
|
445
521
|
if (
|
|
446
522
|
!this.#deps.strategies(provider)?.healsGlobalBlocks &&
|
|
447
|
-
this
|
|
523
|
+
this.#blockedUntilById(credentialId, providerKey, undefined, persistedScopes) !== undefined
|
|
448
524
|
)
|
|
449
525
|
return false;
|
|
450
|
-
return this
|
|
526
|
+
return this.#blockedUntilById(credentialId, providerKey, blockScopeOrScopes, persistedScopes) !== undefined;
|
|
451
527
|
}
|
|
452
528
|
|
|
453
529
|
/**
|
package/src/auth/cascade.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { authPolicyFor } from "@oh-my-pi/pi-catalog/compat/auth";
|
|
2
2
|
import { $env, $envExact } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type ApiKeyResolver, markAfterSiblingWait, type ResolvedApiKey } from "../auth-retry";
|
|
4
|
+
import { getEnvApiKey, getEnvApiKeyName } from "../env-api-key";
|
|
4
5
|
import * as AIError from "../error";
|
|
5
6
|
import { isUsageLimitOutcome } from "../error/rate-limit";
|
|
6
7
|
import { AUTHENTICATED_SENTINEL } from "../registry/types";
|
|
7
|
-
import { getEnvApiKey, getEnvApiKeyName } from "../stream";
|
|
8
8
|
import type { SessionAffinity } from "./affinity";
|
|
9
9
|
import type { CredentialPool } from "./pool";
|
|
10
10
|
import type { CredentialSelector } from "./select";
|
package/src/auth/select.ts
CHANGED
|
@@ -28,7 +28,13 @@ import {
|
|
|
28
28
|
} from "./rank";
|
|
29
29
|
import { mergeRefreshedCredential, OAUTH_REFRESH_SKEW_MS, type OAuthRefresher } from "./refresh";
|
|
30
30
|
import type { AuthCredentialStore } from "./store";
|
|
31
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
ApiKeyCredential,
|
|
33
|
+
AuthApiKeyOptions,
|
|
34
|
+
AuthCredential,
|
|
35
|
+
OAuthCredential,
|
|
36
|
+
StoredAuthCredential,
|
|
37
|
+
} from "./types";
|
|
32
38
|
import type { UsageService } from "./usage";
|
|
33
39
|
import {
|
|
34
40
|
isUsageLimitReached,
|
|
@@ -690,6 +696,10 @@ export class CredentialSelector {
|
|
|
690
696
|
const forceRefreshIndex = options?.forceRefresh
|
|
691
697
|
? (sessionPreferredIndex ?? candidates[0]?.selection.index)
|
|
692
698
|
: undefined;
|
|
699
|
+
// Each candidate's synchronous prefix below runs back to back inside `map`,
|
|
700
|
+
// before any await, so one provider re-list serves every initial rebind.
|
|
701
|
+
// Resyncs after an await (refresh, disable) re-read the store.
|
|
702
|
+
let preflightRows: StoredAuthCredential[] | undefined;
|
|
693
703
|
await Promise.all(
|
|
694
704
|
candidates.map(async candidate => {
|
|
695
705
|
const force = forceRefreshIndex !== undefined && candidate.selection.index === forceRefreshIndex;
|
|
@@ -697,7 +707,8 @@ export class CredentialSelector {
|
|
|
697
707
|
let syncedPeerCredential = false;
|
|
698
708
|
if (initialCredentialId !== undefined) {
|
|
699
709
|
const beforeSync = candidate.selection.credential;
|
|
700
|
-
|
|
710
|
+
preflightRows ??= this.#reloadProviderRows(provider);
|
|
711
|
+
if (!this.#rebindOAuthSelection(preflightRows, candidate.selection, initialCredentialId)) return;
|
|
701
712
|
syncedPeerCredential = !authCredentialEquals(beforeSync, candidate.selection.credential);
|
|
702
713
|
}
|
|
703
714
|
const hasFreshAccess = Date.now() + OAUTH_REFRESH_SKEW_MS < candidate.selection.credential.expires;
|
|
@@ -881,11 +892,25 @@ export class CredentialSelector {
|
|
|
881
892
|
selection: { credential: OAuthCredential; index: number },
|
|
882
893
|
credentialId: number,
|
|
883
894
|
): boolean {
|
|
895
|
+
return this.#rebindOAuthSelection(this.#reloadProviderRows(provider), selection, credentialId);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Re-list one provider's active rows and adopt them in the pool. */
|
|
899
|
+
#reloadProviderRows(provider: string): StoredAuthCredential[] {
|
|
884
900
|
const latestRows = this.#deps.store.listAuthCredentials(provider);
|
|
885
901
|
this.#deps.pool.replace(
|
|
886
902
|
provider,
|
|
887
903
|
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
888
904
|
);
|
|
905
|
+
return latestRows;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Point `selection` at row `credentialId` in `latestRows`; false when it is gone or no longer OAuth. */
|
|
909
|
+
#rebindOAuthSelection(
|
|
910
|
+
latestRows: readonly StoredAuthCredential[],
|
|
911
|
+
selection: { credential: OAuthCredential; index: number },
|
|
912
|
+
credentialId: number,
|
|
913
|
+
): boolean {
|
|
889
914
|
const latestIndex = latestRows.findIndex(row => row.id === credentialId);
|
|
890
915
|
if (latestIndex === -1) return false;
|
|
891
916
|
const latest = latestRows[latestIndex];
|
|
@@ -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
|
|
@@ -386,11 +392,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
386
392
|
#updateIfMatchesWithLeaseStmt: Statement;
|
|
387
393
|
#deleteIfMatchesWithLeaseStmt: Statement;
|
|
388
394
|
#getCredentialBlockStmt: Statement;
|
|
389
|
-
#
|
|
395
|
+
#listCredentialBlockScopesStmt: Statement;
|
|
396
|
+
#listCredentialBlocksStmt: Statement;
|
|
390
397
|
#upsertCredentialBlockStmt: Statement;
|
|
391
398
|
#deleteCredentialBlocksStmt: Statement;
|
|
392
399
|
#deleteCredentialBlockStmt: Statement;
|
|
393
400
|
#deleteExpiredCredentialBlocksStmt: Statement;
|
|
401
|
+
/** Wall-clock time of the last expired-block sweep; throttles read-path sweeps. */
|
|
402
|
+
#lastCredentialBlockSweepMs = Number.NEGATIVE_INFINITY;
|
|
394
403
|
#acquireCredentialRefreshLeaseStmt: Statement;
|
|
395
404
|
#getCredentialRefreshLeaseStmt: Statement;
|
|
396
405
|
#renewCredentialRefreshLeaseStmt: Statement;
|
|
@@ -475,12 +484,18 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
475
484
|
this.#getCredentialBlockStmt = this.#db.prepare(
|
|
476
485
|
"SELECT blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
477
486
|
);
|
|
478
|
-
this.#
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
487
|
+
this.#listCredentialBlockScopesStmt = this.#db.prepare(
|
|
488
|
+
"SELECT block_scope, blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND blocked_until_ms > ?",
|
|
489
|
+
);
|
|
490
|
+
// One statement for any number of ids: `json_each` keeps the caller's id
|
|
491
|
+
// order (`ids.key`) and the per-credential provider/scope order.
|
|
492
|
+
this.#listCredentialBlocksStmt = this.#db.prepare(
|
|
493
|
+
`SELECT b.credential_id, b.provider_key, b.block_scope, b.blocked_until_ms, b.updated_at
|
|
494
|
+
FROM json_each(?) AS ids
|
|
495
|
+
JOIN auth_credential_blocks AS b ON b.credential_id = ids.value
|
|
496
|
+
WHERE b.blocked_until_ms > ?
|
|
497
|
+
AND NOT (b.provider_key = ? AND b.block_scope = ?)
|
|
498
|
+
ORDER BY ids.key ASC, b.provider_key ASC, b.block_scope ASC`,
|
|
484
499
|
);
|
|
485
500
|
this.#upsertCredentialBlockStmt = this.#db.prepare(
|
|
486
501
|
`INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
|
|
@@ -1585,27 +1600,38 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1585
1600
|
}
|
|
1586
1601
|
|
|
1587
1602
|
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
1588
|
-
const nowMs = Date.now();
|
|
1589
|
-
const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1590
1603
|
// Current callers use meter scopes. The physical shared row exists only
|
|
1591
1604
|
// for direct SQLite readers from pre-meter releases.
|
|
1592
|
-
if (
|
|
1605
|
+
if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1593
1606
|
return undefined;
|
|
1594
1607
|
}
|
|
1595
|
-
|
|
1596
|
-
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope,
|
|
1608
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1609
|
+
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, Date.now()) as
|
|
1597
1610
|
| { blocked_until_ms?: number; updated_at?: number }
|
|
1598
1611
|
| undefined;
|
|
1599
1612
|
return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
|
|
1600
1613
|
}
|
|
1601
1614
|
|
|
1615
|
+
getCredentialBlockScopes(credentialId: number, providerKey: string): Map<string, number> {
|
|
1616
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1617
|
+
const rows = this.#listCredentialBlockScopesStmt.all(credentialId, providerKey, Date.now()) as Array<
|
|
1618
|
+
Pick<CredentialBlockRow, "block_scope" | "blocked_until_ms">
|
|
1619
|
+
>;
|
|
1620
|
+
const hidesLegacyShared = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1621
|
+
const scopes = new Map<string, number>();
|
|
1622
|
+
for (const row of rows) {
|
|
1623
|
+
if (hidesLegacyShared && row.block_scope === LEGACY_CODEX_BLOCK_SCOPE) continue;
|
|
1624
|
+
scopes.set(row.block_scope, row.blocked_until_ms);
|
|
1625
|
+
}
|
|
1626
|
+
return scopes;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1602
1629
|
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) {
|
|
1630
|
+
if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1606
1631
|
return undefined;
|
|
1607
1632
|
}
|
|
1608
|
-
|
|
1633
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1634
|
+
const nowMs = Date.now();
|
|
1609
1635
|
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
1610
1636
|
| { blocked_until_ms?: number; updated_at?: number }
|
|
1611
1637
|
| undefined;
|
|
@@ -1622,7 +1648,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1622
1648
|
const isLegacyCodexBlock =
|
|
1623
1649
|
block.providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && block.blockScope === LEGACY_CODEX_BLOCK_SCOPE;
|
|
1624
1650
|
const blockScopes = isLegacyCodexBlock ? CODEX_METER_BLOCK_SCOPES : [block.blockScope];
|
|
1651
|
+
const nowMs = Date.now();
|
|
1625
1652
|
const upsert = this.#db.transaction(() => {
|
|
1653
|
+
// A mark already holds the write lock: sweep expired rows in the same transaction.
|
|
1654
|
+
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
1626
1655
|
for (const blockScope of blockScopes) {
|
|
1627
1656
|
this.#upsertCredentialBlockStmt.run(
|
|
1628
1657
|
block.credentialId,
|
|
@@ -1633,8 +1662,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1633
1662
|
}
|
|
1634
1663
|
});
|
|
1635
1664
|
upsert.immediate();
|
|
1665
|
+
this.#lastCredentialBlockSweepMs = nowMs;
|
|
1666
|
+
this.#pruneCredentialBlockReconcileAfter(nowMs);
|
|
1636
1667
|
|
|
1637
|
-
const reconcileAfterMs = Math.min(block.blockedUntilMs,
|
|
1668
|
+
const reconcileAfterMs = Math.min(block.blockedUntilMs, nowMs + USAGE_REPORT_TTL_MS);
|
|
1638
1669
|
for (const blockScope of blockScopes) {
|
|
1639
1670
|
this.#credentialBlockReconcileAfter.set(
|
|
1640
1671
|
`${block.credentialId}\0${block.providerKey}\0${blockScope}`,
|
|
@@ -1661,38 +1692,49 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1661
1692
|
}
|
|
1662
1693
|
|
|
1663
1694
|
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
1695
|
+
// Stamp wall-clock time first: callers may pass a future `nowMs`, and a
|
|
1696
|
+
// failed sweep must not be retried by every following read.
|
|
1697
|
+
this.#lastCredentialBlockSweepMs = Date.now();
|
|
1664
1698
|
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
1699
|
+
this.#pruneCredentialBlockReconcileAfter(nowMs);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
#pruneCredentialBlockReconcileAfter(nowMs: number): void {
|
|
1665
1703
|
for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
|
|
1666
1704
|
if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
|
|
1667
1705
|
}
|
|
1668
1706
|
}
|
|
1669
1707
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1708
|
+
/**
|
|
1709
|
+
* Read-path expiry sweep, at most once per {@link CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS}.
|
|
1710
|
+
* A busy write lock only postpones housekeeping; it never fails the read.
|
|
1711
|
+
*/
|
|
1712
|
+
#sweepExpiredCredentialBlocks(): void {
|
|
1672
1713
|
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
|
-
}
|
|
1714
|
+
if (nowMs - this.#lastCredentialBlockSweepMs < CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS) return;
|
|
1715
|
+
try {
|
|
1716
|
+
this.cleanExpiredCredentialBlocks(nowMs);
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
if (!isSqliteBusyError(err)) throw err;
|
|
1694
1719
|
}
|
|
1695
|
-
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
1723
|
+
if (credentialIds.length === 0) return [];
|
|
1724
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1725
|
+
const rows = this.#listCredentialBlocksStmt.all(
|
|
1726
|
+
JSON.stringify([...new Set(credentialIds)]),
|
|
1727
|
+
Date.now(),
|
|
1728
|
+
LEGACY_CODEX_BLOCK_PROVIDER_KEY,
|
|
1729
|
+
LEGACY_CODEX_BLOCK_SCOPE,
|
|
1730
|
+
) as CredentialBlockRow[];
|
|
1731
|
+
return rows.map(row => ({
|
|
1732
|
+
credentialId: row.credential_id,
|
|
1733
|
+
providerKey: row.provider_key,
|
|
1734
|
+
blockScope: row.block_scope,
|
|
1735
|
+
blockedUntilMs: row.blocked_until_ms,
|
|
1736
|
+
updatedAtMs: row.updated_at * 1000,
|
|
1737
|
+
}));
|
|
1696
1738
|
}
|
|
1697
1739
|
|
|
1698
1740
|
tryAcquireCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean {
|
|
@@ -2050,7 +2092,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
2050
2092
|
this.#upsertCacheStmt.finalize();
|
|
2051
2093
|
this.#deleteExpiredCacheStmt.finalize();
|
|
2052
2094
|
this.#getCredentialBlockStmt.finalize();
|
|
2053
|
-
this.#
|
|
2095
|
+
this.#listCredentialBlockScopesStmt.finalize();
|
|
2096
|
+
this.#listCredentialBlocksStmt.finalize();
|
|
2054
2097
|
this.#upsertCredentialBlockStmt.finalize();
|
|
2055
2098
|
this.#deleteCredentialBlocksStmt.finalize();
|
|
2056
2099
|
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. */
|