@sovovs/bycli 2.1.16 → 2.1.18
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/cli-manifest.json +198 -0
- package/clis/weixin/_wechat/collections.js +429 -0
- package/clis/weixin/_wechat/fixtures/collection-detail.json +55 -0
- package/clis/weixin/_wechat/fixtures/collections-list.json +38 -0
- package/clis/weixin/_wechat/fixtures/published-page.json +6 -0
- package/clis/weixin/_wechat/publish-download.js +165 -0
- package/clis/weixin/_wechat/publish-records.js +304 -0
- package/clis/weixin/collection-detail.js +75 -0
- package/clis/weixin/collections.js +66 -0
- package/clis/weixin/download-publish-data.js +66 -0
- package/clis/weixin/published.js +72 -0
- package/dist/src/browser/base-page.d.ts +2 -1
- package/dist/src/browser/base-page.js +14 -3
- package/dist/src/browser/page.d.ts +2 -2
- package/dist/src/browser/page.js +3 -1
- package/dist/src/download/article-download.js +30 -3
- package/dist/src/observation/redaction.js +3 -1
- package/dist/src/types.d.ts +6 -1
- package/package.json +1 -1
- package/dist/src/browser/extension-version-metadata.test.d.ts +0 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ArgumentError, EmptyResultError } from '@sovovs/bycli/errors';
|
|
2
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
3
|
+
import { resolveBrowserCredentials } from './_wechat/auth-session.js';
|
|
4
|
+
import { collectCollections } from './_wechat/collections.js';
|
|
5
|
+
|
|
6
|
+
const SAFE_REFERER = 'https://mp.weixin.qq.com/cgi-bin/appmsgalbum?action=list';
|
|
7
|
+
const PAGE_SIZE = 20;
|
|
8
|
+
|
|
9
|
+
const positiveSafeInteger = (value, name) => {
|
|
10
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
11
|
+
throw new ArgumentError(`${name} must be a positive safe integer`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const collectionsCommand = cli({
|
|
17
|
+
site: 'weixin',
|
|
18
|
+
name: 'collections',
|
|
19
|
+
access: 'read',
|
|
20
|
+
description: 'List WeChat official-account content collections',
|
|
21
|
+
domain: 'mp.weixin.qq.com',
|
|
22
|
+
strategy: Strategy.COOKIE,
|
|
23
|
+
browser: true,
|
|
24
|
+
navigateBefore: false,
|
|
25
|
+
args: [
|
|
26
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of collections to return' },
|
|
27
|
+
{ name: 'max-pages', type: 'int', default: 5, help: 'Maximum number of collection pages to scan' },
|
|
28
|
+
],
|
|
29
|
+
columns: [
|
|
30
|
+
'collectionId', 'title', 'collectionType', 'itemCount', 'views', 'continuousRead',
|
|
31
|
+
'isUpdating', 'isBanned', 'isPaid', 'createdAt', 'updatedAt', 'coverUrl',
|
|
32
|
+
],
|
|
33
|
+
func: async (page, args) => {
|
|
34
|
+
const limit = positiveSafeInteger(args.limit, 'limit');
|
|
35
|
+
const maxPages = positiveSafeInteger(args['max-pages'], 'max-pages');
|
|
36
|
+
const { token } = await resolveBrowserCredentials(page);
|
|
37
|
+
const rows = await collectCollections({
|
|
38
|
+
page,
|
|
39
|
+
token,
|
|
40
|
+
safeReferer: SAFE_REFERER,
|
|
41
|
+
limit,
|
|
42
|
+
pageSize: PAGE_SIZE,
|
|
43
|
+
maxPages,
|
|
44
|
+
});
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
throw new EmptyResultError(
|
|
47
|
+
'weixin collections',
|
|
48
|
+
'No collections were found. Create a collection in the WeChat Official Accounts dashboard first.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return rows.map(row => ({
|
|
52
|
+
collectionId: row.collectionId,
|
|
53
|
+
title: row.title,
|
|
54
|
+
collectionType: row.collectionType,
|
|
55
|
+
itemCount: row.itemCount,
|
|
56
|
+
views: row.views,
|
|
57
|
+
continuousRead: row.continuousRead,
|
|
58
|
+
isUpdating: row.isUpdating,
|
|
59
|
+
isBanned: row.isBanned,
|
|
60
|
+
isPaid: row.isPaid,
|
|
61
|
+
createdAt: row.createdAt,
|
|
62
|
+
updatedAt: row.updatedAt,
|
|
63
|
+
coverUrl: row.coverUrl,
|
|
64
|
+
}));
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ArgumentError } from '@sovovs/bycli/errors';
|
|
2
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
3
|
+
import { resolveBrowserCredentials } from './_wechat/auth-session.js';
|
|
4
|
+
import { downloadPublishData } from './_wechat/publish-download.js';
|
|
5
|
+
import {
|
|
6
|
+
buildDetailUrl,
|
|
7
|
+
collectPublishedRecords,
|
|
8
|
+
matchPublishedRecord,
|
|
9
|
+
positiveSafeInteger,
|
|
10
|
+
validatePublishDate,
|
|
11
|
+
} from './_wechat/publish-records.js';
|
|
12
|
+
|
|
13
|
+
const COLUMNS = ['title', 'published_at', 'url', 'status', 'path', 'size'];
|
|
14
|
+
|
|
15
|
+
export const downloadPublishDataCommand = cli({
|
|
16
|
+
site: 'weixin',
|
|
17
|
+
name: 'download-publish-data',
|
|
18
|
+
access: 'write',
|
|
19
|
+
domain: 'mp.weixin.qq.com',
|
|
20
|
+
description: 'Match a Weixin published article and download its detail spreadsheet',
|
|
21
|
+
strategy: Strategy.INTERCEPT,
|
|
22
|
+
browser: true,
|
|
23
|
+
navigateBefore: false,
|
|
24
|
+
args: [
|
|
25
|
+
{ name: 'query', positional: true, required: true, help: 'Exact article URL or title text' },
|
|
26
|
+
{ name: 'date', help: 'Optional publication date in YYYY-MM-DD' },
|
|
27
|
+
{ name: 'output', default: './weixin-publish-data', help: 'Directory for downloaded spreadsheets' },
|
|
28
|
+
{ name: 'max-pages', type: 'int', default: 5, help: 'Maximum published-record pages to scan' },
|
|
29
|
+
{ name: 'timeout', type: 'int', default: 60, help: 'Maximum seconds for capture and download' },
|
|
30
|
+
],
|
|
31
|
+
columns: COLUMNS,
|
|
32
|
+
func: async (page, args) => {
|
|
33
|
+
const query = String(args.query ?? '').trim();
|
|
34
|
+
if (!query) throw new ArgumentError('query required');
|
|
35
|
+
|
|
36
|
+
const timeoutSeconds = positiveSafeInteger(args.timeout, 'timeout', 60);
|
|
37
|
+
const maxPages = positiveSafeInteger(args['max-pages'], 'max-pages', 5);
|
|
38
|
+
const validatedDate = validatePublishDate(args.date);
|
|
39
|
+
const scanLimit = maxPages * 10;
|
|
40
|
+
if (!Number.isSafeInteger(scanLimit)) throw new ArgumentError('max-pages is too large');
|
|
41
|
+
const { token } = await resolveBrowserCredentials(page);
|
|
42
|
+
const rows = await collectPublishedRecords(page, {
|
|
43
|
+
token,
|
|
44
|
+
limit: scanLimit,
|
|
45
|
+
maxPages,
|
|
46
|
+
timeout: timeoutSeconds,
|
|
47
|
+
});
|
|
48
|
+
const record = matchPublishedRecord(rows, query, validatedDate);
|
|
49
|
+
const detailUrl = buildDetailUrl(record, token);
|
|
50
|
+
const result = await downloadPublishData(page, {
|
|
51
|
+
detailUrl,
|
|
52
|
+
title: record.title,
|
|
53
|
+
outputDir: args.output ?? './weixin-publish-data',
|
|
54
|
+
timeoutSeconds,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return [{
|
|
58
|
+
title: record.title,
|
|
59
|
+
published_at: record.publishedAt,
|
|
60
|
+
url: record.url,
|
|
61
|
+
status: result.status,
|
|
62
|
+
path: result.path,
|
|
63
|
+
size: result.size,
|
|
64
|
+
}];
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { EmptyResultError } from '@sovovs/bycli/errors';
|
|
2
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
3
|
+
import { resolveBrowserCredentials } from './_wechat/auth-session.js';
|
|
4
|
+
import { collectPublishedRecords, positiveSafeInteger } from './_wechat/publish-records.js';
|
|
5
|
+
|
|
6
|
+
const COLUMNS = [
|
|
7
|
+
'title',
|
|
8
|
+
'published_at',
|
|
9
|
+
'url',
|
|
10
|
+
'notified',
|
|
11
|
+
'failed',
|
|
12
|
+
'reads',
|
|
13
|
+
'likes',
|
|
14
|
+
'shares',
|
|
15
|
+
'recommends',
|
|
16
|
+
'comments',
|
|
17
|
+
'underlines',
|
|
18
|
+
'reprints',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const publishedCommand = cli({
|
|
22
|
+
site: 'weixin',
|
|
23
|
+
name: 'published',
|
|
24
|
+
access: 'read',
|
|
25
|
+
domain: 'mp.weixin.qq.com',
|
|
26
|
+
description: 'List Weixin published records and engagement metrics',
|
|
27
|
+
strategy: Strategy.INTERCEPT,
|
|
28
|
+
browser: true,
|
|
29
|
+
navigateBefore: false,
|
|
30
|
+
args: [
|
|
31
|
+
{ name: 'query', positional: true, required: false, help: 'Optional article title or URL filter' },
|
|
32
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Maximum articles to return' },
|
|
33
|
+
{ name: 'max-pages', type: 'int', default: 5, help: 'Maximum published-record pages to scan' },
|
|
34
|
+
{ name: 'timeout', type: 'int', default: 30, help: 'Maximum seconds for request capture' },
|
|
35
|
+
],
|
|
36
|
+
columns: COLUMNS,
|
|
37
|
+
func: async (page, args) => {
|
|
38
|
+
const limit = positiveSafeInteger(args.limit, 'limit', 10);
|
|
39
|
+
const { token } = await resolveBrowserCredentials(page);
|
|
40
|
+
const query = String(args.query ?? '').trim();
|
|
41
|
+
const maxPages = args['max-pages'] ?? 5;
|
|
42
|
+
const rows = await collectPublishedRecords(page, {
|
|
43
|
+
token,
|
|
44
|
+
limit: query ? maxPages * 10 : limit,
|
|
45
|
+
maxPages,
|
|
46
|
+
timeout: args.timeout,
|
|
47
|
+
});
|
|
48
|
+
const matched = rows.filter(row => (
|
|
49
|
+
!query || row.title.includes(query) || row.url.includes(query)
|
|
50
|
+
));
|
|
51
|
+
if (matched.length === 0) {
|
|
52
|
+
throw new EmptyResultError(
|
|
53
|
+
'weixin published',
|
|
54
|
+
`No published record matched "${query}".`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return matched.slice(0, limit).map(row => ({
|
|
58
|
+
title: row.title,
|
|
59
|
+
published_at: row.publishedAt,
|
|
60
|
+
url: row.url,
|
|
61
|
+
notified: row.notified,
|
|
62
|
+
failed: row.failed,
|
|
63
|
+
reads: row.reads,
|
|
64
|
+
likes: row.likes,
|
|
65
|
+
shares: row.shares,
|
|
66
|
+
recommends: row.recommends,
|
|
67
|
+
comments: row.comments,
|
|
68
|
+
underlines: row.underlines,
|
|
69
|
+
reprints: row.reprints,
|
|
70
|
+
}));
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -65,7 +65,8 @@ export declare abstract class BasePage implements IPage {
|
|
|
65
65
|
/**
|
|
66
66
|
* Safely evaluate JS with pre-serialized arguments.
|
|
67
67
|
* Each key in `args` becomes a `const` declaration with JSON-serialized value,
|
|
68
|
-
*
|
|
68
|
+
* scoped to this invocation. Prevents injection and persistent-context lexical
|
|
69
|
+
* redeclarations by design.
|
|
69
70
|
*
|
|
70
71
|
* Usage:
|
|
71
72
|
* page.evaluateWithArgs(`(async () => { return sym; })()`, { sym: userInput })
|
|
@@ -59,6 +59,14 @@ function parseKeyChord(rawKey) {
|
|
|
59
59
|
const key = parts.at(-1);
|
|
60
60
|
return key ? { key, modifiers } : { key: rawKey, modifiers: [] };
|
|
61
61
|
}
|
|
62
|
+
const EVALUATE_ARG_RESERVED_WORDS = new Set([
|
|
63
|
+
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
64
|
+
'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false',
|
|
65
|
+
'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof',
|
|
66
|
+
'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public',
|
|
67
|
+
'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
|
|
68
|
+
'var', 'void', 'while', 'with', 'yield',
|
|
69
|
+
]);
|
|
62
70
|
export class BasePage {
|
|
63
71
|
_lastUrl = null;
|
|
64
72
|
/** Cached previous snapshot hashes for incremental diff marking */
|
|
@@ -68,7 +76,8 @@ export class BasePage {
|
|
|
68
76
|
/**
|
|
69
77
|
* Safely evaluate JS with pre-serialized arguments.
|
|
70
78
|
* Each key in `args` becomes a `const` declaration with JSON-serialized value,
|
|
71
|
-
*
|
|
79
|
+
* scoped to this invocation. Prevents injection and persistent-context lexical
|
|
80
|
+
* redeclarations by design.
|
|
72
81
|
*
|
|
73
82
|
* Usage:
|
|
74
83
|
* page.evaluateWithArgs(`(async () => { return sym; })()`, { sym: userInput })
|
|
@@ -76,18 +85,19 @@ export class BasePage {
|
|
|
76
85
|
async evaluateWithArgs(js, args) {
|
|
77
86
|
const declarations = Object.entries(args)
|
|
78
87
|
.map(([key, value]) => {
|
|
79
|
-
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
|
|
88
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) || EVALUATE_ARG_RESERVED_WORDS.has(key)) {
|
|
80
89
|
throw new Error(`evaluateWithArgs: invalid key "${key}"`);
|
|
81
90
|
}
|
|
82
91
|
return `const ${key} = ${JSON.stringify(value)};`;
|
|
83
92
|
})
|
|
84
93
|
.join('\n');
|
|
85
|
-
return this.evaluate(
|
|
94
|
+
return this.evaluate(`{\n${declarations}\n${js}\n}`);
|
|
86
95
|
}
|
|
87
96
|
async fetchJson(url, opts = {}) {
|
|
88
97
|
const request = {
|
|
89
98
|
url,
|
|
90
99
|
method: opts.method ?? 'GET',
|
|
100
|
+
referrer: opts.referrer,
|
|
91
101
|
headers: opts.headers ?? {},
|
|
92
102
|
body: opts.body,
|
|
93
103
|
hasBody: opts.body !== undefined,
|
|
@@ -105,6 +115,7 @@ export class BasePage {
|
|
|
105
115
|
headers,
|
|
106
116
|
signal: ctrl.signal,
|
|
107
117
|
};
|
|
118
|
+
if (request.referrer !== undefined) init.referrer = request.referrer;
|
|
108
119
|
if (request.hasBody) {
|
|
109
120
|
if (!Object.keys(headers).some((key) => key.toLowerCase() === 'content-type')) {
|
|
110
121
|
headers['Content-Type'] = 'application/json';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* by the navigate action and pass it to all subsequent commands. This ensures
|
|
9
9
|
* page-scoped operations target the correct page without guessing.
|
|
10
10
|
*/
|
|
11
|
-
import type { BrowserCookie, BrowserDownloadWaitResult, BrowserEvaluateFunction, ScreenshotOptions } from '../types.js';
|
|
11
|
+
import type { BrowserCookie, BrowserDownloadWaitOptions, BrowserDownloadWaitResult, BrowserEvaluateFunction, ScreenshotOptions } from '../types.js';
|
|
12
12
|
import { BasePage } from './base-page.js';
|
|
13
13
|
/**
|
|
14
14
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
@@ -57,7 +57,7 @@ export declare class Page extends BasePage {
|
|
|
57
57
|
screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
58
58
|
startNetworkCapture(pattern?: string): Promise<boolean>;
|
|
59
59
|
readNetworkCapture(): Promise<unknown[]>;
|
|
60
|
-
waitForDownload(pattern?: string, timeoutMs?: number): Promise<BrowserDownloadWaitResult>;
|
|
60
|
+
waitForDownload(pattern?: string, timeoutMs?: number, options?: BrowserDownloadWaitOptions): Promise<BrowserDownloadWaitResult>;
|
|
61
61
|
/**
|
|
62
62
|
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
63
63
|
* Chrome reads the files directly from the local filesystem, avoiding the
|
package/dist/src/browser/page.js
CHANGED
|
@@ -302,10 +302,12 @@ export class Page extends BasePage {
|
|
|
302
302
|
return [];
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
async waitForDownload(pattern = '', timeoutMs = 30_000) {
|
|
305
|
+
async waitForDownload(pattern = '', timeoutMs = 30_000, options) {
|
|
306
306
|
const result = await sendCommand('wait-download', {
|
|
307
307
|
pattern,
|
|
308
308
|
timeoutMs,
|
|
309
|
+
...(options?.includeRecent === undefined ? {} : { includeRecent: options.includeRecent }),
|
|
310
|
+
...(options?.startedAfterMs === undefined ? {} : { startedAfterMs: options.startedAfterMs }),
|
|
309
311
|
...this._cmdOpts(),
|
|
310
312
|
});
|
|
311
313
|
return result;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Flow: ArticleData → TurndownService → image download → frontmatter → .md file
|
|
7
7
|
*/
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
8
9
|
import * as fs from 'node:fs';
|
|
9
10
|
import * as path from 'node:path';
|
|
10
11
|
import TurndownService from 'turndown';
|
|
@@ -238,11 +239,38 @@ function defaultDetectImageExt(url) {
|
|
|
238
239
|
const extMatch = url.match(/\.(\w{3,4})(?:\?|$)/);
|
|
239
240
|
return extMatch ? extMatch[1] : 'jpg';
|
|
240
241
|
}
|
|
242
|
+
/** Random 8-char lowercase hex segment used in downloaded image filenames. */
|
|
243
|
+
function randomImageId() {
|
|
244
|
+
return crypto.randomBytes(4).toString('hex');
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build a collision-free `img_<8 hex>.<ext>` filename inside `imgDir`.
|
|
248
|
+
*
|
|
249
|
+
* `taken` guards against collisions between images downloaded concurrently in
|
|
250
|
+
* the same run, since those files do not exist on disk yet when the name is
|
|
251
|
+
* picked. The on-disk check covers files from earlier runs into the same dir.
|
|
252
|
+
*/
|
|
253
|
+
function buildImageFilename(imgDir, ext, taken) {
|
|
254
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
255
|
+
const filename = `img_${randomImageId()}.${ext}`;
|
|
256
|
+
if (taken.has(filename))
|
|
257
|
+
continue;
|
|
258
|
+
if (fs.existsSync(path.join(imgDir, filename)))
|
|
259
|
+
continue;
|
|
260
|
+
taken.add(filename);
|
|
261
|
+
return filename;
|
|
262
|
+
}
|
|
263
|
+
// Astronomically unlikely; fall back to a longer id rather than overwriting.
|
|
264
|
+
const filename = `img_${randomImageId()}${randomImageId()}.${ext}`;
|
|
265
|
+
taken.add(filename);
|
|
266
|
+
return filename;
|
|
267
|
+
}
|
|
241
268
|
async function downloadImages(imgUrls, imgDir, headers, detectExt) {
|
|
242
269
|
const urlMap = {};
|
|
243
270
|
if (imgUrls.length === 0)
|
|
244
271
|
return urlMap;
|
|
245
272
|
const detect = detectExt || defaultDetectImageExt;
|
|
273
|
+
const takenFilenames = new Set();
|
|
246
274
|
// Deduplicate image URLs
|
|
247
275
|
const seen = new Set();
|
|
248
276
|
const uniqueUrls = imgUrls.filter(url => {
|
|
@@ -253,13 +281,12 @@ async function downloadImages(imgUrls, imgDir, headers, detectExt) {
|
|
|
253
281
|
});
|
|
254
282
|
for (let i = 0; i < uniqueUrls.length; i += IMAGE_CONCURRENCY) {
|
|
255
283
|
const batch = uniqueUrls.slice(i, i + IMAGE_CONCURRENCY);
|
|
256
|
-
const results = await Promise.all(batch.map(async (rawUrl
|
|
257
|
-
const index = i + j + 1;
|
|
284
|
+
const results = await Promise.all(batch.map(async (rawUrl) => {
|
|
258
285
|
let imgUrl = rawUrl;
|
|
259
286
|
if (imgUrl.startsWith('//'))
|
|
260
287
|
imgUrl = `https:${imgUrl}`;
|
|
261
288
|
const ext = detect(imgUrl);
|
|
262
|
-
const filename =
|
|
289
|
+
const filename = buildImageFilename(imgDir, ext, takenFilenames);
|
|
263
290
|
const filepath = path.join(imgDir, filename);
|
|
264
291
|
try {
|
|
265
292
|
const result = await httpDownload(imgUrl, filepath, {
|
|
@@ -11,6 +11,7 @@ const SENSITIVE_HEADER_NAMES = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
const SENSITIVE_FIELD_PATTERN = /(password|passwd|pwd|token|secret|authorization|cookie|set-cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?id|csrf|xsrf)/i;
|
|
13
13
|
const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|fingerprint|password|auth|access_token|api_key|session_id|csrf|xsrf)=[^&]*/gi;
|
|
14
|
+
const ENCODED_SENSITIVE_QUERY_ASSIGNMENT = /(%26(?:token|key|secret|fingerprint|password|auth|access_token|api_key|session_id|csrf|xsrf)(?:%3D|=))(?:(?!%26|["'\s,;}&#]).)+|(\\u0026(?:token|key|secret|fingerprint|password|auth|access_token|api_key|session_id|csrf|xsrf)(?:%3D|=))(?:(?!\\u0026|["'\s,;}&#]).)+/gi;
|
|
14
15
|
function hasFingerprintFieldSegment(name) {
|
|
15
16
|
return name
|
|
16
17
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
@@ -38,7 +39,8 @@ export function redactText(text, opts = {}) {
|
|
|
38
39
|
let out = text
|
|
39
40
|
.replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
|
|
40
41
|
.replace(/(["'])(password|passwd|pwd|token|secret|fingerprint|api_key|apikey|access_token|session_id)\1\s*:\s*(["'])(.*?)\3/gi, '$1$2$1:$3[REDACTED]$3')
|
|
41
|
-
.replace(
|
|
42
|
+
.replace(ENCODED_SENSITIVE_QUERY_ASSIGNMENT, (_match, percentPrefix, escapedPrefix) => `${percentPrefix ?? escapedPrefix}[REDACTED]`)
|
|
43
|
+
.replace(/(token|secret|fingerprint|password|api_key|apikey|access_token|session_id)\s*[=:]\s*(?!['"]?\[REDACTED\](?=\\u0026|%26|['"\s,;}&#]|$))['"]?[^'"\s,;}&]+['"]?/gi, '$1=[REDACTED]')
|
|
42
44
|
.replace(/(cookie[=:]\s*)[^\n;]{3,}/gi, '$1[REDACTED]')
|
|
43
45
|
.replace(/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, '[REDACTED_JWT]');
|
|
44
46
|
if (out.length > max)
|
package/dist/src/types.d.ts
CHANGED
|
@@ -42,6 +42,10 @@ export interface BrowserDownloadWaitResult {
|
|
|
42
42
|
error?: string;
|
|
43
43
|
elapsedMs: number;
|
|
44
44
|
}
|
|
45
|
+
export interface BrowserDownloadWaitOptions {
|
|
46
|
+
includeRecent?: boolean;
|
|
47
|
+
startedAfterMs?: number;
|
|
48
|
+
}
|
|
45
49
|
export interface ScreenshotOptions {
|
|
46
50
|
format?: 'png' | 'jpeg';
|
|
47
51
|
quality?: number;
|
|
@@ -56,6 +60,7 @@ export interface ScreenshotOptions {
|
|
|
56
60
|
}
|
|
57
61
|
export interface FetchJsonOptions {
|
|
58
62
|
method?: string;
|
|
63
|
+
referrer?: string;
|
|
59
64
|
headers?: Record<string, string>;
|
|
60
65
|
body?: unknown;
|
|
61
66
|
timeoutMs?: number;
|
|
@@ -178,7 +183,7 @@ export interface IPage {
|
|
|
178
183
|
}): Promise<any>;
|
|
179
184
|
getFormState(): Promise<any>;
|
|
180
185
|
wait(options: number | WaitOptions): Promise<void>;
|
|
181
|
-
waitForDownload?(pattern?: string, timeoutMs?: number): Promise<BrowserDownloadWaitResult>;
|
|
186
|
+
waitForDownload?(pattern?: string, timeoutMs?: number, options?: BrowserDownloadWaitOptions): Promise<BrowserDownloadWaitResult>;
|
|
182
187
|
tabs(): Promise<any>;
|
|
183
188
|
closeTab?(target?: number | string): Promise<void>;
|
|
184
189
|
newTab?(url?: string): Promise<string | undefined>;
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|