@mayurajs/browser-viewer 0.0.0-stage → 1.1.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.
@@ -0,0 +1,235 @@
1
+ import { MayuraError } from 'mayura';
2
+ import { connectCdp } from 'mayura/browser';
3
+ const named = {
4
+ Enter: { code: 'Enter', keyCode: 13, text: '\r' }, Tab: { code: 'Tab', keyCode: 9 }, Escape: { code: 'Escape', keyCode: 27 },
5
+ Backspace: { code: 'Backspace', keyCode: 8 }, Delete: { code: 'Delete', keyCode: 46 },
6
+ ArrowUp: { code: 'ArrowUp', keyCode: 38 }, ArrowDown: { code: 'ArrowDown', keyCode: 40 }, ArrowLeft: { code: 'ArrowLeft', keyCode: 37 }, ArrowRight: { code: 'ArrowRight', keyCode: 39 },
7
+ Home: { code: 'Home', keyCode: 36 }, End: { code: 'End', keyCode: 35 }, PageUp: { code: 'PageUp', keyCode: 33 }, PageDown: { code: 'PageDown', keyCode: 34 },
8
+ Shift: { code: 'ShiftLeft', keyCode: 16 }, Control: { code: 'ControlLeft', keyCode: 17 }, Alt: { code: 'AltLeft', keyCode: 18 }, Meta: { code: 'MetaLeft', keyCode: 91 },
9
+ };
10
+ const buttons = { left: 'left', middle: 'middle', right: 'right' };
11
+ const mouseTypes = { down: 'mousePressed', up: 'mouseReleased', move: 'mouseMoved' };
12
+ function bound(value, name, fallback, min, max) {
13
+ const result = value ?? fallback;
14
+ if (!Number.isSafeInteger(result) || result < min || result > max)
15
+ throw new MayuraError('INVALID_CONFIG', `browserScreencast(): ${name} is ${min.toLocaleString('en-US')} to ${max.toLocaleString('en-US')}.`);
16
+ return result;
17
+ }
18
+ const fraction = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
19
+ const modifiersOf = (value) => {
20
+ if (value === undefined)
21
+ return 0;
22
+ if (!Number.isSafeInteger(value) || value < 0 || value > 15)
23
+ throw new MayuraError('INVALID_INPUT', 'modifiers is 0 to 15 (Alt 1, Control 2, Meta 4, Shift 8).');
24
+ return value;
25
+ };
26
+ function decode(base64) {
27
+ const binary = atob(base64);
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let index = 0; index < binary.length; index++)
30
+ bytes[index] = binary.charCodeAt(index);
31
+ return bytes;
32
+ }
33
+ /**
34
+ * A screencast of a `mayura/browser` browser: the active tab's frames as JPEGs, over a CDP connection of its own, and,
35
+ * with `interact`, a way for a person to use the page. Frames come no faster than `maxFps`, and only as fast as they
36
+ * are taken. The browser's origins hold for whatever a person does. It needs a browser other clients can join
37
+ * (`browser.cdp`).
38
+ */
39
+ export async function browserScreencast(browser, options = {}) {
40
+ if (!browser || typeof browser.tabs !== 'function')
41
+ throw new MayuraError('INVALID_CONFIG', 'browserScreencast() needs a browser.');
42
+ const quality = bound(options.quality, 'quality', 60, 1, 100);
43
+ const maxWidth = bound(options.maxWidth, 'maxWidth', 1_280, 320, 3_840);
44
+ const maxHeight = bound(options.maxHeight, 'maxHeight', 800, 240, 2_160);
45
+ const maxFps = bound(options.maxFps, 'maxFps', 5, 1, 30);
46
+ const maxFrameBytes = bound(options.maxFrameBytes, 'maxFrameBytes', 2_097_152, 65_536, 16_777_216);
47
+ if (options.interact !== undefined && typeof options.interact !== 'boolean')
48
+ throw new MayuraError('INVALID_CONFIG', 'browserScreencast(): interact must be a boolean.');
49
+ const cdp = browser.cdp;
50
+ if (!cdp)
51
+ throw new MayuraError('INVALID_CONFIG', `The ${browser.provider} provider's browsers cannot be joined: each connection starts a browser of its own.`);
52
+ if (browser.ended)
53
+ throw new MayuraError('INVALID_INPUT', 'The browser has ended.');
54
+ const connection = await connectCdp(cdp.url, { headers: cdp.headers, ...(options.webSocket ? { webSocket: options.webSocket } : {}), ...(options.signal ? { signal: options.signal } : {}) });
55
+ let ended = false;
56
+ let shown;
57
+ let size = { width: 0, height: 0 };
58
+ let waiting;
59
+ let taker;
60
+ let lastAck = 0;
61
+ let ackTimer;
62
+ let followTimer;
63
+ let failures = 0;
64
+ const end = () => {
65
+ if (ended)
66
+ return;
67
+ ended = true;
68
+ clearTimeout(ackTimer);
69
+ clearTimeout(followTimer);
70
+ options.signal?.removeEventListener('abort', end);
71
+ waiting = undefined;
72
+ const take = taker;
73
+ taker = undefined;
74
+ take?.(undefined);
75
+ connection.close();
76
+ };
77
+ options.signal?.addEventListener('abort', end, { once: true });
78
+ void connection.closed.then(end);
79
+ const ack = (sessionId, id) => { void connection.send('Page.screencastFrameAck', { sessionId: id }, { sessionId }).catch(() => undefined); };
80
+ const offFrames = connection.on('Page.screencastFrame', (params, sessionId) => {
81
+ if (ended || !shown || sessionId !== shown.sessionId)
82
+ return;
83
+ const id = params['sessionId'];
84
+ const data = params['data'];
85
+ const metadata = params['metadata'];
86
+ if (typeof id !== 'number' || typeof data !== 'string')
87
+ return;
88
+ // Base64 is 4/3 of the bytes: a frame too large is skipped, unread.
89
+ if (data.length > Math.ceil(maxFrameBytes / 3) * 4) {
90
+ ack(sessionId, id);
91
+ return;
92
+ }
93
+ const width = typeof metadata?.deviceWidth === 'number' && metadata.deviceWidth > 0 ? metadata.deviceWidth : size.width;
94
+ const height = typeof metadata?.deviceHeight === 'number' && metadata.deviceHeight > 0 ? metadata.deviceHeight : size.height;
95
+ let bytes;
96
+ try {
97
+ bytes = decode(data);
98
+ }
99
+ catch {
100
+ ack(sessionId, id);
101
+ return;
102
+ }
103
+ size = { width, height };
104
+ const frame = Object.freeze({ data: bytes, mediaType: 'image/jpeg', width, height, tab: shown.tab });
105
+ if (taker) {
106
+ const take = taker;
107
+ taker = undefined;
108
+ take(frame);
109
+ schedule(sessionId, id);
110
+ }
111
+ else
112
+ waiting = { frame, ack: id, sessionId };
113
+ });
114
+ void connection.closed.then(offFrames);
115
+ // The next frame is asked for once this one is taken, and no sooner than maxFps allows.
116
+ const schedule = (sessionId, id) => {
117
+ const wait = Math.max(0, lastAck + 1_000 / maxFps - Date.now());
118
+ ackTimer = setTimeout(() => { lastAck = Date.now(); ack(sessionId, id); }, wait);
119
+ };
120
+ const show = async (tab) => {
121
+ const previous = shown;
122
+ shown = undefined;
123
+ waiting = undefined;
124
+ if (previous)
125
+ await connection.send('Target.detachFromTarget', { sessionId: previous.sessionId }).catch(() => undefined);
126
+ const { sessionId } = await connection.send('Target.attachToTarget', { targetId: tab, flatten: true });
127
+ shown = { tab, sessionId };
128
+ await connection.send('Page.startScreencast', { format: 'jpeg', quality, maxWidth, maxHeight, everyNthFrame: 1 }, { sessionId });
129
+ };
130
+ // Follows the tab the browser has active, as its agent moves between tabs.
131
+ const follow = async () => {
132
+ if (ended)
133
+ return;
134
+ if (browser.ended) {
135
+ end();
136
+ return;
137
+ }
138
+ try {
139
+ const active = (await browser.tabs(options.signal ? { signal: options.signal } : {})).find(tab => tab.active);
140
+ if (active && active.tab !== shown?.tab)
141
+ await show(active.tab);
142
+ failures = 0;
143
+ }
144
+ catch {
145
+ if (browser.ended || ++failures >= 3) {
146
+ end();
147
+ return;
148
+ }
149
+ }
150
+ if (!ended)
151
+ followTimer = setTimeout(() => void follow(), 1_000);
152
+ };
153
+ await follow();
154
+ const next = () => {
155
+ if (ended)
156
+ return Promise.resolve(undefined);
157
+ if (waiting) {
158
+ const { frame, ack: id, sessionId } = waiting;
159
+ waiting = undefined;
160
+ schedule(sessionId, id);
161
+ return Promise.resolve(frame);
162
+ }
163
+ if (taker)
164
+ return Promise.reject(new MayuraError('INVALID_INPUT', 'Take one frame at a time.'));
165
+ return new Promise(resolve => { taker = resolve; });
166
+ };
167
+ const input = async (event, inputOptions = {}) => {
168
+ if (options.interact !== true)
169
+ throw new MayuraError('PERMISSION_DENIED', 'This screencast is view only.');
170
+ if (ended)
171
+ throw new MayuraError('INVALID_INPUT', 'The screencast has ended.');
172
+ const on = shown;
173
+ if (!on || size.width === 0)
174
+ throw new MayuraError('INVALID_INPUT', 'No page is shown yet.');
175
+ const send = (method, params) => connection.send(method, params, { sessionId: on.sessionId, ...(inputOptions.signal ? { signal: inputOptions.signal } : {}) });
176
+ const item = event;
177
+ if (!item || typeof item !== 'object')
178
+ throw new MayuraError('INVALID_INPUT', 'An input is an object with a type.');
179
+ if (item['type'] === 'mouse' || item['type'] === 'wheel') {
180
+ if (!fraction(item['x']) || !fraction(item['y']))
181
+ throw new MayuraError('INVALID_INPUT', 'x and y are fractions of the frame, 0 to 1.');
182
+ const point = { x: Math.round(item['x'] * size.width), y: Math.round(item['y'] * size.height) };
183
+ if (item['type'] === 'wheel') {
184
+ const { dx, dy } = item;
185
+ if (typeof dx !== 'number' || typeof dy !== 'number' || !Number.isFinite(dx) || !Number.isFinite(dy) || Math.abs(dx) > 10_000 || Math.abs(dy) > 10_000)
186
+ throw new MayuraError('INVALID_INPUT', 'dx and dy are pixels, -10,000 to 10,000.');
187
+ await send('Input.dispatchMouseEvent', { type: 'mouseWheel', ...point, deltaX: dx, deltaY: dy });
188
+ return;
189
+ }
190
+ const type = mouseTypes[item['action']];
191
+ const button = item['button'] === undefined ? 'left' : buttons[item['button']];
192
+ const clicks = item['clicks'] ?? 1;
193
+ if (!Object.hasOwn(mouseTypes, item['action']) || !button || !Object.hasOwn(buttons, button) || !Number.isSafeInteger(clicks) || clicks < 1 || clicks > 3) {
194
+ throw new MayuraError('INVALID_INPUT', 'A mouse input has action down, up or move, a button left, middle or right, and clicks 1 to 3.');
195
+ }
196
+ await send('Input.dispatchMouseEvent', { type, ...point, modifiers: modifiersOf(item['modifiers']), ...(type === 'mouseMoved' ? {} : { button, clickCount: clicks }) });
197
+ return;
198
+ }
199
+ if (item['type'] === 'key') {
200
+ const key = item['key'];
201
+ if ((item['action'] !== 'down' && item['action'] !== 'up') || typeof key !== 'string')
202
+ throw new MayuraError('INVALID_INPUT', 'A key input has action down or up, and a key.');
203
+ const modifiers = modifiersOf(item['modifiers']);
204
+ const definition = Object.hasOwn(named, key) ? named[key] : [...key].length === 1 && /^[^\p{Cc}]$/u.test(key) ? {
205
+ code: /^[a-z]$/iu.test(key) ? `Key${key.toUpperCase()}` : /^\d$/u.test(key) ? `Digit${key}` : '', keyCode: /^[a-z\d]$/iu.test(key) ? key.toUpperCase().charCodeAt(0) : 0,
206
+ // Typed as text unless Control, Alt or Meta is held.
207
+ ...(modifiers & 7 ? {} : { text: key }),
208
+ } : undefined;
209
+ if (!definition)
210
+ throw new MayuraError('INVALID_INPUT', 'key is one character, or a key such as Enter, Tab, Backspace or ArrowDown.');
211
+ const common = { key, code: definition.code, windowsVirtualKeyCode: definition.keyCode, modifiers };
212
+ await send('Input.dispatchKeyEvent', item['action'] === 'up' ? { type: 'keyUp', ...common }
213
+ : definition.text !== undefined ? { type: 'keyDown', ...common, text: definition.text, unmodifiedText: definition.text } : { type: 'rawKeyDown', ...common });
214
+ return;
215
+ }
216
+ if (item['type'] === 'text') {
217
+ if (typeof item['text'] !== 'string' || item['text'].length === 0 || item['text'].length > 1_000)
218
+ throw new MayuraError('INVALID_INPUT', 'text is 1 to 1,000 characters.');
219
+ await send('Input.insertText', { text: item['text'] });
220
+ return;
221
+ }
222
+ throw new MayuraError('INVALID_INPUT', 'type is mouse, wheel, key or text.');
223
+ };
224
+ const screencast = {
225
+ next, input,
226
+ get ended() { return ended; },
227
+ close: end,
228
+ [Symbol.asyncIterator]: () => ({
229
+ next: async () => { const frame = await next(); return frame ? { value: frame, done: false } : { value: undefined, done: true }; },
230
+ return: async () => { end(); return { value: undefined, done: true }; },
231
+ }),
232
+ };
233
+ return screencast;
234
+ }
235
+ //# sourceMappingURL=screencast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screencast.js","sourceRoot":"","sources":["../src/screencast.ts"],"names":[],"mappings":"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","sourcesContent":["import { MayuraError } from 'mayura';\nimport { connectCdp, type Browser, type CdpConnection, type CdpSocketFactory } from 'mayura/browser';\n\nexport interface ScreencastOptions {\n /** JPEG quality, 1 to 100; 60 by default. */\n readonly quality?: number;\n /** The widest frame, in pixels; 1,280 by default (320 to 3,840). */\n readonly maxWidth?: number;\n /** The tallest frame, in pixels; 800 by default (240 to 2,160). */\n readonly maxHeight?: number;\n /** The most frames a second; 5 by default (1 to 30). */\n readonly maxFps?: number;\n /** Frames larger than this are skipped, in bytes; 2 MiB by default. */\n readonly maxFrameBytes?: number;\n /** Lets `input` use the page, as a person would: off by default. */\n readonly interact?: boolean;\n /** For runtimes whose WebSocket cannot send the headers a browser's CDP connection needs. */\n readonly webSocket?: CdpSocketFactory;\n /** Ends the screencast. */\n readonly signal?: AbortSignal;\n}\n\nexport interface ScreencastFrame {\n /** A JPEG of the page as shown. */\n readonly data: Uint8Array;\n readonly mediaType: 'image/jpeg';\n /** The page's size in CSS pixels, which `input` coordinates are fractions of. */\n readonly width: number;\n readonly height: number;\n /** The tab shown, as `browser.tabs()` names it. */\n readonly tab: string;\n}\n\n/** What a person does in the viewer. Coordinates are fractions of the frame, 0 to 1 from the top left. */\nexport type ViewerInput =\n | { readonly type: 'mouse'; readonly action: 'down' | 'up' | 'move'; readonly x: number; readonly y: number; readonly button?: 'left' | 'middle' | 'right'; readonly clicks?: number; readonly modifiers?: number }\n | { readonly type: 'wheel'; readonly x: number; readonly y: number; readonly dx: number; readonly dy: number }\n | { readonly type: 'key'; readonly action: 'down' | 'up'; readonly key: string; readonly modifiers?: number }\n | { readonly type: 'text'; readonly text: string };\n\n/** The frames of a browser's active tab as it changes, following the tab the browser has active. */\nexport interface Screencast extends AsyncIterable<ScreencastFrame> {\n /** The next frame, waiting for one; undefined once the screencast ended. */\n next(): Promise<ScreencastFrame | undefined>;\n /** Uses the page shown, for screencasts with `interact`. */\n input(event: ViewerInput, options?: { readonly signal?: AbortSignal }): Promise<void>;\n /** True once closed, or once its browser or connection ended. */\n readonly ended: boolean;\n close(): void;\n}\n\nconst named: Readonly<Record<string, { readonly code: string; readonly keyCode: number; readonly text?: string }>> = {\n Enter: { code: 'Enter', keyCode: 13, text: '\\r' }, Tab: { code: 'Tab', keyCode: 9 }, Escape: { code: 'Escape', keyCode: 27 },\n Backspace: { code: 'Backspace', keyCode: 8 }, Delete: { code: 'Delete', keyCode: 46 },\n ArrowUp: { code: 'ArrowUp', keyCode: 38 }, ArrowDown: { code: 'ArrowDown', keyCode: 40 }, ArrowLeft: { code: 'ArrowLeft', keyCode: 37 }, ArrowRight: { code: 'ArrowRight', keyCode: 39 },\n Home: { code: 'Home', keyCode: 36 }, End: { code: 'End', keyCode: 35 }, PageUp: { code: 'PageUp', keyCode: 33 }, PageDown: { code: 'PageDown', keyCode: 34 },\n Shift: { code: 'ShiftLeft', keyCode: 16 }, Control: { code: 'ControlLeft', keyCode: 17 }, Alt: { code: 'AltLeft', keyCode: 18 }, Meta: { code: 'MetaLeft', keyCode: 91 },\n};\nconst buttons = { left: 'left', middle: 'middle', right: 'right' } as const;\nconst mouseTypes = { down: 'mousePressed', up: 'mouseReleased', move: 'mouseMoved' } as const;\n\nfunction bound(value: number | undefined, name: string, fallback: number, min: number, max: number): number {\n const result = value ?? fallback;\n if (!Number.isSafeInteger(result) || result < min || result > max) throw new MayuraError('INVALID_CONFIG', `browserScreencast(): ${name} is ${min.toLocaleString('en-US')} to ${max.toLocaleString('en-US')}.`);\n return result;\n}\nconst fraction = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;\nconst modifiersOf = (value: unknown): number => {\n if (value === undefined) return 0;\n if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > 15) throw new MayuraError('INVALID_INPUT', 'modifiers is 0 to 15 (Alt 1, Control 2, Meta 4, Shift 8).');\n return value as number;\n};\nfunction decode(base64: string): Uint8Array {\n const binary = atob(base64); const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);\n return bytes;\n}\n\n/**\n * A screencast of a `mayura/browser` browser: the active tab's frames as JPEGs, over a CDP connection of its own, and,\n * with `interact`, a way for a person to use the page. Frames come no faster than `maxFps`, and only as fast as they\n * are taken. The browser's origins hold for whatever a person does. It needs a browser other clients can join\n * (`browser.cdp`).\n */\nexport async function browserScreencast(browser: Browser, options: ScreencastOptions = {}): Promise<Screencast> {\n if (!browser || typeof browser.tabs !== 'function') throw new MayuraError('INVALID_CONFIG', 'browserScreencast() needs a browser.');\n const quality = bound(options.quality, 'quality', 60, 1, 100);\n const maxWidth = bound(options.maxWidth, 'maxWidth', 1_280, 320, 3_840);\n const maxHeight = bound(options.maxHeight, 'maxHeight', 800, 240, 2_160);\n const maxFps = bound(options.maxFps, 'maxFps', 5, 1, 30);\n const maxFrameBytes = bound(options.maxFrameBytes, 'maxFrameBytes', 2_097_152, 65_536, 16_777_216);\n if (options.interact !== undefined && typeof options.interact !== 'boolean') throw new MayuraError('INVALID_CONFIG', 'browserScreencast(): interact must be a boolean.');\n const cdp = browser.cdp;\n if (!cdp) throw new MayuraError('INVALID_CONFIG', `The ${browser.provider} provider's browsers cannot be joined: each connection starts a browser of its own.`);\n if (browser.ended) throw new MayuraError('INVALID_INPUT', 'The browser has ended.');\n\n const connection: CdpConnection = await connectCdp(cdp.url, { headers: cdp.headers, ...(options.webSocket ? { webSocket: options.webSocket } : {}), ...(options.signal ? { signal: options.signal } : {}) });\n let ended = false;\n let shown: { readonly tab: string; readonly sessionId: string } | undefined;\n let size = { width: 0, height: 0 };\n let waiting: { readonly frame: ScreencastFrame; readonly ack: number; readonly sessionId: string } | undefined;\n let taker: ((frame: ScreencastFrame | undefined) => void) | undefined;\n let lastAck = 0; let ackTimer: ReturnType<typeof setTimeout> | undefined;\n let followTimer: ReturnType<typeof setTimeout> | undefined; let failures = 0;\n\n const end = () => {\n if (ended) return;\n ended = true;\n clearTimeout(ackTimer); clearTimeout(followTimer);\n options.signal?.removeEventListener('abort', end);\n waiting = undefined; const take = taker; taker = undefined; take?.(undefined);\n connection.close();\n };\n options.signal?.addEventListener('abort', end, { once: true });\n void connection.closed.then(end);\n\n const ack = (sessionId: string, id: number) => { void connection.send('Page.screencastFrameAck', { sessionId: id }, { sessionId }).catch(() => undefined); };\n const offFrames = connection.on('Page.screencastFrame', (params, sessionId) => {\n if (ended || !shown || sessionId !== shown.sessionId) return;\n const id = params['sessionId']; const data = params['data']; const metadata = params['metadata'] as { deviceWidth?: unknown; deviceHeight?: unknown } | undefined;\n if (typeof id !== 'number' || typeof data !== 'string') return;\n // Base64 is 4/3 of the bytes: a frame too large is skipped, unread.\n if (data.length > Math.ceil(maxFrameBytes / 3) * 4) { ack(sessionId, id); return; }\n const width = typeof metadata?.deviceWidth === 'number' && metadata.deviceWidth > 0 ? metadata.deviceWidth : size.width;\n const height = typeof metadata?.deviceHeight === 'number' && metadata.deviceHeight > 0 ? metadata.deviceHeight : size.height;\n let bytes: Uint8Array;\n try { bytes = decode(data); } catch { ack(sessionId, id); return; }\n size = { width, height };\n const frame: ScreencastFrame = Object.freeze({ data: bytes, mediaType: 'image/jpeg' as const, width, height, tab: shown.tab });\n if (taker) { const take = taker; taker = undefined; take(frame); schedule(sessionId, id); }\n else waiting = { frame, ack: id, sessionId };\n });\n void connection.closed.then(offFrames);\n // The next frame is asked for once this one is taken, and no sooner than maxFps allows.\n const schedule = (sessionId: string, id: number) => {\n const wait = Math.max(0, lastAck + 1_000 / maxFps - Date.now());\n ackTimer = setTimeout(() => { lastAck = Date.now(); ack(sessionId, id); }, wait);\n };\n\n const show = async (tab: string) => {\n const previous = shown; shown = undefined; waiting = undefined;\n if (previous) await connection.send('Target.detachFromTarget', { sessionId: previous.sessionId }).catch(() => undefined);\n const { sessionId } = await connection.send<{ sessionId: string }>('Target.attachToTarget', { targetId: tab, flatten: true });\n shown = { tab, sessionId };\n await connection.send('Page.startScreencast', { format: 'jpeg', quality, maxWidth, maxHeight, everyNthFrame: 1 }, { sessionId });\n };\n // Follows the tab the browser has active, as its agent moves between tabs.\n const follow = async () => {\n if (ended) return;\n if (browser.ended) { end(); return; }\n try {\n const active = (await browser.tabs(options.signal ? { signal: options.signal } : {})).find(tab => tab.active);\n if (active && active.tab !== shown?.tab) await show(active.tab);\n failures = 0;\n } catch {\n if (browser.ended || ++failures >= 3) { end(); return; }\n }\n if (!ended) followTimer = setTimeout(() => void follow(), 1_000);\n };\n await follow();\n\n const next = (): Promise<ScreencastFrame | undefined> => {\n if (ended) return Promise.resolve(undefined);\n if (waiting) { const { frame, ack: id, sessionId } = waiting; waiting = undefined; schedule(sessionId, id); return Promise.resolve(frame); }\n if (taker) return Promise.reject(new MayuraError('INVALID_INPUT', 'Take one frame at a time.'));\n return new Promise(resolve => { taker = resolve; });\n };\n\n const input = async (event: ViewerInput, inputOptions: { readonly signal?: AbortSignal } = {}): Promise<void> => {\n if (options.interact !== true) throw new MayuraError('PERMISSION_DENIED', 'This screencast is view only.');\n if (ended) throw new MayuraError('INVALID_INPUT', 'The screencast has ended.');\n const on = shown;\n if (!on || size.width === 0) throw new MayuraError('INVALID_INPUT', 'No page is shown yet.');\n const send = (method: string, params: Record<string, unknown>) => connection.send(method, params, { sessionId: on.sessionId, ...(inputOptions.signal ? { signal: inputOptions.signal } : {}) });\n const item = event as Record<string, unknown> | null;\n if (!item || typeof item !== 'object') throw new MayuraError('INVALID_INPUT', 'An input is an object with a type.');\n if (item['type'] === 'mouse' || item['type'] === 'wheel') {\n if (!fraction(item['x']) || !fraction(item['y'])) throw new MayuraError('INVALID_INPUT', 'x and y are fractions of the frame, 0 to 1.');\n const point = { x: Math.round(item['x'] * size.width), y: Math.round(item['y'] * size.height) };\n if (item['type'] === 'wheel') {\n const { dx, dy } = item;\n if (typeof dx !== 'number' || typeof dy !== 'number' || !Number.isFinite(dx) || !Number.isFinite(dy) || Math.abs(dx) > 10_000 || Math.abs(dy) > 10_000) throw new MayuraError('INVALID_INPUT', 'dx and dy are pixels, -10,000 to 10,000.');\n await send('Input.dispatchMouseEvent', { type: 'mouseWheel', ...point, deltaX: dx, deltaY: dy });\n return;\n }\n const type = mouseTypes[item['action'] as keyof typeof mouseTypes];\n const button = item['button'] === undefined ? 'left' : buttons[item['button'] as keyof typeof buttons];\n const clicks = item['clicks'] ?? 1;\n if (!Object.hasOwn(mouseTypes, item['action'] as string) || !button || !Object.hasOwn(buttons, button) || !Number.isSafeInteger(clicks) || (clicks as number) < 1 || (clicks as number) > 3) {\n throw new MayuraError('INVALID_INPUT', 'A mouse input has action down, up or move, a button left, middle or right, and clicks 1 to 3.');\n }\n await send('Input.dispatchMouseEvent', { type, ...point, modifiers: modifiersOf(item['modifiers']), ...(type === 'mouseMoved' ? {} : { button, clickCount: clicks }) });\n return;\n }\n if (item['type'] === 'key') {\n const key = item['key'];\n if ((item['action'] !== 'down' && item['action'] !== 'up') || typeof key !== 'string') throw new MayuraError('INVALID_INPUT', 'A key input has action down or up, and a key.');\n const modifiers = modifiersOf(item['modifiers']);\n const definition = Object.hasOwn(named, key) ? named[key]! : [...key].length === 1 && /^[^\\p{Cc}]$/u.test(key) ? {\n code: /^[a-z]$/iu.test(key) ? `Key${key.toUpperCase()}` : /^\\d$/u.test(key) ? `Digit${key}` : '', keyCode: /^[a-z\\d]$/iu.test(key) ? key.toUpperCase().charCodeAt(0) : 0,\n // Typed as text unless Control, Alt or Meta is held.\n ...(modifiers & 7 ? {} : { text: key }),\n } : undefined;\n if (!definition) throw new MayuraError('INVALID_INPUT', 'key is one character, or a key such as Enter, Tab, Backspace or ArrowDown.');\n const common = { key, code: definition.code, windowsVirtualKeyCode: definition.keyCode, modifiers };\n await send('Input.dispatchKeyEvent', item['action'] === 'up' ? { type: 'keyUp', ...common }\n : definition.text !== undefined ? { type: 'keyDown', ...common, text: definition.text, unmodifiedText: definition.text } : { type: 'rawKeyDown', ...common });\n return;\n }\n if (item['type'] === 'text') {\n if (typeof item['text'] !== 'string' || item['text'].length === 0 || item['text'].length > 1_000) throw new MayuraError('INVALID_INPUT', 'text is 1 to 1,000 characters.');\n await send('Input.insertText', { text: item['text'] });\n return;\n }\n throw new MayuraError('INVALID_INPUT', 'type is mouse, wheel, key or text.');\n };\n\n const screencast: Screencast = {\n next, input,\n get ended() { return ended; },\n close: end,\n [Symbol.asyncIterator]: () => ({\n next: async () => { const frame = await next(); return frame ? { value: frame, done: false } : { value: undefined, done: true }; },\n return: async () => { end(); return { value: undefined, done: true }; },\n }),\n };\n return screencast;\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import type { Browser } from 'mayura/browser';
2
+ import { type BrowserViewerOptions, type ViewerShare, type ViewerShareOptions } from './viewer.js';
3
+ export interface ServeBrowserViewerOptions extends Omit<BrowserViewerOptions, 'basePath'> {
4
+ /** The address to listen on; `127.0.0.1` by default. Another needs `remote`. */
5
+ readonly host?: string;
6
+ /** The port; any free one by default. */
7
+ readonly port?: number;
8
+ /**
9
+ * Listen beyond this machine. Links carry their token in the clear over plain HTTP: put TLS in front, and give
10
+ * `publicUrl`.
11
+ */
12
+ readonly remote?: boolean;
13
+ /** The origin people reach this server at, such as `https://viewer.example`, for the links; this server's by default. */
14
+ readonly publicUrl?: string;
15
+ }
16
+ export interface ServedBrowserViewer {
17
+ /** Where the viewer is reached. */
18
+ readonly url: string;
19
+ /** A link to watch the browser, and with `interact` to use it: `url` is the whole link, a secret. */
20
+ share(browser: Browser, options?: ViewerShareOptions): Promise<ViewerShare & {
21
+ readonly url: string;
22
+ }>;
23
+ /** Revokes every share and stops the server. */
24
+ close(): Promise<void>;
25
+ }
26
+ /**
27
+ * A live viewer of its own on Node: an HTTP server answering `browserViewer`'s pages, on this machine only unless
28
+ * `remote`. Node only; on other runtimes, mount `browserViewer().handle` in your server.
29
+ */
30
+ export declare function serveBrowserViewer(options?: ServeBrowserViewerOptions): Promise<ServedBrowserViewer>;
31
+ //# sourceMappingURL=serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAiB,KAAK,oBAAoB,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAElH,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IACvF,gFAAgF;IAChF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,yHAAyH;IACzH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qGAAqG;IACrG,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvG,gDAAgD;IAChD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAID;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA0D9G"}
package/dist/serve.js ADDED
@@ -0,0 +1,89 @@
1
+ import { MayuraError } from 'mayura';
2
+ import { browserViewer } from './viewer.js';
3
+ const loopback = new Set(['127.0.0.1', '::1', 'localhost']);
4
+ /**
5
+ * A live viewer of its own on Node: an HTTP server answering `browserViewer`'s pages, on this machine only unless
6
+ * `remote`. Node only; on other runtimes, mount `browserViewer().handle` in your server.
7
+ */
8
+ export async function serveBrowserViewer(options = {}) {
9
+ const host = options.host ?? '127.0.0.1';
10
+ if (typeof host !== 'string' || host === '')
11
+ throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): host is an address to listen on.');
12
+ if (!loopback.has(host) && options.remote !== true)
13
+ throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): listening beyond this machine needs remote: true, with TLS in front.');
14
+ const port = options.port ?? 0;
15
+ if (!Number.isSafeInteger(port) || port < 0 || port > 65_535)
16
+ throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): port is 0 to 65,535.');
17
+ let publicUrl;
18
+ if (options.publicUrl !== undefined) {
19
+ try {
20
+ publicUrl = new URL(options.publicUrl);
21
+ }
22
+ catch {
23
+ publicUrl = undefined;
24
+ }
25
+ if (!publicUrl || (publicUrl.protocol !== 'https:' && !loopback.has(publicUrl.hostname.replace(/^\[|\]$/gu, ''))) || publicUrl.pathname !== '/' || publicUrl.search || publicUrl.hash || publicUrl.username || publicUrl.password) {
26
+ throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): publicUrl is an https origin (http:// only on this machine).');
27
+ }
28
+ }
29
+ const { host: _host, port: _port, remote: _remote, publicUrl: _publicUrl, ...viewerOptions } = options;
30
+ const viewer = browserViewer(viewerOptions);
31
+ const { createServer } = await import('node:http');
32
+ const { Readable } = await import('node:stream');
33
+ const server = createServer((req, res) => {
34
+ void (async () => {
35
+ const chunks = [];
36
+ let size = 0;
37
+ if (req.method === 'POST') {
38
+ for await (const chunk of req) {
39
+ size += chunk.byteLength;
40
+ if (size > 4_096)
41
+ break;
42
+ chunks.push(chunk);
43
+ }
44
+ if (size > 4_096) {
45
+ res.writeHead(413, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
46
+ res.end('Too large.');
47
+ return;
48
+ }
49
+ }
50
+ const headers = new Headers();
51
+ for (const [name, value] of Object.entries(req.headers))
52
+ if (typeof value === 'string')
53
+ headers.set(name, value);
54
+ const request = new Request(new URL(req.url ?? '/', 'http://viewer.invalid'), {
55
+ method: req.method ?? 'GET', headers, ...(req.method === 'POST' ? { body: Buffer.concat(chunks) } : {}),
56
+ });
57
+ const response = await viewer.handle(request) ?? new Response('Not found.', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } });
58
+ res.writeHead(response.status, Object.fromEntries(response.headers));
59
+ if (!response.body) {
60
+ res.end();
61
+ return;
62
+ }
63
+ // A stream's headers go now: its first frame may be a while coming.
64
+ res.flushHeaders();
65
+ const body = Readable.fromWeb(response.body);
66
+ res.on('close', () => body.destroy());
67
+ body.on('error', () => res.destroy());
68
+ body.pipe(res);
69
+ })().catch(() => { if (!res.headersSent)
70
+ res.writeHead(500); res.end(); });
71
+ });
72
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, () => { server.off('error', reject); resolve(); }); });
73
+ const address = server.address();
74
+ const listening = typeof address === 'object' && address ? `http://${address.family === 'IPv6' ? `[${address.address}]` : address.address}:${address.port}` : `http://${host}:${port}`;
75
+ const url = publicUrl ? publicUrl.href : `${listening}/`;
76
+ return {
77
+ url,
78
+ share: async (browser, shareOptions) => {
79
+ const share = await viewer.share(browser, shareOptions);
80
+ return Object.freeze({ path: share.path, interact: share.interact, expiresAt: share.expiresAt, revoke: share.revoke, url: new URL(share.path.slice(1), url).href });
81
+ },
82
+ close: async () => {
83
+ viewer.close();
84
+ server.closeAllConnections();
85
+ await new Promise(resolve => server.close(() => resolve()));
86
+ },
87
+ };
88
+ }
89
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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","sourcesContent":["import { MayuraError } from 'mayura';\nimport type { Browser } from 'mayura/browser';\nimport { browserViewer, type BrowserViewerOptions, type ViewerShare, type ViewerShareOptions } from './viewer.js';\n\nexport interface ServeBrowserViewerOptions extends Omit<BrowserViewerOptions, 'basePath'> {\n /** The address to listen on; `127.0.0.1` by default. Another needs `remote`. */\n readonly host?: string;\n /** The port; any free one by default. */\n readonly port?: number;\n /**\n * Listen beyond this machine. Links carry their token in the clear over plain HTTP: put TLS in front, and give\n * `publicUrl`.\n */\n readonly remote?: boolean;\n /** The origin people reach this server at, such as `https://viewer.example`, for the links; this server's by default. */\n readonly publicUrl?: string;\n}\n\nexport interface ServedBrowserViewer {\n /** Where the viewer is reached. */\n readonly url: string;\n /** A link to watch the browser, and with `interact` to use it: `url` is the whole link, a secret. */\n share(browser: Browser, options?: ViewerShareOptions): Promise<ViewerShare & { readonly url: string }>;\n /** Revokes every share and stops the server. */\n close(): Promise<void>;\n}\n\nconst loopback = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * A live viewer of its own on Node: an HTTP server answering `browserViewer`'s pages, on this machine only unless\n * `remote`. Node only; on other runtimes, mount `browserViewer().handle` in your server.\n */\nexport async function serveBrowserViewer(options: ServeBrowserViewerOptions = {}): Promise<ServedBrowserViewer> {\n const host = options.host ?? '127.0.0.1';\n if (typeof host !== 'string' || host === '') throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): host is an address to listen on.');\n if (!loopback.has(host) && options.remote !== true) throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): listening beyond this machine needs remote: true, with TLS in front.');\n const port = options.port ?? 0;\n if (!Number.isSafeInteger(port) || port < 0 || port > 65_535) throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): port is 0 to 65,535.');\n let publicUrl: URL | undefined;\n if (options.publicUrl !== undefined) {\n try { publicUrl = new URL(options.publicUrl); } catch { publicUrl = undefined; }\n if (!publicUrl || (publicUrl.protocol !== 'https:' && !loopback.has(publicUrl.hostname.replace(/^\\[|\\]$/gu, ''))) || publicUrl.pathname !== '/' || publicUrl.search || publicUrl.hash || publicUrl.username || publicUrl.password) {\n throw new MayuraError('INVALID_CONFIG', 'serveBrowserViewer(): publicUrl is an https origin (http:// only on this machine).');\n }\n }\n const { host: _host, port: _port, remote: _remote, publicUrl: _publicUrl, ...viewerOptions } = options;\n const viewer = browserViewer(viewerOptions);\n const { createServer } = await import('node:http');\n const { Readable } = await import('node:stream');\n\n const server = createServer((req, res) => {\n void (async () => {\n const chunks: Buffer[] = []; let size = 0;\n if (req.method === 'POST') {\n for await (const chunk of req as AsyncIterable<Buffer>) { size += chunk.byteLength; if (size > 4_096) break; chunks.push(chunk); }\n if (size > 4_096) { res.writeHead(413, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' }); res.end('Too large.'); return; }\n }\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) if (typeof value === 'string') headers.set(name, value);\n const request = new Request(new URL(req.url ?? '/', 'http://viewer.invalid'), {\n method: req.method ?? 'GET', headers, ...(req.method === 'POST' ? { body: Buffer.concat(chunks) } : {}),\n });\n const response = await viewer.handle(request) ?? new Response('Not found.', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } });\n res.writeHead(response.status, Object.fromEntries(response.headers));\n if (!response.body) { res.end(); return; }\n // A stream's headers go now: its first frame may be a while coming.\n res.flushHeaders();\n const body = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);\n res.on('close', () => body.destroy());\n body.on('error', () => res.destroy());\n body.pipe(res);\n })().catch(() => { if (!res.headersSent) res.writeHead(500); res.end(); });\n });\n await new Promise<void>((resolve, reject) => { server.once('error', reject); server.listen(port, host, () => { server.off('error', reject); resolve(); }); });\n const address = server.address();\n const listening = typeof address === 'object' && address ? `http://${address.family === 'IPv6' ? `[${address.address}]` : address.address}:${address.port}` : `http://${host}:${port}`;\n const url = publicUrl ? publicUrl.href : `${listening}/`;\n\n return {\n url,\n share: async (browser, shareOptions) => {\n const share = await viewer.share(browser, shareOptions);\n return Object.freeze({ path: share.path, interact: share.interact, expiresAt: share.expiresAt, revoke: share.revoke, url: new URL(share.path.slice(1), url).href });\n },\n close: async () => {\n viewer.close();\n server.closeAllConnections();\n await new Promise<void>(resolve => server.close(() => resolve()));\n },\n };\n}\n"]}
@@ -0,0 +1,50 @@
1
+ import type { Browser } from 'mayura/browser';
2
+ import { type ScreencastOptions } from './screencast.js';
3
+ export interface BrowserViewerOptions extends Omit<ScreencastOptions, 'interact' | 'signal'> {
4
+ /** Where the viewer is mounted in your server, such as `/viewer/`; `/` by default. */
5
+ readonly basePath?: string;
6
+ /**
7
+ * Who may show the viewer page in a frame (its CSP `frame-ancestors`), such as `['https://app.example']`; nobody by
8
+ * default.
9
+ */
10
+ readonly frameAncestors?: readonly string[];
11
+ /** How many people may watch one share at once; 2 by default (1 to 16). */
12
+ readonly maxViewersPerShare?: number;
13
+ /** How many shares may be open at once; 16 by default (1 to 256). */
14
+ readonly maxShares?: number;
15
+ /** Input events a second, per share; 60 by default (1 to 500). */
16
+ readonly maxInputPerSecond?: number;
17
+ }
18
+ export interface ViewerShareOptions {
19
+ /** Let whoever has the link use the browser (mouse, keyboard, typing); off by default. */
20
+ readonly interact?: boolean;
21
+ /** How long the link works, in milliseconds; 1 hour by default (1 s to 24 hours). It also stops when the browser ends. */
22
+ readonly expiresInMs?: number;
23
+ }
24
+ export interface ViewerShare {
25
+ /** The viewer page's path, under `basePath`, with the link's token in it: treat it as a secret. */
26
+ readonly path: string;
27
+ readonly interact: boolean;
28
+ /** When the link stops working, in milliseconds since the epoch. */
29
+ readonly expiresAt: number;
30
+ /** Stops the link now, and ends what is being watched through it. */
31
+ revoke(): void;
32
+ }
33
+ export interface BrowserViewer {
34
+ /** A link to watch the browser, and with `interact` to use it, for whoever has it. */
35
+ share(browser: Browser, options?: ViewerShareOptions): Promise<ViewerShare>;
36
+ /** Answers a request for a viewer path (`<basePath>v/...`); undefined for any other, for your server to answer. */
37
+ handle(request: Request): Promise<Response | undefined>;
38
+ /** How many shares are open. */
39
+ readonly size: number;
40
+ /** Revokes every share, and makes no more. */
41
+ close(): void;
42
+ }
43
+ /**
44
+ * A live viewer for `mayura/browser` browsers, as web-standard request handling for any server: `share(browser)` gives
45
+ * a link with a secret token to a page showing the browser's active tab as it changes (an MJPEG stream of its
46
+ * screencast), and with `interact` a person can use the page too. Links expire, and end with the browser; nothing is
47
+ * shown that is not shared. For a server of its own on Node, see `serveBrowserViewer`.
48
+ */
49
+ export declare function browserViewer(options?: BrowserViewerOptions): BrowserViewer;
50
+ //# sourceMappingURL=viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer.d.ts","sourceRoot":"","sources":["../src/viewer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAsC,KAAK,iBAAiB,EAAoB,MAAM,iBAAiB,CAAC;AAE/G,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC1F,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,kBAAkB;IACjC,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,0HAA0H;IAC1H,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,mGAAmG;IACnG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qEAAqE;IACrE,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5E,mHAAmH;IACnH,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IACxD,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,KAAK,IAAI,IAAI,CAAC;CACf;AAwCD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAuH/E"}