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,588 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { BrokerError } from "../errors.js";
4
+ import { SIGN_IN_OWNER_UNKNOWN_REMEDY } from "../service/signin-recovery.js";
5
+ import { refuseNetworkLocation } from "../store/network-path.js";
6
+ import { EXPECTED_VERSION } from "../store/schema/steps.js";
7
+ /**
8
+ * Exit codes, one per failing precondition group (`SCHEMA.md` §5.5: "Exits
9
+ * with a distinct code on any failure").
10
+ *
11
+ * **Distinct rather than sequential**, and the grouping is the substance: a
12
+ * caller using this as a readiness check branches on *what* is wrong, and the
13
+ * useful distinctions are the ones that call for different responses — a
14
+ * store on a network filesystem needs configuration changed, a browser that
15
+ * does not answer needs a browser started, and a budget disagreement needs
16
+ * an environment reconciled.
17
+ *
18
+ * Zero is every precondition either passing or being unevaluable. **An
19
+ * `unknown` does not fail the command**, for the reason given on
20
+ * {@link CheckStatus}: a fresh install with no browser launched is not a
21
+ * broken install, and a readiness check that refused one would be unusable
22
+ * on the machine it is most needed on.
23
+ *
24
+ * When several groups fail, the **lowest** code is reported, so the exit code
25
+ * is stable rather than depending on check order — and the full report on the
26
+ * output stream is where the rest of them are. A single number cannot carry
27
+ * more than one failure and pretending otherwise is how a code becomes
28
+ * meaningless.
29
+ */
30
+ export const DOCTOR_EXIT = {
31
+ ok: 0,
32
+ /** The store: where it is, what it is on, what version it is at. */
33
+ store: 10,
34
+ /** The automation tool is absent or unusable. */
35
+ automation: 11,
36
+ /** An artifact or profile root is not writable. */
37
+ roots: 12,
38
+ /** A browser's discovery record does not check out. */
39
+ browsers: 13,
40
+ /** The capture-surface check. */
41
+ capture: 14,
42
+ /** A keeper tab is missing. */
43
+ keeper: 15,
44
+ /** The stored tab budget disagrees with this process's environment. */
45
+ budget: 16,
46
+ /**
47
+ * The sign-in state of the persistent profile.
48
+ *
49
+ * It has a group so the report can carry it, and it is **the one group no
50
+ * check ever fails into**: a profile without a session is the ordinary
51
+ * state of a fresh install rather than a fault. The code exists so the
52
+ * grouping is total rather than because anything returns it.
53
+ */
54
+ session: 17,
55
+ };
56
+ /**
57
+ * The exit code for a set of results.
58
+ *
59
+ * Lowest failing group wins, per the note on {@link DOCTOR_EXIT}. Ordering by
60
+ * the code's numeric value rather than by the order checks happened to run in
61
+ * is what makes it reproducible.
62
+ */
63
+ export function exitCodeFor(results) {
64
+ const failed = results
65
+ .filter((result) => result.status === 'failed')
66
+ .map((result) => DOCTOR_EXIT[result.group]);
67
+ return failed.length === 0 ? DOCTOR_EXIT.ok : Math.min(...failed);
68
+ }
69
+ /**
70
+ * Where the store is, and that it is not somewhere it may not be (§1.0).
71
+ *
72
+ * **It calls the same refusal the store open calls**, rather than
73
+ * reimplementing the test. That is the whole reason this check is worth
74
+ * anything: a doctor with its own opinion about what a network path is would
75
+ * eventually disagree with the thing that actually refuses to start, and the
76
+ * disagreement would be discovered by somebody whose doctor said the
77
+ * installation was fine and whose service would not run.
78
+ *
79
+ * The refusal throws rather than returning, so it is caught and turned into a
80
+ * report line — reporting rather than refusing is this command's whole
81
+ * contract.
82
+ */
83
+ export function checkStoreLocation(environment, checks) {
84
+ const id = 'store.not_on_network_filesystem';
85
+ const title = 'The store is not on a network filesystem';
86
+ try {
87
+ if (environment.configuredDatabasePath !== undefined) {
88
+ refuseNetworkLocation(environment.configuredDatabasePath, checks);
89
+ }
90
+ refuseNetworkLocation(environment.databasePath, checks);
91
+ }
92
+ catch (error) {
93
+ if (error instanceof BrokerError) {
94
+ return {
95
+ group: 'store',
96
+ id,
97
+ title,
98
+ status: 'failed',
99
+ // The refusal's own message, which names the variable and what to set
100
+ // it to — and which deliberately does not echo the path back. The
101
+ // path is a real location on a real machine and this report is a
102
+ // thing people paste into messages.
103
+ detail: error.message,
104
+ remedy: 'Point BROKER_DB at a local filesystem. Several processes arbitrate against this store at once, and the write-ahead log requires every one of them to be on the same host.',
105
+ };
106
+ }
107
+ throw error;
108
+ }
109
+ return {
110
+ group: 'store',
111
+ id,
112
+ title,
113
+ status: 'ok',
114
+ detail: 'The configured store location is local.',
115
+ };
116
+ }
117
+ /** That the store file exists and can be read and written. */
118
+ export function checkStorePresent(environment) {
119
+ const location = environment.databasePath;
120
+ if (!fs.existsSync(location)) {
121
+ return {
122
+ group: 'store',
123
+ id: 'store.present',
124
+ title: 'The store exists',
125
+ status: 'unknown',
126
+ // Not a failure: the store is created on first spawn, so its absence
127
+ // means nothing has run yet rather than that something is broken.
128
+ detail: 'No store file at the configured location. It is created on the first spawn.',
129
+ };
130
+ }
131
+ try {
132
+ fs.accessSync(location, fs.constants.R_OK | fs.constants.W_OK);
133
+ return {
134
+ group: 'store',
135
+ id: 'store.present',
136
+ title: 'The store exists',
137
+ status: 'ok',
138
+ detail: 'The store file is present and both readable and writable.',
139
+ };
140
+ }
141
+ catch {
142
+ return {
143
+ group: 'store',
144
+ id: 'store.present',
145
+ title: 'The store exists',
146
+ status: 'failed',
147
+ detail: 'The store file is present but cannot be both read and written.',
148
+ remedy: 'Check the file’s ownership and permissions for the account running this command.',
149
+ };
150
+ }
151
+ }
152
+ /**
153
+ * That the store is at the version this build expects.
154
+ *
155
+ * Reports and does not step. A build ahead of its store is the ordinary
156
+ * upgrade case and is fixed by the next spawn; a **store ahead of the build**
157
+ * is the dangerous direction, because the running code does not know what the
158
+ * newer schema means, so the two are distinguished in the message.
159
+ */
160
+ export function checkSchemaVersion(found) {
161
+ if (found === null) {
162
+ return {
163
+ group: 'store',
164
+ id: 'store.version',
165
+ title: 'The store is at the version this build expects',
166
+ status: 'unknown',
167
+ detail: `No store to read a version from. This build expects version ${String(EXPECTED_VERSION)}.`,
168
+ };
169
+ }
170
+ if (found === EXPECTED_VERSION) {
171
+ return {
172
+ group: 'store',
173
+ id: 'store.version',
174
+ title: 'The store is at the version this build expects',
175
+ status: 'ok',
176
+ detail: `Version ${String(found)}.`,
177
+ };
178
+ }
179
+ const ahead = found > EXPECTED_VERSION;
180
+ return {
181
+ group: 'store',
182
+ id: 'store.version',
183
+ title: 'The store is at the version this build expects',
184
+ status: 'failed',
185
+ detail: ahead
186
+ ? `The store is at version ${String(found)} and this build expects ${String(EXPECTED_VERSION)}. The store was written by a newer build.`
187
+ : `The store is at version ${String(found)} and this build expects ${String(EXPECTED_VERSION)}.`,
188
+ remedy: ahead
189
+ ? 'Run the newer build against this store, or point BROKER_DB at a store this build understands. This build cannot safely read a schema it does not know.'
190
+ : 'Any spawn steps the schema. Run the service once; this command reports and does not step.',
191
+ };
192
+ }
193
+ export function checkAutomation(probe) {
194
+ if (probe.present === undefined) {
195
+ return {
196
+ group: 'automation',
197
+ id: 'automation.present',
198
+ title: 'The automation tool is present',
199
+ status: 'unknown',
200
+ detail: probe.detail ??
201
+ 'No automation probe was supplied, so there is nothing to report a version for.',
202
+ };
203
+ }
204
+ if (!probe.present) {
205
+ return {
206
+ group: 'automation',
207
+ id: 'automation.present',
208
+ title: 'The automation tool is present',
209
+ status: 'failed',
210
+ detail: probe.detail ?? 'No automation tool could be found for this process to use.',
211
+ remedy: 'Install a browser binary for the automation driver this build depends on — see the README’s install section.',
212
+ };
213
+ }
214
+ return {
215
+ group: 'automation',
216
+ id: 'automation.present',
217
+ title: 'The automation tool is present',
218
+ status: 'ok',
219
+ detail: `Present${probe.version === undefined ? '' : `, version ${probe.version}`}.`,
220
+ };
221
+ }
222
+ /**
223
+ * That a root is writable, proved by writing.
224
+ *
225
+ * `fs.access` with the write bit answers a different question on more than
226
+ * one platform — it consults permission bits that a filesystem, a container
227
+ * mount or an access-control list can override, so it reports writable for
228
+ * directories that refuse the write. The only answer that is not a guess is
229
+ * the write itself.
230
+ *
231
+ * **What it wrote is removed.** This is the one place the doctor touches
232
+ * anything outside the store, and it is named in this module's header for
233
+ * that reason. It does **not** create the directory: creating it is the setup
234
+ * handshake's job, and a doctor that created what it was asked to check would
235
+ * report on a state it had just produced.
236
+ */
237
+ export function checkRootWritable(id, title, root) {
238
+ if (!fs.existsSync(root)) {
239
+ return {
240
+ group: 'roots',
241
+ id,
242
+ title,
243
+ status: 'unknown',
244
+ detail: 'The directory does not exist. Every spawn creates it; this command does not.',
245
+ };
246
+ }
247
+ const probe = path.join(root, `.broker-doctor-${String(process.pid)}-${String(Date.now())}`);
248
+ try {
249
+ fs.writeFileSync(probe, '');
250
+ return { group: 'roots', id, title, status: 'ok', detail: 'The directory accepted a write.' };
251
+ }
252
+ catch {
253
+ return {
254
+ group: 'roots',
255
+ id,
256
+ title,
257
+ status: 'failed',
258
+ detail: 'The directory exists but refused a write.',
259
+ remedy: 'Check the directory’s ownership and permissions for the account running this command.',
260
+ };
261
+ }
262
+ finally {
263
+ // Best effort: a probe left behind is untidy and harmless, and throwing
264
+ // out of a cleanup would turn a passing check into a crash.
265
+ try {
266
+ fs.rmSync(probe, { force: true });
267
+ }
268
+ catch {
269
+ /* the probe outlives us; the check itself already answered */
270
+ }
271
+ }
272
+ }
273
+ export function checkDiscoveryRecord(browser, probe) {
274
+ const id = `browser.${browser}.discovery`;
275
+ const title = `The ${browser} browser’s discovery record checks out`;
276
+ if (!probe.recorded) {
277
+ return {
278
+ group: 'browsers',
279
+ id,
280
+ title,
281
+ status: 'unknown',
282
+ // Not a failure: no record means no browser has been launched, which is
283
+ // the state of every fresh install.
284
+ detail: 'No discovery record. This browser has not been launched.',
285
+ };
286
+ }
287
+ if (probe.answered !== true) {
288
+ return {
289
+ group: 'browsers',
290
+ id,
291
+ title,
292
+ status: 'failed',
293
+ detail: 'A discovery record is present but the endpoint it names did not answer.',
294
+ remedy: 'The record survives the browser it describes, so a stale one is expected after a browser exits. The next caller will take the launch race and start one.',
295
+ };
296
+ }
297
+ if (probe.expectedUuid === undefined || probe.reportedUuid === undefined) {
298
+ return {
299
+ group: 'browsers',
300
+ id,
301
+ title,
302
+ status: 'failed',
303
+ // Answering is not enough, and this is the case that would silently
304
+ // pass a port-only check.
305
+ detail: 'The endpoint answered but the browser’s own identifier could not be compared, so what answered cannot be shown to be the expected browser.',
306
+ remedy: 'Treat this browser as not running. Attaching to something that cannot be identified is worse than failing to attach, because it succeeds.',
307
+ };
308
+ }
309
+ if (probe.expectedUuid !== probe.reportedUuid) {
310
+ return {
311
+ group: 'browsers',
312
+ id,
313
+ title,
314
+ status: 'failed',
315
+ detail: 'The endpoint answered, but as a different browser than the record names — the port has been reused.',
316
+ remedy: 'Treat this browser as not running. The next caller will take the launch race and start one against the recorded profile.',
317
+ };
318
+ }
319
+ return {
320
+ group: 'browsers',
321
+ id,
322
+ title,
323
+ status: 'ok',
324
+ detail: 'The endpoint answered and identified itself as the browser the record names.',
325
+ };
326
+ }
327
+ /**
328
+ * The capture-surface check.
329
+ *
330
+ * The setting exists and the check that enforces it belongs to the capture
331
+ * pipeline's row. Reported here from the first version so the report has the
332
+ * entry §5.5 lists, and reported as `unknown` rather than invented, because a
333
+ * check that always passes is worse than one that says it has nothing to
334
+ * check.
335
+ */
336
+ export function checkCaptureSurface(configured) {
337
+ if (configured === undefined) {
338
+ return {
339
+ group: 'capture',
340
+ id: 'capture.surface',
341
+ title: 'The capture surface is configured',
342
+ status: 'unknown',
343
+ detail: 'This build does not read a capture-surface setting yet, so there is nothing to check it against.',
344
+ };
345
+ }
346
+ return {
347
+ group: 'capture',
348
+ id: 'capture.surface',
349
+ title: 'The capture surface is configured',
350
+ status: 'ok',
351
+ detail: `Configured as ${configured}.`,
352
+ };
353
+ }
354
+ /**
355
+ * The keeper tab (§3.15).
356
+ *
357
+ * **One blank page per browser that is never leased, never addressable and
358
+ * never counted against the budget** — and it is a correctness mechanism, not
359
+ * tidiness: a headed browser dies within about half a second of its last tab
360
+ * closing, so without it the ordinary release path destroys the shared
361
+ * authenticated session.
362
+ *
363
+ * §3.15 puts this check on `broker doctor` explicitly, "which checks it is
364
+ * present before either browser is allowed to serve". Answering needs a live
365
+ * browser to ask, so a browser that is not running reports `unknown` rather
366
+ * than failing.
367
+ */
368
+ export function checkKeeperTab(browser, present) {
369
+ const id = `browser.${browser}.keeper_tab`;
370
+ const title = `The ${browser} browser has its keeper tab`;
371
+ if (present === undefined) {
372
+ return {
373
+ group: 'keeper',
374
+ id,
375
+ title,
376
+ status: 'unknown',
377
+ detail: 'This browser is not reachable, so there is nothing to ask about a keeper tab.',
378
+ };
379
+ }
380
+ return present
381
+ ? { group: 'keeper', id, title, status: 'ok', detail: 'The keeper tab is present.' }
382
+ : {
383
+ group: 'keeper',
384
+ id,
385
+ title,
386
+ status: 'failed',
387
+ detail: 'No keeper tab. The last lease to be released would close the final tab.',
388
+ remedy: 'A spawn establishes the keeper tab. This command reports and does not create one — run the service against this browser.',
389
+ };
390
+ }
391
+ /**
392
+ * **Whether the stored tab budget agrees with this process's environment**
393
+ * (§5.5, §1.10).
394
+ *
395
+ * This is the precondition §1.10 argues for at length, and it is worth
396
+ * restating because it is the one whose failure is invisible without it:
397
+ * several processes arbitrate against the budget at once, so in one process's
398
+ * environment it can be fifteen and in another's thirty. **Each admits
399
+ * callers against its own belief, each is internally consistent, and the
400
+ * ceiling silently stops being a ceiling.** Nothing reports it. The count is
401
+ * correct in every process and the machine is over budget anyway.
402
+ *
403
+ * So a disagreement is a **failure**, not a warning — and neither number is
404
+ * adopted. A process running against a bound it was not configured for is a
405
+ * configuration error somebody needs to see, and overwriting the stored value
406
+ * would let whichever process started most recently move a bound the others
407
+ * are mid-arbitration against.
408
+ */
409
+ export function checkTabBudget(stored, configured) {
410
+ const id = 'config.tab_budget_agrees';
411
+ const title = 'The stored tab budget agrees with this process’s environment';
412
+ if (stored === null) {
413
+ return {
414
+ group: 'budget',
415
+ id,
416
+ title,
417
+ status: 'unknown',
418
+ detail: 'No budget has been recorded in this store. The first process to open it records the value it believes.',
419
+ };
420
+ }
421
+ if (configured === null) {
422
+ return {
423
+ group: 'budget',
424
+ id,
425
+ title,
426
+ status: 'unknown',
427
+ detail: `The store records ${String(stored)}, and this build does not read a tab-budget variable yet.`,
428
+ };
429
+ }
430
+ if (stored !== configured) {
431
+ return {
432
+ group: 'budget',
433
+ id,
434
+ title,
435
+ status: 'failed',
436
+ detail: `The store records ${String(stored)} and this process’s environment says ${String(configured)}.`,
437
+ remedy: 'Reconcile the environment with the stored value. Two processes arbitrating against different bounds means the ceiling is not a ceiling, and neither value is adopted automatically.',
438
+ };
439
+ }
440
+ return {
441
+ group: 'budget',
442
+ id,
443
+ title,
444
+ status: 'ok',
445
+ detail: `Both say ${String(stored)}.`,
446
+ };
447
+ }
448
+ /**
449
+ * **Whether the signed-in browser's profile looks signed in** (§5.5.1).
450
+ *
451
+ * ── Why this check exists, and why it never fails ───────────────────────
452
+ *
453
+ * Signing in is the one step a person performs by hand, and until this check
454
+ * existed there was **no way to confirm it worked** short of opening a
455
+ * browser and looking — which is the one thing this command may not do.
456
+ *
457
+ * **It reports and never fails**, which is a deliberate choice rather than an
458
+ * omission. A profile with no session in it is not a broken installation: it
459
+ * is every installation, up until the moment somebody signs in. Failing here
460
+ * would make `broker doctor` exit non-zero on a correct fresh install, and a
461
+ * readiness check that cannot go green on a working machine is one people
462
+ * learn to ignore. What it does instead is **say which of the two states it
463
+ * found**, which is the question being asked.
464
+ *
465
+ * ── Why a zero count is not reported as "not signed in" ─────────────────
466
+ *
467
+ * `session.ts` carries the measurements; the part that matters here is that
468
+ * the evidence is **positive-only**. Stored cookies mean a session was
469
+ * written down. No stored cookies mean no evidence was found — which is also
470
+ * what a site keeping its session in local storage looks like, and what a
471
+ * profile whose browser has not flushed yet looks like. So those come back as
472
+ * `unknown` **with the reason**, never as a confident negative.
473
+ */
474
+ /**
475
+ * **Whether a sign-in has been abandoned** (§5.5.1).
476
+ *
477
+ * ── Why this is the one sign-in check that can fail ─────────────────────
478
+ *
479
+ * Its neighbour {@link checkSignInSession} never fails, because a profile with
480
+ * no session in it is every installation up until somebody signs in. This one
481
+ * is the opposite kind of fact: a browser stuck in `signing-in` with **no
482
+ * process holding it** is not a state any working installation is in. It
483
+ * refuses every caller, and before it was recoverable it did so permanently.
484
+ * So it is a genuine failure and is reported as one.
485
+ *
486
+ * ── The three answers, and why only one of them is a failure ────────────
487
+ *
488
+ * - **Owner gone** — failed. Nobody is signing in and every caller is being
489
+ * turned away. The remedy is the recovery path, named explicitly, because a
490
+ * check that reports a fault without saying what to do has moved the work
491
+ * rather than done it.
492
+ * - **Owner running** — `ok`. Somebody is signing in right now, which is the
493
+ * command working exactly as designed. Reported rather than silent, so a
494
+ * person who cannot understand why callers are being refused can see the
495
+ * reason on this report instead of guessing.
496
+ * - **Owner unknown** — `unknown`, never failed. A row written before the
497
+ * owner column existed records nobody, and calling that a fault would fail
498
+ * the report on an installation that may be perfectly healthy. The detail
499
+ * says what was found and what to do about it.
500
+ */
501
+ export function checkAbandonedSignIn(browser, owner) {
502
+ const id = `browser.${browser}.signin_owner`;
503
+ const title = `No sign-in has been abandoned on the ${browser} browser`;
504
+ if (owner.kind === 'not-signing-in') {
505
+ return {
506
+ group: 'session',
507
+ id,
508
+ title,
509
+ status: 'ok',
510
+ detail: 'The browser is not being signed into, so there is nothing to have been abandoned.',
511
+ };
512
+ }
513
+ if (owner.kind === 'owner-running') {
514
+ return {
515
+ group: 'session',
516
+ id,
517
+ title,
518
+ status: 'ok',
519
+ detail: `A sign-in is in progress and the process holding it (${String(owner.pid)}) is running. ` +
520
+ 'Callers are being refused while it lasts; that is a pause rather than a fault.',
521
+ };
522
+ }
523
+ if (owner.kind === 'owner-unknown') {
524
+ return {
525
+ group: 'session',
526
+ id,
527
+ title,
528
+ status: 'unknown',
529
+ detail: 'The browser is recorded as being signed into, but this store does not say which process began it — ' +
530
+ 'so whether anybody is still signing in cannot be determined from here.',
531
+ remedy: SIGN_IN_OWNER_UNKNOWN_REMEDY,
532
+ };
533
+ }
534
+ return {
535
+ group: 'session',
536
+ id,
537
+ title,
538
+ status: 'failed',
539
+ detail: `The browser is stuck being signed into: the process that began it (${String(owner.pid)}) has gone. ` +
540
+ 'Every caller asking for this browser is being refused, and nothing will clear it on its own.',
541
+ remedy: 'Run `broker login` — it reclaims a sign-in whose process is gone and then hands you the window. ' +
542
+ 'If you do not want to sign in, running it and closing the browser window it opens returns the browser to service.',
543
+ };
544
+ }
545
+ export function checkSignInSession(browser, probe) {
546
+ const id = `browser.${browser}.session`;
547
+ const title = `The ${browser} browser’s profile carries a sign-in`;
548
+ if (probe.evidence === 'session-present') {
549
+ return {
550
+ group: 'session',
551
+ id,
552
+ title,
553
+ status: 'ok',
554
+ detail: `The profile holds ${String(probe.cookieCount ?? 0)} stored cookie(s), so a session was established and written down.`,
555
+ };
556
+ }
557
+ if (probe.evidence === 'no-profile') {
558
+ return {
559
+ group: 'session',
560
+ id,
561
+ title,
562
+ status: 'unknown',
563
+ detail: probe.reason ??
564
+ 'There is no profile directory yet, so there is nothing to look for a session in.',
565
+ };
566
+ }
567
+ if (probe.evidence === 'undetermined') {
568
+ return {
569
+ group: 'session',
570
+ id,
571
+ title,
572
+ status: 'unknown',
573
+ detail: probe.reason ?? 'The profile could not be inspected, so nothing is concluded.',
574
+ };
575
+ }
576
+ // `no-session-found`. Reported as unknown rather than failed, and the
577
+ // detail says exactly how strong the evidence is — the absence of a stored
578
+ // cookie is not proof that nobody is signed in.
579
+ return {
580
+ group: 'session',
581
+ id,
582
+ title,
583
+ status: 'unknown',
584
+ detail: probe.reason ??
585
+ 'No stored cookies were found in the profile, which is what a profile nobody has signed into looks like.',
586
+ remedy: `Run \`broker login\` to sign in to the ${browser} browser by hand. It is the one step a person performs, and it happens once.`,
587
+ };
588
+ }