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.
Files changed (123) hide show
  1. package/.env.example +173 -0
  2. package/LICENSE +21 -0
  3. package/README.md +374 -0
  4. package/RELEASES.md +97 -0
  5. package/dist/package.json +58 -0
  6. package/dist/src/adapter/conformance/case.js +1 -0
  7. package/dist/src/adapter/conformance/cases.js +429 -0
  8. package/dist/src/adapter/conformance/discovery.js +156 -0
  9. package/dist/src/adapter/conformance/driver.js +1 -0
  10. package/dist/src/adapter/conformance/drivers.js +36 -0
  11. package/dist/src/adapter/conformance/run.js +224 -0
  12. package/dist/src/adapter/conformance/service-subject.js +165 -0
  13. package/dist/src/adapter/contract.js +24 -0
  14. package/dist/src/adapter/operations.js +114 -0
  15. package/dist/src/adapter/service-seam.js +1 -0
  16. package/dist/src/artifacts/names.js +229 -0
  17. package/dist/src/artifacts/store.js +174 -0
  18. package/dist/src/bin/broker-tool.js +63 -0
  19. package/dist/src/bin/broker.js +111 -0
  20. package/dist/src/browser/adoption.js +143 -0
  21. package/dist/src/browser/automation-probe.js +113 -0
  22. package/dist/src/browser/conformance/case.js +1 -0
  23. package/dist/src/browser/conformance/cases.js +192 -0
  24. package/dist/src/browser/conformance/run.js +102 -0
  25. package/dist/src/browser/conformance/subjects.js +19 -0
  26. package/dist/src/browser/discovery.js +226 -0
  27. package/dist/src/browser/driver.js +195 -0
  28. package/dist/src/browser/fake.js +585 -0
  29. package/dist/src/browser/launch.js +504 -0
  30. package/dist/src/browser/real.js +1425 -0
  31. package/dist/src/browser/setup.js +161 -0
  32. package/dist/src/capture/accounting.js +59 -0
  33. package/dist/src/capture/image.js +112 -0
  34. package/dist/src/capture/ladder.js +72 -0
  35. package/dist/src/capture/legibility.js +195 -0
  36. package/dist/src/capture/pipeline.js +153 -0
  37. package/dist/src/capture/tiers.js +166 -0
  38. package/dist/src/cli/adapter.js +233 -0
  39. package/dist/src/cli/commands.js +270 -0
  40. package/dist/src/cli/conformance-driver.js +119 -0
  41. package/dist/src/cli/diffs.js +122 -0
  42. package/dist/src/cli/image.js +274 -0
  43. package/dist/src/cli/index.js +895 -0
  44. package/dist/src/cli/login-command.js +401 -0
  45. package/dist/src/cli/operations-commands.js +186 -0
  46. package/dist/src/cli/reconcile-command.js +137 -0
  47. package/dist/src/cli/sign-in.js +134 -0
  48. package/dist/src/cli/telemetry.js +222 -0
  49. package/dist/src/config/environment.js +446 -0
  50. package/dist/src/diff/artifact-path.js +77 -0
  51. package/dist/src/diff/crops.js +102 -0
  52. package/dist/src/diff/geometry.js +122 -0
  53. package/dist/src/diff/image.js +132 -0
  54. package/dist/src/diff/mask.js +46 -0
  55. package/dist/src/diff/regions.js +263 -0
  56. package/dist/src/diff/settings.js +135 -0
  57. package/dist/src/doctor/checks.js +588 -0
  58. package/dist/src/doctor/report.js +152 -0
  59. package/dist/src/doctor/session.js +161 -0
  60. package/dist/src/errors.js +36 -0
  61. package/dist/src/feedback/read.js +119 -0
  62. package/dist/src/feedback/record.js +199 -0
  63. package/dist/src/operations/addresses.js +175 -0
  64. package/dist/src/operations/derive.js +109 -0
  65. package/dist/src/operations/ledger.js +194 -0
  66. package/dist/src/operations/status.js +197 -0
  67. package/dist/src/operations/telemetry.js +280 -0
  68. package/dist/src/report/document.js +419 -0
  69. package/dist/src/report/escape.js +68 -0
  70. package/dist/src/report/snapshot.js +97 -0
  71. package/dist/src/service/arbitration.js +537 -0
  72. package/dist/src/service/artifacts.js +85 -0
  73. package/dist/src/service/bridge.js +577 -0
  74. package/dist/src/service/broker.js +120 -0
  75. package/dist/src/service/browser-session.js +269 -0
  76. package/dist/src/service/capacity.js +62 -0
  77. package/dist/src/service/capture-seam.js +83 -0
  78. package/dist/src/service/capture-store.js +91 -0
  79. package/dist/src/service/comparison-store.js +101 -0
  80. package/dist/src/service/comparison.js +173 -0
  81. package/dist/src/service/events.js +93 -0
  82. package/dist/src/service/keys.js +68 -0
  83. package/dist/src/service/leases.js +147 -0
  84. package/dist/src/service/nudge.js +66 -0
  85. package/dist/src/service/operations/claim.js +692 -0
  86. package/dist/src/service/operations/give-back.js +131 -0
  87. package/dist/src/service/operations/pages.js +771 -0
  88. package/dist/src/service/operations/sign-in.js +915 -0
  89. package/dist/src/service/operations/status.js +62 -0
  90. package/dist/src/service/ownership.js +93 -0
  91. package/dist/src/service/pages.js +616 -0
  92. package/dist/src/service/pending-seeds.js +20 -0
  93. package/dist/src/service/queue.js +233 -0
  94. package/dist/src/service/reconcile.js +220 -0
  95. package/dist/src/service/refusals.js +262 -0
  96. package/dist/src/service/runtime.js +131 -0
  97. package/dist/src/service/signin-recovery.js +148 -0
  98. package/dist/src/service/storage-seed.js +239 -0
  99. package/dist/src/service/tabs.js +123 -0
  100. package/dist/src/store/budget.js +99 -0
  101. package/dist/src/store/location.js +42 -0
  102. package/dist/src/store/network-path.js +182 -0
  103. package/dist/src/store/network-volume.js +92 -0
  104. package/dist/src/store/open.js +226 -0
  105. package/dist/src/store/schema/step-001-initial.js +523 -0
  106. package/dist/src/store/schema/step-002-tab-budget.js +53 -0
  107. package/dist/src/store/schema/step-003-queue-order.js +110 -0
  108. package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
  109. package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
  110. package/dist/src/store/schema/step-006-signin-events.js +104 -0
  111. package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
  112. package/dist/src/store/schema/step-008-signin-owner.js +76 -0
  113. package/dist/src/store/schema/step-009-named-browsers.js +138 -0
  114. package/dist/src/store/schema/step-010-signin-request.js +135 -0
  115. package/dist/src/store/schema/step.js +172 -0
  116. package/dist/src/store/schema/steps.js +58 -0
  117. package/dist/src/store/transaction.js +37 -0
  118. package/dist/src/tool/adapter.js +90 -0
  119. package/dist/src/tool/conformance-driver.js +184 -0
  120. package/dist/src/tool/protocol.js +310 -0
  121. package/dist/src/tool/session.js +351 -0
  122. package/dist/src/tool/tools.js +310 -0
  123. 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.';