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,226 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Reading the browser's own record of where it can be reached, and checking
|
|
5
|
+
* it before anybody attaches on the strength of it.
|
|
6
|
+
*
|
|
7
|
+
* ── Why the browser picks the port and not this service ─────────────────
|
|
8
|
+
*
|
|
9
|
+
* `SCHEMA.md` §1.2c: the browser is asked to listen on an **unspecified**
|
|
10
|
+
* port, so the operating system assigns a free one, and the browser writes
|
|
11
|
+
* the result into a file inside its own profile directory. Two properties
|
|
12
|
+
* follow, and both are the reason for the arrangement:
|
|
13
|
+
*
|
|
14
|
+
* - **No collision with anything else on the machine.** A fixed port is a
|
|
15
|
+
* guess about what else is running, and the inward-isolation rule says this
|
|
16
|
+
* service must start correctly on a host where unrelated things are already
|
|
17
|
+
* listening.
|
|
18
|
+
* - **The record cannot drift from the identity it describes**, because it
|
|
19
|
+
* lives inside the profile directory that *is* the identity (§1.2). A
|
|
20
|
+
* record kept anywhere else is a second place the truth lives, and the two
|
|
21
|
+
* go out of step the first time something exits badly.
|
|
22
|
+
*
|
|
23
|
+
* ── The property this module exists for: a claim, not a proof ───────────
|
|
24
|
+
*
|
|
25
|
+
* The record **survives the browser**. This was verified rather than assumed,
|
|
26
|
+
* and re-verified while building this row: after the process was killed
|
|
27
|
+
* outright the file was still present, still readable, and still naming a
|
|
28
|
+
* port that answered nothing.
|
|
29
|
+
*
|
|
30
|
+
* So {@link readDiscoveryRecord} returning a record means *a file said so*
|
|
31
|
+
* and nothing more. {@link verifyDiscoveryRecord} is what turns it into a
|
|
32
|
+
* fact, and it owes **two** checks rather than one:
|
|
33
|
+
*
|
|
34
|
+
* 1. **Liveness.** The endpoint answers. A file is not a process.
|
|
35
|
+
* 2. **Identity.** The browser's own identifier matches. **Ports are
|
|
36
|
+
* reused** — a stale record plus an unrelated process that happened to be
|
|
37
|
+
* given the same port reads as a successful match against the number
|
|
38
|
+
* alone, and the service would attach to something it has no business
|
|
39
|
+
* touching.
|
|
40
|
+
*
|
|
41
|
+
* A record that fails either check is stale: the browser is treated as not
|
|
42
|
+
* running, and whichever caller notices takes the launch race (§1.2a).
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* The file the browser writes inside its profile directory.
|
|
46
|
+
*
|
|
47
|
+
* Named by the browser rather than chosen here, which is why it is a constant
|
|
48
|
+
* and not configuration: this service reads a file somebody else's code
|
|
49
|
+
* writes, so the name is a fact to be matched, not a preference to be set.
|
|
50
|
+
*/
|
|
51
|
+
export const PORT_FILE_NAME = 'DevToolsActivePort';
|
|
52
|
+
/** Where a profile directory lives, given the configured root and a browser. */
|
|
53
|
+
export function profileDirectory(profileRoot, browser) {
|
|
54
|
+
// The directory is the root plus the browser's own id, computed rather than
|
|
55
|
+
// stored: `SCHEMA.md` §1.2 keeps no `profile_dir` column, because storing
|
|
56
|
+
// one stores an absolute path the database already knows how to compute,
|
|
57
|
+
// and §1.7a's rule is that no absolute path is ever stored anywhere.
|
|
58
|
+
return path.join(profileRoot, browser);
|
|
59
|
+
}
|
|
60
|
+
/** The record file's path for a profile directory. */
|
|
61
|
+
export function portFilePath(profileDir) {
|
|
62
|
+
return path.join(profileDir, PORT_FILE_NAME);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse the record file's contents.
|
|
66
|
+
*
|
|
67
|
+
* Returns `undefined` for anything that is not a well-formed record rather
|
|
68
|
+
* than throwing, because an unreadable or half-written record is *the browser
|
|
69
|
+
* is not reachable this way* — the same conclusion as an absent one — and a
|
|
70
|
+
* caller that has to tell a malformed file from a missing one in order to
|
|
71
|
+
* decide whether to launch has been handed a distinction that changes
|
|
72
|
+
* nothing. A truncated file is a real state: the browser writes it in two
|
|
73
|
+
* lines and a reader can arrive between them.
|
|
74
|
+
*/
|
|
75
|
+
export function parsePortFile(contents) {
|
|
76
|
+
const [portLine, wsPath] = contents.split('\n');
|
|
77
|
+
if (portLine === undefined || wsPath === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const port = Number(portLine.trim());
|
|
81
|
+
// An empty string converts to zero and a partly-written line converts to a
|
|
82
|
+
// non-number, so both cases have to be excluded explicitly rather than by
|
|
83
|
+
// truthiness — zero is exactly what an unfinished write looks like.
|
|
84
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
// The identifier is the last segment of the per-browser debugging path.
|
|
88
|
+
const browserUuid = wsPath.trim().split('/').at(-1);
|
|
89
|
+
if (browserUuid === undefined || browserUuid === '') {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return { port, browserUuid };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Read the record a browser left in its profile directory.
|
|
96
|
+
*
|
|
97
|
+
* **The returned record deliberately carries no `browserUuid`.** The shape on
|
|
98
|
+
* the seam makes that field optional and says why: it is absent on a record
|
|
99
|
+
* that has been read off disk but not yet checked against a live browser,
|
|
100
|
+
* which is the ordinary state of one. The identifier read from the *file* is
|
|
101
|
+
* the browser's claim about itself; the identifier that matters is the one a
|
|
102
|
+
* live endpoint reports. Keeping the file's copy out of the record is what
|
|
103
|
+
* stops a later reader treating a value that came off disk as though it had
|
|
104
|
+
* been confirmed.
|
|
105
|
+
*
|
|
106
|
+
* The file's identifier is not discarded — {@link verifyDiscoveryRecord}
|
|
107
|
+
* takes it as the expectation to match against. It simply does not travel
|
|
108
|
+
* inside a type whose presence means *verified*.
|
|
109
|
+
*/
|
|
110
|
+
export function readDiscoveryRecord(profileDir) {
|
|
111
|
+
let contents;
|
|
112
|
+
try {
|
|
113
|
+
contents = fs.readFileSync(portFilePath(profileDir), 'utf8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Absent, unreadable, or a directory. Every one of them means the same
|
|
117
|
+
// thing to a caller: there is no record to attach on.
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const parsed = parsePortFile(contents);
|
|
121
|
+
if (parsed === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
record: { endpoint: `http://127.0.0.1:${String(parsed.port)}` },
|
|
126
|
+
expectedUuid: parsed.browserUuid,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* How long to wait for the endpoint to answer before calling it unreachable.
|
|
131
|
+
*
|
|
132
|
+
* This bounds **one request to a loopback address**, which either answers
|
|
133
|
+
* immediately or is not there — it is not the launch-readiness bound, which
|
|
134
|
+
* is row #55's open question (§1.2b) and is a different quantity entirely. A
|
|
135
|
+
* bound is needed at all because a port can be held by something that accepts
|
|
136
|
+
* a connection and never replies, and a verification step that inherits that
|
|
137
|
+
* hang stalls every caller instead of concluding the browser is not usable.
|
|
138
|
+
*/
|
|
139
|
+
export const ENDPOINT_TIMEOUT_MS = 2000;
|
|
140
|
+
/**
|
|
141
|
+
* Check that the endpoint answers **and** that the browser behind it is the
|
|
142
|
+
* one the record described.
|
|
143
|
+
*
|
|
144
|
+
* Both checks, never one. Liveness alone attaches to whatever inherited the
|
|
145
|
+
* port; identity alone cannot be read without something answering.
|
|
146
|
+
*/
|
|
147
|
+
export async function verifyDiscoveryRecord(record, expectedUuid, options = {}) {
|
|
148
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
149
|
+
const timeoutMs = options.timeoutMs ?? ENDPOINT_TIMEOUT_MS;
|
|
150
|
+
// ── Why the bound is a ref'd timer and NOT `AbortSignal.timeout` ───────
|
|
151
|
+
//
|
|
152
|
+
// `AbortSignal.timeout` is the obvious way to bound a request, it reads as
|
|
153
|
+
// correct, and it **is** correct whenever something else is keeping the
|
|
154
|
+
// process alive — which is every test run. Its timer is **unref'd**, so it
|
|
155
|
+
// cannot on its own keep the event loop running.
|
|
156
|
+
//
|
|
157
|
+
// That is fatal here specifically. This service is a **short-lived spawn**
|
|
158
|
+
// (§1.0a): started by a caller, serves that session, exits with it. On the
|
|
159
|
+
// adopt path the whole design rests on, a caller that is only verifying a
|
|
160
|
+
// record has **no other outstanding ref'd work** — and against a port that
|
|
161
|
+
// accepts a connection and never replies, the only thing that could settle
|
|
162
|
+
// the request is a timer incapable of holding the loop open. The process
|
|
163
|
+
// therefore **exits without concluding**, silently and with a success code,
|
|
164
|
+
// rather than reporting `endpoint_unreachable`.
|
|
165
|
+
//
|
|
166
|
+
// Measured on the runtime the pipeline uses: with nothing else outstanding
|
|
167
|
+
// the abort never fires and the process exits; adding one unrelated ref'd
|
|
168
|
+
// timer to the byte-identical call makes it conclude correctly. That
|
|
169
|
+
// contrast is the mechanism — the bound was being enforced by luck.
|
|
170
|
+
//
|
|
171
|
+
// So the timer is **ref'd** (an ordinary `setTimeout`, which keeps the loop
|
|
172
|
+
// alive until it fires) and is **cleared in a `finally`**. Both halves are
|
|
173
|
+
// required: ref'd without clearing would trade a silent early exit for a
|
|
174
|
+
// guaranteed delay on every call that answered promptly.
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const bound = setTimeout(() => {
|
|
177
|
+
controller.abort(new Error(`the endpoint did not answer within ${String(timeoutMs)}ms`));
|
|
178
|
+
}, timeoutMs);
|
|
179
|
+
let payload;
|
|
180
|
+
try {
|
|
181
|
+
const response = await fetchImpl(`${record.endpoint}/json/version`, {
|
|
182
|
+
signal: controller.signal,
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
failure: 'endpoint_unreachable',
|
|
188
|
+
detail: `The endpoint answered with status ${String(response.status)} rather than a version document.`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
payload = (await response.json());
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
failure: 'endpoint_unreachable',
|
|
197
|
+
detail: `The endpoint did not answer: ${error instanceof Error ? error.message : String(error)}. The record is a claim, not a proof — it survives the browser it describes.`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
// Cleared on every path out of the request, including the early returns
|
|
202
|
+
// above: a bound that outlived the thing it bounds would hold the process
|
|
203
|
+
// open for the rest of its window on every successful call.
|
|
204
|
+
clearTimeout(bound);
|
|
205
|
+
}
|
|
206
|
+
const url = payload.webSocketDebuggerUrl;
|
|
207
|
+
const actualUuid = typeof url === 'string' ? url.split('/').at(-1) : undefined;
|
|
208
|
+
if (actualUuid === undefined || actualUuid === '') {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
failure: 'identity_mismatch',
|
|
212
|
+
detail: 'The endpoint answered but reported no browser identifier, so there is nothing to match the record against.',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (actualUuid !== expectedUuid) {
|
|
216
|
+
// The case this exists for: the recorded browser exited and an unrelated
|
|
217
|
+
// process was handed the same port. Matching the number alone would read
|
|
218
|
+
// as success and attach to a stranger.
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
failure: 'identity_mismatch',
|
|
222
|
+
detail: 'The endpoint answered, but it is a different browser from the one the record describes. Ports are reused, so a matching port is not a matching browser.',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, record: { endpoint: record.endpoint, browserUuid: actualUuid } };
|
|
226
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam between the service and whatever actually drives a browser.
|
|
3
|
+
*
|
|
4
|
+
* Everything in this file is types and constants. There is no automation
|
|
5
|
+
* library behind it yet and no runtime dependency added for it — row #20
|
|
6
|
+
* brings the real implementation, and rows #21 through #24, #53, #54 and #56
|
|
7
|
+
* fill in the operations declared here. What lands now is the shape those
|
|
8
|
+
* rows plug into, and a fake that satisfies it (`fake.ts`).
|
|
9
|
+
*
|
|
10
|
+
* ── Why the interface is worth its own row ──────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* `DECISIONS.md` §5: *a guard that returns "denied" after the tab already
|
|
13
|
+
* opened is worse than no guard*, because it reports a refusal that did not
|
|
14
|
+
* happen and everything downstream believes it. A rejection test that only
|
|
15
|
+
* asserts the response cannot tell those two apart. So the seam exists to be
|
|
16
|
+
* faked, and the fake exists to make *nothing happened* an assertable claim.
|
|
17
|
+
* That is why this row comes before the real driver rather than after it.
|
|
18
|
+
*
|
|
19
|
+
* ── What this shape enforces, and what it merely encourages ─────────────
|
|
20
|
+
*
|
|
21
|
+
* Worth stating exactly, in the manner `transaction.ts` states its own
|
|
22
|
+
* bypasses, because the difference is where a later row will get hurt:
|
|
23
|
+
*
|
|
24
|
+
* **Structural — the compiler refuses these.**
|
|
25
|
+
*
|
|
26
|
+
* - **A browser's *kind* is one of two, and there is no third.**
|
|
27
|
+
* {@link BrowserKind} is a union of two string literals, and the store
|
|
28
|
+
* enforces the same thing with a check constraint (schema step nine), so
|
|
29
|
+
* the code that talks to browsers agrees with the database by
|
|
30
|
+
* construction.
|
|
31
|
+
*
|
|
32
|
+
* **What is not structural, said plainly rather than left to be
|
|
33
|
+
* discovered:** the *number* of browsers. `DECISIONS.md` §13i makes the
|
|
34
|
+
* configured browsers a bounded list per kind read from the environment,
|
|
35
|
+
* and a set that is not known until a spawn starts cannot be a type. The
|
|
36
|
+
* bound is enforced — by
|
|
37
|
+
* `src/config/environment.ts`, which refuses a list over its cap and names
|
|
38
|
+
* the entries — but **it is enforced at startup rather than by the
|
|
39
|
+
* compiler**, and a reader who assumes otherwise will look for a guarantee
|
|
40
|
+
* in the wrong place.
|
|
41
|
+
* - **No operation takes a list of tabs.** Every member of
|
|
42
|
+
* {@link TabOperations} is singular, because a lease is one tab (§2.3) and
|
|
43
|
+
* `SCHEMA.md` §3.1 puts it plainly: there was never more than one to list.
|
|
44
|
+
* There is no plural close and no "close all my tabs" to write.
|
|
45
|
+
* - **A tab is closed through the handle for that tab.** {@link TabHandle} is
|
|
46
|
+
* what {@link BrowserSession.openTab} returns, so closing one requires
|
|
47
|
+
* already holding it.
|
|
48
|
+
*
|
|
49
|
+
* **Conventional — this shape makes the wrong thing awkward and nothing
|
|
50
|
+
* more.** Say so rather than implying otherwise:
|
|
51
|
+
*
|
|
52
|
+
* - **`browser_scoped.never` is not enforced here.** No method on
|
|
53
|
+
* {@link BrowserSession} closes, kills, restarts or reaps a browser, so
|
|
54
|
+
* there is no browser-scoped destructive call to reach for and adding one
|
|
55
|
+
* would be a visible new method on a documented seam rather than an
|
|
56
|
+
* argument slipped into an existing call. That is deliberately as far as it
|
|
57
|
+
* goes: an implementation of this interface holds a live connection to a
|
|
58
|
+
* browser, and nothing in a type signature can stop the module that holds
|
|
59
|
+
* it from doing something else with it. `SCHEMA.md` §7.3 makes
|
|
60
|
+
* `browser_scoped.never` a **build rule** for exactly that reason, and the
|
|
61
|
+
* rule is checked against the agent surface, which this is not. Do not read
|
|
62
|
+
* the absence of a close-browser method as the guarantee; it is the easy
|
|
63
|
+
* path pointing the right way.
|
|
64
|
+
* - **Adoption is a protocol, not a constructor.** {@link BrowserDriver}
|
|
65
|
+
* offers {@link BrowserDriver.attach} and {@link BrowserDriver.coldStart}
|
|
66
|
+
* as separate calls because they are separate acts with different risks
|
|
67
|
+
* (§1.2a), and nothing here forces a caller to try the first before the
|
|
68
|
+
* second. Row #54 arbitrates which one a given caller performs, in the
|
|
69
|
+
* store, in the same transaction that arbitrates claims. **The ordering
|
|
70
|
+
* rule lives there, not in this file.**
|
|
71
|
+
* - **Nothing here keeps browser work out of the arbitration transaction.**
|
|
72
|
+
* Every operation is `async`, and `SCHEMA.md` §2.4b's rule is that none of
|
|
73
|
+
* them is reachable from inside a transaction. What makes that the path of
|
|
74
|
+
* least resistance is `transaction.ts`'s `afterCommit`, which already takes
|
|
75
|
+
* the collect-inside-act-outside shape; what makes it checkable is the
|
|
76
|
+
* `arbitration.no_browser_io` build rule (§7.3). This file cannot do either
|
|
77
|
+
* job, and a comment claiming it did would be the kind of false assurance
|
|
78
|
+
* §7.3 exists to replace.
|
|
79
|
+
*/
|
|
80
|
+
/** Both kinds, in a fixed order, for anything that has to visit each. */
|
|
81
|
+
export const BROWSER_KINDS = ['regular', 'private'];
|
|
82
|
+
/**
|
|
83
|
+
* The browsers the default configuration names, in a fixed order.
|
|
84
|
+
*
|
|
85
|
+
* **The default set, never the permitted set.** Anything deciding what a
|
|
86
|
+
* particular installation actually has must read the configured lists or the
|
|
87
|
+
* store; this is what an installation that has set nothing runs, and it is
|
|
88
|
+
* what fixtures and the fake driver stand up when no configuration is in
|
|
89
|
+
* play. Reading it as the permitted set is the mistake this sentence exists
|
|
90
|
+
* to prevent.
|
|
91
|
+
*/
|
|
92
|
+
export const DEFAULT_BROWSER_IDS = ['regular', 'private'];
|
|
93
|
+
/**
|
|
94
|
+
* **Which browser to claim, in one sentence** (`SCHEMA.md` §1.2, §3.2, row
|
|
95
|
+
* #66).
|
|
96
|
+
*
|
|
97
|
+
* ── Why this string exists, rather than the guidance being written twice ──
|
|
98
|
+
*
|
|
99
|
+
* §3.2 requires the same guidance in two places: **the tool's description
|
|
100
|
+
* text**, which is the only surface a calling agent reliably reads, and **the
|
|
101
|
+
* claim refusal**, because a caller re-reading a refusal is a caller
|
|
102
|
+
* re-making this decision. Two hand-written copies drift, and the copy that
|
|
103
|
+
* goes stale is the one nobody is looking at.
|
|
104
|
+
*
|
|
105
|
+
* ── Why the guidance is needed at all ───────────────────────────────────
|
|
106
|
+
*
|
|
107
|
+
* **Measured: 25 sessions seeded authentication into an isolated browser
|
|
108
|
+
* while the signed-in browser sat unused** — doing by hand, unreliably, the
|
|
109
|
+
* one thing that browser exists to provide. Nothing in this design assigns a
|
|
110
|
+
* kind of caller to a browser; it is a per-claim choice, and nothing told
|
|
111
|
+
* callers how to make it. **A capability nobody finds is worth what an absent
|
|
112
|
+
* capability is worth.**
|
|
113
|
+
*
|
|
114
|
+
* ── The caveat travels with it, and that is not optional ────────────────
|
|
115
|
+
*
|
|
116
|
+
* **Tabs in one browser share its cookie jar.** Two callers in the same
|
|
117
|
+
* browser are clean-room relative to every other browser and **relative to
|
|
118
|
+
* nothing else** — not to each other. That is right for reviewing an
|
|
119
|
+
* authenticated surface, where every caller wants the same identity and wants
|
|
120
|
+
* it to be the real one, and **wrong for exercising two identities at once in
|
|
121
|
+
* one browser**.
|
|
122
|
+
*
|
|
123
|
+
* Two identities at once is what a second configured browser is for
|
|
124
|
+
* (`DECISIONS.md` §13i), and the guidance says so rather than leaving it to
|
|
125
|
+
* be discovered from a test that mysteriously sees the wrong account.
|
|
126
|
+
*
|
|
127
|
+
* Stating the caveat in the same breath is the whole point: guidance that
|
|
128
|
+
* sent callers to the signed-in browser without it would trade one silent
|
|
129
|
+
* failure for another.
|
|
130
|
+
*
|
|
131
|
+
* ── The default makes this string carry more, not less ──────────────────
|
|
132
|
+
*
|
|
133
|
+
* `browser` is optional, and an unstated one resolves to the first signed-in
|
|
134
|
+
* browser (§3.2, `DECISIONS.md` §13i). **That is an argument for this text
|
|
135
|
+
* being more load-bearing rather than less**, and it is worth saying because
|
|
136
|
+
* the opposite reading is the tempting one. A caller that states nothing
|
|
137
|
+
* never sees the unknown-browser refusal, so the refusal stops being a place
|
|
138
|
+
* the caveat can reach it — and the default routes *more* traffic into one
|
|
139
|
+
* shared cookie jar, which is the exact condition the caveat is about. This
|
|
140
|
+
* is where a caller learns it or does not learn it at all.
|
|
141
|
+
*
|
|
142
|
+
* ── Kept short on purpose ───────────────────────────────────────────────
|
|
143
|
+
*
|
|
144
|
+
* §3.1: **surface area is a standing tax.** This text sits in a connected
|
|
145
|
+
* session's context on every turn, so it is the shortest wording that still
|
|
146
|
+
* carries the choice and its caveat. It is not an essay and must not become
|
|
147
|
+
* one.
|
|
148
|
+
*/
|
|
149
|
+
export const BROWSER_CHOICE_GUIDANCE = 'Omit this and you get the first signed-in browser, which is what most work wants. ' +
|
|
150
|
+
'Pass "private" for genuinely-fresh-visitor work — first-visit behaviour, an ' +
|
|
151
|
+
'undismissed banner, a consent prompt as a stranger sees it — or a configured ' +
|
|
152
|
+
'browser name to pick that one exactly. Tabs in one browser share its cookie jar, ' +
|
|
153
|
+
'so callers in the same browser are isolated from other browsers but not from each ' +
|
|
154
|
+
'other: for two identities at once, claim two differently-named browsers rather than ' +
|
|
155
|
+
'two tabs in one.';
|
|
156
|
+
/** Every action, in the order §3.8 lists them, for a refusal that names them all. */
|
|
157
|
+
export const PAGE_ACTIONS = [
|
|
158
|
+
'click',
|
|
159
|
+
'type',
|
|
160
|
+
'fill',
|
|
161
|
+
'press',
|
|
162
|
+
'select',
|
|
163
|
+
'hover',
|
|
164
|
+
'check',
|
|
165
|
+
'scroll',
|
|
166
|
+
'resize',
|
|
167
|
+
'emulate',
|
|
168
|
+
'dialog',
|
|
169
|
+
'fill_form',
|
|
170
|
+
'drag',
|
|
171
|
+
];
|
|
172
|
+
/**
|
|
173
|
+
* Every artefact a read can ask for, for a caller that has to enumerate them.
|
|
174
|
+
*
|
|
175
|
+
* Snapshot first because it is the default and the only load-bearing one
|
|
176
|
+
* (`SCHEMA.md` §3.9): every element reference `browser_act` takes comes from
|
|
177
|
+
* it, so a read that omitted it would be useless in the ordinary case.
|
|
178
|
+
*/
|
|
179
|
+
export const READ_ARTIFACTS = [
|
|
180
|
+
'snapshot',
|
|
181
|
+
'console',
|
|
182
|
+
'network',
|
|
183
|
+
'cookies',
|
|
184
|
+
];
|
|
185
|
+
/**
|
|
186
|
+
* Which artefacts are which. Written down so *"is this already being
|
|
187
|
+
* collected"* has a stable answer per artefact rather than being something to
|
|
188
|
+
* reason out each time somebody reads the default.
|
|
189
|
+
*/
|
|
190
|
+
export const ARTIFACT_COLLECTION = {
|
|
191
|
+
snapshot: 'generated',
|
|
192
|
+
console: 'accumulated',
|
|
193
|
+
network: 'accumulated',
|
|
194
|
+
cookies: 'live',
|
|
195
|
+
};
|