@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.
- 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 +84 -38
- 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/file-lock.d.ts +86 -0
- package/dist/file-lock.js +331 -0
- package/dist/file-lock.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/grant.js +2 -2
- package/dist/grant.js.map +1 -1
- package/dist/intake/queue.d.ts +1 -21
- package/dist/intake/queue.js +2 -172
- package/dist/intake/queue.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 +24 -0
- package/dist/profile.js +243 -8
- 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 +1 -1
|
@@ -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';
|
|
@@ -7,10 +8,12 @@ import { NodeWasmBindings, activateValidatedRuntimeDelegation, deserializeDelega
|
|
|
7
8
|
import { cmd } from './program.js';
|
|
8
9
|
import { CourierError, CourierPending, errInfo } from './errors.js';
|
|
9
10
|
import { AGENT_GRANT, AGENT_GRANT_EXPIRY, COURIER_SPACE, DEFAULT_HOST, EXT, k, peerReadPermissions, PREFIX } from './keys.js';
|
|
10
|
-
import { loadProfile, saveProfile, getPeer, profileDir, profileExists, loadKey, saveKey, } from './profile.js';
|
|
11
|
+
import { loadProfile, saveProfile, assertNotPaired, getPeer, profileDir, profileExists, loadKey, saveKey, } from './profile.js';
|
|
11
12
|
import { courierNode, peerKv } from './node.js';
|
|
13
|
+
import { REVOKE_URL } from './grant-refusal.js';
|
|
12
14
|
import { courierGrantRequest, grantFromInput, installRefusing, isShortCode, noSpaceError, pendingSpaceError, receiveGrant, resumeAwaitingGrant, resumeGrant } from './grant.js';
|
|
13
|
-
import {
|
|
15
|
+
import { completeApproval, DEFAULT_WAIT_MS, MAX_WAIT_MS, requestApproval, waitApproval } from './grant-request.js';
|
|
16
|
+
import { approvalPhase, assertPendingReadable, clearPending, holdsApproval, loadPending, readApprovalEntry, stageOf } from './approval-phase.js';
|
|
14
17
|
import { artifactA, artifactWithDelegation, parseArtifact, peerRecordBody, PEER_DELEGATION_EXPIRY, validateArtifactIdentity, } from './pairing.js';
|
|
15
18
|
import { authRequiredTask, buildMessage, buildTask, COUNTERPARTY_NOTICE, isRecord, messageView, oneLine, safeText, messageRole, parseTaskState, taskView, INTENTS, } from './a2a.js';
|
|
16
19
|
import { commitReceipts, collectInbound, renderInboxText, writeOut } from './inbox.js';
|
|
@@ -96,12 +99,41 @@ export async function cmdInit(args) {
|
|
|
96
99
|
const grantRaw = grantFile === undefined || args.has('request')
|
|
97
100
|
? undefined
|
|
98
101
|
: grantFile === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(grantFile, 'utf8');
|
|
102
|
+
// Explicit recovery before any mode acts: finish an interrupted rotation, remove stray key-bearing temp files; then derive once.
|
|
103
|
+
recoverInterrupted();
|
|
104
|
+
const state = deriveState();
|
|
105
|
+
for (const note of state.notes)
|
|
106
|
+
console.error(`warning: ${note}`);
|
|
107
|
+
// A profile.json that exists and cannot be read is never treated as absent: `loadProfile` refuses with its own message (an unreadable
|
|
108
|
+
// profile is replaced only by an explicit `--rotate-key`).
|
|
109
|
+
if (state.action === 'fix-profile' && !args.has('rotate-key')) {
|
|
110
|
+
loadProfile();
|
|
111
|
+
fail('bad-profile', adviceFor(state));
|
|
112
|
+
}
|
|
113
|
+
// A saved approval without its profile or key is left as it is; `--rotate-key` starts over with a new key.
|
|
114
|
+
if (state.action === 'refuse' && !args.has('rotate-key'))
|
|
115
|
+
fail('approval-orphaned', adviceFor(state));
|
|
116
|
+
// 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
|
|
117
|
+
// discards the grant by design, so it goes on.
|
|
118
|
+
if (state.grantMode && !args.has('rotate-key')) {
|
|
119
|
+
const file = path.join(profileDir(), 'grant.json');
|
|
120
|
+
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.`);
|
|
121
|
+
}
|
|
122
|
+
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
123
|
if (args.has('resume')) {
|
|
100
124
|
if (!profileExists())
|
|
101
125
|
fail('no-grant', `No Courier profile to resume. Run \`${cmd('init')}\` and approve a grant first.`);
|
|
102
126
|
const profile = loadProfile();
|
|
127
|
+
// An approval the owner gave, with no grant saved to install first, is collected by `--wait`, never discarded or shadowed by a cleanup.
|
|
128
|
+
if (state.action === 'wait' && heldApproval(state.inputs.approval) && profile.status !== 'ready') {
|
|
129
|
+
console.error(`${adviceFor(state)} Continuing as \`${cmd('init --wait')}\`.`);
|
|
130
|
+
await collectApproval(profile, loadKey(), { expiry: args.get('expiry'), openkeyApiHost: apiHostFlag, waitMs });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
103
133
|
if (profile.status === 'awaiting-grant' && await resumeAwaitingGrant(profile)) {
|
|
104
134
|
dropOpenRequest();
|
|
135
|
+
if (heldApproval(state.inputs.approval))
|
|
136
|
+
console.error(HELD_APPROVAL_KEPT);
|
|
105
137
|
await finishInit(profile, profile.host);
|
|
106
138
|
return;
|
|
107
139
|
}
|
|
@@ -109,22 +141,20 @@ export async function cmdInit(args) {
|
|
|
109
141
|
fail('usage', `\`${cmd('init --resume')}\` requires a profile in awaiting-space state.`);
|
|
110
142
|
await resumeGrant(profile, loadKey());
|
|
111
143
|
dropOpenRequest();
|
|
144
|
+
if (heldApproval(state.inputs.approval))
|
|
145
|
+
console.error(HELD_APPROVAL_KEPT);
|
|
112
146
|
await finishInit(profile, profile.host);
|
|
113
147
|
return;
|
|
114
148
|
}
|
|
115
149
|
const rotate = args.has('rotate-key');
|
|
116
150
|
// A writer must learn that the saved approval is unreadable BEFORE the key, grant or profile is changed.
|
|
117
|
-
|
|
151
|
+
// 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.
|
|
152
|
+
if (args.has('request') || args.has('wait') || rotate)
|
|
118
153
|
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
154
|
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
|
-
}
|
|
155
|
+
// A renewal is asked for with `--request`; `--wait` only collects. The derived state says when `--wait` goes on.
|
|
156
|
+
if (args.has('wait') && !rotate && !state.wait)
|
|
157
|
+
fail('usage', adviceFor(state));
|
|
128
158
|
const key = prior ? loadKey() : newAgentKey();
|
|
129
159
|
const host = hostFlag ?? prior?.host ?? process.env.COURIER_HOST ?? DEFAULT_HOST;
|
|
130
160
|
const openkeyHost = openkeyHostFlag ?? prior?.openkeyHost ?? process.env.COURIER_OPENKEY_HOST ?? 'https://openkey.so';
|
|
@@ -143,25 +173,23 @@ export async function cmdInit(args) {
|
|
|
143
173
|
profile.label = labelFlag ?? profile.label;
|
|
144
174
|
if (handleFlag)
|
|
145
175
|
profile.selfHandle = handleFlag;
|
|
176
|
+
profile.courierDid = bareDid(key.did);
|
|
146
177
|
if (rotate) {
|
|
147
178
|
profile.peers = {};
|
|
148
179
|
profile.ownerDid = undefined;
|
|
149
180
|
profile.spaceId = undefined;
|
|
150
181
|
profile.grantExpiresAt = undefined;
|
|
151
182
|
profile.status = 'awaiting-grant';
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
// One journaled step: key, profile, grant and a discarded approval change together, and an interruption is finished by the next init.
|
|
184
|
+
await rotateKey(key, profile);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
saveKey(key);
|
|
188
|
+
saveProfile(profile);
|
|
156
189
|
}
|
|
157
|
-
profile.courierDid = bareDid(key.did);
|
|
158
|
-
saveKey(key);
|
|
159
|
-
saveProfile(profile);
|
|
160
190
|
const request = courierGrantRequest(profile, key, expiry, { openkeyApiHost: apiHostFlag });
|
|
161
191
|
if (rotate)
|
|
162
192
|
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
193
|
if (args.has('request')) {
|
|
166
194
|
const pending = await requestApproval(profile, key, args.get('expiry'), { openkeyApiHost: apiHostFlag });
|
|
167
195
|
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 +199,7 @@ export async function cmdInit(args) {
|
|
|
171
199
|
return;
|
|
172
200
|
}
|
|
173
201
|
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);
|
|
202
|
+
await collectApproval(profile, key, { expiry: args.get('expiry'), openkeyApiHost: apiHostFlag, waitMs });
|
|
186
203
|
return;
|
|
187
204
|
}
|
|
188
205
|
let payload;
|
|
@@ -211,14 +228,14 @@ export async function cmdInit(args) {
|
|
|
211
228
|
*/
|
|
212
229
|
function clearPendingQuietly() {
|
|
213
230
|
try {
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
231
|
+
const phase = approvalPhase(readApprovalEntry());
|
|
232
|
+
if (phase.phase === 'held' && phase.stage === 'committed') {
|
|
216
233
|
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
234
|
}
|
|
218
|
-
else if (
|
|
235
|
+
else if (phase.phase === 'held') {
|
|
219
236
|
console.error('This grant replaces the saved short-link approval: discarded a saved approval that was not installed.');
|
|
220
237
|
}
|
|
221
|
-
else if (
|
|
238
|
+
else if (phase.phase === 'unreadable' || phase.phase === 'corrupt') {
|
|
222
239
|
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
240
|
}
|
|
224
241
|
clearPending();
|
|
@@ -237,6 +254,21 @@ function dropOpenRequest() {
|
|
|
237
254
|
if (saved && !holdsApproval(saved))
|
|
238
255
|
clearPendingQuietly();
|
|
239
256
|
}
|
|
257
|
+
/** `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. */
|
|
258
|
+
async function collectApproval(profile, key, options) {
|
|
259
|
+
const deadline = Date.now() + options.waitMs;
|
|
260
|
+
const { pending, resumed } = await waitApproval(profile, key, options.expiry, { openkeyApiHost: options.openkeyApiHost });
|
|
261
|
+
if (stageOf(pending) !== 'committed') {
|
|
262
|
+
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.`);
|
|
263
|
+
}
|
|
264
|
+
if (await completeApproval(profile, key, pending, { deadline }) === 'deadline') {
|
|
265
|
+
throw new CourierPending({ step: 1, of: 1, approvalUrl: pending.approvalUrl, expiresAt: pending.requestExpiresAt });
|
|
266
|
+
}
|
|
267
|
+
const active = loadProfile();
|
|
268
|
+
if (active.status !== 'ready')
|
|
269
|
+
throw active.spacePending ? pendingSpaceError(active) : noSpaceError(active);
|
|
270
|
+
await finishInit(active, active.host);
|
|
271
|
+
}
|
|
240
272
|
async function finishInit(profile, host) {
|
|
241
273
|
const active = await ctx();
|
|
242
274
|
for (const peer of Object.values(active.profile.peers)) {
|
|
@@ -326,6 +358,7 @@ async function pairAccept(args) {
|
|
|
326
358
|
fail('bad-artifact', 'Refusing to pair with this agent identity.');
|
|
327
359
|
if (art.step === 'A') {
|
|
328
360
|
const handle = art.callMe;
|
|
361
|
+
assertNotPaired(profile, handle); // before delegateTo: a refusal after it would leave a live delegation nothing records
|
|
329
362
|
const outbound = await node.delegateTo(art.from.courier, peerReadPermissions(handle, profile.spaceId), { expiry: PEER_DELEGATION_EXPIRY });
|
|
330
363
|
const peer = {
|
|
331
364
|
handle, ownerDid: art.from.owner, courierDid: art.from.courier, label: art.from.label,
|
|
@@ -333,7 +366,7 @@ async function pairAccept(args) {
|
|
|
333
366
|
};
|
|
334
367
|
recordOutbound(peer, outbound.delegation);
|
|
335
368
|
profile.peers[handle] = peer;
|
|
336
|
-
|
|
369
|
+
saveAfterDelegation(profile, outbound.delegation);
|
|
337
370
|
await writePeerRecord(node, peer);
|
|
338
371
|
printJson(artifactWithDelegation('B', profile, outbound.delegation, handle, art.peer));
|
|
339
372
|
return;
|
|
@@ -355,7 +388,7 @@ async function pairAccept(args) {
|
|
|
355
388
|
const outgoing = await node.delegateTo(peer.courierDid, peerReadPermissions(peer.handle, profile.spaceId), { expiry: PEER_DELEGATION_EXPIRY });
|
|
356
389
|
recordOutbound(peer, outgoing.delegation);
|
|
357
390
|
peer.status = 'paired';
|
|
358
|
-
|
|
391
|
+
saveAfterDelegation(profile, outgoing.delegation);
|
|
359
392
|
await writePeerRecord(node, peer);
|
|
360
393
|
printJson(artifactWithDelegation('C', profile, outgoing.delegation, peer.handle, art.peer));
|
|
361
394
|
return;
|
|
@@ -388,6 +421,19 @@ async function pairAccept(args) {
|
|
|
388
421
|
await writePeerRecord(node, peer);
|
|
389
422
|
printJson({ ok: true, paired: peer.handle, peerDid: peer.courierDid, peerSpace: peer.spaceId, delegationExpiresAt: peer.inbound.expiresAt });
|
|
390
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Saves after `delegateTo` made a delegation that is live from that moment. A save that refuses (the pairing completed, or the peer was
|
|
426
|
+
* revoked, meanwhile) leaves a delegation no profile records: say so, with its CID and where the owner can revoke it, and never publish it.
|
|
427
|
+
*/
|
|
428
|
+
function saveAfterDelegation(profile, delegation) {
|
|
429
|
+
try {
|
|
430
|
+
saveProfile(profile);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
console.error(`warning: the delegation ${delegationCid(delegation)} created for this pairing is live and was not saved. Nothing was published. The owner can revoke it at ${REVOKE_URL}.`);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
391
437
|
function recordOutbound(peer, delegation) {
|
|
392
438
|
const expiresAt = delegation.expiry instanceof Date ? delegation.expiry.toISOString() : new Date(delegation.expiry).toISOString();
|
|
393
439
|
peer.outbound.push({ cid: delegationCid(delegation), issuedAt: new Date().toISOString(), expiresAt });
|