chrome-devtools-frontend
Version:
Chrome DevTools UI
143 lines (123 loc) • 5.29 kB
text/typescript
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../../core/common/common.js';
import * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
import * as SDK from '../../../core/sdk/sdk.js';
import {areOriginsEquivalent, extractContextOrigin, isOpaqueOrigin} from '../AiOrigins.js';
import {MAX_TARGET_ORIGINS, resolveDOMStorages} from './DOMStorageUtils.js';
import {
type BaseToolCapability,
type DataHandlerResult,
type DataTool,
type OriginLockCapability,
type ServerLoggingCapability,
ToolAnnotation,
type ToolArgs,
ToolName,
} from './Tool.js';
const lockedString = i18n.i18n.lockedString;
export interface ListStorageKeysArgs extends ToolArgs {
type: 'localStorage'|'sessionStorage';
origins: string[];
storageKey?: string;
}
export interface ListStorageKeysResult {
storageKeysByOrigin: Record<string, {
partitions: Array<{
storageKey: string,
keys: string[],
}>,
}>;
}
export class ListStorageKeysTool implements DataTool<ListStorageKeysArgs, ListStorageKeysResult,
BaseToolCapability&OriginLockCapability&ServerLoggingCapability> {
readonly name = ToolName.LIST_STORAGE_KEYS;
readonly description =
'Lists all keys for a given storage type for requested origins. Returns keys grouped by storage partition under their origin.';
readonly annotations = [ToolAnnotation.REDACT_FROM_HISTORY];
readonly parameters: Host.AidaClient.FunctionObjectParam<keyof ListStorageKeysArgs> = {
type: Host.AidaClient.ParametersTypes.OBJECT,
description: '',
nullable: false,
properties: {
type: {
type: Host.AidaClient.ParametersTypes.STRING,
description: 'Storage type: localStorage or sessionStorage',
nullable: false,
},
origins: {
type: Host.AidaClient.ParametersTypes.ARRAY,
description: 'List of origins to list keys for.',
items: {type: Host.AidaClient.ParametersTypes.STRING, description: 'An origin URL.'},
nullable: false,
},
storageKey: {
type: Host.AidaClient.ParametersTypes.STRING,
description: 'Optional. Specific storageKey to list keys for. Only applies if single origin is provided.',
nullable: true,
},
},
required: ['type', 'origins'],
};
displayInfoFromArgs(args: ListStorageKeysArgs): {
title: string,
action: string,
} {
return {
title: lockedString('Reading storage keys'),
action: `listStorageKeys('${args.type}', ${JSON.stringify(args.origins)})`,
};
}
async handler(
args: ListStorageKeysArgs,
context: BaseToolCapability&OriginLockCapability&ServerLoggingCapability,
): Promise<DataHandlerResult<ListStorageKeysResult>> {
context.setLoggingEnabled(false);
// eslint-disable-next-line @devtools/no-instance-of-migrated-singletons
const targetManager = SDK.TargetManager.TargetManager.instance();
const primaryPageTarget = targetManager.primaryPageTarget();
const allowedOrigin = context.getEstablishedOrigin();
if (!allowedOrigin || isOpaqueOrigin(allowedOrigin)) {
return {error: 'No origin available or not allowed.'};
}
if (!primaryPageTarget) {
return {error: 'No origin available or not allowed.'};
}
const pageOrigin = Common.ParsedURL.ParsedURL.extractOrigin(primaryPageTarget.inspectedURL());
if (!pageOrigin || !areOriginsEquivalent(pageOrigin, allowedOrigin)) {
return {error: 'No origin available or not allowed.'};
}
const rawList = (args.origins && args.origins.length > 0) ? args.origins : [allowedOrigin];
const validOrigins = rawList.map(origin => extractContextOrigin(origin))
.filter(origin => areOriginsEquivalent(origin, allowedOrigin));
const targetOrigins = Array.from(new Set(validOrigins)).slice(0, MAX_TARGET_ORIGINS);
if (targetOrigins.length === 0) {
return {error: 'No valid origins found.'};
}
const storageKey = (targetOrigins.length === 1 && args.storageKey) ? args.storageKey : undefined;
const storageKeysByOrigin: ListStorageKeysResult['storageKeysByOrigin'] = {};
await Promise.all(targetOrigins.map(async origin => {
const storages = resolveDOMStorages(origin, args.type, targetManager, primaryPageTarget, storageKey);
const keyAndItems = await Promise.all(storages.map(async storage => {
// Silently catch CDP errors (e.g. target navigation, detachment, or frame crash)
// so that transient target loss does not fail the entire batch.
const items = await storage.getItems().catch(() => null);
return {storageKey: storage.storageKey, items};
}));
const partitions = [];
for (const {storageKey: partKey, items} of keyAndItems) {
if (!items || !partKey) {
continue;
}
const keys = items.map(([key]) => key);
if (keys.length > 0) {
partitions.push({storageKey: partKey, keys});
}
}
storageKeysByOrigin[origin] = {partitions};
}));
return {result: {storageKeysByOrigin}};
}
}