@oh-my-pi/pi-ai 18.2.0 → 18.2.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/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +17 -0
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/session-state.d.ts +118 -0
- package/dist/types/auth-storage.d.ts +17 -0
- package/dist/types/error/body-error.d.ts +15 -0
- package/dist/types/error/flags.d.ts +16 -0
- package/dist/types/error/index.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/oneshot-retry.d.ts +6 -0
- package/dist/types/provider-session-state.d.ts +46 -0
- package/dist/types/providers/amazon-bedrock.d.ts +3 -0
- package/dist/types/providers/aws-sigv4.d.ts +12 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +27 -0
- package/dist/types/providers/openai-responses.d.ts +15 -0
- package/dist/types/providers/openai-shared.d.ts +20 -3
- package/dist/types/registry/oauth/perplexity.d.ts +1 -7
- package/dist/types/registry/oauth/types.d.ts +8 -0
- package/dist/types/stream.d.ts +2 -0
- package/dist/types/types.d.ts +3 -1
- package/dist/types/usage/openai-codex.d.ts +3 -1
- package/dist/types/usage.d.ts +11 -1
- package/dist/types/utils/block-symbols.d.ts +36 -0
- package/dist/types/utils/openai-http.d.ts +2 -0
- package/dist/types/utils/retry-after.d.ts +2 -0
- package/dist/types/utils/schema/wire.d.ts +4 -5
- package/dist/types/utils.d.ts +9 -0
- package/package.json +6 -6
- package/src/auth/sqlite-credential-store.ts +8 -33
- package/src/auth-broker/remote-store.ts +73 -8
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/server.ts +186 -74
- package/src/auth-gateway/session-state.ts +312 -0
- package/src/auth-storage.ts +146 -15
- package/src/error/body-error.ts +310 -0
- package/src/error/flags.ts +63 -13
- package/src/error/index.ts +1 -0
- package/src/error/retryable.ts +2 -0
- package/src/index.ts +1 -0
- package/src/oneshot-retry.ts +13 -3
- package/src/provider-session-state.ts +56 -0
- package/src/providers/amazon-bedrock.ts +20 -3
- package/src/providers/anthropic-messages-server.ts +104 -23
- package/src/providers/anthropic-signature.ts +5 -2
- package/src/providers/anthropic.ts +101 -15
- package/src/providers/aws-sigv4.ts +16 -5
- package/src/providers/cursor.ts +60 -10
- package/src/providers/devin.ts +82 -28
- package/src/providers/openai-chat-server.ts +4 -0
- package/src/providers/openai-codex/request-transformer.ts +36 -0
- package/src/providers/openai-codex-responses.ts +35 -12
- package/src/providers/openai-completions.ts +49 -12
- package/src/providers/openai-reasoning-fallback.ts +6 -6
- package/src/providers/openai-responses-server.ts +2 -1
- package/src/providers/openai-responses.ts +52 -4
- package/src/providers/openai-shared.ts +199 -51
- package/src/registry/oauth/perplexity.ts +94 -28
- package/src/registry/oauth/types.ts +9 -0
- package/src/stream.ts +23 -2
- package/src/types.ts +3 -0
- package/src/usage/claude.ts +33 -0
- package/src/usage/google-antigravity.ts +8 -2
- package/src/usage/openai-codex.ts +94 -11
- package/src/usage.ts +8 -1
- package/src/utils/block-symbols.ts +57 -0
- package/src/utils/http-inspector.ts +20 -0
- package/src/utils/openai-http.ts +39 -3
- package/src/utils/retry-after.ts +12 -0
- package/src/utils/schema/normalize.ts +3 -3
- package/src/utils/schema/stamps.ts +33 -45
- package/src/utils/schema/wire.ts +9 -7
- package/src/utils.ts +67 -22
|
@@ -5,6 +5,8 @@ import type { CredentialRankingStrategy, UsageProvider, UsageReport } from "../u
|
|
|
5
5
|
* ingesting them lets credential selection block an exhausted account before
|
|
6
6
|
* the next request burns a wire 429.
|
|
7
7
|
*/
|
|
8
|
-
export declare function parseCodexRateLimitHeaders(headers: Record<string, string>, now?: number
|
|
8
|
+
export declare function parseCodexRateLimitHeaders(headers: Record<string, string>, now?: number, context?: {
|
|
9
|
+
responseStatus?: number;
|
|
10
|
+
}): UsageReport | null;
|
|
9
11
|
export declare const openaiCodexUsageProvider: UsageProvider;
|
|
10
12
|
export declare const codexRankingStrategy: CredentialRankingStrategy;
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface UsageScope {
|
|
|
43
43
|
tier?: string;
|
|
44
44
|
windowId?: string;
|
|
45
45
|
shared?: boolean;
|
|
46
|
+
/** Stable identity shared by routing-specific copies of one upstream quota. */
|
|
47
|
+
sharedGroup?: string;
|
|
46
48
|
}
|
|
47
49
|
/** Normalized limit entry for a single window or quota bucket. */
|
|
48
50
|
export interface UsageLimit {
|
|
@@ -241,6 +243,7 @@ export declare const usageScopeSchema: import("@oh-my-pi/omptype").FluentType<{
|
|
|
241
243
|
projectId?: string | undefined;
|
|
242
244
|
provider: string;
|
|
243
245
|
shared?: boolean | undefined;
|
|
246
|
+
sharedGroup?: string | undefined;
|
|
244
247
|
tier?: string | undefined;
|
|
245
248
|
windowId?: string | undefined;
|
|
246
249
|
}, {
|
|
@@ -250,6 +253,7 @@ export declare const usageScopeSchema: import("@oh-my-pi/omptype").FluentType<{
|
|
|
250
253
|
projectId?: string | undefined;
|
|
251
254
|
provider: string;
|
|
252
255
|
shared?: boolean | undefined;
|
|
256
|
+
sharedGroup?: string | undefined;
|
|
253
257
|
tier?: string | undefined;
|
|
254
258
|
windowId?: string | undefined;
|
|
255
259
|
}>;
|
|
@@ -272,6 +276,7 @@ export declare const usageLimitSchema: import("@oh-my-pi/omptype").FluentType<{
|
|
|
272
276
|
projectId?: string | undefined;
|
|
273
277
|
provider: string;
|
|
274
278
|
shared?: boolean | undefined;
|
|
279
|
+
sharedGroup?: string | undefined;
|
|
275
280
|
tier?: string | undefined;
|
|
276
281
|
windowId?: string | undefined;
|
|
277
282
|
};
|
|
@@ -302,6 +307,7 @@ export declare const usageLimitSchema: import("@oh-my-pi/omptype").FluentType<{
|
|
|
302
307
|
projectId?: string | undefined;
|
|
303
308
|
provider: string;
|
|
304
309
|
shared?: boolean | undefined;
|
|
310
|
+
sharedGroup?: string | undefined;
|
|
305
311
|
tier?: string | undefined;
|
|
306
312
|
windowId?: string | undefined;
|
|
307
313
|
};
|
|
@@ -359,6 +365,7 @@ export declare const usageReportSchema: import("@oh-my-pi/omptype").FluentType<{
|
|
|
359
365
|
projectId?: string | undefined;
|
|
360
366
|
provider: string;
|
|
361
367
|
shared?: boolean | undefined;
|
|
368
|
+
sharedGroup?: string | undefined;
|
|
362
369
|
tier?: string | undefined;
|
|
363
370
|
windowId?: string | undefined;
|
|
364
371
|
};
|
|
@@ -404,6 +411,7 @@ export declare const usageReportSchema: import("@oh-my-pi/omptype").FluentType<{
|
|
|
404
411
|
projectId?: string | undefined;
|
|
405
412
|
provider: string;
|
|
406
413
|
shared?: boolean | undefined;
|
|
414
|
+
sharedGroup?: string | undefined;
|
|
407
415
|
tier?: string | undefined;
|
|
408
416
|
windowId?: string | undefined;
|
|
409
417
|
};
|
|
@@ -472,7 +480,9 @@ export interface UsageProvider {
|
|
|
472
480
|
id: Provider;
|
|
473
481
|
fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null>;
|
|
474
482
|
/** Parse provider rate-limit response headers (lowercased keys) into a usage report, if supported. */
|
|
475
|
-
parseRateLimitHeaders?(headers: Record<string, string>, now?: number
|
|
483
|
+
parseRateLimitHeaders?(headers: Record<string, string>, now?: number, context?: {
|
|
484
|
+
responseStatus?: number;
|
|
485
|
+
}): UsageReport | null;
|
|
476
486
|
supports?(params: UsageFetchParams): boolean;
|
|
477
487
|
/** True when fetchUsage contacts upstream and can authenticate the credential for health checks. */
|
|
478
488
|
validatesCredentials?: boolean;
|
|
@@ -113,3 +113,39 @@ export type SyntheticUserCarrier = object & {
|
|
|
113
113
|
};
|
|
114
114
|
/** True for `user` messages synthesized by message transformation. */
|
|
115
115
|
export declare function isSyntheticUser(message: SyntheticUserCarrier | null | undefined): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Marks a message synthesized by a per-call context transform rather than
|
|
118
|
+
* loaded from persisted conversation history.
|
|
119
|
+
*
|
|
120
|
+
* Prompt-cache boundaries must skip these messages: their content is rebuilt
|
|
121
|
+
* for each request and cannot anchor a prefix reused by the next turn.
|
|
122
|
+
* Symbol-keyed so the marker never persists or reaches the provider wire.
|
|
123
|
+
*/
|
|
124
|
+
export declare const kPerCallContextMessage: unique symbol;
|
|
125
|
+
/** Carries per-call context provenance without exposing a string-keyed property. */
|
|
126
|
+
export type PerCallContextMessageCarrier = object & {
|
|
127
|
+
[kPerCallContextMessage]?: true;
|
|
128
|
+
};
|
|
129
|
+
/** Marks a message as synthesized for the current provider call. */
|
|
130
|
+
export declare function markPerCallContextMessage(message: PerCallContextMessageCarrier): void;
|
|
131
|
+
/** Copies per-call context provenance to a converted or projected message. */
|
|
132
|
+
export declare function copyPerCallContextMessage(target: PerCallContextMessageCarrier, source: PerCallContextMessageCarrier): void;
|
|
133
|
+
/** True when a message was synthesized for the current provider call. */
|
|
134
|
+
export declare function isPerCallContextMessage(message: PerCallContextMessageCarrier | null | undefined): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Original history position carried by a context message clone.
|
|
137
|
+
*
|
|
138
|
+
* Object-spread transforms retain this symbol, allowing the extension runner
|
|
139
|
+
* to distinguish byte-identical historical copies from inserted messages.
|
|
140
|
+
*/
|
|
141
|
+
export declare const kContextHistoryIndex: unique symbol;
|
|
142
|
+
/** Carries a context message's original history position. */
|
|
143
|
+
export type ContextHistoryIndexCarrier = object & {
|
|
144
|
+
[kContextHistoryIndex]?: number;
|
|
145
|
+
};
|
|
146
|
+
/** Reads a context message's original history position. */
|
|
147
|
+
export declare function getContextHistoryIndex(message: ContextHistoryIndexCarrier | null | undefined): number | undefined;
|
|
148
|
+
/** Records a context message's original history position. */
|
|
149
|
+
export declare function setContextHistoryIndex(message: ContextHistoryIndexCarrier, index: number): void;
|
|
150
|
+
/** Removes context-history tracking before provider conversion. */
|
|
151
|
+
export declare function clearContextHistoryIndex(message: ContextHistoryIndexCarrier): void;
|
|
@@ -26,6 +26,8 @@ export interface OpenAIStreamRequestInit {
|
|
|
26
26
|
body: unknown;
|
|
27
27
|
signal: AbortSignal;
|
|
28
28
|
fetch?: FetchImpl;
|
|
29
|
+
/** Optional caller-specific gate composed with shared transport retry exclusions. */
|
|
30
|
+
shouldRetryResponse?: (response: Response, bodyText: string) => boolean | Promise<boolean>;
|
|
29
31
|
/** Raw wire-frame observer (`onSseEvent` debug pipeline). */
|
|
30
32
|
onSseEvent?: SseEventObserver;
|
|
31
33
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type HeadersLike = Headers | Record<string, string | undefined> | undefined | null;
|
|
2
|
+
/** Extracts retry timing using the provider's catalog-declared timestamp timezone. */
|
|
3
|
+
export declare function extractProviderRetryHint(provider: string | undefined, message: string | undefined): number | undefined;
|
|
2
4
|
export declare function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string;
|
|
3
5
|
export declare function getRetryAfterMsFromHeaders(headers: HeadersLike): number | undefined;
|
|
4
6
|
export declare function getHeadersFromError(error: unknown): HeadersLike;
|
|
@@ -41,10 +41,9 @@ export declare function arkToWireSchema(schema: Type): Record<string, unknown>;
|
|
|
41
41
|
export declare function toolWireSchema(tool: Tool): Record<string, unknown>;
|
|
42
42
|
/**
|
|
43
43
|
* Return a deep clone of `schema` with every `description` annotation removed.
|
|
44
|
-
* The result is memoized
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* system-prompt/UI rendering.
|
|
44
|
+
* The result is memoized against the input (`stamp`) so repeated provider
|
|
45
|
+
* requests reuse the same stripped object; the input is never mutated, so the
|
|
46
|
+
* memoized `toolWireSchema` result stays intact for system-prompt/UI rendering.
|
|
48
47
|
*/
|
|
49
48
|
export declare function stripSchemaDescriptions(schema: Record<string, unknown>): Record<string, unknown>;
|
|
50
49
|
/**
|
|
@@ -53,6 +52,6 @@ export declare function stripSchemaDescriptions(schema: Record<string, unknown>)
|
|
|
53
52
|
* Used when the full tool catalog is rendered into the system prompt instead, so
|
|
54
53
|
* the descriptions ride the wire once (in the prompt) rather than duplicated on
|
|
55
54
|
* every tool definition. Parameters are resolved to wire JSON Schema and cloned,
|
|
56
|
-
* leaving the original tool objects and the
|
|
55
|
+
* leaving the original tool objects and the memoized schema cache untouched.
|
|
57
56
|
*/
|
|
58
57
|
export declare function stripToolDescriptions(tools: readonly Tool[]): Tool[];
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -48,6 +48,15 @@ export declare function stripUnpairedOpenAIResponsesComputerReasoningIdsForRepla
|
|
|
48
48
|
* Returns `undefined` for hidden-empty turns that only contain reasoning and an
|
|
49
49
|
* empty assistant message, allowing callers to rebuild visible transcript
|
|
50
50
|
* history instead of replaying stale native state.
|
|
51
|
+
*
|
|
52
|
+
* When the turn does carry replayable output, whitespace-only assistant
|
|
53
|
+
* messages are dropped rather than replayed. gpt-5.6 Codex ends a turn whose
|
|
54
|
+
* text all landed in the `commentary` phase with an empty `final_answer`
|
|
55
|
+
* message; replaying that item verbatim seeds the next turn with an empty slot
|
|
56
|
+
* the model then fills - `""` → `"\n\n"` → stray words → non-Latin residue -
|
|
57
|
+
* and each contaminated item is replayed in turn, so the drift compounds until
|
|
58
|
+
* the visible answer collapses. Codex CLI replays the empty item too
|
|
59
|
+
* (openai/codex#32389); this is the layer where omp can refuse to.
|
|
51
60
|
*/
|
|
52
61
|
export declare function sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(items: Array<Record<string, unknown>>, options?: OpenAIResponsesReplaySanitizeOptions): ResponseInput | undefined;
|
|
53
62
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.2.
|
|
3
|
+
"version": "18.2.2",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -124,11 +124,11 @@
|
|
|
124
124
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
125
125
|
},
|
|
126
126
|
"dependencies": {
|
|
127
|
-
"@oh-my-pi/omptype": "18.2.
|
|
128
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
129
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
130
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
131
|
-
"@oh-my-pi/pi-wire": "18.2.
|
|
127
|
+
"@oh-my-pi/omptype": "18.2.2",
|
|
128
|
+
"@oh-my-pi/pi-catalog": "18.2.2",
|
|
129
|
+
"@oh-my-pi/pi-natives": "18.2.2",
|
|
130
|
+
"@oh-my-pi/pi-utils": "18.2.2",
|
|
131
|
+
"@oh-my-pi/pi-wire": "18.2.2"
|
|
132
132
|
},
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"@types/bun": "^1.3.14"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The public AuthCredentialStore interface remains in ../auth-storage so local
|
|
5
5
|
* and remote stores share the same contract.
|
|
6
6
|
*/
|
|
7
|
-
import { Database,
|
|
7
|
+
import type { Database, Statement } from "bun:sqlite";
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isSqliteBusyError,
|
|
16
16
|
isSqliteCorruptionError,
|
|
17
17
|
logger,
|
|
18
|
+
openSqliteDatabase,
|
|
18
19
|
} from "@oh-my-pi/pi-utils";
|
|
19
20
|
import type {
|
|
20
21
|
AuthCredential,
|
|
@@ -25,7 +26,6 @@ import type {
|
|
|
25
26
|
StoredAuthCredential,
|
|
26
27
|
StoredCredentialBlock,
|
|
27
28
|
} from "../auth-storage";
|
|
28
|
-
import * as AIError from "../error";
|
|
29
29
|
import type { OAuthCredentials } from "../registry/oauth/types";
|
|
30
30
|
import type { Provider } from "../types";
|
|
31
31
|
import type {
|
|
@@ -509,6 +509,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
509
509
|
);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
/** Opens credential storage with bounded busy retries and one-shot corruption recovery. */
|
|
512
513
|
static async open(dbPath: string = getAgentDbPath()): Promise<SqliteAuthCredentialStore> {
|
|
513
514
|
const dir = path.dirname(dbPath);
|
|
514
515
|
const dirExists = await fs
|
|
@@ -519,23 +520,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
519
520
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
520
521
|
}
|
|
521
522
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// exponential backoff before surfacing the failure. See issue #2421.
|
|
526
|
-
const maxAttempts = 4;
|
|
527
|
-
const baseDelayMs = 100;
|
|
528
|
-
let lastBusyError: Error | undefined;
|
|
529
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
530
|
-
let db: Database | undefined;
|
|
531
|
-
try {
|
|
532
|
-
db = new Database(dbPath);
|
|
533
|
-
// Install the busy handler BEFORE the first lock-taking statement
|
|
534
|
-
// on this connection. The leases DDL below and the constructor's
|
|
535
|
-
// schema init both acquire locks during WAL recovery; without a
|
|
536
|
-
// non-zero `busy_timeout` they fail immediately with SQLITE_BUSY.
|
|
537
|
-
// See issue #2421.
|
|
538
|
-
SqliteAuthCredentialStore.#installBusyTimeout(db);
|
|
523
|
+
return openSqliteDatabase(
|
|
524
|
+
dbPath,
|
|
525
|
+
async db => {
|
|
539
526
|
try {
|
|
540
527
|
await fs.chmod(dbPath, 0o600);
|
|
541
528
|
} catch {
|
|
@@ -543,20 +530,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
543
530
|
}
|
|
544
531
|
SqliteAuthCredentialStore.#ensureAuthCredentialRefreshLeasesTable(db);
|
|
545
532
|
return new SqliteAuthCredentialStore(db);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (!isSqliteBusyError(err)) {
|
|
549
|
-
throw err;
|
|
550
|
-
}
|
|
551
|
-
lastBusyError = err instanceof Error ? err : new Error(String(err));
|
|
552
|
-
if (attempt < maxAttempts - 1) {
|
|
553
|
-
await Bun.sleep(baseDelayMs * 2 ** attempt);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
throw new AIError.ConfigurationError(
|
|
558
|
-
`Failed to open auth database at '${dbPath}' after ${maxAttempts} attempts: ${lastBusyError?.message}`,
|
|
559
|
-
{ cause: lastBusyError },
|
|
533
|
+
},
|
|
534
|
+
{ recoverCorruption: true },
|
|
560
535
|
);
|
|
561
536
|
}
|
|
562
537
|
|
|
@@ -255,6 +255,18 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
255
255
|
#snapshot: SnapshotResponse = emptySnapshot();
|
|
256
256
|
#snapshotReceivedAt = Date.now();
|
|
257
257
|
#generation = 0;
|
|
258
|
+
/**
|
|
259
|
+
* Content fingerprint of the credential set in {@link #snapshot} (id +
|
|
260
|
+
* provider + credential material), recomputed after every snapshot mutation.
|
|
261
|
+
* Drives {@link #credentialRevision} independently of the broker's numeric
|
|
262
|
+
* generation, which is an in-memory counter that resets when the broker
|
|
263
|
+
* process restarts and so cannot be trusted for change detection.
|
|
264
|
+
*/
|
|
265
|
+
#credentialFingerprint = "";
|
|
266
|
+
/** Monotonic local counter bumped whenever {@link #credentialFingerprint} changes. */
|
|
267
|
+
#credentialRevision = 0;
|
|
268
|
+
/** Revision last reported as "seen" by {@link pollExternalChanges}; seeded from the initial snapshot. */
|
|
269
|
+
#acknowledgedRevision = 0;
|
|
258
270
|
#usageOverlays: Map<string, UsageReport> = new Map();
|
|
259
271
|
#backgroundAbort = new AbortController();
|
|
260
272
|
readonly #backgroundIdleMs: number;
|
|
@@ -300,6 +312,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
300
312
|
? new Map([...opts.accountPool].map(([provider, identities]) => [provider, new Set(identities)]))
|
|
301
313
|
: undefined;
|
|
302
314
|
this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0);
|
|
315
|
+
this.#acknowledgedRevision = this.#credentialRevision;
|
|
303
316
|
this.#onSnapshot = opts.onSnapshot;
|
|
304
317
|
void this.#runBackground();
|
|
305
318
|
}
|
|
@@ -325,6 +338,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
325
338
|
this.#snapshot = { ...snapshot, credentials };
|
|
326
339
|
this.#generation = generation;
|
|
327
340
|
this.#snapshotReceivedAt = nowMs;
|
|
341
|
+
this.#refreshCredentialRevision();
|
|
328
342
|
const onSnapshot = this.#onSnapshot;
|
|
329
343
|
if (!onSnapshot) return;
|
|
330
344
|
try {
|
|
@@ -333,6 +347,34 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
333
347
|
logger.debug("auth-broker snapshot callback failed", { error: String(error) });
|
|
334
348
|
}
|
|
335
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Recompute the credential-content fingerprint and bump
|
|
353
|
+
* {@link #credentialRevision} when it changes. Called after every snapshot
|
|
354
|
+
* mutation so {@link pollExternalChanges} detects add/remove/replace even
|
|
355
|
+
* when the broker's numeric generation repeats (e.g. after a broker
|
|
356
|
+
* restart resets its in-memory counter).
|
|
357
|
+
*/
|
|
358
|
+
#refreshCredentialRevision(): void {
|
|
359
|
+
const fingerprint = this.#computeCredentialFingerprint();
|
|
360
|
+
if (fingerprint === this.#credentialFingerprint) return;
|
|
361
|
+
this.#credentialFingerprint = fingerprint;
|
|
362
|
+
this.#credentialRevision += 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Order-independent digest of the routable credential material — exactly the
|
|
367
|
+
* fields {@link listAuthCredentials} exposes (id, provider, credential). A
|
|
368
|
+
* token rotation or an add/remove changes it; credential blocks and usage
|
|
369
|
+
* overlays do not.
|
|
370
|
+
*/
|
|
371
|
+
#computeCredentialFingerprint(): string {
|
|
372
|
+
const parts = this.#snapshot.credentials.map(
|
|
373
|
+
entry => `${entry.id}\u0000${entry.provider}\u0000${JSON.stringify(entry.credential)}`,
|
|
374
|
+
);
|
|
375
|
+
parts.sort();
|
|
376
|
+
return parts.join("\u0001");
|
|
377
|
+
}
|
|
336
378
|
#protectNewSnapshotBlocks(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[], nowMs: number): void {
|
|
337
379
|
const previousBlocksByKey = new Map<string, string>();
|
|
338
380
|
for (const entry of previous) {
|
|
@@ -499,15 +541,13 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
499
541
|
#applyStreamEvent(event: SnapshotStreamEvent): void {
|
|
500
542
|
switch (event.kind) {
|
|
501
543
|
case "snapshot": {
|
|
502
|
-
//
|
|
544
|
+
// The first frame of every SSE connection is a full authoritative
|
|
545
|
+
// snapshot. Always adopt it as the new generation baseline: the
|
|
546
|
+
// broker's in-memory generation counter resets on restart and may
|
|
547
|
+
// therefore be lower than the previous stream's last value.
|
|
548
|
+
// Subsequent entry/removal frames remain guarded against reordering
|
|
549
|
+
// relative to this new baseline below.
|
|
503
550
|
const { kind: _kind, ...snapshot } = event;
|
|
504
|
-
if (snapshot.generation < this.#generation) {
|
|
505
|
-
logger.debug("auth-broker stream snapshot older than local; ignoring", {
|
|
506
|
-
local: this.#generation,
|
|
507
|
-
incoming: snapshot.generation,
|
|
508
|
-
});
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
551
|
this.#applySnapshot(snapshot, snapshot.generation);
|
|
512
552
|
return;
|
|
513
553
|
}
|
|
@@ -548,6 +588,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
548
588
|
this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
|
|
549
589
|
this.#generation = generation;
|
|
550
590
|
this.#snapshotReceivedAt = Date.now();
|
|
591
|
+
this.#refreshCredentialRevision();
|
|
551
592
|
}
|
|
552
593
|
|
|
553
594
|
#removeStreamCredential(
|
|
@@ -564,6 +605,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
564
605
|
this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
|
|
565
606
|
this.#generation = generation;
|
|
566
607
|
this.#snapshotReceivedAt = Date.now();
|
|
608
|
+
this.#refreshCredentialRevision();
|
|
567
609
|
}
|
|
568
610
|
|
|
569
611
|
/** Re-hydrate the in-memory snapshot from the broker. */
|
|
@@ -574,6 +616,29 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
574
616
|
return this.#snapshot;
|
|
575
617
|
}
|
|
576
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Stateful probe for broker-side credential changes, mirroring
|
|
621
|
+
* {@link SqliteAuthCredentialStore.pollExternalChanges} so long-lived broker
|
|
622
|
+
* clients (notably `auth-gateway serve`) pick up logins/logouts made by
|
|
623
|
+
* another process without a restart.
|
|
624
|
+
*
|
|
625
|
+
* Compares a local content revision, not the broker's numeric generation:
|
|
626
|
+
* generation is an in-memory counter that resets when the broker process
|
|
627
|
+
* restarts, so a reconnecting stream can deliver a different credential set
|
|
628
|
+
* under a repeated (or lower) generation. {@link #refreshCredentialRevision}
|
|
629
|
+
* bumps the revision whenever the applied credential material actually
|
|
630
|
+
* changes, catching those cases too. Records foreground activity first: a
|
|
631
|
+
* low-traffic client's background sync parks after `#backgroundIdleMs`, and
|
|
632
|
+
* without this wakeup it would never fetch the new snapshot to report in the
|
|
633
|
+
* first place.
|
|
634
|
+
*/
|
|
635
|
+
pollExternalChanges(): boolean {
|
|
636
|
+
this.#noteActivity();
|
|
637
|
+
if (this.#credentialRevision === this.#acknowledgedRevision) return false;
|
|
638
|
+
this.#acknowledgedRevision = this.#credentialRevision;
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
|
|
577
642
|
listAuthCredentials(provider?: string): StoredAuthCredential[] {
|
|
578
643
|
this.#noteActivity();
|
|
579
644
|
const out: StoredAuthCredential[] = [];
|