browser-debugger-cli 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/bdg/SKILL.md +101 -187
- package/README.md +4 -4
- package/dist/commands/cdp.js +1 -0
- package/dist/commands/cleanup.js +3 -0
- package/dist/commands/console.js +5 -1
- package/dist/commands/dom/a11y.d.ts +1 -1
- package/dist/commands/dom/a11y.js +20 -20
- package/dist/commands/dom/eval.d.ts +3 -1
- package/dist/commands/dom/eval.js +8 -5
- package/dist/commands/dom/formInteraction.js +1 -1
- package/dist/commands/dom/get.js +25 -7
- package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
- package/dist/commands/dom/helpers/evalResult.js +59 -0
- package/dist/commands/dom/index.js +7 -2
- package/dist/commands/dom/query.d.ts +2 -1
- package/dist/commands/dom/query.js +5 -3
- package/dist/commands/dom/screenshot.js +1 -0
- package/dist/commands/helpJson.d.ts +1 -1
- package/dist/commands/helpJson.js +4 -4
- package/dist/commands/helpTopic.js +10 -4
- package/dist/commands/network/har.js +18 -14
- package/dist/commands/network/list.js +46 -3
- package/dist/commands/optionBehaviors.d.ts +25 -2
- package/dist/commands/optionBehaviors.js +60 -42
- package/dist/commands/peek.js +3 -0
- package/dist/commands/shared/CommandRunner.js +13 -13
- package/dist/commands/shared/daemonErrorHandler.js +2 -2
- package/dist/commands/shared/dataFetcher.d.ts +4 -2
- package/dist/commands/shared/dataFetcher.js +11 -3
- package/dist/commands/shared/handleValidationError.js +3 -3
- package/dist/commands/shared/optionTypes.d.ts +15 -3
- package/dist/commands/shared/outputFile.d.ts +2 -1
- package/dist/commands/shared/outputFile.js +7 -4
- package/dist/commands/shared/startHelpers.js +3 -3
- package/dist/commands/status.js +3 -1
- package/dist/commands/stop.js +2 -1
- package/dist/connection/chromeIdentity.d.ts +8 -2
- package/dist/connection/chromeIdentity.js +85 -13
- package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
- package/dist/connection/launcher/flagsBuilder.js +107 -23
- package/dist/connection/launcher.d.ts +1 -1
- package/dist/connection/launcher.js +1 -2
- package/dist/constants.d.ts +31 -5
- package/dist/constants.js +37 -5
- package/dist/daemon/SessionController.js +2 -0
- package/dist/daemon/launcher.d.ts +17 -3
- package/dist/daemon/launcher.js +37 -7
- package/dist/daemon/session/Session.d.ts +2 -1
- package/dist/daemon/session/Session.js +10 -2
- package/dist/daemon/session/TelemetryStore.d.ts +7 -0
- package/dist/daemon/session/TelemetryStore.js +6 -0
- package/dist/daemon/session/commandRegistry.js +25 -7
- package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
- package/dist/daemon/session/matchedStylesReset.js +46 -0
- package/dist/daemon/session/plugins.js +1 -0
- package/dist/daemon/session/triggeredRequests.d.ts +0 -5
- package/dist/daemon/session/triggeredRequests.js +13 -7
- package/dist/daemon.js +8742 -8315
- package/dist/errors/messages.d.ts +31 -0
- package/dist/errors/messages.js +96 -6
- package/dist/index.js +1129 -548
- package/dist/ipc/client.d.ts +6 -1
- package/dist/ipc/client.js +11 -2
- package/dist/ipc/protocol/commands.d.ts +8 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
- package/dist/ipc/session/types.d.ts +5 -1
- package/dist/ipc/transport/index.d.ts +6 -0
- package/dist/ipc/transport/index.js +16 -1
- package/dist/program.d.ts +14 -0
- package/dist/program.js +53 -0
- package/dist/runtime/dom/elementGeometry.d.ts +23 -0
- package/dist/runtime/dom/elementGeometry.js +17 -15
- package/dist/runtime/dom/elementInfo.d.ts +13 -4
- package/dist/runtime/dom/elementInfo.js +15 -5
- package/dist/runtime/dom/evalHelpers.d.ts +24 -4
- package/dist/runtime/dom/evalHelpers.js +40 -12
- package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
- package/dist/runtime/dom/frameScopedConnection.js +2 -2
- package/dist/runtime/dom/frames.d.ts +2 -1
- package/dist/runtime/dom/frames.js +3 -1
- package/dist/runtime/dom/inspect.d.ts +17 -3
- package/dist/runtime/dom/inspect.js +40 -26
- package/dist/runtime/dom/inspectModel.d.ts +3 -3
- package/dist/runtime/dom/inspectRules.d.ts +29 -3
- package/dist/runtime/dom/inspectRules.js +205 -11
- package/dist/runtime/dom/layout.d.ts +0 -2
- package/dist/runtime/dom/layout.js +1 -2
- package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
- package/dist/runtime/dom/reactEventHelpers.js +9 -2
- package/dist/runtime/dom/targetNode.d.ts +10 -6
- package/dist/runtime/dom/targetNode.js +15 -8
- package/dist/runtime/page/emulation.js +6 -5
- package/dist/runtime/page/userAgent.d.ts +86 -2
- package/dist/runtime/page/userAgent.js +154 -33
- package/dist/session/paths.d.ts +38 -3
- package/dist/session/paths.js +154 -7
- package/dist/session/portClaims.d.ts +0 -8
- package/dist/session/portClaims.js +1 -22
- package/dist/session/sessionList.d.ts +5 -1
- package/dist/session/sessionList.js +5 -1
- package/dist/telemetry/a11y.d.ts +15 -1
- package/dist/telemetry/a11y.js +83 -0
- package/dist/telemetry/har/builder.d.ts +12 -1
- package/dist/telemetry/har/builder.js +11 -3
- package/dist/telemetry/har/sanitize.d.ts +24 -0
- package/dist/telemetry/har/sanitize.js +138 -0
- package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
- package/dist/telemetry/har/sanitizeBody.js +168 -0
- package/dist/telemetry/network.d.ts +13 -16
- package/dist/telemetry/network.js +30 -52
- package/dist/telemetry/networkRetention.d.ts +83 -0
- package/dist/telemetry/networkRetention.js +117 -0
- package/dist/types.d.ts +26 -0
- package/dist/ui/OutputBuilder.d.ts +10 -0
- package/dist/ui/OutputBuilder.js +12 -0
- package/dist/ui/formatters/a11y.d.ts +5 -7
- package/dist/ui/formatters/a11y.js +7 -61
- package/dist/ui/formatters/console/chronological.js +4 -4
- package/dist/ui/formatters/console/follow.d.ts +4 -2
- package/dist/ui/formatters/console/follow.js +6 -3
- package/dist/ui/formatters/console/json.d.ts +3 -6
- package/dist/ui/formatters/console/json.js +9 -13
- package/dist/ui/formatters/console/shared.d.ts +17 -2
- package/dist/ui/formatters/console/shared.js +17 -0
- package/dist/ui/formatters/console/summarize.d.ts +2 -2
- package/dist/ui/formatters/console/summarize.js +22 -7
- package/dist/ui/formatters/console.d.ts +1 -1
- package/dist/ui/formatters/console.js +1 -5
- package/dist/ui/formatters/details.js +1 -1
- package/dist/ui/formatters/dom.d.ts +13 -4
- package/dist/ui/formatters/dom.js +25 -7
- package/dist/ui/formatters/layout.js +2 -1
- package/dist/ui/formatters/longValues.d.ts +14 -0
- package/dist/ui/formatters/longValues.js +23 -0
- package/dist/ui/formatters/networkList.d.ts +8 -2
- package/dist/ui/formatters/networkList.js +11 -2
- package/dist/ui/formatters/preview.d.ts +4 -1
- package/dist/ui/formatters/preview.js +55 -13
- package/dist/ui/formatters/sessions.d.ts +3 -2
- package/dist/ui/formatters/sessions.js +10 -3
- package/dist/ui/formatters/status.js +7 -0
- package/dist/ui/formatters/triggeredRequests.js +2 -1
- package/dist/ui/messages/chrome.d.ts +34 -7
- package/dist/ui/messages/chrome.js +81 -15
- package/dist/ui/messages/commands.d.ts +29 -8
- package/dist/ui/messages/commands.js +36 -8
- package/dist/ui/messages/networkMessages.d.ts +50 -0
- package/dist/ui/messages/networkMessages.js +66 -0
- package/dist/ui/messages/session.d.ts +8 -0
- package/dist/ui/messages/session.js +10 -0
- package/dist/utils/atomicFile.d.ts +2 -1
- package/dist/utils/atomicFile.js +5 -2
- package/dist/utils/directories.d.ts +41 -0
- package/dist/utils/directories.js +48 -0
- package/dist/utils/http.d.ts +9 -2
- package/dist/utils/http.js +4 -3
- package/dist/utils/strings.d.ts +19 -0
- package/dist/utils/strings.js +16 -0
- package/package.json +2 -2
package/dist/session/paths.js
CHANGED
|
@@ -9,7 +9,8 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { createLogger, logDebugError } from '../ui/logging/index.js';
|
|
12
|
-
import { makeDirectory } from '../utils/directories.js';
|
|
12
|
+
import { dirTrustProblem, makeDirectory, } from '../utils/directories.js';
|
|
13
|
+
import { getErrorMessage } from '../utils/errors.js';
|
|
13
14
|
const log = createLogger('session');
|
|
14
15
|
/**
|
|
15
16
|
* Session file paths relative to ~/.bdg/
|
|
@@ -57,12 +58,21 @@ export const SESSION_STATE_FILES = [
|
|
|
57
58
|
* @returns Full path to the base session directory
|
|
58
59
|
*/
|
|
59
60
|
export function getSessionBaseDir() {
|
|
60
|
-
const override =
|
|
61
|
-
if (override
|
|
61
|
+
const override = sessionDirOverride();
|
|
62
|
+
if (override !== null) {
|
|
62
63
|
return path.isAbsolute(override) ? override : path.resolve(override);
|
|
63
64
|
}
|
|
64
65
|
return path.join(os.homedir(), '.bdg');
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The base session directory the user chose with `$BDG_SESSION_DIR`.
|
|
69
|
+
*
|
|
70
|
+
* @returns The variable's value, or null when unset or blank (`~/.bdg`)
|
|
71
|
+
*/
|
|
72
|
+
function sessionDirOverride() {
|
|
73
|
+
const override = process.env[SESSION_DIR_OVERRIDE_ENV];
|
|
74
|
+
return override && override.trim().length > 0 ? override : null;
|
|
75
|
+
}
|
|
66
76
|
/**
|
|
67
77
|
* The selected session name (`--session` / `BDG_SESSION`), lower-cased:
|
|
68
78
|
* session names are case-insensitive.
|
|
@@ -163,16 +173,153 @@ export function getSessionFilePath(fileType) {
|
|
|
163
173
|
export function getDaemonSocketPath() {
|
|
164
174
|
return getSessionFilePath('DAEMON_SOCKET');
|
|
165
175
|
}
|
|
176
|
+
/** Mode of the session directories bdg creates: only the user can enter them */
|
|
177
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
178
|
+
/** Permission bits that give group or others any access */
|
|
179
|
+
const GROUP_OTHER_ACCESS = 0o077;
|
|
166
180
|
/**
|
|
167
181
|
* Ensure the session directory exists.
|
|
168
182
|
*
|
|
169
|
-
* Creates
|
|
170
|
-
*
|
|
171
|
-
* would spin on a
|
|
183
|
+
* Creates it, and missing parents (the base directory, `sessions/`), with
|
|
184
|
+
* mode 0700. Safe to call multiple times (idempotent). A path that cannot
|
|
185
|
+
* hold a directory is refused before `mkdir`, which would spin on a
|
|
186
|
+
* pseudo-filesystem ({@link makeDirectory}).
|
|
172
187
|
*
|
|
173
188
|
* @throws Error if the directory cannot be created
|
|
174
189
|
*/
|
|
175
190
|
export function ensureSessionDir() {
|
|
176
|
-
makeDirectory(getSessionDir());
|
|
191
|
+
makeDirectory(getSessionDir(), PRIVATE_DIR_MODE);
|
|
192
|
+
}
|
|
193
|
+
/** Session directories need not keep group write out (umask 002 made them 0775) */
|
|
194
|
+
const SESSION_DIR_TRUST = { allowGroupWrite: true };
|
|
195
|
+
/**
|
|
196
|
+
* Directories whose trust a session directory depends on: the base
|
|
197
|
+
* directory and each directory from it down to the session directory
|
|
198
|
+
* (`<base>`, `<base>/sessions`, `<base>/sessions/<name>`), or the directory
|
|
199
|
+
* alone when it is outside the base.
|
|
200
|
+
*
|
|
201
|
+
* @param dir - Session directory
|
|
202
|
+
* @returns Directories, outermost first
|
|
203
|
+
*/
|
|
204
|
+
function sessionDirChain(dir) {
|
|
205
|
+
const base = getSessionBaseDir();
|
|
206
|
+
const relative = path.relative(base, dir);
|
|
207
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
208
|
+
return [{ dir, bdgOwned: false, isBase: false }];
|
|
209
|
+
}
|
|
210
|
+
const chain = [{ dir: base, bdgOwned: sessionDirOverride() === null, isBase: true }];
|
|
211
|
+
let current = base;
|
|
212
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
213
|
+
current = path.join(current, part);
|
|
214
|
+
chain.push({ dir: current, bdgOwned: true, isBase: false });
|
|
215
|
+
}
|
|
216
|
+
return chain;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Remove group and other access from a directory of the user's (best
|
|
220
|
+
* effort). The directory is opened without following a symlink and changed
|
|
221
|
+
* through that descriptor, so a path swapped after the trust check is not
|
|
222
|
+
* affected.
|
|
223
|
+
*
|
|
224
|
+
* @param dir - Trusted directory
|
|
225
|
+
*/
|
|
226
|
+
function tightenDir(dir) {
|
|
227
|
+
if (process.platform === 'win32')
|
|
228
|
+
return;
|
|
229
|
+
let fd;
|
|
230
|
+
try {
|
|
231
|
+
const { O_RDONLY, O_DIRECTORY, O_NOFOLLOW } = fs.constants;
|
|
232
|
+
fd = fs.openSync(dir, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
|
233
|
+
const stat = fs.fstatSync(fd);
|
|
234
|
+
if (stat.uid !== process.getuid?.() || (stat.mode & GROUP_OTHER_ACCESS) === 0)
|
|
235
|
+
return;
|
|
236
|
+
fs.fchmodSync(fd, PRIVATE_DIR_MODE);
|
|
237
|
+
log.debug(`Session directory ${dir} restricted to mode 700`);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
log.debug(`Session directory ${dir} not restricted: ${getErrorMessage(error)}`);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
if (fd !== undefined)
|
|
244
|
+
fs.closeSync(fd);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Why a directory of the chain cannot be trusted. The base directory alone
|
|
249
|
+
* may be a symlink (`~/.bdg` kept with dotfiles or on another disk): its
|
|
250
|
+
* target is checked instead. `sessions/` and session directories must be
|
|
251
|
+
* real directories.
|
|
252
|
+
*
|
|
253
|
+
* @param entry - Directory of the chain
|
|
254
|
+
* @returns The problem, or null when it can be trusted
|
|
255
|
+
* @throws Error from `lstat`/`realpath` (e.g. `ENOENT`)
|
|
256
|
+
*/
|
|
257
|
+
function chainDirProblem(entry) {
|
|
258
|
+
const problem = dirTrustProblem(entry.dir, SESSION_DIR_TRUST);
|
|
259
|
+
if (problem?.kind !== 'symlink' || !entry.isBase)
|
|
260
|
+
return problem;
|
|
261
|
+
const target = fs.realpathSync(entry.dir);
|
|
262
|
+
const targetProblem = dirTrustProblem(target, SESSION_DIR_TRUST);
|
|
263
|
+
return (targetProblem && {
|
|
264
|
+
...targetProblem,
|
|
265
|
+
reason: `it links to ${target}, which is ${targetProblem.reason}`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Accept a trusted directory: tighten it to 0700 when bdg owns it, leave a
|
|
270
|
+
* directory the user chose as it is (noted in the debug log when group or
|
|
271
|
+
* others can use it).
|
|
272
|
+
*
|
|
273
|
+
* @param entry - Trusted directory
|
|
274
|
+
*/
|
|
275
|
+
function acceptTrustedDir(entry) {
|
|
276
|
+
if (entry.bdgOwned) {
|
|
277
|
+
tightenDir(entry.dir);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const mode = fs.statSync(entry.dir, { throwIfNoEntry: false })?.mode ?? 0;
|
|
281
|
+
if ((mode & GROUP_OTHER_ACCESS) === 0)
|
|
282
|
+
return;
|
|
283
|
+
log.debug(`Session directory ${entry.dir} is open to group or others; left as is (chosen with ${SESSION_DIR_OVERRIDE_ENV})`);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Check that a session directory and the directories above it up to the base
|
|
287
|
+
* directory can be trusted before a daemon is started there or its socket is
|
|
288
|
+
* connected to: another user who can write to one of them could replace the
|
|
289
|
+
* socket (and receive every command) or plant files. Each existing directory
|
|
290
|
+
* must be a real directory (not a symlink), owned by the user, and not
|
|
291
|
+
* writable by others ({@link dirTrustProblem}); group write is accepted, since
|
|
292
|
+
* under umask 002 (per-user groups) older versions created `~/.bdg` 0775.
|
|
293
|
+
* The base directory may be a symlink whose target passes the same rule
|
|
294
|
+
* ({@link chainDirProblem}). A directory owned by another uid (a bind mount,
|
|
295
|
+
* `sudo -E`) is refused.
|
|
296
|
+
*
|
|
297
|
+
* Trusted directories bdg owns (the default `~/.bdg`, `sessions/` and named
|
|
298
|
+
* session directories) that group or others can still use are tightened to
|
|
299
|
+
* 0700; a base directory chosen with `$BDG_SESSION_DIR`, or a symlinked base
|
|
300
|
+
* (opened without following links), is never changed.
|
|
301
|
+
* Missing paths are skipped (as is a path through a file, which fails on its
|
|
302
|
+
* own): {@link ensureSessionDir} creates them 0700.
|
|
303
|
+
*
|
|
304
|
+
* @param dir - Session directory (default: the selected session's)
|
|
305
|
+
* @returns The first untrusted directory and why, or null
|
|
306
|
+
*/
|
|
307
|
+
export function secureSessionDir(dir = getSessionDir()) {
|
|
308
|
+
for (const entry of sessionDirChain(dir)) {
|
|
309
|
+
let problem;
|
|
310
|
+
try {
|
|
311
|
+
problem = chainDirProblem(entry);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
const code = error.code;
|
|
315
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
316
|
+
continue;
|
|
317
|
+
problem = { reason: getErrorMessage(error), kind: 'not-directory' };
|
|
318
|
+
}
|
|
319
|
+
if (problem !== null)
|
|
320
|
+
return { dir: entry.dir, ...problem, bdgOwned: entry.bdgOwned };
|
|
321
|
+
acceptTrustedDir(entry);
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
177
324
|
}
|
|
178
325
|
//# sourceMappingURL=paths.js.map
|
|
@@ -30,14 +30,6 @@ export declare function readPortFile(portPath: string): number | null;
|
|
|
30
30
|
* @returns `$BDG_PORT_REGISTRY_DIR`, else `<os temp dir>/bdg-ports-<uid>`
|
|
31
31
|
*/
|
|
32
32
|
export declare function getPortRegistryDir(): string;
|
|
33
|
-
/**
|
|
34
|
-
* Whether a path is a directory the current user can trust: a real directory
|
|
35
|
-
* (not a symlink), owned by the user, not writable by group or others.
|
|
36
|
-
*
|
|
37
|
-
* @param dir - Directory
|
|
38
|
-
* @returns Why it cannot be trusted, or null if it can
|
|
39
|
-
*/
|
|
40
|
-
export declare function untrustedDirReason(dir: string): string | null;
|
|
41
33
|
/**
|
|
42
34
|
* Session directories other than the selected one that bdg knows of: those of
|
|
43
35
|
* this base directory, and those of any base directory found in the
|
|
@@ -20,7 +20,7 @@ import * as path from 'path';
|
|
|
20
20
|
import { getSessionBaseDir, getSessionDir, listSessionDirs, sessionFilePathIn, } from './paths.js';
|
|
21
21
|
import { createLogger, logDebugError } from '../ui/logging/index.js';
|
|
22
22
|
import { delay } from '../utils/async.js';
|
|
23
|
-
import { makeDirectory } from '../utils/directories.js';
|
|
23
|
+
import { makeDirectory, untrustedDirReason } from '../utils/directories.js';
|
|
24
24
|
const log = createLogger('session');
|
|
25
25
|
/** Lock file guarding port selection, in the port registry (or base session) directory */
|
|
26
26
|
const PORT_LOCK_FILE = 'port.lock';
|
|
@@ -33,8 +33,6 @@ const LOCK_WAIT_MS = 5000;
|
|
|
33
33
|
/** Lock age after which its holder is assumed dead */
|
|
34
34
|
const STALE_LOCK_MS = 10000;
|
|
35
35
|
const LOCK_POLL_MS = 25;
|
|
36
|
-
/** Permission bits that let other users write */
|
|
37
|
-
const GROUP_OTHER_WRITE = 0o022;
|
|
38
36
|
/**
|
|
39
37
|
* Read a port number from a `port.txt` file.
|
|
40
38
|
*
|
|
@@ -64,25 +62,6 @@ export function getPortRegistryDir() {
|
|
|
64
62
|
const uid = process.getuid?.();
|
|
65
63
|
return path.join(os.tmpdir(), uid === undefined ? 'bdg-ports' : `bdg-ports-${uid}`);
|
|
66
64
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Whether a path is a directory the current user can trust: a real directory
|
|
69
|
-
* (not a symlink), owned by the user, not writable by group or others.
|
|
70
|
-
*
|
|
71
|
-
* @param dir - Directory
|
|
72
|
-
* @returns Why it cannot be trusted, or null if it can
|
|
73
|
-
*/
|
|
74
|
-
export function untrustedDirReason(dir) {
|
|
75
|
-
const stat = fs.lstatSync(dir);
|
|
76
|
-
if (!stat.isDirectory())
|
|
77
|
-
return 'not a directory';
|
|
78
|
-
const uid = process.getuid?.();
|
|
79
|
-
if (uid !== undefined && stat.uid !== uid)
|
|
80
|
-
return `owned by uid ${stat.uid}`;
|
|
81
|
-
if (process.platform !== 'win32' && (stat.mode & GROUP_OTHER_WRITE) !== 0) {
|
|
82
|
-
return `writable by others (mode ${(stat.mode & 0o777).toString(8)})`;
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
65
|
/**
|
|
87
66
|
* The port registry directory, created (mode 0700) if missing, if it can be
|
|
88
67
|
* trusted (see {@link untrustedDirReason}).
|
|
@@ -11,8 +11,10 @@ import { type SessionDirEntry } from './paths.js';
|
|
|
11
11
|
* `unresponsive`), died and left its Chrome running (`crashed`) or only
|
|
12
12
|
* files (`stale`), or exited after the session ended without `bdg stop`
|
|
13
13
|
* (`ended`: Chrome crashed or was closed, the page was closed, `--timeout`).
|
|
14
|
+
* `untrusted`: something listens on its socket, but its directory is not
|
|
15
|
+
* safe to use, so it is not asked.
|
|
14
16
|
*/
|
|
15
|
-
export type RunningSessionState = 'active' | 'starting' | 'ending' | 'unresponsive' | 'crashed' | 'stale' | 'ended';
|
|
17
|
+
export type RunningSessionState = 'active' | 'starting' | 'ending' | 'unresponsive' | 'crashed' | 'stale' | 'ended' | 'untrusted';
|
|
16
18
|
/**
|
|
17
19
|
* One session in the list.
|
|
18
20
|
*/
|
|
@@ -33,6 +35,8 @@ export interface RunningSessionInfo {
|
|
|
33
35
|
endReason?: UnexpectedEndReason;
|
|
34
36
|
/** When an `ended` session ended (epoch ms) */
|
|
35
37
|
endedAt?: number;
|
|
38
|
+
/** Why an `untrusted` session's directory is not safe to use */
|
|
39
|
+
untrusted?: string;
|
|
36
40
|
}
|
|
37
41
|
/**
|
|
38
42
|
* Summarize a daemon's status response.
|
|
@@ -7,7 +7,7 @@ import { getStatus } from '../ipc/client.js';
|
|
|
7
7
|
import { isSessionChrome, readLiveDaemonPid } from './cleanup/staleSession.js';
|
|
8
8
|
import { probeDaemonSocket } from './daemonSocket.js';
|
|
9
9
|
import { readLastSessionEnd } from './lastSession.js';
|
|
10
|
-
import { SESSION_STATE_FILES, getNamedSessionDir, listSessionDirs, sessionFilePathIn, } from './paths.js';
|
|
10
|
+
import { SESSION_STATE_FILES, getNamedSessionDir, listSessionDirs, secureSessionDir, sessionFilePathIn, } from './paths.js';
|
|
11
11
|
import { readPidFromFile } from './pid.js';
|
|
12
12
|
import { readPortFile } from './portClaims.js';
|
|
13
13
|
import { isValidSessionName, normalizeSessionName } from './sessionName.js';
|
|
@@ -48,6 +48,10 @@ async function describeSession(entry) {
|
|
|
48
48
|
const probe = await probeDaemonSocket(socketPath);
|
|
49
49
|
if (probe !== 'alive')
|
|
50
50
|
return describeWithoutSocket(entry, probe);
|
|
51
|
+
const untrusted = secureSessionDir(dir);
|
|
52
|
+
if (untrusted) {
|
|
53
|
+
return { name, state: 'untrusted', untrusted: `${untrusted.dir}: ${untrusted.reason}` };
|
|
54
|
+
}
|
|
51
55
|
try {
|
|
52
56
|
const response = await getStatus(socketPath);
|
|
53
57
|
if (response.status === 'ok' && response.data)
|
package/dist/telemetry/a11y.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Protocol } from '../connection/typed-cdp.js';
|
|
2
|
-
import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, NodeRef } from '../types.js';
|
|
2
|
+
import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, ListedA11yTree, NodeRef } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Builds accessibility tree from raw CDP nodes.
|
|
5
5
|
*
|
|
@@ -14,6 +14,20 @@ import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, NodeRef } f
|
|
|
14
14
|
* @throws Error if no root node found
|
|
15
15
|
*/
|
|
16
16
|
export declare function buildTreeFromRawNodes(rawNodes: Protocol.Accessibility.AXNode[]): A11yTree;
|
|
17
|
+
/**
|
|
18
|
+
* The nodes `dom a11y tree` lists, depth-first from the root (then nodes not
|
|
19
|
+
* under it, such as frame content): the first `limit` (0 = all) no deeper
|
|
20
|
+
* than `maxDepth`. Noise ({@link isTreeNoise}) is counted as skipped and its
|
|
21
|
+
* children move up a level, so the budget goes to meaningful nodes; nodes cut
|
|
22
|
+
* by the limit or depth are counted as omitted. Every node of the tree is
|
|
23
|
+
* listed, omitted or skipped.
|
|
24
|
+
*
|
|
25
|
+
* @param tree - Accessibility tree
|
|
26
|
+
* @param limit - Nodes to list (0 = all listable ones)
|
|
27
|
+
* @param maxDepth - Deepest level to list (0 = root only; undefined = all)
|
|
28
|
+
* @returns Listed nodes with their depth, the tree size, and how many were cut or skipped
|
|
29
|
+
*/
|
|
30
|
+
export declare function listA11yTree(tree: A11yTree, limit: number, maxDepth?: number): ListedA11yTree;
|
|
17
31
|
/**
|
|
18
32
|
* Collects the full accessibility tree from the page via IPC.
|
|
19
33
|
*
|
package/dist/telemetry/a11y.js
CHANGED
|
@@ -47,6 +47,89 @@ export function buildTreeFromRawNodes(rawNodes) {
|
|
|
47
47
|
const root = nodes.get(rootRaw.nodeId);
|
|
48
48
|
return { root, nodes, count: nodes.size };
|
|
49
49
|
}
|
|
50
|
+
/** Roles that only lay out their children and say nothing themselves */
|
|
51
|
+
const LAYOUT_ROLES = new Set([
|
|
52
|
+
'generic',
|
|
53
|
+
'none',
|
|
54
|
+
'presentation',
|
|
55
|
+
'LayoutTable',
|
|
56
|
+
'LayoutTableRow',
|
|
57
|
+
'LayoutTableCell',
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* Whether a node is left out of `dom a11y tree`: text boxes, blank text,
|
|
61
|
+
* text that repeats its parent's name, and nameless layout wrappers.
|
|
62
|
+
*
|
|
63
|
+
* @param node - Accessibility node
|
|
64
|
+
* @param parentName - Name of the nearest named ancestor
|
|
65
|
+
* @returns True when the node says nothing of its own
|
|
66
|
+
*/
|
|
67
|
+
function isTreeNoise(node, parentName) {
|
|
68
|
+
return (node.role === 'InlineTextBox' ||
|
|
69
|
+
(node.role === 'StaticText' && (node.name === parentName || !node.name?.trim())) ||
|
|
70
|
+
(LAYOUT_ROLES.has(node.role) && !node.name));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The nodes `dom a11y tree` lists, depth-first from the root (then nodes not
|
|
74
|
+
* under it, such as frame content): the first `limit` (0 = all) no deeper
|
|
75
|
+
* than `maxDepth`. Noise ({@link isTreeNoise}) is counted as skipped and its
|
|
76
|
+
* children move up a level, so the budget goes to meaningful nodes; nodes cut
|
|
77
|
+
* by the limit or depth are counted as omitted. Every node of the tree is
|
|
78
|
+
* listed, omitted or skipped.
|
|
79
|
+
*
|
|
80
|
+
* @param tree - Accessibility tree
|
|
81
|
+
* @param limit - Nodes to list (0 = all listable ones)
|
|
82
|
+
* @param maxDepth - Deepest level to list (0 = root only; undefined = all)
|
|
83
|
+
* @returns Listed nodes with their depth, the tree size, and how many were cut or skipped
|
|
84
|
+
*/
|
|
85
|
+
export function listA11yTree(tree, limit, maxDepth) {
|
|
86
|
+
const nodes = [];
|
|
87
|
+
const visited = new Set();
|
|
88
|
+
let omitted = 0;
|
|
89
|
+
let skipped = 0;
|
|
90
|
+
const visit = (node, depth, parentName) => {
|
|
91
|
+
if (visited.has(node.nodeId))
|
|
92
|
+
return;
|
|
93
|
+
visited.add(node.nodeId);
|
|
94
|
+
const listable = !isTreeNoise(node, parentName);
|
|
95
|
+
if (listable) {
|
|
96
|
+
const fits = (limit === 0 || nodes.length < limit) && (maxDepth ?? depth) >= depth;
|
|
97
|
+
if (fits)
|
|
98
|
+
nodes.push(listedNode(node, depth));
|
|
99
|
+
else
|
|
100
|
+
omitted++;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
skipped++;
|
|
104
|
+
}
|
|
105
|
+
for (const childId of node.childIds ?? []) {
|
|
106
|
+
const child = tree.nodes.get(childId);
|
|
107
|
+
if (child)
|
|
108
|
+
visit(child, listable ? depth + 1 : depth, node.name ?? parentName);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
visit(tree.root, 0, undefined);
|
|
112
|
+
for (const node of tree.nodes.values())
|
|
113
|
+
visit(node, 0, undefined);
|
|
114
|
+
return {
|
|
115
|
+
nodes,
|
|
116
|
+
count: tree.count,
|
|
117
|
+
...(omitted > 0 && { omitted }),
|
|
118
|
+
...(skipped > 0 && { skipped }),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A node as the tree lists it: its depth instead of child ids (its children
|
|
123
|
+
* follow it, one level deeper).
|
|
124
|
+
*
|
|
125
|
+
* @param node - Accessibility node
|
|
126
|
+
* @param depth - Its level in the listing
|
|
127
|
+
* @returns Listed node
|
|
128
|
+
*/
|
|
129
|
+
function listedNode(node, depth) {
|
|
130
|
+
const { childIds: _childIds, ...rest } = node;
|
|
131
|
+
return { ...rest, depth };
|
|
132
|
+
}
|
|
50
133
|
/**
|
|
51
134
|
* Ids of a node's children as shown: ignored children are replaced by their
|
|
52
135
|
* own non-ignored descendants; ids missing from the tree are dropped.
|
|
@@ -18,11 +18,22 @@ export interface HARMetadata {
|
|
|
18
18
|
/** Target title (optional) */
|
|
19
19
|
targetTitle?: string;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Options for HAR generation.
|
|
23
|
+
*/
|
|
24
|
+
export interface HAROptions {
|
|
25
|
+
/** Keep credentials as captured instead of redacting them (see sanitize.ts) */
|
|
26
|
+
includeSensitive?: boolean;
|
|
27
|
+
}
|
|
21
28
|
/**
|
|
22
29
|
* Build HAR 1.2 format from network telemetry data.
|
|
23
30
|
*
|
|
31
|
+
* Credentials are redacted (`log.comment` says so) unless
|
|
32
|
+
* `options.includeSensitive` is set.
|
|
33
|
+
*
|
|
24
34
|
* @param requests - Array of network requests collected during session
|
|
25
35
|
* @param metadata - Metadata for HAR creator/browser info
|
|
36
|
+
* @param options - Whether to keep credentials
|
|
26
37
|
* @returns Complete HAR object
|
|
27
38
|
*
|
|
28
39
|
* @remarks
|
|
@@ -39,5 +50,5 @@ export interface HARMetadata {
|
|
|
39
50
|
* fs.writeFileSync('capture.har', JSON.stringify(har, null, 2));
|
|
40
51
|
* ```
|
|
41
52
|
*/
|
|
42
|
-
export declare function buildHAR(requests: NetworkRequest[], metadata: HARMetadata): HAR;
|
|
53
|
+
export declare function buildHAR(requests: NetworkRequest[], metadata: HARMetadata, options?: HAROptions): HAR;
|
|
43
54
|
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* HAR (HTTP Archive) builder for transforming network telemetry to HAR 1.2 format.
|
|
3
3
|
*/
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
-
import {
|
|
5
|
+
import { sanitizeEntry } from './sanitize.js';
|
|
6
|
+
import { skippedBodyReason } from '../networkRetention.js';
|
|
7
|
+
import { harSanitizedComment } from '../../ui/messages/networkMessages.js';
|
|
6
8
|
/**
|
|
7
9
|
* Loads Node builtins on first use: importing `node:http` in an ES module
|
|
8
10
|
* reads all its exports, which loads undici and zlib (about 8 ms of every CLI
|
|
@@ -14,8 +16,12 @@ const DEFAULT_HTTP_VERSION = 'HTTP/1.1';
|
|
|
14
16
|
/**
|
|
15
17
|
* Build HAR 1.2 format from network telemetry data.
|
|
16
18
|
*
|
|
19
|
+
* Credentials are redacted (`log.comment` says so) unless
|
|
20
|
+
* `options.includeSensitive` is set.
|
|
21
|
+
*
|
|
17
22
|
* @param requests - Array of network requests collected during session
|
|
18
23
|
* @param metadata - Metadata for HAR creator/browser info
|
|
24
|
+
* @param options - Whether to keep credentials
|
|
19
25
|
* @returns Complete HAR object
|
|
20
26
|
*
|
|
21
27
|
* @remarks
|
|
@@ -32,8 +38,9 @@ const DEFAULT_HTTP_VERSION = 'HTTP/1.1';
|
|
|
32
38
|
* fs.writeFileSync('capture.har', JSON.stringify(har, null, 2));
|
|
33
39
|
* ```
|
|
34
40
|
*/
|
|
35
|
-
export function buildHAR(requests, metadata) {
|
|
36
|
-
const
|
|
41
|
+
export function buildHAR(requests, metadata, options = {}) {
|
|
42
|
+
const built = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
|
|
43
|
+
const entries = options.includeSensitive ? built : built.map(sanitizeEntry);
|
|
37
44
|
const log = {
|
|
38
45
|
version: '1.2',
|
|
39
46
|
creator: {
|
|
@@ -42,6 +49,7 @@ export function buildHAR(requests, metadata) {
|
|
|
42
49
|
comment: 'Browser Debugger CLI - https://github.com/szymdzum/browser-debugger-cli',
|
|
43
50
|
},
|
|
44
51
|
entries,
|
|
52
|
+
...(!options.includeSensitive && { comment: harSanitizedComment() }),
|
|
45
53
|
};
|
|
46
54
|
if (metadata.chromeVersion) {
|
|
47
55
|
log.browser = {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction for HAR exports.
|
|
3
|
+
*
|
|
4
|
+
* HAR files are made to be shared (bug reports, tickets), so `bdg network har`
|
|
5
|
+
* redacts credentials by default, as Chrome DevTools does since Chrome 130.
|
|
6
|
+
* Chrome's sanitized export drops the `Cookie`, `Set-Cookie` and
|
|
7
|
+
* `Authorization` headers and empties the `cookies` arrays; bdg instead keeps
|
|
8
|
+
* every header, cookie and parameter name (and cookie attributes such as
|
|
9
|
+
* `httpOnly`) with the value `[redacted]`, so the export still shows that a
|
|
10
|
+
* request was authenticated and which cookies were set. It also covers API
|
|
11
|
+
* key, token and session headers, credential query parameters in URLs
|
|
12
|
+
* (`?code=`, `?access_token=`) and credential fields of request bodies
|
|
13
|
+
* (sanitizeBody.ts). `headersSize` and `bodySize` stay those of the captured
|
|
14
|
+
* request. Response bodies and WebSocket messages are not redacted.
|
|
15
|
+
*/
|
|
16
|
+
import type { Entry } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Redact the credentials of a HAR entry.
|
|
19
|
+
*
|
|
20
|
+
* @param entry - Entry built from the captured request
|
|
21
|
+
* @returns Copy of the entry with credential values replaced by {@link REDACTED}
|
|
22
|
+
*/
|
|
23
|
+
export declare function sanitizeEntry(entry: Entry): Entry;
|
|
24
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction for HAR exports.
|
|
3
|
+
*
|
|
4
|
+
* HAR files are made to be shared (bug reports, tickets), so `bdg network har`
|
|
5
|
+
* redacts credentials by default, as Chrome DevTools does since Chrome 130.
|
|
6
|
+
* Chrome's sanitized export drops the `Cookie`, `Set-Cookie` and
|
|
7
|
+
* `Authorization` headers and empties the `cookies` arrays; bdg instead keeps
|
|
8
|
+
* every header, cookie and parameter name (and cookie attributes such as
|
|
9
|
+
* `httpOnly`) with the value `[redacted]`, so the export still shows that a
|
|
10
|
+
* request was authenticated and which cookies were set. It also covers API
|
|
11
|
+
* key, token and session headers, credential query parameters in URLs
|
|
12
|
+
* (`?code=`, `?access_token=`) and credential fields of request bodies
|
|
13
|
+
* (sanitizeBody.ts). `headersSize` and `bodySize` stay those of the captured
|
|
14
|
+
* request. Response bodies and WebSocket messages are not redacted.
|
|
15
|
+
*/
|
|
16
|
+
import { REDACTED, isSensitiveField, redactPairs, redactRequestBody, } from './sanitizeBody.js';
|
|
17
|
+
/** {@link REDACTED} as written in a URL */
|
|
18
|
+
const URL_REDACTED = encodeURIComponent(REDACTED);
|
|
19
|
+
/** Headers whose values are credentials, lowercased (others match by pattern) */
|
|
20
|
+
const SENSITIVE_HEADERS = new Set([
|
|
21
|
+
'authorization',
|
|
22
|
+
'proxy-authorization',
|
|
23
|
+
'authentication',
|
|
24
|
+
'cookie',
|
|
25
|
+
'set-cookie',
|
|
26
|
+
]);
|
|
27
|
+
/** Custom headers carrying keys or tokens (`X-Api-Key`, `X-Auth-Token`, `X-CSRF-Token`) */
|
|
28
|
+
const SENSITIVE_CUSTOM_HEADER = /^x-.*(key|token|secret|auth)/i;
|
|
29
|
+
/**
|
|
30
|
+
* Headers with a credential segment between hyphens (`api-key`,
|
|
31
|
+
* `private-token`, `cf-access-jwt-assertion`, `ocp-apim-subscription-key`,
|
|
32
|
+
* `session-id`); `www-authenticate` and `proxy-authenticate` do not match
|
|
33
|
+
*/
|
|
34
|
+
const SENSITIVE_HEADER_SEGMENT = /(^|-)(api-?key|apikey|token|secret|jwt|subscription-key|session(-?id)?)(-|$)/i;
|
|
35
|
+
/** Headers whose values are URLs that may carry credential parameters */
|
|
36
|
+
const URL_HEADERS = new Set(['location', 'referer']);
|
|
37
|
+
/** Query parameter names that hold credentials in URLs only (OAuth codes, signed URLs, API keys) */
|
|
38
|
+
const SENSITIVE_URL_PARAM = /^(code|sig|key)$/i;
|
|
39
|
+
/**
|
|
40
|
+
* Redact the credentials of a HAR entry.
|
|
41
|
+
*
|
|
42
|
+
* @param entry - Entry built from the captured request
|
|
43
|
+
* @returns Copy of the entry with credential values replaced by {@link REDACTED}
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeEntry(entry) {
|
|
46
|
+
const { request, response } = entry;
|
|
47
|
+
const postData = request.postData;
|
|
48
|
+
return {
|
|
49
|
+
...entry,
|
|
50
|
+
request: {
|
|
51
|
+
...request,
|
|
52
|
+
url: redactUrl(request.url),
|
|
53
|
+
cookies: request.cookies.map(redactCookie),
|
|
54
|
+
headers: request.headers.map(redactHeader),
|
|
55
|
+
queryString: request.queryString.map(redactQueryParam),
|
|
56
|
+
...(postData?.text !== undefined && {
|
|
57
|
+
postData: { ...postData, text: redactRequestBody(postData.text, postData.mimeType) },
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
response: {
|
|
61
|
+
...response,
|
|
62
|
+
cookies: response.cookies.map(redactCookie),
|
|
63
|
+
headers: response.headers.map(redactHeader),
|
|
64
|
+
redirectURL: redactUrl(response.redirectURL),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a header carries a credential.
|
|
70
|
+
*
|
|
71
|
+
* @param name - Header name (any case)
|
|
72
|
+
* @returns True for auth, cookie, API key, token, secret and session headers
|
|
73
|
+
*/
|
|
74
|
+
function isSensitiveHeader(name) {
|
|
75
|
+
return (SENSITIVE_HEADERS.has(name.toLowerCase()) ||
|
|
76
|
+
SENSITIVE_CUSTOM_HEADER.test(name) ||
|
|
77
|
+
SENSITIVE_HEADER_SEGMENT.test(name));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Redact a header's value if it carries a credential, or the credential
|
|
81
|
+
* parameters of a `Location` or `Referer` URL.
|
|
82
|
+
*
|
|
83
|
+
* @param header - HAR header
|
|
84
|
+
* @returns The header, or a copy with its value redacted
|
|
85
|
+
*/
|
|
86
|
+
function redactHeader(header) {
|
|
87
|
+
if (isSensitiveHeader(header.name))
|
|
88
|
+
return { ...header, value: REDACTED };
|
|
89
|
+
if (!URL_HEADERS.has(header.name.toLowerCase()))
|
|
90
|
+
return header;
|
|
91
|
+
const value = redactUrl(header.value);
|
|
92
|
+
return value === header.value ? header : { ...header, value };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Redact a cookie's value, keeping its name and attributes.
|
|
96
|
+
*
|
|
97
|
+
* @param cookie - HAR cookie
|
|
98
|
+
* @returns Copy with the value redacted
|
|
99
|
+
*/
|
|
100
|
+
function redactCookie(cookie) {
|
|
101
|
+
return { ...cookie, value: REDACTED };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a URL query or fragment parameter holds a credential.
|
|
105
|
+
*
|
|
106
|
+
* @param name - Decoded parameter name
|
|
107
|
+
* @returns True for credential field names, `code`, `sig` and `key`
|
|
108
|
+
*/
|
|
109
|
+
function isSensitiveParam(name) {
|
|
110
|
+
return isSensitiveField(name) || SENSITIVE_URL_PARAM.test(name);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Redact a parsed query parameter if it holds a credential.
|
|
114
|
+
*
|
|
115
|
+
* @param param - HAR query parameter
|
|
116
|
+
* @returns The parameter, or a copy with its value redacted
|
|
117
|
+
*/
|
|
118
|
+
function redactQueryParam(param) {
|
|
119
|
+
return isSensitiveParam(param.name) ? { ...param, value: REDACTED } : param;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Redact credential parameter values in a URL's query and fragment
|
|
123
|
+
* (`#access_token=` of the OAuth implicit flow), leaving the rest byte for byte.
|
|
124
|
+
*
|
|
125
|
+
* @param url - URL (empty for none)
|
|
126
|
+
* @returns URL with credential values replaced by the URL-encoded {@link REDACTED}
|
|
127
|
+
*/
|
|
128
|
+
function redactUrl(url) {
|
|
129
|
+
const hash = url.indexOf('#');
|
|
130
|
+
const beforeHash = hash === -1 ? url : url.slice(0, hash);
|
|
131
|
+
const fragment = hash === -1 ? '' : `#${redactPairs(url.slice(hash + 1), isSensitiveParam, URL_REDACTED)}`;
|
|
132
|
+
const question = beforeHash.indexOf('?');
|
|
133
|
+
if (question === -1)
|
|
134
|
+
return beforeHash + fragment;
|
|
135
|
+
const query = redactPairs(beforeHash.slice(question + 1), isSensitiveParam, URL_REDACTED);
|
|
136
|
+
return `${beforeHash.slice(0, question)}?${query}${fragment}`;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=sanitize.js.map
|