@sovovs/bycli 2.0.0 → 2.1.1
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 +169 -0
- package/clis/twitter/search.js +3 -3
- package/clis/weixin/_wechat/args.js +48 -0
- package/clis/weixin/_wechat/article-content.js +53 -0
- package/clis/weixin/_wechat/article-service.js +124 -0
- package/clis/weixin/_wechat/auth-session.js +142 -0
- package/clis/weixin/_wechat/fingerprint.js +443 -0
- package/clis/weixin/_wechat/fixtures/articles-auth-expired.json +3 -0
- package/clis/weixin/_wechat/fixtures/articles-page.json +4 -0
- package/clis/weixin/_wechat/fixtures/search-auth-expired.json +4 -0
- package/clis/weixin/_wechat/fixtures/search-success.json +7 -0
- package/clis/weixin/_wechat/markdown.js +29 -0
- package/clis/weixin/_wechat/redact.js +405 -0
- package/clis/weixin/_wechat/save-service.js +175 -0
- package/clis/weixin/_wechat/search-biz.js +102 -0
- package/clis/weixin/_wechat/wechat-api.js +133 -0
- package/clis/weixin/accounts.js +38 -0
- package/clis/weixin/articles.js +35 -0
- package/clis/weixin/download.js +5 -47
- package/clis/weixin/save-articles.js +175 -0
- package/dist/src/browser/cdp.js +3 -0
- package/dist/src/browser/daemon-client.d.ts +2 -0
- package/dist/src/browser/daemon-client.js +1 -1
- package/dist/src/browser/extension-capabilities.d.ts +13 -0
- package/dist/src/browser/extension-capabilities.js +22 -0
- package/dist/src/browser/extension-capabilities.test.d.ts +1 -0
- package/dist/src/browser/extension-version-metadata.test.d.ts +1 -0
- package/dist/src/browser/page.d.ts +1 -0
- package/dist/src/browser/page.js +20 -1
- package/dist/src/build-manifest.js +4 -2
- package/dist/src/capabilityRouting.d.ts +3 -2
- package/dist/src/capabilityRouting.js +10 -2
- package/dist/src/cli.js +1 -1
- package/dist/src/commanderAdapter.js +5 -5
- package/dist/src/daemon.js +16 -0
- package/dist/src/discovery.d.ts +5 -0
- package/dist/src/discovery.js +12 -4
- package/dist/src/discovery.test.d.ts +1 -0
- package/dist/src/download/article-download.d.ts +6 -0
- package/dist/src/download/article-download.js +78 -17
- package/dist/src/download/wechat-article.d.ts +8 -0
- package/dist/src/download/wechat-article.js +137 -0
- package/dist/src/download/wechat-article.test.d.ts +1 -0
- package/dist/src/execution.d.ts +5 -0
- package/dist/src/execution.js +269 -50
- package/dist/src/help.js +8 -8
- package/dist/src/manifest-schema.d.ts +9 -0
- package/dist/src/manifest-schema.js +162 -0
- package/dist/src/manifest-schema.test.d.ts +1 -0
- package/dist/src/manifest-types.d.ts +1 -1
- package/dist/src/observation/redaction.js +10 -4
- package/dist/src/recorder/highlevel/verify.d.ts +3 -0
- package/dist/src/recorder/highlevel/verify.js +4 -0
- package/dist/src/recorder/highlevel/verify.test.d.ts +1 -0
- package/dist/src/recorder/runner/runner-port.js +1 -0
- package/dist/src/recorder/runner/verify-runner-main.d.ts +23 -7
- package/dist/src/recorder/runner/verify-runner-main.js +92 -19
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry-api.types.test.d.ts +1 -0
- package/dist/src/registry-transaction.d.ts +42 -0
- package/dist/src/registry-transaction.js +194 -0
- package/dist/src/registry-transaction.test.d.ts +1 -0
- package/dist/src/registry.d.ts +58 -16
- package/dist/src/registry.js +131 -15
- package/dist/src/serialization.d.ts +1 -1
- package/dist/src/serialization.js +3 -3
- package/dist/src/types.d.ts +2 -0
- package/dist/src/weixin-built-in-docs.test.d.ts +1 -0
- package/package.json +7 -3
- package/scripts/check-package-install.mjs +71 -0
- package/scripts/recorder.sh +0 -186
|
@@ -10,7 +10,13 @@ const SENSITIVE_HEADER_NAMES = new Set([
|
|
|
10
10
|
'x-xsrf-token',
|
|
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
|
-
const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|password|auth|access_token|api_key|session_id|csrf|xsrf)=[^&]*/gi;
|
|
13
|
+
const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|fingerprint|password|auth|access_token|api_key|session_id|csrf|xsrf)=[^&]*/gi;
|
|
14
|
+
function hasFingerprintFieldSegment(name) {
|
|
15
|
+
return name
|
|
16
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
17
|
+
.split(/[-_.\s]+/)
|
|
18
|
+
.some((segment) => segment.toLowerCase() === 'fingerprint');
|
|
19
|
+
}
|
|
14
20
|
export function redactUrl(url) {
|
|
15
21
|
return url.replace(SENSITIVE_URL_PARAMS, '$1$2=[REDACTED]');
|
|
16
22
|
}
|
|
@@ -31,8 +37,8 @@ export function redactText(text, opts = {}) {
|
|
|
31
37
|
const max = opts.maxStringLength ?? 50_000;
|
|
32
38
|
let out = text
|
|
33
39
|
.replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
|
|
34
|
-
.replace(/(["'])(password|passwd|pwd|token|secret|api_key|apikey|access_token|session_id)\1\s*:\s*(["'])(.*?)\3/gi, '$1$2$1:$3[REDACTED]$3')
|
|
35
|
-
.replace(/(token|secret|password|api_key|apikey|access_token|session_id)[=:]\s*['"]?[^'"\s,;}&]+['"]?/gi, '$1=[REDACTED]')
|
|
40
|
+
.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(/(token|secret|fingerprint|password|api_key|apikey|access_token|session_id)\s*[=:]\s*['"]?[^'"\s,;}&]+['"]?/gi, '$1=[REDACTED]')
|
|
36
42
|
.replace(/(cookie[=:]\s*)[^\n;]{3,}/gi, '$1[REDACTED]')
|
|
37
43
|
.replace(/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, '[REDACTED_JWT]');
|
|
38
44
|
if (out.length > max)
|
|
@@ -41,7 +47,7 @@ export function redactText(text, opts = {}) {
|
|
|
41
47
|
}
|
|
42
48
|
export function redactValue(value, opts = {}, keyHint, depth = 0) {
|
|
43
49
|
const allow = new Set((opts.allowlist ?? []).map((key) => key.toLowerCase()));
|
|
44
|
-
if (keyHint && SENSITIVE_FIELD_PATTERN.test(keyHint) && !allow.has(keyHint.toLowerCase())) {
|
|
50
|
+
if (keyHint && (SENSITIVE_FIELD_PATTERN.test(keyHint) || hasFingerprintFieldSegment(keyHint)) && !allow.has(keyHint.toLowerCase())) {
|
|
45
51
|
return DEFAULT_REDACTION;
|
|
46
52
|
}
|
|
47
53
|
if (typeof value === 'string') {
|
|
@@ -29,6 +29,8 @@ export interface VerifyInput {
|
|
|
29
29
|
trace?: 'off' | 'retain-on-failure' | 'always';
|
|
30
30
|
/** N3:显式 adapter 路径 override —— verify 录制器 LLM 生成的临时草稿(不在 clis/),缺省按 name 派生。 */
|
|
31
31
|
adapterPath?: string;
|
|
32
|
+
/** Optional lowercase SHA-256 expected for the exact adapter bytes the runner will execute. */
|
|
33
|
+
expectedSourceSha256?: string;
|
|
32
34
|
}
|
|
33
35
|
/** The runner boundary (08). M6 provides the real child-process implementation. */
|
|
34
36
|
export interface RunnerPort {
|
|
@@ -42,6 +44,7 @@ export interface RunnerPort {
|
|
|
42
44
|
trace: string;
|
|
43
45
|
/** N3: explicit adapter path override (recorder draft verify); default = name→clis path. */
|
|
44
46
|
adapterPath?: string;
|
|
47
|
+
expectedSourceSha256?: string;
|
|
45
48
|
}): Promise<{
|
|
46
49
|
requestId: string;
|
|
47
50
|
}>;
|
|
@@ -49,6 +49,9 @@ export async function verifyAdapter(input, sessionHmacKey, runner) {
|
|
|
49
49
|
}
|
|
50
50
|
adapterPath = abs;
|
|
51
51
|
}
|
|
52
|
+
if (input.expectedSourceSha256 !== undefined && !/^[0-9a-f]{64}$/.test(input.expectedSourceSha256)) {
|
|
53
|
+
return { ok: false, errorCode: 'validation_failed', reason: 'expectedSourceSha256 must be 64 lowercase hex characters' };
|
|
54
|
+
}
|
|
52
55
|
const port = runner ?? defaultRunnerPort();
|
|
53
56
|
const rawSeedArgs = input.executionSeedArgs ?? {};
|
|
54
57
|
const evidenceSeedArgs = deriveEvidenceSeedArgs(rawSeedArgs, sessionHmacKey);
|
|
@@ -61,6 +64,7 @@ export async function verifyAdapter(input, sessionHmacKey, runner) {
|
|
|
61
64
|
fixture: input.fixture ?? 'ignore',
|
|
62
65
|
trace: input.trace ?? 'retain-on-failure',
|
|
63
66
|
adapterPath, // N3: validated draft path override (undefined → name→clis)
|
|
67
|
+
expectedSourceSha256: input.expectedSourceSha256,
|
|
64
68
|
});
|
|
65
69
|
return { ok: true, requestId };
|
|
66
70
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -252,6 +252,7 @@ export function createRunnerPort(opts = {}) {
|
|
|
252
252
|
requestId,
|
|
253
253
|
name: input.name,
|
|
254
254
|
adapterPath: input.adapterPath ?? resolveAdapterPath(input.name),
|
|
255
|
+
expectedSourceSha256: input.expectedSourceSha256,
|
|
255
256
|
executionSeedArgs: input.rawSeedArgs, // raw → input.json only
|
|
256
257
|
fixture: input.fixture,
|
|
257
258
|
trace: input.trace,
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* adapters report not-yet (their Page must come from the daemon — M6b). The spawn /
|
|
12
12
|
* input.json security / timeout / byte-cap mechanism lives in `runner-port.ts`.
|
|
13
13
|
*
|
|
14
|
-
* SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600)
|
|
15
|
-
*
|
|
14
|
+
* SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600), then are
|
|
15
|
+
* defaulted, coerced, and validated before execution. Neither raw nor prepared arguments
|
|
16
|
+
* are echoed into the emitted result/started events.
|
|
16
17
|
*/
|
|
17
|
-
import { type BrowserCliCommand, type CliCommand } from '../../registry.js';
|
|
18
|
+
import { type BrowserCliCommand, type CliCommand, type ConditionalBrowserCliCommand } from '../../registry.js';
|
|
18
19
|
import type { RunnerEvent, RunnerResultEvent } from '@sovovs/bycli-recorder-core';
|
|
19
20
|
/** Parsed input.json (written by RunnerPort; raw seed args are execution-only). */
|
|
20
21
|
export interface RunnerInput {
|
|
@@ -22,9 +23,11 @@ export interface RunnerInput {
|
|
|
22
23
|
name: string;
|
|
23
24
|
/** Resolved adapter module file to import. */
|
|
24
25
|
adapterPath: string;
|
|
26
|
+
/** Expected hash supplied by the caller; mismatch means the captured module is never loaded. */
|
|
27
|
+
expectedSourceSha256?: string;
|
|
25
28
|
/** Browser profile contextId for browser adapters (M6b). Omitted → daemon default profile. */
|
|
26
29
|
contextId?: string;
|
|
27
|
-
/** Raw seed args —
|
|
30
|
+
/** Raw seed args — prepared before resolver/adapter calls and never echoed into events. */
|
|
28
31
|
executionSeedArgs?: Record<string, unknown>;
|
|
29
32
|
fixture?: 'ignore' | 'match' | 'update';
|
|
30
33
|
trace?: 'off' | 'retain-on-failure' | 'always';
|
|
@@ -46,13 +49,26 @@ export declare function installRunnerBackstops(maxRuntimeMs: number): void;
|
|
|
46
49
|
* Load an adapter by importing its module (which registers via `cli()`), then look it up
|
|
47
50
|
* in the registry by name. Mirrors execution.ts's lazy-import pattern (118-135).
|
|
48
51
|
*/
|
|
49
|
-
export
|
|
52
|
+
export interface AdapterSourceSnapshot {
|
|
53
|
+
canonicalUrl: string;
|
|
54
|
+
source: ArrayBuffer;
|
|
55
|
+
sourceSha256: string;
|
|
56
|
+
}
|
|
57
|
+
/** Read the main module once. The same exact bytes are hashed and transferred to the ESM loader. */
|
|
58
|
+
export declare function captureAdapterSource(adapterPath: string): AdapterSourceSnapshot;
|
|
59
|
+
/** Import exactly the captured main-module bytes while preserving its canonical URL as import base. */
|
|
60
|
+
export declare function loadAdapterSnapshot(snapshot: AdapterSourceSnapshot, name: string): Promise<CliCommand | undefined>;
|
|
61
|
+
export interface VerifyRunnerDependencies {
|
|
62
|
+
capture?: (adapterPath: string) => AdapterSourceSnapshot | Promise<AdapterSourceSnapshot>;
|
|
63
|
+
load?: (snapshot: AdapterSourceSnapshot, name: string) => Promise<CliCommand | undefined>;
|
|
64
|
+
browserRunner?: BrowserAdapterRunner;
|
|
65
|
+
}
|
|
50
66
|
/**
|
|
51
67
|
* The browser-adapter execution seam (M6b). A browser adapter's `func` needs an IPage; the
|
|
52
68
|
* default implementation connects BACK to the running daemon for one. Injectable so unit
|
|
53
69
|
* tests can exercise executeAdapterForVerify without a real daemon/browser.
|
|
54
70
|
*/
|
|
55
|
-
export type BrowserAdapterRunner = (command: BrowserCliCommand, opts: {
|
|
71
|
+
export type BrowserAdapterRunner = (command: BrowserCliCommand | ConditionalBrowserCliCommand, opts: {
|
|
56
72
|
seedArgs: Record<string, unknown>;
|
|
57
73
|
contextId?: string;
|
|
58
74
|
preNavUrl: string | null;
|
|
@@ -84,7 +100,7 @@ export declare function executeAdapterForVerify(command: CliCommand | undefined,
|
|
|
84
100
|
* tests); `load` is injected so unit tests can supply an in-memory command. Never throws —
|
|
85
101
|
* any failure becomes a terminal result so the parent always sees one and only one.
|
|
86
102
|
*/
|
|
87
|
-
export declare function runVerifyRunner(input: RunnerInput, emit: (event: RunnerEvent) => void,
|
|
103
|
+
export declare function runVerifyRunner(input: RunnerInput, emit: (event: RunnerEvent) => void, dependencies?: VerifyRunnerDependencies): Promise<void>;
|
|
88
104
|
/**
|
|
89
105
|
* Entry point for `bycli internal verify-runner --jsonl --request-id … --name … --input …`.
|
|
90
106
|
* Writes JSONL events to the dedicated protocol fd (`--protocol-fd`, set by the parent RunnerPort
|
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
* adapters report not-yet (their Page must come from the daemon — M6b). The spawn /
|
|
12
12
|
* input.json security / timeout / byte-cap mechanism lives in `runner-port.ts`.
|
|
13
13
|
*
|
|
14
|
-
* SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600)
|
|
15
|
-
*
|
|
14
|
+
* SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600), then are
|
|
15
|
+
* defaulted, coerced, and validated before execution. Neither raw nor prepared arguments
|
|
16
|
+
* are echoed into the emitted result/started events.
|
|
16
17
|
*/
|
|
17
18
|
import * as fs from 'node:fs';
|
|
18
|
-
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
20
|
+
import { register } from 'node:module';
|
|
19
21
|
import { pathToFileURL } from 'node:url';
|
|
20
|
-
import { getRegistry } from '../../registry.js';
|
|
22
|
+
import { getRegistry, } from '../../registry.js';
|
|
23
|
+
import { prepareCommandArgsOrThrowArgumentError } from '../../execution.js';
|
|
21
24
|
// ── Lease cleanup on signal / orphan watchdog (Codex #4, #7) ────────────────
|
|
22
25
|
// A browser adapter holds a daemon-side tab lease. If the parent SIGTERMs/cancels this child
|
|
23
26
|
// (timeout/cancel) or this child is orphaned (parent crashed → no SIGTERM, and on win32 the reaper
|
|
@@ -76,14 +79,48 @@ function fieldCountOf(rows) {
|
|
|
76
79
|
}
|
|
77
80
|
return undefined;
|
|
78
81
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
export
|
|
84
|
-
|
|
82
|
+
const SNAPSHOT_LOADER_URL = `data:text/javascript,${encodeURIComponent(`
|
|
83
|
+
let targetSpecifier = '';
|
|
84
|
+
let targetUrl = '';
|
|
85
|
+
let source = new ArrayBuffer(0);
|
|
86
|
+
export function initialize(data) {
|
|
87
|
+
targetSpecifier = data.targetSpecifier;
|
|
88
|
+
targetUrl = data.targetUrl;
|
|
89
|
+
source = data.source;
|
|
90
|
+
}
|
|
91
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
92
|
+
if (specifier === targetSpecifier) {
|
|
93
|
+
return { url: targetUrl, shortCircuit: true };
|
|
94
|
+
}
|
|
95
|
+
return nextResolve(specifier, context);
|
|
96
|
+
}
|
|
97
|
+
export async function load(url, context, nextLoad) {
|
|
98
|
+
if (url === targetUrl) {
|
|
99
|
+
return { format: 'module', shortCircuit: true, source: new Uint8Array(source) };
|
|
100
|
+
}
|
|
101
|
+
return nextLoad(url, context);
|
|
102
|
+
}
|
|
103
|
+
`)}`;
|
|
104
|
+
/** Read the main module once. The same exact bytes are hashed and transferred to the ESM loader. */
|
|
105
|
+
export function captureAdapterSource(adapterPath) {
|
|
106
|
+
const canonicalPath = fs.realpathSync(adapterPath);
|
|
107
|
+
const bytes = fs.readFileSync(canonicalPath);
|
|
108
|
+
const source = Uint8Array.from(bytes).buffer;
|
|
109
|
+
return {
|
|
110
|
+
canonicalUrl: pathToFileURL(canonicalPath).href,
|
|
111
|
+
source,
|
|
112
|
+
sourceSha256: createHash('sha256').update(new Uint8Array(source)).digest('hex'),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Import exactly the captured main-module bytes while preserving its canonical URL as import base. */
|
|
116
|
+
export async function loadAdapterSnapshot(snapshot, name) {
|
|
85
117
|
try {
|
|
86
|
-
|
|
118
|
+
const targetSpecifier = `bycli-verify-snapshot:${randomUUID()}`;
|
|
119
|
+
register(SNAPSHOT_LOADER_URL, import.meta.url, {
|
|
120
|
+
data: { targetSpecifier, targetUrl: snapshot.canonicalUrl, source: snapshot.source },
|
|
121
|
+
transferList: [snapshot.source],
|
|
122
|
+
});
|
|
123
|
+
await import(targetSpecifier);
|
|
87
124
|
}
|
|
88
125
|
catch (e) {
|
|
89
126
|
// The adapter module's top-level code threw during evaluation (a SyntaxError, or a deliberate
|
|
@@ -131,9 +168,24 @@ export async function executeAdapterForVerify(command, opts) {
|
|
|
131
168
|
return { ok: false, data: { stage: 'load', trace }, error: { code: 'runner_protocol_error', message: `adapter "${opts.name}" has no func` } };
|
|
132
169
|
}
|
|
133
170
|
try {
|
|
171
|
+
const preparedArgs = prepareCommandArgsOrThrowArgumentError(command, opts.seedArgs);
|
|
134
172
|
let rows;
|
|
135
173
|
if (command.browser === false) {
|
|
136
|
-
rows = await command.func(
|
|
174
|
+
rows = await command.func(preparedArgs, false);
|
|
175
|
+
}
|
|
176
|
+
else if (command.browser === 'conditional') {
|
|
177
|
+
const browserRequired = Boolean(command.requiresBrowser(preparedArgs));
|
|
178
|
+
if (!browserRequired) {
|
|
179
|
+
rows = await command.func(null, preparedArgs, false);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const runner = opts.browserRunner ?? defaultBrowserAdapterRunner;
|
|
183
|
+
rows = await runner(command, {
|
|
184
|
+
seedArgs: preparedArgs,
|
|
185
|
+
contextId: opts.contextId,
|
|
186
|
+
preNavUrl: typeof command.navigateBefore === 'string' ? command.navigateBefore : null,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
137
189
|
}
|
|
138
190
|
else {
|
|
139
191
|
// M6b: browser adapter connects back to the daemon for a Page. navigateBefore is a
|
|
@@ -141,7 +193,7 @@ export async function executeAdapterForVerify(command, opts) {
|
|
|
141
193
|
// `true`/`undefined` mean "adapter handles its own navigation".
|
|
142
194
|
const runner = opts.browserRunner ?? defaultBrowserAdapterRunner;
|
|
143
195
|
rows = await runner(command, {
|
|
144
|
-
seedArgs:
|
|
196
|
+
seedArgs: preparedArgs,
|
|
145
197
|
contextId: opts.contextId,
|
|
146
198
|
preNavUrl: typeof command.navigateBefore === 'string' ? command.navigateBefore : null,
|
|
147
199
|
});
|
|
@@ -172,29 +224,50 @@ export async function executeAdapterForVerify(command, opts) {
|
|
|
172
224
|
* tests); `load` is injected so unit tests can supply an in-memory command. Never throws —
|
|
173
225
|
* any failure becomes a terminal result so the parent always sees one and only one.
|
|
174
226
|
*/
|
|
175
|
-
export async function runVerifyRunner(input, emit,
|
|
227
|
+
export async function runVerifyRunner(input, emit, dependencies = {}) {
|
|
176
228
|
emit({ type: 'started', requestId: input.requestId, pid: process.pid, stage: 'load' });
|
|
229
|
+
let sourceSha256;
|
|
177
230
|
try {
|
|
178
|
-
const
|
|
231
|
+
const capture = dependencies.capture ?? captureAdapterSource;
|
|
232
|
+
const load = dependencies.load ?? loadAdapterSnapshot;
|
|
233
|
+
const snapshot = await capture(input.adapterPath);
|
|
234
|
+
sourceSha256 = snapshot.sourceSha256;
|
|
235
|
+
if (input.expectedSourceSha256 !== undefined && input.expectedSourceSha256 !== sourceSha256) {
|
|
236
|
+
emit({
|
|
237
|
+
type: 'result', requestId: input.requestId, ok: false,
|
|
238
|
+
data: { stage: 'load', sourceSha256 },
|
|
239
|
+
error: { code: 'source_hash_mismatch', message: 'adapter source hash does not match expected source' },
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const command = await load(snapshot, input.name);
|
|
179
244
|
const r = await executeAdapterForVerify(command, {
|
|
180
245
|
name: input.name,
|
|
181
246
|
fixture: input.fixture,
|
|
182
247
|
trace: input.trace,
|
|
183
248
|
seedArgs: input.executionSeedArgs ?? {},
|
|
184
249
|
contextId: input.contextId,
|
|
185
|
-
browserRunner,
|
|
250
|
+
browserRunner: dependencies.browserRunner,
|
|
251
|
+
});
|
|
252
|
+
emit({
|
|
253
|
+
type: 'result', requestId: input.requestId, ok: r.ok,
|
|
254
|
+
data: { ...r.data, sourceSha256 },
|
|
255
|
+
error: r.ok ? null : r.error,
|
|
186
256
|
});
|
|
187
|
-
emit({ type: 'result', requestId: input.requestId, ok: r.ok, data: r.data, error: r.ok ? null : r.error });
|
|
188
257
|
}
|
|
189
258
|
catch (e) {
|
|
190
|
-
// Load failure → single terminal result. An adapter-evaluation error (tagged by
|
|
259
|
+
// Load failure → single terminal result. An adapter-evaluation error (tagged by loadAdapterSnapshot)
|
|
191
260
|
// is adapter-controlled and may echo adapter-file contents, so its message is redacted; a
|
|
192
261
|
// runner-side failure (bad path / resolve) is runner-generated and surfaces verbatim (Codex M7c).
|
|
193
262
|
const adapterEval = e?.adapterEvaluation === true;
|
|
194
263
|
const message = adapterEval ? REDACTED_ADAPTER_LOAD_MESSAGE : (e instanceof Error ? e.message : String(e));
|
|
195
264
|
emit({
|
|
196
265
|
type: 'result', requestId: input.requestId, ok: false,
|
|
197
|
-
data: {
|
|
266
|
+
data: {
|
|
267
|
+
stage: 'load',
|
|
268
|
+
...(sourceSha256 === undefined ? {} : { sourceSha256 }),
|
|
269
|
+
trace: { policy: input.trace ?? 'retain-on-failure', retained: false, path: null },
|
|
270
|
+
},
|
|
198
271
|
error: { code: 'adapter_runtime_error', message, hint: 'adapter failed to load' },
|
|
199
272
|
});
|
|
200
273
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* plugins are dynamically imported during discoverPlugins().
|
|
8
8
|
*/
|
|
9
9
|
export { cli, Strategy, getRegistry, fullName, registerCommand } from './registry.js';
|
|
10
|
-
export type { CliCommand, Arg, CliOptions, CommandArgs, SiteSessionMode } from './registry.js';
|
|
10
|
+
export type { CliCommand, Arg, CliOptions, CommandArgs, SiteSessionMode, BrowserDeclaration, BrowserRequirementResolver, ConditionalBrowserCommandFunc, NormalizedBrowserRequirement, } from './registry.js';
|
|
11
11
|
export type { IPage } from './types.js';
|
|
12
12
|
export { onStartup, onBeforeExecute, onAfterExecute } from './hooks.js';
|
|
13
13
|
export type { HookFn, HookContext, HookName } from './hooks.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CliCommand } from './registry.js';
|
|
2
|
+
interface RegistrySlot {
|
|
3
|
+
present: boolean;
|
|
4
|
+
value: CliCommand | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface RegistryTransactionWrite {
|
|
7
|
+
key: string;
|
|
8
|
+
before: RegistrySlot;
|
|
9
|
+
after: RegistrySlot;
|
|
10
|
+
group: number;
|
|
11
|
+
beforeRevision: number;
|
|
12
|
+
afterRevision: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RegistryTransaction {
|
|
15
|
+
readonly writes: RegistryTransactionWrite[];
|
|
16
|
+
readonly id: number;
|
|
17
|
+
active: boolean;
|
|
18
|
+
currentGroup: number | undefined;
|
|
19
|
+
finalized: boolean;
|
|
20
|
+
readonly finalizedGroups: Set<number>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createRegistryTransaction(): RegistryTransaction;
|
|
23
|
+
export declare function closeRegistryTransaction(transaction: RegistryTransaction): void;
|
|
24
|
+
export declare function runRegistryTransaction<T>(transaction: RegistryTransaction, operation: () => Promise<T>): Promise<T>;
|
|
25
|
+
export declare function withRegistryMutationGroup<T>(operation: () => T): T;
|
|
26
|
+
export declare function recordRegistryMutation(key: string, before: RegistrySlot, after: RegistrySlot): void;
|
|
27
|
+
/**
|
|
28
|
+
* Keys with revision ownership, including absent-key tombstones.
|
|
29
|
+
*
|
|
30
|
+
* Tombstones intentionally remain until a later mutation or rollback supersedes
|
|
31
|
+
* them. Pruning an absent revision while an overlapping transaction may still
|
|
32
|
+
* reference it would allow an older rollback to resurrect the key.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registryMutationKeys(): string[];
|
|
35
|
+
export declare function pruneRegistryMutationKey(key: string, registry: ReadonlyMap<string, CliCommand>): void;
|
|
36
|
+
export declare function finalizeRegistryTransaction(transaction: RegistryTransaction, registry: ReadonlyMap<string, CliCommand>, groups?: ReadonlySet<number>): void;
|
|
37
|
+
export declare function capturedRegistryValue(transaction: RegistryTransaction, key: string): RegistrySlot;
|
|
38
|
+
export declare function capturedRegistryValues(transaction: RegistryTransaction): Map<string, CliCommand>;
|
|
39
|
+
export declare function transactionGroupsForKey(transaction: RegistryTransaction, key: string): Set<number>;
|
|
40
|
+
export declare function rollbackRegistryTransaction(transaction: RegistryTransaction, registry: Map<string, CliCommand>, groups?: ReadonlySet<number>): void;
|
|
41
|
+
export declare function resetRegistryTransactionStateForTests(): void;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const TRANSACTION_STATE_KEY = Symbol.for('@sovovs/bycli/registry-transaction-state');
|
|
3
|
+
const globalState = globalThis;
|
|
4
|
+
const state = globalState[TRANSACTION_STATE_KEY] ??= {
|
|
5
|
+
storage: new AsyncLocalStorage(),
|
|
6
|
+
transactionCounter: 0,
|
|
7
|
+
groupCounter: 0,
|
|
8
|
+
revisionCounter: 0,
|
|
9
|
+
revisions: new Map(),
|
|
10
|
+
transactions: new Map(),
|
|
11
|
+
owners: new Map(),
|
|
12
|
+
};
|
|
13
|
+
state.transactions ??= new Map();
|
|
14
|
+
state.owners ??= new Map();
|
|
15
|
+
export function createRegistryTransaction() {
|
|
16
|
+
const transaction = {
|
|
17
|
+
writes: [],
|
|
18
|
+
id: ++state.transactionCounter,
|
|
19
|
+
active: false,
|
|
20
|
+
currentGroup: undefined,
|
|
21
|
+
finalized: false,
|
|
22
|
+
finalizedGroups: new Set(),
|
|
23
|
+
};
|
|
24
|
+
state.transactions.set(transaction.id, transaction);
|
|
25
|
+
return transaction;
|
|
26
|
+
}
|
|
27
|
+
export function closeRegistryTransaction(transaction) {
|
|
28
|
+
transaction.active = false;
|
|
29
|
+
transaction.currentGroup = undefined;
|
|
30
|
+
}
|
|
31
|
+
export async function runRegistryTransaction(transaction, operation) {
|
|
32
|
+
if (transaction.finalized) {
|
|
33
|
+
throw new Error('Adapter registration transaction is finalized');
|
|
34
|
+
}
|
|
35
|
+
transaction.active = true;
|
|
36
|
+
return state.storage.run(transaction, async () => {
|
|
37
|
+
try {
|
|
38
|
+
return await operation();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
closeRegistryTransaction(transaction);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function withRegistryMutationGroup(operation) {
|
|
46
|
+
const transaction = state.storage.getStore();
|
|
47
|
+
if (!transaction)
|
|
48
|
+
return operation();
|
|
49
|
+
if (!transaction.active) {
|
|
50
|
+
throw new Error('Adapter registration transaction is closed; delayed registration is not allowed');
|
|
51
|
+
}
|
|
52
|
+
const previousGroup = transaction.currentGroup;
|
|
53
|
+
transaction.currentGroup = ++state.groupCounter;
|
|
54
|
+
try {
|
|
55
|
+
return operation();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
transaction.currentGroup = previousGroup;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function recordRegistryMutation(key, before, after) {
|
|
62
|
+
const transaction = state.storage.getStore();
|
|
63
|
+
if (transaction && !transaction.active) {
|
|
64
|
+
throw new Error('Adapter registration transaction is closed; delayed registration is not allowed');
|
|
65
|
+
}
|
|
66
|
+
if (transaction?.finalized) {
|
|
67
|
+
throw new Error('Adapter registration transaction is finalized');
|
|
68
|
+
}
|
|
69
|
+
const beforeRevision = state.revisions.get(key) ?? 0;
|
|
70
|
+
const afterRevision = ++state.revisionCounter;
|
|
71
|
+
state.revisions.set(key, afterRevision);
|
|
72
|
+
if (!transaction)
|
|
73
|
+
return;
|
|
74
|
+
const group = transaction.currentGroup ?? ++state.groupCounter;
|
|
75
|
+
transaction.writes.push({
|
|
76
|
+
key,
|
|
77
|
+
before,
|
|
78
|
+
after,
|
|
79
|
+
group,
|
|
80
|
+
beforeRevision,
|
|
81
|
+
afterRevision,
|
|
82
|
+
});
|
|
83
|
+
const owners = state.owners.get(key) ?? new Set();
|
|
84
|
+
owners.add(transaction.id);
|
|
85
|
+
state.owners.set(key, owners);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Keys with revision ownership, including absent-key tombstones.
|
|
89
|
+
*
|
|
90
|
+
* Tombstones intentionally remain until a later mutation or rollback supersedes
|
|
91
|
+
* them. Pruning an absent revision while an overlapping transaction may still
|
|
92
|
+
* reference it would allow an older rollback to resurrect the key.
|
|
93
|
+
*/
|
|
94
|
+
export function registryMutationKeys() {
|
|
95
|
+
return [...state.revisions.keys()];
|
|
96
|
+
}
|
|
97
|
+
export function pruneRegistryMutationKey(key, registry) {
|
|
98
|
+
if (registry.has(key))
|
|
99
|
+
return;
|
|
100
|
+
if ((state.owners.get(key)?.size ?? 0) > 0)
|
|
101
|
+
return;
|
|
102
|
+
state.revisions.delete(key);
|
|
103
|
+
}
|
|
104
|
+
export function finalizeRegistryTransaction(transaction, registry, groups) {
|
|
105
|
+
if (transaction.active) {
|
|
106
|
+
throw new Error('Cannot finalize an active adapter registration transaction');
|
|
107
|
+
}
|
|
108
|
+
const selectedGroups = groups ?? new Set(transaction.writes.map(write => write.group));
|
|
109
|
+
const newGroups = new Set([...selectedGroups].filter(group => !transaction.finalizedGroups.has(group)));
|
|
110
|
+
const affectedKeys = new Set(transaction.writes
|
|
111
|
+
.filter(write => newGroups.has(write.group))
|
|
112
|
+
.map(write => write.key));
|
|
113
|
+
for (const group of newGroups)
|
|
114
|
+
transaction.finalizedGroups.add(group);
|
|
115
|
+
for (const key of affectedKeys) {
|
|
116
|
+
const stillOwned = transaction.writes.some(write => write.key === key && !transaction.finalizedGroups.has(write.group));
|
|
117
|
+
if (!stillOwned) {
|
|
118
|
+
const owners = state.owners.get(key);
|
|
119
|
+
owners?.delete(transaction.id);
|
|
120
|
+
if (owners?.size === 0)
|
|
121
|
+
state.owners.delete(key);
|
|
122
|
+
}
|
|
123
|
+
pruneRegistryMutationKey(key, registry);
|
|
124
|
+
}
|
|
125
|
+
const hasUnfinalizedWrites = transaction.writes.some(write => !transaction.finalizedGroups.has(write.group));
|
|
126
|
+
if (!hasUnfinalizedWrites) {
|
|
127
|
+
transaction.finalized = true;
|
|
128
|
+
state.transactions.delete(transaction.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function capturedRegistryValue(transaction, key) {
|
|
132
|
+
for (let index = transaction.writes.length - 1; index >= 0; index -= 1) {
|
|
133
|
+
const write = transaction.writes[index];
|
|
134
|
+
if (write.key === key)
|
|
135
|
+
return write.after;
|
|
136
|
+
}
|
|
137
|
+
return { present: false, value: undefined };
|
|
138
|
+
}
|
|
139
|
+
export function capturedRegistryValues(transaction) {
|
|
140
|
+
const keys = new Set(transaction.writes.map(write => write.key));
|
|
141
|
+
const values = new Map();
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
const captured = capturedRegistryValue(transaction, key);
|
|
144
|
+
if (captured.present && captured.value)
|
|
145
|
+
values.set(key, captured.value);
|
|
146
|
+
}
|
|
147
|
+
return values;
|
|
148
|
+
}
|
|
149
|
+
export function transactionGroupsForKey(transaction, key) {
|
|
150
|
+
return new Set(transaction.writes.filter(write => write.key === key).map(write => write.group));
|
|
151
|
+
}
|
|
152
|
+
export function rollbackRegistryTransaction(transaction, registry, groups) {
|
|
153
|
+
const groupIds = [...new Set(transaction.writes.map(write => write.group))]
|
|
154
|
+
.filter(group => !transaction.finalizedGroups.has(group) && (!groups || groups.has(group)))
|
|
155
|
+
.sort((a, b) => b - a);
|
|
156
|
+
for (const group of groupIds) {
|
|
157
|
+
const writesByKey = new Map();
|
|
158
|
+
for (const write of transaction.writes) {
|
|
159
|
+
if (write.group !== group)
|
|
160
|
+
continue;
|
|
161
|
+
const writes = writesByKey.get(write.key) ?? [];
|
|
162
|
+
writes.push(write);
|
|
163
|
+
writesByKey.set(write.key, writes);
|
|
164
|
+
}
|
|
165
|
+
for (const writes of writesByKey.values()) {
|
|
166
|
+
const finalWrite = writes.at(-1);
|
|
167
|
+
if ((state.revisions.get(finalWrite.key) ?? 0) !== finalWrite.afterRevision)
|
|
168
|
+
continue;
|
|
169
|
+
for (let index = writes.length - 1; index >= 0; index -= 1) {
|
|
170
|
+
const write = writes[index];
|
|
171
|
+
if (write.before.present)
|
|
172
|
+
Map.prototype.set.call(registry, write.key, write.before.value);
|
|
173
|
+
else
|
|
174
|
+
Map.prototype.delete.call(registry, write.key);
|
|
175
|
+
if (write.beforeRevision === 0)
|
|
176
|
+
state.revisions.delete(write.key);
|
|
177
|
+
else
|
|
178
|
+
state.revisions.set(write.key, write.beforeRevision);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finalizeRegistryTransaction(transaction, registry, new Set(groupIds));
|
|
183
|
+
}
|
|
184
|
+
export function resetRegistryTransactionStateForTests() {
|
|
185
|
+
for (const transaction of state.transactions.values()) {
|
|
186
|
+
closeRegistryTransaction(transaction);
|
|
187
|
+
transaction.finalized = true;
|
|
188
|
+
for (const write of transaction.writes)
|
|
189
|
+
transaction.finalizedGroups.add(write.group);
|
|
190
|
+
}
|
|
191
|
+
state.transactions.clear();
|
|
192
|
+
state.owners.clear();
|
|
193
|
+
state.revisions.clear();
|
|
194
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|