@agent-flow-board/bridge 0.1.0 → 0.3.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 (62) hide show
  1. package/README.md +29 -1
  2. package/dist/checks/appdir.js +105 -0
  3. package/dist/checks/approve.js +213 -0
  4. package/dist/checks/audit.js +126 -0
  5. package/dist/checks/bind.js +100 -0
  6. package/dist/checks/board.js +107 -0
  7. package/dist/checks/broker.js +439 -0
  8. package/dist/checks/browser.js +275 -0
  9. package/dist/checks/browserpool.js +221 -0
  10. package/dist/checks/cache.js +710 -0
  11. package/dist/checks/child.js +211 -0
  12. package/dist/checks/command.js +267 -0
  13. package/dist/checks/commands.js +219 -0
  14. package/dist/checks/connect.js +79 -0
  15. package/dist/checks/connections.js +42 -0
  16. package/dist/checks/copy.js +320 -0
  17. package/dist/checks/dbpool.js +710 -0
  18. package/dist/checks/doctor.js +649 -0
  19. package/dist/checks/envPolicy.js +212 -0
  20. package/dist/checks/envrules.js +322 -0
  21. package/dist/checks/evidence.js +114 -0
  22. package/dist/checks/fsretry.js +29 -0
  23. package/dist/checks/guard.js +188 -0
  24. package/dist/checks/guardrails.js +155 -0
  25. package/dist/checks/importlegacy.js +300 -0
  26. package/dist/checks/install.js +150 -0
  27. package/dist/checks/live.js +76 -0
  28. package/dist/checks/lock.js +207 -0
  29. package/dist/checks/login.js +203 -0
  30. package/dist/checks/mailguard-hook.cjs +216 -0
  31. package/dist/checks/mailguard.js +98 -0
  32. package/dist/checks/paths.js +85 -0
  33. package/dist/checks/preview.js +577 -0
  34. package/dist/checks/profiles.js +110 -0
  35. package/dist/checks/provision.js +175 -0
  36. package/dist/checks/recipe.js +246 -0
  37. package/dist/checks/redact.js +52 -0
  38. package/dist/checks/registry.js +548 -0
  39. package/dist/checks/repoenv.js +162 -0
  40. package/dist/checks/roots.js +144 -0
  41. package/dist/checks/routing.js +68 -0
  42. package/dist/checks/run.js +443 -0
  43. package/dist/checks/sandbox.js +208 -0
  44. package/dist/checks/scheduler.js +383 -0
  45. package/dist/checks/scope.js +383 -0
  46. package/dist/checks/sealedNames.js +39 -0
  47. package/dist/checks/secrets.js +452 -0
  48. package/dist/checks/secretscmd.js +80 -0
  49. package/dist/checks/service.js +589 -0
  50. package/dist/checks/signin.js +79 -0
  51. package/dist/checks/singleflight.js +307 -0
  52. package/dist/checks/store.js +92 -0
  53. package/dist/checks/surface.js +124 -0
  54. package/dist/checks/types.js +2 -0
  55. package/dist/checks/warm.js +342 -0
  56. package/dist/checks/watch.js +228 -0
  57. package/dist/checks/wire.js +87 -0
  58. package/dist/cli.js +18 -1
  59. package/dist/hooks/adapters.js +3 -1
  60. package/dist/hooks/call.js +6 -1
  61. package/dist/hooks/modVersion.js +26 -0
  62. package/package.json +2 -2
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ // MAIL GUARD, THE RUNTIME HOOK. The tool, never a recipe, loads this file into the start step of a preview that has sealed mail settings (NODE_OPTIONS=--require). It stands
3
+ // between the app and Postmark: every send to a Postmark host is rewritten to go to the ONE test inbox the owner named, and anything it cannot read and rewrite is refused.
4
+ // Each rewrite and each refusal is a line in the run log (addresses masked) and a JSON line in AFB_MAIL_FENCE_LOG, which the tool counts into the run's result.
5
+ //
6
+ // WHAT THIS DOES AND DOES NOT PROTECT. It is a fence against mistakes (an app that mails the people in a test database), not against the repo's own code: code the agent wrote
7
+ // can load another HTTP client, spawn a process or call a mail API this file does not know. The owner's word that the token is a test token is what covers that, and it is in
8
+ // the audit chain. This file is CommonJS (a .cts compiled to .cjs) because --require loads CommonJS only; import-equals keeps the real module objects so they can be patched.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ // import-equals is the only form that keeps the real CommonJS module objects, so the guard can patch them (a --require hook is CommonJS)
11
+ /* eslint-disable @typescript-eslint/no-require-imports */
12
+ const fs = require("node:fs");
13
+ const path = require("node:path");
14
+ const net = require("node:net");
15
+ const http = require("node:http");
16
+ const https = require("node:https");
17
+ // -- CONSTANTS -- //
18
+ const INBOX = String(process.env.AFB_MAIL_FENCE_INBOX || '')
19
+ .trim()
20
+ .toLowerCase();
21
+ const LOG = process.env.AFB_MAIL_FENCE_LOG || '';
22
+ const POSTMARK = /(^|\.)postmarkapp\.com\.?$/i;
23
+ const SEND_PATHS = new Set([
24
+ '/email',
25
+ '/email/batch',
26
+ '/email/withtemplate',
27
+ '/email/batchwithtemplates'
28
+ ]);
29
+ const ADDRESS = /[^<>\s,;"]+@[^<>\s,;"]+/g;
30
+ // -- FUNCTIONS -- //
31
+ const record = (event) => {
32
+ const line = JSON.stringify({ at: new Date().toISOString(), pid: process.pid, ...event });
33
+ try {
34
+ if (LOG)
35
+ fs.appendFileSync(LOG, line + '\n');
36
+ }
37
+ catch {
38
+ // a log that cannot be written must not take the preview down; the console line below still shows
39
+ }
40
+ if (event.event !== 'loaded' && event.event !== 'listening')
41
+ process.stderr.write(`[afb-checks mail guard] ${event.event}: ${event.note || ''}\n`);
42
+ };
43
+ const mask = (a) => String(a).replace(/^(.).*@/, '$1***@');
44
+ const addresses = (v) => (typeof v === 'string' ? v.match(ADDRESS) || [] : []);
45
+ record({
46
+ event: 'loaded',
47
+ exe: path.basename(String(process.argv[1] || process.argv[0] || '')),
48
+ inbox: INBOX ? mask(INBOX) : null
49
+ });
50
+ /** One Postmark message: only the test inbox stays as a recipient; Cc and Bcc go; the subject says who it would have gone to (masked). */
51
+ function rewriteMessage(m) {
52
+ if (!m || typeof m !== 'object' || Array.isArray(m))
53
+ return { ok: false, why: 'a message the guard could not read' };
54
+ const msg = m;
55
+ // A recipient field in another spelling (cc, TO) could still be read by Postmark: the whole message is refused
56
+ if (Object.keys(msg).some((k) => /^(to|cc|bcc)$/i.test(k) && !['To', 'Cc', 'Bcc'].includes(k)))
57
+ return { ok: false, why: 'a recipient field spelled in a way the guard does not rewrite' };
58
+ const all = ['To', 'Cc', 'Bcc'].flatMap((k) => addresses(msg[k]));
59
+ if (!all.length)
60
+ return { ok: false, why: 'a message with no recipient it could read' };
61
+ if (all.every((a) => a.toLowerCase() === INBOX) && !msg.Cc && !msg.Bcc)
62
+ return { ok: true, message: msg, changed: false };
63
+ const out = { ...msg, To: INBOX };
64
+ delete out.Cc;
65
+ delete out.Bcc;
66
+ if (typeof msg.Subject === 'string')
67
+ out.Subject = `[afb-checks test, would go to ${[...new Set(all.map(mask))].join(', ')}] ${msg.Subject}`;
68
+ return { ok: true, message: out, changed: true, was: all.map(mask) };
69
+ }
70
+ function rewriteBody(text) {
71
+ let json;
72
+ try {
73
+ json = JSON.parse(text);
74
+ }
75
+ catch {
76
+ return { ok: false, why: 'a body that is not JSON' };
77
+ }
78
+ const wrapped = json &&
79
+ typeof json === 'object' &&
80
+ !Array.isArray(json) &&
81
+ Array.isArray(json.Messages);
82
+ const list = Array.isArray(json)
83
+ ? json
84
+ : wrapped
85
+ ? json.Messages
86
+ : [json];
87
+ const done = list.map(rewriteMessage);
88
+ for (const d of done)
89
+ if (!d.ok)
90
+ return { ok: false, why: d.why };
91
+ const messages = done.map((d) => (d.ok ? d.message : {}));
92
+ const body = Array.isArray(json)
93
+ ? messages
94
+ : wrapped
95
+ ? { ...json, Messages: messages }
96
+ : messages[0];
97
+ return {
98
+ ok: true,
99
+ body: JSON.stringify(body),
100
+ count: done.length,
101
+ changed: done.filter((d) => d.ok && d.changed).length
102
+ };
103
+ }
104
+ const refusal = (why) => new Response(JSON.stringify({
105
+ ErrorCode: 300,
106
+ Message: `afb-checks mail guard refused this send: ${why}. Mail from a preview goes only to the test inbox.`
107
+ }), { status: 422, headers: { 'content-type': 'application/json' } });
108
+ const realFetch = globalThis.fetch;
109
+ if (typeof realFetch === 'function') {
110
+ globalThis.fetch = async function guardedFetch(input, init) {
111
+ let url;
112
+ try {
113
+ const raw = typeof input === 'object' &&
114
+ input !== null &&
115
+ 'url' in input &&
116
+ typeof input.url === 'string'
117
+ ? input.url
118
+ : String(input);
119
+ url = new URL(raw);
120
+ }
121
+ catch {
122
+ // An address the guard cannot read is not sent on: it could be a mail host written in a way it does not know
123
+ record({ event: 'refused', note: 'an address the guard could not read' });
124
+ return refusal('an address the guard could not read');
125
+ }
126
+ if (!POSTMARK.test(url.hostname))
127
+ return realFetch.call(this, input, init);
128
+ const asRequest = typeof input === 'object' && input !== null && 'method' in input ? input : null;
129
+ const method = String((init && init.method) || (asRequest && asRequest.method) || 'GET').toUpperCase();
130
+ // Reading the server's own settings or a message's status sends nothing
131
+ if (method === 'GET' || method === 'HEAD')
132
+ return realFetch.call(this, input, init);
133
+ const key = url.pathname.replace(/\/+$/, '').toLowerCase();
134
+ if (!INBOX) {
135
+ record({ event: 'refused', note: 'no test inbox was given to the guard' });
136
+ return refusal('the guard has no test inbox');
137
+ }
138
+ if (method !== 'POST' || !SEND_PATHS.has(key)) {
139
+ record({
140
+ event: 'refused',
141
+ note: `${method} ${url.pathname} is not a send the guard can rewrite`
142
+ });
143
+ return refusal(`${method} ${url.pathname} is not a send the guard can rewrite`);
144
+ }
145
+ let text = null;
146
+ try {
147
+ if (init && init.body !== undefined && init.body !== null)
148
+ text = typeof init.body === 'string' ? init.body : null;
149
+ else if (asRequest && typeof asRequest.clone === 'function')
150
+ text = await asRequest.clone().text();
151
+ }
152
+ catch {
153
+ text = null;
154
+ }
155
+ if (typeof text !== 'string') {
156
+ record({ event: 'refused', note: 'the body was not plain text the guard could read' });
157
+ return refusal('the body was not plain text');
158
+ }
159
+ const r = rewriteBody(text);
160
+ if (!r.ok) {
161
+ record({ event: 'refused', note: r.why });
162
+ return refusal(r.why);
163
+ }
164
+ record({
165
+ event: 'sent',
166
+ note: `${r.count} message(s) to the test inbox, ${r.changed} rewritten`,
167
+ count: r.count,
168
+ rewritten: r.changed
169
+ });
170
+ const headers = new Headers((init && init.headers) || (asRequest && asRequest.headers) || {});
171
+ headers.delete('content-length');
172
+ return realFetch.call(this, url.toString(), { method: 'POST', headers, body: r.body });
173
+ };
174
+ }
175
+ // The tool counts the guard as loaded only when a process that has it listens on the preview's port, so a launcher (yarn, cross-env) or another runtime cannot stand in for the app
176
+ const realListen = net.Server.prototype.listen;
177
+ net.Server.prototype.listen = function guardedListen(...args) {
178
+ this.once('listening', () => {
179
+ const a = this.address();
180
+ if (a && typeof a === 'object')
181
+ record({ event: 'listening', port: a.port });
182
+ });
183
+ return realListen.apply(this, args);
184
+ };
185
+ // Another way into Postmark that cannot be rewritten (node:http, node:https) is refused outright
186
+ const hostOf = (name, a) => {
187
+ try {
188
+ if (typeof a === 'string')
189
+ return new URL(a.startsWith('//') ? `${name}:${a}` : a).hostname;
190
+ if (a instanceof URL)
191
+ return a.hostname;
192
+ if (a && typeof a === 'object') {
193
+ const o = a;
194
+ return String(o.hostname || String(o.host || '').split(':')[0] || '');
195
+ }
196
+ return '';
197
+ }
198
+ catch {
199
+ return '';
200
+ }
201
+ };
202
+ for (const [name, mod] of [
203
+ ['http', http],
204
+ ['https', https]
205
+ ]) {
206
+ for (const fn of ['request', 'get']) {
207
+ const real = mod[fn];
208
+ mod[fn] = function guarded(...args) {
209
+ if (POSTMARK.test(hostOf(name, args[0]))) {
210
+ record({ event: 'refused', note: `node:${name} ${fn} to a mail host` });
211
+ throw new Error('afb-checks mail guard refused a direct request to a mail host. Mail from a preview goes only to the test inbox, through fetch.');
212
+ }
213
+ return real.apply(this, args);
214
+ };
215
+ }
216
+ }
@@ -0,0 +1,98 @@
1
+ // THE TOOL SIDE OF THE MAIL GUARD. The guard itself is mailguard-hook.cts (compiled to mailguard-hook.cjs), loaded into the preview's start step. Here the tool decides when it
2
+ // is on (the registration has sealed mail settings), gives it the one test inbox, reads back what it did, and applies the fail-closed rule over the WHOLE mail set: a mail
3
+ // setting that did not come from the sealed store is never given to a preview, and a run with sealed mail settings counts only when the guard reported listening on the preview port.
4
+ // A recipe cannot ask for the guard, change it or turn it off: the environment is built here after every recipe check.
5
+ // -- DEP-MODULES -- //
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ // -- MODULES -- //
10
+ import { EMAIL, MAIL_SET_NAME } from './sealedNames.js';
11
+ // -- CONSTANTS -- //
12
+ /** The run's own mail setting: the mailer is told to only log. It is set by the tool, so it is never counted as an unsealed mail setting. */
13
+ const RUN_OWN_MAIL = new Set(['EMAIL_PROVIDER']);
14
+ // -- FUNCTIONS -- //
15
+ /** The hook file next to this module (dist/checks/mailguard-hook.cjs once built). */
16
+ export const GUARD_FILE = join(dirname(fileURLToPath(import.meta.url)), 'mailguard-hook.cjs');
17
+ const maskAddress = (a) => String(a).replace(/^(.).*@/, '$1***@');
18
+ /** Every variable of the whole mail set: POSTMARK, SMTP, SENDGRID, MAILGUN, RESEND, SES and anything with MAIL in its name. */
19
+ export const mailSetNames = (env) => Object.keys(env).filter((k) => MAIL_SET_NAME.test(k) && !RUN_OWN_MAIL.has(k.toUpperCase()));
20
+ /**
21
+ * Fail closed over the whole mail set. `sealed` is the list of names that were filled from the sealed store (SealedResolution.filled). Any other mail-set name in `env`
22
+ * is stripped ('strip', the default) or listed as a problem so the run refuses ('refuse'). Names only, never a value.
23
+ */
24
+ export function enforceMailSet(env, sealed, mode = 'strip') {
25
+ const out = { ...env };
26
+ const stripped = [];
27
+ const problems = [];
28
+ for (const k of mailSetNames(env)) {
29
+ if (sealed.includes(k))
30
+ continue;
31
+ if (mode === 'strip') {
32
+ delete out[k];
33
+ stripped.push(k);
34
+ }
35
+ else
36
+ problems.push(`${k} is a mail setting that did not come from the sealed store, so this run does not start. Import the mail settings with a test inbox, or remove it.`);
37
+ }
38
+ return { env: out, stripped, problems };
39
+ }
40
+ /**
41
+ * The start environment with the guard loaded and told its inbox. `--require <hook>` is appended to NODE_OPTIONS LAST, so nothing earlier can unload it; a NODE_OPTIONS
42
+ * that was already there is kept in front. A variable spelled in another case (Windows) is folded into the one NODE_OPTIONS.
43
+ */
44
+ export function withMailGuard(env, { inbox, log, hookFile = GUARD_FILE }) {
45
+ if (!EMAIL.test(inbox))
46
+ throw new Error('The mail guard needs one test inbox that is an email address.');
47
+ if (!log)
48
+ throw new Error('The mail guard needs a log file to report to.');
49
+ const out = {};
50
+ let previous = '';
51
+ for (const [k, v] of Object.entries(env)) {
52
+ if (k.toUpperCase() === 'NODE_OPTIONS')
53
+ previous = [previous, v].filter(Boolean).join(' ');
54
+ else
55
+ out[k] = v;
56
+ }
57
+ const req = `--require "${hookFile.replace(/\\/g, '/')}"`;
58
+ out.NODE_OPTIONS = [previous, req].filter(Boolean).join(' ');
59
+ out.AFB_MAIL_FENCE_INBOX = inbox;
60
+ out.AFB_MAIL_FENCE_LOG = log;
61
+ return out;
62
+ }
63
+ /**
64
+ * What the guard wrote: whether a process of the app loaded it, and how many messages it sent on (rewritten to the test inbox) and refused. `port` is the preview's port:
65
+ * the guard counts as loaded only when a process that has it is listening there, so a launcher (yarn, cross-env) or another runtime cannot stand in for the app.
66
+ */
67
+ export function readFence(log, port = null) {
68
+ const out = { loaded: false, sends: 0, rewritten: 0, refused: 0 };
69
+ if (!log || !existsSync(log))
70
+ return out;
71
+ for (const line of readFileSync(log, 'utf8').split('\n')) {
72
+ if (!line.trim())
73
+ continue;
74
+ try {
75
+ const e = JSON.parse(line);
76
+ if (e.event === 'listening' && port !== null && Number(e.port) === Number(port))
77
+ out.loaded = true;
78
+ else if (e.event === 'sent') {
79
+ out.sends += Number(e.count) || 0;
80
+ out.rewritten += Number(e.rewritten) || 0;
81
+ }
82
+ else if (e.event === 'refused')
83
+ out.refused += 1;
84
+ }
85
+ catch {
86
+ /* A line the guard did not write completely */
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ /** The sentence that stops a run, or null when it may count: sealed mail settings were given and the guard did not report listening on the preview port. */
92
+ export function fenceProblem(mailGiven, fence) {
93
+ if (!mailGiven || fence.loaded)
94
+ return null;
95
+ return 'The mail guard did not report listening on the preview port, so mail may not have been fenced. The run does not count.';
96
+ }
97
+ /** One plain sentence for the run log. */
98
+ export const describeFence = (inbox, f) => `Mail guard on: mail goes only to ${maskAddress(inbox)}. ${f.sends} message(s) sent there (${f.rewritten} rewritten from another recipient), ${f.refused} refused.`;
@@ -0,0 +1,85 @@
1
+ // WHERE THE CHECK COMMANDS KEEP THEIR STATE: ONE FOLDER UNDER THE BRIDGE HOME, NEVER INSIDE A REPO
2
+ // The bridge home also holds the token store (key and token files). Nothing here ever names those, and the sandboxed child of a check is
3
+ // given read access to the scripts folder, its own run folder and the Playwright runtime only (see sandbox.ts).
4
+ // -- DEP-MODULES -- //
5
+ import { mkdirSync, realpathSync } from 'node:fs';
6
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
7
+ // -- MODULES -- //
8
+ import { configDir } from '../tokenStore.js';
9
+ // -- CONSTANTS -- //
10
+ /** The version of the conversation between the board and the check tool. The board refuses a tool older than its minimum. */
11
+ export const TOOL_PROTOCOL = 2;
12
+ /** Item references and check keys: letters, digits, dash and underscore only, so a name can never leave its folder. */
13
+ export const NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
14
+ /** An organisation slug as the app writes it. */
15
+ export const ORG_SLUG = /^[a-z0-9][a-z0-9-]{1,38}$/;
16
+ // -- FUNCTIONS -- //
17
+ /** The folder of all check state. AFB_BRIDGE_HOME moves it, which is how the tests get a throwaway one. */
18
+ export const checksHome = () => join(configDir(), 'checks');
19
+ /** Check scripts: scripts/<org>/<item>/<key>.mjs. Written by the agent, never fetched and run from the board. */
20
+ export const scriptsDir = () => join(checksHome(), 'scripts');
21
+ /** One folder per run, named by the run id. */
22
+ export const runsDir = () => join(checksHome(), 'runs');
23
+ export const locksDir = () => join(checksHome(), 'locks');
24
+ export const auditFile = () => join(checksHome(), 'audit.log');
25
+ export const bindingsFile = () => join(checksHome(), 'bindings.json');
26
+ /** Private copies of repos, one per run, kept so a crashed run can be swept. */
27
+ export const copiesDir = () => join(checksHome(), 'copies');
28
+ /**
29
+ * The live and end markers of runs, written by the tool only. The sandboxed child can write its run folder, so a marker kept there could be forged and make a later
30
+ * sweep kill a process or remove a container: markers live here, outside every folder the child may read or write.
31
+ */
32
+ export const markersDir = () => join(checksHome(), 'markers');
33
+ export const markerPath = (runId) => join(markersDir(), `${assertName(runId, 'run id')}.json`);
34
+ export const endedPath = (runId) => join(markersDir(), `${assertName(runId, 'run id')}.ended`);
35
+ /** Build, install and preview logs and the mail guard's log: they can hold values the app printed, so they are kept where the child cannot read. */
36
+ export const logsDir = () => join(checksHome(), 'logs');
37
+ export const logDirFor = (runId) => join(logsDir(), assertName(runId, 'run id'));
38
+ export function ensureDir(dir) {
39
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
40
+ return dir;
41
+ }
42
+ /** Refuses a name that is not a plain name (a path separator, a dot dot, an empty string). */
43
+ export function assertName(value, what) {
44
+ if (!NAME.test(value))
45
+ throw new Error(`${what} "${value.slice(0, 40)}" is not a plain name (letters, digits, dash, underscore).`);
46
+ return value;
47
+ }
48
+ /** Joins parts below a base and proves the result stays below it. Throws when it does not. */
49
+ export function under(base, ...parts) {
50
+ const full = resolve(base, ...parts);
51
+ const rel = relative(resolve(base), full);
52
+ if (rel === '' || rel.startsWith('..') || isAbsolute(rel))
53
+ throw new Error('That path leaves its folder.');
54
+ return full;
55
+ }
56
+ /** The path of a check script. */
57
+ export function scriptPath(org, item, key) {
58
+ if (!ORG_SLUG.test(org))
59
+ throw new Error('The organisation is not a slug.');
60
+ return under(scriptsDir(), org, assertName(item, 'item reference'), `${assertName(key, 'check key')}.mjs`);
61
+ }
62
+ /** True when `child` is `parent` or inside it, compared by real path so a link cannot step outside. */
63
+ export function isInside(parent, child) {
64
+ try {
65
+ const rel = relative(realpathSync(parent), realpathSync(child));
66
+ return (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel) && !rel.split(sep).includes('..')));
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ // -- REGISTRATIONS, SECRETS, CACHES AND POOLS (AFB-20261108-0925) -- //
73
+ /** One JSON file per registration: registrations/<org>/<NAME>.json. */
74
+ export const registrationsDir = () => join(checksHome(), 'registrations');
75
+ /** Everything one registration owns apart from its definition: sealed secrets, sign-ins, kept builds. Never shared with another registration. */
76
+ export const stateDirFor = (org, name) => join(checksHome(), 'state', org, name);
77
+ /** Kept dependencies and builds of one registration. */
78
+ export const cacheDirFor = (org, name) => join(stateDirFor(org, name), 'cache');
79
+ /** The machine broker: slot files with a heartbeat, shared by `check run` processes and the worker. */
80
+ export const brokerDir = () => join(checksHome(), 'broker');
81
+ /** The shared browser and database pools of this machine, found through files here. */
82
+ export const poolDir = () => join(checksHome(), 'pool');
83
+ /** Owner approvals: single-use codes (hashed) and the approved roots. */
84
+ export const approvalsFile = () => join(checksHome(), 'approvals.json');
85
+ export const rootsFile = () => join(checksHome(), 'roots.json');