@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
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import * as AIError from "../error";
|
|
29
29
|
import { AuthBrokerClient, AuthBrokerError } from "./client";
|
|
30
30
|
import { type AuthBrokerAccountPool, RemoteAuthCredentialStore } from "./remote-store";
|
|
31
|
-
import { readAuthBrokerSnapshotCache,
|
|
31
|
+
import { readAuthBrokerSnapshotCache, scheduleAuthBrokerSnapshotCacheWrite } from "./snapshot-cache";
|
|
32
32
|
import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "./types";
|
|
33
33
|
|
|
34
34
|
export interface AuthBrokerClientConfig {
|
|
@@ -52,7 +52,7 @@ export interface DiscoverAuthStorageOptions {
|
|
|
52
52
|
authStorageOptions?: Omit<AuthStorageOptions, "accountPolicies" | "configValueResolver" | "sourceLabel">;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** Path to the local bearer token file. Created by `
|
|
55
|
+
/** Path to the local bearer token file. Created by `zetacode auth-broker token`. */
|
|
56
56
|
export function getAuthBrokerTokenFilePath(): string {
|
|
57
57
|
return path.join(getConfigRootDir(), "auth-broker.token");
|
|
58
58
|
}
|
|
@@ -396,16 +396,17 @@ export async function openAuthCredentialStore(
|
|
|
396
396
|
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
397
397
|
const cachePath = options.cachePath ?? getAuthBrokerSnapshotCachePath();
|
|
398
398
|
const ttlMs = resolveSnapshotTtlMs();
|
|
399
|
+
// Coalesced fire-and-forget: the first snapshot lands immediately (so a
|
|
400
|
+
// fresh boot seeds the cache), bursts collapse into one trailing write,
|
|
401
|
+
// and anything pending is flushed on shutdown.
|
|
399
402
|
const persist =
|
|
400
403
|
ttlMs > 0
|
|
401
404
|
? (snapshot: SnapshotResponse): void => {
|
|
402
|
-
|
|
405
|
+
scheduleAuthBrokerSnapshotCacheWrite({
|
|
403
406
|
path: cachePath,
|
|
404
407
|
token: brokerConfig.token,
|
|
405
408
|
url: brokerConfig.url,
|
|
406
409
|
snapshot,
|
|
407
|
-
}).catch(error => {
|
|
408
|
-
logger.debug("auth-broker snapshot cache write failed", { error: String(error) });
|
|
409
410
|
});
|
|
410
411
|
}
|
|
411
412
|
: undefined;
|
|
@@ -75,7 +75,12 @@ export class AuthBrokerRefresher {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Run one sweep. Exposed for tests.
|
|
80
|
+
*
|
|
81
|
+
* Never rejects: callers fire it unawaited from a timer, so a failed sweep
|
|
82
|
+
* (e.g. an unreadable credential store) is logged and retried next interval.
|
|
83
|
+
*/
|
|
79
84
|
async tick(): Promise<void> {
|
|
80
85
|
if (this.#running) return;
|
|
81
86
|
this.#running = true;
|
|
@@ -94,6 +99,8 @@ export class AuthBrokerRefresher {
|
|
|
94
99
|
targets.push(entry.id);
|
|
95
100
|
}
|
|
96
101
|
await Promise.all(targets.map(id => this.#refreshOne(id)));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.warn("auth-broker refresh sweep failed", { error: String(error) });
|
|
97
104
|
} finally {
|
|
98
105
|
this.#running = false;
|
|
99
106
|
this.#nextSweepAt = this.#now() + this.#refreshIntervalMs;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* runs isn't required.
|
|
9
9
|
*/
|
|
10
10
|
import * as os from "node:os";
|
|
11
|
-
import { getAppName, getInstallId, logger } from "@linxiraos/pi-utils";
|
|
11
|
+
import { getAppName, getInstallId, logger, postmortem } from "@linxiraos/pi-utils";
|
|
12
12
|
import type { AuthCredentialStore } from "../auth/store";
|
|
13
13
|
import {
|
|
14
14
|
type AuthCredential,
|
|
@@ -69,6 +69,8 @@ const BACKGROUND_BACKOFF_INITIAL_MS = 500;
|
|
|
69
69
|
const BACKGROUND_BACKOFF_MAX_MS = 30_000;
|
|
70
70
|
/** Idle window after the last foreground store use before background sync parks. */
|
|
71
71
|
const BACKGROUND_IDLE_MS = 20_000;
|
|
72
|
+
/** Longest a process exit waits for the final observed-usage report to reach the broker. */
|
|
73
|
+
const OBSERVED_USAGE_EXIT_FLUSH_MS = 2_000;
|
|
72
74
|
|
|
73
75
|
function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBlockSnapshot {
|
|
74
76
|
return {
|
|
@@ -124,6 +126,14 @@ function credentialEntryWithBlocks(
|
|
|
124
126
|
return incoming;
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Change-detection hash of one row's routable credential material (provider +
|
|
131
|
+
* credential; the id is the map key). Used only for equality, never persisted.
|
|
132
|
+
*/
|
|
133
|
+
function credentialContentHash(entry: SnapshotEntry): number | bigint {
|
|
134
|
+
return Bun.hash(`${entry.provider}\u0000${JSON.stringify(entry.credential)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
function emptySnapshot(): SnapshotResponse {
|
|
128
138
|
return {
|
|
129
139
|
generation: 0,
|
|
@@ -255,14 +265,19 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
255
265
|
#snapshotReceivedAt = Date.now();
|
|
256
266
|
#generation = 0;
|
|
257
267
|
/**
|
|
258
|
-
* Content
|
|
259
|
-
*
|
|
260
|
-
*
|
|
268
|
+
* Content hash of each row's routable credential material, keyed by id —
|
|
269
|
+
* exactly what {@link listAuthCredentials} exposes (id, provider,
|
|
270
|
+
* credential); blocks and usage overlays are excluded. Drives
|
|
271
|
+
* {@link #credentialRevision} independently of the broker's numeric
|
|
261
272
|
* generation, which is an in-memory counter that resets when the broker
|
|
262
|
-
* process restarts and so cannot be trusted for change detection.
|
|
273
|
+
* process restarts and so cannot be trusted for change detection. Full
|
|
274
|
+
* snapshots rehash every row; deltas and local writes rehash only the ids
|
|
275
|
+
* they touched (see {@link #dirtyCredentialIds}).
|
|
263
276
|
*/
|
|
264
|
-
#
|
|
265
|
-
/**
|
|
277
|
+
#credentialHashes = new Map<number, number | bigint>();
|
|
278
|
+
/** Ids whose row changed since {@link #credentialHashes} was last reconciled. */
|
|
279
|
+
#dirtyCredentialIds = new Set<number>();
|
|
280
|
+
/** Monotonic local counter bumped whenever {@link #credentialHashes} changes. */
|
|
266
281
|
#credentialRevision = 0;
|
|
267
282
|
/** Revision last reported as "seen" by {@link pollExternalChanges}; seeded from the initial snapshot. */
|
|
268
283
|
#acknowledgedRevision = 0;
|
|
@@ -305,6 +320,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
305
320
|
readonly #observedUsageFlushMs: number;
|
|
306
321
|
/** Latched once the broker answered 404 — old broker, never report again. */
|
|
307
322
|
#observedUsageUnsupported = false;
|
|
323
|
+
/** Tail of the serialized observed-usage sends; each flush waits for the one before it. */
|
|
324
|
+
#observedUsageFlush: Promise<void> = Promise.resolve();
|
|
325
|
+
/** Cancels the exit flush registered with the first buffered usage. */
|
|
326
|
+
#cancelObservedUsageExitFlush: (() => void) | undefined;
|
|
308
327
|
|
|
309
328
|
constructor(opts: RemoteAuthCredentialStoreOptions) {
|
|
310
329
|
this.#client = opts.client;
|
|
@@ -331,6 +350,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
331
350
|
|
|
332
351
|
#applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void {
|
|
333
352
|
const nowMs = Date.now();
|
|
353
|
+
// Reads check block expiry inline; expired rows and bookkeeping are pruned
|
|
354
|
+
// here so the block diff below compares only live rows.
|
|
355
|
+
this.cleanExpiredCredentialBlocks(nowMs);
|
|
334
356
|
this.#replaceBrokerUsageAccounts(snapshot.credentials);
|
|
335
357
|
const previousCredentials = this.#snapshot.credentials;
|
|
336
358
|
const credentials = snapshot.credentials
|
|
@@ -341,7 +363,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
341
363
|
this.#snapshot = { ...snapshot, credentials };
|
|
342
364
|
this.#generation = generation;
|
|
343
365
|
this.#snapshotReceivedAt = nowMs;
|
|
344
|
-
this.#
|
|
366
|
+
this.#rebuildCredentialHashes();
|
|
345
367
|
const onSnapshot = this.#onSnapshot;
|
|
346
368
|
if (!onSnapshot) return;
|
|
347
369
|
try {
|
|
@@ -352,32 +374,52 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
352
374
|
}
|
|
353
375
|
|
|
354
376
|
/**
|
|
355
|
-
*
|
|
356
|
-
* {@link #credentialRevision} when
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
377
|
+
* Rehash every row after a full snapshot apply and bump
|
|
378
|
+
* {@link #credentialRevision} when the routable credential set changed, so
|
|
379
|
+
* {@link pollExternalChanges} detects add/remove/replace even when the
|
|
380
|
+
* broker's numeric generation repeats (e.g. after a broker restart resets
|
|
381
|
+
* its in-memory counter).
|
|
360
382
|
*/
|
|
361
|
-
#
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.#
|
|
383
|
+
#rebuildCredentialHashes(): void {
|
|
384
|
+
const previous = this.#credentialHashes;
|
|
385
|
+
const next = new Map<number, number | bigint>();
|
|
386
|
+
let changed = false;
|
|
387
|
+
for (const entry of this.#snapshot.credentials) {
|
|
388
|
+
const hash = credentialContentHash(entry);
|
|
389
|
+
next.set(entry.id, hash);
|
|
390
|
+
if (previous.get(entry.id) !== hash) changed = true;
|
|
391
|
+
}
|
|
392
|
+
this.#credentialHashes = next;
|
|
393
|
+
this.#dirtyCredentialIds.clear();
|
|
394
|
+
if (changed || next.size !== previous.size) this.#credentialRevision += 1;
|
|
366
395
|
}
|
|
367
396
|
|
|
368
397
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
* overlays do not.
|
|
398
|
+
* Rehash only the rows marked in {@link #dirtyCredentialIds} (stream deltas
|
|
399
|
+
* and local writes) and bump {@link #credentialRevision} when any of them
|
|
400
|
+
* was added, removed, or changed since the last reconcile.
|
|
373
401
|
*/
|
|
374
|
-
#
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
402
|
+
#refreshCredentialRevision(): void {
|
|
403
|
+
const dirty = this.#dirtyCredentialIds;
|
|
404
|
+
if (dirty.size === 0) return;
|
|
405
|
+
let changed = false;
|
|
406
|
+
for (const entry of this.#snapshot.credentials) {
|
|
407
|
+
if (!dirty.delete(entry.id)) continue;
|
|
408
|
+
const hash = credentialContentHash(entry);
|
|
409
|
+
if (this.#credentialHashes.get(entry.id) !== hash) {
|
|
410
|
+
this.#credentialHashes.set(entry.id, hash);
|
|
411
|
+
changed = true;
|
|
412
|
+
}
|
|
413
|
+
if (dirty.size === 0) break;
|
|
414
|
+
}
|
|
415
|
+
// Dirty ids no longer in the snapshot were removed.
|
|
416
|
+
for (const id of dirty) {
|
|
417
|
+
if (this.#credentialHashes.delete(id)) changed = true;
|
|
418
|
+
}
|
|
419
|
+
dirty.clear();
|
|
420
|
+
if (changed) this.#credentialRevision += 1;
|
|
380
421
|
}
|
|
422
|
+
|
|
381
423
|
#protectNewSnapshotBlocks(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[], nowMs: number): void {
|
|
382
424
|
const previousBlocksByKey = new Map<string, string>();
|
|
383
425
|
for (const entry of previous) {
|
|
@@ -575,19 +617,25 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
575
617
|
this.#removeStreamCredential(entry.id, refresher, generation, serverNowMs, { retainBrokerUsageAccount: true });
|
|
576
618
|
return;
|
|
577
619
|
}
|
|
578
|
-
const
|
|
620
|
+
const nowMs = Date.now();
|
|
621
|
+
const incoming = this.#normalizeSnapshotEntryBlocks(entry, nowMs);
|
|
579
622
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === incoming.id);
|
|
580
|
-
|
|
623
|
+
// Expired rows linger until the next prune; compare only live ones.
|
|
624
|
+
const previousBlocks =
|
|
625
|
+
index === -1
|
|
626
|
+
? undefined
|
|
627
|
+
: this.#snapshot.credentials[index]?.blocks?.filter(block => block.blockedUntilMs > nowMs);
|
|
581
628
|
const blocksChanged = !credentialBlockSnapshotsEqual(previousBlocks, incoming.blocks);
|
|
582
629
|
if (blocksChanged) this.#invalidateUsageCache();
|
|
583
630
|
const credentials =
|
|
584
631
|
index === -1
|
|
585
632
|
? [...this.#snapshot.credentials, incoming]
|
|
586
633
|
: this.#snapshot.credentials.map((candidate, i) => (i === index ? incoming : candidate));
|
|
587
|
-
if (blocksChanged) this.#protectNewSnapshotBlocks(this.#snapshot.credentials, credentials,
|
|
634
|
+
if (blocksChanged) this.#protectNewSnapshotBlocks(this.#snapshot.credentials, credentials, nowMs);
|
|
588
635
|
this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
|
|
589
636
|
this.#generation = generation;
|
|
590
|
-
this.#snapshotReceivedAt =
|
|
637
|
+
this.#snapshotReceivedAt = nowMs;
|
|
638
|
+
this.#dirtyCredentialIds.add(incoming.id);
|
|
591
639
|
this.#refreshCredentialRevision();
|
|
592
640
|
}
|
|
593
641
|
|
|
@@ -599,12 +647,14 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
599
647
|
options?: { retainBrokerUsageAccount?: boolean },
|
|
600
648
|
): void {
|
|
601
649
|
if (!options?.retainBrokerUsageAccount) this.#removeBrokerUsageAccount(id);
|
|
650
|
+
const nowMs = Date.now();
|
|
602
651
|
const removed = this.#snapshot.credentials.find(entry => entry.id === id);
|
|
603
|
-
if (removed?.blocks
|
|
652
|
+
if (removed?.blocks?.some(block => block.blockedUntilMs > nowMs)) this.#invalidateUsageCache();
|
|
604
653
|
const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id);
|
|
605
654
|
this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
|
|
606
655
|
this.#generation = generation;
|
|
607
|
-
this.#snapshotReceivedAt =
|
|
656
|
+
this.#snapshotReceivedAt = nowMs;
|
|
657
|
+
this.#dirtyCredentialIds.add(id);
|
|
608
658
|
this.#refreshCredentialRevision();
|
|
609
659
|
}
|
|
610
660
|
|
|
@@ -662,26 +712,38 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
662
712
|
|
|
663
713
|
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
664
714
|
this.#noteActivity();
|
|
665
|
-
|
|
666
|
-
this.cleanExpiredCredentialBlocks(nowMs);
|
|
715
|
+
// Expired rows are pruned on snapshot apply, not per read; filter inline.
|
|
667
716
|
const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
|
|
668
717
|
if (!entry?.blocks) return undefined;
|
|
669
718
|
const block = entry.blocks.find(
|
|
670
719
|
candidate => candidate.providerKey === providerKey && candidate.blockScope === blockScope,
|
|
671
720
|
);
|
|
672
|
-
if (!block || block.blockedUntilMs <=
|
|
721
|
+
if (!block || block.blockedUntilMs <= Date.now()) return undefined;
|
|
673
722
|
return block.blockedUntilMs;
|
|
674
723
|
}
|
|
675
724
|
|
|
725
|
+
getCredentialBlockScopes(credentialId: number, providerKey: string): Map<string, number> {
|
|
726
|
+
this.#noteActivity();
|
|
727
|
+
const nowMs = Date.now();
|
|
728
|
+
const scopes = new Map<string, number>();
|
|
729
|
+
const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
|
|
730
|
+
for (const block of entry?.blocks ?? []) {
|
|
731
|
+
if (block.providerKey !== providerKey || block.blockedUntilMs <= nowMs) continue;
|
|
732
|
+
scopes.set(block.blockScope, block.blockedUntilMs);
|
|
733
|
+
}
|
|
734
|
+
return scopes;
|
|
735
|
+
}
|
|
736
|
+
|
|
676
737
|
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
677
738
|
if (this.getCredentialBlock(credentialId, providerKey, blockScope) === undefined) return undefined;
|
|
678
|
-
|
|
739
|
+
const key = blockKey(credentialId, { providerKey, blockScope });
|
|
740
|
+
const reconcileAfterMs = this.#credentialBlockReconcileAfter.get(key);
|
|
741
|
+
return reconcileAfterMs !== undefined && reconcileAfterMs > Date.now() ? reconcileAfterMs : undefined;
|
|
679
742
|
}
|
|
680
743
|
|
|
681
744
|
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
682
745
|
this.#noteActivity();
|
|
683
746
|
const nowMs = Date.now();
|
|
684
|
-
this.cleanExpiredCredentialBlocks(nowMs);
|
|
685
747
|
const ids = new Set(credentialIds);
|
|
686
748
|
const blocks: StoredCredentialBlock[] = [];
|
|
687
749
|
for (const entry of this.#snapshot.credentials) {
|
|
@@ -802,6 +864,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
802
864
|
for (const entry of this.#snapshot.credentials) {
|
|
803
865
|
if (entry.id !== id) continue;
|
|
804
866
|
entry.credential = credential as typeof entry.credential;
|
|
867
|
+
this.#dirtyCredentialIds.add(id);
|
|
805
868
|
return;
|
|
806
869
|
}
|
|
807
870
|
}
|
|
@@ -924,15 +987,20 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
924
987
|
// `entries` is the broker's authoritative post-upsert list of rows for
|
|
925
988
|
// `provider`. Drop our existing rows for the same provider and splice in
|
|
926
989
|
// the fresh set — preserving every other provider's rows in place.
|
|
927
|
-
const existingBlocks = new Map(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
990
|
+
const existingBlocks = new Map<number, readonly CredentialBlockSnapshot[] | undefined>();
|
|
991
|
+
const others: SnapshotEntry[] = [];
|
|
992
|
+
for (const entry of this.#snapshot.credentials) {
|
|
993
|
+
if (entry.provider !== provider) {
|
|
994
|
+
others.push(entry);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
this.#dirtyCredentialIds.add(entry.id);
|
|
998
|
+
if (entry.blocks !== undefined) existingBlocks.set(entry.id, entry.blocks);
|
|
999
|
+
}
|
|
933
1000
|
const incoming = entries
|
|
934
1001
|
.filter(entry => isCredentialInAccountPool(entry, this.#accountPool))
|
|
935
1002
|
.map(entry => credentialEntryWithBlocks(entry, existingBlocks.get(entry.id)));
|
|
1003
|
+
for (const entry of incoming) this.#dirtyCredentialIds.add(entry.id);
|
|
936
1004
|
this.#snapshot = { ...this.#snapshot, credentials: [...others, ...incoming] };
|
|
937
1005
|
}
|
|
938
1006
|
#applyCredentialEntry(entry: AuthCredentialSnapshotEntry): boolean {
|
|
@@ -943,6 +1011,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
943
1011
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
|
|
944
1012
|
const existingBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
|
|
945
1013
|
const incoming = credentialEntryWithBlocks(entry, existingBlocks);
|
|
1014
|
+
this.#dirtyCredentialIds.add(entry.id);
|
|
946
1015
|
if (index === -1) {
|
|
947
1016
|
this.#snapshot = { ...this.#snapshot, credentials: [...this.#snapshot.credentials, incoming] };
|
|
948
1017
|
return true;
|
|
@@ -954,13 +1023,18 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
954
1023
|
}
|
|
955
1024
|
|
|
956
1025
|
#removeProviderEntries(provider: string): void {
|
|
957
|
-
const next =
|
|
1026
|
+
const next: SnapshotEntry[] = [];
|
|
1027
|
+
for (const entry of this.#snapshot.credentials) {
|
|
1028
|
+
if (entry.provider === provider) this.#dirtyCredentialIds.add(entry.id);
|
|
1029
|
+
else next.push(entry);
|
|
1030
|
+
}
|
|
958
1031
|
this.#snapshot = { ...this.#snapshot, credentials: next };
|
|
959
1032
|
}
|
|
960
1033
|
|
|
961
1034
|
#removeCredentialById(id: number): void {
|
|
962
1035
|
const next = this.#snapshot.credentials.filter(entry => entry.id !== id);
|
|
963
1036
|
this.#snapshot = { ...this.#snapshot, credentials: next };
|
|
1037
|
+
this.#dirtyCredentialIds.add(id);
|
|
964
1038
|
}
|
|
965
1039
|
|
|
966
1040
|
#normalizeSnapshotEntryBlocks(entry: SnapshotEntry, nowMs: number): SnapshotEntry {
|
|
@@ -1030,7 +1104,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1030
1104
|
if (index === -1) return;
|
|
1031
1105
|
const entry = this.#snapshot.credentials[index]!;
|
|
1032
1106
|
const incoming = toCredentialBlockSnapshot(block);
|
|
1033
|
-
|
|
1107
|
+
// Expired rows linger until the next prune; drop this entry's now so an
|
|
1108
|
+
// expired row in the same scope cannot lend the new block its stale fields.
|
|
1109
|
+
const nowMs = Date.now();
|
|
1110
|
+
const blocks = (entry.blocks ?? []).filter(candidate => candidate.blockedUntilMs > nowMs);
|
|
1034
1111
|
const blockIndex = blocks.findIndex(
|
|
1035
1112
|
candidate => candidate.providerKey === incoming.providerKey && candidate.blockScope === incoming.blockScope,
|
|
1036
1113
|
);
|
|
@@ -1125,11 +1202,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1125
1202
|
}
|
|
1126
1203
|
}
|
|
1127
1204
|
|
|
1128
|
-
async invalidateUsageCache(signal?: AbortSignal): Promise<void> {
|
|
1205
|
+
async invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void> {
|
|
1129
1206
|
this.#noteActivity();
|
|
1130
1207
|
this.#invalidateUsageCache();
|
|
1131
1208
|
try {
|
|
1132
|
-
await this.#client.notifyUsageStale(signal);
|
|
1209
|
+
await this.#client.notifyUsageStale(provider, signal);
|
|
1133
1210
|
} catch (err) {
|
|
1134
1211
|
logger.warn("auth-broker notification of stale usage failed", { error: String(err) });
|
|
1135
1212
|
} finally {
|
|
@@ -1385,9 +1462,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1385
1462
|
|
|
1386
1463
|
/**
|
|
1387
1464
|
* Fold locally observed request usage into the pending report and schedule
|
|
1388
|
-
* a flush. One `POST /v1/usage/observed` at most per flush interval
|
|
1389
|
-
*
|
|
1390
|
-
* (pre-endpoint broker) disables
|
|
1465
|
+
* a flush. One `POST /v1/usage/observed` at most per flush interval, plus a
|
|
1466
|
+
* final one when the process exits; on failure the batch is retained and
|
|
1467
|
+
* retried with the next flush. A 404 (pre-endpoint broker) disables
|
|
1468
|
+
* reporting for the life of this store.
|
|
1391
1469
|
*
|
|
1392
1470
|
* `client` overrides the reporting identity — the auth-gateway attributes
|
|
1393
1471
|
* each request to the originating install/app instead of the gateway host.
|
|
@@ -1417,9 +1495,31 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1417
1495
|
}, this.#observedUsageFlushMs);
|
|
1418
1496
|
this.#observedUsageTimer.unref?.();
|
|
1419
1497
|
}
|
|
1498
|
+
// The timer never holds the process open, so a process that exits first (a
|
|
1499
|
+
// one-shot run, a signal) sends what is still buffered on the way out. The
|
|
1500
|
+
// wait is bounded: an unreachable broker must not stall the exit.
|
|
1501
|
+
this.#cancelObservedUsageExitFlush ??= postmortem.register("auth-broker-observed-usage", () =>
|
|
1502
|
+
raceSignal(
|
|
1503
|
+
this.#flushObservedUsage(),
|
|
1504
|
+
AbortSignal.timeout(OBSERVED_USAGE_EXIT_FLUSH_MS),
|
|
1505
|
+
"observed usage exit flush timed out",
|
|
1506
|
+
).catch(error => {
|
|
1507
|
+
logger.debug("auth-broker observed usage dropped at exit", { error: String(error) });
|
|
1508
|
+
}),
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
#flushObservedUsage(): Promise<void> {
|
|
1513
|
+
// The tail never rejects, so one failed send cannot stall every later flush.
|
|
1514
|
+
this.#observedUsageFlush = this.#observedUsageFlush
|
|
1515
|
+
.then(() => this.#sendObservedUsage())
|
|
1516
|
+
.catch(error => {
|
|
1517
|
+
logger.debug("auth-broker observed usage flush failed", { error: String(error) });
|
|
1518
|
+
});
|
|
1519
|
+
return this.#observedUsageFlush;
|
|
1420
1520
|
}
|
|
1421
1521
|
|
|
1422
|
-
async #
|
|
1522
|
+
async #sendObservedUsage(): Promise<void> {
|
|
1423
1523
|
if (this.#observedUsage.size === 0 || this.#observedUsageUnsupported) return;
|
|
1424
1524
|
const batch = [...this.#observedUsage.values()];
|
|
1425
1525
|
this.#observedUsage.clear();
|
|
@@ -1468,8 +1568,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1468
1568
|
clearTimeout(this.#observedUsageTimer);
|
|
1469
1569
|
this.#observedUsageTimer = undefined;
|
|
1470
1570
|
}
|
|
1471
|
-
//
|
|
1472
|
-
|
|
1571
|
+
// Final flush. The exit registration outlives close() until it settles, so a
|
|
1572
|
+
// process exiting right after close() still sends it; failures are dropped.
|
|
1573
|
+
void this.#flushObservedUsage().finally(() => this.#cancelObservedUsageExitFlush?.());
|
|
1473
1574
|
this.#cache.clear();
|
|
1474
1575
|
this.#usageOverlays.clear();
|
|
1475
1576
|
}
|
|
@@ -1481,9 +1582,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1481
1582
|
* pick the one whose identity (accountId / email / projectId) lines up with
|
|
1482
1583
|
* the credential the caller is asking about.
|
|
1483
1584
|
*
|
|
1484
|
-
* Falls back to the lone
|
|
1485
|
-
* through to `null` when nothing matches, which
|
|
1486
|
-
* usage data" (ranking proceeds without a usage
|
|
1585
|
+
* Falls back to the provider's lone report when it shares no identity field
|
|
1586
|
+
* with the credential; falls through to `null` when nothing matches, which
|
|
1587
|
+
* `AuthStorage` treats as "no usage data" (ranking proceeds without a usage
|
|
1588
|
+
* signal for this credential).
|
|
1487
1589
|
*/
|
|
1488
1590
|
function matchUsageReport(reports: UsageReport[], provider: Provider, credential: OAuthCredential): UsageReport | null {
|
|
1489
1591
|
const all = reports.filter(report => report.provider === provider);
|
|
@@ -1534,7 +1636,17 @@ function matchUsageReport(reports: UsageReport[], provider: Provider, credential
|
|
|
1534
1636
|
report => !readMetadataString((report.metadata ?? {}) as Record<string, unknown>, "orgId"),
|
|
1535
1637
|
);
|
|
1536
1638
|
if (candidates.length === 0) return null;
|
|
1537
|
-
|
|
1639
|
+
// The sole report stands in for a credential it shares no identity field
|
|
1640
|
+
// with; otherwise identity decides, because a report naming another email,
|
|
1641
|
+
// account or project is a sibling's pool whose fetch succeeded where this
|
|
1642
|
+
// credential's failed.
|
|
1643
|
+
if (
|
|
1644
|
+
all.length === 1 &&
|
|
1645
|
+
candidates.length === 1 &&
|
|
1646
|
+
!reportHasComparableIdentity(candidates[0], accountId, email, projectId)
|
|
1647
|
+
) {
|
|
1648
|
+
return candidates[0];
|
|
1649
|
+
}
|
|
1538
1650
|
for (const report of candidates) {
|
|
1539
1651
|
if (reportMatchesIdentity(report, accountId, email, projectId)) return report;
|
|
1540
1652
|
}
|
|
@@ -1598,7 +1710,13 @@ function findMatchingReportIndex(reports: UsageReport[], overlay: UsageReport):
|
|
|
1598
1710
|
candidate => !readMetadataString((candidate.report.metadata ?? {}) as Record<string, unknown>, "orgId"),
|
|
1599
1711
|
);
|
|
1600
1712
|
if (candidates.length === 0) return -1;
|
|
1601
|
-
if (
|
|
1713
|
+
if (
|
|
1714
|
+
all.length === 1 &&
|
|
1715
|
+
candidates.length === 1 &&
|
|
1716
|
+
!reportHasComparableIdentity(candidates[0]!.report, accountId, email, projectId)
|
|
1717
|
+
) {
|
|
1718
|
+
return candidates[0]!.index;
|
|
1719
|
+
}
|
|
1602
1720
|
for (const candidate of candidates) {
|
|
1603
1721
|
if (reportMatchesIdentity(candidate.report, accountId, email, projectId)) return candidate.index;
|
|
1604
1722
|
}
|
|
@@ -1620,19 +1738,39 @@ function reportMatchesIdentity(
|
|
|
1620
1738
|
const metaAccount = readMetadataString(metadata, "accountId") ?? readMetadataString(metadata, "account_id");
|
|
1621
1739
|
if (metaAccount && metaAccount.toLowerCase() === accountId) return true;
|
|
1622
1740
|
for (const limit of report.limits) {
|
|
1623
|
-
if (limit.scope.accountId?.toLowerCase() === accountId) return true;
|
|
1741
|
+
if (limit.scope.accountId?.trim().toLowerCase() === accountId) return true;
|
|
1624
1742
|
}
|
|
1625
1743
|
}
|
|
1626
1744
|
if (projectId) {
|
|
1627
1745
|
const metaProject = readMetadataString(metadata, "projectId") ?? readMetadataString(metadata, "project_id");
|
|
1628
1746
|
if (metaProject && metaProject.toLowerCase() === projectId) return true;
|
|
1629
1747
|
for (const limit of report.limits) {
|
|
1630
|
-
if (limit.scope.projectId?.toLowerCase() === projectId) return true;
|
|
1748
|
+
if (limit.scope.projectId?.trim().toLowerCase() === projectId) return true;
|
|
1631
1749
|
}
|
|
1632
1750
|
}
|
|
1633
1751
|
return false;
|
|
1634
1752
|
}
|
|
1635
1753
|
|
|
1754
|
+
/** Whether the report names an email, account or project the caller can compare against. */
|
|
1755
|
+
function reportHasComparableIdentity(
|
|
1756
|
+
report: UsageReport,
|
|
1757
|
+
accountId: string | undefined,
|
|
1758
|
+
email: string | undefined,
|
|
1759
|
+
projectId: string | undefined,
|
|
1760
|
+
): boolean {
|
|
1761
|
+
const metadata = report.metadata ?? {};
|
|
1762
|
+
if (email && readMetadataString(metadata, "email")) return true;
|
|
1763
|
+
if (accountId) {
|
|
1764
|
+
if (readMetadataString(metadata, "accountId") ?? readMetadataString(metadata, "account_id")) return true;
|
|
1765
|
+
if (report.limits.some(limit => limit.scope.accountId?.trim())) return true;
|
|
1766
|
+
}
|
|
1767
|
+
if (projectId) {
|
|
1768
|
+
if (readMetadataString(metadata, "projectId") ?? readMetadataString(metadata, "project_id")) return true;
|
|
1769
|
+
if (report.limits.some(limit => limit.scope.projectId?.trim())) return true;
|
|
1770
|
+
}
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1636
1774
|
function readMetadataString(metadata: Record<string, unknown>, key: string): string | undefined {
|
|
1637
1775
|
const value = metadata[key];
|
|
1638
1776
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|