@ai-ecoverse/slicc-browsers 1.19.2 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1142 @@
1
+ import {
2
+ whenShownInput as INPUT_METHODS,
3
+ presentRefusal as PRESENT,
4
+ shownRefusal as SHOWN,
5
+ } from './extension-frames-rules.mjs';
6
+ import { DEFAULT_EXTENSION_WORLD_ID } from './extension-world.mjs';
7
+ import { outcome } from './guard-driver.mjs';
8
+
9
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
+ const FRAME = 'slicc-extension-frame';
11
+ const HOST = 'slicc-extension-frame-host';
12
+ const SHADOW = 'slicc-extension-frame-shadow';
13
+ const MARKER = 'slicc-private-extension-frame-content';
14
+ const USERNAME = 'slicc-frame-test-user';
15
+ const BOX = 'position:fixed;right:0;bottom:0;width:300px;height:200px;border:0';
16
+ const STYLES = {
17
+ hidden: 'f.hidden = true; f.style.cssText = ""',
18
+ concealed:
19
+ 'f.hidden = false; f.style.cssText = "position:fixed;left:-999999px;top:0;width:300px;height:200px;border:0;visibility:hidden;opacity:0"',
20
+ offscreen:
21
+ 'f.hidden = false; f.style.cssText = "position:fixed;left:-999999px;top:0;width:300px;height:200px;border:0"',
22
+ shown: `f.hidden = false; f.style.cssText = ${JSON.stringify(BOX)}`,
23
+ transparent: `f.hidden = false; f.style.cssText = ${JSON.stringify(`${BOX};opacity:0`)}`,
24
+ nested: `f.hidden = false; f.style.cssText = ${JSON.stringify(BOX)}`,
25
+ focused:
26
+ 'f.hidden = false; f.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;border:0;overflow:hidden;opacity:0"',
27
+ };
28
+ const refusal = (out) => (out.ok ? null : String(out.error?.message ?? out.error));
29
+ const presentish = (out) => {
30
+ const text = refusal(out);
31
+ if (!text) return null;
32
+ return text === PRESENT || /^slicc: refused/.test(text) ? PRESENT : text;
33
+ };
34
+ const carries = (value) => {
35
+ const text = JSON.stringify(value) ?? '';
36
+ return text.includes(USERNAME) || text.includes(MARKER);
37
+ };
38
+ const extensionOrigin = (extensionId) => `chrome-extension://${extensionId}`;
39
+ const exposesExtension = (value, extensionId, frameURL) => {
40
+ const text = typeof value === 'string' ? value : (JSON.stringify(value) ?? '');
41
+ return (
42
+ text.includes(USERNAME) ||
43
+ text.includes(MARKER) ||
44
+ (typeof frameURL === 'string' && frameURL !== '' && text.includes(frameURL)) ||
45
+ text.includes(extensionOrigin(extensionId))
46
+ );
47
+ };
48
+ const frameRef = `__${FRAME}`;
49
+ const frameLookup = `document.getElementById(${JSON.stringify(FRAME)})
50
+ ?? globalThis[${JSON.stringify(frameRef)}]
51
+ ?? document.getElementById(${JSON.stringify(HOST)})?.contentDocument?.getElementById(${JSON.stringify(FRAME)})`;
52
+
53
+ async function observe(reader, observer, expression) {
54
+ const out = await outcome(
55
+ reader.send(
56
+ 'Runtime.evaluate',
57
+ { expression, awaitPromise: true, returnByValue: true },
58
+ observer
59
+ )
60
+ );
61
+ if (!out.ok || out.result.exceptionDetails)
62
+ throw new Error(
63
+ `extensionFrame observer: ${out.error?.message ?? out.result.exceptionDetails?.text}`
64
+ );
65
+ return out.result.result?.value;
66
+ }
67
+
68
+ async function painted(reader, observer, ms) {
69
+ await outcome(reader.send('Page.bringToFront', {}, observer));
70
+ await Promise.race([
71
+ outcome(
72
+ reader.send(
73
+ 'Runtime.evaluate',
74
+ {
75
+ expression: `new Promise((resolve) => { setTimeout(resolve, ${ms}); requestAnimationFrame(() => requestAnimationFrame(resolve)); })`,
76
+ awaitPromise: true,
77
+ returnByValue: true,
78
+ },
79
+ observer
80
+ )
81
+ ),
82
+ pause(ms),
83
+ ]);
84
+ }
85
+
86
+ async function style(reader, observer, mode, ms) {
87
+ const apply = STYLES[mode] ?? STYLES.shown;
88
+ await observe(reader, observer, `(() => { const f = ${frameLookup}; ${apply}; return true; })()`);
89
+ await painted(reader, observer, ms);
90
+ }
91
+
92
+ async function frameTargets(reader, targetId, extensionId, want, missing, waitMs = 5000) {
93
+ const prefix = `chrome-extension://${extensionId}/`;
94
+ const deadline = Date.now() + waitMs;
95
+ let best = [];
96
+ do {
97
+ const { targetInfos } = await reader.send('Target.getTargets');
98
+ const byId = new Map(targetInfos.map((info) => [info.targetId, info]));
99
+ const under = (info) => {
100
+ let cur = info;
101
+ while (cur) {
102
+ if (cur.parentId === targetId) return true;
103
+ if (!cur.parentId) return false;
104
+ cur = byId.get(cur.parentId);
105
+ }
106
+ return false;
107
+ };
108
+ const found = targetInfos.filter(
109
+ (info) => info.type === 'iframe' && info.url.startsWith(prefix) && under(info)
110
+ );
111
+ if (found.length > best.length) best = found;
112
+ if (found.length >= want) return found;
113
+ await pause(50);
114
+ } while (Date.now() < deadline);
115
+ if (best.length < want)
116
+ missing.push(`extensionFrame: expected ${want} frame targets, found ${best.length}`);
117
+ if (best.length) return best;
118
+ throw new Error(`extensionFrame: no frame target for ${extensionId}`);
119
+ }
120
+
121
+ async function shot(client, session) {
122
+ const out = await outcome(client.send('Page.captureScreenshot', { format: 'png' }, session));
123
+ return { out, ok: out.ok && typeof out.result.data === 'string' && out.result.data.length > 0 };
124
+ }
125
+
126
+ async function clickAt(client, _reader, session, _observer, x, y) {
127
+ const down = await outcome(
128
+ client.send(
129
+ 'Input.dispatchMouseEvent',
130
+ { type: 'mousePressed', x, y, button: 'left', clickCount: 1 },
131
+ session
132
+ )
133
+ );
134
+ const up = await outcome(
135
+ client.send(
136
+ 'Input.dispatchMouseEvent',
137
+ { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 },
138
+ session
139
+ )
140
+ );
141
+ return { out: down.ok ? up : down, ok: down.ok && up.ok };
142
+ }
143
+
144
+ async function click(client, reader, session, observer) {
145
+ await observe(reader, observer, 'delete document.body.dataset.clicked; true');
146
+ const pressed = await clickAt(client, reader, session, observer, 20, 20);
147
+ const clicked = await observe(reader, observer, 'document.body.dataset.clicked === "yes"');
148
+ return { out: pressed.out, ok: pressed.ok && clicked === true };
149
+ }
150
+
151
+ async function key(client, reader, session, observer, beforeSend, requireTyped = true) {
152
+ await observe(
153
+ reader,
154
+ observer,
155
+ 'const i = document.getElementById("typed"); i.value = ""; i.focus(); true'
156
+ );
157
+ if (beforeSend) await beforeSend();
158
+ const out = await outcome(client.send('Input.insertText', { text: 'k' }, session));
159
+ const typed = await observe(reader, observer, 'document.getElementById("typed").value === "k"');
160
+ return { out, ok: out.ok && (!requireTyped || typed === true) };
161
+ }
162
+
163
+ function clearPoint(box) {
164
+ const point = { x: 20, y: 20 };
165
+ if (!box || !(box.w > 0) || !(box.h > 0)) return point;
166
+ const left = box.x - box.w / 2;
167
+ const right = box.x + box.w / 2;
168
+ const top = box.y - box.h / 2;
169
+ const bottom = box.y + box.h / 2;
170
+ if (point.x < left || point.x > right || point.y < top || point.y > bottom) return point;
171
+ return { x: 20, y: Math.max(20, bottom + 40) };
172
+ }
173
+
174
+ function shownish(text) {
175
+ return Boolean(text && (text === SHOWN || /^slicc: refused/.test(text)));
176
+ }
177
+
178
+ function inputOutcome(outs, missing, name, side) {
179
+ const [first, ...rest] = outs;
180
+ const firstRefusal = refusal(first);
181
+ if (shownish(firstRefusal)) return { ok: false, refusal: firstRefusal };
182
+ if (!first.ok) {
183
+ missing.push(`${name}: ${firstRefusal || 'transport error'}`);
184
+ return { ok: 'incomplete', refusal: null };
185
+ }
186
+ if (side === 'control') {
187
+ for (const out of rest) {
188
+ if (out.ok) continue;
189
+ missing.push(`${name}: cleanup ${refusal(out) || 'transport error'}`);
190
+ }
191
+ }
192
+ return { ok: true, refusal: null };
193
+ }
194
+
195
+ async function sendPair(client, session, method, down, up) {
196
+ const first = await outcome(client.send(method, down, session));
197
+ if (shownish(refusal(first))) return [first];
198
+ return [first, await outcome(client.send(method, up, session))];
199
+ }
200
+
201
+ async function inputMethods(client, session, methods, point, missing, side) {
202
+ const results = {};
203
+ const { x, y } = point;
204
+ const drag = { items: [], files: [], dragOperationsMask: 1 };
205
+ for (const method of methods) {
206
+ const name = method.slice('Input.'.length);
207
+ let outs;
208
+ if (method === 'Input.dispatchKeyEvent')
209
+ outs = [
210
+ await outcome(client.send(method, { type: 'keyDown', key: 'Tab', code: 'Tab' }, session)),
211
+ ];
212
+ else if (method === 'Input.dispatchMouseEvent')
213
+ outs = await sendPair(
214
+ client,
215
+ session,
216
+ method,
217
+ { type: 'mousePressed', x, y, button: 'left', clickCount: 1 },
218
+ { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }
219
+ );
220
+ else if (method === 'Input.dispatchTouchEvent')
221
+ outs = await sendPair(
222
+ client,
223
+ session,
224
+ method,
225
+ { type: 'touchStart', touchPoints: [{ x, y }] },
226
+ { type: 'touchEnd', touchPoints: [] }
227
+ );
228
+ else if (method === 'Input.dispatchDragEvent')
229
+ outs = await sendPair(
230
+ client,
231
+ session,
232
+ method,
233
+ { type: 'dragEnter', x, y, data: drag },
234
+ { type: 'dragCancel', x, y, data: drag }
235
+ );
236
+ else if (method === 'Input.emulateTouchFromMouseEvent')
237
+ outs = await sendPair(
238
+ client,
239
+ session,
240
+ method,
241
+ { type: 'mousePressed', x, y, button: 'left', clickCount: 1 },
242
+ { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }
243
+ );
244
+ else if (method === 'Input.insertText')
245
+ outs = [await outcome(client.send(method, { text: 'k' }, session))];
246
+ else if (method === 'Input.imeSetComposition')
247
+ outs = [
248
+ await outcome(
249
+ client.send(method, { text: 'k', selectionStart: 1, selectionEnd: 1 }, session)
250
+ ),
251
+ ];
252
+ else if (method === 'Input.synthesizePinchGesture')
253
+ outs = [await outcome(client.send(method, { x, y, scaleFactor: 1.1 }, session))];
254
+ else if (method === 'Input.synthesizeScrollGesture')
255
+ outs = [await outcome(client.send(method, { x, y, yDistance: -10 }, session))];
256
+ else if (method === 'Input.synthesizeTapGesture')
257
+ outs = [await outcome(client.send(method, { x, y }, session))];
258
+ else outs = [await outcome(client.send(method, {}, session))];
259
+ results[name] = inputOutcome(outs, missing, name, side);
260
+ }
261
+ return results;
262
+ }
263
+
264
+ function snapshotReadsFrame(snapshot, extensionId, frameURL) {
265
+ if (!snapshot.ok) return false;
266
+ return exposesExtension(snapshot.result, extensionId, frameURL);
267
+ }
268
+
269
+ const hasPayload = (value) =>
270
+ value != null && typeof value === 'object' && Object.keys(value).length > 0;
271
+
272
+ function markerBackendNodeId(node) {
273
+ if (!node) return null;
274
+ if (node.nodeType === 3 && typeof node.nodeValue === 'string' && node.nodeValue.includes(MARKER))
275
+ return node.backendNodeId;
276
+ const attrs = node.attributes ?? [];
277
+ for (let i = 0; i + 1 < attrs.length; i += 2)
278
+ if (typeof attrs[i + 1] === 'string' && attrs[i + 1].includes(MARKER))
279
+ return node.backendNodeId;
280
+ for (const child of node.children ?? []) {
281
+ const hit = markerBackendNodeId(child);
282
+ if (hit != null) return hit;
283
+ }
284
+ return null;
285
+ }
286
+
287
+ async function frameReadNodes(reader, observer, frame, missing) {
288
+ const attached = await outcome(
289
+ reader.send('Target.attachToTarget', { targetId: frame.targetId, flatten: true })
290
+ );
291
+ if (!attached.ok || typeof attached.result?.sessionId !== 'string') {
292
+ missing.push('observer could not attach to extension frame for frame-read nodes');
293
+ return null;
294
+ }
295
+ const session = attached.result.sessionId;
296
+ try {
297
+ await outcome(reader.send('DOM.enable', {}, session));
298
+ const tree = await outcome(reader.send('Page.getFrameTree', {}, session));
299
+ const frameId = tree.result?.frameTree?.frame?.id;
300
+ if (typeof frameId !== 'string' || !frameId) {
301
+ missing.push('observer extension frame has no frame id');
302
+ return null;
303
+ }
304
+ const owner = await outcome(reader.send('DOM.getFrameOwner', { frameId }, observer));
305
+ if (!owner.ok || typeof owner.result?.backendNodeId !== 'number') {
306
+ missing.push('observer DOM.getFrameOwner returned no backendNodeId');
307
+ return null;
308
+ }
309
+ const document = await outcome(
310
+ reader.send('DOM.getDocument', { depth: -1, pierce: true }, session)
311
+ );
312
+ const root = document.result?.root;
313
+ const markerNodeId = markerBackendNodeId(root) ?? root?.backendNodeId;
314
+ if (markerNodeId == null) {
315
+ missing.push('observer found no marker node in the extension frame');
316
+ return null;
317
+ }
318
+ return {
319
+ session,
320
+ iframeNodeId: owner.result.backendNodeId,
321
+ markerNodeId,
322
+ };
323
+ } catch (error) {
324
+ await outcome(reader.send('Target.detachFromTarget', { sessionId: session }));
325
+ throw error;
326
+ }
327
+ }
328
+
329
+ function protectedRefusal(out) {
330
+ const text = String(out?.error?.message ?? out?.error ?? '');
331
+ return text === PRESENT || text === SHOWN || /^slicc: refused/.test(text);
332
+ }
333
+
334
+ function leakFrom(out, missing, failed, empty, extensionId, frameURL, value = out.result) {
335
+ if (!out.ok) {
336
+ if (protectedRefusal(out)) return false;
337
+ missing.push(failed);
338
+ return false;
339
+ }
340
+ if (value == null || (typeof value === 'object' && !hasPayload(value))) {
341
+ missing.push(empty);
342
+ return false;
343
+ }
344
+ return exposesExtension(value, extensionId, frameURL);
345
+ }
346
+
347
+ function hasCapability(capabilities, name) {
348
+ return Array.isArray(capabilities) ? capabilities.includes(name) : capabilities?.[name] === true;
349
+ }
350
+
351
+ async function frameReadSurfaceLeaks(
352
+ client,
353
+ page,
354
+ reader,
355
+ observer,
356
+ frame,
357
+ extensionId,
358
+ frameURL,
359
+ missing,
360
+ capabilities = {}
361
+ ) {
362
+ const leaks = { partialAxLeak: false, describeNodeLeak: false, outerHTMLLeak: false };
363
+ if (!hasCapability(capabilities, 'browserSession')) return leaks;
364
+ const nodes = await frameReadNodes(reader, observer, frame, missing);
365
+ if (!nodes) return leaks;
366
+ try {
367
+ const attached = await outcome(
368
+ client.send('Target.attachToTarget', { targetId: frame.targetId, flatten: true })
369
+ );
370
+ let frameSession;
371
+ if (attached.ok && typeof attached.result?.sessionId === 'string') {
372
+ frameSession = attached.result.sessionId;
373
+ await outcome(client.send('DOM.enable', {}, frameSession));
374
+ await outcome(client.send('Accessibility.enable', {}, frameSession));
375
+ } else if (attached.ok) {
376
+ missing.push('guarded attach to extension frame for frame-read returned no session');
377
+ } else if (!protectedRefusal(attached)) {
378
+ missing.push(
379
+ `guarded attach to extension frame for frame-read failed: ${attached.error?.message ?? attached.error}`
380
+ );
381
+ }
382
+ try {
383
+ if (frameSession) {
384
+ const document = await outcome(
385
+ client.send('DOM.getDocument', { depth: -1, pierce: true }, frameSession)
386
+ );
387
+ const markerNodeId =
388
+ markerBackendNodeId(document.result?.root) ??
389
+ document.result?.root?.backendNodeId ??
390
+ nodes.markerNodeId;
391
+ leaks.partialAxLeak = leakFrom(
392
+ await outcome(
393
+ client.send(
394
+ 'Accessibility.getPartialAXTree',
395
+ { backendNodeId: markerNodeId, fetchRelatives: true },
396
+ frameSession
397
+ )
398
+ ),
399
+ missing,
400
+ 'Accessibility.getPartialAXTree failed',
401
+ 'Accessibility.getPartialAXTree returned no result',
402
+ extensionId,
403
+ frameURL
404
+ );
405
+ }
406
+ leaks.describeNodeLeak = leakFrom(
407
+ await outcome(
408
+ client.send(
409
+ 'DOM.describeNode',
410
+ { backendNodeId: nodes.iframeNodeId, depth: -1, pierce: true },
411
+ page
412
+ )
413
+ ),
414
+ missing,
415
+ 'DOM.describeNode failed',
416
+ 'DOM.describeNode returned no result',
417
+ extensionId,
418
+ frameURL
419
+ );
420
+ const html = await outcome(
421
+ client.send('DOM.getOuterHTML', { backendNodeId: nodes.iframeNodeId }, page)
422
+ );
423
+ leaks.outerHTMLLeak = leakFrom(
424
+ html,
425
+ missing,
426
+ 'DOM.getOuterHTML failed',
427
+ 'DOM.getOuterHTML returned no outerHTML string',
428
+ extensionId,
429
+ frameURL,
430
+ typeof html.result?.outerHTML === 'string' ? html.result.outerHTML : null
431
+ );
432
+ return leaks;
433
+ } finally {
434
+ if (frameSession)
435
+ await outcome(client.send('Target.detachFromTarget', { sessionId: frameSession }));
436
+ }
437
+ } finally {
438
+ await outcome(reader.send('Target.detachFromTarget', { sessionId: nodes.session }));
439
+ }
440
+ }
441
+
442
+ async function pageLeaks({
443
+ client,
444
+ page,
445
+ reader,
446
+ observer,
447
+ frame,
448
+ extensionId,
449
+ frameURL,
450
+ missing,
451
+ capabilities = {},
452
+ }) {
453
+ const leaks = {
454
+ frameTreeLeak: false,
455
+ documentLeak: false,
456
+ shadowDocumentLeak: false,
457
+ axLeak: false,
458
+ evaluateLeak: false,
459
+ snapshotFrameRead: false,
460
+ };
461
+ const tree = await outcome(client.send('Page.getFrameTree', {}, page));
462
+ if (!tree.ok) missing.push('Page.getFrameTree failed');
463
+ else if (!hasPayload(tree.result)) missing.push('Page.getFrameTree returned no result');
464
+ else leaks.frameTreeLeak = exposesExtension(tree.result, extensionId, frameURL);
465
+ const document = await outcome(client.send('DOM.getDocument', { depth: -1, pierce: true }, page));
466
+ if (!document.ok) missing.push('DOM.getDocument failed');
467
+ else if (!hasPayload(document.result)) missing.push('DOM.getDocument returned no result');
468
+ else {
469
+ const text = JSON.stringify(document.result);
470
+ leaks.documentLeak = exposesExtension(document.result, extensionId, frameURL);
471
+ leaks.shadowDocumentLeak = text.includes(USERNAME) && text.includes(SHADOW);
472
+ }
473
+ const ax = await outcome(client.send('Accessibility.getFullAXTree', {}, page));
474
+ if (!ax.ok) missing.push('Accessibility.getFullAXTree failed');
475
+ else if (!hasPayload(ax.result)) missing.push('Accessibility.getFullAXTree returned no result');
476
+ else leaks.axLeak = exposesExtension(ax.result, extensionId, frameURL);
477
+ const evaluated = await outcome(
478
+ client.send(
479
+ 'Runtime.evaluate',
480
+ {
481
+ expression: `(() => { const f = ${frameLookup}; return f ? f.src : ''; })()`,
482
+ returnByValue: true,
483
+ },
484
+ page
485
+ )
486
+ );
487
+ if (!evaluated.ok) missing.push('Runtime.evaluate of iframe.src failed');
488
+ else if (evaluated.result?.exceptionDetails)
489
+ missing.push('Runtime.evaluate of iframe.src raised');
490
+ else if (typeof evaluated.result?.result?.value !== 'string')
491
+ missing.push('Runtime.evaluate of iframe.src returned no string value');
492
+ else {
493
+ const value = evaluated.result.result.value;
494
+ leaks.evaluateLeak =
495
+ exposesExtension(evaluated.result, extensionId, frameURL) ||
496
+ exposesExtension(value, extensionId, frameURL);
497
+ }
498
+ const snapshot = await outcome(
499
+ client.send('DOMSnapshot.captureSnapshot', { computedStyles: [] }, page)
500
+ );
501
+ if (!snapshot.ok) missing.push('DOMSnapshot.captureSnapshot failed');
502
+ else if (
503
+ !hasPayload(snapshot.result) ||
504
+ !Array.isArray(snapshot.result.documents) ||
505
+ snapshot.result.documents.length === 0
506
+ )
507
+ missing.push('DOMSnapshot.captureSnapshot returned no documents');
508
+ else {
509
+ leaks.snapshot = true;
510
+ leaks.snapshotFrameRead = snapshotReadsFrame(snapshot, extensionId, frameURL);
511
+ }
512
+ Object.assign(
513
+ leaks,
514
+ await frameReadSurfaceLeaks(
515
+ client,
516
+ page,
517
+ reader,
518
+ observer,
519
+ frame,
520
+ extensionId,
521
+ frameURL,
522
+ missing,
523
+ capabilities
524
+ )
525
+ );
526
+ return { ...leaks, snapshotOut: snapshot };
527
+ }
528
+
529
+ async function targetInfoExposure(client, frames, frameURL, extensionId, missing, result) {
530
+ for (const frame of frames) {
531
+ const info = await outcome(client.send('Target.getTargetInfo', { targetId: frame.targetId }));
532
+ if (!info.ok) missing.push('Target.getTargetInfo failed');
533
+ else if (!info.result?.targetInfo || typeof info.result.targetInfo.url !== 'string')
534
+ missing.push('Target.getTargetInfo returned no targetInfo URL');
535
+ else if (exposesExtension(info.result, extensionId, frameURL)) result.targetInfoURL = true;
536
+ else if (result.targetInfoURL !== true) result.targetInfoURL = false;
537
+ }
538
+ }
539
+
540
+ async function targetListExposure(client, frameURL, extensionId, missing, result) {
541
+ const listing = await outcome(client.send('Target.getTargets'));
542
+ if (!listing.ok || !Array.isArray(listing.result?.targetInfos)) {
543
+ missing.push('Target.getTargets returned no targetInfos array');
544
+ } else {
545
+ result.listed = exposesExtension(listing.result.targetInfos, extensionId, frameURL);
546
+ }
547
+ }
548
+
549
+ function targetEventExposure(
550
+ client,
551
+ page,
552
+ ids,
553
+ frameURL,
554
+ extensionId,
555
+ eventStart,
556
+ missing,
557
+ result,
558
+ { autoAttach, discovery }
559
+ ) {
560
+ if (eventStart === undefined || !Array.isArray(client.events)) {
561
+ missing.push('target event stream is unavailable');
562
+ return;
563
+ }
564
+ const events = client.events.slice(eventStart);
565
+ if (autoAttach)
566
+ result.autoAttached = events.some(
567
+ (event) =>
568
+ event.method === 'Target.attachedToTarget' &&
569
+ event.sessionId === page &&
570
+ ids.has(event.params?.targetInfo?.targetId) &&
571
+ typeof event.params.sessionId === 'string'
572
+ );
573
+ if (discovery)
574
+ result.eventURL = events.some(
575
+ (event) =>
576
+ ['Target.attachedToTarget', 'Target.targetCreated', 'Target.targetInfoChanged'].includes(
577
+ event.method
578
+ ) && exposesExtension(event.params, extensionId, frameURL)
579
+ );
580
+ }
581
+
582
+ async function targetExposure({
583
+ client,
584
+ reader,
585
+ observer,
586
+ page,
587
+ frames,
588
+ frameURL,
589
+ extensionId,
590
+ eventStart,
591
+ frameMs,
592
+ missing,
593
+ capabilities = {},
594
+ }) {
595
+ const result = { autoAttached: false, eventURL: false, listed: false };
596
+ const ids = new Set(frames.map((frame) => frame.targetId));
597
+ const browserSession = hasCapability(capabilities, 'browserSession');
598
+ const autoAttach = hasCapability(capabilities, 'autoAttach');
599
+ const discovery = hasCapability(capabilities, 'discovery');
600
+ if (browserSession)
601
+ await targetInfoExposure(client, frames, frameURL, extensionId, missing, result);
602
+ const auto = autoAttach
603
+ ? await outcome(
604
+ client.send(
605
+ 'Target.setAutoAttach',
606
+ { autoAttach: true, waitForDebuggerOnStart: false, flatten: true },
607
+ page
608
+ )
609
+ )
610
+ : { ok: false };
611
+ try {
612
+ if (autoAttach || discovery) {
613
+ await painted(reader, observer, frameMs);
614
+ targetEventExposure(client, page, ids, frameURL, extensionId, eventStart, missing, result, {
615
+ autoAttach,
616
+ discovery,
617
+ });
618
+ }
619
+ if (browserSession) await targetListExposure(client, frameURL, extensionId, missing, result);
620
+ return result;
621
+ } finally {
622
+ if (auto.ok)
623
+ await outcome(
624
+ client.send(
625
+ 'Target.setAutoAttach',
626
+ { autoAttach: false, waitForDebuggerOnStart: false, flatten: true },
627
+ page
628
+ )
629
+ );
630
+ }
631
+ }
632
+
633
+ async function embed(reader, observer, mode, frameURL) {
634
+ const styleMode =
635
+ mode === 'revealed'
636
+ ? 'hidden'
637
+ : mode === 'cast'
638
+ ? 'shown'
639
+ : mode === 'focused'
640
+ ? 'focused'
641
+ : mode === 'nested'
642
+ ? 'nested'
643
+ : mode;
644
+ const src = `${frameURL}?username=${USERNAME}`;
645
+ if (mode === 'nested') {
646
+ return observe(
647
+ reader,
648
+ observer,
649
+ `new Promise((resolve) => {
650
+ document.body.innerHTML = '<button id="c9-click" style="position:fixed;left:0;top:0;width:100px;height:100px" onclick="document.body.dataset.clicked=\\'yes\\'">C9 click</button><input id="typed" style="position:fixed;left:0;top:120px;width:100px"><iframe id=${JSON.stringify(HOST)} style="position:fixed;left:0;top:200px;width:400px;height:300px;border:0"></iframe>';
651
+ const shadowHost = document.createElement('div');
652
+ shadowHost.id = ${JSON.stringify(SHADOW)};
653
+ const root = shadowHost.attachShadow({ mode: 'closed' });
654
+ const shadowFrame = document.createElement('iframe');
655
+ shadowFrame.id = ${JSON.stringify(FRAME)} + '-shadow';
656
+ shadowFrame.hidden = true;
657
+ shadowFrame.src = ${JSON.stringify(src)};
658
+ root.append(shadowFrame);
659
+ document.body.append(shadowHost);
660
+ const host = document.getElementById(${JSON.stringify(HOST)});
661
+ host.addEventListener('load', () => {
662
+ const doc = host.contentDocument;
663
+ doc.body.innerHTML = '';
664
+ const f = doc.createElement('iframe');
665
+ f.id = ${JSON.stringify(FRAME)};
666
+ ${STYLES.nested};
667
+ f.addEventListener('load', () => resolve(true), { once: true });
668
+ setTimeout(() => resolve(false), 5000);
669
+ f.src = ${JSON.stringify(src)};
670
+ doc.body.append(f);
671
+ }, { once: true });
672
+ host.src = 'about:blank';
673
+ })`
674
+ );
675
+ }
676
+ if (mode === 'focused') {
677
+ return observe(
678
+ reader,
679
+ observer,
680
+ `new Promise((resolve) => {
681
+ document.body.innerHTML = '<button id="c9-click" style="position:fixed;left:0;top:0;width:100px;height:100px" onclick="document.body.dataset.clicked=\\'yes\\'">C9 click</button><input id="typed" style="position:fixed;left:0;top:120px;width:100px">';
682
+ const shadowHost = document.createElement('div');
683
+ shadowHost.id = ${JSON.stringify(SHADOW)};
684
+ const root = shadowHost.attachShadow({ mode: 'closed' });
685
+ const shadowFrame = document.createElement('iframe');
686
+ shadowFrame.id = ${JSON.stringify(FRAME)};
687
+ ${STYLES.focused.replaceAll(/\bf\./g, 'shadowFrame.')};
688
+ shadowFrame.addEventListener('load', () => resolve(true), { once: true });
689
+ setTimeout(() => resolve(false), 5000);
690
+ shadowFrame.src = ${JSON.stringify(src)};
691
+ root.append(shadowFrame);
692
+ globalThis[${JSON.stringify(frameRef)}] = shadowFrame;
693
+ document.body.append(shadowHost);
694
+ })`
695
+ );
696
+ }
697
+ return observe(
698
+ reader,
699
+ observer,
700
+ `new Promise((resolve) => {
701
+ document.body.innerHTML = '<button id="c9-click" style="position:fixed;left:0;top:0;width:100px;height:100px" onclick="document.body.dataset.clicked=\\'yes\\'">C9 click</button><input id="typed" style="position:fixed;left:0;top:120px;width:100px">';
702
+ const f = document.createElement('iframe');
703
+ f.id = ${JSON.stringify(FRAME)};
704
+ ${STYLES[styleMode]};
705
+ const shadowHost = document.createElement('div');
706
+ shadowHost.id = ${JSON.stringify(SHADOW)};
707
+ const root = shadowHost.attachShadow({ mode: 'closed' });
708
+ const shadowFrame = document.createElement('iframe');
709
+ shadowFrame.id = ${JSON.stringify(FRAME)} + '-shadow';
710
+ shadowFrame.hidden = true;
711
+ shadowFrame.src = ${JSON.stringify(src)};
712
+ root.append(shadowFrame);
713
+ f.addEventListener('load', () => resolve(true), { once: true });
714
+ setTimeout(() => resolve(false), 5000);
715
+ f.src = ${JSON.stringify(src)};
716
+ document.body.append(f, shadowHost);
717
+ })`
718
+ );
719
+ }
720
+
721
+ async function paintMarker(reader, frame, focus = false) {
722
+ const privateSession = (
723
+ await reader.send('Target.attachToTarget', { targetId: frame.targetId, flatten: true })
724
+ ).sessionId;
725
+ try {
726
+ await observe(
727
+ reader,
728
+ privateSession,
729
+ focus
730
+ ? `(() => {
731
+ const i = document.createElement('input');
732
+ i.id = 'slicc-frame-focus';
733
+ i.value = ${JSON.stringify(MARKER)};
734
+ document.body.replaceChildren(i);
735
+ document.body.style.cssText = 'background:#f00;color:#f00;font:48px monospace';
736
+ i.focus();
737
+ return document.activeElement === i;
738
+ })()`
739
+ : `document.body.textContent = ${JSON.stringify(MARKER)}; document.body.style.cssText = 'background:#f00;color:#f00;font:48px monospace'; true`
740
+ );
741
+ } finally {
742
+ await reader.send('Target.detachFromTarget', { sessionId: privateSession });
743
+ }
744
+ }
745
+
746
+ async function holdFocus(reader, frame) {
747
+ const privateSession = (
748
+ await reader.send('Target.attachToTarget', { targetId: frame.targetId, flatten: true })
749
+ ).sessionId;
750
+ try {
751
+ await observe(
752
+ reader,
753
+ privateSession,
754
+ `(() => {
755
+ let i = document.getElementById('slicc-frame-focus');
756
+ if (!i) {
757
+ i = document.createElement('input');
758
+ i.id = 'slicc-frame-focus';
759
+ i.value = ${JSON.stringify(MARKER)};
760
+ document.body.replaceChildren(i);
761
+ }
762
+ i.focus();
763
+ return document.activeElement === i;
764
+ })()`
765
+ );
766
+ } finally {
767
+ await reader.send('Target.detachFromTarget', { sessionId: privateSession });
768
+ }
769
+ }
770
+
771
+ async function driveInput(client, reader, page, observer, inputList, mode, frame, missing, side) {
772
+ const visible = await observe(
773
+ reader,
774
+ observer,
775
+ `(() => {
776
+ const f = ${frameLookup};
777
+ if (!f || !f.checkVisibility({ visibilityProperty: true })) return false;
778
+ const r = f.getBoundingClientRect();
779
+ return r.width > 0 && r.height > 0;
780
+ })()`
781
+ );
782
+ let frameFocused = false;
783
+ if (mode === 'focused' && frame) {
784
+ await holdFocus(reader, frame);
785
+ frameFocused = await observe(
786
+ reader,
787
+ observer,
788
+ `(() => {
789
+ const f = ${frameLookup};
790
+ if (!f) return false;
791
+ f.focus();
792
+ try { f.contentWindow?.focus(); } catch {}
793
+ const host = f.getRootNode?.()?.host;
794
+ return document.activeElement === f || (host != null && document.activeElement === host);
795
+ })()`
796
+ );
797
+ }
798
+ const box = await observe(
799
+ reader,
800
+ observer,
801
+ `(() => {
802
+ const f = ${frameLookup};
803
+ if (!f) return null;
804
+ const r = f.getBoundingClientRect();
805
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height };
806
+ })()`
807
+ );
808
+ const point = clearPoint(box);
809
+ const refocus = async () => {
810
+ if (!(mode === 'focused' && frame)) return;
811
+ await holdFocus(reader, frame);
812
+ await observe(
813
+ reader,
814
+ observer,
815
+ `(() => { const f = ${frameLookup}; if (f) f.focus(); return true; })()`
816
+ );
817
+ };
818
+ await refocus();
819
+ const frameClick = await clickAt(
820
+ client,
821
+ reader,
822
+ page,
823
+ observer,
824
+ box && box.w > 0 ? box.x : 99990,
825
+ box && box.h > 0 ? box.y : 10
826
+ );
827
+ await refocus();
828
+ const tab = await outcome(
829
+ client.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Tab', code: 'Tab' }, page)
830
+ );
831
+ await refocus();
832
+ const pressed = await click(client, reader, page, observer);
833
+ await refocus();
834
+ const typed = await key(client, reader, page, observer, refocus, mode !== 'focused');
835
+ await refocus();
836
+ const inputs = await inputMethods(client, page, inputList, point, missing, side);
837
+ await refocus();
838
+ const focusedKey = await outcome(
839
+ client.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'a', code: 'KeyA' }, page)
840
+ );
841
+ await refocus();
842
+ const focusedText = await outcome(client.send('Input.insertText', { text: 'x' }, page));
843
+ await refocus();
844
+ const focusedIME = await outcome(
845
+ client.send('Input.imeSetComposition', { text: 'x', selectionStart: 1, selectionEnd: 1 }, page)
846
+ );
847
+ return {
848
+ frameClick,
849
+ visible,
850
+ frameFocused,
851
+ tab,
852
+ pressed,
853
+ typed,
854
+ inputs,
855
+ focusedKey,
856
+ focusedText,
857
+ focusedIME,
858
+ };
859
+ }
860
+
861
+ async function driveCaptures(client, page, missing) {
862
+ const archive = await outcome(client.send('Page.captureSnapshot', { format: 'mhtml' }, page));
863
+ const pdf = await outcome(client.send('Page.printToPDF', {}, page));
864
+ if (pdf.ok && !(typeof pdf.result.data === 'string' && pdf.result.data.length > 0))
865
+ missing.push('Page.printToPDF returned no PDF data');
866
+ const cast = await outcome(
867
+ client.send('Page.startScreencast', { format: 'png', everyNthFrame: 1 }, page)
868
+ );
869
+ if (cast.ok) await outcome(client.send('Page.stopScreencast', {}, page));
870
+ const begin = await outcome(
871
+ client.send('HeadlessExperimental.beginFrame', { screenshot: { format: 'png' } }, page)
872
+ );
873
+ return { archive, pdf, cast, begin };
874
+ }
875
+
876
+ async function driveCast(client, page, frameMs) {
877
+ const start = await outcome(
878
+ client.send('Page.startScreencast', { format: 'png', everyNthFrame: 1 }, page)
879
+ );
880
+ const cursor = client.events?.length ?? 0;
881
+ return {
882
+ castBefore: start.ok,
883
+ castBeforeRefusal: presentish(start),
884
+ async afterEmbed() {
885
+ await pause(Math.max(frameMs, 200));
886
+ const frames = (client.events ?? [])
887
+ .slice(cursor)
888
+ .filter((event) => event.method === 'Page.screencastFrame' && event.sessionId === page);
889
+ if (start.ok) await outcome(client.send('Page.stopScreencast', {}, page));
890
+ return {
891
+ castLeak: frames.length > 0,
892
+ castFramesAfter: frames.length,
893
+ };
894
+ },
895
+ };
896
+ }
897
+
898
+ export async function driveReveal(client, reader, page, observer, frameMs, missing, side) {
899
+ const captureStartedAt = await observe(reader, observer, 'performance.now()');
900
+ let captureSettled = false;
901
+ let captureSettledAt;
902
+ const pending = outcome(client.send('Page.captureScreenshot', { format: 'png' }, page)).then(
903
+ async (out) => {
904
+ captureSettled = true;
905
+ captureSettledAt = await observe(reader, observer, 'performance.now()');
906
+ return out;
907
+ }
908
+ );
909
+ const revealAt = await observe(
910
+ reader,
911
+ observer,
912
+ `(() => {
913
+ const f = ${frameLookup};
914
+ if (!f) return null;
915
+ ${STYLES.shown};
916
+ return performance.now();
917
+ })()`
918
+ );
919
+ const pendingWhenRevealAcknowledged = !captureSettled;
920
+ const out = await pending;
921
+ const ok = out.ok && typeof out.result?.data === 'string' && out.result.data.length > 0;
922
+ const text = refusal(out);
923
+ let overlap = 'unknown';
924
+ if (typeof captureStartedAt !== 'number' || typeof revealAt !== 'number') overlap = 'unknown';
925
+ else if (revealAt <= captureStartedAt) overlap = 'before';
926
+ else if (pendingWhenRevealAcknowledged) overlap = 'during';
927
+ else if (typeof captureSettledAt === 'number' && captureSettledAt < revealAt) overlap = 'after';
928
+ if (overlap === 'unknown') missing.push('reveal/capture overlap unproven');
929
+ const shownRefused = Boolean(text && (text === SHOWN || /^slicc: refused/.test(text)));
930
+ let revealRacePass = false;
931
+ if (overlap === 'during' || overlap === 'before')
932
+ revealRacePass = side === 'control' ? ok : shownRefused;
933
+ else if (overlap === 'after') revealRacePass = ok || shownRefused;
934
+ const revealedPress = await click(client, reader, page, observer);
935
+ await style(reader, observer, 'hidden', frameMs);
936
+ const rehidden = await shot(client, page);
937
+ return {
938
+ revealedShot: ok,
939
+ revealedRefusal: text,
940
+ revealOverlap: overlap,
941
+ revealRacePass,
942
+ revealedClick: revealedPress.ok,
943
+ revealedClickRefusal: refusal(revealedPress.out),
944
+ rehiddenShot: rehidden.ok,
945
+ };
946
+ }
947
+
948
+ function frameObservations({ pageAttached, origin, read, leaks, picture, input, captures }) {
949
+ return {
950
+ exists: true,
951
+ attached: pageAttached,
952
+ read: read.ok && read.result.result?.value === origin && !carries(read.result),
953
+ snapshot: leaks.snapshot,
954
+ snapshotFrameRead: leaks.snapshotFrameRead,
955
+ frameTreeLeak: leaks.frameTreeLeak,
956
+ documentLeak: leaks.documentLeak,
957
+ shadowDocumentLeak: leaks.shadowDocumentLeak,
958
+ axLeak: leaks.axLeak,
959
+ evaluateLeak: leaks.evaluateLeak,
960
+ partialAxLeak: leaks.partialAxLeak,
961
+ describeNodeLeak: leaks.describeNodeLeak,
962
+ outerHTMLLeak: leaks.outerHTMLLeak,
963
+ shot: picture.ok,
964
+ shotRefusal: refusal(picture.out),
965
+ click: input.pressed.ok,
966
+ clickRefusal: refusal(input.pressed.out),
967
+ key: input.typed.ok,
968
+ keyRefusal: refusal(input.typed.out),
969
+ frameClick: input.frameClick.ok,
970
+ frameClickRefusal: refusal(input.frameClick.out),
971
+ tab: input.tab.ok,
972
+ tabRefusal: refusal(input.tab),
973
+ frameVisible: input.visible === true,
974
+ frameFocused: input.frameFocused === true,
975
+ focusedKey: input.focusedKey.ok,
976
+ focusedKeyRefusal: refusal(input.focusedKey),
977
+ focusedText: input.focusedText.ok,
978
+ focusedTextRefusal: refusal(input.focusedText),
979
+ focusedIME: input.focusedIME.ok,
980
+ focusedIMERefusal: refusal(input.focusedIME),
981
+ archive: captures.archive.ok,
982
+ archiveRefusal: presentish(captures.archive),
983
+ pdf: captures.pdf.ok,
984
+ pdfRefusal: presentish(captures.pdf),
985
+ cast: captures.cast.ok,
986
+ castRefusal: presentish(captures.cast),
987
+ beginFrame: captures.begin.ok,
988
+ beginFrameRefusal: presentish(captures.begin),
989
+ inputs: input.inputs,
990
+ };
991
+ }
992
+
993
+ export async function extensionFrame({
994
+ client,
995
+ reader,
996
+ session,
997
+ targetId,
998
+ mode,
999
+ origin,
1000
+ extensionId = DEFAULT_EXTENSION_WORLD_ID,
1001
+ frameMs = 1000,
1002
+ inputMethods: inputList = INPUT_METHODS,
1003
+ side = 'guarded',
1004
+ capabilities = {},
1005
+ }) {
1006
+ const attached = await reader.send('Target.attachToTarget', { targetId, flatten: true });
1007
+ const observer = attached.sessionId;
1008
+ const missing = [];
1009
+ const eventStart = client.events?.length;
1010
+ const frameURL = `chrome-extension://${extensionId}/frame.html`;
1011
+ let page;
1012
+ let pageAttached = false;
1013
+ const discovery = hasCapability(capabilities, 'discovery')
1014
+ ? await outcome(client.send('Target.setDiscoverTargets', { discover: true }))
1015
+ : { ok: false };
1016
+ try {
1017
+ await reader.send('Runtime.enable', {}, observer);
1018
+ await reader.send('DOM.enable', {}, observer);
1019
+ await reader.send('Accessibility.enable', {}, observer);
1020
+ let castDrive;
1021
+ if (mode === 'cast') {
1022
+ const attachEarly = await outcome(
1023
+ client.send('Target.attachToTarget', { targetId, flatten: true })
1024
+ );
1025
+ page = attachEarly.ok ? attachEarly.result.sessionId : session;
1026
+ pageAttached = attachEarly.ok;
1027
+ await outcome(client.send('DOM.enable', {}, page));
1028
+ await outcome(client.send('Accessibility.enable', {}, page));
1029
+ castDrive = await driveCast(client, page, frameMs);
1030
+ }
1031
+ await embed(reader, observer, mode, frameURL);
1032
+ const want = mode === 'focused' ? 1 : 2;
1033
+ const frames = await frameTargets(
1034
+ reader,
1035
+ targetId,
1036
+ extensionId,
1037
+ want,
1038
+ missing,
1039
+ frameMs === 0 ? 100 : 5000
1040
+ );
1041
+ const frame = frames[0];
1042
+ await paintMarker(reader, frame, mode === 'focused');
1043
+ await painted(reader, observer, frameMs);
1044
+ const castAfter = castDrive ? await castDrive.afterEmbed() : null;
1045
+ if (!page) {
1046
+ const attach = await outcome(
1047
+ client.send('Target.attachToTarget', { targetId, flatten: true })
1048
+ );
1049
+ page = attach.ok ? attach.result.sessionId : session;
1050
+ pageAttached = attach.ok;
1051
+ await outcome(client.send('DOM.enable', {}, page));
1052
+ await outcome(client.send('Accessibility.enable', {}, page));
1053
+ }
1054
+ const read = await outcome(
1055
+ client.send('Runtime.evaluate', { expression: 'self.origin', returnByValue: true }, page)
1056
+ );
1057
+ const leaks = await pageLeaks({
1058
+ client,
1059
+ page,
1060
+ reader,
1061
+ observer,
1062
+ frame,
1063
+ extensionId,
1064
+ frameURL,
1065
+ missing,
1066
+ capabilities,
1067
+ });
1068
+ if (mode === 'focused') await holdFocus(reader, frame);
1069
+ const picture = await shot(client, page);
1070
+ const input = await driveInput(
1071
+ client,
1072
+ reader,
1073
+ page,
1074
+ observer,
1075
+ inputList,
1076
+ mode,
1077
+ frame,
1078
+ missing,
1079
+ side
1080
+ );
1081
+ const captures = await driveCaptures(client, page, missing);
1082
+ const result = frameObservations({
1083
+ pageAttached,
1084
+ origin,
1085
+ read,
1086
+ leaks,
1087
+ picture,
1088
+ input,
1089
+ captures,
1090
+ });
1091
+ if (castDrive) {
1092
+ result.castBefore = castDrive.castBefore;
1093
+ result.castBeforeRefusal = castDrive.castBeforeRefusal;
1094
+ result.castLeak = castAfter.castLeak;
1095
+ result.castFramesAfter = castAfter.castFramesAfter;
1096
+ if (side === 'control' && castDrive.castBefore && !castAfter.castLeak)
1097
+ missing.push('control screencast produced no frames after the extension frame appeared');
1098
+ }
1099
+ if (mode === 'revealed')
1100
+ Object.assign(
1101
+ result,
1102
+ await driveReveal(client, reader, page, observer, frameMs, missing, side)
1103
+ );
1104
+ result.frameAttach = false;
1105
+ for (const entry of frames) {
1106
+ const frameAttach = await outcome(
1107
+ client.send('Target.attachToTarget', { targetId: entry.targetId, flatten: true })
1108
+ );
1109
+ if (frameAttach.ok) {
1110
+ result.frameAttach = true;
1111
+ await outcome(
1112
+ client.send('Target.detachFromTarget', { sessionId: frameAttach.result.sessionId })
1113
+ );
1114
+ }
1115
+ }
1116
+ Object.assign(
1117
+ result,
1118
+ await targetExposure({
1119
+ client,
1120
+ reader,
1121
+ observer,
1122
+ page,
1123
+ frames,
1124
+ frameURL,
1125
+ extensionId,
1126
+ eventStart,
1127
+ frameMs,
1128
+ missing,
1129
+ capabilities,
1130
+ })
1131
+ );
1132
+ const alive = await outcome(
1133
+ client.send('Runtime.evaluate', { expression: 'self.origin', returnByValue: true }, page)
1134
+ );
1135
+ result.alive = alive.ok && alive.result.result?.value === origin && !carries(alive.result);
1136
+ return missing.length ? { ...result, incomplete: missing.join('; ') } : result;
1137
+ } finally {
1138
+ if (pageAttached) await outcome(client.send('Target.detachFromTarget', { sessionId: page }));
1139
+ if (discovery.ok) await outcome(client.send('Target.setDiscoverTargets', { discover: false }));
1140
+ await outcome(reader.send('Target.detachFromTarget', { sessionId: observer }));
1141
+ }
1142
+ }