@tinycloud/courier 0.5.1-beta.2 → 0.5.1-beta.4

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.
@@ -0,0 +1,331 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { execFileSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { randomBytes } from 'node:crypto';
6
+ import { CourierError } from './errors.js';
7
+ /** Makes a link or rename in `dir` durable: the directory entry is fsynced, not only the file contents. */
8
+ export function fsyncDir(dir) {
9
+ const fd = fs.openSync(dir, 'r');
10
+ try {
11
+ fs.fsyncSync(fd);
12
+ }
13
+ finally {
14
+ fs.closeSync(fd);
15
+ }
16
+ }
17
+ /** Writes the full body to a same-directory temp file with the given mode, fsynced, and returns its path. */
18
+ export function writeTemp(dir, name, body, mode) {
19
+ const temp = path.join(dir, `.${name}.${randomBytes(6).toString('hex')}.tmp`);
20
+ const fd = fs.openSync(temp, 'wx', mode);
21
+ try {
22
+ fs.writeFileSync(fd, body);
23
+ fs.fchmodSync(fd, mode);
24
+ fs.fsyncSync(fd);
25
+ }
26
+ catch (error) {
27
+ fs.closeSync(fd);
28
+ fs.rmSync(temp, { force: true });
29
+ throw error;
30
+ }
31
+ fs.closeSync(fd);
32
+ return temp;
33
+ }
34
+ /**
35
+ * Creates `dir/name` only if nothing is there yet (hard link of the temp file, which fails if the target exists, symlinks
36
+ * included). Returns false when the target already exists, so a queued message is never overwritten.
37
+ */
38
+ export function writeNew(dir, name, body, mode) {
39
+ const temp = writeTemp(dir, name, body, mode);
40
+ try {
41
+ fs.linkSync(temp, path.join(dir, name));
42
+ fsyncDir(dir);
43
+ return true;
44
+ }
45
+ catch (error) {
46
+ if (error.code === 'EEXIST')
47
+ return false;
48
+ throw error;
49
+ }
50
+ finally {
51
+ fs.rmSync(temp, { force: true });
52
+ }
53
+ }
54
+ const MAX_FIELD = 128;
55
+ const OPTIONAL_FIELDS = ['host', 'pidns', 'machine', 'boot'];
56
+ /** Parses `{"pid":N,"nonce":"..."}` (plus the optional identity fields). A legacy `pid\n` lock parses with its own text as the nonce. */
57
+ function parseRecord(text) {
58
+ if (/^\d+\n?$/.test(text)) {
59
+ const pid = Number(text);
60
+ return Number.isInteger(pid) && pid > 0 ? { pid, nonce: text } : undefined;
61
+ }
62
+ try {
63
+ const value = JSON.parse(text);
64
+ if (typeof value !== 'object' || value === null)
65
+ return undefined;
66
+ const fields = value;
67
+ const { pid, nonce } = fields;
68
+ if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0)
69
+ return undefined;
70
+ if (typeof nonce !== 'string' || nonce.length === 0 || nonce.length > MAX_FIELD)
71
+ return undefined;
72
+ const record = { pid, nonce };
73
+ for (const key of OPTIONAL_FIELDS) {
74
+ const field = fields[key];
75
+ if (field === undefined)
76
+ continue;
77
+ if (typeof field !== 'string' || field.length > MAX_FIELD)
78
+ return undefined; // an oversized or mistyped field is a corrupt record
79
+ record[key] = field;
80
+ }
81
+ return record;
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function recordBody(record) {
88
+ return `${JSON.stringify(record)}\n`;
89
+ }
90
+ /** Reads the lock. An unparseable lock is never broken: it refuses, as a live one would. */
91
+ function readLock(file) {
92
+ let text;
93
+ try {
94
+ text = fs.readFileSync(file, 'utf8');
95
+ }
96
+ catch (error) {
97
+ if (error.code === 'ENOENT')
98
+ return { kind: 'missing' };
99
+ throw error;
100
+ }
101
+ const record = parseRecord(text);
102
+ return record ? { kind: 'held', record } : { kind: 'corrupt' };
103
+ }
104
+ function pidNamespace() {
105
+ try {
106
+ return fs.readlinkSync('/proc/self/ns/pid');
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ const realIo = {
113
+ platform: process.platform,
114
+ ...(process.env.SystemRoot === undefined ? {} : { systemRoot: process.env.SystemRoot }),
115
+ readFile: (file) => { try {
116
+ return fs.readFileSync(file, 'utf8');
117
+ }
118
+ catch {
119
+ return undefined;
120
+ } },
121
+ exec: (command, args) => { try {
122
+ return execFileSync(command, args, { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
123
+ }
124
+ catch {
125
+ return undefined;
126
+ } },
127
+ };
128
+ const firstLine = (text) => text?.trim().split(/\r?\n/)[0]?.trim() || undefined;
129
+ /**
130
+ * The machine and the boot this process runs on. `boot` is a true per-boot identifier or absent, and is prefixed with the platform that
131
+ * produced it so two kinds are never compared. Linux: /etc/machine-id and the kernel boot id. macOS: IOPlatformUUID (`/usr/sbin/ioreg`) and
132
+ * kern.bootsessionuuid (`/usr/sbin/sysctl`). Windows: MachineGuid (`%SystemRoot%\\System32\\reg.exe`); Windows has no per-boot identifier
133
+ * verified to be stable, so it has none. Wall-clock values (`kern.boottime`, LastBootUpTime, uptime) are never used: they jitter and shift
134
+ * with the clock, and a lock must not be called dead on a clock difference. An identity that cannot be read stays absent: records are then not judged.
135
+ */
136
+ export function machineIdentity(io = realIo) {
137
+ let machine;
138
+ let boot;
139
+ if (io.platform === 'linux') {
140
+ machine = firstLine(io.readFile('/etc/machine-id')) ?? firstLine(io.readFile('/var/lib/dbus/machine-id'));
141
+ const id = firstLine(io.readFile('/proc/sys/kernel/random/boot_id'));
142
+ if (id)
143
+ boot = `linux:${id}`;
144
+ }
145
+ else if (io.platform === 'darwin') {
146
+ machine = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(io.exec('/usr/sbin/ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']) ?? '')?.[1];
147
+ const id = firstLine(io.exec('/usr/sbin/sysctl', ['-n', 'kern.bootsessionuuid']));
148
+ if (id)
149
+ boot = `macos:${id}`;
150
+ }
151
+ else if (io.platform === 'win32') {
152
+ const reg = `${io.systemRoot ?? 'C:\\Windows'}\\System32\\reg.exe`;
153
+ machine = /MachineGuid\s+REG_SZ\s+(\S+)/.exec(io.exec(reg, ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid']) ?? '')?.[1];
154
+ }
155
+ return { ...(machine === undefined ? {} : { machine }), ...(boot === undefined ? {} : { boot }) };
156
+ }
157
+ let cachedIdentity;
158
+ const currentIdentity = () => (cachedIdentity ??= machineIdentity());
159
+ /** What this process records about itself: enough to tell another machine's, boot's or pid namespace's records apart. */
160
+ function identity(here) {
161
+ const out = { host: os.hostname(), ...here };
162
+ const ns = pidNamespace();
163
+ if (ns !== undefined)
164
+ out.pidns = ns;
165
+ return out;
166
+ }
167
+ /**
168
+ * Whether the process that wrote `record` still runs. A holder is `dead` in exactly two cases, and `unknown` (the lock is kept) in every other:
169
+ * (a) a different true boot id of the same kind on the same machine and host: no process survives a reboot; or
170
+ * (b) its pid is absent (`ESRCH`).
171
+ * Before either, the record must name this machine (machine-id) and this host (another machine, a cloned machine-id, a renamed host, an old
172
+ * pid-only record, or an identity this process cannot read is never judged), and on Linux the same pid namespace as this process: a reader in
173
+ * another pid namespace sees a live holder's pid as absent. A pid that exists is never `dead`, whatever its start time, state or owner: a
174
+ * crashed holder whose pid was reused needs the manual recovery line.
175
+ */
176
+ export function recordLiveness(record, here = currentIdentity()) {
177
+ if (record.machine === undefined || here.machine === undefined || record.machine !== here.machine)
178
+ return 'unknown';
179
+ if (record.host === undefined || record.host !== os.hostname())
180
+ return 'unknown';
181
+ if (record.boot !== undefined && here.boot !== undefined) {
182
+ const [kindA, kindB] = [record.boot.split(':', 1)[0], here.boot.split(':', 1)[0]];
183
+ if ((kindA === 'linux' || kindA === 'macos') && kindA === kindB && record.boot !== here.boot)
184
+ return 'dead';
185
+ }
186
+ const ns = pidNamespace();
187
+ if (process.platform === 'linux' ? record.pidns === undefined || ns === undefined || record.pidns !== ns : record.pidns !== undefined && record.pidns !== ns)
188
+ return 'unknown';
189
+ try {
190
+ process.kill(record.pid, 0);
191
+ }
192
+ catch (error) {
193
+ return error.code === 'ESRCH' ? 'dead' : 'unknown';
194
+ }
195
+ return 'unknown';
196
+ }
197
+ /** Guard slots per dead lock. The bound stops a corrupt directory from looping forever. */
198
+ const MAX_BREAK_GUARDS = 8;
199
+ /** The one line a refused lock prints: what holds it, and the only safe way out. The lock itself is never removed on doubt. */
200
+ export function lockRefusal({ file, reason, pid, host, record }, platform = process.platform) {
201
+ // Text from a lock record is data, not words: JSON-quoted so a hostile host name cannot add lines or terminal escapes.
202
+ const holder = pid === undefined ? 'a holder that cannot be read' : `pid ${pid}${host === undefined ? '' : ` on host ${JSON.stringify(host)}`}`;
203
+ const why = reason === 'alive' ? `is held by a running Courier command (${holder})`
204
+ : reason === 'corrupt' ? 'has a record Courier cannot read'
205
+ : reason === 'unknown' ? `is held by ${holder}, and Courier cannot tell whether that process is still running`
206
+ : `is held by ${holder}, and another Courier command is already working on clearing it`;
207
+ const remove = platform === 'win32'
208
+ ? `Remove-Item -LiteralPath '${file.replace(/['\u2018-\u201B]/g, (quote) => quote + quote)}' -Force` // PowerShell: single quotes are literal; ' and its typographic forms U+2018 to U+201B each end the string unless doubled
209
+ : `rm -f -- '${file.replace(/'/g, `'\\''`)}'`;
210
+ return `${file} ${why}${record === undefined ? '' : ` (record ${record})`}; the lock was left in place. Try again; only after confirming no Courier command is running anywhere that uses this lock, clear it with: ${remove}`;
211
+ }
212
+ /**
213
+ * Takes the lock `file`. A live or unknowable holder always refuses, however old its file is; only a holder that is established dead
214
+ * is broken. The lock is created whole (never empty) and carries a nonce, so a release or a break removes only the lock it actually observed.
215
+ */
216
+ export function acquireLock(file, opts = {}) {
217
+ const dir = path.dirname(file);
218
+ const name = path.basename(file);
219
+ const pid = opts.pid ?? process.pid;
220
+ const here = (opts.identity ?? currentIdentity)();
221
+ const self = { pid, nonce: randomBytes(6).toString('hex'), ...identity(here) };
222
+ const state = (record) => (opts.alive ? (opts.alive(record.pid) ? 'alive' : 'dead') : recordLiveness(record, here));
223
+ const refuse = (info) => {
224
+ throw new CourierError('locked', (opts.refusal ?? lockRefusal)({ file, ...info }));
225
+ };
226
+ for (let attempt = 0; attempt < 3; attempt++) {
227
+ let seen = readLock(file);
228
+ if (seen.kind === 'missing') {
229
+ // Only a missing lock costs a temp file and two fsyncs: a held one is judged from a read, so a waiter polling every 10 ms writes nothing.
230
+ let created;
231
+ try {
232
+ created = writeNew(dir, name, recordBody(self), 0o600);
233
+ }
234
+ catch (error) {
235
+ const now = readLock(file); // linked, then a later step (the directory fsync) failed: do not leave a live-looking lock nobody holds
236
+ if (now.kind === 'held' && now.record.nonce === self.nonce)
237
+ fs.rmSync(file, { force: true });
238
+ throw error;
239
+ }
240
+ if (created) {
241
+ return () => {
242
+ const current = readLock(file);
243
+ if (current.kind === 'held' && current.record.nonce === self.nonce)
244
+ fs.rmSync(file, { force: true });
245
+ };
246
+ }
247
+ seen = readLock(file);
248
+ if (seen.kind === 'missing')
249
+ continue;
250
+ }
251
+ if (seen.kind === 'corrupt')
252
+ return refuse({ reason: 'corrupt' });
253
+ if (seen.kind !== 'held')
254
+ continue;
255
+ const holder = { pid: seen.record.pid, record: JSON.stringify({ pid: seen.record.pid, nonce: seen.record.nonce, ...(seen.record.host === undefined ? {} : { host: seen.record.host }) }), ...(seen.record.host === undefined ? {} : { host: seen.record.host }) };
256
+ const live = state(seen.record);
257
+ if (live !== 'dead')
258
+ return refuse({ reason: live, ...holder });
259
+ opts.afterObserve?.();
260
+ if (!breakDead(file, seen.record, self, state))
261
+ return refuse({ reason: 'contended', ...holder });
262
+ }
263
+ return refuse({ reason: 'contended' });
264
+ }
265
+ /**
266
+ * Breaks the dead lock `stale`. Breakers serialize on numbered guard files `<lock>.break.<nonce>.<k>`: guard k is
267
+ * claimed exclusively; a guard whose holder is not dead refuses the break, and one whose holder is dead is passed over.
268
+ * Under the guard the lock is re-read and unlinked only if it is still exactly `stale`. Returns false when a live
269
+ * contender holds the break, and true when the caller should retry creating its own lock.
270
+ */
271
+ function breakDead(file, stale, self, state) {
272
+ const dir = path.dirname(file);
273
+ const tag = `${path.basename(file)}.break.${stale.nonce.trim().replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64)}.`;
274
+ const body = recordBody(self);
275
+ /** Guards whose breaker was read and judged dead: removed only while they still hold exactly that record, never one claimed meanwhile. */
276
+ const dead = new Map();
277
+ const clearDead = () => {
278
+ for (const [guard, text] of dead) {
279
+ try {
280
+ if (fs.readFileSync(guard, 'utf8') === text)
281
+ fs.rmSync(guard, { force: true });
282
+ }
283
+ catch { /* already gone */ }
284
+ }
285
+ };
286
+ let own;
287
+ let retries = 0;
288
+ for (let k = 1; k <= MAX_BREAK_GUARDS && own === undefined; k++) {
289
+ const candidate = path.join(dir, `${tag}${k}`);
290
+ if (writeNew(dir, `${tag}${k}`, body, 0o600)) {
291
+ own = candidate;
292
+ break;
293
+ }
294
+ let text;
295
+ try {
296
+ text = fs.readFileSync(candidate, 'utf8');
297
+ }
298
+ catch (error) {
299
+ // The breaker that held this guard finished and removed it between our failed create and this read: take the slot again.
300
+ if (error.code === 'ENOENT' && retries++ < MAX_BREAK_GUARDS) {
301
+ k -= 1;
302
+ continue;
303
+ }
304
+ return false;
305
+ }
306
+ const holder = parseRecord(text);
307
+ if (!holder || state(holder) !== 'dead')
308
+ return false;
309
+ dead.set(candidate, text);
310
+ }
311
+ if (own === undefined) {
312
+ // Every slot holds a guard whose breaker is dead: clear exactly those, or the break could never run again. The lock itself is still removed only by exact nonce.
313
+ clearDead();
314
+ return true;
315
+ }
316
+ try {
317
+ const now = readLock(file);
318
+ if (now.kind === 'held' && now.record.nonce === stale.nonce)
319
+ fs.rmSync(file, { force: true });
320
+ return true;
321
+ }
322
+ finally {
323
+ clearDead();
324
+ try {
325
+ if (fs.readFileSync(own, 'utf8') === body)
326
+ fs.rmSync(own, { force: true });
327
+ }
328
+ catch { /* already gone */ }
329
+ }
330
+ }
331
+ //# sourceMappingURL=file-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-lock.js","sourceRoot":"","sources":["../src/file-lock.ts"],"names":[],"mappings":"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"}
@@ -1,60 +1,12 @@
1
- import { type LinkRecord, type OpenKeyApprovalRequestV1 } from '@tinycloud/share-envelope/links';
2
1
  import { type AgentKey, type Profile } from './profile.js';
3
- export declare const DEFAULT_WAIT_MS = 60000;
4
- export declare const MAX_WAIT_MS: number;
5
- /** The durable record of one approval request. The result stays encrypted at rest under `relayPrivateKey`. */
6
- export interface PendingGrantRequest {
7
- version: 1;
8
- request: OpenKeyApprovalRequestV1;
9
- requestId: string;
10
- bodyCid: string;
11
- approvalUrl: string;
12
- requestExpiresAt: string;
13
- record: LinkRecord;
14
- apiOrigin: string;
15
- linkApiOrigin: string;
16
- openkeyOrigin: string;
17
- relayPrivateKey: string;
18
- /** Sent with the acknowledgement; stable across retries so OpenKey can recognise the same ack. */
19
- ackId: string;
20
- resultRef?: string;
21
- resultExpiresAt?: string;
22
- resultEnvelope?: unknown;
23
- /** The grant and profile are saved; only the acknowledgement may remain. */
24
- committed?: true;
25
- }
26
- export type PendingRead = {
27
- kind: 'none';
28
- } | {
29
- kind: 'ok';
30
- pending: PendingGrantRequest;
31
- }
32
- /** The file or its directory has the wrong mode, or the file cannot be read (EACCES, EIO): the record may well be intact. */
33
- | {
34
- kind: 'unreadable';
35
- reason: string;
36
- fix: string;
37
- }
38
- /** The file is readable and is not a saved request. */
39
- | {
40
- kind: 'corrupt';
41
- reason: string;
42
- };
43
- export declare function readPending(): PendingRead;
44
- /**
45
- * Stops (without changing anything) when a saved approval request exists but cannot be read. Call it before a writer changes any
46
- * other state, so a key, grant or profile is never replaced while the approval it belongs to is unreachable.
47
- */
48
- export declare function assertPendingReadable(): void;
2
+ import { type PendingGrantRequest } from './approval-phase.js';
49
3
  /**
50
- * The saved request for READ-ONLY and cleanup callers. A record that cannot be read is reported on stderr and treated as no record, so
51
- * it never fails a command that already installed a grant or blocks `--rotate-key`. Callers that WRITE the record use
52
- * `loadPendingForWrite`, which never lets an unreadable record be overwritten.
4
+ * The OpenKey short-link approval flow (the same one `tc onboard` uses): `init --request` creates a sealed approval
5
+ * request at the links API and saves it here; `init --wait` polls OpenKey for the result with the agent key's signed
6
+ * proof, installs the grant, and only then acknowledges it. Every step is resumable from `approval-pending.json`.
53
7
  */
54
- export declare function loadPending(): PendingGrantRequest | undefined;
55
- export declare function clearPending(): void;
56
- /** The saved request already holds something that cannot be made again: a reference to an approved result, the result itself, or an installed grant. */
57
- export declare const holdsApproval: (pending: PendingGrantRequest) => boolean;
8
+ export declare const DEFAULT_WAIT_MS = 60000;
9
+ export declare const MAX_WAIT_MS: number;
58
10
  /**
59
11
  * `init --request`: the saved request when it is still the same and open, else a new one. A saved request that already holds an
60
12
  * approval (a result reference, the encrypted result, or an installed grant awaiting its acknowledgement) is NEVER replaced:
@@ -1,12 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
2
  import { ackApprovalResult, checkApprovalResultBinding, collectApprovalResult, createApprovalRequest, DEFAULT_LINKS_API_ORIGIN, discoverApprovalResult, generateRelayKey, isApprovalClientError, isLinkError, openRelayResult, signerFromJwk, } from '@tinycloud/share-envelope/links';
5
3
  import { canonicalJson } from './canonical.js';
6
4
  import { cmd } from './program.js';
7
5
  import { CourierError } from './errors.js';
8
6
  import { AGENT_GRANT, AGENT_GRANT_EXPIRY } from './keys.js';
9
- import { profileDir, writeSecure } from './profile.js';
7
+ import { profileDir } from './profile.js';
8
+ import { ackRef, clearPending, holdsApproval, knownResult, loadPendingForWrite, markCommitted, recordResultEnvelope, recordResultRef, resultEnvelopeOf, resultSeen, savePending, stageOf, approvalPhase, } from './approval-phase.js';
10
9
  import { assertBoundToKey, installGrant, isClockRefusal, isPermanentRefusal, parseGrantPayload, resolveOpenKeyApiHost } from './grant.js';
11
10
  import { withRefusalNotice } from './grant-refusal.js';
12
11
  import { bareDid, CLOCK_SKEW_MS, durationMs, siweTimes } from './authority.js';
@@ -15,7 +14,7 @@ import { bareDid, CLOCK_SKEW_MS, durationMs, siweTimes } from './authority.js';
15
14
  * request at the links API and saves it here; `init --wait` polls OpenKey for the result with the agent key's signed
16
15
  * proof, installs the grant, and only then acknowledges it. Every step is resumable from `approval-pending.json`.
17
16
  */
18
- const PENDING_FILE = 'approval-pending.json';
17
+ // The saved request file, its reader, writers and phase live in approval-phase.ts; this module only calls them.
19
18
  export const DEFAULT_WAIT_MS = 60_000;
20
19
  export const MAX_WAIT_MS = 9 * 60_000;
21
20
  /**
@@ -31,100 +30,6 @@ const MAX_SPACE_WAIT_MS = 150_000;
31
30
  const ACK_TIMEOUT_MS = 5_000;
32
31
  /** installGrant failures that no retry can fix: the approved result itself is wrong, so the request is dropped. */
33
32
  const REFUSED_CODES = { 'bad-grant': true, 'grant-too-broad': true, 'grant-incomplete': true, 'grant-expired': true, 'grant-too-long': true };
34
- export function readPending() {
35
- const file = path.join(profileDir(), PENDING_FILE);
36
- if (!fs.existsSync(file))
37
- return { kind: 'none' };
38
- const dirMode = fs.statSync(profileDir()).mode & 0o777;
39
- if ((dirMode & 0o077) !== 0) {
40
- return { kind: 'unreadable', reason: `${profileDir()} has mode ${dirMode.toString(8)}`, fix: `chmod 700 ${profileDir()}` };
41
- }
42
- const mode = fs.statSync(file).mode & 0o777;
43
- if ((mode & 0o077) !== 0)
44
- return { kind: 'unreadable', reason: `${file} has mode ${mode.toString(8)}, not 600`, fix: `chmod 600 ${file}` };
45
- let text;
46
- try {
47
- text = fs.readFileSync(file, 'utf8');
48
- }
49
- catch (error) {
50
- return { kind: 'unreadable', reason: `${file} cannot be read (${error.message})`, fix: `chmod 600 ${file}` };
51
- }
52
- let pending;
53
- try {
54
- pending = JSON.parse(text);
55
- }
56
- catch (error) {
57
- return { kind: 'corrupt', reason: error.message };
58
- }
59
- if (pending && typeof pending.version === 'number' && pending.version > 1) {
60
- // Written by a newer Courier: not corrupt, and not ours to set aside. Use that Courier, or move the file away by hand.
61
- return { kind: 'unreadable', reason: `${file} was written by a newer Courier (record version ${pending.version})`, fix: `mv ${file} ${file}.newer` };
62
- }
63
- if (!pending || pending.version !== 1 || !pending.request || typeof pending.requestId !== 'string' || typeof pending.bodyCid !== 'string'
64
- || typeof pending.approvalUrl !== 'string' || typeof pending.relayPrivateKey !== 'string' || typeof pending.ackId !== 'string') {
65
- return { kind: 'corrupt', reason: 'unexpected contents' };
66
- }
67
- return { kind: 'ok', pending };
68
- }
69
- /**
70
- * Stops (without changing anything) when a saved approval request exists but cannot be read. Call it before a writer changes any
71
- * other state, so a key, grant or profile is never replaced while the approval it belongs to is unreachable.
72
- */
73
- export function assertPendingReadable() {
74
- const read = readPending();
75
- if (read.kind === 'unreadable')
76
- throw unreadableError(read);
77
- }
78
- function unreadableError(read) {
79
- return new CourierError('approval-unreadable', `The saved approval request cannot be used: ${read.reason}. It may hold an approval the owner already gave, so it was not touched and nothing else was changed. Fix it with \`${read.fix}\`, then run \`${cmd('init --wait')}\` again.`);
80
- }
81
- /**
82
- * The saved request for READ-ONLY and cleanup callers. A record that cannot be read is reported on stderr and treated as no record, so
83
- * it never fails a command that already installed a grant or blocks `--rotate-key`. Callers that WRITE the record use
84
- * `loadPendingForWrite`, which never lets an unreadable record be overwritten.
85
- */
86
- export function loadPending() {
87
- const read = readPending();
88
- if (read.kind === 'ok')
89
- return read.pending;
90
- if (read.kind !== 'none') {
91
- console.error(`warning: ignoring the saved approval request ${PENDING_FILE} in ${profileDir()} (${read.reason}).`);
92
- }
93
- return undefined;
94
- }
95
- /**
96
- * The saved request for a caller that is about to write one. A record that exists but cannot be read stops the command with the file
97
- * named and how to fix it: the approval in it may be intact (it comes back after the fix). Only a record that is readable and corrupt
98
- * is set aside, as `approval-pending.json.corrupt-<time>` (0600), and the command goes on without it.
99
- */
100
- function loadPendingForWrite() {
101
- const read = readPending();
102
- if (read.kind === 'ok')
103
- return read.pending;
104
- if (read.kind === 'unreadable') {
105
- throw unreadableError(read);
106
- }
107
- if (read.kind === 'corrupt') {
108
- const file = path.join(profileDir(), PENDING_FILE);
109
- const aside = `${file}.corrupt-${new Date().toISOString().replace(/[:.]/g, '-')}`;
110
- fs.renameSync(file, aside);
111
- fs.chmodSync(aside, 0o600);
112
- console.error(`warning: ${PENDING_FILE} is corrupt (${read.reason}); it was moved to ${aside} and will be replaced by a new request.`);
113
- }
114
- return undefined;
115
- }
116
- function savePending(pending) {
117
- writeSecure(path.join(profileDir(), PENDING_FILE), pending);
118
- }
119
- export function clearPending() {
120
- try {
121
- fs.unlinkSync(path.join(profileDir(), PENDING_FILE));
122
- }
123
- catch (error) {
124
- if (error.code !== 'ENOENT')
125
- throw error;
126
- }
127
- }
128
33
  function configuredOrigin(value, label) {
129
34
  let url;
130
35
  try {
@@ -178,8 +83,6 @@ function matches(pending, profile, want) {
178
83
  && (!want.explicitLifetime || grantLifetimeSeconds === want.lifetime)
179
84
  && pending.openkeyOrigin === want.openkeyOrigin && pending.apiOrigin === want.apiOrigin && pending.linkApiOrigin === want.linkApiOrigin;
180
85
  }
181
- /** The saved request already holds something that cannot be made again: a reference to an approved result, the result itself, or an installed grant. */
182
- export const holdsApproval = (pending) => pending.resultRef !== undefined || pending.resultEnvelope !== undefined || pending.committed === true;
183
86
  /**
184
87
  * OpenKey keeps an approved result for this long after the approval: production APPROVAL_RESULT_RETENTION_SECONDS=600,
185
88
  * docker-compose.prod.yml:28 (OpenKey repo). The constant follows the DEPLOYED value.
@@ -188,7 +91,10 @@ const RESULT_RETENTION_MS = 600_000;
188
91
  /** Clock difference tolerated between this machine and OpenKey when judging the end of that window. */
189
92
  const EXPIRY_SKEW_MS = 60_000;
190
93
  /** The request's own expiry has passed and nothing was ever collected for it. */
191
- const requestLapsed = (pending) => !holdsApproval(pending) && Date.parse(pending.requestExpiresAt) <= Date.now();
94
+ const requestLapsed = (pending) => {
95
+ const phase = approvalPhase({ kind: 'ok', pending });
96
+ return phase.phase === 'open' && phase.lapsed;
97
+ };
192
98
  /**
193
99
  * A lapsed, unapproved request may still have been approved just before it expired: OpenKey serves the result for
194
100
  * RESULT_RETENTION_MS after the approval. So it is asked ONCE (the normal discover) before the record is dropped:
@@ -203,8 +109,7 @@ async function settleLapsed(pending, key) {
203
109
  const found = await discoverApprovalResult(requesterState(pending, key));
204
110
  if (found.status === 'pending')
205
111
  return 'open';
206
- pending.resultRef = found.resultRef;
207
- pending.resultExpiresAt = found.expiresAt;
112
+ recordResultRef(pending, found.resultRef, found.expiresAt);
208
113
  savePending(pending);
209
114
  return 'approved';
210
115
  }
@@ -324,7 +229,7 @@ function requesterState(pending, key) {
324
229
  async function verifiedPayload(profile, pending, key) {
325
230
  let delegationCid;
326
231
  try {
327
- const opened = await openRelayResult(pending.resultEnvelope, pending.requestId, pending.relayPrivateKey);
232
+ const opened = await openRelayResult(resultEnvelopeOf(pending), pending.requestId, pending.relayPrivateKey);
328
233
  const claimedCid = opened.delegation?.delegationCid;
329
234
  delegationCid = typeof claimedCid === 'string' && claimedCid !== '' ? claimedCid : undefined;
330
235
  const checked = checkApprovalResultBinding(opened, { requestId: pending.requestId, bodyCid: pending.bodyCid, request: pending.request });
@@ -354,12 +259,13 @@ async function verifiedPayload(profile, pending, key) {
354
259
  }
355
260
  /** Acknowledge a saved grant. A failure is reported, never fatal: the grant is installed and the saved request keeps the ack to retry. */
356
261
  async function acknowledge(pending, key) {
357
- if (!pending.resultRef) {
262
+ const ref = ackRef(pending);
263
+ if (!ref) {
358
264
  clearPending();
359
265
  return;
360
266
  }
361
267
  try {
362
- await ackApprovalResult(requesterState(pending, key), pending.resultRef, pending.ackId, { timeoutMs: ACK_TIMEOUT_MS });
268
+ await ackApprovalResult(requesterState(pending, key), ref, pending.ackId, { timeoutMs: ACK_TIMEOUT_MS });
363
269
  clearPending();
364
270
  }
365
271
  catch (error) {
@@ -380,16 +286,17 @@ async function acknowledge(pending, key) {
380
286
  * are durably saved; every earlier step is resumable from the saved request alone.
381
287
  */
382
288
  export async function completeApproval(profile, key, pending, options) {
383
- if (!pending.committed) {
384
- if (pending.resultEnvelope === undefined) {
289
+ const stage = stageOf(pending);
290
+ if (stage !== 'committed') {
291
+ if (stage !== 'result') {
292
+ const known = knownResult(pending);
385
293
  let collected;
386
294
  try {
387
295
  collected = await collectApprovalResult(requesterState(pending, key), {
388
296
  deadline: options.deadline,
389
- ...(pending.resultRef && pending.resultExpiresAt ? { known: { resultRef: pending.resultRef, expiresAt: pending.resultExpiresAt } } : {}),
297
+ ...(known ? { known } : {}),
390
298
  onDiscovered: (found) => {
391
- pending.resultRef = found.resultRef;
392
- pending.resultExpiresAt = found.expiresAt;
299
+ recordResultRef(pending, found.resultRef, found.expiresAt);
393
300
  savePending(pending);
394
301
  },
395
302
  });
@@ -397,11 +304,11 @@ export async function completeApproval(profile, key, pending, options) {
397
304
  catch (error) {
398
305
  if (!isApprovalClientError(error))
399
306
  throw error;
400
- throw collectFailure(error, pending.resultRef !== undefined);
307
+ throw collectFailure(error, resultSeen(pending));
401
308
  }
402
309
  if (collected.status === 'deadline')
403
310
  return 'deadline';
404
- pending.resultEnvelope = collected.relay;
311
+ recordResultEnvelope(pending, collected.relay);
405
312
  savePending(pending); // the encrypted result is on disk before any install work
406
313
  }
407
314
  const payload = await verifiedPayload(profile, pending, key);
@@ -423,7 +330,7 @@ export async function completeApproval(profile, key, pending, options) {
423
330
  throw withRefusalNotice(error, payload.delegationCid);
424
331
  }
425
332
  }
426
- pending.committed = true;
333
+ markCommitted(pending);
427
334
  savePending(pending);
428
335
  await acknowledge(pending, key);
429
336
  if (spaceError)