test(auth): switched OAuth ranking tests to weighted selection

- Replaced top-rank assertions with weighted-preference distribution checks.
- Added cases for equal-priority balancing and 2x best-bucket cap.
- Added coding-agent snapshot-cache boot and seed tests.
This commit is contained in:
can1357
2026-06-05 11:13:44 +02:00
parent 363d88ca5c
commit b8a602ac2a
13 changed files with 273 additions and 40 deletions
+10
View File
@@ -140,6 +140,14 @@ When the gateway (or any other broker client) calls `fetchUsageReports()` / `get
The 15 s client window deliberately sits below the broker’s 5 min server cache, so almost every client poll is served from the broker’s already-cached value; the client cache exists to absorb the parallel fan-out generated by `AuthStorage.#rankOAuthSelections` into a single broker round-trip.
## Client snapshot cache
`discoverAuthStorage()` persists the broker snapshot to `~/.omp/cache/auth-broker-snapshot.enc` after the initial `/v1/snapshot` fetch and after later broker-sourced full snapshots. The file is AES-256-GCM encrypted with `SHA-256(OMP_AUTH_BROKER_TOKEN)` and authenticated with the broker URL as additional data, so changing either the token or URL makes the cache unreadable. The file is written atomically with mode `0600`.
Freshness is anchored to the broker-stamped `snapshot.generatedAt`, not local write time. Default TTL is 1 h (`OMP_AUTH_BROKER_SNAPSHOT_TTL_MS`); `0` disables the cache and restores the old always-fetch boot path. When the cached snapshot is still fresh, `omp` boots from it and skips the blocking `/v1/snapshot` query. `RemoteAuthCredentialStore` still starts its normal SSE / long-poll background sync immediately, so deleted or rotated credentials reconcile after startup, and expired OAuth access tokens still refresh through `POST /v1/credential/:id/refresh`.
If the broker is down at boot and a fresh cache exists, startup now succeeds from the cached snapshot. If the cache is missing, expired, corrupt, written for a different URL, or encrypted with a different token, startup falls back to the live fetch and fails the same way it did before if the broker is unreachable.
## Operator opt-in
The broker is **off** unless `OMP_AUTH_BROKER_URL` (or `auth.broker.url` in `config.yml`) is set. When set, `discoverAuthStorage` in `packages/coding-agent/src/sdk.ts` swaps the local SQLite credential store for `RemoteAuthCredentialStore` and every API call resolves credentials through the broker.
@@ -150,6 +158,8 @@ The broker is **off** unless `OMP_AUTH_BROKER_URL` (or `auth.broker.url` in `con
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `OMP_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`). Selecting this puts the client in broker mode — local SQLite is bypassed. | Any time the omp client should resolve credentials through a broker (and required by `omp auth-gateway serve`). |
| `OMP_AUTH_BROKER_TOKEN` | Bearer token used for every broker endpoint except `/v1/healthz`. | When `OMP_AUTH_BROKER_URL` is set and no token is available from `auth.broker.token` or `<config-dir>/auth-broker.token`. |
| `OMP_AUTH_BROKER_SNAPSHOT_TTL_MS` | Freshness window for the encrypted local snapshot cache. Default `3600000` (1 h); `0` disables cache reads and writes. | Optional in broker mode. |
| `OMP_AUTH_BROKER_SNAPSHOT_CACHE` | Path override for the encrypted local snapshot cache. Default `~/.omp/cache/auth-broker-snapshot.enc` (or XDG cache equivalent). | Optional in broker mode. |
Resolution order in `resolveAuthBrokerConfig()`:
+2
View File
@@ -97,6 +97,8 @@ When the broker is enabled, the local SQLite credential store is bypassed and al
| ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `OMP_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`); selects broker mode | Resolving credentials through a broker; also required by `omp auth-gateway serve` (the gateway is itself a broker client) | Wins over `auth.broker.url` in `config.yml`. When set with no resolvable token, `resolveAuthBrokerConfig()` hard-errors instead of falling back to local SQLite. |
| `OMP_AUTH_BROKER_TOKEN` | Bearer token sent on every broker endpoint except `/v1/healthz` | `OMP_AUTH_BROKER_URL` is set and no token is available from `auth.broker.token` or `<config-dir>/auth-broker.token` | Resolution: this env → `auth.broker.token` (`$ENV_NAME` indirection supported) → `<config-dir>/auth-broker.token` (mode `0600`). `<config-dir>` is `~/.omp/` (respecting `PI_CONFIG_DIR`). |
| `OMP_AUTH_BROKER_SNAPSHOT_TTL_MS` | Freshness window for the encrypted local broker snapshot cache | Optional in broker mode | Default `3600000` (1 h). Freshness is based on broker `snapshot.generatedAt`; `0` disables cache reads/writes and forces the old blocking fetch every startup. |
| `OMP_AUTH_BROKER_SNAPSHOT_CACHE` | Path to the encrypted local broker snapshot cache | Optional in broker mode | Defaults to `~/.omp/cache/auth-broker-snapshot.enc` (or XDG cache equivalent). Useful for tests, ephemeral hosts, or relocating the `0600` cache file. |
The gateway has no dedicated env vars — it inherits `OMP_AUTH_BROKER_*`. Its own inbound bearer token lives at `<config-dir>/auth-gateway.token` and is managed via `omp auth-gateway token`.
+8
View File
@@ -2,6 +2,14 @@
## [Unreleased]
### Added
- Added an AES-256-GCM auth-broker snapshot cache module and `RemoteAuthCredentialStoreOptions.onSnapshot` so broker clients can persist broker-sourced full snapshots without blocking startup on every run.
### Changed
- Changed usage-ranked OAuth credential selection to pick deterministic session-sticky weighted buckets instead of always choosing the top-ranked account, capping the best account at 2x the baseline session likelihood while keeping equal-priority accounts evenly balanced.
## [15.9.1] - 2026-06-04
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
export * from "./client";
export * from "./refresher";
export * from "./remote-store";
export * from "./snapshot-cache";
export * from "./server";
export * from "./snapshot-cache";
export * from "./types";
@@ -126,7 +126,6 @@ describe("RemoteAuthCredentialStore SSE integration", () => {
});
expect(callbacks).toHaveLength(0);
storage!.upsertCredential("anthropic", mintOAuthCredential("callback", Date.now() + 120_000));
const refreshed = await remote.refreshSnapshot();
expect(callbacks).toHaveLength(1);
@@ -2,11 +2,7 @@ import { describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import {
readAuthBrokerSnapshotCache,
type SnapshotResponse,
writeAuthBrokerSnapshotCache,
} from "../src";
import { readAuthBrokerSnapshotCache, type SnapshotResponse, writeAuthBrokerSnapshotCache } from "../src";
const TOKEN = "broker-cache-token";
const URL = "http://127.0.0.1:8765";
@@ -173,7 +173,7 @@ describe("AuthStorage codex oauth ranking", () => {
}
});
test("prefers near-reset weekly account over lower-used far-reset account", async () => {
test("weights near-reset weekly account over lower-used far-reset account", async () => {
if (!authStorage) throw new Error("test setup failed");
await authStorage.set("openai-codex", [
@@ -198,11 +198,11 @@ describe("AuthStorage codex oauth ranking", () => {
}),
);
const apiKey = await authStorage.getApiKey("openai-codex", "session-weekly-reset");
expect(apiKey).toBe("api-acct-near");
const counts = await countApiKeySelections(authStorage, "openai-codex", "weighted-codex-near");
expectWeightedPreference(counts, "api-acct-near", "api-acct-far");
});
test("prioritizes fresh 5h ticker account at 0% usage", async () => {
test("weights fresh 5h ticker account at 0% usage", async () => {
if (!authStorage) throw new Error("test setup failed");
await authStorage.set("openai-codex", [
@@ -235,8 +235,8 @@ describe("AuthStorage codex oauth ranking", () => {
}),
);
const apiKey = await authStorage.getApiKey("openai-codex", "session-five-hour-start");
expect(apiKey).toBe("api-acct-zero");
const counts = await countApiKeySelections(authStorage, "openai-codex", "weighted-codex-zero");
expectWeightedPreference(counts, "api-acct-zero", "api-acct-progress");
});
test("skips exhausted weekly account even when reset is near", async () => {
if (!authStorage) throw new Error("test setup failed");
@@ -426,7 +426,7 @@ describe("AuthStorage codex oauth ranking", () => {
expect(elapsedMs).toBeLessThan(1_000);
});
test("sorts 3 accounts by weekly drain rate", async () => {
test("weights 3 accounts by weekly drain rate", async () => {
if (!authStorage) throw new Error("test setup failed");
await authStorage.set("openai-codex", [
@@ -460,8 +460,9 @@ describe("AuthStorage codex oauth ranking", () => {
}),
);
const apiKey = await authStorage.getApiKey("openai-codex", "session-three-accounts");
expect(apiKey).toBe("api-acct-slow");
const counts = await countApiKeySelections(authStorage, "openai-codex", "weighted-codex-three");
expect(countFor(counts, "api-acct-slow")).toBeGreaterThan(countFor(counts, "api-acct-medium"));
expect(countFor(counts, "api-acct-slow")).toBeGreaterThan(countFor(counts, "api-acct-fast"));
});
test("handles usage fetch failure gracefully (null report)", async () => {
@@ -482,8 +483,8 @@ describe("AuthStorage codex oauth ranking", () => {
}),
);
const apiKey = await authStorage.getApiKey("openai-codex", "session-null-usage");
expect(apiKey).toBe("api-acct-known");
const counts = await countApiKeySelections(authStorage, "openai-codex", "weighted-codex-known", 300);
expectWeightedPreference(counts, "api-acct-known", "api-acct-null");
});
test("refreshes expired oauth candidates in parallel before selection", async () => {
if (!authStorage) throw new Error("test setup failed");
@@ -650,7 +651,7 @@ describe("AuthStorage claude oauth ranking", () => {
}
});
test("prefers lower secondary drain rate account", async () => {
test("weights lower secondary drain rate account", async () => {
if (!authStorage) throw new Error("test setup failed");
await authStorage.set("anthropic", [
@@ -675,8 +676,67 @@ describe("AuthStorage claude oauth ranking", () => {
}),
);
const apiKey = await authStorage.getApiKey("anthropic", "session-claude-drain");
expect(apiKey).toBe("api-acct-near");
const counts = await countApiKeySelections(authStorage, "anthropic", "weighted-claude-near");
expectWeightedPreference(counts, "api-acct-near", "api-acct-far");
});
test("balances equal-priority accounts evenly", async () => {
if (!authStorage) throw new Error("test setup failed");
await authStorage.set("anthropic", [
{ type: "oauth", ...createCredential("acct-a", "a@example.com") },
{ type: "oauth", ...createCredential("acct-b", "b@example.com") },
]);
for (const accountId of ["acct-a", "acct-b"]) {
usageByAccount.set(
accountId,
createClaudeUsageReport({
accountId,
primary: { usedFraction: 0.25, resetInMs: 4 * HOUR_MS },
secondary: { usedFraction: 0.25, resetInMs: 4 * 24 * HOUR_MS },
}),
);
}
const counts = await countApiKeySelections(authStorage, "anthropic", "weighted-claude-equal", 200);
expect(Math.abs(countFor(counts, "api-acct-a") - countFor(counts, "api-acct-b"))).toBeLessThanOrEqual(25);
});
test("caps the strongest priority bucket at about 2x baseline weight", async () => {
if (!authStorage) throw new Error("test setup failed");
await authStorage.set("anthropic", [
{ type: "oauth", ...createCredential("acct-best", "best@example.com") },
{ type: "oauth", ...createCredential("acct-base-a", "base-a@example.com") },
{ type: "oauth", ...createCredential("acct-base-b", "base-b@example.com") },
]);
usageByAccount.set(
"acct-best",
createClaudeUsageReport({
accountId: "acct-best",
primary: { usedFraction: 0.05, resetInMs: 4 * HOUR_MS },
secondary: { usedFraction: 0.05, resetInMs: 6 * 24 * HOUR_MS },
}),
);
for (const accountId of ["acct-base-a", "acct-base-b"]) {
usageByAccount.set(
accountId,
createClaudeUsageReport({
accountId,
primary: { usedFraction: 0.7, resetInMs: 2 * HOUR_MS },
secondary: { usedFraction: 0.7, resetInMs: 2 * 24 * HOUR_MS },
}),
);
}
const counts = await countApiKeySelections(authStorage, "anthropic", "claude-cap", 300);
expectWeightedPreference(counts, "api-acct-best", "api-acct-base-a");
expectWeightedPreference(counts, "api-acct-best", "api-acct-base-b");
expect(Math.abs(countFor(counts, "api-acct-base-a") - countFor(counts, "api-acct-base-b"))).toBeLessThanOrEqual(
15,
);
});
test("skips exhausted account and picks healthy", async () => {
@@ -737,7 +797,7 @@ describe("AuthStorage claude oauth ranking", () => {
expect(apiKey).toBe("api-acct-soon");
});
test("sorts 3 accounts by secondary drain rate", async () => {
test("weights 3 accounts by secondary drain rate", async () => {
if (!authStorage) throw new Error("test setup failed");
await authStorage.set("anthropic", [
@@ -771,8 +831,9 @@ describe("AuthStorage claude oauth ranking", () => {
}),
);
const apiKey = await authStorage.getApiKey("anthropic", "session-claude-three");
expect(apiKey).toBe("api-acct-slow");
const counts = await countApiKeySelections(authStorage, "anthropic", "weighted-claude-three");
expect(countFor(counts, "api-acct-slow")).toBeGreaterThan(countFor(counts, "api-acct-medium"));
expect(countFor(counts, "api-acct-slow")).toBeGreaterThan(countFor(counts, "api-acct-fast"));
});
test("single credential works without ranking", async () => {
+1
View File
@@ -3,6 +3,7 @@
## [Unreleased]
### Added
- Added an encrypted local auth-broker snapshot cache for `discoverAuthStorage`, with `OMP_AUTH_BROKER_SNAPSHOT_TTL_MS` and `OMP_AUTH_BROKER_SNAPSHOT_CACHE`, so fresh cached broker credentials can boot without a blocking `/v1/snapshot` fetch and survive broker-down startup windows.
- Added `dry-balance` CLI command to perform a dry-run OAuth account balancing check across configurable random session IDs, with sample and concurrency options, JSON output, and success/failure summary reporting
- Added `--json` output mode and machine-readable result format to `omp dry-balance` for automated use
@@ -4,10 +4,10 @@ import chalk from "chalk";
import { ModelRegistry } from "../config/model-registry";
import {
formatModelString,
type ModelMatchPreferences,
resolveAllowedModels,
resolveCliModel,
resolveModelRoleValue,
type ModelMatchPreferences,
} from "../config/model-resolver";
import { Settings } from "../config/settings";
import { discoverAuthStorage } from "../sdk";
@@ -32,7 +32,11 @@ export interface DryBalanceAuthOptions {
}
export interface DryBalanceAuthStorage {
getOAuthAccess(provider: string, sessionId?: string, options?: DryBalanceAuthOptions): Promise<OAuthAccess | undefined>;
getOAuthAccess(
provider: string,
sessionId?: string,
options?: DryBalanceAuthOptions,
): Promise<OAuthAccess | undefined>;
}
export interface DryBalanceModelRegistry {
@@ -142,7 +146,9 @@ async function resolveDryBalanceModel(
const allowedModels = await resolveAllowedModels(modelRegistry, settings, preferences);
if (allowedModels.length === 0) {
throw new Error("No models available. Use --model to select a model or configure enabledModels/default model settings.");
throw new Error(
"No models available. Use --model to select a model or configure enabledModels/default model settings.",
);
}
const defaultRoleSpec = resolveModelRoleValue(settings?.getModelRole("default"), allowedModels, {
@@ -161,12 +167,11 @@ async function resolveDryBalanceModel(
return {
model: allowedModels[0],
warning: "No allowed model had usable credentials during default resolution; dry-balance will report OAuth failures for the first allowed model.",
warning:
"No allowed model had usable credentials during default resolution; dry-balance will report OAuth failures for the first allowed model.",
};
}
async function runOneAttempt(
model: Model<Api>,
modelRegistry: DryBalanceModelRegistry,
@@ -181,7 +186,8 @@ async function runOneAttempt(
modelId: model.id,
});
if (!access) return { ok: false, reason: "no OAuth access resolved" };
const account = access.email ?? access.accountId ?? access.projectId ?? access.enterpriseUrl ?? "(unknown oauth account)";
const account =
access.email ?? access.accountId ?? access.projectId ?? access.enterpriseUrl ?? "(unknown oauth account)";
return { ok: true, account };
} catch (error) {
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
@@ -205,8 +211,10 @@ async function mapConcurrent<T, R>(items: T[], concurrency: number, fn: (item: T
return results;
}
function sortedStats(map: Map<string, number>, samples: number): Array<{ label: string; count: number; percent: number }> {
function sortedStats(
map: Map<string, number>,
samples: number,
): Array<{ label: string; count: number; percent: number }> {
return [...map.entries()]
.map(([label, count]) => ({ label, count, percent: (count / samples) * 100 }))
.sort((left, right) => right.count - left.count || left.label.localeCompare(right.label));
@@ -253,7 +261,6 @@ function summarizeResults(
};
}
function formatRows(rows: Array<{ count: number; percent: number; label: string }>): string[] {
if (rows.length === 0) return [` ${chalk.dim("(none)")}`];
const maxCountWidth = Math.max(...rows.map(row => row.count.toString().length));
@@ -296,13 +303,18 @@ export async function runDryBalanceCommand(
deps: DryBalanceDependencies = {},
): Promise<DryBalanceSummary> {
const samples = normalizePositiveInteger("count", command.flags.count, DEFAULT_SAMPLE_COUNT);
const concurrency = Math.min(samples, normalizePositiveInteger("concurrency", command.flags.concurrency, DEFAULT_CONCURRENCY));
const concurrency = Math.min(
samples,
normalizePositiveInteger("concurrency", command.flags.concurrency, DEFAULT_CONCURRENCY),
);
const randomSessionId = deps.randomSessionId ?? (() => Bun.randomUUIDv7());
const writeStdout = deps.writeStdout ?? ((text: string) => process.stdout.write(text));
const writeStderr = deps.writeStderr ?? ((text: string) => process.stderr.write(text));
const setExitCode = deps.setExitCode ?? ((code: number) => {
process.exitCode = code;
});
const setExitCode =
deps.setExitCode ??
((code: number) => {
process.exitCode = code;
});
const runtime = await (deps.createRuntime ?? createDefaultRuntime)();
try {
const modelSelector = command.flags.model ?? command.model;
+1 -1
View File
@@ -106,8 +106,8 @@ import {
AuthBrokerClient,
AuthStorage,
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
readAuthBrokerSnapshotCache,
RemoteAuthCredentialStore,
readAuthBrokerSnapshotCache,
type SnapshotResponse,
writeAuthBrokerSnapshotCache,
} from "./session/auth-storage";
@@ -19,9 +19,9 @@ export {
AuthBrokerClient,
AuthStorage,
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
readAuthBrokerSnapshotCache,
REMOTE_REFRESH_SENTINEL,
RemoteAuthCredentialStore,
readAuthBrokerSnapshotCache,
SqliteAuthCredentialStore,
writeAuthBrokerSnapshotCache,
} from "@oh-my-pi/pi-ai";
@@ -0,0 +1,140 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { type AuthBrokerServerHandle, AuthStorage, SqliteAuthCredentialStore, startAuthBroker } from "@oh-my-pi/pi-ai";
import { discoverAuthStorage } from "../src/sdk";
import {
readAuthBrokerSnapshotCache,
type SnapshotResponse,
writeAuthBrokerSnapshotCache,
} from "../src/session/auth-storage";
const ENV_KEYS = [
"OMP_AUTH_BROKER_URL",
"OMP_AUTH_BROKER_TOKEN",
"OMP_AUTH_BROKER_SNAPSHOT_CACHE",
"OMP_AUTH_BROKER_SNAPSHOT_TTL_MS",
] as const;
const PROVIDER = "unit-auth-broker-cache";
const TOKEN = "coding-agent-cache-token";
const savedEnv: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>> = {};
function makeSnapshot(urlTime: number): SnapshotResponse {
return {
generation: 11,
generatedAt: urlTime,
serverNowMs: urlTime,
refresher: {
enabled: false,
intervalMs: 60_000,
skewMs: 300_000,
nextSweepInMs: Number.MAX_SAFE_INTEGER,
},
credentials: [
{
id: 1,
provider: PROVIDER,
credential: { type: "api_key", key: "cached-api-key" },
identityKey: null,
rotatesInMs: null,
},
],
};
}
async function waitUntil(predicate: () => boolean | Promise<boolean>, timeoutMs = 2_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await predicate()) return;
await Bun.sleep(10);
}
if (!(await predicate())) throw new Error("waitUntil timeout");
}
describe("discoverAuthStorage auth-broker snapshot cache", () => {
let tempDir = "";
beforeEach(async () => {
for (const key of ENV_KEYS) savedEnv[key] = process.env[key];
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "coding-agent-auth-broker-cache-"));
});
afterEach(async () => {
for (const key of ENV_KEYS) {
if (savedEnv[key] === undefined) delete process.env[key];
else process.env[key] = savedEnv[key];
}
await fs.rm(tempDir, { recursive: true, force: true });
});
test("boots from a fresh encrypted cache when the broker is down", async () => {
const cachePath = path.join(tempDir, "snapshot.enc");
const downUrl = "http://127.0.0.1:1";
process.env.OMP_AUTH_BROKER_URL = downUrl;
process.env.OMP_AUTH_BROKER_TOKEN = TOKEN;
process.env.OMP_AUTH_BROKER_SNAPSHOT_CACHE = cachePath;
process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS = "3600000";
await writeAuthBrokerSnapshotCache({
path: cachePath,
token: TOKEN,
url: downUrl,
snapshot: makeSnapshot(Date.now()),
});
const storage = await discoverAuthStorage(tempDir);
try {
expect(await storage.getApiKey(PROVIDER)).toBe("cached-api-key");
} finally {
storage.close();
}
});
test("seeds the encrypted cache after an initial broker fetch", async () => {
const cachePath = path.join(tempDir, "snapshot.enc");
const brokerStore = await SqliteAuthCredentialStore.open(path.join(tempDir, "broker.db"));
brokerStore.saveApiKey(PROVIDER, "broker-api-key");
const brokerStorage = new AuthStorage(brokerStore);
await brokerStorage.reload();
let handle: AuthBrokerServerHandle | undefined;
let storage: AuthStorage | undefined;
try {
handle = startAuthBroker({
storage: brokerStorage,
bind: "127.0.0.1:0",
bearerTokens: [TOKEN],
disableRefresher: true,
});
process.env.OMP_AUTH_BROKER_URL = handle.url;
process.env.OMP_AUTH_BROKER_TOKEN = TOKEN;
process.env.OMP_AUTH_BROKER_SNAPSHOT_CACHE = cachePath;
process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS = "3600000";
storage = await discoverAuthStorage(tempDir);
expect(await storage.getApiKey(PROVIDER)).toBe("broker-api-key");
await waitUntil(async () => {
const cached = await readAuthBrokerSnapshotCache({
path: cachePath,
token: TOKEN,
url: handle!.url,
ttlMs: 3_600_000,
});
return cached?.credentials.some(entry => entry.provider === PROVIDER) ?? false;
});
const cached = await readAuthBrokerSnapshotCache({
path: cachePath,
token: TOKEN,
url: handle.url,
ttlMs: 3_600_000,
});
const entry = cached?.credentials.find(candidate => candidate.provider === PROVIDER);
expect(entry?.credential).toEqual({ type: "api_key", key: "broker-api-key" });
} finally {
storage?.close();
await handle?.close();
brokerStorage.close();
brokerStore.close();
}
});
});
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added `getAuthBrokerSnapshotCachePath()` with `OMP_AUTH_BROKER_SNAPSHOT_CACHE` override support for isolating the encrypted broker snapshot cache.
## [15.9.1] - 2026-06-04
### Fixed