@ai-ecoverse/slicc-browsers 1.22.1 → 1.23.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
@@ -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
+ }
@@ -76,6 +76,16 @@ function route(req, url, send, html, redirects) {
76
76
  { sourcemap: '/listener.js.map' }
77
77
  ),
78
78
  '/page': () => html('trigger', trigger(p)),
79
+ '/blob-nav': () =>
80
+ html(
81
+ 'blob-nav',
82
+ `<p>blob-nav ${escape(p.get('n') ?? '')}</p><script>const b=URL.createObjectURL(new Blob(['<!doctype html><title>blob</title><p>blob ${escape(p.get('n') ?? '')}</p>'],{type:'text/html'}));location.replace(b);</script>`
83
+ ),
84
+ '/blank': () => html('blank', `<p>blank ${escape(p.get('n') ?? '')}</p>`),
85
+ '/blank-open': () => html('blank-open', `<p>blank-open ${escape(p.get('n') ?? '')}</p>`),
86
+ '/os/': () => html('os', `<p>os ${escape(p.get('n') ?? '')}</p>`),
87
+ '/os/index.html': () => html('os', `<p>os ${escape(p.get('n') ?? '')}</p>`),
88
+ '/os/callback.html': () => html('os-callback', `<p>callback ${escape(p.get('n') ?? '')}</p>`),
79
89
  '/api/cookies': () => send(200, req.headers.cookie ?? 'none', 'text/plain'),
80
90
  '/plant': () =>
81
91
  html('plant', '<script>localStorage.setItem("canary","planted")</script>', {
@@ -147,10 +157,31 @@ function requestURL(req, scheme) {
147
157
  };
148
158
  }
149
159
 
160
+ async function holdSlow({ req, res, url, p, html, entries, history, slows }) {
161
+ const n = p.get('n') ?? '';
162
+ const ms = Math.min(Math.max(Number(p.get('ms') ?? 15000) || 15000, 1), 60000);
163
+ const entry = {
164
+ time: Date.now(),
165
+ url: url.href,
166
+ method: req.method,
167
+ headers: { ...req.headers },
168
+ };
169
+ entries.push(entry);
170
+ history.push(entry);
171
+ await new Promise((resolve) => {
172
+ const timer = setTimeout(resolve, ms);
173
+ slows.set(n, { resolve, timer });
174
+ });
175
+ slows.delete(n);
176
+ if (res.writableEnded) return;
177
+ return html('slow', `<p>slow ${escape(n)}</p>`);
178
+ }
179
+
150
180
  export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency } = {}) {
151
181
  let entries = [];
152
182
  const history = [];
153
183
  const redirects = new Map();
184
+ const slows = new Map();
154
185
  const redirectSecret = async ({ path, host = '', search = '', remove, ...value }) => {
155
186
  if (
156
187
  typeof path !== 'string' ||
@@ -162,6 +193,14 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency
162
193
  if (remove) redirects.delete(key);
163
194
  else redirects.set(key, value);
164
195
  };
196
+ const releaseSlow = (n) => {
197
+ const entry = slows.get(String(n ?? ''));
198
+ if (!entry) return false;
199
+ clearTimeout(entry.timer);
200
+ entry.resolve();
201
+ slows.delete(String(n ?? ''));
202
+ return true;
203
+ };
165
204
  const scheme = tls ? 'https' : 'http';
166
205
  const handler = async (req, res) => {
167
206
  const url = requestURL(req, scheme);
@@ -183,6 +222,10 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency
183
222
  'text/html',
184
223
  headers
185
224
  );
225
+ if (req.method === 'GET' && url.pathname === '/slow') {
226
+ await holdSlow({ req, res, url, p, html, entries, history, slows });
227
+ return;
228
+ }
186
229
  if (req.method === 'GET' && url.pathname === '/log') {
187
230
  const from = Number(p.get('since') ?? 0);
188
231
  if (!Number.isInteger(from) || from < 0 || from > history.length)
@@ -220,6 +263,7 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency
220
263
  return {
221
264
  redirects,
222
265
  redirectSecret,
266
+ releaseSlow,
223
267
  port: address.port,
224
268
  origin: `${scheme}://${host}:${address.port}`,
225
269
  log: () => structuredClone(entries),
@@ -228,6 +272,7 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency
228
272
  },
229
273
  close: () =>
230
274
  new Promise((resolve, reject) => {
275
+ for (const n of [...slows.keys()]) releaseSlow(n);
231
276
  server.closeAllConnections();
232
277
  server.close((error) => (error ? reject(error) : resolve()));
233
278
  }),
package/guard-runner.mjs CHANGED
@@ -3,6 +3,7 @@ import { fixtureClient, prepareFixture } from './fixture-client.mjs';
3
3
  export { fixtureClient };
4
4
 
5
5
  import { bfcacheRestore } from './bfcache-restore.mjs';
6
+ import { closeProtectedShape } from './close-protected-shape.mjs';
6
7
  import { extensionFrame } from './extension-frame.mjs';
7
8
  import { DEFAULT_EXTENSION_WORLD_ID, extensionWorld } from './extension-world.mjs';
8
9
  import { frameRead } from './frame-read.mjs';
@@ -194,6 +195,7 @@ class Execution {
194
195
  this.unsupported = undefined;
195
196
  this.preSent = [];
196
197
  this.notes = [];
198
+ this.sharedNotes = Array.isArray(options.notes) ? options.notes : undefined;
197
199
  this.answers = [];
198
200
  this.internalSessions = new Set();
199
201
  this.attachSettings = new Map();
@@ -222,6 +224,10 @@ class Execution {
222
224
  note(message) {
223
225
  if (this.notes.length >= 80) this.notes.shift();
224
226
  Array.prototype.push.call(this.notes, message);
227
+ if (this.sharedNotes && this.sharedNotes !== this.notes) {
228
+ if (this.sharedNotes.length >= 80) this.sharedNotes.shift();
229
+ Array.prototype.push.call(this.sharedNotes, message);
230
+ }
225
231
  }
226
232
 
227
233
  installClientTrace() {
@@ -894,6 +900,7 @@ class Execution {
894
900
  'redirectSecret',
895
901
  'unparseable',
896
902
  'errorPage',
903
+ 'closeProtectedShape',
897
904
  'extensionWorld',
898
905
  'extensionFrame',
899
906
  'frameRead',
@@ -902,9 +909,14 @@ class Execution {
902
909
  if (probe && !out.ok) this.errors.push(`${probe}: ${out.error?.message ?? 'probe failed'}`);
903
910
  if (step.client && !out.ok) this.errors.push(`client exit: ${out.error.message}`);
904
911
  if (
905
- ['errorPage', 'redirectSecret', 'extensionFrame', 'frameRead', 'bfcacheRestore'].includes(
906
- probe
907
- ) &&
912
+ [
913
+ 'errorPage',
914
+ 'closeProtectedShape',
915
+ 'redirectSecret',
916
+ 'extensionFrame',
917
+ 'frameRead',
918
+ 'bfcacheRestore',
919
+ ].includes(probe) &&
908
920
  out.ok &&
909
921
  out.result?.incomplete
910
922
  )
@@ -1000,9 +1012,14 @@ class Execution {
1000
1012
  return;
1001
1013
  }
1002
1014
  if (step.client) return outcome(this.exit());
1003
- if (step.redirectSecret || step.errorPage) return this.originProbe(step, session);
1004
- if (step.spawnWorker) return this.spawnWorker(step);
1005
- if (step.probeSessions) return this.probeSessions(step);
1015
+ if (
1016
+ step.redirectSecret ||
1017
+ step.errorPage ||
1018
+ step.closeProtectedShape ||
1019
+ step.spawnWorker ||
1020
+ step.probeSessions
1021
+ )
1022
+ return this.stepProbe(step, session);
1006
1023
  if (step.bfcacheRestore)
1007
1024
  return outcome(
1008
1025
  bfcacheRestore({
@@ -1143,6 +1160,13 @@ class Execution {
1143
1160
  }
1144
1161
  }
1145
1162
 
1163
+ stepProbe(step, session) {
1164
+ if (step.redirectSecret || step.errorPage) return this.originProbe(step, session);
1165
+ if (step.closeProtectedShape) return this.runCloseProtectedShape(step);
1166
+ if (step.spawnWorker) return this.spawnWorker(step);
1167
+ return this.probeSessions(step);
1168
+ }
1169
+
1146
1170
  async originProbe(step, session) {
1147
1171
  if (step.redirectSecret) {
1148
1172
  this.internalSessions.add(session);
@@ -1187,6 +1211,23 @@ class Execution {
1187
1211
  );
1188
1212
  }
1189
1213
 
1214
+ runCloseProtectedShape(step) {
1215
+ return outcome(
1216
+ closeProtectedShape({
1217
+ client: this.client,
1218
+ reader: this.reader,
1219
+ ordinary: this.vars.ordinary,
1220
+ protectedOrigin: this.vars.target,
1221
+ shape: step.closeProtectedShape,
1222
+ tag: this.vars.tag,
1223
+ internalSessions: this.internalSessions,
1224
+ notes: this.notes,
1225
+ budget: this.bound(5000),
1226
+ releaseSlow: this.options.fixture.releaseSlow,
1227
+ })
1228
+ );
1229
+ }
1230
+
1190
1231
  spawnWorkerKind() {
1191
1232
  return allSteps([...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])]).find(
1192
1233
  (step) => step.spawnWorker
@@ -2175,7 +2216,7 @@ const observerRequired = (check) => {
2175
2216
  const needsObserver = (vector) =>
2176
2217
  vector.control.via === 'unguarded' ||
2177
2218
  allSteps([...(vector.setup ?? []), ...(vector.trigger ?? [])]).some(
2178
- (step) => step.isolatedWorld || step.observer || step.spawnWorker
2219
+ (step) => step.isolatedWorld || step.observer || step.spawnWorker || step.closeProtectedShape
2179
2220
  ) ||
2180
2221
  observerRequired(vector.liveCheck) ||
2181
2222
  Object.values(vector.observe ?? {}).some(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "setVersion": "1.22.1",
3
+ "setVersion": "1.23.0",
4
4
  "sources": [
5
5
  {
6
6
  "repo": "ai-ecoverse/slicc-cli",
@@ -2406,6 +2406,410 @@
2406
2406
  "fullURL": false
2407
2407
  }
2408
2408
  },
2409
+ {
2410
+ "id": "C8.close-protected.view-source",
2411
+ "family": "C",
2412
+ "kind": "cdp",
2413
+ "scope": "any",
2414
+ "defaultContext": true,
2415
+ "requires": ["closeProtected"],
2416
+ "setup": [
2417
+ {
2418
+ "page": "tab",
2419
+ "url": "{ordinary}/blank"
2420
+ }
2421
+ ],
2422
+ "trigger": [
2423
+ {
2424
+ "closeProtectedShape": "view-source",
2425
+ "as": "shape",
2426
+ "requires": ["closeProtected"]
2427
+ }
2428
+ ],
2429
+ "targets": ["vault"],
2430
+ "observe": {
2431
+ "shaped": {
2432
+ "means": "closeProtected view-source vault: the unguarded observer opened the shaped tab",
2433
+ "check": {
2434
+ "result": "shape",
2435
+ "path": "result.shaped",
2436
+ "equals": true,
2437
+ "observer": "client"
2438
+ }
2439
+ },
2440
+ "attachRefused": {
2441
+ "means": "closeProtected view-source vault: Target.attachToTarget through the client is refused",
2442
+ "check": {
2443
+ "result": "shape",
2444
+ "path": "result.attachRefused",
2445
+ "equals": true,
2446
+ "observer": "client"
2447
+ }
2448
+ },
2449
+ "closeRefused": {
2450
+ "means": "closeProtected view-source vault: Target.closeTarget through the client is refused",
2451
+ "check": {
2452
+ "result": "shape",
2453
+ "path": "result.closeRefused",
2454
+ "equals": true,
2455
+ "observer": "client"
2456
+ }
2457
+ },
2458
+ "closed": {
2459
+ "means": "closeProtected view-source vault: Target.closeTarget on the locked tab is answered with success and the tab is gone",
2460
+ "check": {
2461
+ "result": "shape",
2462
+ "path": "result.closed",
2463
+ "equals": true,
2464
+ "observer": "client"
2465
+ }
2466
+ }
2467
+ },
2468
+ "live": "the unguarded observer opened the shaped tab",
2469
+ "liveCheck": {
2470
+ "result": "shape",
2471
+ "path": "result.live",
2472
+ "equals": true,
2473
+ "observer": "client"
2474
+ },
2475
+ "control": {
2476
+ "origin": "ordinary",
2477
+ "via": "unguarded",
2478
+ "expected": {
2479
+ "shaped": true,
2480
+ "attachRefused": false,
2481
+ "closeRefused": false,
2482
+ "closed": true
2483
+ }
2484
+ },
2485
+ "expected": {
2486
+ "shaped": true,
2487
+ "attachRefused": true,
2488
+ "closeRefused": true,
2489
+ "closed": false
2490
+ }
2491
+ },
2492
+ {
2493
+ "id": "C8.close-protected.blob",
2494
+ "family": "C",
2495
+ "kind": "cdp",
2496
+ "scope": "any",
2497
+ "defaultContext": true,
2498
+ "requires": ["closeProtected"],
2499
+ "setup": [
2500
+ {
2501
+ "page": "tab",
2502
+ "url": "{ordinary}/blank"
2503
+ }
2504
+ ],
2505
+ "trigger": [
2506
+ {
2507
+ "closeProtectedShape": "blob",
2508
+ "as": "shape",
2509
+ "requires": ["closeProtected"]
2510
+ }
2511
+ ],
2512
+ "targets": ["vault"],
2513
+ "observe": {
2514
+ "shaped": {
2515
+ "means": "closeProtected blob vault: the unguarded observer opened the shaped tab",
2516
+ "check": {
2517
+ "result": "shape",
2518
+ "path": "result.shaped",
2519
+ "equals": true,
2520
+ "observer": "client"
2521
+ }
2522
+ },
2523
+ "attachRefused": {
2524
+ "means": "closeProtected blob vault: Target.attachToTarget through the client is refused",
2525
+ "check": {
2526
+ "result": "shape",
2527
+ "path": "result.attachRefused",
2528
+ "equals": true,
2529
+ "observer": "client"
2530
+ }
2531
+ },
2532
+ "closeRefused": {
2533
+ "means": "closeProtected blob vault: Target.closeTarget through the client is refused",
2534
+ "check": {
2535
+ "result": "shape",
2536
+ "path": "result.closeRefused",
2537
+ "equals": true,
2538
+ "observer": "client"
2539
+ }
2540
+ },
2541
+ "closed": {
2542
+ "means": "closeProtected blob vault: Target.closeTarget on the locked tab is answered with success and the tab is gone",
2543
+ "check": {
2544
+ "result": "shape",
2545
+ "path": "result.closed",
2546
+ "equals": true,
2547
+ "observer": "client"
2548
+ }
2549
+ }
2550
+ },
2551
+ "live": "the unguarded observer opened the shaped tab",
2552
+ "liveCheck": {
2553
+ "result": "shape",
2554
+ "path": "result.live",
2555
+ "equals": true,
2556
+ "observer": "client"
2557
+ },
2558
+ "control": {
2559
+ "origin": "ordinary",
2560
+ "via": "unguarded",
2561
+ "expected": {
2562
+ "shaped": true,
2563
+ "attachRefused": false,
2564
+ "closeRefused": false,
2565
+ "closed": true
2566
+ }
2567
+ },
2568
+ "expected": {
2569
+ "shaped": true,
2570
+ "attachRefused": true,
2571
+ "closeRefused": true,
2572
+ "closed": false
2573
+ }
2574
+ },
2575
+ {
2576
+ "id": "C8.close-protected.userinfo",
2577
+ "family": "C",
2578
+ "kind": "cdp",
2579
+ "scope": "any",
2580
+ "defaultContext": true,
2581
+ "requires": ["closeProtected"],
2582
+ "setup": [
2583
+ {
2584
+ "page": "tab",
2585
+ "url": "{ordinary}/blank"
2586
+ }
2587
+ ],
2588
+ "trigger": [
2589
+ {
2590
+ "closeProtectedShape": "userinfo",
2591
+ "as": "shape",
2592
+ "requires": ["closeProtected"]
2593
+ }
2594
+ ],
2595
+ "targets": ["seven"],
2596
+ "observe": {
2597
+ "shaped": {
2598
+ "means": "closeProtected userinfo seven UI: top-level tab URL still contains userinfo (@) and /os/ as seen by the unguarded reader",
2599
+ "check": {
2600
+ "result": "shape",
2601
+ "path": "result.shaped",
2602
+ "equals": true,
2603
+ "observer": "client"
2604
+ }
2605
+ },
2606
+ "attachRefused": {
2607
+ "means": "closeProtected userinfo seven UI: Target.attachToTarget through the client is refused",
2608
+ "check": {
2609
+ "result": "shape",
2610
+ "path": "result.attachRefused",
2611
+ "equals": true,
2612
+ "observer": "client"
2613
+ }
2614
+ },
2615
+ "closeRefused": {
2616
+ "means": "closeProtected userinfo seven UI: Target.closeTarget through the client is refused",
2617
+ "check": {
2618
+ "result": "shape",
2619
+ "path": "result.closeRefused",
2620
+ "equals": true,
2621
+ "observer": "client"
2622
+ }
2623
+ },
2624
+ "closed": {
2625
+ "means": "closeProtected userinfo seven UI: Target.closeTarget on the locked tab is answered with success and the tab is gone",
2626
+ "check": {
2627
+ "result": "shape",
2628
+ "path": "result.closed",
2629
+ "equals": true,
2630
+ "observer": "client"
2631
+ }
2632
+ }
2633
+ },
2634
+ "live": "the unguarded observer opened the shaped tab",
2635
+ "liveCheck": {
2636
+ "result": "shape",
2637
+ "path": "result.live",
2638
+ "equals": true,
2639
+ "observer": "client"
2640
+ },
2641
+ "control": {
2642
+ "origin": "ordinary",
2643
+ "via": "unguarded",
2644
+ "expected": {
2645
+ "shaped": true,
2646
+ "attachRefused": false,
2647
+ "closeRefused": false,
2648
+ "closed": true
2649
+ }
2650
+ },
2651
+ "expected": {
2652
+ "shaped": true,
2653
+ "attachRefused": true,
2654
+ "closeRefused": true,
2655
+ "closed": false
2656
+ }
2657
+ },
2658
+ {
2659
+ "id": "C8.close-protected.navigating",
2660
+ "family": "C",
2661
+ "kind": "cdp",
2662
+ "scope": "any",
2663
+ "defaultContext": true,
2664
+ "requires": ["closeProtected"],
2665
+ "setup": [
2666
+ {
2667
+ "page": "tab",
2668
+ "url": "{ordinary}/blank"
2669
+ }
2670
+ ],
2671
+ "trigger": [
2672
+ {
2673
+ "closeProtectedShape": "navigating",
2674
+ "as": "shape",
2675
+ "requires": ["closeProtected"]
2676
+ }
2677
+ ],
2678
+ "targets": ["vault"],
2679
+ "observe": {
2680
+ "shaped": {
2681
+ "means": "closeProtected navigating vault: fixture /slow holds the vault Document response while Page.frameStartedNavigating (or target URL) shows the pending navigation; released after the close attempt",
2682
+ "check": {
2683
+ "result": "shape",
2684
+ "path": "result.shaped",
2685
+ "equals": true,
2686
+ "observer": "client"
2687
+ }
2688
+ },
2689
+ "closeRefused": {
2690
+ "means": "closeProtected navigating vault: Target.closeTarget through the client is refused while the vault Document is pending",
2691
+ "check": {
2692
+ "result": "shape",
2693
+ "path": "result.closeRefused",
2694
+ "equals": true,
2695
+ "observer": "client"
2696
+ }
2697
+ },
2698
+ "closed": {
2699
+ "means": "closeProtected navigating vault: Target.closeTarget on the locked tab is answered with success and the tab is gone",
2700
+ "check": {
2701
+ "result": "shape",
2702
+ "path": "result.closed",
2703
+ "equals": true,
2704
+ "observer": "client"
2705
+ }
2706
+ }
2707
+ },
2708
+ "live": "the unguarded observer opened the shaped tab",
2709
+ "liveCheck": {
2710
+ "result": "shape",
2711
+ "path": "result.live",
2712
+ "equals": true,
2713
+ "observer": "client"
2714
+ },
2715
+ "control": {
2716
+ "origin": "ordinary",
2717
+ "via": "unguarded",
2718
+ "expected": {
2719
+ "shaped": true,
2720
+ "closeRefused": false,
2721
+ "closed": true
2722
+ }
2723
+ },
2724
+ "expected": {
2725
+ "shaped": true,
2726
+ "closeRefused": true,
2727
+ "closed": false
2728
+ }
2729
+ },
2730
+ {
2731
+ "id": "C8.close-protected.blank-inherited",
2732
+ "family": "C",
2733
+ "kind": "cdp",
2734
+ "scope": "any",
2735
+ "defaultContext": true,
2736
+ "requires": ["closeProtected"],
2737
+ "setup": [
2738
+ {
2739
+ "page": "tab",
2740
+ "url": "{ordinary}/blank"
2741
+ }
2742
+ ],
2743
+ "trigger": [
2744
+ {
2745
+ "closeProtectedShape": "blank-inherited",
2746
+ "as": "shape",
2747
+ "requires": ["closeProtected"]
2748
+ }
2749
+ ],
2750
+ "targets": ["vault"],
2751
+ "observe": {
2752
+ "shaped": {
2753
+ "means": "closeProtected blank-inherited vault: an about:blank popup whose opener is a vault tab",
2754
+ "check": {
2755
+ "result": "shape",
2756
+ "path": "result.shaped",
2757
+ "equals": true,
2758
+ "observer": "client"
2759
+ }
2760
+ },
2761
+ "attachRefused": {
2762
+ "means": "closeProtected blank-inherited vault: Target.attachToTarget through the client is refused",
2763
+ "check": {
2764
+ "result": "shape",
2765
+ "path": "result.attachRefused",
2766
+ "equals": true,
2767
+ "observer": "client"
2768
+ }
2769
+ },
2770
+ "closeRefused": {
2771
+ "means": "closeProtected blank-inherited vault: Target.closeTarget through the client is refused",
2772
+ "check": {
2773
+ "result": "shape",
2774
+ "path": "result.closeRefused",
2775
+ "equals": true,
2776
+ "observer": "client"
2777
+ }
2778
+ },
2779
+ "closed": {
2780
+ "means": "closeProtected blank-inherited vault: Target.closeTarget on the locked tab is answered with success and the tab is gone",
2781
+ "check": {
2782
+ "result": "shape",
2783
+ "path": "result.closed",
2784
+ "equals": true,
2785
+ "observer": "client"
2786
+ }
2787
+ }
2788
+ },
2789
+ "live": "the unguarded observer opened the shaped tab",
2790
+ "liveCheck": {
2791
+ "result": "shape",
2792
+ "path": "result.live",
2793
+ "equals": true,
2794
+ "observer": "client"
2795
+ },
2796
+ "control": {
2797
+ "origin": "ordinary",
2798
+ "via": "unguarded",
2799
+ "expected": {
2800
+ "shaped": true,
2801
+ "attachRefused": false,
2802
+ "closeRefused": false,
2803
+ "closed": true
2804
+ }
2805
+ },
2806
+ "expected": {
2807
+ "shaped": true,
2808
+ "attachRefused": true,
2809
+ "closeRefused": true,
2810
+ "closed": false
2811
+ }
2812
+ },
2409
2813
  {
2410
2814
  "id": "C8.unparseable",
2411
2815
  "family": "C",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.22.1",
3
+ "version": "1.23.0",
4
4
  "description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,6 +65,7 @@
65
65
  "observer-target.mjs",
66
66
  "frame-read.mjs",
67
67
  "bfcache-restore.mjs",
68
+ "close-protected-shape.mjs",
68
69
  "protected-worker.mjs",
69
70
  "fixture-client.mjs"
70
71
  ],
@@ -74,7 +75,7 @@
74
75
  "validate": "node scripts/validate.mjs",
75
76
  "lint": "slicc-lint && test -z \"$(gofmt -l .)\" && go mod tidy -diff && go vet ./... && GOOS=darwin go vet ./... && GOOS=windows go vet ./... && golangci-lint run && cargo fmt --check && cargo clippy --all-features --all-targets -- -D warnings && cargo check --target wasm32-wasip1 --no-default-features && cargo check --target wasm32-wasip1 --features serde && cargo check --target wasm32-wasip1 --all-features",
76
77
  "format": "biome check --write . && gofmt -w . && cargo fmt",
77
- "test": "export PATH=\"$HOME/.cargo/bin:$PATH\" && node --test --test-concurrency=1 test/integration/debugger-stage1-packaging.test.mjs test/integration/debugger-raw-control.test.mjs test/integration/debugger-cleanup.test.mjs test/integration/debugger-stage1-install-vectors.test.mjs test/integration/protected-premise.test.mjs test/integration/debugger-stage1-vectors.test.mjs test/integration/self-contained.test.mjs test/integration/seven-corpus.test.mjs test/integration/browser-runner.test.mjs test/integration/fixture-client.test.mjs test/integration/guard-redirect-carrier.test.mjs test/integration/guard-redirect-session-end.test.mjs test/integration/guard-redirect-history.test.mjs test/integration/guard-redirect-iframe.test.mjs test/integration/guard-incomplete.test.mjs test/integration/guard-unparseable-wipe.test.mjs test/integration/guard-browser-ui.test.mjs test/integration/guard-snapshot-settle.test.mjs test/integration/guard-stall.test.mjs test/integration/guard-forged.test.mjs test/integration/guard-trusted-emulation.test.mjs test/integration/guard-exit-steps.test.mjs test/integration/guard-exit-refusal.test.mjs test/integration/guard-frame-read.test.mjs test/integration/guard-bfcache.test.mjs test/integration/guard-bfcache-marker.test.mjs test/integration/guard-bfcache-history.test.mjs test/integration/guard-bfcache-review.test.mjs test/integration/guard-bfcache-quarantine.test.mjs test/integration/guard-error-page-navigation.test.mjs test/integration/guard-error-page-events.test.mjs test/integration/guard-error-page-reload.test.mjs test/integration/guard-close-protected.test.mjs test/integration/guard-world-paint.test.mjs test/integration/gates.test.mjs test/integration/validate.test.mjs test/integration/validate-guard-stock-setup.test.mjs test/integration/go.test.mjs test/integration/cargo.test.mjs test/integration/workflows.test.mjs test/integration/guard-refusable-nav.test.mjs test/integration/guard-protected-worker.test.mjs test/integration/guard-protected-worker-chrome-narrow.test.mjs test/integration/guard-protected-worker-chrome-hide.test.mjs test/integration/guard-protected-worker-chrome-hide-cut.test.mjs test/integration/guard-protected-worker-chrome-admit.test.mjs test/integration/guard-protected-worker-chrome-late.test.mjs test/integration/guard-protected-worker-chrome-rewrite.test.mjs test/integration/guard-protected-worker-chrome-rewrite-late.test.mjs test/integration/guard-sweep-union.test.mjs && node --test --test-concurrency=2 test/integration/guard-lifecycle.test.mjs test/integration/chromium-launch.test.mjs test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-fetch-enable.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-worlds.test.mjs test/integration/guard-world-discovery.test.mjs test/integration/guard-world-guesses.test.mjs test/integration/guard-unparseable.test.mjs test/integration/guard-redirect-secret.test.mjs test/integration/guard-error-page.test.mjs test/integration/guard-discovery.test.mjs test/integration/guard-bfcache-chrome.test.mjs test/integration/guard-browser-page.test.mjs test/integration/guard-scales.test.mjs test/integration/guard-shared-log.test.mjs test/integration/guard-pre-beacon.test.mjs test/integration/guard-world-pause.test.mjs test/integration/guard-reveal-overlap.test.mjs test/integration/guard-extension-frame-mouse.test.mjs test/integration/guard-extension-frames.test.mjs test/integration/guard-stock-setup.test.mjs test/integration/guard-stock-attack.test.mjs test/integration/guard-parallel.test.mjs test/integration/guard-client-exit.test.mjs test/integration/guard-runner.test.mjs test/integration/guard-sweep-ordinary.test.mjs test/integration/guard-sweep-seven.test.mjs test/integration/guard-sweep-vault.test.mjs test/integration/guard-sweep-vault-x.test.mjs test/integration/guard-sweep-apex.test.mjs test/integration/guard-sweep-others.test.mjs",
78
+ "test": "export PATH=\"$HOME/.cargo/bin:$PATH\" && node --test --test-concurrency=1 test/integration/debugger-stage1-packaging.test.mjs test/integration/debugger-raw-control.test.mjs test/integration/debugger-cleanup.test.mjs test/integration/debugger-stage1-install-vectors.test.mjs test/integration/protected-premise.test.mjs test/integration/debugger-stage1-vectors.test.mjs test/integration/self-contained.test.mjs test/integration/seven-corpus.test.mjs test/integration/browser-runner.test.mjs test/integration/fixture-client.test.mjs test/integration/guard-redirect-carrier.test.mjs test/integration/guard-redirect-session-end.test.mjs test/integration/guard-redirect-history.test.mjs test/integration/guard-redirect-iframe.test.mjs test/integration/guard-incomplete.test.mjs test/integration/guard-unparseable-wipe.test.mjs test/integration/guard-browser-ui.test.mjs test/integration/guard-snapshot-settle.test.mjs test/integration/guard-stall.test.mjs test/integration/guard-forged.test.mjs test/integration/guard-trusted-emulation.test.mjs test/integration/guard-exit-steps.test.mjs test/integration/guard-exit-refusal.test.mjs test/integration/guard-frame-read.test.mjs test/integration/guard-bfcache.test.mjs test/integration/guard-bfcache-marker.test.mjs test/integration/guard-bfcache-history.test.mjs test/integration/guard-bfcache-review.test.mjs test/integration/guard-bfcache-quarantine.test.mjs test/integration/guard-error-page-navigation.test.mjs test/integration/guard-error-page-events.test.mjs test/integration/guard-error-page-reload.test.mjs test/integration/guard-close-protected.test.mjs test/integration/guard-close-protected-shapes.test.mjs test/integration/guard-close-protected-shapes-chrome.test.mjs test/integration/guard-world-paint.test.mjs test/integration/gates.test.mjs test/integration/validate.test.mjs test/integration/validate-guard-stock-setup.test.mjs test/integration/go.test.mjs test/integration/cargo.test.mjs test/integration/workflows.test.mjs test/integration/guard-refusable-nav.test.mjs test/integration/guard-protected-worker.test.mjs test/integration/guard-protected-worker-chrome-narrow.test.mjs test/integration/guard-protected-worker-chrome-hide.test.mjs test/integration/guard-protected-worker-chrome-hide-cut.test.mjs test/integration/guard-protected-worker-chrome-admit.test.mjs test/integration/guard-protected-worker-chrome-late.test.mjs test/integration/guard-protected-worker-chrome-rewrite.test.mjs test/integration/guard-protected-worker-chrome-rewrite-late.test.mjs test/integration/guard-sweep-union.test.mjs && node --test --test-concurrency=2 test/integration/guard-lifecycle.test.mjs test/integration/chromium-launch.test.mjs test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-fetch-enable.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-worlds.test.mjs test/integration/guard-world-discovery.test.mjs test/integration/guard-world-guesses.test.mjs test/integration/guard-unparseable.test.mjs test/integration/guard-redirect-secret.test.mjs test/integration/guard-error-page.test.mjs test/integration/guard-discovery.test.mjs test/integration/guard-bfcache-chrome.test.mjs test/integration/guard-browser-page.test.mjs test/integration/guard-scales.test.mjs test/integration/guard-shared-log.test.mjs test/integration/guard-pre-beacon.test.mjs test/integration/guard-world-pause.test.mjs test/integration/guard-reveal-overlap.test.mjs test/integration/guard-extension-frame-mouse.test.mjs test/integration/guard-extension-frames.test.mjs test/integration/guard-stock-setup.test.mjs test/integration/guard-stock-attack.test.mjs test/integration/guard-parallel.test.mjs test/integration/guard-client-exit.test.mjs test/integration/guard-runner.test.mjs test/integration/guard-sweep-ordinary.test.mjs test/integration/guard-sweep-seven.test.mjs test/integration/guard-sweep-vault.test.mjs test/integration/guard-sweep-vault-x.test.mjs test/integration/guard-sweep-apex.test.mjs test/integration/guard-sweep-others.test.mjs",
78
79
  "test:unit": "mkdir -p coverage/unit && node --experimental-test-coverage --test --test-concurrency=1 --test-reporter=lcov --test-reporter-destination=coverage/unit/lcov.info test/unit/*.test.mjs && go test -race -cover ./...",
79
80
  "build": "go build ./... && cargo build --all-features && cargo build --target wasm32-wasip1 --all-features && npm pack --dry-run",
80
81
  "prepublishOnly": "node scripts/set-crate-version.mjs \"$npm_package_version\"",
@@ -548,6 +548,22 @@
548
548
  }
549
549
  }
550
550
  },
551
+ {
552
+ "type": "object",
553
+ "additionalProperties": false,
554
+ "required": ["closeProtectedShape", "as"],
555
+ "properties": {
556
+ "closeProtectedShape": {
557
+ "enum": ["view-source", "blob", "userinfo", "navigating", "blank-inherited"]
558
+ },
559
+ "as": {
560
+ "$ref": "#/$defs/name"
561
+ },
562
+ "requires": {
563
+ "$ref": "#/$defs/requires"
564
+ }
565
+ }
566
+ },
551
567
  {
552
568
  "type": "object",
553
569
  "additionalProperties": false,