@milpa/comal 0.1.7 → 0.2.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/README.md +19 -1
- package/backend/contracts/remote-commands-v1.json +12 -0
- package/governance/chrome-task-v10.json +323 -0
- package/governance/chrome-task-v11.json +355 -0
- package/governance/chrome-task-v12.json +387 -0
- package/governance/chrome-task-v13.json +419 -0
- package/governance/chrome-task-v3.json +99 -0
- package/governance/chrome-task-v4.json +131 -0
- package/governance/chrome-task-v5.json +163 -0
- package/governance/chrome-task-v6.json +195 -0
- package/governance/chrome-task-v7.json +227 -0
- package/governance/chrome-task-v8.json +259 -0
- package/governance/chrome-task-v9.json +291 -0
- package/governance/source-task-v1.json +1 -0
- package/governance/source-task-v2.json +1 -0
- package/package.json +14 -1
- package/src/agent-recovery-proof.mjs +12 -1
- package/src/agent.mjs +94 -30
- package/src/audit.mjs +21 -6
- package/src/auto-guard.mjs +48 -1
- package/src/chrome-candidate-recovery-proof.mjs +47 -0
- package/src/chrome-recovery-proof.mjs +6 -2
- package/src/chrome-source-syntax.mjs +93 -0
- package/src/cli.mjs +158 -54
- package/src/codex-provider.mjs +3 -2
- package/src/connection-settings.mjs +204 -0
- package/src/context-pressure.mjs +2 -1
- package/src/durable-agent-runs.mjs +5 -11
- package/src/event-channels.mjs +8 -0
- package/src/file-recovery-proof.mjs +11 -3
- package/src/file-recovery.mjs +24 -0
- package/src/gemini-provider.mjs +6 -1
- package/src/git-agent-revision.mjs +12 -0
- package/src/git-agent-workspaces.mjs +143 -0
- package/src/git-integration-lineage.mjs +20 -0
- package/src/git-integration-operations.mjs +200 -0
- package/src/git-integration-recovery-proof.mjs +70 -0
- package/src/git-page-cli-bridge.mjs +61 -0
- package/src/git-page-evidence.mjs +30 -0
- package/src/git-page-identity.mjs +10 -0
- package/src/git-page-proof.mjs +96 -0
- package/src/git-page-settlement.mjs +21 -0
- package/src/git-page-verification.mjs +134 -0
- package/src/git-plumbing.mjs +212 -0
- package/src/git-process-worker.mjs +50 -0
- package/src/git-source.mjs +110 -0
- package/src/git-workspaces.mjs +519 -0
- package/src/goals.mjs +13 -7
- package/src/house.mjs +246 -20
- package/src/i18n/cli-es.mjs +4 -0
- package/src/i18n/es.mjs +35 -0
- package/src/judges.mjs +27 -11
- package/src/mcp-runtime.mjs +15 -3
- package/src/model-controls.mjs +11 -1
- package/src/model-deadline.mjs +4 -3
- package/src/native-refusal.mjs +38 -0
- package/src/onboarding.mjs +88 -24
- package/src/openai-provider.mjs +14 -7
- package/src/operations.mjs +9 -2
- package/src/output-recovery.mjs +48 -0
- package/src/peer-tools.mjs +18 -8
- package/src/permission-controls.mjs +232 -0
- package/src/permission-rules.mjs +312 -0
- package/src/permissions.mjs +113 -30
- package/src/plugin-catalog.mjs +84 -0
- package/src/plugin-config.mjs +48 -6
- package/src/plugin-controls.mjs +332 -0
- package/src/plugin-notices.mjs +88 -0
- package/src/plugin-trust.mjs +104 -0
- package/src/plugins/chrome-lab/bridge.mjs +38 -12
- package/src/plugins/chrome-lab/client.mjs +103 -25
- package/src/plugins/chrome-lab/diagnostics.mjs +19 -0
- package/src/plugins/chrome-lab/index.mjs +116 -26
- package/src/plugins/chrome-lab/project.mjs +30 -2
- package/src/plugins/chrome-lab/scoped-input.mjs +135 -0
- package/src/plugins/chrome-lab/worker-diagnostics.mjs +51 -0
- package/src/plugins/open-relay/actions.mjs +205 -0
- package/src/plugins/open-relay/bound.mjs +47 -0
- package/src/plugins/open-relay/client.mjs +158 -0
- package/src/plugins/open-relay/dossier.mjs +119 -0
- package/src/plugins/open-relay/index.mjs +153 -0
- package/src/plugins/open-relay/lookup.mjs +45 -0
- package/src/plugins/open-relay/notices.mjs +79 -0
- package/src/plugins/open-relay/watch.mjs +127 -0
- package/src/plugins.mjs +72 -29
- package/src/private-fs.mjs +40 -0
- package/src/process-lock.mjs +24 -13
- package/src/provider-auth.mjs +14 -3
- package/src/provider.mjs +11 -2
- package/src/read-only-check.mjs +7 -3
- package/src/scope-ceiling.mjs +56 -0
- package/src/scoped-policy.mjs +39 -0
- package/src/secret-login.mjs +68 -0
- package/src/session-bus.mjs +41 -7
- package/src/session-projections-candidate.mjs +119 -0
- package/src/session-projections.mjs +119 -0
- package/src/session-read-cache.mjs +23 -3
- package/src/sessions.mjs +35 -11
- package/src/source-editor-role.mjs +13 -0
- package/src/source-ownership.mjs +73 -0
- package/src/source-proposal-proof.mjs +55 -0
- package/src/source-proposals.mjs +118 -0
- package/src/source-requests.mjs +148 -0
- package/src/source-task-proof.mjs +24 -0
- package/src/source-task-revision-proof.mjs +44 -0
- package/src/subagents.mjs +187 -87
- package/src/tool-packs.mjs +5 -5
- package/src/tools.mjs +82 -15
- package/src/trusted-context.mjs +5 -1
- package/src/tui/connection-transition.mjs +1 -1
- package/src/tui/controller.mjs +8 -4
- package/src/tui/discovery.mjs +24 -12
- package/src/tui/inbound-blocks.mjs +55 -0
- package/src/tui/present.mjs +504 -0
- package/src/tui/screen.mjs +49 -13
- package/src/windows-private.ps1 +61 -0
- package/src/work-assertions.mjs +6 -2
- package/src/work-plan.mjs +34 -8
- package/src/work-state.mjs +15 -9
- package/src/workflows/chrome-page-cases.mjs +156 -0
- package/src/workflows/chrome-page-checkpoint-reader.mjs +10 -0
- package/src/workflows/chrome-page-diagnostics.mjs +113 -0
- package/src/workflows/chrome-page-law.mjs +48 -0
- package/src/workflows/chrome-page-recovery-host-v13.mjs +14 -0
- package/src/workflows/chrome-page-recovery-v13.mjs +104 -0
- package/src/workflows/chrome-page-task.mjs +429 -0
- package/src/workflows/chrome-page-verifier.mjs +123 -0
- package/src/workflows/chrome-source.mjs +73 -0
- package/src/workflows/chrome-supplied-inputs.mjs +43 -0
- package/src/workflows/chrome-task-adoption.mjs +122 -0
- package/src/workflows/chrome-task.mjs +379 -74
- package/src/workflows/source-task-v1.mjs +124 -0
- package/src/workflows/source-task-v2.mjs +227 -0
- package/src/workflows/source-task.mjs +13 -0
|
@@ -7,11 +7,31 @@ export async function remoteBridge(config, lines) {
|
|
|
7
7
|
if (!path.isAbsolute(config.remoteRoot) || root === path.parse(root).root) throw new Error('remoteRoot debe ser un directorio de laboratorio absoluto.');
|
|
8
8
|
const send = value => process.stdout.write(JSON.stringify(value) + '\n');
|
|
9
9
|
const digest = data => createHash('sha256').update(data).digest('hex');
|
|
10
|
-
let socket, sequence = 0, closing = false; const pending = new Map(), servers = new Map(), attached = new Set();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
let socket, sequence = 0, closing = false; const pending = new Map(), servers = new Map(), attached = new Set(), flattened = new Set();
|
|
11
|
+
// Only this private, validated optional path gets the smaller response-wait timer.
|
|
12
|
+
const diagnosticPolicy = request => {
|
|
13
|
+
const option = request.attachDiagnostic;
|
|
14
|
+
if (!option || typeof option !== 'object' || Array.isArray(option) || Object.keys(option).join() !== 'timeout_ms' ||
|
|
15
|
+
request.action !== 'cdp' || !['Log.enable','Network.enable'].includes(request.method) ||
|
|
16
|
+
typeof request.sessionId !== 'string' || !request.sessionId || !flattened.has(request.sessionId) ||
|
|
17
|
+
!Number.isInteger(option.timeout_ms) || option.timeout_ms < 1 || option.timeout_ms > Math.min(config.timeoutMs,2000)) throw new Error('Invalid private attach diagnostic policy.');
|
|
18
|
+
const params = request.params;
|
|
19
|
+
const valid = params && typeof params === 'object' && !Array.isArray(params) && (request.method === 'Log.enable'
|
|
20
|
+
? Object.keys(params).length === 0
|
|
21
|
+
: Object.keys(params).sort().join() === 'maxResourceBufferSize,maxTotalBufferSize' && params.maxTotalBufferSize === 100000 && params.maxResourceBufferSize === 10000);
|
|
22
|
+
if (!valid) throw new Error('Invalid private attach diagnostic arguments.');
|
|
23
|
+
return option;
|
|
24
|
+
};
|
|
25
|
+
const cdp = (method, params = {}, sessionId, diagnostic) => new Promise((resolve, reject) => {
|
|
26
|
+
const id = ++sequence, started = performance.now();
|
|
27
|
+
const outcome = (state,reason,error) => ({diagnostic:{version:1,method,session_id:sessionId,state,reason,remote_dispatched:true,timeout_ms:diagnostic.timeout_ms,elapsed_ms:performance.now()-started,...(error?{error}: {})}});
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
pending.delete(id);
|
|
30
|
+
diagnostic ? resolve(outcome('unknown','command_timeout')) : reject(new Error(`CDP timeout: ${method}; no se reintentó.`));
|
|
31
|
+
}, diagnostic?.timeout_ms ?? config.timeoutMs);
|
|
32
|
+
pending.set(id, { resolve, reject, timer, method, params, diagnostic, outcome });
|
|
33
|
+
try { socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) })); }
|
|
34
|
+
catch(error) { pending.delete(id); clearTimeout(timer); reject(error); }
|
|
15
35
|
});
|
|
16
36
|
const shutdown = async (detach = true) => {
|
|
17
37
|
if (closing) return; closing = true;
|
|
@@ -40,8 +60,10 @@ export async function remoteBridge(config, lines) {
|
|
|
40
60
|
await new Promise((resolve, reject) => { socket.addEventListener('open', resolve, { once: true }); socket.addEventListener('error', () => reject(new Error('No se pudo abrir WebSocket CDP.')), { once: true }); });
|
|
41
61
|
socket.addEventListener('message', event => {
|
|
42
62
|
const message = JSON.parse(event.data);
|
|
43
|
-
if (message.id) { const entry = pending.get(message.id); if (!entry) return; pending.delete(message.id); clearTimeout(entry.timer); if (!message.error) { if (entry.method === 'Target.attachToTarget' && message.result?.sessionId) attached.add(message.result.sessionId); if (entry.method === 'Target.detachFromTarget') attached.delete(entry.params.sessionId); } message.error ? entry.reject(new Error(`${message.error.code}: ${message.error.message}`)) : entry.resolve(message.result); }
|
|
63
|
+
if (message.id) { const entry = pending.get(message.id); if (!entry) return; pending.delete(message.id); clearTimeout(entry.timer); if (!message.error) { if (entry.method === 'Target.attachToTarget' && message.result?.sessionId) { attached.add(message.result.sessionId); if(entry.params.flatten === true) flattened.add(message.result.sessionId); } if (entry.method === 'Target.detachFromTarget') { attached.delete(entry.params.sessionId); flattened.delete(entry.params.sessionId); } } if(entry.diagnostic) { if(message.error && (!Number.isInteger(message.error.code) || typeof message.error.message !== 'string')) entry.reject(new Error('Malformed diagnostic CDP error.')); else entry.resolve(message.error ? entry.outcome('rejected','command_error',{code:message.error.code,message:message.error.message.slice(0,1000)}) : entry.outcome('enabled','command_reply')); } else message.error ? entry.reject(Object.assign(new Error(`${message.error.code}: ${message.error.message}`), { cdpError: { method: entry.method, code: message.error.code, message: String(message.error.message).slice(0, 1000) } })) : entry.resolve(message.result); }
|
|
44
64
|
else if (['Runtime.consoleAPICalled', 'Runtime.exceptionThrown', 'Runtime.executionContextCreated', 'Runtime.executionContextDestroyed', 'Runtime.executionContextsCleared', 'Log.entryAdded', 'Network.loadingFailed', 'Inspector.detached', 'Target.detachedFromTarget'].includes(message.method)) {
|
|
65
|
+
if(message.method === 'Target.detachedFromTarget') { attached.delete(message.params?.sessionId); flattened.delete(message.params?.sessionId); }
|
|
66
|
+
if(message.method === 'Inspector.detached') { attached.delete(message.sessionId); flattened.delete(message.sessionId); }
|
|
45
67
|
// Never ship arbitrary object previews or network bodies into the resident's context.
|
|
46
68
|
if (message.method === 'Runtime.consoleAPICalled') message.params.args = (message.params.args || []).slice(0, 12).map(arg => ({ type: arg.type, text: String(arg.value ?? arg.description ?? arg.type).slice(0, 2000) }));
|
|
47
69
|
if (JSON.stringify(message).length < 64000) send({ event: message });
|
|
@@ -88,7 +110,7 @@ export async function remoteBridge(config, lines) {
|
|
|
88
110
|
await checkURL(targetInfo.url); return targetInfo;
|
|
89
111
|
};
|
|
90
112
|
const extensionFor = async name => { const dir = await owned(name); const expected = path.join(dir, 'current').toLowerCase(); const extension = (await extensions()).find(item => path.resolve(item.path).toLowerCase() === expected); if (!extension) throw new Error('Extensión del proyecto no cargada. Usa Deploy.'); return extension; };
|
|
91
|
-
const deploy = async ({ project, files, attempt_id = randomUUID() }) => {
|
|
113
|
+
const deploy = async ({ project, files, attempt_id = randomUUID(), create_only=false }) => {
|
|
92
114
|
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(attempt_id)) throw new Error('Identidad de publicación inválida.');
|
|
93
115
|
slug(project); if (!Array.isArray(files) || !files.length || files.length > 1000) throw new Error('Límite de archivos de despliegue.');
|
|
94
116
|
let bytes = 0; const names = new Set();
|
|
@@ -98,7 +120,7 @@ export async function remoteBridge(config, lines) {
|
|
|
98
120
|
if (manifest?.manifest_version !== 3 || !manifest.name || !manifest.version) throw new Error('Se requiere manifest.json MV3 válido.');
|
|
99
121
|
await fs.mkdir(root, { recursive: true }); await realDirectory(root);
|
|
100
122
|
const dir = projectDir(project);
|
|
101
|
-
try { await fs.mkdir(dir); await fs.writeFile(path.join(dir, '.comal-lab.json'), JSON.stringify({ owner: 'comal-chrome-lab-v1', project }), { flag: 'wx' }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
123
|
+
try { await fs.mkdir(dir); await fs.writeFile(path.join(dir, '.comal-lab.json'), JSON.stringify({ owner: 'comal-chrome-lab-v1', project }), { flag: 'wx' }); } catch (error) { if (error.code !== 'EEXIST') throw error; if(create_only)throw new Error(JSON.stringify({code:'CHROME_PROJECT_EXISTS',executed:false,project,reason:'Atomic creation requires a new project directory; no source changed and no Chrome load dispatched.'})); }
|
|
102
124
|
await owned(project);
|
|
103
125
|
const lock = path.join(dir, '.deploy-lock'); await fs.mkdir(lock); // Fail closed if another Comal is deploying.
|
|
104
126
|
const stage = path.join(dir, `stage-${randomUUID()}`), current = path.join(dir, 'current'), backup = path.join(dir, `backup-${randomUUID()}`);
|
|
@@ -138,7 +160,7 @@ export async function remoteBridge(config, lines) {
|
|
|
138
160
|
const artifact = JSON.parse(raw);
|
|
139
161
|
if (artifact.project !== project || artifact.remote_path?.toLowerCase() !== current.toLowerCase() || !Array.isArray(artifact.files) || !artifact.files.length || artifact.files.length > 1000) throw new Error('Recibo de otro proyecto o snapshot inválido.');
|
|
140
162
|
await realDirectory(current);
|
|
141
|
-
const observedFiles = []; let bytes = 0;
|
|
163
|
+
const observedFiles = []; let bytes = 0, manifestBytes;
|
|
142
164
|
const visit = async (directory, prefix = '') => {
|
|
143
165
|
await realDirectory(directory);
|
|
144
166
|
for (const entry of (await fs.readdir(directory, { withFileTypes: true })).sort((a,b)=>a.name.localeCompare(b.name))) {
|
|
@@ -149,13 +171,16 @@ export async function remoteBridge(config, lines) {
|
|
|
149
171
|
const data = await fs.readFile(absolute), after = await fs.lstat(absolute);
|
|
150
172
|
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino || data.length !== before.size) throw new Error('Snapshot remoto cambió durante la lectura.');
|
|
151
173
|
observedFiles.push({ path: relative, sha256: digest(data) });
|
|
174
|
+
if (relative === 'manifest.json') manifestBytes = data;
|
|
152
175
|
}
|
|
153
176
|
};
|
|
154
177
|
await visit(current);
|
|
155
178
|
const extension = (await extensions()).find(item=>item.id===artifact.extension_id && path.resolve(item.path).toLowerCase()===current.toLowerCase());
|
|
156
179
|
if (await locked() || await readReceipt() !== raw) return { status: 'busy', project };
|
|
157
180
|
if (!extension?.enabled || JSON.stringify(observedFiles) !== JSON.stringify(artifact.files)) return { status: 'conflict', project, attempt_id: artifact.attempt_id || null, reason: 'Loaded extension or physical snapshot no longer matches the receipt.' };
|
|
158
|
-
|
|
181
|
+
const workerScript = manifestBytes && JSON.parse(manifestBytes).background?.service_worker;
|
|
182
|
+
if (workerScript !== undefined) safeFile(workerScript);
|
|
183
|
+
return { ...artifact, status: 'observed', worker_script: typeof workerScript === 'string' ? workerScript : null, files: artifact.files.slice(0,30), file_count: artifact.files.length, files_truncated: artifact.files.length>30, file_manifest_sha256: digest(JSON.stringify(artifact.files)), file_manifest_remote_path: receipt, checked_at: new Date().toISOString() };
|
|
159
184
|
};
|
|
160
185
|
const preview = async ({ project, file = 'fixture.html' }) => {
|
|
161
186
|
const dir = path.join(await owned(project), 'current'); safeFile(file);
|
|
@@ -183,7 +208,8 @@ export async function remoteBridge(config, lines) {
|
|
|
183
208
|
try {
|
|
184
209
|
if (line.length > config.maxDeployBytes * 1.5 + 1000000) throw new Error('Mensaje demasiado grande.');
|
|
185
210
|
request = JSON.parse(line); let result;
|
|
186
|
-
|
|
211
|
+
const diagnostic = Object.hasOwn(request,'attachDiagnostic') ? diagnosticPolicy(request) : undefined;
|
|
212
|
+
if (request.action === 'cdp') result = await cdp(request.method, request.params, request.sessionId, diagnostic);
|
|
187
213
|
else if (request.action === 'status') result = await ownExtensions();
|
|
188
214
|
else if (request.action === 'checkTarget') result = await checkTarget(request.params.targetId);
|
|
189
215
|
else if (request.action === 'open') result = await cdp('Target.createTarget', { url: await checkURL(request.params.url) });
|
|
@@ -200,7 +226,7 @@ export async function remoteBridge(config, lines) {
|
|
|
200
226
|
const output = JSON.stringify({ id: request.id, result });
|
|
201
227
|
if (output.length > 12000000) throw new Error('Respuesta excesiva; usa consultas acotadas.');
|
|
202
228
|
process.stdout.write(output + '\n');
|
|
203
|
-
} catch (error) { send({ id: request?.id, error: error.message }); }
|
|
229
|
+
} catch (error) { send({ id: request?.id, error: error.message, ...(error.cdpError ? { cdp_error: error.cdpError } : {}) }); }
|
|
204
230
|
});
|
|
205
231
|
send({ ready: { browser: version.Browser, browser_endpoint_id: /^\/devtools\/browser\/([^/]+)$/.exec(endpoint.pathname)?.[1] || null, protocol_version: version['Protocol-Version'], extensions: protocol.domains.find(domain => domain.domain === 'Extensions')?.commands.map(command => command.name) || [], remote_root: root, node: process.version } });
|
|
206
232
|
}
|
|
@@ -2,10 +2,11 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { remoteBridge } from './bridge.mjs';
|
|
5
|
+
import { OPTIONAL_PHASE_MS, PUBLIC_DIAGNOSTIC_BYTES, diagnosticEnvelope } from './diagnostics.mjs';
|
|
5
6
|
|
|
6
7
|
export class ChromeLabClient {
|
|
7
8
|
constructor(config, { spawnProcess = spawn } = {}) {
|
|
8
|
-
this.config = config; this.spawnProcess = spawnProcess; this.pending = new Map(); this.sessions = new Map(); this.attaching = new Map(); this.targetInfo = new Map(); this.contexts = new Map(); this.events = []; this.sequence = 0; this.closed = false;
|
|
9
|
+
this.config = config; this.spawnProcess = spawnProcess; this.pending = new Map(); this.sessions = new Map(); this.attaching = new Map(); this.targetInfo = new Map(); this.contexts = new Map(); this.collections = new Map(); this.events = []; this.sequence = 0; this.closed = false; this.attachDiagnostics = new Map();
|
|
9
10
|
}
|
|
10
11
|
async connect(signal) {
|
|
11
12
|
signal?.throwIfAborted(); if (this.closed) throw new Error('Plugin Chrome Lab cerrado.');
|
|
@@ -25,10 +26,10 @@ export class ChromeLabClient {
|
|
|
25
26
|
const bootstrap = "const r=require('readline').createInterface({input:process.stdin});r.once('line',l=>{const p=JSON.parse(l);eval(p.source)(p.config,r).catch(e=>{console.log(JSON.stringify({fatal:e.message}));process.exit(1)})})";
|
|
26
27
|
const child = this.child = this.spawnProcess('ssh', ['-T', '-o', 'BatchMode=yes', '-o', 'ClearAllForwardings=yes', '-o', 'ConnectTimeout=10', '-o', 'ServerAliveInterval=10', '-o', 'ServerAliveCountMax=2', this.config.sshHost, `node -e "${bootstrap}"`], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
27
28
|
const timer = setTimeout(() => { fail(new Error('Timeout iniciando Chrome Lab por SSH.')); child.kill(); }, this.config.timeoutMs);
|
|
28
|
-
const fail = error => { clearTimeout(timer); if (this.child !== child) return; this.failure = error; if (!ready) reject(error); for (const entry of this.pending.values()) entry.finish(error); this.pending.clear(); };
|
|
29
|
+
const fail = (error,reason='transport_uncertain') => { clearTimeout(timer); if (this.child !== child) return; for(const record of this.attachDiagnostics.values()) this.retireDiagnostic(record.session_id,reason); this.failure = error; if (!ready) reject(error); for (const entry of this.pending.values()) entry.finish(error); this.pending.clear(); };
|
|
29
30
|
child.on('error', fail); child.stdin.on('error', fail);
|
|
30
31
|
child.stderr.on('data', chunk => { stderr = (stderr + chunk.toString()).slice(-1500); });
|
|
31
|
-
child.on('exit', (code, sig) => fail(new Error(`Chrome Lab desconectado (${code ?? sig}); no se reintentaron efectos pendientes. ${stderr}`)));
|
|
32
|
+
child.on('exit', (code, sig) => fail(new Error(`Chrome Lab desconectado (${code ?? sig}); no se reintentaron efectos pendientes. ${stderr}`),'connection_lost'));
|
|
32
33
|
const lines = createInterface({ input: child.stdout });
|
|
33
34
|
lines.on('line', line => {
|
|
34
35
|
if (this.child !== child) return;
|
|
@@ -38,7 +39,7 @@ export class ChromeLabClient {
|
|
|
38
39
|
if (message.fatal) { fail(new Error(message.fatal)); child.kill(); }
|
|
39
40
|
else if (message.ready) { ready = true; clearTimeout(timer); this.info = message.ready; resolve(message.ready); }
|
|
40
41
|
else if (message.event) this.observe(message.event);
|
|
41
|
-
else { const entry = this.pending.get(message.id); if (entry)
|
|
42
|
+
else { const entry = this.pending.get(message.id); if (entry) { const error = message.error ? new Error(message.error) : null; if(error && entry.action==='cdp' && message.cdp_error?.method===entry.method && Number.isInteger(message.cdp_error.code) && typeof message.cdp_error.message==='string')error.cdpError={method:entry.method,code:message.cdp_error.code,message:message.cdp_error.message.slice(0,1000)};let result=message.result; if(!error && entry.attachDiagnostic) diagnosticEnvelope(result,{method:entry.method,sessionId:entry.sessionId,timeout_ms:entry.attachDiagnostic.timeout_ms}); entry.finish(error,result); } }
|
|
42
43
|
} catch (error) { fail(error); child.kill(); }
|
|
43
44
|
});
|
|
44
45
|
child.stdin.write(JSON.stringify({ source: `(${remoteBridge.toString()})`, config: this.config }) + '\n');
|
|
@@ -46,35 +47,71 @@ export class ChromeLabClient {
|
|
|
46
47
|
}
|
|
47
48
|
observe(event) {
|
|
48
49
|
const { method, params = {}, sessionId } = event;
|
|
49
|
-
if (method === 'Target.detachedFromTarget') { for (const [target, session] of this.sessions) if (session === params.sessionId) this.sessions.delete(target); this.contexts.delete(params.sessionId); }
|
|
50
|
-
if (method === 'Inspector.detached') { for (const [target, session] of this.sessions) if (session === sessionId) this.sessions.delete(target); this.contexts.delete(sessionId); }
|
|
51
|
-
if (method === 'Runtime.executionContextCreated') {
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
if (method === 'Target.detachedFromTarget') { this.retireDiagnostic(params.sessionId); for (const [target, session] of this.sessions) if (session === params.sessionId) this.sessions.delete(target); this.contexts.delete(params.sessionId); }
|
|
51
|
+
if (method === 'Inspector.detached') { this.retireDiagnostic(sessionId); for (const [target, session] of this.sessions) if (session === sessionId) this.sessions.delete(target); this.contexts.delete(sessionId); }
|
|
52
|
+
if (method === 'Runtime.executionContextCreated') {
|
|
53
|
+
const contexts = this.contexts.get(sessionId) || new Map(), collection = this.collection(sessionId), context = params.context;
|
|
54
|
+
collection.epoch++;
|
|
55
|
+
const seen = collection.seen.get(context.id);
|
|
56
|
+
if (collection.seen.has(context.id) && seen !== context.uniqueId) collection.reused.add(context.id);
|
|
57
|
+
if (collection.seen.size < 512) collection.seen.set(context.id, context.uniqueId); else collection.ambiguous = true;
|
|
58
|
+
contexts.set(context.id, context); this.contexts.set(sessionId, contexts); return;
|
|
59
|
+
}
|
|
60
|
+
if (method === 'Runtime.executionContextDestroyed') {
|
|
61
|
+
const contexts = this.contexts.get(sessionId);
|
|
62
|
+
// A delayed destruction for an old unique context must not remove a reused numeric ID.
|
|
63
|
+
if (typeof params.executionContextUniqueId === 'string') { for (const [id, context] of contexts || []) if (context.uniqueId === params.executionContextUniqueId) contexts.delete(id); }
|
|
64
|
+
else contexts?.delete(params.executionContextId);
|
|
65
|
+
this.collection(sessionId).epoch++; return;
|
|
66
|
+
}
|
|
67
|
+
if (method === 'Runtime.executionContextsCleared') { this.contexts.delete(sessionId); this.collection(sessionId).epoch++; return; }
|
|
54
68
|
const target_id = [...this.sessions].find(([, session]) => session === sessionId)?.[0];
|
|
55
69
|
const raw = JSON.stringify(params);
|
|
56
|
-
|
|
70
|
+
const contextId = method === 'Runtime.consoleAPICalled' ? params.executionContextId : method === 'Runtime.exceptionThrown' ? params.exceptionDetails?.executionContextId : undefined;
|
|
71
|
+
const collection = this.collections.get(sessionId), context = this.contexts.get(sessionId)?.get(contextId);
|
|
72
|
+
const unique = context && !collection?.ambiguous && !collection?.reused.has(contextId) ? context.uniqueId : null;
|
|
73
|
+
this.events.push({ method, target_id, observed_at: new Date().toISOString(), binding: { connection_id: this.connectionId || null, browser_endpoint_id: this.info?.browser_endpoint_id || null, session_id: sessionId || null, context_unique_id: unique || null, context_epoch: collection?.epoch ?? null }, details: raw.length > 4000 ? { truncated: true, preview: raw.slice(0, 4000) } : params });
|
|
57
74
|
if (this.events.length > 200) this.events.shift();
|
|
58
75
|
}
|
|
59
|
-
async request(action, params, { signal, ...extra } = {}) {
|
|
60
|
-
|
|
76
|
+
async request(action, params, { signal, expectedBinding, onDispatch, ...extra } = {}) {
|
|
77
|
+
// Optional original transport ownership, checked again after connect's async boundary.
|
|
78
|
+
// This protects a pinned caller from known local replacement before the write.
|
|
79
|
+
const checkBinding=()=>{if(expectedBinding&&(this.closed||this.failure||this.connectionId!==expectedBinding.connectionId||this.info?.browser_endpoint_id!==expectedBinding.browserEndpointId||extra.sessionId!==expectedBinding.sessionId||this.sessions.get(expectedBinding.targetId)!==expectedBinding.sessionId))throw Object.assign(new Error('Original CDP connection/session binding changed before dispatch.'),{code:'CHROME_CDP_BINDING_CHANGED',details:{remote_dispatched:false}});};
|
|
80
|
+
checkBinding();await this.connect(signal); signal?.throwIfAborted();checkBinding();if (this.failure) throw this.failure;
|
|
61
81
|
return new Promise((resolve, reject) => {
|
|
62
82
|
const id = ++this.sequence;
|
|
63
83
|
const abort = () => finish(new Error('Operación abortada tras despacho; el efecto remoto puede haber ocurrido. Verifica antes de repetir.'));
|
|
64
84
|
const timer = setTimeout(() => finish(new Error('Timeout Chrome Lab; efecto remoto desconocido, no se reintentó.')), this.config.timeoutMs + 2000);
|
|
65
85
|
const finish = (error, value) => { clearTimeout(timer); signal?.removeEventListener('abort', abort); this.pending.delete(id); error ? reject(error) : resolve(value); };
|
|
66
|
-
this.pending.set(id, { finish }); signal?.addEventListener('abort', abort, { once: true });
|
|
86
|
+
this.pending.set(id, { finish, action, method: extra.method, sessionId:extra.sessionId, attachDiagnostic:extra.attachDiagnostic }); signal?.addEventListener('abort', abort, { once: true });
|
|
87
|
+
try { onDispatch?.(); } catch(error) { finish(error); return; }
|
|
67
88
|
this.child.stdin.write(JSON.stringify({ id, action, params, ...extra }) + '\n', error => { if (error) finish(error); });
|
|
68
89
|
});
|
|
69
90
|
}
|
|
70
|
-
cdp(method, params = {}, { signal, sessionId } = {}) { return this.request('cdp', params, { signal, method, sessionId }); }
|
|
71
|
-
resourceIdentity(targetId) {
|
|
91
|
+
cdp(method, params = {}, { signal, sessionId, expectedBinding, attachDiagnostic, onDispatch } = {}) { return this.request('cdp', params, { signal, method, sessionId, expectedBinding, onDispatch, ...(attachDiagnostic !== undefined ? {attachDiagnostic} : {}) }); }
|
|
92
|
+
resourceIdentity(targetId, { frameId, sessionId, connectionId } = {}) {
|
|
93
|
+
// A sole default context may belong to a child frame. Bind only to the
|
|
94
|
+
// root frame observed in the same attached session and connection.
|
|
95
|
+
const owned = typeof frameId === 'string' && frameId.length > 0 &&
|
|
96
|
+
typeof sessionId === 'string' && sessionId.length > 0 && sessionId === this.sessions.get(targetId) &&
|
|
97
|
+
typeof connectionId === 'string' && connectionId.length > 0 && connectionId === this.connectionId && !this.failure && !this.closed;
|
|
72
98
|
const contexts = [...(this.contexts.get(this.sessions.get(targetId))?.values() || [])];
|
|
73
|
-
const defaults = contexts.filter(item => item.auxData?.isDefault === true && typeof item.uniqueId === 'string' && item.uniqueId);
|
|
99
|
+
const defaults = owned ? contexts.filter(item => item.auxData?.isDefault === true && item.auxData.frameId === frameId && typeof item.uniqueId === 'string' && item.uniqueId) : [];
|
|
74
100
|
return { connection_id: this.connectionId || null, browser_endpoint_id: this.info?.browser_endpoint_id || null,
|
|
75
101
|
context_unique_id: defaults.length === 1 ? defaults[0].uniqueId : null,
|
|
76
102
|
frame_id: defaults.length === 1 ? defaults[0].auxData.frameId || null : null };
|
|
77
103
|
}
|
|
104
|
+
collection(sessionId) {
|
|
105
|
+
if (!this.collections.has(sessionId)) this.collections.set(sessionId, { started_at: new Date().toISOString(), epoch: 0, seen: new Map(), reused: new Set(), ambiguous: false });
|
|
106
|
+
return this.collections.get(sessionId);
|
|
107
|
+
}
|
|
108
|
+
workerIdentity(targetId) {
|
|
109
|
+
const target = this.targetInfo.get(targetId), session = this.sessions.get(targetId), contexts = [...(this.contexts.get(session)?.values() || [])];
|
|
110
|
+
if (this.failure || this.closed || target?.type !== 'service_worker' || typeof target.url !== 'string' || !target.url.startsWith('chrome-extension://') || contexts.length !== 1) return null;
|
|
111
|
+
const context = contexts[0], collection = this.collections.get(session);
|
|
112
|
+
if (!this.connectionId || !this.info?.browser_endpoint_id || !session || !collection || !Number.isInteger(context.id) || context.id < 1 || typeof context.uniqueId !== 'string' || !context.uniqueId || context.origin !== target.url) return null;
|
|
113
|
+
return { realm: 'extension_service_worker', target_id: targetId, url: target.url, connection_id: this.connectionId, browser_endpoint_id: this.info.browser_endpoint_id, session_id: session, context_unique_id: context.uniqueId, context_id: context.id, context_epoch: collection.epoch, collection_start: collection.started_at };
|
|
114
|
+
}
|
|
78
115
|
// Explicit recovery only. A new connection does not replay any request or resolve its effect.
|
|
79
116
|
async reconnect(signal) {
|
|
80
117
|
signal?.throwIfAborted();
|
|
@@ -82,10 +119,49 @@ export class ChromeLabClient {
|
|
|
82
119
|
this.reconnecting = true;
|
|
83
120
|
try {
|
|
84
121
|
await this.close(); this.child = undefined; this.connection = undefined; this.failure = undefined; this.info = undefined; this.closed = false;
|
|
85
|
-
this.sessions.clear(); this.contexts.clear(); this.attaching.clear(); this.targetInfo.clear(); this.events = [];
|
|
122
|
+
this.attachDiagnostics.clear(); this.sessions.clear(); this.contexts.clear(); this.collections.clear(); this.attaching.clear(); this.targetInfo.clear(); this.events = [];
|
|
86
123
|
return await this.connect(signal);
|
|
87
124
|
} finally { this.reconnecting = false; }
|
|
88
125
|
}
|
|
126
|
+
retireDiagnostic(sessionId, reason = 'session_retired') {
|
|
127
|
+
for(const record of this.attachDiagnostics.values()) if(record.session_id === sessionId && !record.retired) {
|
|
128
|
+
record.retired = true;
|
|
129
|
+
for(const domain of record.domains) if(!domain.completed_at) Object.assign(domain,{state:domain.command_count?'unknown':'not_attempted',reason,completed_at:new Date().toISOString()});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
diagnostics(targetId) {
|
|
133
|
+
const record=this.attachDiagnostics.get(targetId);
|
|
134
|
+
if(!record || record.retired || this.closed || this.failure || record.connection_id !== this.connectionId || record.browser_endpoint_id !== this.info?.browser_endpoint_id || this.sessions.get(targetId) !== record.session_id)return null;
|
|
135
|
+
const value=structuredClone(record);
|
|
136
|
+
return Buffer.byteLength(JSON.stringify(value))<=PUBLIC_DIAGNOSTIC_BYTES ? value : {version:1,state:'unavailable',reason:'details_omitted_due_to_limit',history_complete:false};
|
|
137
|
+
}
|
|
138
|
+
async optionalDiagnostics(targetId, sessionId, signal) {
|
|
139
|
+
const binding={targetId,sessionId,connectionId:this.connectionId,browserEndpointId:this.info?.browser_endpoint_id};
|
|
140
|
+
const record={version:1,policy:'awaited-optional-2000ms/1',connection_id:binding.connectionId,browser_endpoint_id:binding.browserEndpointId,target_id:targetId,session_id:sessionId,generation:randomUUID(),history_complete:false,runtime_collection:'since_attach',domains:['Log.enable','Network.enable'].map(method=>({method,state:'not_attempted',reason:'phase_budget_exhausted',command_count:0,timeout_ms:0,started_at:null,completed_at:null,elapsed_ms:0}))};
|
|
141
|
+
// One live record per target. A replacement generation cannot be updated by this closure.
|
|
142
|
+
this.attachDiagnostics.set(targetId,record);
|
|
143
|
+
const current=()=>!record.retired && this.attachDiagnostics.get(targetId)===record && !this.closed && !this.failure && this.connectionId===binding.connectionId && this.info?.browser_endpoint_id===binding.browserEndpointId && this.sessions.get(targetId)===sessionId;
|
|
144
|
+
const check=()=>{signal?.throwIfAborted();if(!current())throw Object.assign(new Error('Original attach diagnostic session retired.'),{code:'CHROME_CDP_BINDING_CHANGED'});};
|
|
145
|
+
const started=performance.now(), budget=Math.min(OPTIONAL_PHASE_MS,this.config.timeoutMs);
|
|
146
|
+
for(const domain of record.domains) {
|
|
147
|
+
check();const remaining=Math.floor(budget-(performance.now()-started));
|
|
148
|
+
if(remaining < 1) {domain.completed_at=new Date().toISOString();continue;}
|
|
149
|
+
domain.timeout_ms=remaining;const begin=performance.now();
|
|
150
|
+
try {
|
|
151
|
+
const value=await this.cdp(domain.method,domain.method==='Log.enable'?{}:{maxTotalBufferSize:100000,maxResourceBufferSize:10000},{signal,sessionId,expectedBinding:binding,attachDiagnostic:{timeout_ms:remaining},onDispatch:()=>{check();if(performance.now()-started>=budget)throw Object.assign(new Error('Optional phase expired before dispatch.'),{code:'CHROME_DIAGNOSTIC_BUDGET'});domain.command_count=1;domain.started_at=new Date().toISOString();}});
|
|
152
|
+
check();const result=diagnosticEnvelope(value,{method:domain.method,sessionId,timeout_ms:remaining});
|
|
153
|
+
Object.assign(domain,{state:result.state,reason:result.reason,remote_dispatched:result.remote_dispatched,bridge_elapsed_ms:result.elapsed_ms,elapsed_ms:performance.now()-begin,completed_at:new Date().toISOString(),...(result.error?{error:result.error}: {})});
|
|
154
|
+
if(result.state==='enabled')domain.enabled_acknowledged_at=domain.completed_at;
|
|
155
|
+
} catch(error) {
|
|
156
|
+
if(error.code==='CHROME_DIAGNOSTIC_BUDGET' && domain.command_count===0) {Object.assign(domain,{reason:'phase_budget_exhausted',completed_at:new Date().toISOString(),elapsed_ms:performance.now()-begin});continue;}
|
|
157
|
+
// Only a validated command envelope is optional. Abort, retirement, protocol
|
|
158
|
+
// and transport failure propagate, including after an enable was dispatched.
|
|
159
|
+
if(this.attachDiagnostics.get(targetId)===record && !domain.completed_at) Object.assign(domain,{state:domain.command_count?'unknown':'not_attempted',reason:signal?.aborted?'operation_aborted':record.retired||error.code==='CHROME_CDP_BINDING_CHANGED'?'session_retired':'transport_uncertain',elapsed_ms:performance.now()-begin,completed_at:new Date().toISOString()});
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
check();record.elapsed_ms=performance.now()-started;
|
|
164
|
+
}
|
|
89
165
|
async attach(targetId, signal) {
|
|
90
166
|
this.targetInfo.set(targetId, await this.request('checkTarget', { targetId }, { signal }));
|
|
91
167
|
if (this.attaching.has(targetId)) return this.attaching.get(targetId);
|
|
@@ -93,22 +169,24 @@ export class ChromeLabClient {
|
|
|
93
169
|
const attaching = (async () => {
|
|
94
170
|
const { sessionId } = await this.cdp('Target.attachToTarget', { targetId, flatten: true }, { signal });
|
|
95
171
|
this.sessions.set(targetId, sessionId);
|
|
172
|
+
this.collection(sessionId);
|
|
96
173
|
try {
|
|
97
174
|
await this.cdp('Runtime.enable', {}, { signal, sessionId });
|
|
98
|
-
await this.
|
|
99
|
-
await this.cdp('Network.enable', { maxTotalBufferSize: 100000, maxResourceBufferSize: 10000 }, { signal, sessionId }).catch(() => {});
|
|
175
|
+
await this.optionalDiagnostics(targetId,sessionId,signal);
|
|
100
176
|
return sessionId;
|
|
101
|
-
} catch (error) { this.sessions.delete(targetId); this.contexts.delete(sessionId); await this.cdp('Target.detachFromTarget', { sessionId }).catch(() => {}); throw error; }
|
|
177
|
+
} catch (error) { this.retireDiagnostic(sessionId); if(this.sessions.get(targetId)===sessionId)this.sessions.delete(targetId); this.contexts.delete(sessionId); await this.cdp('Target.detachFromTarget', { sessionId }).catch(() => {}); throw error; }
|
|
102
178
|
})();
|
|
103
179
|
this.attaching.set(targetId, attaching);
|
|
104
|
-
try { return await attaching; } finally { this.attaching.delete(targetId); }
|
|
180
|
+
try { return await attaching; } finally { if(this.attaching.get(targetId)===attaching)this.attaching.delete(targetId); }
|
|
105
181
|
}
|
|
106
|
-
async evaluate(targetId, expression, { signal, context_id, await_promise = false } = {}) {
|
|
182
|
+
async evaluate(targetId, expression, { signal, context_id, unique_context_id, read_only = false, await_promise = false, extension_only = false } = {}) {
|
|
183
|
+
if (read_only && (!unique_context_id || await_promise || context_id !== undefined)) throw Object.assign(new Error('Read-only inspection requires an exact unique context and synchronous evaluation.'), { code: 'CHROME_INSPECTION_CONTEXT' });
|
|
107
184
|
const sessionId = await this.attach(targetId, signal);
|
|
108
|
-
|
|
185
|
+
if (extension_only && (this.targetInfo.get(targetId)?.type !== 'page' || !this.targetInfo.get(targetId)?.url.startsWith('chrome-extension://'))) throw Object.assign(new Error('Typed runtime messages require an owned extension page.'),{code:'CHROME_MESSAGE_TARGET'});
|
|
186
|
+
const result = await this.cdp('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: await_promise, timeout: Math.min(10000, this.config.timeoutMs), allowUnsafeEvalBlockedByCSP: false, ...(context_id !== undefined ? { contextId: context_id } : {}), ...(unique_context_id ? { uniqueContextId: unique_context_id } : {}), ...(read_only ? { throwOnSideEffect: true, disableBreaks: true } : {}) }, { signal, sessionId });
|
|
109
187
|
if (result.exceptionDetails) {
|
|
110
188
|
const info = this.targetInfo.get(targetId);
|
|
111
|
-
return { ok: false, error: (result.exceptionDetails.exception?.description || result.exceptionDetails.text).slice(0, 4000), ...(info ? { target: { target_id: targetId, type: info.type, url: info.url } } : {}), ...(/^https?:/.test(info?.url || '') && context_id === undefined ? { next_action: 'This is the HTTP page main world. chrome.storage/tabs extension APIs belong to the OWN extension target, or a suitable isolated context from Contexts. Use the exact target by project URL; await Promise APIs with await_promise:true.' } : {}) };
|
|
189
|
+
return { ok: false, error: (result.exceptionDetails.exception?.description || result.exceptionDetails.text).slice(0, 4000), ...(info ? { target: { target_id: targetId, type: info.type, url: info.url } } : {}), ...(read_only ? { next_action: 'Read-only inspection failed. Use a synchronous expression whose side effects V8 can rule out in the exact accepted document. Do not fall back to effectful evaluation or await a Promise.' } : /^https?:/.test(info?.url || '') && context_id === undefined ? { next_action: 'This is the HTTP page main world. chrome.storage/tabs extension APIs belong to the OWN extension target, or a suitable isolated context from Contexts. Use the exact target by project URL; await Promise APIs with await_promise:true.' } : {}) };
|
|
112
190
|
}
|
|
113
191
|
const value = result.result.value ?? result.result.unserializableValue ?? null;
|
|
114
192
|
const serialized = Buffer.from(JSON.stringify(value));
|
|
@@ -116,7 +194,7 @@ export class ChromeLabClient {
|
|
|
116
194
|
return { ok: true, value };
|
|
117
195
|
}
|
|
118
196
|
async close() {
|
|
119
|
-
if (this.closed) return; this.closed = true;
|
|
197
|
+
if (this.closed) return; this.closed = true; for(const record of this.attachDiagnostics.values())this.retireDiagnostic(record.session_id);
|
|
120
198
|
if (!this.child || this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
121
199
|
await new Promise(resolve => { const timer = setTimeout(() => { this.child.kill('SIGKILL'); resolve(); }, 1500); this.child.once('exit', () => { clearTimeout(timer); resolve(); }); this.child.stdin.end(); });
|
|
122
200
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Builtin private attach protocol adopted from reviewed candidate03. No resident-controlled input or global health verdict.
|
|
2
|
+
export const OPTIONAL_PHASE_MS = 2000;
|
|
3
|
+
// UTF8 serialized JSON bytes. Oversize identity metadata becomes unavailable;
|
|
4
|
+
// identities are never truncated into a different, apparently exact binding.
|
|
5
|
+
export const PUBLIC_DIAGNOSTIC_BYTES = 16000;
|
|
6
|
+
export function diagnosticEnvelope(value, { method, sessionId, timeout_ms }) {
|
|
7
|
+
const fail = () => { throw Object.assign(new Error('Invalid attach diagnostic response envelope.'), { code: 'CHROME_DIAGNOSTIC_PROTOCOL' }); };
|
|
8
|
+
if (!value || Object.keys(value).length !== 1 || !value.diagnostic) return fail();
|
|
9
|
+
const d = value.diagnostic;
|
|
10
|
+
if (Object.keys(d).some(k => !['version','method','session_id','state','reason','remote_dispatched','timeout_ms','elapsed_ms','error'].includes(k)) ||
|
|
11
|
+
d.version !== 1 || d.method !== method || d.session_id !== sessionId || d.timeout_ms !== timeout_ms || d.remote_dispatched !== true ||
|
|
12
|
+
!Number.isFinite(d.elapsed_ms) || d.elapsed_ms < 0 ||
|
|
13
|
+
!['enabled','rejected','unknown'].includes(d.state) ||
|
|
14
|
+
!({enabled:'command_reply',rejected:'command_error',unknown:'command_timeout'}[d.state] === d.reason)) return fail();
|
|
15
|
+
if (d.state === 'rejected') {
|
|
16
|
+
if (!d.error || Object.keys(d.error).sort().join() !== 'code,message' || !Number.isInteger(d.error.code) || typeof d.error.message !== 'string' || d.error.message.length > 1000) return fail();
|
|
17
|
+
} else if (d.error !== undefined) return fail();
|
|
18
|
+
return structuredClone(d);
|
|
19
|
+
}
|