swarmbox 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +104 -2
- package/dist/checks.d.ts +48 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +43 -0
- package/dist/checks.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -0
- package/dist/control-room/page.d.ts +2 -0
- package/dist/control-room/page.d.ts.map +1 -0
- package/dist/control-room/page.js +212 -0
- package/dist/control-room/page.js.map +1 -0
- package/dist/control-room/server.d.ts +8 -0
- package/dist/control-room/server.d.ts.map +1 -0
- package/dist/control-room/server.js +180 -0
- package/dist/control-room/server.js.map +1 -0
- package/dist/device/device.d.ts +20 -0
- package/dist/device/device.d.ts.map +1 -0
- package/dist/device/device.js +108 -0
- package/dist/device/device.js.map +1 -0
- package/dist/device/props.d.ts +31 -0
- package/dist/device/props.d.ts.map +1 -0
- package/dist/device/props.js +143 -0
- package/dist/device/props.js.map +1 -0
- package/dist/fleet/browser.d.ts +116 -0
- package/dist/fleet/browser.d.ts.map +1 -0
- package/dist/fleet/browser.js +298 -0
- package/dist/fleet/browser.js.map +1 -0
- package/dist/fleet/sight.d.ts +49 -0
- package/dist/fleet/sight.d.ts.map +1 -0
- package/dist/fleet/sight.js +234 -0
- package/dist/fleet/sight.js.map +1 -0
- package/dist/harness.d.ts +232 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +848 -0
- package/dist/harness.js.map +1 -0
- package/dist/hooks.d.ts +69 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +80 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/device.d.ts +6 -0
- package/dist/mcp/device.d.ts.map +1 -0
- package/dist/mcp/device.js +61 -0
- package/dist/mcp/device.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +640 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/standins.d.ts +11 -0
- package/dist/mcp/standins.d.ts.map +1 -0
- package/dist/mcp/standins.js +142 -0
- package/dist/mcp/standins.js.map +1 -0
- package/dist/product.d.ts +222 -0
- package/dist/product.d.ts.map +1 -0
- package/dist/product.js +10 -0
- package/dist/product.js.map +1 -0
- package/dist/provenance.d.ts +22 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +55 -0
- package/dist/provenance.js.map +1 -0
- package/dist/runs/budget.d.ts +23 -0
- package/dist/runs/budget.d.ts.map +1 -0
- package/dist/runs/budget.js +56 -0
- package/dist/runs/budget.js.map +1 -0
- package/dist/runs/report.d.ts +3 -0
- package/dist/runs/report.d.ts.map +1 -0
- package/dist/runs/report.js +125 -0
- package/dist/runs/report.js.map +1 -0
- package/dist/runs/run.d.ts +86 -0
- package/dist/runs/run.d.ts.map +1 -0
- package/dist/runs/run.js +227 -0
- package/dist/runs/run.js.map +1 -0
- package/dist/scenarios/spec.d.ts +33 -0
- package/dist/scenarios/spec.d.ts.map +1 -0
- package/dist/scenarios/spec.js +89 -0
- package/dist/scenarios/spec.js.map +1 -0
- package/dist/serve.d.ts +31 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +39 -0
- package/dist/serve.js.map +1 -0
- package/dist/standins/builtin/email/index.d.ts +2 -0
- package/dist/standins/builtin/email/index.d.ts.map +1 -0
- package/dist/standins/builtin/email/index.js +213 -0
- package/dist/standins/builtin/email/index.js.map +1 -0
- package/dist/standins/builtin/email/manifest.json +41 -0
- package/dist/standins/builtin/expo-push/index.d.ts +2 -0
- package/dist/standins/builtin/expo-push/index.d.ts.map +1 -0
- package/dist/standins/builtin/expo-push/index.js +203 -0
- package/dist/standins/builtin/expo-push/index.js.map +1 -0
- package/dist/standins/builtin/expo-push/manifest.json +45 -0
- package/dist/standins/builtin/helcim/index.d.ts +7 -0
- package/dist/standins/builtin/helcim/index.d.ts.map +1 -0
- package/dist/standins/builtin/helcim/index.js +438 -0
- package/dist/standins/builtin/helcim/index.js.map +1 -0
- package/dist/standins/builtin/helcim/manifest.json +37 -0
- package/dist/standins/builtin/index.d.ts +8 -0
- package/dist/standins/builtin/index.d.ts.map +1 -0
- package/dist/standins/builtin/index.js +22 -0
- package/dist/standins/builtin/index.js.map +1 -0
- package/dist/standins/builtin/s3/checksums.d.ts +8 -0
- package/dist/standins/builtin/s3/checksums.d.ts.map +1 -0
- package/dist/standins/builtin/s3/checksums.js +67 -0
- package/dist/standins/builtin/s3/checksums.js.map +1 -0
- package/dist/standins/builtin/s3/index.d.ts +6 -0
- package/dist/standins/builtin/s3/index.d.ts.map +1 -0
- package/dist/standins/builtin/s3/index.js +688 -0
- package/dist/standins/builtin/s3/index.js.map +1 -0
- package/dist/standins/builtin/s3/manifest.json +35 -0
- package/dist/standins/builtin/twilio/index.d.ts +10 -0
- package/dist/standins/builtin/twilio/index.d.ts.map +1 -0
- package/dist/standins/builtin/twilio/index.js +477 -0
- package/dist/standins/builtin/twilio/index.js.map +1 -0
- package/dist/standins/builtin/twilio/manifest.json +133 -0
- package/dist/standins/guard.d.ts +16 -0
- package/dist/standins/guard.d.ts.map +1 -0
- package/dist/standins/guard.js +78 -0
- package/dist/standins/guard.js.map +1 -0
- package/dist/standins/manifest.d.ts +94 -0
- package/dist/standins/manifest.d.ts.map +1 -0
- package/dist/standins/manifest.js +121 -0
- package/dist/standins/manifest.js.map +1 -0
- package/dist/standins/protocol.d.ts +114 -0
- package/dist/standins/protocol.d.ts.map +1 -0
- package/dist/standins/protocol.js +11 -0
- package/dist/standins/protocol.js.map +1 -0
- package/dist/standins/runner.d.ts +78 -0
- package/dist/standins/runner.d.ts.map +1 -0
- package/dist/standins/runner.js +746 -0
- package/dist/standins/runner.js.map +1 -0
- package/dist/standins/sdk.d.ts +73 -0
- package/dist/standins/sdk.d.ts.map +1 -0
- package/dist/standins/sdk.js +217 -0
- package/dist/standins/sdk.js.map +1 -0
- package/dist/standins/shape.d.ts +25 -0
- package/dist/standins/shape.d.ts.map +1 -0
- package/dist/standins/shape.js +131 -0
- package/dist/standins/shape.js.map +1 -0
- package/dist/time.d.ts +12 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +64 -0
- package/dist/time.js.map +1 -0
- package/package.json +71 -4
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// The control room: one live tile per open screen, and take-over.
|
|
2
|
+
//
|
|
3
|
+
// Tiles are Chromium's own screencast (CDP Page.startScreencast). The page polls /state and fetches a
|
|
4
|
+
// tile's latest picture (/screens/:id/frame) only when its frame count moved: short requests, because a
|
|
5
|
+
// browser opens at most 6 connections to one address, and one never-ending stream per tile used them all,
|
|
6
|
+
// so /state stopped getting through and the page froze (16 Oct, with 8 tiles; /stream remains for other
|
|
7
|
+
// viewers). Take-over flips the screen's controller in the harness, the same state the MCP tools check,
|
|
8
|
+
// so "a person has this screen" is enforced rather than hoped for; while they have it, their clicks,
|
|
9
|
+
// wheel and keys on the tile go to that screen's browser (/screens/:id/input).
|
|
10
|
+
//
|
|
11
|
+
// Loopback only. Every request must carry this server's own Host (stops DNS rebinding, which would
|
|
12
|
+
// otherwise let a website read the tiles: they show the product's data), and the one write must come
|
|
13
|
+
// from this page's own origin (stops cross-site form posts).
|
|
14
|
+
import http from 'node:http';
|
|
15
|
+
import { page } from "./page.js";
|
|
16
|
+
/** How long a screen may be quiet before its latest frame is re-sent to make the tile show it. */
|
|
17
|
+
const REPEAT_MS = 500;
|
|
18
|
+
export async function serveControlRoom(h, port) {
|
|
19
|
+
const PAGE = page(h.product.title ?? h.product.name);
|
|
20
|
+
let actualPort = port;
|
|
21
|
+
const own = () => [`127.0.0.1:${actualPort}`, `localhost:${actualPort}`];
|
|
22
|
+
const sockets = new Set();
|
|
23
|
+
const server = http.createServer((req, res) => {
|
|
24
|
+
if (!own().includes(req.headers.host ?? '')) {
|
|
25
|
+
res.writeHead(403).end('wrong host');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
29
|
+
if (req.method === 'GET' && url.pathname === '/') {
|
|
30
|
+
res
|
|
31
|
+
.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
|
|
32
|
+
.end(PAGE);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (req.method === 'GET' && url.pathname === '/state') {
|
|
36
|
+
const state = {
|
|
37
|
+
worlds: h.worlds().map((w) => ({
|
|
38
|
+
id: w.id,
|
|
39
|
+
port: w.world.port,
|
|
40
|
+
tenants: w.world.tenants.map((t) => ({ slug: t, origin: w.world.origin(t) })),
|
|
41
|
+
})),
|
|
42
|
+
screens: h.screens().map((s) => ({
|
|
43
|
+
id: s.id,
|
|
44
|
+
world: s.worldId,
|
|
45
|
+
tenant: s.persona.tenant,
|
|
46
|
+
person: s.persona.label,
|
|
47
|
+
controller: s.controller,
|
|
48
|
+
url: s.screen.page.url(),
|
|
49
|
+
frameSeq: s.frameSeq,
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
res
|
|
53
|
+
.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
54
|
+
.end(JSON.stringify(state));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const frame = url.pathname.match(/^\/screens\/(s\d+)\/frame$/);
|
|
58
|
+
if (req.method === 'GET' && frame) {
|
|
59
|
+
const entry = h.screens().find((s) => s.id === frame[1]);
|
|
60
|
+
if (!entry?.frame) {
|
|
61
|
+
res.writeHead(entry ? 204 : 404).end();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
res
|
|
65
|
+
.writeHead(200, {
|
|
66
|
+
'content-type': 'image/jpeg',
|
|
67
|
+
'cache-control': 'no-store',
|
|
68
|
+
'x-frame-seq': String(entry.frameSeq),
|
|
69
|
+
})
|
|
70
|
+
.end(entry.frame);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const stream = url.pathname.match(/^\/screens\/(s\d+)\/stream$/);
|
|
74
|
+
if (req.method === 'GET' && stream) {
|
|
75
|
+
const entry = h.screens().find((s) => s.id === stream[1]);
|
|
76
|
+
if (!entry) {
|
|
77
|
+
res.writeHead(404).end();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
res.writeHead(200, {
|
|
81
|
+
'content-type': 'multipart/x-mixed-replace; boundary=frame',
|
|
82
|
+
'cache-control': 'no-store',
|
|
83
|
+
connection: 'close',
|
|
84
|
+
});
|
|
85
|
+
// Chromium shows a part of a multipart image only once the NEXT part arrives. The screencast emits
|
|
86
|
+
// only when the page changes, so a quiet screen would leave its tile blank, or a step behind, until
|
|
87
|
+
// something else happened on it. So when a screen has been quiet for a moment its latest frame is
|
|
88
|
+
// sent again, which pushes the previous one onto the tile.
|
|
89
|
+
res.write('--frame\r\n');
|
|
90
|
+
let last = 0;
|
|
91
|
+
const send = (jpeg) => {
|
|
92
|
+
last = Date.now();
|
|
93
|
+
res.write(`Content-Type: image/jpeg\r\nContent-Length: ${jpeg.length}\r\n\r\n`);
|
|
94
|
+
res.write(jpeg);
|
|
95
|
+
res.write('\r\n--frame\r\n');
|
|
96
|
+
};
|
|
97
|
+
if (entry.frame)
|
|
98
|
+
send(entry.frame);
|
|
99
|
+
const repeat = setInterval(() => {
|
|
100
|
+
if (entry.frame && Date.now() - last >= REPEAT_MS)
|
|
101
|
+
send(entry.frame);
|
|
102
|
+
}, REPEAT_MS);
|
|
103
|
+
entry.watchers.add(send);
|
|
104
|
+
req.on('close', () => {
|
|
105
|
+
clearInterval(repeat);
|
|
106
|
+
entry.watchers.delete(send);
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const sameOrigin = () => {
|
|
111
|
+
const origin = req.headers.origin;
|
|
112
|
+
if (origin &&
|
|
113
|
+
own()
|
|
114
|
+
.map((x) => `http://${x}`)
|
|
115
|
+
.includes(origin))
|
|
116
|
+
return true;
|
|
117
|
+
res.writeHead(403).end('wrong origin');
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
const input = url.pathname.match(/^\/screens\/(s\d+)\/input$/);
|
|
121
|
+
if (req.method === 'POST' && input) {
|
|
122
|
+
if (!sameOrigin())
|
|
123
|
+
return;
|
|
124
|
+
let body = '';
|
|
125
|
+
req.on('data', (c) => (body += c));
|
|
126
|
+
req.on('end', () => {
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(body);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
res.writeHead(400).end('json please');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
h.personInput(input[1], parsed).then(() => res.writeHead(204).end(), (e) => res.writeHead(409).end(e.message));
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const control = url.pathname.match(/^\/screens\/(s\d+)\/control$/);
|
|
140
|
+
if (req.method === 'POST' && control) {
|
|
141
|
+
if (!sameOrigin())
|
|
142
|
+
return;
|
|
143
|
+
let body = '';
|
|
144
|
+
req.on('data', (c) => (body += c));
|
|
145
|
+
req.on('end', () => {
|
|
146
|
+
const to = (() => {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(body).controller;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
if (to !== 'person' && to !== 'agent') {
|
|
155
|
+
res.writeHead(400).end('controller must be person or agent');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
h.setController(control[1], to).then(() => res.writeHead(204).end(), (e) => res.writeHead(404).end(e.message));
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
res.writeHead(404).end();
|
|
163
|
+
});
|
|
164
|
+
// MJPEG responses never end on their own; track sockets so close() does not wait forever.
|
|
165
|
+
server.on('connection', (s) => {
|
|
166
|
+
sockets.add(s);
|
|
167
|
+
s.on('close', () => sockets.delete(s));
|
|
168
|
+
});
|
|
169
|
+
await new Promise((resolve) => server.listen(port, '127.0.0.1', resolve));
|
|
170
|
+
actualPort = server.address().port;
|
|
171
|
+
return {
|
|
172
|
+
url: `http://127.0.0.1:${actualPort}/`,
|
|
173
|
+
close: () => new Promise((resolve) => {
|
|
174
|
+
for (const s of sockets)
|
|
175
|
+
s.destroy();
|
|
176
|
+
server.close(() => resolve());
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/control-room/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BrowserContext } from 'playwright-core';
|
|
2
|
+
import type { Prop } from './props.ts';
|
|
3
|
+
export interface DeviceState {
|
|
4
|
+
/** Props queued for the next file choosers, first first. */
|
|
5
|
+
files: Prop[];
|
|
6
|
+
/** What the camera shows now, if anything. */
|
|
7
|
+
camera: Prop | null;
|
|
8
|
+
/** Counts camera changes, so a page reads the picture again only when it changed. */
|
|
9
|
+
cameraVersion: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Set a new screen's device up, before its first page: the camera allowed and stood in for, and every file
|
|
13
|
+
* chooser answered from the queue, or noted when nothing was queued.
|
|
14
|
+
*/
|
|
15
|
+
export declare function setUpDevice(context: BrowserContext, state: DeviceState, note: (text: string) => void): Promise<void>;
|
|
16
|
+
/** Put a prop in front of the camera (null: nothing): open pages see it within half a second, new ones at once. */
|
|
17
|
+
export declare function showOnCamera(state: DeviceState, prop: Prop | null): void;
|
|
18
|
+
export declare function setLocation(context: BrowserContext, latitude: number, longitude: number, accuracy?: number): Promise<void>;
|
|
19
|
+
export declare function setNotifications(context: BrowserContext, allow: boolean): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/device/device.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,iBAAiB,CAAC;AACzE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,8CAA8C;IAC9C,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;IACpB,qFAAqF;IACrF,aAAa,EAAE,MAAM,CAAC;CACvB;AAuED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAGxE;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAK,GACZ,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAM7F"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The device a screen runs on (docs/standins.md, The device and props): its camera, the files it can pick, where it
|
|
2
|
+
// is and whether it shows notifications. Network, clock, shape and appearance are set elsewhere (harness.ts,
|
|
3
|
+
// fleet/browser.ts); this is the part a person reaches with their hands.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
/**
|
|
6
|
+
* Inside every page: the camera is a canvas that draws whatever Swarmbox last put in front of it (a grey frame
|
|
7
|
+
* until then), so an app asking for the camera gets a working one with no prompt and no real device.
|
|
8
|
+
*/
|
|
9
|
+
function cameraInPage() {
|
|
10
|
+
const md = navigator.mediaDevices;
|
|
11
|
+
if (!md)
|
|
12
|
+
return;
|
|
13
|
+
// What the camera shows lives in Swarmbox, read through two exposed functions: its version (cheap, polled) and,
|
|
14
|
+
// when that changes, the picture itself. Nothing is added to the page per prop.
|
|
15
|
+
const w = window;
|
|
16
|
+
md.getUserMedia = async (constraints) => {
|
|
17
|
+
if (!constraints?.video)
|
|
18
|
+
throw new DOMException('This device has no microphone in a Swarmbox world', 'NotFoundError');
|
|
19
|
+
const canvas = document.createElement('canvas');
|
|
20
|
+
canvas.width = 1280;
|
|
21
|
+
canvas.height = 960;
|
|
22
|
+
const ctx = canvas.getContext('2d');
|
|
23
|
+
const img = new Image();
|
|
24
|
+
let version = -1;
|
|
25
|
+
let url = null;
|
|
26
|
+
const draw = () => {
|
|
27
|
+
if (url && img.complete && img.naturalWidth)
|
|
28
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
29
|
+
else {
|
|
30
|
+
ctx.fillStyle = '#444';
|
|
31
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const look = async () => {
|
|
35
|
+
const v = (await w.__swarmboxCameraVersion?.()) ?? 0;
|
|
36
|
+
if (v === version)
|
|
37
|
+
return;
|
|
38
|
+
version = v;
|
|
39
|
+
url = (await w.__swarmboxCameraNow?.()) ?? null;
|
|
40
|
+
if (url)
|
|
41
|
+
img.src = url;
|
|
42
|
+
};
|
|
43
|
+
await look();
|
|
44
|
+
await new Promise((r) => (img.complete ? r(null) : (img.onload = img.onerror = () => r(null))));
|
|
45
|
+
draw();
|
|
46
|
+
const drawing = setInterval(draw, 66);
|
|
47
|
+
const looking = setInterval(() => void look(), 500);
|
|
48
|
+
const stream = canvas.captureStream(15);
|
|
49
|
+
// A stopped camera stops its timers too.
|
|
50
|
+
for (const track of stream.getTracks()) {
|
|
51
|
+
const stop = track.stop.bind(track);
|
|
52
|
+
track.stop = () => {
|
|
53
|
+
clearInterval(drawing);
|
|
54
|
+
clearInterval(looking);
|
|
55
|
+
stop();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return stream;
|
|
59
|
+
};
|
|
60
|
+
md.enumerateDevices = async () => [
|
|
61
|
+
{
|
|
62
|
+
deviceId: 'swarmbox-camera',
|
|
63
|
+
groupId: 'swarmbox',
|
|
64
|
+
kind: 'videoinput',
|
|
65
|
+
label: 'Swarmbox camera',
|
|
66
|
+
toJSON: () => ({}),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
const dataUrl = (prop) => `data:${prop.mime};base64,${readFileSync(prop.file).toString('base64')}`;
|
|
71
|
+
/**
|
|
72
|
+
* Set a new screen's device up, before its first page: the camera allowed and stood in for, and every file
|
|
73
|
+
* chooser answered from the queue, or noted when nothing was queued.
|
|
74
|
+
*/
|
|
75
|
+
export async function setUpDevice(context, state, note) {
|
|
76
|
+
await context.grantPermissions(['camera', 'microphone']);
|
|
77
|
+
await context.exposeFunction('__swarmboxCameraVersion', () => state.cameraVersion);
|
|
78
|
+
await context.exposeFunction('__swarmboxCameraNow', () => (state.camera ? dataUrl(state.camera) : null));
|
|
79
|
+
await context.addInitScript(cameraInPage);
|
|
80
|
+
const answer = async (chooser) => {
|
|
81
|
+
const prop = state.files.shift();
|
|
82
|
+
if (!prop) {
|
|
83
|
+
note('a file chooser opened and nothing was picked: file_pick puts a prop in it');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await chooser.setFiles({ name: prop.name, mimeType: prop.mime, buffer: readFileSync(prop.file) });
|
|
87
|
+
note(`picked ${prop.id} (${prop.kind}: ${prop.about}) in a file chooser`);
|
|
88
|
+
};
|
|
89
|
+
const watch = (page) => page.on('filechooser', (c) => void answer(c).catch((e) => note(`could not pick: ${e.message}`)));
|
|
90
|
+
context.on('page', watch);
|
|
91
|
+
for (const page of context.pages())
|
|
92
|
+
watch(page);
|
|
93
|
+
}
|
|
94
|
+
/** Put a prop in front of the camera (null: nothing): open pages see it within half a second, new ones at once. */
|
|
95
|
+
export function showOnCamera(state, prop) {
|
|
96
|
+
state.camera = prop;
|
|
97
|
+
state.cameraVersion++;
|
|
98
|
+
}
|
|
99
|
+
export async function setLocation(context, latitude, longitude, accuracy = 20) {
|
|
100
|
+
await context.grantPermissions(['geolocation']);
|
|
101
|
+
await context.setGeolocation({ latitude, longitude, accuracy });
|
|
102
|
+
}
|
|
103
|
+
export async function setNotifications(context, allow) {
|
|
104
|
+
// Permissions can only be cleared all at once, so the camera's go back with them.
|
|
105
|
+
await context.clearPermissions();
|
|
106
|
+
await context.grantPermissions(allow ? ['camera', 'microphone', 'notifications'] : ['camera', 'microphone']);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.js","sourceRoot":"","sources":["../../src/device/device.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type PropKind = 'photo' | 'document' | 'signature';
|
|
2
|
+
/** Props kept at once; past it the oldest go, with their files. */
|
|
3
|
+
export declare const PROPS_KEPT = 200;
|
|
4
|
+
export interface Prop {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: PropKind;
|
|
7
|
+
/** What it shows, as asked for. */
|
|
8
|
+
about: string;
|
|
9
|
+
file: string;
|
|
10
|
+
name: string;
|
|
11
|
+
mime: string;
|
|
12
|
+
bytes: number;
|
|
13
|
+
sha256: string;
|
|
14
|
+
/** The tag inside the file: `swarmbox-prop:<id>`. */
|
|
15
|
+
tag: string;
|
|
16
|
+
/** The colour a photo is drawn in, so a frame of it on a screen can be told apart. */
|
|
17
|
+
colour?: string;
|
|
18
|
+
}
|
|
19
|
+
/** A JPEG with a comment segment (FF FE) after its start and its JFIF header (APP0): where a photo carries its tag. */
|
|
20
|
+
export declare function withJpegComment(jpeg: Buffer, text: string): Buffer;
|
|
21
|
+
/** A PNG with a tEXt chunk right after its header: where a signature carries its tag. */
|
|
22
|
+
export declare function withPngText(png: Buffer, keyword: string, text: string): Buffer;
|
|
23
|
+
export interface PropMaker {
|
|
24
|
+
make(kind: PropKind, about: string): Promise<Prop>;
|
|
25
|
+
get(id: string): Prop;
|
|
26
|
+
list(): Prop[];
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** Makes props into `dir`, drawing them in a headless Chromium of its own, started the first time it's needed. */
|
|
30
|
+
export declare function propMaker(dir: string): PropMaker;
|
|
31
|
+
//# sourceMappingURL=props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../../src/device/props.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,CAAC;AAE1D,mEAAmE;AACnE,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,uHAAuH;AACvH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAUlE;AAaD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAU9E;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,IAAI,IAAI,EAAE,CAAC;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,kHAAkH;AAClH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAoGhD"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Props (docs/standins.md, The device and props): things a person makes or carries, a photo of the job, a vendor's
|
|
2
|
+
// bill as a PDF, a signature. Swarmbox makes them, so a world never needs a real person's files, and tags each one
|
|
3
|
+
// with its id, so a check can later tell which arrived where: visibly in a corner, and inside the file itself (a
|
|
4
|
+
// JPEG comment, a PNG text chunk, a line of the PDF's text).
|
|
5
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
6
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { chromium } from 'playwright-core';
|
|
9
|
+
/** Props kept at once; past it the oldest go, with their files. */
|
|
10
|
+
export const PROPS_KEPT = 200;
|
|
11
|
+
const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
12
|
+
/** A JPEG with a comment segment (FF FE) after its start and its JFIF header (APP0): where a photo carries its tag. */
|
|
13
|
+
export function withJpegComment(jpeg, text) {
|
|
14
|
+
if (jpeg[0] !== 0xff || jpeg[1] !== 0xd8)
|
|
15
|
+
throw new Error('not a JPEG');
|
|
16
|
+
// After APP0 when there is one, where strict readers expect it to stay first.
|
|
17
|
+
const at = jpeg[2] === 0xff && jpeg[3] === 0xe0 ? 4 + jpeg.readUInt16BE(4) : 2;
|
|
18
|
+
const body = Buffer.from(text, 'latin1');
|
|
19
|
+
const segment = Buffer.alloc(4 + body.length);
|
|
20
|
+
segment.writeUInt16BE(0xfffe, 0);
|
|
21
|
+
segment.writeUInt16BE(body.length + 2, 2);
|
|
22
|
+
body.copy(segment, 4);
|
|
23
|
+
return Buffer.concat([jpeg.subarray(0, at), segment, jpeg.subarray(at)]);
|
|
24
|
+
}
|
|
25
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
26
|
+
let c = n;
|
|
27
|
+
for (let k = 0; k < 8; k++)
|
|
28
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
29
|
+
return c >>> 0;
|
|
30
|
+
});
|
|
31
|
+
const crc32 = (b) => {
|
|
32
|
+
let c = 0xffffffff;
|
|
33
|
+
for (const byte of b)
|
|
34
|
+
c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
|
35
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
36
|
+
};
|
|
37
|
+
/** A PNG with a tEXt chunk right after its header: where a signature carries its tag. */
|
|
38
|
+
export function withPngText(png, keyword, text) {
|
|
39
|
+
const IHDR_END = 8 + 25; // signature, then the IHDR chunk (4 length + 4 type + 13 data + 4 crc)
|
|
40
|
+
if (png.toString('latin1', 12, 16) !== 'IHDR')
|
|
41
|
+
throw new Error('not a PNG');
|
|
42
|
+
const data = Buffer.concat([Buffer.from(keyword, 'latin1'), Buffer.from([0]), Buffer.from(text, 'latin1')]);
|
|
43
|
+
const typed = Buffer.concat([Buffer.from('tEXt', 'latin1'), data]);
|
|
44
|
+
const chunk = Buffer.alloc(8 + data.length + 4);
|
|
45
|
+
chunk.writeUInt32BE(data.length, 0);
|
|
46
|
+
typed.copy(chunk, 4);
|
|
47
|
+
chunk.writeUInt32BE(crc32(typed), 8 + data.length);
|
|
48
|
+
return Buffer.concat([png.subarray(0, IHDR_END), chunk, png.subarray(IHDR_END)]);
|
|
49
|
+
}
|
|
50
|
+
/** Makes props into `dir`, drawing them in a headless Chromium of its own, started the first time it's needed. */
|
|
51
|
+
export function propMaker(dir) {
|
|
52
|
+
const props = new Map();
|
|
53
|
+
let browser;
|
|
54
|
+
// Drawn offline: the HTML is static and escaped, and there's still no reason for this browser to reach anything.
|
|
55
|
+
const page = async () => (await (await (browser ??= chromium.launch({ channel: 'chromium', headless: true }))).newContext({
|
|
56
|
+
offline: true,
|
|
57
|
+
})).newPage();
|
|
58
|
+
return {
|
|
59
|
+
async make(kind, about) {
|
|
60
|
+
const text = about.trim().slice(0, 300) || kind;
|
|
61
|
+
const id = `prop_${randomBytes(4).toString('hex')}`;
|
|
62
|
+
const tag = `swarmbox-prop:${id}`;
|
|
63
|
+
const hue = parseInt(id.slice(5, 9), 16) % 360;
|
|
64
|
+
const colour = `hsl(${hue}, 55%, 42%)`;
|
|
65
|
+
const p = await page();
|
|
66
|
+
let file;
|
|
67
|
+
let ext;
|
|
68
|
+
let mime;
|
|
69
|
+
try {
|
|
70
|
+
if (kind === 'photo') {
|
|
71
|
+
await p.setViewportSize({ width: 1280, height: 960 });
|
|
72
|
+
await p.setContent(`<body style="margin:0;width:1280px;height:960px;background:${colour};font:600 56px system-ui;color:#fff;display:flex;align-items:center;justify-content:center;text-align:center">` +
|
|
73
|
+
`<div style="max-width:1000px">${escape(text)}</div>` +
|
|
74
|
+
`<div style="position:absolute;right:24px;bottom:18px;font:24px ui-monospace,monospace;opacity:.8">${tag}</div></body>`);
|
|
75
|
+
file = withJpegComment(await p.screenshot({ type: 'jpeg', quality: 85 }), tag);
|
|
76
|
+
[ext, mime] = ['jpg', 'image/jpeg'];
|
|
77
|
+
}
|
|
78
|
+
else if (kind === 'signature') {
|
|
79
|
+
await p.setViewportSize({ width: 600, height: 200 });
|
|
80
|
+
await p.setContent(`<body style="margin:0;background:#fff"><svg width="600" height="200"><path d="M40 140 C 90 40, 130 40, 160 120 S 230 180, 270 90 S 360 40, 400 130 S 500 150, 560 70" ` +
|
|
81
|
+
`fill="none" stroke="#123" stroke-width="5" stroke-linecap="round"/><text x="40" y="190" font-size="14" fill="#999">${escape(text)}</text></svg></body>`);
|
|
82
|
+
file = withPngText(await p.screenshot({ type: 'png' }), 'Comment', tag);
|
|
83
|
+
[ext, mime] = ['png', 'image/png'];
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await p.setContent(`<body style="font:16px system-ui;padding:48px"><h1 style="font-size:28px">${escape(text)}</h1>` +
|
|
87
|
+
`<p>Made by Swarmbox for a test world. Not a real document.</p>` +
|
|
88
|
+
`<p style="color:#fff;font-size:6px">${tag}</p></body>`);
|
|
89
|
+
file = await p.pdf({ format: 'Letter', printBackground: true });
|
|
90
|
+
[ext, mime] = ['pdf', 'application/pdf'];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await p.context().close();
|
|
95
|
+
}
|
|
96
|
+
mkdirSync(dir, { recursive: true });
|
|
97
|
+
const name = `${text
|
|
98
|
+
.toLowerCase()
|
|
99
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
100
|
+
.replace(/^-|-$/g, '')
|
|
101
|
+
.slice(0, 40) || kind}.${ext}`;
|
|
102
|
+
const path = join(dir, `${id}-${name}`);
|
|
103
|
+
writeFileSync(path, file);
|
|
104
|
+
const prop = {
|
|
105
|
+
id,
|
|
106
|
+
kind,
|
|
107
|
+
about: text,
|
|
108
|
+
file: path,
|
|
109
|
+
name,
|
|
110
|
+
mime,
|
|
111
|
+
bytes: file.length,
|
|
112
|
+
sha256: createHash('sha256').update(file).digest('hex'),
|
|
113
|
+
tag,
|
|
114
|
+
...(kind === 'photo' ? { colour } : {}),
|
|
115
|
+
};
|
|
116
|
+
props.set(id, prop);
|
|
117
|
+
// At most PROPS_KEPT: the oldest go, with their files.
|
|
118
|
+
while (props.size > PROPS_KEPT) {
|
|
119
|
+
const [oldest, old] = props.entries().next().value;
|
|
120
|
+
props.delete(oldest);
|
|
121
|
+
rmSync(old.file, { force: true });
|
|
122
|
+
}
|
|
123
|
+
return prop;
|
|
124
|
+
},
|
|
125
|
+
get(id) {
|
|
126
|
+
const prop = props.get(id);
|
|
127
|
+
if (!prop)
|
|
128
|
+
throw new Error(`no prop ${id}; make one with prop_make`);
|
|
129
|
+
return prop;
|
|
130
|
+
},
|
|
131
|
+
list: () => [...props.values()],
|
|
132
|
+
async close() {
|
|
133
|
+
const b = await browser?.catch(() => undefined);
|
|
134
|
+
browser = undefined;
|
|
135
|
+
await b?.close();
|
|
136
|
+
// A world's props are for its run: none outlive the harness.
|
|
137
|
+
for (const p of props.values())
|
|
138
|
+
rmSync(p.file, { force: true });
|
|
139
|
+
props.clear();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=props.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"props.js","sourceRoot":"","sources":["../../src/device/props.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type BrowserContext, type Page } from 'playwright-core';
|
|
2
|
+
import type { Persona, SignIn, StartCookie } from '../product.ts';
|
|
3
|
+
/** A sign-in's cookies, on the tenant's own origin: how a screen is signed in, and kept signed in. */
|
|
4
|
+
export declare function setCookies(context: BrowserContext, origin: string, cookies: StartCookie[]): Promise<void>;
|
|
5
|
+
/** Makes `*.{baseDomain}` resolve to loopback inside this browser, and nowhere else on the machine. */
|
|
6
|
+
export declare function resolverRule(baseDomain: string): string;
|
|
7
|
+
export interface FleetOptions {
|
|
8
|
+
baseDomain: string;
|
|
9
|
+
/** The port the product serves its tenants on, used to build each tenant's origin. */
|
|
10
|
+
port: number;
|
|
11
|
+
/** Headed is the default: the point of the sandbox is that the screens can be watched. */
|
|
12
|
+
headless?: boolean;
|
|
13
|
+
/** Slow actions down so a person can follow along. Milliseconds between operations. */
|
|
14
|
+
slowMo?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Provider pages a stand-in plays in the browser (a card form's script and iframe): every request to `origin`
|
|
17
|
+
* is answered by the stand-in at `to`, so the page keeps the provider's origin and its origin checks pass.
|
|
18
|
+
*/
|
|
19
|
+
routes?: {
|
|
20
|
+
origin: string;
|
|
21
|
+
to: string;
|
|
22
|
+
}[];
|
|
23
|
+
/**
|
|
24
|
+
* With routes, where else the browsers may go: the world's stand-ins and what the product loads from outside.
|
|
25
|
+
* A routed provider page is the stand-in's own code in the browser, so the world's browsers then reach only the
|
|
26
|
+
* world's hosts, the routed origins and these; every other request and WebSocket is blocked.
|
|
27
|
+
*/
|
|
28
|
+
allowOrigins?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* The world's stand-ins' own addresses (http://127.0.0.1:<port>): reachable from its browsers (a presigned
|
|
31
|
+
* upload goes straight to S3's), but when its browsers play a provider's pages, only through the same checked
|
|
32
|
+
* proxy as the routed origins, so a stand-in can't bounce the browser out with a redirect of its own.
|
|
33
|
+
*/
|
|
34
|
+
standins?: string[];
|
|
35
|
+
}
|
|
36
|
+
/** Whether a URL is inside a world whose browsers play a provider's pages (see FleetOptions.allowOrigins). */
|
|
37
|
+
export declare function insideWorld(url: string, options: FleetOptions): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The devices a screen can be. A sign-in's own viewport (a product's tablet app) is the default for that
|
|
40
|
+
* app; a device named when the screen opens wins over it, so the same app can be tried on a phone or turned
|
|
41
|
+
* to portrait. Sizes are CSS pixels of common devices: a 6.1" phone, an 11" tablet each way, a laptop.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEVICES: {
|
|
44
|
+
readonly desktop: {
|
|
45
|
+
readonly viewport: {
|
|
46
|
+
readonly width: 1440;
|
|
47
|
+
readonly height: 900;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
readonly phone: {
|
|
51
|
+
readonly viewport: {
|
|
52
|
+
readonly width: 390;
|
|
53
|
+
readonly height: 844;
|
|
54
|
+
};
|
|
55
|
+
readonly isMobile: true;
|
|
56
|
+
readonly hasTouch: true;
|
|
57
|
+
readonly deviceScaleFactor: 3;
|
|
58
|
+
};
|
|
59
|
+
readonly tablet: {
|
|
60
|
+
readonly viewport: {
|
|
61
|
+
readonly width: 1180;
|
|
62
|
+
readonly height: 820;
|
|
63
|
+
};
|
|
64
|
+
readonly hasTouch: true;
|
|
65
|
+
readonly deviceScaleFactor: 2;
|
|
66
|
+
};
|
|
67
|
+
readonly 'tablet-portrait': {
|
|
68
|
+
readonly viewport: {
|
|
69
|
+
readonly width: 820;
|
|
70
|
+
readonly height: 1180;
|
|
71
|
+
};
|
|
72
|
+
readonly hasTouch: true;
|
|
73
|
+
readonly deviceScaleFactor: 2;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export type Device = keyof typeof DEVICES;
|
|
77
|
+
/** How a screen looks: its device and the light or dark appearance its system asks for. */
|
|
78
|
+
export interface Appearance {
|
|
79
|
+
device?: Device;
|
|
80
|
+
colorScheme?: 'light' | 'dark';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Who a screen is for: a signed-in person, or someone with only a link, a customer, whose browser has no
|
|
84
|
+
* sign-in at all (signIn null).
|
|
85
|
+
*/
|
|
86
|
+
export interface Who {
|
|
87
|
+
persona: Pick<Persona, 'tenant' | 'timeZone'>;
|
|
88
|
+
signIn: SignIn | null;
|
|
89
|
+
}
|
|
90
|
+
export interface Screen {
|
|
91
|
+
/** Null for a screen with no sign-in: a customer's, opened at their link. */
|
|
92
|
+
signIn: SignIn | null;
|
|
93
|
+
/** The client address the api sees for this screen (see open()). */
|
|
94
|
+
address: string;
|
|
95
|
+
context: BrowserContext;
|
|
96
|
+
page: Page;
|
|
97
|
+
close(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
export interface Fleet {
|
|
100
|
+
/** Open a screen as one person, already signed in, or as a customer with nothing but a link. */
|
|
101
|
+
open(who: Who, path?: string, options?: Appearance & {
|
|
102
|
+
address?: string;
|
|
103
|
+
/** Called with the new context before it loads anything, so nothing it does is missed. */
|
|
104
|
+
onContext?: (context: BrowserContext) => void | Promise<void>;
|
|
105
|
+
/** The world's time, when its clock has moved: the page starts on it, not on this machine's. */
|
|
106
|
+
clock?: {
|
|
107
|
+
at: string;
|
|
108
|
+
paused: boolean;
|
|
109
|
+
};
|
|
110
|
+
}): Promise<Screen>;
|
|
111
|
+
originFor(tenant: string): string;
|
|
112
|
+
readonly screens: readonly Screen[];
|
|
113
|
+
close(): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
export declare function launchFleet(options: FleetOptions): Promise<Fleet>;
|
|
116
|
+
//# sourceMappingURL=browser.d.ts.map
|