@ai-ecoverse/slicc-browsers 1.22.1 → 1.24.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 CHANGED
@@ -195,7 +195,7 @@ For stock Playwright, Puppeteer and raw CDP clients, a guard that declares `stoc
195
195
  - `Page.addScriptToEvaluateOnNewDocument` whose `source` matches `^(?:[ \t\n]|//[^\n\r\u2028\u2029]*)*$` on the whole string without the multiline flag (no code), with any `worldName` or `runImmediately`, and its removal;
196
196
  - exactly `Emulation.setFocusEmulationEnabled`, `setEmulatedMedia`, `setDeviceMetricsOverride` and `setTouchEmulationEnabled`;
197
197
  - `Page.createIsolatedWorld` on a frame the guard already classifies as ordinary: strip `grantUniveralAccess` and `contentSecurityPolicy`, return no `executionContextId` for any other frame; `Runtime.addBinding` scoped to such a world by a non-empty `executionContextName` or its `executionContextId`, with `Runtime.bindingCalled` only from those worlds;
198
- - `Target.setAutoAttach`: either narrow the filter to tab targets or hide per target; no protected-origin page, iframe or worker (dedicated, shared or service) may reach the client; pages under the tab are held like pages (same URL redaction in `targetInfo`); a transport that synthesizes it declares `syntheticAutoAttach`.
198
+ - `Target.setAutoAttach`: either narrow the filter to tab targets or hide per target; no protected-origin page, iframe (including ordinary-URL children of a protected parent) or worker may reach the client; pages under the tab are held like pages (same URL redaction in `targetInfo`); a transport that synthesizes it declares `syntheticAutoAttach`.
199
199
 
200
200
  The strip holds whether or not a given Chrome lets that world reach another origin. Code init scripts, main-world bindings and other overrides stay refused or harmless. Admitted display overrides and no-code init scripts must not remain in effect on a protected document — refuse that navigation, or clear them before it runs. Undeclared, they skip, never pass.
201
201
 
@@ -223,7 +223,7 @@ await fixture.close();
223
223
  You supply:
224
224
  - **a guarded client** per row: your guard's CDP connection, with `send(method, params, sessionId)`, `waitEvent(predicate, ms)` and an `events` array;
225
225
  - **an unguarded observer**: a direct CDP connection to the same browser, which runs the controls and checks liveness;
226
- - **origins**: the fixture server reachable as seven, the vault and an ordinary site. Point the protected hostnames at it with Chrome's `--host-resolver-rules`, so tests never touch the real seven;
226
+ - **origins**: the fixture server reachable as seven, the vault, an ordinary site, and for some rows a cross-site ordinary `{cross}` (e.g. `cross.test`). Point those hostnames at it with Chrome's `--host-resolver-rules`, so tests never touch the real seven;
227
227
  - **capabilities**: what your transport can do. A row that needs something you don't declare is skipped as `not-supported:<capability>`.
228
228
 
229
229
  | capability | means the transport can |
@@ -239,7 +239,7 @@ You supply:
239
239
  | `siteIsolationOff`, `inboundDelay`, `forwardAudit` | no site isolation; hold inbound; audit forwarded commands |
240
240
  | `secureOrigin` | HTTPS origins (service workers) |
241
241
  | `stockSetup`, `syntheticAutoAttach` | admit stock clients' setup calls; synthesize auto-attach |
242
- | `closeProtected` | close a protected tab |
242
+ | `closeProtected` | close a protected tab, except live vault/seven-UI disguises |
243
243
 
244
244
  ### Reading the result
245
245
 
@@ -0,0 +1,563 @@
1
+ import { outcome, parseURL, waitFor } from './guard-driver.mjs';
2
+ import { createObserverTarget } from './observer-target.mjs';
3
+
4
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
+ const refused = (out) => !out.ok && /^slicc: refused:/.test(out.error?.message ?? '');
6
+
7
+ const peel = (text) => {
8
+ let u = parseURL(text);
9
+ for (
10
+ let i = 0;
11
+ u && ['blob', 'filesystem', 'view-source'].includes(u.protocol.slice(0, -1)) && i < 8;
12
+ i++
13
+ )
14
+ u = parseURL(u.href.slice(u.protocol.length));
15
+ return u;
16
+ };
17
+
18
+ const hostOf = (u) => {
19
+ const host = (u?.hostname ?? '').toLowerCase();
20
+ return host.endsWith('.') ? host.slice(0, -1) : host;
21
+ };
22
+
23
+ export function keptDocument(text) {
24
+ const u = peel(text);
25
+ if (!u || !['http:', 'https:'].includes(u.protocol)) return false;
26
+ const host = hostOf(u);
27
+ if (host === 'vault.sliccy.ai' || (host.startsWith('vault-') && host.endsWith('.sliccy.ai')))
28
+ return true;
29
+ const path = u.pathname || '/';
30
+ return host === 'seven.sliccy.ai' && path.startsWith('/os/') && path !== '/os/callback.html';
31
+ }
32
+
33
+ export function keptSecurityOrigin(url, origin) {
34
+ if (keptDocument(origin)) return true;
35
+ const u = parseURL(url);
36
+ const opaque =
37
+ u?.protocol === 'about:' && ['blank', 'srcdoc'].includes((u.pathname || '').toLowerCase());
38
+ if (!opaque) return false;
39
+ const o = peel(origin);
40
+ if (!o || !['http:', 'https:'].includes(o.protocol)) return false;
41
+ const host = hostOf(o);
42
+ return (
43
+ host === 'seven.sliccy.ai' ||
44
+ host === 'vault.sliccy.ai' ||
45
+ (host.startsWith('vault-') && host.endsWith('.sliccy.ai'))
46
+ );
47
+ }
48
+
49
+ export function unclosableShape({ url, documentURL, securityOrigin, navigating, navigationURL }) {
50
+ if (navigating && (!navigationURL || keptDocument(navigationURL))) return true;
51
+ const error =
52
+ documentURL === 'chrome-error://chromewebdata/' ||
53
+ (typeof documentURL === 'string' && documentURL.startsWith('chrome-error:'));
54
+ const keptNow =
55
+ keptDocument(url) ||
56
+ keptDocument(documentURL) ||
57
+ keptSecurityOrigin(documentURL ?? url, securityOrigin ?? '');
58
+ if (!keptNow) return false;
59
+ if (!error) return true;
60
+ return keptDocument(documentURL) || keptSecurityOrigin(documentURL, securityOrigin ?? '');
61
+ }
62
+
63
+ async function lockChecks({
64
+ client,
65
+ reader,
66
+ session,
67
+ targetId,
68
+ result,
69
+ until,
70
+ closeOnly,
71
+ skipEvaluate,
72
+ attachRefused,
73
+ }) {
74
+ if (!closeOnly) {
75
+ if (session && !skipEvaluate) {
76
+ result.evaluateRefused = refused(
77
+ await outcome(
78
+ client.send(
79
+ 'Runtime.evaluate',
80
+ { expression: 'document.title', returnByValue: true },
81
+ session
82
+ )
83
+ )
84
+ );
85
+ }
86
+ result.attachRefused = attachRefused;
87
+ }
88
+ await measureClose({ client, reader, targetId, result, until });
89
+ }
90
+
91
+ async function measureClose({ client, reader, targetId, result, until }) {
92
+ const close = await outcome(client.send('Target.closeTarget', { targetId }));
93
+ const gone = await until(
94
+ async () =>
95
+ !(await reader.send('Target.getTargets')).targetInfos.some((i) => i.targetId === targetId)
96
+ );
97
+ result.closed = gone;
98
+ result.closeRefused = refused(close) && !gone;
99
+ }
100
+
101
+ async function attachClient(client, targetId) {
102
+ const attached = await outcome(client.send('Target.attachToTarget', { targetId, flatten: true }));
103
+ const session = attached.result?.sessionId;
104
+ if (session) {
105
+ await outcome(client.send('Page.enable', {}, session));
106
+ await outcome(client.send('Runtime.enable', {}, session));
107
+ }
108
+ return { session, attachRefused: refused(attached) };
109
+ }
110
+
111
+ async function observerPage(reader, url, notes, internalSessions) {
112
+ const opened = await outcome(
113
+ createObserverTarget(reader, { url }, notes).then(async (created) => {
114
+ const { sessionId } = await reader.send('Target.attachToTarget', {
115
+ targetId: created.targetId,
116
+ flatten: true,
117
+ });
118
+ await reader.send('Page.enable', {}, sessionId);
119
+ await reader.send('Runtime.enable', {}, sessionId);
120
+ return { targetId: created.targetId, sessionId };
121
+ })
122
+ );
123
+ if (!opened.ok) return opened;
124
+ internalSessions.add(opened.result.sessionId);
125
+ return opened;
126
+ }
127
+
128
+ function noteShape(notes, label, evidence) {
129
+ notes.push(`${label}: ${JSON.stringify(evidence)}`);
130
+ }
131
+
132
+ async function shapeViewSource({ reader, shapedURL, notes, internalSessions, until }) {
133
+ const opened = await observerPage(reader, 'about:blank', notes, internalSessions);
134
+ if (!opened.ok) return opened;
135
+ const { targetId, sessionId } = opened.result;
136
+ await reader.send('Page.navigate', { url: shapedURL }, sessionId);
137
+ let info;
138
+ const live = await until(async () => {
139
+ ({ targetInfo: info } = await reader.send('Target.getTargetInfo', { targetId }));
140
+ return info.url === shapedURL || info.url.startsWith('view-source:');
141
+ });
142
+ noteShape(notes, 'view-source', {
143
+ targetId,
144
+ type: info?.type,
145
+ url: info?.url,
146
+ });
147
+ return { ok: true, result: { targetId, sessionId, live, shaped: live } };
148
+ }
149
+
150
+ async function shapeBlob({ reader, origin, tag, notes, internalSessions, until }) {
151
+ const page = `${origin}/blob-nav?n=${encodeURIComponent(tag)}`;
152
+ const opened = await observerPage(reader, page, notes, internalSessions);
153
+ if (!opened.ok) return opened;
154
+ const { targetId, sessionId } = opened.result;
155
+ let info;
156
+ const live = await until(async () => {
157
+ ({ targetInfo: info } = await reader.send('Target.getTargetInfo', { targetId }));
158
+ return info.url.startsWith('blob:');
159
+ });
160
+ noteShape(notes, 'blob', { targetId, type: info?.type, url: info?.url });
161
+ return { ok: true, result: { targetId, sessionId, live, shaped: live } };
162
+ }
163
+
164
+ async function shapeUserinfo({ reader, shapedURL, notes, internalSessions, until }) {
165
+ const opened = await observerPage(reader, 'about:blank', notes, internalSessions);
166
+ if (!opened.ok) return opened;
167
+ const { targetId, sessionId } = opened.result;
168
+ await reader.send('Page.navigate', { url: shapedURL }, sessionId);
169
+ let info;
170
+ let frame;
171
+ const live = await until(async () => {
172
+ ({
173
+ frameTree: { frame },
174
+ } = await reader.send('Page.getFrameTree', {}, sessionId));
175
+ ({ targetInfo: info } = await reader.send('Target.getTargetInfo', { targetId }));
176
+ return (
177
+ info.url.includes('@') &&
178
+ info.url.includes('/os/') &&
179
+ frame.url.includes('@') &&
180
+ !frame.unreachableUrl
181
+ );
182
+ });
183
+ noteShape(notes, 'userinfo', {
184
+ targetId,
185
+ type: info?.type,
186
+ url: info?.url,
187
+ frameUrl: frame?.url,
188
+ hasUserinfo: Boolean(info?.url?.includes('@')),
189
+ });
190
+ if (!live && info?.url?.includes('/os/') && !info.url.includes('@'))
191
+ return {
192
+ ok: true,
193
+ result: { incomplete: 'userinfo stripped from top-level target URL' },
194
+ };
195
+ return { ok: true, result: { targetId, sessionId, live, shaped: live } };
196
+ }
197
+
198
+ const raced = (promise, ms) => Promise.race([promise, pause(ms).then(() => undefined)]);
199
+
200
+ async function shapeNavigating({
201
+ reader,
202
+ ordinary,
203
+ destination,
204
+ notes,
205
+ internalSessions,
206
+ hold,
207
+ releaseSlow,
208
+ tag,
209
+ }) {
210
+ const started = Date.now();
211
+ const waitBudget = 8000;
212
+ const start = `${ordinary}/blank?n=close-nav-start`;
213
+ notes.push(`navigating.start: destination=${destination} budgetMs=${waitBudget}`);
214
+ notes.push('navigating.fetch: skipped; fixture /slow holds Document (no Fetch.enable)');
215
+ const opened = await raced(observerPage(reader, start, notes, internalSessions), 3000);
216
+ if (!opened?.ok)
217
+ return {
218
+ ok: true,
219
+ result: {
220
+ incomplete: `navigating shape did not appear: observer setup ${opened?.error?.message ?? 'timed out'}`,
221
+ },
222
+ };
223
+ const { targetId, sessionId } = opened.result;
224
+ hold.targetId = targetId;
225
+ const sequence = [];
226
+ let cursor = reader.events.length;
227
+ let stopped = false;
228
+ const elapsed = () => Date.now() - started;
229
+ const readInfo = async () => {
230
+ const info = await raced(reader.send('Target.getTargetInfo', { targetId }), 400);
231
+ const listed = await raced(reader.send('Target.getTargets'), 400);
232
+ const row = listed?.targetInfos?.find((i) => i.targetId === targetId);
233
+ return info?.targetInfo ?? row;
234
+ };
235
+ const snap = async (label) => {
236
+ const targetInfo = await readInfo();
237
+ notes.push(
238
+ `navigating.${label}: ${JSON.stringify({
239
+ atMs: elapsed(),
240
+ targetId,
241
+ type: targetInfo?.type,
242
+ url: targetInfo?.url,
243
+ title: targetInfo?.title,
244
+ attached: targetInfo?.attached,
245
+ openerId: targetInfo?.openerId,
246
+ pendingUrl: destination,
247
+ observerSession: sessionId,
248
+ sequence: sequence.slice(-12),
249
+ })}`
250
+ );
251
+ return targetInfo;
252
+ };
253
+ hold.release = async () => {
254
+ stopped = true;
255
+ releaseSlow?.(tag);
256
+ await pause(20);
257
+ };
258
+ hold.abort = hold.release;
259
+ await snap('0s');
260
+ const pump = (async () => {
261
+ while (!stopped) {
262
+ for (const event of reader.events.slice(cursor)) {
263
+ cursor++;
264
+ if (
265
+ event.method === 'Fetch.requestPaused' ||
266
+ event.method === 'Page.frameStartedNavigating' ||
267
+ event.method === 'Page.frameRequestedNavigation' ||
268
+ event.method === 'Page.frameNavigated'
269
+ ) {
270
+ const row = {
271
+ atMs: elapsed(),
272
+ method: event.method,
273
+ sessionId: event.sessionId,
274
+ url: event.params.request?.url ?? event.params.url ?? event.params.frame?.url,
275
+ resourceType: event.params.resourceType,
276
+ requestStage: event.params.requestStage,
277
+ frameId: event.params.frameId ?? event.params.frame?.id,
278
+ };
279
+ sequence.push(row);
280
+ notes.push(`navigating.event: ${JSON.stringify(row)}`);
281
+ }
282
+ }
283
+ await pause(5);
284
+ }
285
+ })();
286
+ await raced(reader.send('Page.enable', {}, sessionId), 500);
287
+ notes.push(`navigating.navigate: not-awaited url=${destination} session=${sessionId}`);
288
+ reader
289
+ .send('Page.navigate', { url: destination }, sessionId)
290
+ .then((result) => {
291
+ notes.push(
292
+ `navigating.navigateResult: ${JSON.stringify({
293
+ atMs: elapsed(),
294
+ frameId: result?.frameId,
295
+ loaderId: result?.loaderId,
296
+ errorText: result?.errorText,
297
+ })}`
298
+ );
299
+ })
300
+ .catch((error) => {
301
+ notes.push(`navigating.navigateSettled: ${error?.message ?? error}`);
302
+ });
303
+ const marks = [1000, 2000, 5000];
304
+ let mark = 0;
305
+ let targetInfo;
306
+ let pending = false;
307
+ const deadline = started + waitBudget;
308
+ while (Date.now() < deadline && !pending) {
309
+ pending = sequence.some(
310
+ (event) =>
311
+ (event.method === 'Page.frameStartedNavigating' ||
312
+ event.method === 'Page.frameRequestedNavigation' ||
313
+ event.method === 'Fetch.requestPaused') &&
314
+ (event.url === destination || String(event.url ?? '').includes('/slow'))
315
+ );
316
+ if (!pending) {
317
+ targetInfo = await readInfo();
318
+ pending = targetInfo?.url === destination || String(targetInfo?.url ?? '').includes('/slow');
319
+ }
320
+ while (mark < marks.length && elapsed() >= marks[mark]) {
321
+ await snap(`${marks[mark] / 1000}s`);
322
+ mark++;
323
+ }
324
+ if (!pending) await pause(25);
325
+ }
326
+ stopped = true;
327
+ await Promise.race([pump, pause(20)]);
328
+ targetInfo = targetInfo ?? (await readInfo());
329
+ const shaped = pending;
330
+ notes.push(
331
+ `navigating.done: ${JSON.stringify({
332
+ atMs: elapsed(),
333
+ shaped,
334
+ url: targetInfo?.url,
335
+ title: targetInfo?.title,
336
+ attached: targetInfo?.attached,
337
+ pendingUrl: destination,
338
+ sequence: sequence.slice(-20),
339
+ })}`
340
+ );
341
+ if (!shaped) {
342
+ notes.push(`navigating.giveUp: no pending /slow within ${waitBudget}ms`);
343
+ return {
344
+ ok: true,
345
+ result: {
346
+ incomplete: `navigating shape did not appear: no pending /slow within ${waitBudget}ms`,
347
+ targetId,
348
+ sessionId,
349
+ },
350
+ };
351
+ }
352
+ return {
353
+ ok: true,
354
+ result: {
355
+ targetId,
356
+ sessionId,
357
+ live: true,
358
+ shaped: true,
359
+ navigating: true,
360
+ navigationURL: destination,
361
+ closeOnly: true,
362
+ },
363
+ };
364
+ }
365
+
366
+ async function shapeBlankInherited({ reader, origin, tag, notes, internalSessions, until }) {
367
+ const page = `${origin}/blank-open?n=${encodeURIComponent(tag)}`;
368
+ const opened = await observerPage(reader, page, notes, internalSessions);
369
+ if (!opened.ok) return opened;
370
+ const parent = opened.result;
371
+ const before = new Set(
372
+ (await reader.send('Target.getTargets')).targetInfos.map((info) => info.targetId)
373
+ );
374
+ const openedBlank = await outcome(
375
+ reader.send(
376
+ 'Runtime.evaluate',
377
+ { expression: "window.open(''); undefined", returnByValue: true, userGesture: true },
378
+ parent.sessionId
379
+ )
380
+ );
381
+ if (!openedBlank.ok)
382
+ return {
383
+ ok: true,
384
+ result: {
385
+ incomplete: `window.open failed: ${openedBlank.error?.message ?? openedBlank.error}`,
386
+ parentId: parent.targetId,
387
+ },
388
+ };
389
+ let child;
390
+ const found = await until(async () => {
391
+ const listed = await reader.send('Target.getTargets');
392
+ child = listed.targetInfos.find(
393
+ (info) =>
394
+ !before.has(info.targetId) &&
395
+ info.type === 'page' &&
396
+ (info.url === 'about:blank' || info.url === '') &&
397
+ info.openerId === parent.targetId
398
+ );
399
+ return Boolean(child);
400
+ });
401
+ if (!found) {
402
+ noteShape(notes, 'blank-inherited', {
403
+ parentId: parent.targetId,
404
+ popup: null,
405
+ reason: 'no about:blank page target with opener',
406
+ });
407
+ return {
408
+ ok: true,
409
+ result: {
410
+ incomplete: 'about:blank popup with opener was not created',
411
+ parentId: parent.targetId,
412
+ },
413
+ };
414
+ }
415
+ const { sessionId } = await reader.send('Target.attachToTarget', {
416
+ targetId: child.targetId,
417
+ flatten: true,
418
+ });
419
+ internalSessions.add(sessionId);
420
+ await reader.send('Page.enable', {}, sessionId);
421
+ let securityOrigin;
422
+ try {
423
+ const { frameTree } = await reader.send('Page.getFrameTree', {}, sessionId);
424
+ securityOrigin = frameTree.frame.securityOrigin;
425
+ } catch {
426
+ securityOrigin = undefined;
427
+ }
428
+ noteShape(notes, 'blank-inherited', {
429
+ targetId: child.targetId,
430
+ type: child.type,
431
+ url: child.url,
432
+ openerId: child.openerId,
433
+ parentId: parent.targetId,
434
+ securityOrigin,
435
+ });
436
+ return {
437
+ ok: true,
438
+ result: {
439
+ targetId: child.targetId,
440
+ sessionId,
441
+ parentId: parent.targetId,
442
+ live: true,
443
+ shaped: true,
444
+ securityOrigin,
445
+ },
446
+ };
447
+ }
448
+
449
+ export async function closeProtectedShape({
450
+ client,
451
+ reader,
452
+ ordinary,
453
+ protectedOrigin,
454
+ shape,
455
+ tag,
456
+ internalSessions = new Set(),
457
+ notes = [],
458
+ budget = 5000,
459
+ releaseSlow,
460
+ }) {
461
+ const until = (check) => waitFor(check, budget);
462
+ const result = {};
463
+ const hold = {};
464
+ let shaped;
465
+ const missing = async (reason) => {
466
+ hold.abort?.();
467
+ await hold.release?.();
468
+ const targetId = shaped?.result?.targetId ?? hold.targetId;
469
+ const parentId = shaped?.result?.parentId ?? hold.parentId;
470
+ if (targetId) await outcome(reader.send('Target.closeTarget', { targetId }));
471
+ if (parentId) await outcome(reader.send('Target.closeTarget', { targetId: parentId }));
472
+ return { ...result, incomplete: `close-protected-shape: ${reason}` };
473
+ };
474
+ if (!reader) return missing('missing unguarded reader');
475
+ const origin = protectedOrigin;
476
+ if (shape === 'view-source') {
477
+ shaped = await shapeViewSource({
478
+ reader,
479
+ shapedURL: `view-source:${origin}/doc?n=${encodeURIComponent(tag)}`,
480
+ notes,
481
+ internalSessions,
482
+ until,
483
+ });
484
+ } else if (shape === 'blob') {
485
+ shaped = await shapeBlob({ reader, origin, tag, notes, internalSessions, until });
486
+ } else if (shape === 'userinfo') {
487
+ const host = parseURL(origin)?.host;
488
+ const shapedURL = `${parseURL(origin)?.protocol}//user:pass@${host}/os/index.html?n=${encodeURIComponent(tag)}`;
489
+ shaped = await shapeUserinfo({ reader, shapedURL, notes, internalSessions, until });
490
+ } else if (shape === 'navigating') {
491
+ const navigating = shapeNavigating({
492
+ reader,
493
+ ordinary,
494
+ destination: `${origin}/slow?n=${encodeURIComponent(tag)}&ms=15000`,
495
+ notes,
496
+ internalSessions,
497
+ hold,
498
+ releaseSlow,
499
+ tag,
500
+ }).then((value) => ({ timedOut: false, value }));
501
+ let hardTimer;
502
+ const hard = new Promise((resolve) => {
503
+ hardTimer = setTimeout(() => resolve({ timedOut: true }), 9000);
504
+ });
505
+ const winner = await Promise.race([navigating, hard]);
506
+ clearTimeout(hardTimer);
507
+ if (winner.timedOut) {
508
+ notes.push('navigating.giveUp: hard 9s shape budget');
509
+ hold.abort?.();
510
+ await hold.release?.();
511
+ shaped = {
512
+ ok: true,
513
+ result: {
514
+ incomplete: 'navigating shape did not appear: hard 9s shape budget',
515
+ targetId: hold.targetId,
516
+ },
517
+ };
518
+ } else shaped = winner.value;
519
+ } else if (shape === 'blank-inherited') {
520
+ shaped = await shapeBlankInherited({ reader, origin, tag, notes, internalSessions, until });
521
+ } else {
522
+ return await missing(`unknown shape ${shape}`);
523
+ }
524
+ if (!shaped.ok)
525
+ return await missing(`observer setup failed: ${shaped.error?.message ?? shaped.error}`);
526
+ if (shaped.result.incomplete) return await missing(shaped.result.incomplete);
527
+ if (!shaped.result.live) return await missing(`shape ${shape} did not appear`);
528
+ Object.assign(result, {
529
+ shaped: shaped.result.shaped,
530
+ live: shaped.result.live,
531
+ });
532
+ const { targetId } = shaped.result;
533
+ const closeOnly = shaped.result.closeOnly === true;
534
+ const { session, attachRefused } = await attachClient(client, targetId);
535
+ if (!session) {
536
+ if (!closeOnly) result.attachRefused = attachRefused;
537
+ await measureClose({ client, reader, targetId, result, until });
538
+ await hold.release?.();
539
+ await outcome(reader.send('Target.closeTarget', { targetId }));
540
+ if (shaped.result.parentId)
541
+ await outcome(reader.send('Target.closeTarget', { targetId: shaped.result.parentId }));
542
+ return result;
543
+ }
544
+ try {
545
+ await lockChecks({
546
+ client,
547
+ reader,
548
+ session,
549
+ targetId,
550
+ result,
551
+ until,
552
+ closeOnly,
553
+ skipEvaluate: true,
554
+ attachRefused,
555
+ });
556
+ return result;
557
+ } finally {
558
+ await hold.release?.();
559
+ await outcome(reader.send('Target.closeTarget', { targetId }));
560
+ if (shaped.result.parentId)
561
+ await outcome(reader.send('Target.closeTarget', { targetId: shaped.result.parentId }));
562
+ }
563
+ }