mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-01 12:22:24 +03:00
Compare commits
1 Commits
fix/11433-
...
fix/11500-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b32817c80d |
@@ -1 +0,0 @@
|
||||
- **fix(combos):** the combo builder's precision-select, global-model-search, and manual-entry flows now serialize a model step's `model` string using the provider's already-computed routing-alias prefix (e.g. `oc/`) instead of rebuilding it from the raw canonical `providerId`, fixing the no-auth "OpenCode Free" provider (`opencode`) being routed to the unrelated paid "OpenCode Zen" provider (`opencode-zen`) because `opencode` doubles as a manual routing-prefix override ([#11433](https://github.com/diegosouzapw/OmniRoute/issues/11433)).
|
||||
1
changelog.d/fixes/11500-decrypt-log-dedup.md
Normal file
1
changelog.d/fixes/11500-decrypt-log-dedup.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(db):** dedupe the raw `[Encryption] Decryption failed...` log line emitted by the lazy-decrypt views (`createLazyRowProxy`/`createLazyConnectionView`), which power `getProviderConnections()` and were re-triggering that line on every CredentialHealth/model-sync cycle for the same corrupt or stale-key credential — a fresh Proxy over a fresh row on every cycle meant the per-proxy memoization never suppressed it, unlike the dedup `decryptConnectionFields()` already had since [#9927](https://github.com/diegosouzapw/OmniRoute/issues/9927). Now shares that dedupe tracking so the line logs at most once per credential ([#11500](https://github.com/diegosouzapw/OmniRoute/issues/11500)).
|
||||
@@ -2177,9 +2177,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
builderConnectionId !== COMBO_BUILDER_AUTO_CONNECTION ? builderConnectionId : null,
|
||||
connectionLabel: selectedBuilderConnection?.label || null,
|
||||
allowedConnectionIds: builderEffectiveAllowedConnectionIds,
|
||||
// #11433: use the already-corrected routing prefix (e.g. "oc" for
|
||||
// OpenCode Free) instead of letting it default to the raw providerId.
|
||||
modelPrefix: parseQualifiedModel(selectedBuilderModel.qualifiedModel)?.providerId,
|
||||
})
|
||||
: null;
|
||||
const builderHasDuplicate =
|
||||
@@ -2504,9 +2501,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
builderConnectionId !== COMBO_BUILDER_AUTO_CONNECTION ? builderConnectionId : null,
|
||||
connectionLabel: selectedBuilderConnection?.label || null,
|
||||
allowedConnectionIds: builderEffectiveAllowedConnectionIds,
|
||||
// #11433: use the already-corrected routing prefix (e.g. "oc" for
|
||||
// OpenCode Free) instead of letting it default to the raw providerId.
|
||||
modelPrefix: parseQualifiedModel(selectedBuilderModel.qualifiedModel)?.providerId,
|
||||
});
|
||||
|
||||
if (hasExactModelStepDuplicate(models, nextStep)) {
|
||||
|
||||
@@ -83,7 +83,6 @@ export function buildPrecisionComboModelStep({
|
||||
connectionLabel,
|
||||
allowedConnectionIds = null,
|
||||
weight = 0,
|
||||
modelPrefix,
|
||||
}: {
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
@@ -92,22 +91,9 @@ export function buildPrecisionComboModelStep({
|
||||
/** #3266: account allowlist scoping round-robin to a subset of connections. */
|
||||
allowedConnectionIds?: string[] | null;
|
||||
weight?: number;
|
||||
/**
|
||||
* #11433: the routing-prefix segment to serialize into `model` (e.g. "oc"
|
||||
* for the no-auth OpenCode Free provider), when it differs from the
|
||||
* canonical `providerId`. Some canonical provider ids collide with an
|
||||
* unrelated manual `ALIAS_TO_PROVIDER_ID` routing override (`opencode` →
|
||||
* `opencode-zen`), so reconstructing `model` from the raw `providerId`
|
||||
* alone can round-trip to the wrong provider on request routing. Falls
|
||||
* back to `providerId` when omitted/blank. `step.providerId` always stays
|
||||
* the canonical id regardless, so routing/duplicate-detection identity is
|
||||
* unaffected.
|
||||
*/
|
||||
modelPrefix?: string | null;
|
||||
}): ComboModelStep {
|
||||
const normalizedProviderId = toTrimmedString(providerId) || "provider";
|
||||
const normalizedModelId = toTrimmedString(modelId) || "model";
|
||||
const normalizedModelPrefix = toTrimmedString(modelPrefix) || normalizedProviderId;
|
||||
const normalizedConnectionId = toTrimmedString(connectionId);
|
||||
const normalizedConnectionLabel = toTrimmedString(connectionLabel);
|
||||
// A pinned single connection wins over an allowlist, so only carry the allowlist
|
||||
@@ -124,7 +110,7 @@ export function buildPrecisionComboModelStep({
|
||||
return {
|
||||
kind: "model",
|
||||
providerId: normalizedProviderId,
|
||||
model: `${normalizedModelPrefix}/${normalizedModelId}`,
|
||||
model: `${normalizedProviderId}/${normalizedModelId}`,
|
||||
...(normalizedConnectionId ? { connectionId: normalizedConnectionId } : {}),
|
||||
...(normalizedConnectionLabel ? { label: normalizedConnectionLabel } : {}),
|
||||
...(normalizedAllowed.length > 0 ? { allowedConnectionIds: normalizedAllowed } : {}),
|
||||
@@ -174,15 +160,10 @@ export function buildManualComboModelStep({
|
||||
const providerId = resolveComboBuilderProviderId(parsed.providerId, providers);
|
||||
if (!providerId) return null;
|
||||
|
||||
// #11433: preserve the user-typed prefix (e.g. "oc") as the routing prefix
|
||||
// instead of letting buildPrecisionComboModelStep rebuild `model` from the
|
||||
// resolved canonical providerId, which can collide with an unrelated
|
||||
// manual alias override (e.g. "opencode" -> "opencode-zen").
|
||||
return buildPrecisionComboModelStep({
|
||||
providerId,
|
||||
modelId: parsed.modelId,
|
||||
weight,
|
||||
modelPrefix: parsed.providerId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -244,7 +225,7 @@ type ComboBuilderGlobalProvider = {
|
||||
displayName?: unknown;
|
||||
connectionCount?: unknown;
|
||||
connections?: unknown[];
|
||||
models?: Array<{ id?: unknown; name?: unknown; qualifiedModel?: unknown }>;
|
||||
models?: Array<{ id?: unknown; name?: unknown }>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -271,18 +252,12 @@ export function buildGlobalModelList(
|
||||
const modelId = toTrimmedString(model?.id);
|
||||
if (!modelId) return;
|
||||
const modelName = toTrimmedString(model?.name) || modelId;
|
||||
// #11433: derive the routing prefix from the model's already-corrected
|
||||
// `qualifiedModel` (e.g. "oc/<model>" for the OpenCode Free provider)
|
||||
// instead of defaulting to the raw providerId, which can collide with
|
||||
// an unrelated manual alias override.
|
||||
const modelPrefix = parseQualifiedModel(model?.qualifiedModel)?.providerId || providerId;
|
||||
const step = buildPrecisionComboModelStep({
|
||||
providerId,
|
||||
modelId,
|
||||
connectionId: null,
|
||||
connectionLabel: null,
|
||||
allowedConnectionIds: [],
|
||||
modelPrefix,
|
||||
});
|
||||
list.push({
|
||||
providerId,
|
||||
|
||||
@@ -288,6 +288,36 @@ export function decrypt(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #11500 — decrypt() wrapper for callers outside decryptConnectionFields()
|
||||
* (the lazy-decrypt views in providers/lazyConnectionView.ts, which call
|
||||
* decrypt() directly on every fresh getProviderConnections() cycle). A
|
||||
* fresh Proxy wraps a fresh row object each cycle, so per-proxy memoization
|
||||
* never survives across cycles — without this wrapper the raw
|
||||
* "[Encryption] Decryption failed..." line re-fires every single cycle for
|
||||
* the same corrupt/stale-key credential. Shares the loggedDecryptFailures
|
||||
* Set with decryptConnectionFields() so a credential already flagged via one
|
||||
* path does not re-log via the other, and logs the SAME raw message
|
||||
* decrypt() would emit (unlike decryptConnectionFields()'s enriched
|
||||
* message) — just deduped to once per (provider + connection + field +
|
||||
* ciphertext) instead of once per cycle.
|
||||
*/
|
||||
export function decryptQuiet(
|
||||
ciphertext: string | null | undefined,
|
||||
meta: { connectionId: string; provider: string; field: string }
|
||||
): string | null | undefined {
|
||||
if (!looksEncrypted(ciphertext)) {
|
||||
return decrypt(ciphertext);
|
||||
}
|
||||
const signature = `${meta.provider}::${meta.connectionId}::${meta.field}:${ciphertext}`;
|
||||
const alreadyLogged = loggedDecryptFailures.has(signature);
|
||||
const result = decrypt(ciphertext, { quiet: alreadyLogged });
|
||||
if (result === null && !alreadyLogged) {
|
||||
loggedDecryptFailures.add(signature);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt sensitive fields in a connection object (mutates in-place).
|
||||
* After decryption that required legacy key, re-encrypt with static key
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* admin, and catalog callers during Phase 2/3 of the lazy-decrypt rollout.
|
||||
*/
|
||||
|
||||
import { decrypt } from "../encryption";
|
||||
import { decryptQuiet } from "../encryption";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -119,10 +119,18 @@ export function createLazyConnectionView(row: Record<string, unknown>): Provider
|
||||
|
||||
const ensureDecrypted = () => {
|
||||
if (!decrypted) {
|
||||
const connectionId = base.id;
|
||||
const provider = base.provider;
|
||||
decrypted = {
|
||||
apiKey: toStringOrNull(decrypt(base.apiKey)),
|
||||
accessToken: toStringOrNull(decrypt(base.accessToken)),
|
||||
refreshToken: toStringOrNull(decrypt(base.refreshToken)),
|
||||
apiKey: toStringOrNull(
|
||||
decryptQuiet(base.apiKey, { connectionId, provider, field: "apiKey" })
|
||||
),
|
||||
accessToken: toStringOrNull(
|
||||
decryptQuiet(base.accessToken, { connectionId, provider, field: "accessToken" })
|
||||
),
|
||||
refreshToken: toStringOrNull(
|
||||
decryptQuiet(base.refreshToken, { connectionId, provider, field: "refreshToken" })
|
||||
),
|
||||
};
|
||||
}
|
||||
return decrypted;
|
||||
@@ -154,11 +162,13 @@ export function createLazyRowProxy(row: Record<string, unknown>): Record<string,
|
||||
|
||||
const ensureDecrypted = () => {
|
||||
if (!decrypted) {
|
||||
const connectionId = typeof row.id === "string" ? row.id : "";
|
||||
const provider = typeof row.provider === "string" ? row.provider : "unknown";
|
||||
decrypted = {
|
||||
apiKey: lazyDecrypt(row.apiKey),
|
||||
accessToken: lazyDecrypt(row.accessToken),
|
||||
refreshToken: lazyDecrypt(row.refreshToken),
|
||||
idToken: lazyDecrypt(row.idToken),
|
||||
apiKey: lazyDecrypt(row.apiKey, { connectionId, provider, field: "apiKey" }),
|
||||
accessToken: lazyDecrypt(row.accessToken, { connectionId, provider, field: "accessToken" }),
|
||||
refreshToken: lazyDecrypt(row.refreshToken, { connectionId, provider, field: "refreshToken" }),
|
||||
idToken: lazyDecrypt(row.idToken, { connectionId, provider, field: "idToken" }),
|
||||
};
|
||||
}
|
||||
return decrypted;
|
||||
@@ -189,7 +199,10 @@ export function createLazyRowProxy(row: Record<string, unknown>): Record<string,
|
||||
});
|
||||
}
|
||||
|
||||
function lazyDecrypt(value: unknown): string | null | undefined {
|
||||
function lazyDecrypt(
|
||||
value: unknown,
|
||||
meta: { connectionId: string; provider: string; field: string }
|
||||
): string | null | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
return decrypt(value);
|
||||
return decryptQuiet(value, meta);
|
||||
}
|
||||
|
||||
@@ -42,12 +42,6 @@ test("buildPrecisionComboModelStep preserves provider/model/account triple", ()
|
||||
});
|
||||
|
||||
test("buildManualComboModelStep resolves provider aliases and uses dynamic account", () => {
|
||||
// #11433: `providerId` resolves to the canonical id ("codex") for
|
||||
// duplicate-detection/routing identity, but the serialized `model` string
|
||||
// now preserves the user-typed prefix ("cx/") verbatim instead of
|
||||
// collapsing back to the canonical id — some canonical ids (e.g.
|
||||
// "opencode") collide with an unrelated manual routing-alias override, so
|
||||
// rebuilding `model` from the canonical id alone can silently misroute.
|
||||
assert.deepEqual(
|
||||
builderDraft.buildManualComboModelStep({
|
||||
value: "cx/gpt-5.5",
|
||||
@@ -56,7 +50,7 @@ test("buildManualComboModelStep resolves provider aliases and uses dynamic accou
|
||||
{
|
||||
kind: "model",
|
||||
providerId: "codex",
|
||||
model: "cx/gpt-5.5",
|
||||
model: "codex/gpt-5.5",
|
||||
weight: 0,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildPrecisionComboModelStep,
|
||||
buildGlobalModelList,
|
||||
buildManualComboModelStep,
|
||||
} from "../../src/lib/combos/builderDraft.ts";
|
||||
import { resolveProviderAlias, parseModel } from "../../open-sse/services/model.ts";
|
||||
|
||||
// Issue #11433: the combo builder's precision-select path builds a step's
|
||||
// `model` string as `${providerId}/${modelId}` using the CANONICAL provider id.
|
||||
// For the no-auth "opencode" (OpenCode Free) provider this produces
|
||||
// `model: "opencode/<modelId>"`, but `opencode` is ALSO a manual routing-prefix
|
||||
// override (`ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen"`) intended only
|
||||
// for user-typed `opencode/` prefixes referring to the OpenCode Zen (api-key)
|
||||
// tier. Parsing the step's own `model` string therefore resolves to a
|
||||
// DIFFERENT provider than the one recorded in `step.providerId`.
|
||||
|
||||
test('sanity: resolveProviderAlias("opencode") is the manual override causing the collision', () => {
|
||||
// Documents the root cause directly: the manual alias override in
|
||||
// open-sse/services/model.ts unconditionally rewrites "opencode" to
|
||||
// "opencode-zen", even though "opencode" is also a registered canonical
|
||||
// provider id (src/shared/constants/providers/noauth.ts).
|
||||
assert.equal(resolveProviderAlias("opencode"), "opencode-zen");
|
||||
});
|
||||
|
||||
test("issue #11433 fix: buildPrecisionComboModelStep honors an explicit modelPrefix override", () => {
|
||||
// The combo builder call sites now thread through the already-computed
|
||||
// routing-alias prefix (e.g. "oc") instead of letting the step default to
|
||||
// the raw providerId, so the serialized `model` field round-trips to the
|
||||
// correct provider.
|
||||
const step = buildPrecisionComboModelStep({
|
||||
providerId: "opencode",
|
||||
modelId: "big-pickle",
|
||||
modelPrefix: "oc",
|
||||
});
|
||||
|
||||
assert.equal(step.providerId, "opencode");
|
||||
assert.equal(step.model, "oc/big-pickle");
|
||||
|
||||
const parsed = parseModel(step.model);
|
||||
assert.equal(parsed.provider, step.providerId);
|
||||
});
|
||||
|
||||
test("issue #11433 fix: buildGlobalModelList derives modelPrefix from qualifiedModel for the no-auth OpenCode Free provider", () => {
|
||||
// Mirrors what src/lib/combos/builderOptions.ts::rewriteQualifiedModelPrefix
|
||||
// produces for the no-auth "opencode" provider entry: `qualifiedModel` is
|
||||
// already rewritten to the "oc/" alias prefix, but (pre-fix)
|
||||
// buildGlobalModelList ignored it and rebuilt `model` from the raw
|
||||
// providerId, producing "opencode/big-pickle" which parses back to the
|
||||
// wrong provider ("opencode-zen").
|
||||
const [entry] = buildGlobalModelList([
|
||||
{
|
||||
providerId: "opencode",
|
||||
displayName: "OpenCode Free",
|
||||
connectionCount: 0,
|
||||
connections: [],
|
||||
models: [{ id: "big-pickle", name: "Big Pickle", qualifiedModel: "oc/big-pickle" }],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(entry.step.providerId, "opencode");
|
||||
assert.equal(entry.step.model, "oc/big-pickle");
|
||||
assert.equal(parseModel(entry.step.model).provider, entry.step.providerId);
|
||||
});
|
||||
|
||||
test("issue #11433 fix: buildManualComboModelStep preserves a user-typed oc/<model> prefix", () => {
|
||||
// buildManualComboModelStep resolves the typed alias ("oc") back to the
|
||||
// canonical providerId ("opencode") before building the step. Pre-fix, it
|
||||
// then handed that canonical id straight to buildPrecisionComboModelStep,
|
||||
// which rebuilt `model` from it and collapsed "oc/<model>" back down to
|
||||
// "opencode/<model>" — reproducing the same collision for manual entry.
|
||||
const step = buildManualComboModelStep({
|
||||
value: "oc/big-pickle",
|
||||
providers: [{ providerId: "opencode", alias: "oc" }],
|
||||
});
|
||||
|
||||
assert.ok(step);
|
||||
assert.equal(step?.providerId, "opencode");
|
||||
assert.equal(step?.model, "oc/big-pickle");
|
||||
assert.equal(parseModel(step!.model).provider, step!.providerId);
|
||||
});
|
||||
140
tests/unit/lazy-connection-view-decrypt-dedupe.test.ts
Normal file
140
tests/unit/lazy-connection-view-decrypt-dedupe.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createCipheriv, randomBytes, scryptSync } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
// #11500 — the #9927 fix deduped raw decrypt-failure logging only inside
|
||||
// decryptConnectionFields(). The lazy-decrypt rollout (createLazyRowProxy /
|
||||
// createLazyConnectionView in src/lib/db/providers/lazyConnectionView.ts,
|
||||
// used by getProviderConnections() on every CredentialHealth/model-sync
|
||||
// cycle) called decrypt() directly with no quiet option and no dedup
|
||||
// tracking, so the raw "[Encryption] Decryption failed..." line re-fired on
|
||||
// every cycle for the same corrupt/stale-key credential.
|
||||
|
||||
const ORIGINAL_STORAGE_KEY = process.env.STORAGE_ENCRYPTION_KEY;
|
||||
|
||||
async function importFresh(modulePath: string) {
|
||||
const url = pathToFileURL(path.resolve(modulePath)).href;
|
||||
return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
||||
}
|
||||
|
||||
function encryptWithStaticSalt(secret: string, salt: string, plaintext: string): string {
|
||||
const key = scryptSync(secret, salt, 32);
|
||||
const iv = randomBytes(16);
|
||||
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
||||
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
const authTag = cipher.getAuthTag().toString("hex");
|
||||
return `enc:v1:${iv.toString("hex")}:${encrypted}:${authTag}`;
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
if (ORIGINAL_STORAGE_KEY === undefined) {
|
||||
delete process.env.STORAGE_ENCRYPTION_KEY;
|
||||
} else {
|
||||
process.env.STORAGE_ENCRYPTION_KEY = ORIGINAL_STORAGE_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
function captureConsoleError(fn: () => void): string[] {
|
||||
const original = console.error;
|
||||
const logs: string[] = [];
|
||||
console.error = (...args: unknown[]) => {
|
||||
logs.push(args.map(String).join(" "));
|
||||
};
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
console.error = original;
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
|
||||
test("#11500 — createLazyRowProxy dedupes decrypt-failure logging across sync cycles", async () => {
|
||||
process.env.STORAGE_ENCRYPTION_KEY = "probe-11500-current-key";
|
||||
|
||||
const { createLazyRowProxy } = await importFresh("src/lib/db/providers/lazyConnectionView.ts");
|
||||
|
||||
// Credential encrypted with a DIFFERENT key than the one currently
|
||||
// configured (stale STORAGE_ENCRYPTION_KEY / corrupted row) — produces
|
||||
// exactly "Auth tag validation likely failed."
|
||||
const staleCiphertext = encryptWithStaticSalt(
|
||||
"some-other-key-that-was-rotated-away",
|
||||
"omniroute-field-encryption-v1",
|
||||
"sk-super-secret-api-key"
|
||||
);
|
||||
|
||||
const rawRow = {
|
||||
id: "conn-zai-1",
|
||||
provider: "zai",
|
||||
apiKey: staleCiphertext,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
idToken: null,
|
||||
};
|
||||
|
||||
const capturedLines = captureConsoleError(() => {
|
||||
// Simulate 3 separate CredentialHealth / model-sync cycles, each of
|
||||
// which calls getProviderConnections() fresh and gets a brand-new
|
||||
// createLazyRowProxy() over a brand-new row object for the SAME
|
||||
// underlying corrupt DB row.
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
const view = createLazyRowProxy({ ...rawRow });
|
||||
void view.apiKey;
|
||||
}
|
||||
});
|
||||
|
||||
const rawDecryptFailureLines = capturedLines.filter((line) =>
|
||||
line.includes("[Encryption] Decryption failed. Ciphertext prefix:")
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
rawDecryptFailureLines.length,
|
||||
1,
|
||||
`expected the raw decrypt-failure line to be logged at most once across 3 sync cycles for the ` +
|
||||
`same corrupt credential, but it was logged ${rawDecryptFailureLines.length} times: ` +
|
||||
JSON.stringify(rawDecryptFailureLines, null, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test("#11500 — createLazyConnectionView dedupes decrypt-failure logging across sync cycles", async () => {
|
||||
process.env.STORAGE_ENCRYPTION_KEY = "probe-11500-current-key-view";
|
||||
|
||||
const { createLazyConnectionView } = await importFresh(
|
||||
"src/lib/db/providers/lazyConnectionView.ts"
|
||||
);
|
||||
|
||||
const staleCiphertext = encryptWithStaticSalt(
|
||||
"some-other-key-that-was-rotated-away-view",
|
||||
"omniroute-field-encryption-v1",
|
||||
"sk-super-secret-api-key-view"
|
||||
);
|
||||
|
||||
const rawRow = {
|
||||
id: "conn-glm-1",
|
||||
provider: "glm",
|
||||
apiKey: staleCiphertext,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
};
|
||||
|
||||
const capturedLines = captureConsoleError(() => {
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
const view = createLazyConnectionView({ ...rawRow });
|
||||
void view.apiKey;
|
||||
}
|
||||
});
|
||||
|
||||
const rawDecryptFailureLines = capturedLines.filter((line) =>
|
||||
line.includes("[Encryption] Decryption failed. Ciphertext prefix:")
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
rawDecryptFailureLines.length,
|
||||
1,
|
||||
`expected the raw decrypt-failure line to be logged at most once across 3 sync cycles for the ` +
|
||||
`same corrupt credential, but it was logged ${rawDecryptFailureLines.length} times: ` +
|
||||
JSON.stringify(rawDecryptFailureLines, null, 2)
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user