@vibe-commit/capture 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -7
- package/dist/account_not_here.d.ts +15 -0
- package/dist/account_not_here.js +25 -0
- package/dist/account_not_here.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +21 -0
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/browser_credential.d.ts +4 -2
- package/dist/commands/browser_credential.js +5 -2
- package/dist/commands/browser_credential.js.map +1 -1
- package/dist/commands/connect.d.ts +9 -0
- package/dist/commands/connect.js +13 -8
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/connect_account.d.ts +135 -0
- package/dist/commands/connect_account.js +321 -0
- package/dist/commands/connect_account.js.map +1 -0
- package/dist/commands/connect_picker.js +160 -26
- package/dist/commands/connect_picker.js.map +1 -1
- package/dist/commands/device_credential.d.ts +3 -1
- package/dist/commands/device_credential.js +1 -0
- package/dist/commands/device_credential.js.map +1 -1
- package/dist/commands/finish_mint.d.ts +28 -0
- package/dist/commands/finish_mint.js +16 -4
- package/dist/commands/finish_mint.js.map +1 -1
- package/dist/commands/status.d.ts +19 -1
- package/dist/commands/status.js +123 -14
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/status_machine.d.ts +12 -3
- package/dist/commands/status_machine.js +64 -3
- package/dist/commands/status_machine.js.map +1 -1
- package/dist/commands/sync_reason.d.ts +17 -0
- package/dist/commands/sync_reason.js +25 -0
- package/dist/commands/sync_reason.js.map +1 -0
- package/dist/consent.d.ts +109 -3
- package/dist/consent.js +210 -13
- package/dist/consent.js.map +1 -1
- package/dist/copy/strings.d.ts +122 -3
- package/dist/copy/strings.js +142 -3
- package/dist/copy/strings.js.map +1 -1
- package/dist/grants_sync.d.ts +165 -0
- package/dist/grants_sync.js +289 -0
- package/dist/grants_sync.js.map +1 -0
- package/dist/grants_verb.d.ts +70 -0
- package/dist/grants_verb.js +121 -0
- package/dist/grants_verb.js.map +1 -0
- package/dist/hooks/entry.js +8 -2
- package/dist/hooks/entry.js.map +1 -1
- package/dist/hooks/session_grants.d.ts +59 -0
- package/dist/hooks/session_grants.js +91 -0
- package/dist/hooks/session_grants.js.map +1 -0
- package/dist/hooks/session_start.d.ts +28 -7
- package/dist/hooks/session_start.js +55 -11
- package/dist/hooks/session_start.js.map +1 -1
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -1
- package/dist/oauth/retire.d.ts +10 -0
- package/dist/oauth/retire.js +13 -0
- package/dist/oauth/retire.js.map +1 -1
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +26 -0
- package/dist/paths.js.map +1 -1
- package/dist/pending_revoke.d.ts +117 -0
- package/dist/pending_revoke.js +233 -0
- package/dist/pending_revoke.js.map +1 -0
- package/dist/pending_revoke_file.d.ts +55 -0
- package/dist/pending_revoke_file.js +74 -0
- package/dist/pending_revoke_file.js.map +1 -0
- package/dist/picker.d.ts +30 -2
- package/dist/picker.js +55 -6
- package/dist/picker.js.map +1 -1
- package/dist/picker_checklist.d.ts +19 -3
- package/dist/picker_checklist.js +40 -4
- package/dist/picker_checklist.js.map +1 -1
- package/dist/policy.d.ts +16 -1
- package/dist/policy.js +21 -2
- package/dist/policy.js.map +1 -1
- package/dist/post.d.ts +41 -2
- package/dist/post.js +40 -12
- package/dist/post.js.map +1 -1
- package/dist/system_message.d.ts +25 -2
- package/dist/system_message.js +29 -5
- package/dist/system_message.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending revokes (slice 2, S4, R3-11): the WRITE-AHEAD record of a credential `g` has made and not yet been told to keep.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* addPendingRevoke(home, credential, endpoint, now) at the mint, BEFORE anything else: refuses (never overwrites) when it cannot
|
|
6
|
+
* removePendingRevoke(home, token) after the revoke, or after the credential is kept: re-reads, removes ONE row
|
|
7
|
+
* revokeHeld(home, token, fetch) one best-effort revoke (3 s) of a row, to ITS recorded origin
|
|
8
|
+
* retryPendingRevokes(home, env, fetch, now) every user-facing verb, once, at its start: one 3 s bound across all rows
|
|
9
|
+
* readPendingRevokes(home) how many are waiting (for `status`'s `pending` count)
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ## ⛔ WHY IT IS WRITE-AHEAD
|
|
13
|
+
*
|
|
14
|
+
* `g` is the one place a credential is made ahead of consent (exception 3). So the credential lives in MEMORY until the yes:
|
|
15
|
+
* `credentials.json` does not exist before it, and the row below is written the instant the server hands the token over. A kill, a
|
|
16
|
+
* second Ctrl+C, a SIGHUP at the y/N or an unexpected throw therefore leaves a ROW (which the next verb revokes), never a live
|
|
17
|
+
* credential on disk that nothing remembers. The yes commits in this order: the grants, then `credentials.json`, then the row's
|
|
18
|
+
* deletion; a retry skips and drops any row whose token IS the current credential, so a kill between the last two never revokes what
|
|
19
|
+
* the user kept.
|
|
20
|
+
*
|
|
21
|
+
* ## ⛔ THE FILE HOLDS A BEARER TOKEN — the same exposure class as `credentials.json`, held to the same rules
|
|
22
|
+
* - written mode 0600 under the 0700 directory, temp + rename;
|
|
23
|
+
* - READ through `readSecretFile`: a file or folder others can read is refused, and an insecure or unreadable file is NEVER
|
|
24
|
+
* overwritten (the writer refuses, and `g` fails closed);
|
|
25
|
+
* - never printed, logged or returned: `status` counts the rows and shows nothing else, `--json` included;
|
|
26
|
+
* - a hook never touches it (only the CLI verbs import this module; `test/connect-account.test.ts` walks the hooks' import graph).
|
|
27
|
+
*
|
|
28
|
+
* ## ⛔ A ROW IS NEVER DROPPED UNREVOKED, except by the clock
|
|
29
|
+
* - it leaves on a 2xx, a 401 (the server does not accept it: already dead) or a 404 (a server that cannot revoke);
|
|
30
|
+
* - or at `minted_at` + 91 days: a token that was never on disk was never used, and an ingest credential ends 90 days unused;
|
|
31
|
+
* - there is no row cap that drops one: at the cap `addPendingRevoke` REFUSES, and `g` fails closed.
|
|
32
|
+
*
|
|
33
|
+
* ## ⛔ THE ORIGIN IS THE ROW'S
|
|
34
|
+
*
|
|
35
|
+
* A row records the issuing server's origin and a retry goes ONLY there (https, or http on loopback: `isAllowedIngestUrl`), never to
|
|
36
|
+
* wherever `VIBECOMMIT_MCP_URL` points today.
|
|
37
|
+
*/
|
|
38
|
+
import { chmodSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
39
|
+
import { writeFileAtomic } from "./atomic_write.js";
|
|
40
|
+
import { IngestCredential, INGEST_TOKEN_PREFIX } from "./credential.js";
|
|
41
|
+
import { LEASE_MS, leased, readPendingRevokes, readQueue } from "./pending_revoke_file.js";
|
|
42
|
+
import { REVOKE_TIMEOUT_MS, selfRevokeOutcome } from "./oauth/retire.js";
|
|
43
|
+
import { credentialsPath, pendingRevokePath, rootDir } from "./paths.js";
|
|
44
|
+
import { isAllowedIngestUrl } from "./post.js";
|
|
45
|
+
export { readPendingRevokes } from "./pending_revoke_file.js";
|
|
46
|
+
/** More rows than `g` can produce in earnest: past it `g` refuses rather than drop one. */
|
|
47
|
+
export const MAX_ROWS = 8;
|
|
48
|
+
/** A token never used ends 90 days after it is made; one day of slack. */
|
|
49
|
+
const MAX_AGE_MS = 91 * 24 * 60 * 60 * 1000;
|
|
50
|
+
/** One best-effort revoke, and the bound on a verb's whole retry. */
|
|
51
|
+
export { REVOKE_TIMEOUT_MS };
|
|
52
|
+
function writeRows(home, rows) {
|
|
53
|
+
if (rows.length === 0) {
|
|
54
|
+
try {
|
|
55
|
+
unlinkSync(pendingRevokePath(home));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* already gone */
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
mkdirSync(rootDir(home), { recursive: true, mode: 0o700 });
|
|
63
|
+
chmodSync(rootDir(home), 0o700);
|
|
64
|
+
writeFileAtomic(pendingRevokePath(home), `${JSON.stringify({ revokes: rows }, null, 2)}\n`, 0o600);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write the row for a credential that has just been made. Never overwrites a file it cannot read, never drops a row.
|
|
68
|
+
* `lease: true` is `g` holding the credential at its y/N: the row carries `{ pid, lease_until }` (30 minutes) so another verb's retry
|
|
69
|
+
* leaves it alone while it is kept; every other writer (a test, an orphaned row) has no lease and is retried at once.
|
|
70
|
+
*/
|
|
71
|
+
export function addPendingRevoke(home, credential, endpoint, now, options = {}) {
|
|
72
|
+
try {
|
|
73
|
+
const token = credential.expose();
|
|
74
|
+
if (!token.startsWith(INGEST_TOKEN_PREFIX))
|
|
75
|
+
return { kind: "failed" };
|
|
76
|
+
const queue = readQueue(home);
|
|
77
|
+
if (queue.kind === "refused")
|
|
78
|
+
return { kind: "refused", read: queue.read };
|
|
79
|
+
const rows = queue.kind === "ok" ? queue.rows : [];
|
|
80
|
+
if (rows.some((row) => row.token === token))
|
|
81
|
+
return { kind: "written" };
|
|
82
|
+
if (rows.length >= MAX_ROWS)
|
|
83
|
+
return { kind: "full" };
|
|
84
|
+
const lease = options.lease === true ? { pid: process.pid, lease_until: new Date(now.getTime() + LEASE_MS).toISOString() } : null;
|
|
85
|
+
writeRows(home, [...rows, { token, minted_at: now.toISOString(), origin: new URL(endpoint).origin, lease }]);
|
|
86
|
+
return { kind: "written" };
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { kind: "failed" };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Remove ONE row, re-reading just before the write so a row queued in between survives. Deletes the file when it is empty. Never throws. */
|
|
93
|
+
export function removePendingRevoke(home, token) {
|
|
94
|
+
removeRows(home, new Set([token]));
|
|
95
|
+
}
|
|
96
|
+
/** Give the row up to the retry: `g` is no longer holding the credential, so the next verb may revoke it. Re-reads before it writes. */
|
|
97
|
+
export function releaseLease(home, token) {
|
|
98
|
+
try {
|
|
99
|
+
const queue = readQueue(home);
|
|
100
|
+
if (queue.kind !== "ok" || !queue.rows.some((row) => row.token === token && row.lease !== null))
|
|
101
|
+
return;
|
|
102
|
+
writeRows(home, queue.rows.map((row) => (row.token === token ? { ...row, lease: null } : row)));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* the lease runs out by itself */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Is the row for `token` still there? `g` asks just before it commits, and the three answers are different things:
|
|
110
|
+
* - `held`: the row is in a queue this client can use;
|
|
111
|
+
* - `gone`: the queue is readable and the row is not in it — another verb settled (revoked) it;
|
|
112
|
+
* - `unusable`: the queue is insecure or unreadable. Nobody revoked anything; this client can neither keep nor settle the record.
|
|
113
|
+
*/
|
|
114
|
+
export function holdsRow(home, token) {
|
|
115
|
+
const queue = readQueue(home);
|
|
116
|
+
if (queue.kind === "refused")
|
|
117
|
+
return "unusable";
|
|
118
|
+
return queue.kind === "ok" && queue.rows.some((row) => row.token === token) ? "held" : "gone";
|
|
119
|
+
}
|
|
120
|
+
function removeRows(home, tokens) {
|
|
121
|
+
try {
|
|
122
|
+
const queue = readQueue(home);
|
|
123
|
+
if (queue.kind !== "ok")
|
|
124
|
+
return;
|
|
125
|
+
const rows = queue.rows.filter((row) => !tokens.has(row.token));
|
|
126
|
+
if (rows.length !== queue.rows.length)
|
|
127
|
+
writeRows(home, rows);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* the row stays; the next verb retries */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Revoke a token, with itself as the bearer, at `origin` — and only where a credential may be sent at all. */
|
|
134
|
+
async function revokeToken(origin, token, fetchImpl, timeoutMs) {
|
|
135
|
+
if (!isAllowedIngestUrl(origin))
|
|
136
|
+
return null;
|
|
137
|
+
const outcome = await selfRevokeOutcome(origin, new IngestCredential(token, "file"), { fetch: fetchImpl, timeoutMs });
|
|
138
|
+
return outcome.kind === "retired" || outcome.kind === "gone" ? "revoked" : outcome.kind === "unknown" ? "unrevocable" : null;
|
|
139
|
+
}
|
|
140
|
+
/** The tokens this machine is using right now: `credentials.json`'s, read RAW (a mode problem does not hide it) and the environment's. */
|
|
141
|
+
function currentTokens(home, env) {
|
|
142
|
+
const out = new Set();
|
|
143
|
+
try {
|
|
144
|
+
const token = JSON.parse(readFileSync(credentialsPath(home), "utf8")).token;
|
|
145
|
+
if (typeof token === "string" && token !== "")
|
|
146
|
+
out.add(token);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* no file, or not ours: nothing to protect */
|
|
150
|
+
}
|
|
151
|
+
const envToken = env.VIBECOMMIT_TOKEN?.trim();
|
|
152
|
+
if (envToken !== undefined && envToken !== "")
|
|
153
|
+
out.add(envToken);
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `g`'s every no-grant exit: one revoke within 3 s. With a readable row it goes to the row's recorded origin and removes the row on a
|
|
158
|
+
* 2xx, 401 or 404 (a failure gives the lease up so the next verb retries). With NO usable row — missing, or the queue refused — it
|
|
159
|
+
* revokes with the in-memory credential at the MINTED origin: a missing row is never taken for "already revoked".
|
|
160
|
+
*/
|
|
161
|
+
export async function revokeHeld(home, credential, mintedOrigin, fetchImpl) {
|
|
162
|
+
const token = credential.expose();
|
|
163
|
+
let insecure = null;
|
|
164
|
+
try {
|
|
165
|
+
const queue = readQueue(home);
|
|
166
|
+
const row = queue.kind === "ok" ? queue.rows.find((r) => r.token === token) : undefined;
|
|
167
|
+
if (queue.kind === "refused" && queue.read.kind === "insecure-file")
|
|
168
|
+
insecure = queue.read;
|
|
169
|
+
if (row !== undefined) {
|
|
170
|
+
const ended = await revokeToken(row.origin, token, fetchImpl, REVOKE_TIMEOUT_MS);
|
|
171
|
+
if (ended !== null) {
|
|
172
|
+
removePendingRevoke(home, token);
|
|
173
|
+
return { kind: ended, insecure };
|
|
174
|
+
}
|
|
175
|
+
releaseLease(home, token);
|
|
176
|
+
return { kind: "waiting", insecure };
|
|
177
|
+
}
|
|
178
|
+
const ended = await revokeToken(mintedOrigin, token, fetchImpl, REVOKE_TIMEOUT_MS);
|
|
179
|
+
return { kind: ended ?? "orphan", insecure };
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return { kind: "orphan", insecure };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Every user-facing verb, once, at its start: one attempt at each row, and ONE 3 s bound across all of them (a row gets what is
|
|
187
|
+
* left of the bound, and none is attempted once it is spent). Silent, and needs no credential.
|
|
188
|
+
*
|
|
189
|
+
* Before EACH request it re-reads the queue and the current credential (a credential committed, or a row settled, after the loop
|
|
190
|
+
* began is never revoked on a stale view), and it
|
|
191
|
+
* - skips and DROPS a row whose token is the current credential (the user kept it; the kill came between the credential's write
|
|
192
|
+
* and the row's deletion) — compared on the file's raw token, so a mode problem does not hide a kept credential;
|
|
193
|
+
* - skips (and keeps) a LEASED row: `g` is holding it at its y/N in another process;
|
|
194
|
+
* - drops a row past `minted_at` + 91 days.
|
|
195
|
+
* Removes each row it settles as it goes, re-reading just before it writes. Returns how many rows still wait. Never throws.
|
|
196
|
+
*/
|
|
197
|
+
export async function retryPendingRevokes(home, env, fetchImpl, now, deadlineMs = REVOKE_TIMEOUT_MS) {
|
|
198
|
+
try {
|
|
199
|
+
const first = readQueue(home);
|
|
200
|
+
if (first.kind !== "ok")
|
|
201
|
+
return 0;
|
|
202
|
+
const started = Date.now();
|
|
203
|
+
for (const listed of first.rows) {
|
|
204
|
+
const fresh = readQueue(home);
|
|
205
|
+
if (fresh.kind !== "ok")
|
|
206
|
+
break;
|
|
207
|
+
const row = fresh.rows.find((r) => r.token === listed.token);
|
|
208
|
+
if (row === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
if (currentTokens(home, env).has(row.token)) {
|
|
211
|
+
removePendingRevoke(home, row.token);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (leased(row, now))
|
|
215
|
+
continue;
|
|
216
|
+
const age = now.getTime() - Date.parse(row.minted_at);
|
|
217
|
+
if (Number.isFinite(age) && age > MAX_AGE_MS) {
|
|
218
|
+
removePendingRevoke(home, row.token);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const left = deadlineMs - (Date.now() - started);
|
|
222
|
+
if (left <= 0)
|
|
223
|
+
break;
|
|
224
|
+
if ((await revokeToken(row.origin, row.token, fetchImpl, left)) !== null)
|
|
225
|
+
removePendingRevoke(home, row.token);
|
|
226
|
+
}
|
|
227
|
+
return readPendingRevokes(home).length;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return readPendingRevokes(home).length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=pending_revoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending_revoke.js","sourceRoot":"","sources":["../src/pending_revoke.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pending-revoke queue's FILE: its path's format, and reading it (slice 2, S4). Split from `pending_revoke.ts` so that what
|
|
3
|
+
* `status` needs — a COUNT — does not drag the writer and the network revoke onto every process's load path.
|
|
4
|
+
*
|
|
5
|
+
* Read through `readSecretFile` (the same mode checks as `credentials.json`): a file or folder others can read, a file that is not
|
|
6
|
+
* what this client wrote, or one with a row it cannot read all come back `refused`, and `pending_revoke.ts` never overwrites those.
|
|
7
|
+
* Nothing here writes, sends or prints a token; the only thing it hands `status` is how many rows wait.
|
|
8
|
+
*/
|
|
9
|
+
import { type SecretFileRead } from "./credential.js";
|
|
10
|
+
export interface PendingRevoke {
|
|
11
|
+
readonly token: string;
|
|
12
|
+
/** ISO 8601, when the server made the credential. */
|
|
13
|
+
readonly minted_at: string;
|
|
14
|
+
/** The issuing server's origin (`https://api.vibecommit.ai`): the only place a retry sends this token. */
|
|
15
|
+
readonly origin: string;
|
|
16
|
+
/**
|
|
17
|
+
* While `g` is holding the credential (waiting at its y/N) the row carries the holder's lease: a retry run by ANOTHER verb in the
|
|
18
|
+
* meantime must not revoke a credential that is about to be kept. A row with no lease is an orphan. See `leased`.
|
|
19
|
+
*/
|
|
20
|
+
readonly lease: {
|
|
21
|
+
readonly pid: number;
|
|
22
|
+
readonly lease_until: string;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
/** How long a holder's lease lasts: a `g` whose question stays open longer may be revoked by another verb, and its commit then refuses. */
|
|
26
|
+
export declare const LEASE_MS: number;
|
|
27
|
+
/**
|
|
28
|
+
* Is the row leased at `now`? BOTH must hold: the holder's pid is alive (`kill(pid, 0)`: EPERM counts as alive, ESRCH as dead) and
|
|
29
|
+
* `now` is before the EARLIER of the row's own `lease_until` and `minted_at` + 30 minutes. The lease is capped at its mint: a
|
|
30
|
+
* `lease_until` written far in the future (a bug, or an edit) does not keep a live pid's row out of the retry for ever. A dead holder or
|
|
31
|
+
* an expired lease is an orphan, which any verb's retry revokes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function leased(row: PendingRevoke, now: Date): boolean;
|
|
34
|
+
export type Queue = {
|
|
35
|
+
readonly kind: "ok";
|
|
36
|
+
readonly rows: PendingRevoke[];
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: "absent";
|
|
39
|
+
}
|
|
40
|
+
/** Present but not safe to read or write: never read, never overwritten. */
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: "refused";
|
|
43
|
+
readonly read: Exclude<SecretFileRead, {
|
|
44
|
+
kind: "ok";
|
|
45
|
+
} | {
|
|
46
|
+
kind: "absent";
|
|
47
|
+
}> | {
|
|
48
|
+
readonly kind: "unreadable";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function readQueue(home: string): Queue;
|
|
52
|
+
/** The rows waiting, for a count. An absent, insecure or unreadable file reads as none (it is never sent from). */
|
|
53
|
+
export declare function readPendingRevokes(home: string): PendingRevoke[];
|
|
54
|
+
/** How many rows wait: all `status` shows of the queue. */
|
|
55
|
+
export declare function countPendingRevokes(home: string): number;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pending-revoke queue's FILE: its path's format, and reading it (slice 2, S4). Split from `pending_revoke.ts` so that what
|
|
3
|
+
* `status` needs — a COUNT — does not drag the writer and the network revoke onto every process's load path.
|
|
4
|
+
*
|
|
5
|
+
* Read through `readSecretFile` (the same mode checks as `credentials.json`): a file or folder others can read, a file that is not
|
|
6
|
+
* what this client wrote, or one with a row it cannot read all come back `refused`, and `pending_revoke.ts` never overwrites those.
|
|
7
|
+
* Nothing here writes, sends or prints a token; the only thing it hands `status` is how many rows wait.
|
|
8
|
+
*/
|
|
9
|
+
import { INGEST_TOKEN_PREFIX, readSecretFile } from "./credential.js";
|
|
10
|
+
import { pendingRevokePath } from "./paths.js";
|
|
11
|
+
/** How long a holder's lease lasts: a `g` whose question stays open longer may be revoked by another verb, and its commit then refuses. */
|
|
12
|
+
export const LEASE_MS = 30 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* Is the row leased at `now`? BOTH must hold: the holder's pid is alive (`kill(pid, 0)`: EPERM counts as alive, ESRCH as dead) and
|
|
15
|
+
* `now` is before the EARLIER of the row's own `lease_until` and `minted_at` + 30 minutes. The lease is capped at its mint: a
|
|
16
|
+
* `lease_until` written far in the future (a bug, or an edit) does not keep a live pid's row out of the retry for ever. A dead holder or
|
|
17
|
+
* an expired lease is an orphan, which any verb's retry revokes.
|
|
18
|
+
*/
|
|
19
|
+
export function leased(row, now) {
|
|
20
|
+
if (row.lease === null)
|
|
21
|
+
return false;
|
|
22
|
+
const until = Math.min(Date.parse(row.lease.lease_until), Date.parse(row.minted_at) + LEASE_MS);
|
|
23
|
+
if (!(until > now.getTime()))
|
|
24
|
+
return false;
|
|
25
|
+
try {
|
|
26
|
+
process.kill(row.lease.pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return error.code === "EPERM";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function readQueue(home) {
|
|
34
|
+
const read = readSecretFile(home, pendingRevokePath(home));
|
|
35
|
+
if (read.kind === "absent")
|
|
36
|
+
return { kind: "absent" };
|
|
37
|
+
if (read.kind !== "ok")
|
|
38
|
+
return { kind: "refused", read };
|
|
39
|
+
try {
|
|
40
|
+
const doc = JSON.parse(read.raw);
|
|
41
|
+
if (doc === null || typeof doc !== "object" || !Array.isArray(doc.revokes))
|
|
42
|
+
return { kind: "refused", read: { kind: "unreadable" } };
|
|
43
|
+
const rows = [];
|
|
44
|
+
for (const row of doc.revokes) {
|
|
45
|
+
if (row === null || typeof row !== "object")
|
|
46
|
+
return { kind: "refused", read: { kind: "unreadable" } };
|
|
47
|
+
const r = row;
|
|
48
|
+
if (typeof r.token !== "string" || !r.token.startsWith(INGEST_TOKEN_PREFIX) || typeof r.minted_at !== "string" || typeof r.origin !== "string") {
|
|
49
|
+
// A row this client cannot read is a row it must not lose: refuse the whole file rather than rewrite it without the row.
|
|
50
|
+
return { kind: "refused", read: { kind: "unreadable" } };
|
|
51
|
+
}
|
|
52
|
+
// A lease this client cannot read is NO lease: the row is an orphan, which errs toward revoking.
|
|
53
|
+
const l = r.lease;
|
|
54
|
+
const lease = l !== null && l !== undefined && typeof l === "object" && typeof l.pid === "number" && Number.isInteger(l.pid) && typeof l.lease_until === "string"
|
|
55
|
+
? { pid: l.pid, lease_until: l.lease_until }
|
|
56
|
+
: null;
|
|
57
|
+
rows.push({ token: r.token, minted_at: r.minted_at, origin: r.origin, lease });
|
|
58
|
+
}
|
|
59
|
+
return { kind: "ok", rows };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { kind: "refused", read: { kind: "unreadable" } };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The rows waiting, for a count. An absent, insecure or unreadable file reads as none (it is never sent from). */
|
|
66
|
+
export function readPendingRevokes(home) {
|
|
67
|
+
const queue = readQueue(home);
|
|
68
|
+
return queue.kind === "ok" ? queue.rows : [];
|
|
69
|
+
}
|
|
70
|
+
/** How many rows wait: all `status` shows of the queue. */
|
|
71
|
+
export function countPendingRevokes(home) {
|
|
72
|
+
return readPendingRevokes(home).length;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=pending_revoke_file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending_revoke_file.js","sourceRoot":"","sources":["../src/pending_revoke_file.ts"],"names":[],"mappings":"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"}
|
package/dist/picker.d.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* row (D9: at most 8 repos before `+ N more`).
|
|
15
15
|
* turned off slugs in `excluded`. Choosing one is a re-include; the grant block names it on a `turn on` row, inside
|
|
16
16
|
* the same y/N (D7 · 3A), and only a yes clears the exclusion.
|
|
17
|
+
* on your account (slice 2) what the account grants that this machine has not named. Unticked until chosen, like any
|
|
18
|
+
* row: choosing one is an ordinary addition, so it goes through the same disclosure and y/N (R3-8, R3-9).
|
|
17
19
|
*
|
|
18
20
|
* ⛔ **A pre-tick is not a consent** (D34): the working folder's repo starts ticked, and Enter on it leads to the naming
|
|
19
21
|
* y/N, which defaults to N. It is an ADDITION to the diff, never part of the baseline.
|
|
@@ -66,9 +68,16 @@ export interface TurnedOffRow {
|
|
|
66
68
|
/** The working folder's repo, turned off: `connect` inside it pre-selects the `turn on` (D34 + the re-include rule). */
|
|
67
69
|
readonly pre: boolean;
|
|
68
70
|
}
|
|
71
|
+
/** One account grant this machine has not named (slice 2): an owner (`key` without its slash) or a repo (`key` is the slug). */
|
|
72
|
+
export interface AccountRow {
|
|
73
|
+
readonly kind: "owner" | "repo";
|
|
74
|
+
readonly key: string;
|
|
75
|
+
}
|
|
69
76
|
export interface PickerModel {
|
|
70
77
|
readonly onNow: readonly OnNowRow[];
|
|
71
78
|
readonly owners: readonly OwnerGroup[];
|
|
79
|
+
/** What the account grants that this machine has not named. Empty before a sync, or on a machine with no credential. */
|
|
80
|
+
readonly account: readonly AccountRow[];
|
|
72
81
|
readonly turnedOff: readonly TurnedOffRow[];
|
|
73
82
|
/** The picker's `Not listed here:` lines (no remote, can't be matched, unreadable agents). */
|
|
74
83
|
readonly notListed: readonly string[];
|
|
@@ -114,6 +123,8 @@ export interface ModelInput {
|
|
|
114
123
|
readonly notListed: readonly string[];
|
|
115
124
|
readonly sawNothing: boolean;
|
|
116
125
|
readonly here: HereRepo | null;
|
|
126
|
+
/** The account's grants this machine has not named, as `accountView` lists them: `owner/` for an owner, the slug for a repo. */
|
|
127
|
+
readonly accountNotHere?: readonly string[];
|
|
117
128
|
}
|
|
118
129
|
export declare function buildModel(input: ModelInput): PickerModel;
|
|
119
130
|
/** What a number on screen stands for. On-now rows come first, then each owner's repos, then turned-off. */
|
|
@@ -123,6 +134,9 @@ export type RowRef = {
|
|
|
123
134
|
} | {
|
|
124
135
|
readonly kind: "repo";
|
|
125
136
|
readonly repo: PickRepo;
|
|
137
|
+
} | {
|
|
138
|
+
readonly kind: "account";
|
|
139
|
+
readonly row: AccountRow;
|
|
126
140
|
} | {
|
|
127
141
|
readonly kind: "off";
|
|
128
142
|
readonly row: TurnedOffRow;
|
|
@@ -136,6 +150,10 @@ export type Parsed = {
|
|
|
136
150
|
readonly kind: "empty";
|
|
137
151
|
} | {
|
|
138
152
|
readonly kind: "more";
|
|
153
|
+
}
|
|
154
|
+
/** `g`, alone, where it was offered (slice 2, exception 3): use the repositories the account already grants. */
|
|
155
|
+
| {
|
|
156
|
+
readonly kind: "account";
|
|
139
157
|
} | {
|
|
140
158
|
readonly kind: "change";
|
|
141
159
|
readonly change: PickerChange;
|
|
@@ -148,13 +166,19 @@ export type Parsed = {
|
|
|
148
166
|
* Parse one typed answer (D8 · 4A). ⛔ **Never applies part of an answer:** any token that is not understood returns
|
|
149
167
|
* `error` naming each such token, and nothing is chosen. `owner/` and `owner/repo` are always accepted, a clone here or
|
|
150
168
|
* not, because a new laptop needs them; the grant block flags `(no clone here yet)`.
|
|
169
|
+
*
|
|
170
|
+
* ⛔ **`g` STANDS ALONE** (exception 3, R3-12): where `offerAccount` is true, `g` alone is the account's grants, and `g` mixed with
|
|
171
|
+
* any other token is refused with nothing applied, so the y/N after it always names a single list. Where it was not offered `g` is
|
|
172
|
+
* an ordinary token nobody understands.
|
|
151
173
|
*/
|
|
152
|
-
export declare function parseTyped(input: string, model: PickerModel, showAll: boolean): Parsed;
|
|
174
|
+
export declare function parseTyped(input: string, model: PickerModel, showAll: boolean, offerAccount?: boolean): Parsed;
|
|
153
175
|
export interface RenderOptions {
|
|
154
176
|
readonly colour: boolean;
|
|
155
177
|
readonly nowMs: number;
|
|
156
178
|
/** Show every repo, numbered afresh (the `more` token). */
|
|
157
179
|
readonly showAll: boolean;
|
|
180
|
+
/** Slice 2: the prompt block's `g` line (screen 1a). Only on a machine with no credential, a terminal and a browser to sign in with. */
|
|
181
|
+
readonly offerAccount?: boolean;
|
|
158
182
|
}
|
|
159
183
|
/** The `Not listed here:` block, muted, or nothing. */
|
|
160
184
|
export declare function notListedBlock(model: PickerModel, colour: boolean): string[];
|
|
@@ -168,13 +192,17 @@ export interface BlockFacts {
|
|
|
168
192
|
readonly reposHere: ReadonlyMap<string, number>;
|
|
169
193
|
/** Typed `owner/` and `owner/repo` with no clone here. */
|
|
170
194
|
readonly noClone: ReadonlySet<string>;
|
|
195
|
+
/** `turn on` slugs the ACCOUNT has turned off: the row says so, because the yes re-includes them on every machine. */
|
|
196
|
+
readonly accountOff?: ReadonlySet<string>;
|
|
171
197
|
}
|
|
172
198
|
/**
|
|
173
199
|
* The `You chose:` block — the thing the y/N is asking about (D7 · 3A), on the §13.5 label gutter. One label per kind of
|
|
174
200
|
* change; an owner row says how many repos are here and that later clones are covered, and a typed name with no clone
|
|
175
201
|
* on this machine says so, so a typo shows BEFORE the question.
|
|
176
202
|
*/
|
|
177
|
-
export declare function renderGrantBlock(change: PickerChange, facts: BlockFacts, colour: boolean
|
|
203
|
+
export declare function renderGrantBlock(change: PickerChange, facts: BlockFacts, colour: boolean,
|
|
204
|
+
/** `You chose:` for the person's picks; the account's own list (`g`, screen 1b) is headed `Your account grants:`. */
|
|
205
|
+
heading?: string): string[];
|
|
178
206
|
/** The grant y/N: additions (with or without removals) get the upload question, removals alone the stop question. */
|
|
179
207
|
export declare function grantQuestion(change: PickerChange, colour: boolean, answers: string): string;
|
|
180
208
|
export {};
|
package/dist/picker.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* row (D9: at most 8 repos before `+ N more`).
|
|
15
15
|
* turned off slugs in `excluded`. Choosing one is a re-include; the grant block names it on a `turn on` row, inside
|
|
16
16
|
* the same y/N (D7 · 3A), and only a yes clears the exclusion.
|
|
17
|
+
* on your account (slice 2) what the account grants that this machine has not named. Unticked until chosen, like any
|
|
18
|
+
* row: choosing one is an ordinary addition, so it goes through the same disclosure and y/N (R3-8, R3-9).
|
|
17
19
|
*
|
|
18
20
|
* ⛔ **A pre-tick is not a consent** (D34): the working folder's repo starts ticked, and Enter on it leads to the naming
|
|
19
21
|
* y/N, which defaults to N. It is an ADDITION to the diff, never part of the baseline.
|
|
@@ -152,9 +154,19 @@ export function buildModel(input) {
|
|
|
152
154
|
const turnedOff = [...excluded]
|
|
153
155
|
.sort()
|
|
154
156
|
.map((slug) => ({ slug, counts: countsOf(slug), pre: slug === hereSlug }));
|
|
157
|
+
// ⛔ THE SCAN ROW WINS: an owner that already has a group above, and a repo already listed under one, are not repeated under "on
|
|
158
|
+
// your account" — choosing the scan row is the same change, and one row per thing is what a person can read.
|
|
159
|
+
const shownOwners = new Set(owners.map((group) => group.owner));
|
|
160
|
+
const shownRepos = new Set(owners.flatMap((group) => group.repos.map((repo) => repo.slug)));
|
|
161
|
+
const account = (input.accountNotHere ?? [])
|
|
162
|
+
.map((name) => name.endsWith("/")
|
|
163
|
+
? { kind: "owner", key: normalizeOwner(name.slice(0, -1)) }
|
|
164
|
+
: { kind: "repo", key: normalizeSlug(name) })
|
|
165
|
+
.filter((row) => (row.kind === "owner" ? !shownOwners.has(row.key) : !shownRepos.has(row.key)));
|
|
155
166
|
return {
|
|
156
167
|
onNow,
|
|
157
168
|
owners,
|
|
169
|
+
account,
|
|
158
170
|
turnedOff,
|
|
159
171
|
notListed: input.notListed,
|
|
160
172
|
found: input.repos.length,
|
|
@@ -172,6 +184,8 @@ export function numberedRows(model, showAll) {
|
|
|
172
184
|
for (const repo of shown)
|
|
173
185
|
rows.push({ kind: "repo", repo });
|
|
174
186
|
}
|
|
187
|
+
for (const row of model.account)
|
|
188
|
+
rows.push({ kind: "account", row });
|
|
175
189
|
for (const row of model.turnedOff)
|
|
176
190
|
rows.push({ kind: "off", row });
|
|
177
191
|
return rows;
|
|
@@ -182,13 +196,20 @@ const REPO_TOKEN = /^[^\s/]+(?:\/[^\s/]+)*\/[^\s/]+$/u;
|
|
|
182
196
|
* Parse one typed answer (D8 · 4A). ⛔ **Never applies part of an answer:** any token that is not understood returns
|
|
183
197
|
* `error` naming each such token, and nothing is chosen. `owner/` and `owner/repo` are always accepted, a clone here or
|
|
184
198
|
* not, because a new laptop needs them; the grant block flags `(no clone here yet)`.
|
|
199
|
+
*
|
|
200
|
+
* ⛔ **`g` STANDS ALONE** (exception 3, R3-12): where `offerAccount` is true, `g` alone is the account's grants, and `g` mixed with
|
|
201
|
+
* any other token is refused with nothing applied, so the y/N after it always names a single list. Where it was not offered `g` is
|
|
202
|
+
* an ordinary token nobody understands.
|
|
185
203
|
*/
|
|
186
|
-
export function parseTyped(input, model, showAll) {
|
|
204
|
+
export function parseTyped(input, model, showAll, offerAccount = false) {
|
|
187
205
|
const tokens = input.split(/[\s,]+/u).filter((t) => t !== "");
|
|
188
206
|
if (tokens.length === 0)
|
|
189
207
|
return { kind: "empty" };
|
|
190
208
|
if (tokens.length === 1 && tokens[0].toLowerCase() === "more")
|
|
191
209
|
return { kind: "more" };
|
|
210
|
+
if (offerAccount && tokens.some((t) => t.toLowerCase() === "g")) {
|
|
211
|
+
return tokens.length === 1 ? { kind: "account" } : { kind: "error", tokens: [PICKER.gMixedToken] };
|
|
212
|
+
}
|
|
192
213
|
const rows = numberedRows(model, showAll);
|
|
193
214
|
const bad = [];
|
|
194
215
|
const addOwners = new Set();
|
|
@@ -236,6 +257,12 @@ export function parseTyped(input, model, showAll) {
|
|
|
236
257
|
bad.push(PICKER.alreadyOn(token, `-${token}`));
|
|
237
258
|
else if (row.kind === "off")
|
|
238
259
|
turnOn.add(row.row.slug);
|
|
260
|
+
else if (row.kind === "account") {
|
|
261
|
+
if (row.row.kind === "owner")
|
|
262
|
+
addOwners.add(row.row.key);
|
|
263
|
+
else
|
|
264
|
+
addSlug(row.row.key, token);
|
|
265
|
+
}
|
|
239
266
|
else
|
|
240
267
|
addSlug(row.repo.slug, token);
|
|
241
268
|
}
|
|
@@ -377,7 +404,11 @@ export function notListedBlock(model, colour) {
|
|
|
377
404
|
export function renderTyped(model, options) {
|
|
378
405
|
const { colour, nowMs, showAll } = options;
|
|
379
406
|
const lines = [""];
|
|
380
|
-
if (model.found === 0 &&
|
|
407
|
+
if (model.found === 0 &&
|
|
408
|
+
model.onNow.length === 0 &&
|
|
409
|
+
model.turnedOff.length === 0 &&
|
|
410
|
+
model.owners.length === 0 &&
|
|
411
|
+
model.account.length === 0) {
|
|
381
412
|
return [
|
|
382
413
|
...lines,
|
|
383
414
|
...wrap(model.sawNothing ? PICKER.emptyNone : SCAN.noneListable, 2).map((l) => paint(colour, "strong", l)),
|
|
@@ -385,6 +416,7 @@ export function renderTyped(model, options) {
|
|
|
385
416
|
...notListedBlock(model, colour),
|
|
386
417
|
"",
|
|
387
418
|
...wrap(model.sawNothing ? PICKER.emptyType : PICKER.emptyListable, 2),
|
|
419
|
+
...(options.offerAccount === true ? wrap(PICKER.gLine, 2) : []),
|
|
388
420
|
` ${PICKER.enterNothing}`,
|
|
389
421
|
];
|
|
390
422
|
}
|
|
@@ -392,12 +424,17 @@ export function renderTyped(model, options) {
|
|
|
392
424
|
const onNow = model.onNow.map((row) => onNowDisplay(row, nowMs));
|
|
393
425
|
const shown = (g) => (showAll ? g.repos : g.repos.slice(0, GROUP_CAP));
|
|
394
426
|
const repoRows = model.owners.flatMap((g) => shown(g).map((r) => repoDisplay(r, nowMs)));
|
|
427
|
+
const account = model.account.map((row) => ({
|
|
428
|
+
name: row.kind === "owner" ? `${row.key}/` : row.key,
|
|
429
|
+
meta: PICKER.onAccountHint,
|
|
430
|
+
agents: "",
|
|
431
|
+
}));
|
|
395
432
|
const off = model.turnedOff.map((row) => ({
|
|
396
433
|
name: row.slug,
|
|
397
434
|
meta: PICKER.turnOnHint,
|
|
398
435
|
agents: "",
|
|
399
436
|
}));
|
|
400
|
-
const { cut, column, withAgents } = layout([...onNow, ...repoRows, ...off]);
|
|
437
|
+
const { cut, column, withAgents } = layout([...onNow, ...repoRows, ...account, ...off]);
|
|
401
438
|
const row = (n, display) => `${String(n).padStart(6)} ${paint(colour, "text", cut(display.name).padEnd(column))}${" ".repeat(GAP)}${paint(colour, "muted", withAgents(display))}`;
|
|
402
439
|
let n = 0;
|
|
403
440
|
if (model.onNow.length > 0) {
|
|
@@ -414,6 +451,11 @@ export function renderTyped(model, options) {
|
|
|
414
451
|
lines.push(paint(colour, "muted", ` + ${PICKER.moreTyped(hidden, group.owner)}`));
|
|
415
452
|
}
|
|
416
453
|
}
|
|
454
|
+
if (account.length > 0) {
|
|
455
|
+
lines.push(paint(colour, "muted", ` ${PICKER.onAccount}`));
|
|
456
|
+
for (const d of account)
|
|
457
|
+
lines.push(row(++n, d));
|
|
458
|
+
}
|
|
417
459
|
if (model.turnedOff.length > 0) {
|
|
418
460
|
lines.push(paint(colour, "muted", ` ${PICKER.turnedOff}`));
|
|
419
461
|
for (const d of off)
|
|
@@ -421,6 +463,8 @@ export function renderTyped(model, options) {
|
|
|
421
463
|
}
|
|
422
464
|
lines.push(...notListedBlock(model, colour), "");
|
|
423
465
|
lines.push(` ${paint(colour, "text", PICKER.choose(model.owners.length > 0))}`);
|
|
466
|
+
if (options.offerAccount === true)
|
|
467
|
+
lines.push(...wrap(PICKER.gLine, 2).map((l) => paint(colour, "text", l)));
|
|
424
468
|
if (model.onNow.length > 0)
|
|
425
469
|
lines.push(` ${paint(colour, "text", PICKER.removeHint)}`);
|
|
426
470
|
const pre = preChange(model);
|
|
@@ -437,8 +481,10 @@ export function notUnderstoodLines(tokens) {
|
|
|
437
481
|
* change; an owner row says how many repos are here and that later clones are covered, and a typed name with no clone
|
|
438
482
|
* on this machine says so, so a typo shows BEFORE the question.
|
|
439
483
|
*/
|
|
440
|
-
export function renderGrantBlock(change, facts, colour
|
|
441
|
-
|
|
484
|
+
export function renderGrantBlock(change, facts, colour,
|
|
485
|
+
/** `You chose:` for the person's picks; the account's own list (`g`, screen 1b) is headed `Your account grants:`. */
|
|
486
|
+
heading = PICKER.chose) {
|
|
487
|
+
const lines = [` ${paint(colour, "strong", heading)}`];
|
|
442
488
|
const rows = [
|
|
443
489
|
[
|
|
444
490
|
PICKER.labelOwners,
|
|
@@ -451,7 +497,10 @@ export function renderGrantBlock(change, facts, colour) {
|
|
|
451
497
|
PICKER.labelRepos,
|
|
452
498
|
change.addRepos.map((r) => (facts.noClone.has(r) ? `${r} ${PICKER.noCloneHere}` : r)),
|
|
453
499
|
],
|
|
454
|
-
[
|
|
500
|
+
[
|
|
501
|
+
PICKER.labelTurnOn,
|
|
502
|
+
change.turnOn.map((slug) => (facts.accountOff?.has(slug) === true ? `${slug} ${PICKER.turnedOffOnAccount}` : slug)),
|
|
503
|
+
],
|
|
455
504
|
[
|
|
456
505
|
PICKER.labelStop,
|
|
457
506
|
[
|