@tinycloud/courier 0.5.1-beta.1 → 0.5.1-beta.3
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/dist/approval-phase.d.ts +125 -0
- package/dist/approval-phase.js +175 -0
- package/dist/approval-phase.js.map +1 -0
- package/dist/commands.js +66 -35
- package/dist/commands.js.map +1 -1
- package/dist/did-key.d.ts +6 -0
- package/dist/did-key.js +36 -0
- package/dist/did-key.js.map +1 -0
- package/dist/grant-request.d.ts +6 -54
- package/dist/grant-request.js +21 -114
- package/dist/grant-request.js.map +1 -1
- package/dist/pending.d.ts +4 -0
- package/dist/pending.js +20 -1
- package/dist/pending.js.map +1 -1
- package/dist/profile.d.ts +9 -0
- package/dist/profile.js +35 -4
- package/dist/profile.js.map +1 -1
- package/dist/state.d.ts +45 -0
- package/dist/state.js +516 -0
- package/dist/state.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { LinkRecord, OpenKeyApprovalRequestV1 } from '@tinycloud/share-envelope/links';
|
|
2
|
+
/**
|
|
3
|
+
* The one module that reads, writes and classifies the saved OpenKey approval request, `approval-pending.json`.
|
|
4
|
+
*
|
|
5
|
+
* `readApprovalEntry` is the only reader. `approvalPhase` is the only classifier: every other module asks it (or `stageOf`, the same rule
|
|
6
|
+
* on one record) what the request is, and never looks at the optional fields itself. The record's schema is unchanged and so is the
|
|
7
|
+
* meaning of every phase: this names what the code already did.
|
|
8
|
+
*
|
|
9
|
+
* Phases: `none` (no file), `open` (a request nobody has answered; `lapsed` is the request's own expiry having passed), `held` (the
|
|
10
|
+
* owner approved: stage `ref` = OpenKey holds a result we have only a reference to, `result` = the encrypted result is saved here,
|
|
11
|
+
* `committed` = the grant is installed and only the acknowledgement may remain), `unreadable` (the file or its directory has the wrong
|
|
12
|
+
* mode, or cannot be read: the approval in it may be intact), `corrupt` (readable, not a request) and `orphaned` (a live request whose
|
|
13
|
+
* profile or agent key is missing: it is never dropped on its own).
|
|
14
|
+
*/
|
|
15
|
+
export declare const APPROVAL_FILE = "approval-pending.json";
|
|
16
|
+
/** The durable record of one approval request. The result stays encrypted at rest under `relayPrivateKey`. */
|
|
17
|
+
export interface PendingGrantRequest {
|
|
18
|
+
version: 1;
|
|
19
|
+
request: OpenKeyApprovalRequestV1;
|
|
20
|
+
requestId: string;
|
|
21
|
+
bodyCid: string;
|
|
22
|
+
approvalUrl: string;
|
|
23
|
+
requestExpiresAt: string;
|
|
24
|
+
record: LinkRecord;
|
|
25
|
+
apiOrigin: string;
|
|
26
|
+
linkApiOrigin: string;
|
|
27
|
+
openkeyOrigin: string;
|
|
28
|
+
relayPrivateKey: string;
|
|
29
|
+
/** Sent with the acknowledgement; stable across retries so OpenKey can recognise the same ack. */
|
|
30
|
+
ackId: string;
|
|
31
|
+
resultRef?: string;
|
|
32
|
+
resultExpiresAt?: string;
|
|
33
|
+
resultEnvelope?: unknown;
|
|
34
|
+
/** The grant and profile are saved; only the acknowledgement may remain. */
|
|
35
|
+
committed?: true;
|
|
36
|
+
}
|
|
37
|
+
export type ApprovalEntry = {
|
|
38
|
+
kind: 'none';
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'ok';
|
|
41
|
+
pending: PendingGrantRequest;
|
|
42
|
+
}
|
|
43
|
+
/** The file or its directory has the wrong mode, or the file cannot be read (EACCES, EIO): the record may well be intact. */
|
|
44
|
+
| {
|
|
45
|
+
kind: 'unreadable';
|
|
46
|
+
reason: string;
|
|
47
|
+
fix: string;
|
|
48
|
+
}
|
|
49
|
+
/** The file is readable and is not a saved request. */
|
|
50
|
+
| {
|
|
51
|
+
kind: 'corrupt';
|
|
52
|
+
reason: string;
|
|
53
|
+
};
|
|
54
|
+
export type HeldStage = 'ref' | 'result' | 'committed';
|
|
55
|
+
export type LivePhase = {
|
|
56
|
+
phase: 'open';
|
|
57
|
+
lapsed: boolean;
|
|
58
|
+
} | {
|
|
59
|
+
phase: 'held';
|
|
60
|
+
stage: HeldStage;
|
|
61
|
+
} | {
|
|
62
|
+
phase: 'unreadable';
|
|
63
|
+
reason: string;
|
|
64
|
+
fix: string;
|
|
65
|
+
};
|
|
66
|
+
export type ApprovalPhase = {
|
|
67
|
+
phase: 'none';
|
|
68
|
+
} | {
|
|
69
|
+
phase: 'corrupt';
|
|
70
|
+
reason: string;
|
|
71
|
+
} | LivePhase | {
|
|
72
|
+
phase: 'orphaned';
|
|
73
|
+
was: LivePhase;
|
|
74
|
+
};
|
|
75
|
+
/** The reader. Reads only: it never removes, repairs or rewrites the file. */
|
|
76
|
+
export declare function readApprovalEntry(): ApprovalEntry;
|
|
77
|
+
/** How far a held request has got: committed outranks a collected result, which outranks a reference. Undefined: nothing held. */
|
|
78
|
+
export declare function stageOf(pending: PendingGrantRequest): HeldStage | undefined;
|
|
79
|
+
/** The request already holds something that cannot be made again: a result reference, the result itself, or an installed grant. */
|
|
80
|
+
export declare const holdsApproval: (pending: PendingGrantRequest) => boolean;
|
|
81
|
+
/** A saved request has no owner to belong to when the profile or the agent key is missing. */
|
|
82
|
+
export declare const lacksOwner: (presence: {
|
|
83
|
+
profile: boolean;
|
|
84
|
+
key: boolean;
|
|
85
|
+
}) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The classifier. `presence` says whether the profile and the agent key exist (default: both do). The request's own expiry only matters
|
|
88
|
+
* while nothing is held, and is judged against `now`, exactly as before.
|
|
89
|
+
*/
|
|
90
|
+
export declare function approvalPhase(entry: ApprovalEntry, presence?: {
|
|
91
|
+
profile: boolean;
|
|
92
|
+
key: boolean;
|
|
93
|
+
}, now?: number): ApprovalPhase;
|
|
94
|
+
/** The saved request when it can be read, else undefined (with a warning when a file is there but unusable). For read-only and cleanup callers. */
|
|
95
|
+
export declare function loadPending(): PendingGrantRequest | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Stops (without changing anything) when a saved approval request exists but cannot be read. Call it before a writer changes any
|
|
98
|
+
* other state, so a key, grant or profile is never replaced while the approval it belongs to is unreachable.
|
|
99
|
+
*/
|
|
100
|
+
export declare function assertPendingReadable(): void;
|
|
101
|
+
/**
|
|
102
|
+
* 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
|
|
103
|
+
* 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
|
|
104
|
+
* is set aside, as `approval-pending.json.corrupt-<time>` (0600), and the command goes on without it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function loadPendingForWrite(): PendingGrantRequest | undefined;
|
|
107
|
+
export declare function savePending(pending: PendingGrantRequest): void;
|
|
108
|
+
export declare function clearPending(): void;
|
|
109
|
+
/** OpenKey holds a result for this request: remember where, and until when. */
|
|
110
|
+
export declare function recordResultRef(pending: PendingGrantRequest, resultRef: string, expiresAt: string): void;
|
|
111
|
+
/** The encrypted result was collected and is about to be saved. */
|
|
112
|
+
export declare function recordResultEnvelope(pending: PendingGrantRequest, relay: unknown): void;
|
|
113
|
+
/** The grant and profile are saved; only the acknowledgement may remain. */
|
|
114
|
+
export declare function markCommitted(pending: PendingGrantRequest): void;
|
|
115
|
+
/** The reference and expiry of a result already discovered, for a collection that should not ask again. */
|
|
116
|
+
export declare function knownResult(pending: PendingGrantRequest): {
|
|
117
|
+
resultRef: string;
|
|
118
|
+
expiresAt: string;
|
|
119
|
+
} | undefined;
|
|
120
|
+
/** The reference an acknowledgement is sent for; undefined when there is none to acknowledge. */
|
|
121
|
+
export declare function ackRef(pending: PendingGrantRequest): string | undefined;
|
|
122
|
+
/** The collected, still encrypted result. */
|
|
123
|
+
export declare function resultEnvelopeOf(pending: PendingGrantRequest): unknown;
|
|
124
|
+
/** Whether a failing collection already had its result reference saved (so the failing call was the fetch). */
|
|
125
|
+
export declare function resultSeen(pending: PendingGrantRequest): boolean;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cmd } from './program.js';
|
|
4
|
+
import { CourierError } from './errors.js';
|
|
5
|
+
import { profileDir, writeSecure } from './profile.js';
|
|
6
|
+
/**
|
|
7
|
+
* The one module that reads, writes and classifies the saved OpenKey approval request, `approval-pending.json`.
|
|
8
|
+
*
|
|
9
|
+
* `readApprovalEntry` is the only reader. `approvalPhase` is the only classifier: every other module asks it (or `stageOf`, the same rule
|
|
10
|
+
* on one record) what the request is, and never looks at the optional fields itself. The record's schema is unchanged and so is the
|
|
11
|
+
* meaning of every phase: this names what the code already did.
|
|
12
|
+
*
|
|
13
|
+
* Phases: `none` (no file), `open` (a request nobody has answered; `lapsed` is the request's own expiry having passed), `held` (the
|
|
14
|
+
* owner approved: stage `ref` = OpenKey holds a result we have only a reference to, `result` = the encrypted result is saved here,
|
|
15
|
+
* `committed` = the grant is installed and only the acknowledgement may remain), `unreadable` (the file or its directory has the wrong
|
|
16
|
+
* mode, or cannot be read: the approval in it may be intact), `corrupt` (readable, not a request) and `orphaned` (a live request whose
|
|
17
|
+
* profile or agent key is missing: it is never dropped on its own).
|
|
18
|
+
*/
|
|
19
|
+
export const APPROVAL_FILE = 'approval-pending.json';
|
|
20
|
+
/** The reader. Reads only: it never removes, repairs or rewrites the file. */
|
|
21
|
+
export function readApprovalEntry() {
|
|
22
|
+
const file = path.join(profileDir(), APPROVAL_FILE);
|
|
23
|
+
if (!fs.existsSync(file))
|
|
24
|
+
return { kind: 'none' };
|
|
25
|
+
const dirMode = fs.statSync(profileDir()).mode & 0o777;
|
|
26
|
+
if ((dirMode & 0o077) !== 0) {
|
|
27
|
+
return { kind: 'unreadable', reason: `${profileDir()} has mode ${dirMode.toString(8)}`, fix: `chmod 700 ${profileDir()}` };
|
|
28
|
+
}
|
|
29
|
+
const mode = fs.statSync(file).mode & 0o777;
|
|
30
|
+
if ((mode & 0o077) !== 0)
|
|
31
|
+
return { kind: 'unreadable', reason: `${file} has mode ${mode.toString(8)}, not 600`, fix: `chmod 600 ${file}` };
|
|
32
|
+
let text;
|
|
33
|
+
try {
|
|
34
|
+
text = fs.readFileSync(file, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return { kind: 'unreadable', reason: `${file} cannot be read (${error.code ?? 'read error'})`, fix: `chmod 600 ${file}` };
|
|
38
|
+
}
|
|
39
|
+
let pending;
|
|
40
|
+
// The parser's message quotes the text around the failure, and this file holds `relayPrivateKey`: the reason is fixed, never the message.
|
|
41
|
+
try {
|
|
42
|
+
pending = JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { kind: 'corrupt', reason: 'the file is not valid JSON' };
|
|
46
|
+
}
|
|
47
|
+
if (pending && typeof pending.version === 'number' && pending.version > 1) {
|
|
48
|
+
// Written by a newer Courier: not corrupt, and not ours to set aside. Use that Courier, or move the file away by hand.
|
|
49
|
+
return { kind: 'unreadable', reason: `${file} was written by a newer Courier (record version ${pending.version})`, fix: `mv ${file} ${file}.newer` };
|
|
50
|
+
}
|
|
51
|
+
if (!pending || pending.version !== 1 || !pending.request || typeof pending.requestId !== 'string' || typeof pending.bodyCid !== 'string'
|
|
52
|
+
|| typeof pending.approvalUrl !== 'string' || typeof pending.relayPrivateKey !== 'string' || typeof pending.ackId !== 'string') {
|
|
53
|
+
return { kind: 'corrupt', reason: 'unexpected contents' };
|
|
54
|
+
}
|
|
55
|
+
return { kind: 'ok', pending };
|
|
56
|
+
}
|
|
57
|
+
/** How far a held request has got: committed outranks a collected result, which outranks a reference. Undefined: nothing held. */
|
|
58
|
+
export function stageOf(pending) {
|
|
59
|
+
if (pending.committed === true)
|
|
60
|
+
return 'committed';
|
|
61
|
+
if (pending.resultEnvelope !== undefined)
|
|
62
|
+
return 'result';
|
|
63
|
+
if (pending.resultRef !== undefined)
|
|
64
|
+
return 'ref';
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/** The request already holds something that cannot be made again: a result reference, the result itself, or an installed grant. */
|
|
68
|
+
export const holdsApproval = (pending) => stageOf(pending) !== undefined;
|
|
69
|
+
/** A saved request has no owner to belong to when the profile or the agent key is missing. */
|
|
70
|
+
export const lacksOwner = (presence) => !presence.profile || !presence.key;
|
|
71
|
+
/**
|
|
72
|
+
* The classifier. `presence` says whether the profile and the agent key exist (default: both do). The request's own expiry only matters
|
|
73
|
+
* while nothing is held, and is judged against `now`, exactly as before.
|
|
74
|
+
*/
|
|
75
|
+
export function approvalPhase(entry, presence = { profile: true, key: true }, now = Date.now()) {
|
|
76
|
+
if (entry.kind === 'none')
|
|
77
|
+
return { phase: 'none' };
|
|
78
|
+
if (entry.kind === 'corrupt')
|
|
79
|
+
return { phase: 'corrupt', reason: entry.reason };
|
|
80
|
+
let live;
|
|
81
|
+
if (entry.kind === 'unreadable') {
|
|
82
|
+
live = { phase: 'unreadable', reason: entry.reason, fix: entry.fix };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const stage = stageOf(entry.pending);
|
|
86
|
+
live = stage ? { phase: 'held', stage } : { phase: 'open', lapsed: Date.parse(entry.pending.requestExpiresAt) <= now };
|
|
87
|
+
}
|
|
88
|
+
return lacksOwner(presence) ? { phase: 'orphaned', was: live } : live;
|
|
89
|
+
}
|
|
90
|
+
/** The saved request when it can be read, else undefined (with a warning when a file is there but unusable). For read-only and cleanup callers. */
|
|
91
|
+
export function loadPending() {
|
|
92
|
+
const entry = readApprovalEntry();
|
|
93
|
+
if (entry.kind === 'ok')
|
|
94
|
+
return entry.pending;
|
|
95
|
+
if (entry.kind !== 'none') {
|
|
96
|
+
console.error(`warning: ignoring the saved approval request ${APPROVAL_FILE} in ${profileDir()} (${entry.reason}).`);
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
function unreadableError(entry) {
|
|
101
|
+
return new CourierError('approval-unreadable', `The saved approval request cannot be used: ${entry.reason}. It may hold an approval the owner already gave, so it was not touched and nothing else was changed. Fix it with \`${entry.fix}\`, then run \`${cmd('init --wait')}\` again.`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Stops (without changing anything) when a saved approval request exists but cannot be read. Call it before a writer changes any
|
|
105
|
+
* other state, so a key, grant or profile is never replaced while the approval it belongs to is unreachable.
|
|
106
|
+
*/
|
|
107
|
+
export function assertPendingReadable() {
|
|
108
|
+
const entry = readApprovalEntry();
|
|
109
|
+
if (entry.kind === 'unreadable')
|
|
110
|
+
throw unreadableError(entry);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 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
|
|
114
|
+
* 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
|
|
115
|
+
* is set aside, as `approval-pending.json.corrupt-<time>` (0600), and the command goes on without it.
|
|
116
|
+
*/
|
|
117
|
+
export function loadPendingForWrite() {
|
|
118
|
+
const entry = readApprovalEntry();
|
|
119
|
+
if (entry.kind === 'ok')
|
|
120
|
+
return entry.pending;
|
|
121
|
+
if (entry.kind === 'unreadable') {
|
|
122
|
+
throw unreadableError(entry);
|
|
123
|
+
}
|
|
124
|
+
if (entry.kind === 'corrupt') {
|
|
125
|
+
const file = path.join(profileDir(), APPROVAL_FILE);
|
|
126
|
+
const aside = `${file}.corrupt-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
127
|
+
fs.renameSync(file, aside);
|
|
128
|
+
fs.chmodSync(aside, 0o600);
|
|
129
|
+
console.error(`warning: ${APPROVAL_FILE} is corrupt (${entry.reason}); it was moved to ${aside} and will be replaced by a new request.`);
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
export function savePending(pending) {
|
|
134
|
+
writeSecure(path.join(profileDir(), APPROVAL_FILE), pending);
|
|
135
|
+
}
|
|
136
|
+
export function clearPending() {
|
|
137
|
+
try {
|
|
138
|
+
fs.unlinkSync(path.join(profileDir(), APPROVAL_FILE));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error.code !== 'ENOENT')
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ---- the record's optional fields: only this module reads or sets them ----
|
|
146
|
+
/** OpenKey holds a result for this request: remember where, and until when. */
|
|
147
|
+
export function recordResultRef(pending, resultRef, expiresAt) {
|
|
148
|
+
pending.resultRef = resultRef;
|
|
149
|
+
pending.resultExpiresAt = expiresAt;
|
|
150
|
+
}
|
|
151
|
+
/** The encrypted result was collected and is about to be saved. */
|
|
152
|
+
export function recordResultEnvelope(pending, relay) {
|
|
153
|
+
pending.resultEnvelope = relay;
|
|
154
|
+
}
|
|
155
|
+
/** The grant and profile are saved; only the acknowledgement may remain. */
|
|
156
|
+
export function markCommitted(pending) {
|
|
157
|
+
pending.committed = true;
|
|
158
|
+
}
|
|
159
|
+
/** The reference and expiry of a result already discovered, for a collection that should not ask again. */
|
|
160
|
+
export function knownResult(pending) {
|
|
161
|
+
return pending.resultRef && pending.resultExpiresAt ? { resultRef: pending.resultRef, expiresAt: pending.resultExpiresAt } : undefined;
|
|
162
|
+
}
|
|
163
|
+
/** The reference an acknowledgement is sent for; undefined when there is none to acknowledge. */
|
|
164
|
+
export function ackRef(pending) {
|
|
165
|
+
return pending.resultRef || undefined;
|
|
166
|
+
}
|
|
167
|
+
/** The collected, still encrypted result. */
|
|
168
|
+
export function resultEnvelopeOf(pending) {
|
|
169
|
+
return pending.resultEnvelope;
|
|
170
|
+
}
|
|
171
|
+
/** Whether a failing collection already had its result reference saved (so the failing call was the fetch). */
|
|
172
|
+
export function resultSeen(pending) {
|
|
173
|
+
return pending.resultRef !== undefined;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=approval-phase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-phase.js","sourceRoot":"","sources":["../src/approval-phase.ts"],"names":[],"mappings":"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"}
|
package/dist/commands.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adviceFor, deriveState, heldApproval, recoverInterrupted, rotateKey } from './state.js';
|
|
1
2
|
import { recentNotices } from './notify.js';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
@@ -10,7 +11,8 @@ import { AGENT_GRANT, AGENT_GRANT_EXPIRY, COURIER_SPACE, DEFAULT_HOST, EXT, k, p
|
|
|
10
11
|
import { loadProfile, saveProfile, getPeer, profileDir, profileExists, loadKey, saveKey, } from './profile.js';
|
|
11
12
|
import { courierNode, peerKv } from './node.js';
|
|
12
13
|
import { courierGrantRequest, grantFromInput, installRefusing, isShortCode, noSpaceError, pendingSpaceError, receiveGrant, resumeAwaitingGrant, resumeGrant } from './grant.js';
|
|
13
|
-
import {
|
|
14
|
+
import { completeApproval, DEFAULT_WAIT_MS, MAX_WAIT_MS, requestApproval, waitApproval } from './grant-request.js';
|
|
15
|
+
import { approvalPhase, assertPendingReadable, clearPending, holdsApproval, loadPending, readApprovalEntry, stageOf } from './approval-phase.js';
|
|
14
16
|
import { artifactA, artifactWithDelegation, parseArtifact, peerRecordBody, PEER_DELEGATION_EXPIRY, validateArtifactIdentity, } from './pairing.js';
|
|
15
17
|
import { authRequiredTask, buildMessage, buildTask, COUNTERPARTY_NOTICE, isRecord, messageView, oneLine, safeText, messageRole, parseTaskState, taskView, INTENTS, } from './a2a.js';
|
|
16
18
|
import { commitReceipts, collectInbound, renderInboxText, writeOut } from './inbox.js';
|
|
@@ -96,12 +98,41 @@ export async function cmdInit(args) {
|
|
|
96
98
|
const grantRaw = grantFile === undefined || args.has('request')
|
|
97
99
|
? undefined
|
|
98
100
|
: grantFile === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(grantFile, 'utf8');
|
|
101
|
+
// Explicit recovery before any mode acts: finish an interrupted rotation, remove stray key-bearing temp files; then derive once.
|
|
102
|
+
recoverInterrupted();
|
|
103
|
+
const state = deriveState();
|
|
104
|
+
for (const note of state.notes)
|
|
105
|
+
console.error(`warning: ${note}`);
|
|
106
|
+
// A profile.json that exists and cannot be read is never treated as absent: `loadProfile` refuses with its own message (an unreadable
|
|
107
|
+
// profile is replaced only by an explicit `--rotate-key`).
|
|
108
|
+
if (state.action === 'fix-profile' && !args.has('rotate-key')) {
|
|
109
|
+
loadProfile();
|
|
110
|
+
fail('bad-profile', adviceFor(state));
|
|
111
|
+
}
|
|
112
|
+
// A saved approval without its profile or key is left as it is; `--rotate-key` starts over with a new key.
|
|
113
|
+
if (state.action === 'refuse' && !args.has('rotate-key'))
|
|
114
|
+
fail('approval-orphaned', adviceFor(state));
|
|
115
|
+
// A grant.json that is not owner-only is a saved grant that cannot be used yet: it is neither replaced nor counted as missing. A rotation
|
|
116
|
+
// discards the grant by design, so it goes on.
|
|
117
|
+
if (state.grantMode && !args.has('rotate-key')) {
|
|
118
|
+
const file = path.join(profileDir(), 'grant.json');
|
|
119
|
+
fail('profile-permissions', `${file} must be owner-only (mode 0600); found mode ${state.grantMode}. It holds a saved grant, so nothing was changed. Run \`chmod 600 ${file}\`, then run the command again.`);
|
|
120
|
+
}
|
|
121
|
+
const HELD_APPROVAL_KEPT = `The saved approval request holds an approval from the owner and was kept: run \`${cmd('init --wait')}\` to collect or acknowledge it.`;
|
|
99
122
|
if (args.has('resume')) {
|
|
100
123
|
if (!profileExists())
|
|
101
124
|
fail('no-grant', `No Courier profile to resume. Run \`${cmd('init')}\` and approve a grant first.`);
|
|
102
125
|
const profile = loadProfile();
|
|
126
|
+
// An approval the owner gave, with no grant saved to install first, is collected by `--wait`, never discarded or shadowed by a cleanup.
|
|
127
|
+
if (state.action === 'wait' && heldApproval(state.inputs.approval) && profile.status !== 'ready') {
|
|
128
|
+
console.error(`${adviceFor(state)} Continuing as \`${cmd('init --wait')}\`.`);
|
|
129
|
+
await collectApproval(profile, loadKey(), { expiry: args.get('expiry'), openkeyApiHost: apiHostFlag, waitMs });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
103
132
|
if (profile.status === 'awaiting-grant' && await resumeAwaitingGrant(profile)) {
|
|
104
133
|
dropOpenRequest();
|
|
134
|
+
if (heldApproval(state.inputs.approval))
|
|
135
|
+
console.error(HELD_APPROVAL_KEPT);
|
|
105
136
|
await finishInit(profile, profile.host);
|
|
106
137
|
return;
|
|
107
138
|
}
|
|
@@ -109,22 +140,20 @@ export async function cmdInit(args) {
|
|
|
109
140
|
fail('usage', `\`${cmd('init --resume')}\` requires a profile in awaiting-space state.`);
|
|
110
141
|
await resumeGrant(profile, loadKey());
|
|
111
142
|
dropOpenRequest();
|
|
143
|
+
if (heldApproval(state.inputs.approval))
|
|
144
|
+
console.error(HELD_APPROVAL_KEPT);
|
|
112
145
|
await finishInit(profile, profile.host);
|
|
113
146
|
return;
|
|
114
147
|
}
|
|
115
148
|
const rotate = args.has('rotate-key');
|
|
116
149
|
// A writer must learn that the saved approval is unreadable BEFORE the key, grant or profile is changed.
|
|
117
|
-
|
|
150
|
+
// Also for a plain `--rotate-key`: an approval that cannot be read may be bound to the old key, and a rotation would leave it there.
|
|
151
|
+
if (args.has('request') || args.has('wait') || rotate)
|
|
118
152
|
assertPendingReadable();
|
|
119
|
-
// Read before the key is replaced: an approval the owner already gave is bound to the old key and is lost by a rotation.
|
|
120
|
-
const heldApproval = rotate && (() => { const saved = loadPending(); return saved !== undefined && holdsApproval(saved); })();
|
|
121
153
|
const prior = profileExists() && !rotate ? loadProfile() : undefined;
|
|
122
|
-
// A renewal is asked for with `--request`; `--wait` only collects.
|
|
123
|
-
if (args.has('wait') &&
|
|
124
|
-
fail('usage',
|
|
125
|
-
? `The grant is saved and the "${COURIER_SPACE}" space is still being set up: run \`${cmd('init --resume')}\`. \`${cmd('init --wait')}\` collects an approval that \`${cmd('init --request')}\` asked for.`
|
|
126
|
-
: `This profile already has a grant and no approval request is pending. To renew it, run \`${cmd('init --request')}\`, send the printed openkeyUrl to the owner, then run \`${cmd('init --wait')}\`.`);
|
|
127
|
-
}
|
|
154
|
+
// A renewal is asked for with `--request`; `--wait` only collects. The derived state says when `--wait` goes on.
|
|
155
|
+
if (args.has('wait') && !rotate && !state.wait)
|
|
156
|
+
fail('usage', adviceFor(state));
|
|
128
157
|
const key = prior ? loadKey() : newAgentKey();
|
|
129
158
|
const host = hostFlag ?? prior?.host ?? process.env.COURIER_HOST ?? DEFAULT_HOST;
|
|
130
159
|
const openkeyHost = openkeyHostFlag ?? prior?.openkeyHost ?? process.env.COURIER_OPENKEY_HOST ?? 'https://openkey.so';
|
|
@@ -143,25 +172,23 @@ export async function cmdInit(args) {
|
|
|
143
172
|
profile.label = labelFlag ?? profile.label;
|
|
144
173
|
if (handleFlag)
|
|
145
174
|
profile.selfHandle = handleFlag;
|
|
175
|
+
profile.courierDid = bareDid(key.did);
|
|
146
176
|
if (rotate) {
|
|
147
177
|
profile.peers = {};
|
|
148
178
|
profile.ownerDid = undefined;
|
|
149
179
|
profile.spaceId = undefined;
|
|
150
180
|
profile.grantExpiresAt = undefined;
|
|
151
181
|
profile.status = 'awaiting-grant';
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
// One journaled step: key, profile, grant and a discarded approval change together, and an interruption is finished by the next init.
|
|
183
|
+
await rotateKey(key, profile);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
saveKey(key);
|
|
187
|
+
saveProfile(profile);
|
|
156
188
|
}
|
|
157
|
-
profile.courierDid = bareDid(key.did);
|
|
158
|
-
saveKey(key);
|
|
159
|
-
saveProfile(profile);
|
|
160
189
|
const request = courierGrantRequest(profile, key, expiry, { openkeyApiHost: apiHostFlag });
|
|
161
190
|
if (rotate)
|
|
162
191
|
console.error('Agent key rotated. Existing peer pairings must be re-established; the old grant remains valid until expiry.');
|
|
163
|
-
if (heldApproval)
|
|
164
|
-
console.error('The saved approval request already held an approval from the owner. It is bound to the old agent key, so this key rotation discards that approval: the owner must approve a new request.');
|
|
165
192
|
if (args.has('request')) {
|
|
166
193
|
const pending = await requestApproval(profile, key, args.get('expiry'), { openkeyApiHost: apiHostFlag });
|
|
167
194
|
console.error(`Send the owner this link to approve (it expires ${pending.requestExpiresAt}). Copy it exactly, never retype it:\n${pending.approvalUrl}\nThen run \`${cmd('init --wait --timeout 60s')}\` to collect the approval.`);
|
|
@@ -171,18 +198,7 @@ export async function cmdInit(args) {
|
|
|
171
198
|
return;
|
|
172
199
|
}
|
|
173
200
|
if (args.has('wait')) {
|
|
174
|
-
|
|
175
|
-
const { pending, resumed } = await waitApproval(profile, key, args.get('expiry'), { openkeyApiHost: apiHostFlag });
|
|
176
|
-
if (!pending.committed) {
|
|
177
|
-
console.error(`${resumed ? 'Resuming the saved approval request.' : 'Created an approval request.'} Owner: approve it in OpenKey (copy the link exactly, never retype it):\n${pending.approvalUrl}\nWaiting for approval (up to ${Math.round(waitMs / 1000)} s). Run \`${cmd('init --wait')}\` again after an interruption; no new approval is needed.`);
|
|
178
|
-
}
|
|
179
|
-
if (await completeApproval(profile, key, pending, { deadline }) === 'deadline') {
|
|
180
|
-
throw new CourierPending({ step: 1, of: 1, approvalUrl: pending.approvalUrl, expiresAt: pending.requestExpiresAt });
|
|
181
|
-
}
|
|
182
|
-
const active = loadProfile();
|
|
183
|
-
if (active.status !== 'ready')
|
|
184
|
-
throw active.spacePending ? pendingSpaceError(active) : noSpaceError(active);
|
|
185
|
-
await finishInit(active, active.host);
|
|
201
|
+
await collectApproval(profile, key, { expiry: args.get('expiry'), openkeyApiHost: apiHostFlag, waitMs });
|
|
186
202
|
return;
|
|
187
203
|
}
|
|
188
204
|
let payload;
|
|
@@ -211,14 +227,14 @@ export async function cmdInit(args) {
|
|
|
211
227
|
*/
|
|
212
228
|
function clearPendingQuietly() {
|
|
213
229
|
try {
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
230
|
+
const phase = approvalPhase(readApprovalEntry());
|
|
231
|
+
if (phase.phase === 'held' && phase.stage === 'committed') {
|
|
216
232
|
console.error('This grant replaces a saved short-link approval that was already installed: its acknowledgement to OpenKey is never sent (harmless; OpenKey discards the result).');
|
|
217
233
|
}
|
|
218
|
-
else if (
|
|
234
|
+
else if (phase.phase === 'held') {
|
|
219
235
|
console.error('This grant replaces the saved short-link approval: discarded a saved approval that was not installed.');
|
|
220
236
|
}
|
|
221
|
-
else if (
|
|
237
|
+
else if (phase.phase === 'unreadable' || phase.phase === 'corrupt') {
|
|
222
238
|
console.error('This grant replaces the saved short-link request: discarded an unreadable saved approval request (it may have held an approval that was not installed).');
|
|
223
239
|
}
|
|
224
240
|
clearPending();
|
|
@@ -237,6 +253,21 @@ function dropOpenRequest() {
|
|
|
237
253
|
if (saved && !holdsApproval(saved))
|
|
238
254
|
clearPendingQuietly();
|
|
239
255
|
}
|
|
256
|
+
/** `init --wait`: create or resume the approval request, wait for the owner, install the result and acknowledge it. `--resume` hands off here while an approval is saved. */
|
|
257
|
+
async function collectApproval(profile, key, options) {
|
|
258
|
+
const deadline = Date.now() + options.waitMs;
|
|
259
|
+
const { pending, resumed } = await waitApproval(profile, key, options.expiry, { openkeyApiHost: options.openkeyApiHost });
|
|
260
|
+
if (stageOf(pending) !== 'committed') {
|
|
261
|
+
console.error(`${resumed ? 'Resuming the saved approval request.' : 'Created an approval request.'} Owner: approve it in OpenKey (copy the link exactly, never retype it):\n${pending.approvalUrl}\nWaiting for approval (up to ${Math.round(options.waitMs / 1000)} s). Run \`${cmd('init --wait')}\` again after an interruption; no new approval is needed.`);
|
|
262
|
+
}
|
|
263
|
+
if (await completeApproval(profile, key, pending, { deadline }) === 'deadline') {
|
|
264
|
+
throw new CourierPending({ step: 1, of: 1, approvalUrl: pending.approvalUrl, expiresAt: pending.requestExpiresAt });
|
|
265
|
+
}
|
|
266
|
+
const active = loadProfile();
|
|
267
|
+
if (active.status !== 'ready')
|
|
268
|
+
throw active.spacePending ? pendingSpaceError(active) : noSpaceError(active);
|
|
269
|
+
await finishInit(active, active.host);
|
|
270
|
+
}
|
|
240
271
|
async function finishInit(profile, host) {
|
|
241
272
|
const active = await ctx();
|
|
242
273
|
for (const peer of Object.values(active.profile.peers)) {
|