browserPermissions.ts ×19

Frontier kind: Code frontier

unlabeled · c_4dfe5355d22e

7 tests · 5485 LOC · 25 files · introduces 0 tests · 335 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
19 ranges335 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
625 ranges5485 lines · 25 files · Browse complete extent
All tests (intent)
7 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Introduced files, introduced tests, and structurally relevant concept specializationbrowserPermissions.ts ×1 · 2 introduced LOCbrowserPermissions.ts ×1browserPermissions.ts ×3 · 6 introduced LOCbrowserPermissions.ts ×3browserPermissions.ts ×2 · 5 introduced LOCbrowserPermissions.ts ×2browserPermissions.ts ×2 · 4 introduced LOCbrowserPermissions.ts ×2browserPermissions.ts ×1 · 2 introduced LOCbrowserPermissions.ts ×1browserPermissions.ts ×3 · 7 introduced LOCbrowserPermissions.ts ×3browserPermissions.ts ×1 · 2 introduced LOCbrowserPermissions.ts ×1browserPermissions.ts ×1 · 2 introduced LOCbrowserPermissions.ts ×1browserPermissions.ts ×5 · 29 introduced LOCbrowserPermissions.ts ×5browserPermissions.ts ×4 · 16 introduced LOCbrowserPermissions.ts ×4browserPermissions.ts ×1 · 5 introduced LOCbrowserPermissions.ts ×1browserPermissions.ts ×3 · 6 introduced LOCbrowserPermissions.ts ×3browserPermissions.ts ×4 · 16 introduced LOCbrowserPermissions.ts ×4browserPermissions.ts ×2 · 16 introduced LOCbrowserPermissions.ts ×2event.ts ×93 · 864 introduced LOCevent.ts ×93codicons.ts ×2 · 846 introduced LOCcodicons.ts ×2types.test|title=Types isString|occurrence=1 · 0 introduced LOCtypes.test|title=Types i…uri.test|title=URI URI#file, win-speciale|occurrence=1, charCode.test|title=CharCode has good values|occurrence=1, +2 · 0 introduced LOCuri.test|title=URI URI#f…utils.ts ×3 · 9 introduced LOCutils.ts ×3nls.ts ×1 · 1 introduced LOCnls.ts ×1linkedList.ts ×13 · 44 introduced LOClinkedList.ts ×13nls.ts ×6 · 13 introduced LOCnls.ts ×6lifecycle.ts ×2 · 4 introduced LOClifecycle.ts ×2lifecycle.ts ×1 · 2 introduced LOClifecycle.ts ×1types.ts ×1 · 2 introduced LOCtypes.ts ×1lifecycle.ts ×4 · 8 introduced LOClifecycle.ts ×4lifecycle.ts ×6 · 16 introduced LOClifecycle.ts ×6lifecycle.ts ×1 · 2 introduced LOClifecycle.ts ×1lifecycle.ts ×1 · 2 introduced LOClifecycle.ts ×1lifecycle.ts ×1 · 3 introduced LOClifecycle.ts ×1map.ts ×97 · 3334 introduced LOCmap.ts ×97src/vs/base/common/arrays.ts · 949 LOCcommon/arrays.tssrc/vs/base/common/arraysFind.ts · 226 LOCcommon/arraysFind.tssrc/vs/base/common/assert.ts · 91 LOCcommon/assert.tssrc/vs/base/common/charCode.ts · 450 LOCcommon/charCode.tssrc/vs/base/common/codicons.ts · 66 LOCcommon/codicons.tssrc/vs/base/common/codiconsLibrary.ts · 758 LOCcommon/codiconsLibrary.t…src/vs/base/common/codiconsUtil.ts · 28 LOCcommon/codiconsUtil.tssrc/vs/base/common/collections.ts · 176 LOCcommon/collections.tssrc/vs/base/common/errors.ts · 357 LOCcommon/errors.tssrc/vs/base/common/event.ts · 1964 LOCcommon/event.tssrc/vs/base/common/functional.ts · 32 LOCcommon/functional.tssrc/vs/base/common/iterator.ts · 194 LOCcommon/iterator.tssrc/vs/base/common/lifecycle.ts · 974 LOCcommon/lifecycle.tssrc/vs/base/common/linkedList.ts · 151 LOCcommon/linkedList.tssrc/vs/base/common/map.ts · 1016 LOCcommon/map.tssrc/vs/base/common/marshallingIds.ts · 33 LOCcommon/marshallingIds.tssrc/vs/base/common/path.ts · 1589 LOCcommon/path.tssrc/vs/base/common/platform.ts · 281 LOCcommon/platform.tssrc/vs/base/common/process.ts · 76 LOCcommon/process.tssrc/vs/base/common/stopwatch.ts · 41 LOCcommon/stopwatch.tssrc/vs/base/common/types.ts · 410 LOCcommon/types.tssrc/vs/base/common/uri.ts · 754 LOCcommon/uri.tssrc/vs/base/test/common/utils.ts · 107 LOCcommon/utils.tssrc/vs/nls.ts · 244 LOCvs/nls.tssrc/vs/platform/browserView/common/browserPermissions.ts · 483 LOCcommon/browserPermission…charCode.test|title=CharCode has good values|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/common/charCode.test|title=CharCode has good values|occurrence=1charCode.test|title=Char…path.test|title=Paths (Node Implementation) path|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/common/path.test|title=Paths (Node Implementation) path|occurrence=1path.test|title=Paths (N…types.test|title=Types isString|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/common/types.test|title=Types isString|occurrence=1types.test|title=Types i…uri.test|title=URI File paths containing apostrophes break URI parsing and cannot be opened #276075|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/common/uri.test|title=URI File paths containing apostrophes break URI parsing and cannot be opened #276075|occurrence=1uri.test|title=URI File …uri.test|title=URI URI#file, win-speciale|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/common/uri.test|title=URI URI#file, win-speciale|occurrence=1uri.test|title=URI URI#f…clear and origin normalization|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/browserView/test/common/browserPermissions.test|title=BrowserPermissionStore get/set/clear and origin normalization|occurrence=1clear and origin normali…browserPermissions.test|title=BrowserPermissionStore hydrate ignores unknown categories and bad input|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/browserView/test/common/browserPermissions.test|title=BrowserPermissionStore hydrate ignores unknown categories and bad input|occurrence=1browserPermissions.test|…browserPermissions.test|title=BrowserPermissionStore isAllowed falls back to per-category default state when unset|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/browserView/test/common/browserPermissions.test|title=BrowserPermissionStore isAllowed falls back to per-category default state when unset|occurrence=1browserPermissions.test|…browserPermissions.test|title=BrowserPermissionStore onDidChange fires only on real changes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/browserView/test/common/browserPermissions.test|title=BrowserPermissionStore onDidChange fires only on real changes|occurrence=1browserPermissions.test|…clear|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/browserView/test/common/browserPermissions.test|title=BrowserPermissionStore serialize/hydrate round-trips and clearOrigin/clear|occurrence=1clear|occurrence=1browserPermissions.test|title=electronPermissionToCategories maps known permissions and media disambiguation|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/browserView/test/common/browserPermissions.test|title=electronPermissionToCategories maps known permissions and media disambiguation|occurrence=1browserPermissions.test|…browserPermissions.test|title=toOriginKey normalizes urls and tolerates bad input|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/browserView/test/common/browserPermissions.test|title=toOriginKey normalizes urls and tolerates bad input|occurrence=1browserPermissions.test|…Focused concept · browserPermissions.ts ×19 · 335 introduced LOCbrowserPermissions.ts ×1…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 335 introduced LOC across 19 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/browserView/common/browserPermissions.ts 335 introduced LOC · 19 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- browserPermissions.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { Codicon } from '../../../base/common/codicons.js';
7 > import { Emitter, Event } from '../../../base/common/event.js';
8 > import { Disposable } from '../../../base/common/lifecycle.js';
9 > import { ThemeIcon } from '../../../base/common/themables.js';
10 > import { localize } from '../../../nls.js';
11 >
12 > /**
13 > * UI-agnostic, per-origin permission model for the integrated browser, modeled
14 > * on Chromium's user-friendly Site Settings categories rather than the raw
15 > * Electron permission strings. This module intentionally ships NO UI and no
16 > * Electron import so it can load in both the main process (authoritative store)
17 > * and the workbench renderer (read mirror hydrated from storage).
18 > *
19 > * The Electron permission string list is taken from Electron's own source
20 > * (`shell/common/gin_converters/content_converter.cc`), the single source of
21 > * truth for the strings passed to the permission handlers. Any
22 > * `blink::PermissionType` Electron does not explicitly name is reported as
23 > * `'unknown'`.
24 > */
25 >
26 > /**
27 > * A decision a user can record for a (origin, category) pair. These are the
28 > * only two values ever persisted; clearing a decision removes it entirely.
29 > * - 'allow' -> grant without prompting
30 > * - 'deny' -> reject without prompting
31 > */
32 > export type PermissionDecision = 'allow' | 'deny';
33 >
34 > /**
35 > * The effective state of a (origin, category) pair: either a recorded
36 > * {@link PermissionDecision}, or 'ask' when no decision has been recorded.
37 > * 'ask' is only ever a default/effective value -- it is never stored.
38 > */
39 > export type PermissionState = PermissionDecision | 'ask';
40 >
41 > /**
42 > * User-facing permission categories. These are deliberately coarser and more
43 > * meaningful than Electron's raw permission strings. For example Electron's
44 > * single `media` permission is split into `Camera` and `Microphone`, and the
45 > * various clipboard permissions collapse into `Clipboard`.
46 > */
47 > export const enum PermissionCategory {
48 > Location = 'location',
49 > Camera = 'camera',
50 > Microphone = 'microphone',
51 > Notifications = 'notifications',
52 > Sensors = 'sensors',
53 > Clipboard = 'clipboard',
54 > Devices = 'devices',
55 > }
56 >
57 > /**
58 > * The kinds of hardware-device chooser flows the {@link PermissionCategory.Devices}
59 > * category gates. Each maps to a distinct Electron device-selection event but is
60 > * surfaced to the user through one unified request/selection flow.
61 > */
62 > export type BrowserDeviceType = 'usb' | 'serial' | 'hid' | 'bluetooth';
63 >
64 > /**
65 > * A single hardware device offered to the user during a device-chooser flow.
66 > * Only plain, user-presentable data crosses the IPC boundary; the opaque
67 > * `deviceId` is echoed back verbatim to select the device.
68 > */
69 > export interface IBrowserDeviceCandidate {
70 > /** Opaque, device-type-specific identifier echoed back to select the device. */
71 > readonly deviceId: string;
72 > /** Primary, user-facing label (e.g. product name). */
73 > readonly label: string;
74 > /** Optional secondary detail (e.g. manufacturer or vendor:product ids). */
75 > readonly detail?: string;
76 > }
77 >
78 > /**
79 > * Static metadata describing a category and how it maps to Electron's raw
80 > * permission strings. A UI can iterate {@link PERMISSION_CATEGORY_DESCRIPTORS}
81 > * to render a settings list directly from this data.
82 > */
83 > export interface IPermissionCategoryDescriptor {
84 > readonly category: PermissionCategory;
85 > /** Short, human-readable label suitable for a settings row. */
86 > readonly label: string;
87 > /** One-line description of what granting this category enables. */
88 > readonly description: string;
89 > /** The icon to display for this category. */
90 > readonly icon: ThemeIcon;
91 > /** Electron permission strings that map to this category. */
92 > readonly permissions: string[];
93 > /** State assumed for this category when an origin has not recorded a decision. */
94 > readonly defaultState: PermissionState;
95 > }
96 >
97 > export const PERMISSION_CATEGORY_DESCRIPTORS: Readonly<Record<PermissionCategory, IPermissionCategoryDescriptor>> = {
98 > [PermissionCategory.Location]: {
99 > category: PermissionCategory.Location,
100 > label: localize('browserPermission.location.label', "Location"),
101 > description: localize('browserPermission.location.description', "Access this device's geographic location"),
102 > icon: Codicon.location,
103 > permissions: ['geolocation', 'geolocation-approximate'],
104 > defaultState: 'ask',
105 > },
106 > [PermissionCategory.Camera]: {
107 > category: PermissionCategory.Camera,
108 > label: localize('browserPermission.camera.label', "Camera"),
109 > description: localize('browserPermission.camera.description', "Capture video from cameras"),
110 > icon: Codicon.deviceCamera,
111 > // `media` is shared with Microphone; disambiguated via mediaType/mediaTypes.
112 > permissions: ['media'],
113 > defaultState: 'ask',
114 > },
115 > [PermissionCategory.Microphone]: {
116 > category: PermissionCategory.Microphone,
117 > label: localize('browserPermission.microphone.label', "Microphone"),
118 > description: localize('browserPermission.microphone.description', "Capture audio from microphones"),
119 > icon: Codicon.mic,
120 > permissions: ['media'],
121 > defaultState: 'ask',
122 > },
123 > [PermissionCategory.Sensors]: {
124 > category: PermissionCategory.Sensors,
125 > label: localize('browserPermission.sensors.label', "Sensors"),
126 > description: localize('browserPermission.sensors.description', "Read motion and environmental sensors"),
127 > icon: Codicon.pulse,
128 > permissions: ['sensors'],
129 > defaultState: 'allow',
130 > },
131 > [PermissionCategory.Clipboard]: {
132 > category: PermissionCategory.Clipboard,
133 > label: localize('browserPermission.clipboard.label', "Clipboard"),
134 > description: localize('browserPermission.clipboard.description', "Read from and write to the system clipboard"),
135 > icon: Codicon.clippy,
136 > permissions: ['clipboard-read'],
137 > defaultState: 'ask',
138 > },
139 > [PermissionCategory.Notifications]: {
140 > category: PermissionCategory.Notifications,
141 > label: localize('browserPermission.notifications.label', "Notifications"),
142 > description: localize('browserPermission.notifications.description', "Display desktop notifications"),
143 > icon: Codicon.bell,
144 > permissions: ['notifications'],
145 > defaultState: 'ask',
146 > },
147 > [PermissionCategory.Devices]: {
148 > category: PermissionCategory.Devices,
149 > label: localize('browserPermission.devices.label', "Devices"),
150 > description: localize('browserPermission.devices.description', "Request access to USB, serial, HID, and Bluetooth devices"),
151 > icon: Codicon.plug,
152 > // Each device kind has its own native chooser; this decision only gates
153 > // whether that chooser is allowed to surface. Bluetooth has no Electron
154 > // permission string (it is gated in the chooser handler directly).
155 > permissions: ['usb', 'serial', 'hid'],
156 > defaultState: 'allow',
157 > },
158 > /**
159 > * Permissions not listed here are either always allowed (see
160 > * {@link ALWAYS_ALLOWED_PERMISSIONS}) or, by default, always denied:
161 > *
162 > * No-op in Electron due to missing backend support
163 > * - Smart Cards (`smart-card`)
164 > * - NFC (`nfc`)
165 > * - Protected Content (`mediaKeySystem`)
166 > * - Augmented / Virtual Reality, Hand Tracking (`ar`, `vr`, `hand-tracking`)
167 > * - Payment Handlers (`payment-handler`)
168 > * - Background Sync (`background-sync`, `periodic-background-sync`, `background-fetch`)
169 > * - Printing (`web-printing`)
170 > * - App Installation (`web-app-installation`)
171 > * - Storage Access (`storage-access`, `top-level-storage-access`)
172 > *
173 > * Not currently implemented (in approximate order of 'might want')
174 > * - Local Network Access (`local-network-access`, `local-network`, `loopback-network`)
175 > * - Screen Capture, Captured Surface Control (`display-capture`, `captured-surface-control`)
176 > * - File Writing (`fileSystem`)
177 > * - Open External (`openExternal`)
178 > * - MIDI (`midi`, `midiSysex`)
179 > * - Persistent Storage (`persistent-storage`)
180 > * - Device Activity (`idle-detection`)
181 > * - Audio Output (`speaker-selection`)
182 > * - Wake Lock (`screen-wake-lock`, `system-wake-lock`)
183 > * - Window Management (`window-management`)
184 > * - Fonts (`local-fonts`)
185 > * - Automatic Fullscreen (`automatic-fullscreen`)
186 > */
187 > };
188 >
189 > /**
190 > * Raw Electron permission strings that are granted unconditionally, with no
191 > * recorded state and no management control. These are low-risk capabilities
192 > * that Chrome itself also always grants automatically.
193 > */
194 > export const ALWAYS_ALLOWED_PERMISSIONS: ReadonlySet<string> = new Set([
195 > 'pointerLock',
196 > 'keyboardLock',
197 > 'fullscreen',
198 > 'clipboard-sanitized-write',
199 > ]);
200 >
201 > /** Whether a raw Electron permission string is granted unconditionally. */
202 > export function isAlwaysAllowedPermission(permission: string): boolean {
203 return ALWAYS_ALLOWED_PERMISSIONS.has(permission);
204 }
206 > /** All categories, in a stable display order. */
207 > export const ALL_PERMISSION_CATEGORIES: readonly PermissionCategory[] = Object.keys(PERMISSION_CATEGORY_DESCRIPTORS) as PermissionCategory[];
208 >
209 > /** The default state for each permission category. */
210 > const DEFAULT_PERMISSION_STATES: Readonly<Record<PermissionCategory, PermissionState>> = Object.freeze(
211 > Object.fromEntries(ALL_PERMISSION_CATEGORIES.map(category => [category, PERMISSION_CATEGORY_DESCRIPTORS[category].defaultState])) as Record<PermissionCategory, PermissionState>
212 > );
213 >
214 > /**
215 > * Reverse lookup table built once from the descriptors:
216 > * electron permission string -> categories that own it.
217 > * `media` is intentionally omitted here because it requires `details` to
218 > * disambiguate; it is handled explicitly in {@link electronPermissionToCategories}.
219 > */
220 > const PERMISSION_TO_CATEGORIES: ReadonlyMap<string, PermissionCategory[]> = (() => {
221 > const map = new Map<string, PermissionCategory[]>();
222 > for (const category of ALL_PERMISSION_CATEGORIES) {
223 > for (const permission of PERMISSION_CATEGORY_DESCRIPTORS[category].permissions) {
224 > if (permission === 'media') {
225 > continue;
226 > }
227 > const existing = map.get(permission);
228 > if (existing) {
229 existing.push(category);
230 > } else { browserPermissions.ts
231 > map.set(permission, [category]);
232 > }
233 > }
234 > }
235 > return map;
236 > })();
237 >
238 > /**
239 > * Map a raw Electron permission string (from either handler, or a device type)
240 > * to the user-friendly category/categories it represents.
241 > *
242 > * Notes:
243 > * - `media` resolves to `Camera`, `Microphone`, or both depending on the
244 > * normalized `mediaKinds` hint extracted by the caller from Electron's
245 > * details. With no hint it conservatively resolves to both, so the caller
246 > * can require the strictest decision.
247 > * - `unknown` (and anything unrecognized) resolves to an empty array.
248 > */
249 > export function electronPermissionToCategories(permission: string, mediaKinds?: ReadonlyArray<'video' | 'audio'>): PermissionCategory[] {
250 if (permission === 'media') {
251 return resolveMediaCategories(mediaKinds);
253 return PERMISSION_TO_CATEGORIES.get(permission) ?? [];
254 }
256 function resolveMediaCategories(mediaKinds?: ReadonlyArray<'video' | 'audio'>): PermissionCategory[] {
257 const categories = new Set<PermissionCategory>();
265 return [...categories];
266 }
268 > /**
269 > * Normalize a full URL down to a stable permission key.
270 > *
271 > * For URLs with a real origin (http/https/etc.) this returns the origin
272 > * (scheme + host + port), e.g. "https://example.com:8443". Host-less URLs such
273 > * as `file:` have no meaningful origin, so they key off the scheme and full
274 > * path instead (query and fragment removed), e.g. "file:///home/user/page.html".
275 > * Falls back to the trimmed raw input if it cannot be parsed.
276 > */
277 > export function toOriginKey(url: string | undefined | null): string {
278 // Trim first so leading/trailing whitespace doesn't push otherwise valid
279 // URLs into the catch path. Electron also reports opaque/unique origins
298 }
299 }
301 > /** A single recorded grant: which origin, which category, what decision. */
302 > export interface IPermissionGrant {
303 > readonly origin: string;
304 > readonly category: PermissionCategory;
305 > readonly state: PermissionDecision;
306 > }
307 >
308 > /**
309 > * A (category, decision) pair for one (implied) origin; the write API payload.
310 > * A `null` decision clears any recorded choice, falling back to the default.
311 > */
312 > export interface IPermissionCategoryState {
313 > readonly category: PermissionCategory;
314 > readonly state: PermissionDecision | null;
315 > }
316 >
317 > /**
318 > * On-disk shape of the whole store: `{ origin: { category: decision } }`.
319 > */
320 > export interface ISerializedBrowserPermissionsSnapshot {
321 > readonly origins: Readonly<Record<string, Partial<Record<PermissionCategory, PermissionDecision>>>>;
322 > }
323 >
324 > const VALID_CATEGORIES = new Set<string>(ALL_PERMISSION_CATEGORIES);
325 >
326 > /**
327 > * In-memory, serializable tracker of permission state keyed by (origin,
328 > * category). The main process owns the authoritative instance; the workbench
329 > * keeps a read mirror hydrated from storage. Mutate with `set` / `setMany`,
330 > * observe with `onDidChange`, persist with `serialize` / `hydrate`.
331 > */
332 > export class BrowserPermissionStore extends Disposable {
333
334 private readonly _data = new Map<string, Map<PermissionCategory, PermissionDecision>>();
336 private readonly _onDidChange = this._register(new Emitter<void>());
337 readonly onDidChange: Event<void> = this._onDidChange.event;
339 > /**
340 > * The default state assumed for a category when an origin has recorded no decision.
341 > */
342 > defaultStateFor(category: PermissionCategory): PermissionState {
343 return PERMISSION_CATEGORY_DESCRIPTORS[category].defaultState;
344 }
346 > /** Get the recorded decision for a (origin, category) pair. */
347 > getDecision(origin: string, category: PermissionCategory): PermissionDecision | undefined {
348 return this._data.get(toOriginKey(origin))?.get(category);
349 }
351 > /**
352 > * Resolve the effective boolean decision for a (origin, category) pair,
353 > * applying {@link defaultStateFor} when the recorded state is 'ask'.
354 > */
355 > isAllowed(origin: string, category: PermissionCategory): boolean {
356 return (this.getDecision(origin, category) ?? this.defaultStateFor(category)) === 'allow';
357 }
359 > /** Set (or clear, via `null`) the decision for a (origin, category) pair. */
360 > set(origin: string, category: PermissionCategory, decision: PermissionDecision | null): void {
361 const key = toOriginKey(origin);
362 if (!key) {
386 this._onDidChange.fire();
387 }
389 > /** Set (or clear) the decision for several categories of one origin at once. */
390 > setMany(origin: string, grants: Iterable<IPermissionCategoryState>): void {
391 for (const { category, state } of grants) {
392 this.set(origin, category, state);
393 }
394 }
396 > /** Remove all recorded state for an origin. */
397 > clearOrigin(origin: string): void {
398 const key = toOriginKey(origin);
399 if (this._data.delete(key)) {
401 }
402 }
404 > /** Remove all recorded state for every origin. */
405 > clear(): void {
406 if (this._data.size === 0) {
407 return;
410 this._onDidChange.fire();
411 }
413 > /**
414 > * Return the full category->state map for one origin, including categories
415 > * with no recorded decision. Ideal for rendering a
416 > * per-site settings page.
417 > */
418 > getOrigin(origin: string): Record<PermissionCategory, PermissionState> {
419 const result: Record<PermissionCategory, PermissionState> = { ...DEFAULT_PERMISSION_STATES };
420 const recorded = this._data.get(toOriginKey(origin));
426 return result;
427 }
429 > /** All origins that have at least one recorded decision. */
430 > origins(): string[] {
431 return [...this._data.keys()];
432 }
434 > /** Flat list of every recorded grant. */
435 > list(): IPermissionGrant[] {
436 const grants: IPermissionGrant[] = [];
437 for (const [origin, categories] of this._data) {
442 return grants;
443 }
445 > serialize(): ISerializedBrowserPermissionsSnapshot {
446 const origins: Record<string, Partial<Record<PermissionCategory, PermissionDecision>>> = {};
447 for (const [origin, categories] of this._data) {
454 return { origins };
455 }
457 > hydrate(snapshot: ISerializedBrowserPermissionsSnapshot | undefined): void {
458 this._data.clear();
459 if (snapshot?.origins && typeof snapshot.origins === 'object') {