fix(browser): preserve independently observed storage identities (#10029)

fix(browser): retain partial storage identity evidence

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-11-11 17:22:30 +08:00
1 parent dd928466cb
commit 6151e13835
3 files changed
+77 -16

No files matched your search

+58 -2
View File
@@ -16,14 +16,15 @@ const NO_CACHE_COMMAND = Symbol("unsupported");
const captureWithCacheResult = async (
cacheResult: unknown,
storageEntries: unknown = [],
): Promise<Awaited<ReturnType<typeof captureStorage>>["value"]> => {
const connection = {
send(method: string): Promise<unknown> {
send(method: string, params: Record<string, unknown>): Promise<unknown> {
switch (method) {
case "Storage.getUsageAndQuota":
return Promise.resolve({ usage: 1, quota: 10 });
case "DOMStorage.getDOMStorageItems":
return Promise.resolve({ entries: [] });
return Promise.resolve({ entries: storageEntries });
case "IndexedDB.requestDatabaseNames":
return Promise.resolve({ databaseNames: [] });
case "Network.getCookies":
@@ -40,6 +41,7 @@ const captureWithCacheResult = async (
)
: Promise.resolve(cacheResult);
case "CacheStorage.requestEntries":
expect(params.cacheId).toEqual(expect.any(String));
return Promise.resolve({ cacheDataEntries: [], returnCount: 0 });
default:
return Promise.reject(new Error(`Unexpected command: ${method}`));
@@ -362,3 +364,57 @@ it("keeps an opaque cookie partition unknown without dropping its value", async
});
expect(storageComparison(plain, known).status).toBe("changed");
});
it("retains known storage keys and cache names when content or locator metadata is malformed", async () => {
const storage = await captureWithCacheResult(
{ caches: [{ cacheName: "known-cache" }] },
[
["known-key", 42],
["valid-peer", "recorded"],
[42, "unknown identity"],
],
);
expect(storage.local_storage_keys).toEqual(["known-key", "valid-peer"]);
expect(storage.session_storage_keys).toEqual(["known-key", "valid-peer"]);
expect(storage.cache_names).toEqual(["known-cache"]);
expect(storage.fingerprints_complete).toBe(false);
const local = storage.content_fingerprints.filter(
({ scope }) => scope === "local_storage",
);
expect(local).toHaveLength(2);
expect(local).toContainEqual(
expect.objectContaining({ complete: false, value_sha256: null }),
);
expect(local).toContainEqual(
expect.objectContaining({
complete: true,
value_sha256: expect.any(String),
}),
);
});
it("retains cookie identity when its content is malformed without inventing a value fingerprint", async () => {
const known = await captureCookies([cookieBase]);
const unavailable = await captureCookies([
{ ...cookieBase, value: 42 },
{ ...cookieBase, name: 42 },
]);
const knownCookie = known.content_fingerprints.find(
({ scope }) => scope === "cookie",
);
if (knownCookie === undefined) throw new Error("Missing known cookie");
expect(unavailable.content_fingerprints).toEqual([
{
scope: "cookie",
identity_sha256: knownCookie.identity_sha256,
value_sha256: null,
complete: false,
},
]);
expect(unavailable.fingerprints_complete).toBe(false);
expect(storageComparison(known, unavailable)).toMatchObject({
status: "unknown",
total_changes: 0,
changes: [],
});
});
+10 -4
View File
@@ -83,7 +83,8 @@ export const captureStorage = async (
// Like IndexedDB names, a missing, non-array or partly unusable cache
// list is an incomplete enumeration rather than an empty one.
Array.isArray(cacheField) &&
caches.length === cacheField.length;
caches.length === cacheField.length &&
caches.every(({ id }) => id !== undefined);
const keysExclusion = !context.input.include_storage_keys
? undefined
: local.exclusion === "provider_unavailable" ||
@@ -127,7 +128,7 @@ const capturedCaches = (
values.flatMap((cache) => {
const name = cdpStringValue(cache.cacheName);
const id = cdpStringValue(cache.cacheId);
return name === undefined || id === undefined ? [] : [{ name, id }];
return name === undefined ? [] : [{ name, id }];
});
const storageItems = async (
@@ -154,9 +155,14 @@ const storageItems = async (
if (!Array.isArray(entry)) return [];
const key = cdpStringValue(entry[0]);
const value = cdpStringValue(entry[1]);
return key === undefined || value === undefined ? [] : [{ key, value }];
return key === undefined ? [] : [{ key, value }];
});
return { items, complete: items.length === entries.length };
return {
items,
complete:
items.length === entries.length &&
items.every(({ value }) => value !== undefined),
};
};
const stringArray = (value: unknown): readonly string[] =>
+9 -10
View File
@@ -20,14 +20,14 @@ type StorageFingerprint =
export interface CapturedStorageItems {
readonly items: readonly {
readonly key: string;
readonly value: string;
readonly value: string | undefined;
}[];
readonly complete: boolean;
}
export interface CapturedCache {
readonly name: string;
readonly id: string;
readonly id: string | undefined;
}
interface StorageFingerprintContext {
@@ -106,7 +106,7 @@ const addStorageItems = (
scope,
identity: item.key,
value: item.value,
complete: true,
complete: item.value !== undefined,
});
};
@@ -139,12 +139,7 @@ const addCookies = async (
const value = cdpStringValue(cookie.value);
const domain = cdpStringValue(cookie.domain);
const path = cdpStringValue(cookie.path);
if (
name === undefined ||
value === undefined ||
domain === undefined ||
path === undefined
) {
if (name === undefined || domain === undefined || path === undefined) {
run.state.complete = false;
continue;
}
@@ -163,7 +158,7 @@ const addCookies = async (
...(partitionOpaque ? { partition_opaque: true } : {}),
},
value,
complete: !partitionOpaque,
complete: value !== undefined && !partitionOpaque,
});
}
};
@@ -357,6 +352,10 @@ const addCaches = async (
caches: readonly CapturedCache[],
): Promise<void> => {
for (const cache of caches) {
if (cache.id === undefined) {
run.state.complete = false;
continue;
}
let skipCount = 0;
let returnCount: number | undefined;
while (returnCount === undefined || skipCount < returnCount) {