@botiverse/hands-cli 0.5.14 → 0.5.16
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 +21 -1
- package/dist/commands/api.d.ts +27 -0
- package/dist/commands/api.js +134 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/login.js +59 -12
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/releases.js +21 -0
- package/dist/commands/releases.js.map +1 -1
- package/dist/commands/whoami.js +2 -2
- package/dist/commands/whoami.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/agent_auth.d.ts +70 -0
- package/dist/lib/agent_auth.js +318 -0
- package/dist/lib/agent_auth.js.map +1 -0
- package/dist/lib/agent_env.d.ts +33 -0
- package/dist/lib/agent_env.js +112 -0
- package/dist/lib/agent_env.js.map +1 -0
- package/dist/lib/agent_refresh.d.ts +32 -0
- package/dist/lib/agent_refresh.js +323 -0
- package/dist/lib/agent_refresh.js.map +1 -0
- package/dist/lib/api.d.ts +8 -0
- package/dist/lib/api.js +62 -23
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/config.d.ts +11 -4
- package/dist/lib/config.js +61 -13
- package/dist/lib/config.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent token auto-refresh + cross-process single-flight (RFC 057, CP3 checkpoint-2).
|
|
3
|
+
*
|
|
4
|
+
* The agent access token is short-lived; a long-lived rotating refresh token in the
|
|
5
|
+
* store renews it. This module:
|
|
6
|
+
* - refreshes PROACTIVELY when the access token is within a skew window of expiry,
|
|
7
|
+
* and REACTIVELY once after a 401 (driven by api.ts);
|
|
8
|
+
* - serializes refresh across concurrent `hands` processes sharing one $SLOCK_HOME
|
|
9
|
+
* via an O_EXCL lock. A concurrent loser re-reads the store and returns ONLY the
|
|
10
|
+
* strictly-newer session the winner persisted (a changed refresh token vs the one
|
|
11
|
+
* it started with); it NEVER rotates in parallel and never re-hands the token it
|
|
12
|
+
* came in with (a double-rotate trips the server's refresh-reuse detection and
|
|
13
|
+
* chain-revokes the family; re-handing a just-401'd token would 401 again).
|
|
14
|
+
*
|
|
15
|
+
* Lock safety:
|
|
16
|
+
* - the refresh op (fetch + bounded body read + parse + persist) is hard-aborted at a
|
|
17
|
+
* deadline, so a live holder cannot hold the lock forever;
|
|
18
|
+
* - a lock is broken ONLY when its owner process is provably DEAD (`process.kill(pid,0)`
|
|
19
|
+
* → ESRCH) — never on elapsed time, so a suspended / stalled but live holder is never
|
|
20
|
+
* stolen;
|
|
21
|
+
* - dead-lock recovery is serialized by an exclusive reaper fence and the main lock is
|
|
22
|
+
* re-acquired WHILE the fence is held, so two concurrent recoverers can never both
|
|
23
|
+
* reap-and-rebuild (→ never double-rotate);
|
|
24
|
+
* - any uncertainty (unparseable owner, non-ENOENT read error, contended/live fence)
|
|
25
|
+
* fails closed: the caller becomes a loser rather than risk an unsafe break.
|
|
26
|
+
*
|
|
27
|
+
* It does its own fetch (not the api client) to avoid an import cycle, and never echoes
|
|
28
|
+
* response bodies in errors.
|
|
29
|
+
*/
|
|
30
|
+
import { openSync, closeSync, writeSync, readFileSync, existsSync, unlinkSync, } from "node:fs";
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
import { randomBytes } from "node:crypto";
|
|
33
|
+
import { agentAuthPath, HANDS_SERVICE, } from "./agent_env.js";
|
|
34
|
+
import { parseAgentSession, writeAgentSession, readBoundedText, } from "./agent_auth.js";
|
|
35
|
+
// Refresh when the access token expires within this window (or is already expired).
|
|
36
|
+
export const REFRESH_SKEW_MS = 60_000;
|
|
37
|
+
// The whole refresh op (fetch + bounded read + parse + persist) is aborted at this
|
|
38
|
+
// deadline. Breaking a lock is NOT time-based, though — only a provably-dead owner is.
|
|
39
|
+
const REFRESH_DEADLINE_MS = 20_000;
|
|
40
|
+
const LOCK_WAIT_MS = 25_000; // how long a loser waits for the winner's strictly-newer session
|
|
41
|
+
const LOCK_POLL_MS = 100;
|
|
42
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
43
|
+
function newOwnerId() {
|
|
44
|
+
return `${process.pid}:${randomBytes(12).toString("hex")}`;
|
|
45
|
+
}
|
|
46
|
+
function readStore(a, service) {
|
|
47
|
+
let path;
|
|
48
|
+
try {
|
|
49
|
+
path = agentAuthPath(a, service);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function accessExpiresWithinSkew(store, now) {
|
|
64
|
+
const exp = Date.parse(store.access_expires_at);
|
|
65
|
+
return !Number.isFinite(exp) || exp - now <= REFRESH_SKEW_MS;
|
|
66
|
+
}
|
|
67
|
+
function accessExpired(store, now) {
|
|
68
|
+
const exp = Date.parse(store.access_expires_at);
|
|
69
|
+
return !Number.isFinite(exp) || exp <= now;
|
|
70
|
+
}
|
|
71
|
+
function lockPath(a, service) {
|
|
72
|
+
return join(dirname(agentAuthPath(a, service)), ".auth.refresh.lock");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* POST the refresh token, validate the session, atomically persist it. Own fetch,
|
|
76
|
+
* hard-aborted at `deadlineMs` across the WHOLE operation (fetch + bounded body read +
|
|
77
|
+
* parse + persist), never following a redirect (a 307/308 would forward the refresh
|
|
78
|
+
* token), and never echoing the body.
|
|
79
|
+
*/
|
|
80
|
+
async function rotate(a, service, store, now, fetchImpl, deadlineMs) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timer = setTimeout(() => controller.abort(), deadlineMs);
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetchImpl(new URL("/api/auth/agent/refresh", store.api_base).toString(), {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
87
|
+
body: JSON.stringify({ schema: "raft-cli-agent-refresh.v1", refresh_token: store.refresh_token }),
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
redirect: "manual",
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
// `manual` leaves a 3xx as a non-ok status. Stable reason only; never echo the body.
|
|
93
|
+
throw new Error(`agent-login: token refresh failed (HTTP ${res.status})`);
|
|
94
|
+
}
|
|
95
|
+
const text = await readBoundedText(res, controller);
|
|
96
|
+
let body;
|
|
97
|
+
try {
|
|
98
|
+
body = JSON.parse(text);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new Error("agent-login: refresh response was not JSON");
|
|
102
|
+
}
|
|
103
|
+
const session = parseAgentSession(body, now);
|
|
104
|
+
writeAgentSession(a, service, session, store.api_base, () => new Date(now).toISOString());
|
|
105
|
+
return session;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
// Deadline stays armed across fetch + bounded read + parse + persist.
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Return a valid access token, refreshing (single-flight) if it is within the skew
|
|
114
|
+
* window. Returns null if there is no stored session (caller must `hands login`).
|
|
115
|
+
*/
|
|
116
|
+
export async function getFreshAgentAccessToken(a, opts = {}) {
|
|
117
|
+
const service = opts.service ?? HANDS_SERVICE;
|
|
118
|
+
const now = opts.now ?? Date.now();
|
|
119
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
120
|
+
const sleepImpl = opts.sleepImpl ?? sleep;
|
|
121
|
+
const deadlineMs = opts.deadlineMs ?? REFRESH_DEADLINE_MS;
|
|
122
|
+
const store = readStore(a, service);
|
|
123
|
+
if (!store)
|
|
124
|
+
return null;
|
|
125
|
+
if (!accessExpiresWithinSkew(store, now))
|
|
126
|
+
return store.access_token; // still fresh
|
|
127
|
+
return singleFlightRefresh(a, service, store, now, fetchImpl, sleepImpl, /*force*/ false, deadlineMs);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Force one refresh (used on a 401 even if the token looked unexpired), single-flight.
|
|
131
|
+
* Returns null if there is no stored session.
|
|
132
|
+
*/
|
|
133
|
+
export async function forceRefreshAgentToken(a, opts = {}) {
|
|
134
|
+
const service = opts.service ?? HANDS_SERVICE;
|
|
135
|
+
const now = opts.now ?? Date.now();
|
|
136
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
137
|
+
const sleepImpl = opts.sleepImpl ?? sleep;
|
|
138
|
+
const deadlineMs = opts.deadlineMs ?? REFRESH_DEADLINE_MS;
|
|
139
|
+
const store = readStore(a, service);
|
|
140
|
+
if (!store)
|
|
141
|
+
return null;
|
|
142
|
+
return singleFlightRefresh(a, service, store, now, fetchImpl, sleepImpl, /*force*/ true, deadlineMs);
|
|
143
|
+
}
|
|
144
|
+
async function singleFlightRefresh(a, service, baseline, now, fetchImpl, sleepImpl, force, deadlineMs) {
|
|
145
|
+
const lock = lockPath(a, service);
|
|
146
|
+
const ownerId = newOwnerId();
|
|
147
|
+
let acquired;
|
|
148
|
+
try {
|
|
149
|
+
const fd = openSync(lock, "wx"); // fast path: no lock present
|
|
150
|
+
try {
|
|
151
|
+
writeSync(fd, ownerId);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
try {
|
|
155
|
+
closeSync(fd);
|
|
156
|
+
}
|
|
157
|
+
catch { /* ignore */ }
|
|
158
|
+
}
|
|
159
|
+
acquired = true;
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
if (e?.code !== "EEXIST")
|
|
163
|
+
throw e;
|
|
164
|
+
// A lock exists: recover it only if its owner is provably dead, under an exclusive
|
|
165
|
+
// fence, re-acquiring the main lock while the fence is held. Anything else → loser.
|
|
166
|
+
acquired = acquireIfDeadOwner(lock, ownerId);
|
|
167
|
+
if (!acquired)
|
|
168
|
+
return waitForNewerSession(a, service, baseline, now, force, sleepImpl);
|
|
169
|
+
}
|
|
170
|
+
// Winner: we hold `lock` carrying ownerId.
|
|
171
|
+
try {
|
|
172
|
+
const fresh = readStore(a, service);
|
|
173
|
+
if (!fresh)
|
|
174
|
+
return null;
|
|
175
|
+
// A prior holder may have refreshed while we blocked; only rotate if still needed.
|
|
176
|
+
if (!force && !accessExpiresWithinSkew(fresh, now))
|
|
177
|
+
return fresh.access_token;
|
|
178
|
+
const session = await rotate(a, service, fresh, now, fetchImpl, deadlineMs);
|
|
179
|
+
return session.access_token;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
releaseOwnLock(lock, ownerId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Delete the lock only if it still carries OUR owner id (never a foreign holder's). */
|
|
186
|
+
export function releaseOwnLock(lock, ownerId) {
|
|
187
|
+
try {
|
|
188
|
+
if (readFileSync(lock, "utf8") === ownerId)
|
|
189
|
+
unlinkSync(lock);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// already gone, unreadable, or replaced by another owner — leave it be.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Recover a lock whose owner process is provably dead and acquire it, serialized by an
|
|
197
|
+
* exclusive reaper fence. Judge-dead → unlink → re-acquire all happen WHILE the fence is
|
|
198
|
+
* held, so at most one recoverer reaps-and-rebuilds — two concurrent recoverers can never
|
|
199
|
+
* both re-acquire (→ never double-rotate). A LIVE owner (incl. suspended/stalled — pid
|
|
200
|
+
* still exists) is never touched. Any uncertainty fails closed. Returns true iff WE now
|
|
201
|
+
* hold the main lock carrying `ownerId`; false → the caller is a loser.
|
|
202
|
+
*/
|
|
203
|
+
export function acquireIfDeadOwner(lock, ownerId) {
|
|
204
|
+
const ffd = acquireReaperFence(`${lock}.reap`);
|
|
205
|
+
if (ffd === null)
|
|
206
|
+
return false; // live/contended/uncertain fence → loser
|
|
207
|
+
try {
|
|
208
|
+
let owner = "";
|
|
209
|
+
try {
|
|
210
|
+
owner = readFileSync(lock, "utf8");
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
// Only "already gone" is safe to proceed on; any other error is uncertain → loser.
|
|
214
|
+
if (e?.code !== "ENOENT")
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (owner) {
|
|
218
|
+
const pid = ownerPid(owner);
|
|
219
|
+
if (pid === null || ownerAlive(pid))
|
|
220
|
+
return false; // live / uncertain owner → loser
|
|
221
|
+
try {
|
|
222
|
+
unlinkSync(lock);
|
|
223
|
+
}
|
|
224
|
+
catch { /* vanished / already handled */ }
|
|
225
|
+
}
|
|
226
|
+
// Re-acquire the main lock while STILL holding the fence, so no other recoverer can
|
|
227
|
+
// rebuild it underneath us.
|
|
228
|
+
let fd;
|
|
229
|
+
try {
|
|
230
|
+
fd = openSync(lock, "wx");
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
if (e?.code === "EEXIST")
|
|
234
|
+
return false; // lost the race → loser
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
writeSync(fd, ownerId);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
try {
|
|
242
|
+
closeSync(fd);
|
|
243
|
+
}
|
|
244
|
+
catch { /* ignore */ }
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
try {
|
|
250
|
+
closeSync(ffd);
|
|
251
|
+
}
|
|
252
|
+
catch { /* ignore */ }
|
|
253
|
+
try {
|
|
254
|
+
unlinkSync(`${lock}.reap`);
|
|
255
|
+
}
|
|
256
|
+
catch { /* ignore */ }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Acquire the exclusive reaper fence via a single O_EXCL create. On EEXIST — a reaper is
|
|
261
|
+
* active, OR one crashed and left the fence — we ALWAYS fail closed (return null → loser).
|
|
262
|
+
* We deliberately do NOT auto-recover a leftover fence: reading its pid and unlinking it
|
|
263
|
+
* would recurse the very reap race the fence exists to prevent (two recoverers both unlink
|
|
264
|
+
* + recreate, then each deletes the other's live fence). A crashed reaper — the fence is
|
|
265
|
+
* held only across synchronous fs calls, never I/O — leaves a diagnosable fence for manual
|
|
266
|
+
* cleanup, the agreed "prefer fail-closed on reaper residue" over a second grabbable lock.
|
|
267
|
+
* Kernel O_EXCL is the sole mutual exclusion; nothing here reads-pid-then-unlinks.
|
|
268
|
+
*/
|
|
269
|
+
function acquireReaperFence(fence) {
|
|
270
|
+
try {
|
|
271
|
+
const fd = openSync(fence, "wx");
|
|
272
|
+
try {
|
|
273
|
+
writeSync(fd, newOwnerId());
|
|
274
|
+
}
|
|
275
|
+
catch { /* diagnostic marker only; ignore */ }
|
|
276
|
+
return fd;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null; // EEXIST or any error → fail closed (loser); never recover a leftover fence
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function ownerPid(owner) {
|
|
283
|
+
const m = /^(\d+):/.exec(owner);
|
|
284
|
+
if (!m)
|
|
285
|
+
return null;
|
|
286
|
+
const pid = Number(m[1]);
|
|
287
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
288
|
+
}
|
|
289
|
+
function ownerAlive(pid) {
|
|
290
|
+
try {
|
|
291
|
+
process.kill(pid, 0); // signal 0: liveness probe, delivers nothing
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
if (e?.code === "ESRCH")
|
|
296
|
+
return false; // no such process → dead
|
|
297
|
+
return true; // EPERM (exists) or anything unexpected → fail closed = treat as alive
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Loser path: poll the store for the winner's strictly-newer session, identified by a
|
|
302
|
+
* refresh token that differs from the one we started with (`baseline`). Never rotates
|
|
303
|
+
* here. On timeout it FAILS rather than hand back the token we came in with; a proactive
|
|
304
|
+
* caller may still use a genuinely-unexpired current token, but a forced (post-401)
|
|
305
|
+
* caller always fails — re-handing a 401'd token would just 401 again.
|
|
306
|
+
*/
|
|
307
|
+
async function waitForNewerSession(a, service, baseline, now, force, sleepImpl) {
|
|
308
|
+
// Bound by poll count (not wall-clock) so it is deterministic under an injected sleep in
|
|
309
|
+
// tests, while keeping the same real-time budget in production.
|
|
310
|
+
const maxPolls = Math.ceil(LOCK_WAIT_MS / LOCK_POLL_MS);
|
|
311
|
+
for (let i = 0; i < maxPolls; i += 1) {
|
|
312
|
+
await sleepImpl(LOCK_POLL_MS);
|
|
313
|
+
const cur = readStore(a, service);
|
|
314
|
+
if (cur && cur.refresh_token !== baseline.refresh_token) {
|
|
315
|
+
return cur.access_token; // the winner rotated: a strictly-newer session is persisted
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const cur = readStore(a, service);
|
|
319
|
+
if (!force && cur && !accessExpired(cur, now))
|
|
320
|
+
return cur.access_token;
|
|
321
|
+
throw new Error("agent-login: timed out waiting for a concurrent token refresh");
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=agent_refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent_refresh.js","sourceRoot":"","sources":["../../src/lib/agent_refresh.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* Endpoints called by the CLI use `requireAppRole("viewer")` or
|
|
8
8
|
* `requireOrgRole("member")` after the user has logged in via `hands login`.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Run a bearer-parameterized request with agent-aware auth, shared by every CLI HTTP
|
|
12
|
+
* path (`apiRequest`, `apiUploadFile`, and `hands api`): resolve the bearer (proactive
|
|
13
|
+
* refresh in agent mode), and on a 401 in agent mode force ONE refresh and retry once.
|
|
14
|
+
* `doFetch` MUST build a fresh request each call — a request body stream cannot be reused
|
|
15
|
+
* across attempts, so callers that send a body rebuild it inside the thunk.
|
|
16
|
+
*/
|
|
17
|
+
export declare function agentAwareFetch(doFetch: (bearer: string | undefined) => Promise<Response>): Promise<Response>;
|
|
10
18
|
export declare class QuiverApiError extends Error {
|
|
11
19
|
readonly status: number;
|
|
12
20
|
readonly body: unknown;
|
package/dist/lib/api.js
CHANGED
|
@@ -8,10 +8,47 @@
|
|
|
8
8
|
* `requireOrgRole("member")` after the user has logged in via `hands login`.
|
|
9
9
|
*/
|
|
10
10
|
import { resolveApiBase, resolveAuthToken } from "./config.js";
|
|
11
|
+
import { admitAgent } from "./agent_env.js";
|
|
12
|
+
import { getFreshAgentAccessToken, forceRefreshAgentToken } from "./agent_refresh.js";
|
|
11
13
|
import { readEnv } from "./env.js";
|
|
12
14
|
import { Blob } from "node:buffer";
|
|
13
15
|
import { readFile } from "node:fs/promises";
|
|
14
16
|
import { basename } from "node:path";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the bearer for a request. In a managed agent this proactively refreshes the
|
|
19
|
+
* stored Hands token when it is near expiry (single-flight); a broken agent env yields
|
|
20
|
+
* no token (fail closed). Human/CI use the ordinary resolver.
|
|
21
|
+
*/
|
|
22
|
+
async function resolveBearer() {
|
|
23
|
+
const admission = admitAgent();
|
|
24
|
+
if (admission.kind === "agent") {
|
|
25
|
+
return (await getFreshAgentAccessToken(admission.env)) ?? undefined;
|
|
26
|
+
}
|
|
27
|
+
if (admission.kind === "fail_closed")
|
|
28
|
+
return undefined;
|
|
29
|
+
return resolveAuthToken();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run a bearer-parameterized request with agent-aware auth, shared by every CLI HTTP
|
|
33
|
+
* path (`apiRequest`, `apiUploadFile`, and `hands api`): resolve the bearer (proactive
|
|
34
|
+
* refresh in agent mode), and on a 401 in agent mode force ONE refresh and retry once.
|
|
35
|
+
* `doFetch` MUST build a fresh request each call — a request body stream cannot be reused
|
|
36
|
+
* across attempts, so callers that send a body rebuild it inside the thunk.
|
|
37
|
+
*/
|
|
38
|
+
export async function agentAwareFetch(doFetch) {
|
|
39
|
+
let res = await doFetch(await resolveBearer());
|
|
40
|
+
// The proactive refresh in resolveBearer covers near-expiry; this covers a token
|
|
41
|
+
// rejected despite looking unexpired (server-side revocation, clock skew).
|
|
42
|
+
if (res.status === 401) {
|
|
43
|
+
const admission = admitAgent();
|
|
44
|
+
if (admission.kind === "agent") {
|
|
45
|
+
const refreshed = await forceRefreshAgentToken(admission.env);
|
|
46
|
+
if (refreshed)
|
|
47
|
+
res = await doFetch(refreshed);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
15
52
|
export class QuiverApiError extends Error {
|
|
16
53
|
status;
|
|
17
54
|
body;
|
|
@@ -38,23 +75,26 @@ export async function apiRequest(path, opts = {}) {
|
|
|
38
75
|
url.searchParams.set(k, String(v));
|
|
39
76
|
}
|
|
40
77
|
}
|
|
41
|
-
const
|
|
78
|
+
const baseHeaders = {
|
|
42
79
|
accept: "application/json",
|
|
43
80
|
};
|
|
44
|
-
const bearer = resolveAuthToken();
|
|
45
|
-
if (bearer)
|
|
46
|
-
headers.authorization = `Bearer ${bearer}`;
|
|
47
81
|
let body;
|
|
48
82
|
if (opts.body !== undefined) {
|
|
49
|
-
|
|
83
|
+
baseHeaders["content-type"] = "application/json";
|
|
50
84
|
body = JSON.stringify(opts.body);
|
|
51
85
|
}
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
const doFetch = (bearer) => {
|
|
87
|
+
const headers = { ...baseHeaders };
|
|
88
|
+
if (bearer)
|
|
89
|
+
headers.authorization = `Bearer ${bearer}`;
|
|
90
|
+
return fetch(url.toString(), {
|
|
91
|
+
method: opts.method ?? "GET",
|
|
92
|
+
headers,
|
|
93
|
+
...(body !== undefined ? { body } : {}),
|
|
94
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const res = await agentAwareFetch(doFetch);
|
|
58
98
|
if (readEnv("VERBOSE") === "1") {
|
|
59
99
|
console.error(`> ${opts.method ?? "GET"} ${url}`);
|
|
60
100
|
console.error(`< ${res.status}`);
|
|
@@ -81,19 +121,18 @@ export async function apiRequest(path, opts = {}) {
|
|
|
81
121
|
}
|
|
82
122
|
export async function apiUploadFile(path, filePath, fieldName = "apk") {
|
|
83
123
|
const url = new URL(path.startsWith("/") ? path : `/${path}`, getApiBase());
|
|
84
|
-
const form = new FormData();
|
|
85
124
|
const bytes = await readFile(filePath);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
body: form
|
|
125
|
+
const name = basename(filePath);
|
|
126
|
+
// Upload is a primary product path, so it uses the SAME agent-aware request + one-401
|
|
127
|
+
// as every other call. The multipart body is rebuilt per attempt: a body stream can't
|
|
128
|
+
// be reused, and agent mode may retry once after a 401.
|
|
129
|
+
const res = await agentAwareFetch((bearer) => {
|
|
130
|
+
const form = new FormData();
|
|
131
|
+
form.append(fieldName, new Blob([bytes]), name);
|
|
132
|
+
const headers = { accept: "application/json" };
|
|
133
|
+
if (bearer)
|
|
134
|
+
headers.authorization = `Bearer ${bearer}`;
|
|
135
|
+
return fetch(url.toString(), { method: "POST", headers, body: form });
|
|
97
136
|
});
|
|
98
137
|
if (readEnv("VERBOSE") === "1") {
|
|
99
138
|
console.error(`> POST ${url}`);
|
package/dist/lib/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,MAAM,CAAS;IACf,IAAI,CAAU;IACvB,YAAY,MAAc,EAAE,IAAa,EAAE,OAAe;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,IAAI,cAAc,GAAkB,IAAI,CAAC;AAEzC,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,cAAc,GAAG,GAAG,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,cAAc,IAAI,cAAc,EAAE,CAAC;AAC5C,CAAC;AAUD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,OAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;IAC5E,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;gBAAE,SAAS;YAC5C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,MAAM,
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config + auth storage for the
|
|
2
|
+
* Config + auth storage for the hands CLI.
|
|
3
3
|
*
|
|
4
4
|
* Resolution order for any setting (first wins):
|
|
5
5
|
* 1. CLI flag (--api, --token, ...)
|
|
6
6
|
* 2. Environment variable (HANDS_API, HANDS_AUTH_TOKEN, ...)
|
|
7
|
-
* 3.
|
|
7
|
+
* 3. Human config file at $XDG_CONFIG_HOME/hands/auth.json (default ~/.config/hands/auth.json)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Agent mode (managed Raft agent) is ISOLATED: it uses ONLY the per-agent Hands token
|
|
10
|
+
* under $SLOCK_HOME and never reads/writes the human config or ambient env token. A
|
|
11
|
+
* broken (partial/invalid) agent environment fails closed rather than falling back.
|
|
12
|
+
*
|
|
13
|
+
* The human config file holds:
|
|
14
|
+
* - apiBase: the Hands Worker URL the CLI talks to
|
|
11
15
|
* - authToken: the signed Hands JWT returned after `hands login`
|
|
16
|
+
* A legacy ~/.config/quiver/auth.json is migrated to the hands path on first use.
|
|
12
17
|
*/
|
|
13
18
|
export interface CliConfig {
|
|
14
19
|
apiBase?: string;
|
|
@@ -16,6 +21,8 @@ export interface CliConfig {
|
|
|
16
21
|
/** Legacy field read during migration from cookie-backed sessions. */
|
|
17
22
|
sessionCookie?: string;
|
|
18
23
|
}
|
|
24
|
+
/** Canonical human config path (post-rename). */
|
|
25
|
+
export declare function configPath(): string;
|
|
19
26
|
export declare function getConfig(): CliConfig;
|
|
20
27
|
export declare function saveConfig(patch: Partial<CliConfig>): CliConfig;
|
|
21
28
|
export declare function clearConfig(): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,37 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config + auth storage for the
|
|
2
|
+
* Config + auth storage for the hands CLI.
|
|
3
3
|
*
|
|
4
4
|
* Resolution order for any setting (first wins):
|
|
5
5
|
* 1. CLI flag (--api, --token, ...)
|
|
6
6
|
* 2. Environment variable (HANDS_API, HANDS_AUTH_TOKEN, ...)
|
|
7
|
-
* 3.
|
|
7
|
+
* 3. Human config file at $XDG_CONFIG_HOME/hands/auth.json (default ~/.config/hands/auth.json)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Agent mode (managed Raft agent) is ISOLATED: it uses ONLY the per-agent Hands token
|
|
10
|
+
* under $SLOCK_HOME and never reads/writes the human config or ambient env token. A
|
|
11
|
+
* broken (partial/invalid) agent environment fails closed rather than falling back.
|
|
12
|
+
*
|
|
13
|
+
* The human config file holds:
|
|
14
|
+
* - apiBase: the Hands Worker URL the CLI talks to
|
|
11
15
|
* - authToken: the signed Hands JWT returned after `hands login`
|
|
16
|
+
* A legacy ~/.config/quiver/auth.json is migrated to the hands path on first use.
|
|
12
17
|
*/
|
|
13
18
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
14
19
|
import { readEnv } from "./env.js";
|
|
20
|
+
import { admitAgent, readAgentAccessToken, readAgentApiBase, } from "./agent_env.js";
|
|
15
21
|
import { dirname, join } from "node:path";
|
|
16
22
|
import { homedir } from "node:os";
|
|
17
23
|
const DEFAULT_API_BASE = "https://hands.build";
|
|
18
|
-
function
|
|
24
|
+
function configDir() {
|
|
19
25
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
20
|
-
|
|
21
|
-
return join(dir, "quiver", "auth.json");
|
|
26
|
+
return xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
/** Canonical human config path (post-rename). */
|
|
29
|
+
export function configPath() {
|
|
30
|
+
return join(configDir(), "hands", "auth.json");
|
|
31
|
+
}
|
|
32
|
+
/** Legacy path from before the quiver→hands rename; read once, then migrated. */
|
|
33
|
+
function legacyConfigPath() {
|
|
34
|
+
return join(configDir(), "quiver", "auth.json");
|
|
35
|
+
}
|
|
36
|
+
function readConfigFile(path) {
|
|
25
37
|
if (!existsSync(path))
|
|
26
|
-
return
|
|
38
|
+
return null;
|
|
27
39
|
try {
|
|
28
|
-
const
|
|
29
|
-
const parsed = JSON.parse(raw);
|
|
40
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
30
41
|
return parsed ?? {};
|
|
31
42
|
}
|
|
32
43
|
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function getConfig() {
|
|
48
|
+
const path = configPath();
|
|
49
|
+
const current = readConfigFile(path);
|
|
50
|
+
if (current)
|
|
51
|
+
return current;
|
|
52
|
+
// One-time migration from the legacy quiver path. On ANY failure keep the old file
|
|
53
|
+
// and old credentials (return the legacy config; do not delete or corrupt it).
|
|
54
|
+
const legacy = readConfigFile(legacyConfigPath());
|
|
55
|
+
if (!legacy)
|
|
33
56
|
return {};
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
59
|
+
writeFileSync(path, JSON.stringify(legacy, null, 2) + "\n", { mode: 0o600 });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return legacy; // migration failed → keep using the legacy credentials
|
|
34
63
|
}
|
|
64
|
+
return legacy;
|
|
35
65
|
}
|
|
36
66
|
export function saveConfig(patch) {
|
|
37
67
|
const current = getConfig();
|
|
@@ -56,13 +86,31 @@ export function resolveApiBase() {
|
|
|
56
86
|
const env = readEnv("API");
|
|
57
87
|
if (env)
|
|
58
88
|
return env;
|
|
89
|
+
const admission = admitAgent();
|
|
90
|
+
if (admission.kind === "agent") {
|
|
91
|
+
// Agent mode: the api base comes from the agent store (recorded at login), never
|
|
92
|
+
// the human config file.
|
|
93
|
+
return readAgentApiBase(admission.env) ?? DEFAULT_API_BASE;
|
|
94
|
+
}
|
|
95
|
+
if (admission.kind === "fail_closed") {
|
|
96
|
+
return DEFAULT_API_BASE; // broken agent env: no human config fallback
|
|
97
|
+
}
|
|
59
98
|
const cfg = getConfig();
|
|
60
99
|
if (cfg.apiBase)
|
|
61
100
|
return cfg.apiBase;
|
|
62
101
|
return DEFAULT_API_BASE;
|
|
63
102
|
}
|
|
64
103
|
export function resolveAuthToken() {
|
|
65
|
-
|
|
104
|
+
const admission = admitAgent();
|
|
105
|
+
if (admission.kind === "agent") {
|
|
106
|
+
// Isolated: ONLY the per-agent store. Missing/bad → require `hands login`; never
|
|
107
|
+
// fall back to HANDS_AUTH_TOKEN or the human config.
|
|
108
|
+
return readAgentAccessToken(admission.env) ?? undefined;
|
|
109
|
+
}
|
|
110
|
+
if (admission.kind === "fail_closed") {
|
|
111
|
+
return undefined; // broken agent env → no ambient credentials
|
|
112
|
+
}
|
|
113
|
+
// Human / CI (no agent markers): env token wins, then the (migrated) human config.
|
|
66
114
|
const env = readEnv("AUTH_TOKEN") ?? readEnv("BEARER_TOKEN") ?? readEnv("SESSION_COOKIE");
|
|
67
115
|
if (env)
|
|
68
116
|
return env;
|