browser-broker 0.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.
- package/.env.example +173 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/RELEASES.md +97 -0
- package/dist/package.json +58 -0
- package/dist/src/adapter/conformance/case.js +1 -0
- package/dist/src/adapter/conformance/cases.js +429 -0
- package/dist/src/adapter/conformance/discovery.js +156 -0
- package/dist/src/adapter/conformance/driver.js +1 -0
- package/dist/src/adapter/conformance/drivers.js +36 -0
- package/dist/src/adapter/conformance/run.js +224 -0
- package/dist/src/adapter/conformance/service-subject.js +165 -0
- package/dist/src/adapter/contract.js +24 -0
- package/dist/src/adapter/operations.js +114 -0
- package/dist/src/adapter/service-seam.js +1 -0
- package/dist/src/artifacts/names.js +229 -0
- package/dist/src/artifacts/store.js +174 -0
- package/dist/src/bin/broker-tool.js +63 -0
- package/dist/src/bin/broker.js +111 -0
- package/dist/src/browser/adoption.js +143 -0
- package/dist/src/browser/automation-probe.js +113 -0
- package/dist/src/browser/conformance/case.js +1 -0
- package/dist/src/browser/conformance/cases.js +192 -0
- package/dist/src/browser/conformance/run.js +102 -0
- package/dist/src/browser/conformance/subjects.js +19 -0
- package/dist/src/browser/discovery.js +226 -0
- package/dist/src/browser/driver.js +195 -0
- package/dist/src/browser/fake.js +585 -0
- package/dist/src/browser/launch.js +504 -0
- package/dist/src/browser/real.js +1425 -0
- package/dist/src/browser/setup.js +161 -0
- package/dist/src/capture/accounting.js +59 -0
- package/dist/src/capture/image.js +112 -0
- package/dist/src/capture/ladder.js +72 -0
- package/dist/src/capture/legibility.js +195 -0
- package/dist/src/capture/pipeline.js +153 -0
- package/dist/src/capture/tiers.js +166 -0
- package/dist/src/cli/adapter.js +233 -0
- package/dist/src/cli/commands.js +270 -0
- package/dist/src/cli/conformance-driver.js +119 -0
- package/dist/src/cli/diffs.js +122 -0
- package/dist/src/cli/image.js +274 -0
- package/dist/src/cli/index.js +895 -0
- package/dist/src/cli/login-command.js +401 -0
- package/dist/src/cli/operations-commands.js +186 -0
- package/dist/src/cli/reconcile-command.js +137 -0
- package/dist/src/cli/sign-in.js +134 -0
- package/dist/src/cli/telemetry.js +222 -0
- package/dist/src/config/environment.js +446 -0
- package/dist/src/diff/artifact-path.js +77 -0
- package/dist/src/diff/crops.js +102 -0
- package/dist/src/diff/geometry.js +122 -0
- package/dist/src/diff/image.js +132 -0
- package/dist/src/diff/mask.js +46 -0
- package/dist/src/diff/regions.js +263 -0
- package/dist/src/diff/settings.js +135 -0
- package/dist/src/doctor/checks.js +588 -0
- package/dist/src/doctor/report.js +152 -0
- package/dist/src/doctor/session.js +161 -0
- package/dist/src/errors.js +36 -0
- package/dist/src/feedback/read.js +119 -0
- package/dist/src/feedback/record.js +199 -0
- package/dist/src/operations/addresses.js +175 -0
- package/dist/src/operations/derive.js +109 -0
- package/dist/src/operations/ledger.js +194 -0
- package/dist/src/operations/status.js +197 -0
- package/dist/src/operations/telemetry.js +280 -0
- package/dist/src/report/document.js +419 -0
- package/dist/src/report/escape.js +68 -0
- package/dist/src/report/snapshot.js +97 -0
- package/dist/src/service/arbitration.js +537 -0
- package/dist/src/service/artifacts.js +85 -0
- package/dist/src/service/bridge.js +577 -0
- package/dist/src/service/broker.js +120 -0
- package/dist/src/service/browser-session.js +269 -0
- package/dist/src/service/capacity.js +62 -0
- package/dist/src/service/capture-seam.js +83 -0
- package/dist/src/service/capture-store.js +91 -0
- package/dist/src/service/comparison-store.js +101 -0
- package/dist/src/service/comparison.js +173 -0
- package/dist/src/service/events.js +93 -0
- package/dist/src/service/keys.js +68 -0
- package/dist/src/service/leases.js +147 -0
- package/dist/src/service/nudge.js +66 -0
- package/dist/src/service/operations/claim.js +692 -0
- package/dist/src/service/operations/give-back.js +131 -0
- package/dist/src/service/operations/pages.js +771 -0
- package/dist/src/service/operations/sign-in.js +915 -0
- package/dist/src/service/operations/status.js +62 -0
- package/dist/src/service/ownership.js +93 -0
- package/dist/src/service/pages.js +616 -0
- package/dist/src/service/pending-seeds.js +20 -0
- package/dist/src/service/queue.js +233 -0
- package/dist/src/service/reconcile.js +220 -0
- package/dist/src/service/refusals.js +262 -0
- package/dist/src/service/runtime.js +131 -0
- package/dist/src/service/signin-recovery.js +148 -0
- package/dist/src/service/storage-seed.js +239 -0
- package/dist/src/service/tabs.js +123 -0
- package/dist/src/store/budget.js +99 -0
- package/dist/src/store/location.js +42 -0
- package/dist/src/store/network-path.js +182 -0
- package/dist/src/store/network-volume.js +92 -0
- package/dist/src/store/open.js +226 -0
- package/dist/src/store/schema/step-001-initial.js +523 -0
- package/dist/src/store/schema/step-002-tab-budget.js +53 -0
- package/dist/src/store/schema/step-003-queue-order.js +110 -0
- package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
- package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
- package/dist/src/store/schema/step-006-signin-events.js +104 -0
- package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
- package/dist/src/store/schema/step-008-signin-owner.js +76 -0
- package/dist/src/store/schema/step-009-named-browsers.js +138 -0
- package/dist/src/store/schema/step-010-signin-request.js +135 -0
- package/dist/src/store/schema/step.js +172 -0
- package/dist/src/store/schema/steps.js +58 -0
- package/dist/src/store/transaction.js +37 -0
- package/dist/src/tool/adapter.js +90 -0
- package/dist/src/tool/conformance-driver.js +184 -0
- package/dist/src/tool/protocol.js +310 -0
- package/dist/src/tool/session.js +351 -0
- package/dist/src/tool/tools.js +310 -0
- package/package.json +58 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { fetchArtifact } from "../service/artifacts.js";
|
|
3
|
+
import { hashKey } from "../service/keys.js";
|
|
4
|
+
export const IMAGE_USAGE = [
|
|
5
|
+
'broker image — write the bytes of one recorded image to a file.',
|
|
6
|
+
'',
|
|
7
|
+
'Usage:',
|
|
8
|
+
' broker image --lease-key <key> --capture <id> --out <file>',
|
|
9
|
+
' broker image --lease-key <key> --overlay <comparison-id> --out <file>',
|
|
10
|
+
' broker image --lease-key <key> --region <comparison-id> --index <n> --side before|after --out <file>',
|
|
11
|
+
'',
|
|
12
|
+
'Exactly one artifact is named, always by the identifier of a row:',
|
|
13
|
+
' --capture <id> a capture, by its own identifier',
|
|
14
|
+
' --overlay <id> a comparison’s full-frame image with the changed regions outlined',
|
|
15
|
+
' --region <id> one changed region from a comparison, cut from either side',
|
|
16
|
+
' --index <n> which region, from the ordered list; defaults to 0, the largest',
|
|
17
|
+
' --side before|after which capture the crop comes from; defaults to after',
|
|
18
|
+
'',
|
|
19
|
+
' --out <file> where to write the bytes. Required.',
|
|
20
|
+
' --json one JSON document, for something reading rather than someone',
|
|
21
|
+
'',
|
|
22
|
+
'Whether the bytes are a whole capture or a crop, they arrive the same way.',
|
|
23
|
+
].join('\n');
|
|
24
|
+
/** The three ways to name an artifact, and the request each builds. */
|
|
25
|
+
const SELECTORS = ['--capture', '--overlay', '--region'];
|
|
26
|
+
function needsValue(flag) {
|
|
27
|
+
return { ok: false, message: `${flag} needs an identifier after it.` };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse the arguments after `image`.
|
|
31
|
+
*
|
|
32
|
+
* **An unrecognised flag refuses rather than being ignored**, matching `diffs`:
|
|
33
|
+
* ignoring one would serve an artifact nobody asked for and print a result that
|
|
34
|
+
* looks like an answer.
|
|
35
|
+
*
|
|
36
|
+
* **Exactly one selector is required.** Two would make the command choose, and
|
|
37
|
+
* a command that silently prefers one flag over another when given both is one
|
|
38
|
+
* whose behaviour has to be learned rather than read.
|
|
39
|
+
*/
|
|
40
|
+
export function parseImageArguments(argv) {
|
|
41
|
+
let leaseKey;
|
|
42
|
+
let out;
|
|
43
|
+
let json = false;
|
|
44
|
+
let index;
|
|
45
|
+
let side;
|
|
46
|
+
const selected = [];
|
|
47
|
+
for (let at = 0; at < argv.length; at += 1) {
|
|
48
|
+
const argument = argv[at] ?? '';
|
|
49
|
+
if (argument === '--json') {
|
|
50
|
+
json = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (argument === '--lease-key' || argument === '--out') {
|
|
54
|
+
const value = argv[at + 1];
|
|
55
|
+
if (value === undefined || value.startsWith('--')) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
message: argument === '--out'
|
|
59
|
+
? '--out needs a file to write to.'
|
|
60
|
+
: '--lease-key needs your lease key after it.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (argument === '--out') {
|
|
64
|
+
out = value;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
leaseKey = value;
|
|
68
|
+
}
|
|
69
|
+
at += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (argument === '--index') {
|
|
73
|
+
const raw = argv[at + 1];
|
|
74
|
+
if (raw === undefined) {
|
|
75
|
+
return { ok: false, message: '--index needs a number after it.' };
|
|
76
|
+
}
|
|
77
|
+
const value = Number(raw);
|
|
78
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
message: `--index takes a whole number of at least zero; got ${JSON.stringify(raw)}.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
index = value;
|
|
85
|
+
at += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (argument === '--side') {
|
|
89
|
+
const raw = argv[at + 1];
|
|
90
|
+
if (raw !== 'before' && raw !== 'after') {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
message: `--side takes "before" or "after"; got ${JSON.stringify(raw ?? '')}. A crop is cut from one capture or the other.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
side = raw;
|
|
97
|
+
at += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const selector = SELECTORS.find((each) => each === argument);
|
|
101
|
+
if (selector !== undefined) {
|
|
102
|
+
const value = argv[at + 1];
|
|
103
|
+
if (value === undefined || value.startsWith('--')) {
|
|
104
|
+
return needsValue(selector);
|
|
105
|
+
}
|
|
106
|
+
selected.push({ flag: selector, id: value });
|
|
107
|
+
at += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
message: `Unrecognised option: ${argument}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (leaseKey === undefined) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
message: '--lease-key is required. An image belongs to the lease that took it, so there is no way to ask for one without saying which lease is asking.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (selected.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
message: `Name one artifact: ${SELECTORS.join(', ')}. Each takes the identifier of a row.`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (selected.length > 1) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
message: `Name exactly one artifact; got ${selected.map((each) => each.flag).join(' and ')}.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (out === undefined) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
message: '--out is required: this command writes the bytes to a file rather than to the terminal, because an image on a terminal is not an image.',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const chosen = selected[0];
|
|
140
|
+
// `--index` and `--side` describe a region and mean nothing otherwise.
|
|
141
|
+
// Refused rather than ignored, for the same reason an unrecognised flag is:
|
|
142
|
+
// a caller who passed them expects them to have done something.
|
|
143
|
+
if (chosen.flag !== '--region' && (index !== undefined || side !== undefined)) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
message: `--index and --side describe which region to cut, so they only apply to --region; got ${chosen.flag}.`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (chosen.flag === '--capture') {
|
|
150
|
+
return { ok: true, leaseKey, request: { kind: 'capture', captureId: chosen.id }, out, json };
|
|
151
|
+
}
|
|
152
|
+
if (chosen.flag === '--overlay') {
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
leaseKey,
|
|
156
|
+
request: { kind: 'overlay', comparisonId: chosen.id },
|
|
157
|
+
out,
|
|
158
|
+
json,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
leaseKey,
|
|
164
|
+
request: {
|
|
165
|
+
kind: 'region',
|
|
166
|
+
comparisonId: chosen.id,
|
|
167
|
+
// The list is ordered largest first (§1.9), so the first region is the
|
|
168
|
+
// one a person almost always wants and is the only defensible default.
|
|
169
|
+
index: index ?? 0,
|
|
170
|
+
side: side ?? 'after',
|
|
171
|
+
},
|
|
172
|
+
out,
|
|
173
|
+
json,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** How a capture row is found. The captures table is the pipeline's. */
|
|
177
|
+
export function captureLookup(db) {
|
|
178
|
+
return {
|
|
179
|
+
find: (captureId) => {
|
|
180
|
+
const row = db
|
|
181
|
+
.prepare('SELECT claim_id AS claimId, path FROM captures WHERE id = ?')
|
|
182
|
+
.get(captureId);
|
|
183
|
+
return row === undefined ? null : { claimId: row.claimId, path: row.path };
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Exit codes, matching the dispatcher's own (`cli/index.ts`). */
|
|
188
|
+
export const IMAGE_EXIT = { served: 0, malformed: 2, refused: 3 };
|
|
189
|
+
/**
|
|
190
|
+
* Run the command.
|
|
191
|
+
*
|
|
192
|
+
* **The lease is resolved by hashing the key**, exactly as every keyed call
|
|
193
|
+
* does. A claim that cannot be found refuses with the *same* sentence as an
|
|
194
|
+
* artifact that is not yours, and that is deliberate rather than lazy: §1.9
|
|
195
|
+
* requires the non-disclosing wording "so probing cannot discover another
|
|
196
|
+
* lease's files", and a distinguishable "no such lease" would let a caller
|
|
197
|
+
* with a wrong key learn which identifiers exist.
|
|
198
|
+
*
|
|
199
|
+
* **This does not extend the lease**, and the difference from the ten
|
|
200
|
+
* operations is the point. §3.1: "every tool except the first takes the lease
|
|
201
|
+
* key, and every call carrying the key extends the lease" — that rule is about
|
|
202
|
+
* the *agent* surface, where a call is evidence the caller is still working.
|
|
203
|
+
* This is a person reading a file that has already been written; a lease is not
|
|
204
|
+
* kept alive by somebody looking at a picture it took.
|
|
205
|
+
*/
|
|
206
|
+
export async function runImage(argv, options) {
|
|
207
|
+
const { db, streams } = options;
|
|
208
|
+
const parsed = parseImageArguments(argv);
|
|
209
|
+
if (!parsed.ok) {
|
|
210
|
+
streams.err(parsed.message);
|
|
211
|
+
streams.err('');
|
|
212
|
+
streams.err(IMAGE_USAGE);
|
|
213
|
+
return IMAGE_EXIT.malformed;
|
|
214
|
+
}
|
|
215
|
+
// Looked up by hash. The key itself is never compared or printed (§5.6).
|
|
216
|
+
const claim = db
|
|
217
|
+
.prepare('SELECT id FROM claims WHERE key_hash = ?')
|
|
218
|
+
.get(hashKey(parsed.leaseKey));
|
|
219
|
+
const outcome = claim === undefined
|
|
220
|
+
? null
|
|
221
|
+
: await fetchArtifact({
|
|
222
|
+
db,
|
|
223
|
+
artifacts: options.artifacts,
|
|
224
|
+
claimId: claim.id,
|
|
225
|
+
captures: captureLookup(db),
|
|
226
|
+
request: parsed.request,
|
|
227
|
+
});
|
|
228
|
+
if (outcome === null || !outcome.served) {
|
|
229
|
+
// One sentence for "not yours" and for "not there", and for a key that
|
|
230
|
+
// names no lease. Three distinguishable answers here would be three ways
|
|
231
|
+
// to enumerate what exists.
|
|
232
|
+
const refusal = outcome === null
|
|
233
|
+
? { reason: 'not_found', message: NOT_FOUND_MESSAGE }
|
|
234
|
+
: outcome.refusal;
|
|
235
|
+
if (parsed.json) {
|
|
236
|
+
streams.out(JSON.stringify({ outcome: 'refused', reason: refusal.reason, message: refusal.message }));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
streams.err(`refused (artifact.${refusal.reason}): ${refusal.message}`);
|
|
240
|
+
}
|
|
241
|
+
return IMAGE_EXIT.refused;
|
|
242
|
+
}
|
|
243
|
+
const write = options.write ?? ((destination, bytes) => fs.writeFile(destination, bytes));
|
|
244
|
+
await write(parsed.out, outcome.artifact.bytes);
|
|
245
|
+
// **The identical report for every kind.** A capture and a crop differ in
|
|
246
|
+
// what they are a picture of and in nothing else a caller can observe here.
|
|
247
|
+
if (parsed.json) {
|
|
248
|
+
streams.out(JSON.stringify({
|
|
249
|
+
outcome: 'served',
|
|
250
|
+
value: {
|
|
251
|
+
// The stored path, which is relative to the artifact root and is
|
|
252
|
+
// therefore safe to report: §1.7a's rule is that no absolute path is
|
|
253
|
+
// reported, because an absolute path names one machine.
|
|
254
|
+
path: outcome.artifact.path,
|
|
255
|
+
bytes: outcome.artifact.bytes.byteLength,
|
|
256
|
+
writtenTo: parsed.out,
|
|
257
|
+
},
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
streams.out(`Wrote ${String(outcome.artifact.bytes.byteLength)} bytes to ${parsed.out} (from ${outcome.artifact.path}).`);
|
|
262
|
+
}
|
|
263
|
+
return IMAGE_EXIT.served;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The sentence used when the key names no lease.
|
|
267
|
+
*
|
|
268
|
+
* Imported rather than respelled would be better, but the artifacts module's
|
|
269
|
+
* constant is about an artifact and this is about a key that matched nothing —
|
|
270
|
+
* two different situations that must produce **one indistinguishable answer**.
|
|
271
|
+
* A test asserts the two strings are byte-identical, so the requirement is
|
|
272
|
+
* checked rather than trusted to whoever edits one of them next.
|
|
273
|
+
*/
|
|
274
|
+
export const NOT_FOUND_MESSAGE = 'No artifact with that identifier belongs to this lease.';
|