@scrappycoco/cli 0.8.3 → 0.8.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/README.md +3 -0
- package/dist/index.js +44 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,9 @@ agent, general workflow, or scheduler commands. For repeated checks, callers
|
|
|
66
66
|
schedule ordinary Runs and pass each returned cursor into the next Run.
|
|
67
67
|
|
|
68
68
|
Node.js 20 or newer is required. Interactive use authenticates with Clerk OAuth Authorization Code + PKCE. CI can set `SCRAPPYCOCO_API_KEY`.
|
|
69
|
+
API keys have no refresh-token rotation step and are preferred for unattended
|
|
70
|
+
concurrent application workloads. OAuth refresh is serialized across CLI
|
|
71
|
+
processes and recovers if another process rotates the stored token first.
|
|
69
72
|
|
|
70
73
|
OAuth and API requests have bounded timeouts. Set
|
|
71
74
|
`SCRAPPYCOCO_AUTH_TIMEOUT_MS` or `SCRAPPYCOCO_API_TIMEOUT_MS` only when a slow
|
package/dist/index.js
CHANGED
|
@@ -60,17 +60,23 @@ function wait(milliseconds) {
|
|
|
60
60
|
}
|
|
61
61
|
async function withCredentialRefreshLock(operation) {
|
|
62
62
|
const path = credentialRefreshLockPath();
|
|
63
|
+
const owner = randomUUID();
|
|
63
64
|
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
64
65
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
65
66
|
while (true) {
|
|
66
67
|
try {
|
|
67
68
|
const handle = await open(path, "wx", 384);
|
|
68
69
|
try {
|
|
69
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, created_at: Date.now() }));
|
|
70
|
+
await handle.writeFile(JSON.stringify({ owner, pid: process.pid, created_at: Date.now() }));
|
|
70
71
|
return await operation();
|
|
71
72
|
} finally {
|
|
72
73
|
await handle.close();
|
|
73
|
-
|
|
74
|
+
try {
|
|
75
|
+
const current = JSON.parse(await readFile(path, "utf8"));
|
|
76
|
+
if (current.owner === owner) await rm(path, { force: true });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code !== "ENOENT") throw error;
|
|
79
|
+
}
|
|
74
80
|
}
|
|
75
81
|
} catch (error) {
|
|
76
82
|
const code = error.code;
|
|
@@ -199,6 +205,8 @@ async function clearRefreshToken() {
|
|
|
199
205
|
var cachedAccessToken;
|
|
200
206
|
var refreshesInFlight = /* @__PURE__ */ new Map();
|
|
201
207
|
var DEFAULT_AUTH_HTTP_TIMEOUT_MS = 15e3;
|
|
208
|
+
var ROTATED_TOKEN_RELOAD_ATTEMPTS = 10;
|
|
209
|
+
var ROTATED_TOKEN_RELOAD_DELAY_MS = 100;
|
|
202
210
|
function positiveInteger(value, fallback) {
|
|
203
211
|
if (!value) return fallback;
|
|
204
212
|
const parsed = Number(value);
|
|
@@ -213,6 +221,12 @@ function networkMessage(error) {
|
|
|
213
221
|
}
|
|
214
222
|
return "failed to reach the authentication service";
|
|
215
223
|
}
|
|
224
|
+
function isInvalidGrant(error) {
|
|
225
|
+
return error instanceof CliError && error.exitCode === EXIT.auth && typeof error.details === "object" && error.details !== null && "oauth_error" in error.details && error.details.oauth_error === "invalid_grant";
|
|
226
|
+
}
|
|
227
|
+
function wait2(milliseconds) {
|
|
228
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
229
|
+
}
|
|
216
230
|
async function oauthFetch(url, init, phase, exitCode) {
|
|
217
231
|
try {
|
|
218
232
|
return await fetch(url, {
|
|
@@ -436,7 +450,9 @@ async function accessToken(apiUrl) {
|
|
|
436
450
|
let tokens;
|
|
437
451
|
let acceptedRefreshToken;
|
|
438
452
|
let lastAuthError;
|
|
453
|
+
const attemptedRefreshTokens = /* @__PURE__ */ new Set();
|
|
439
454
|
for (const candidate of candidates) {
|
|
455
|
+
attemptedRefreshTokens.add(candidate.value);
|
|
440
456
|
try {
|
|
441
457
|
tokens = await tokenRequest(config.issuer, new URLSearchParams({
|
|
442
458
|
grant_type: "refresh_token",
|
|
@@ -452,6 +468,28 @@ async function accessToken(apiUrl) {
|
|
|
452
468
|
lastAuthError = error;
|
|
453
469
|
}
|
|
454
470
|
}
|
|
471
|
+
if (!tokens && isInvalidGrant(lastAuthError)) {
|
|
472
|
+
for (let attempt = 0; attempt < ROTATED_TOKEN_RELOAD_ATTEMPTS && !tokens; attempt += 1) {
|
|
473
|
+
await wait2(ROTATED_TOKEN_RELOAD_DELAY_MS);
|
|
474
|
+
const rotatedCandidates = await loadRefreshTokenCandidates();
|
|
475
|
+
const rotated = rotatedCandidates.find(
|
|
476
|
+
(candidate) => !attemptedRefreshTokens.has(candidate.value)
|
|
477
|
+
);
|
|
478
|
+
if (!rotated) continue;
|
|
479
|
+
attemptedRefreshTokens.add(rotated.value);
|
|
480
|
+
try {
|
|
481
|
+
tokens = await tokenRequest(config.issuer, new URLSearchParams({
|
|
482
|
+
grant_type: "refresh_token",
|
|
483
|
+
client_id: config.client_id,
|
|
484
|
+
refresh_token: rotated.value
|
|
485
|
+
}), "token refresh");
|
|
486
|
+
acceptedRefreshToken = rotated.value;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (!isInvalidGrant(error)) throw error;
|
|
489
|
+
lastAuthError = error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
455
493
|
if (!tokens || !acceptedRefreshToken) throw lastAuthError || new CliError("Stored OAuth login is no longer valid.", EXIT.auth);
|
|
456
494
|
await saveRefreshToken(tokens.refresh_token || acceptedRefreshToken);
|
|
457
495
|
cachedAccessToken = {
|
|
@@ -1238,9 +1276,11 @@ async function emitExecution(response, options, command) {
|
|
|
1238
1276
|
await emit(summary, jsonMode);
|
|
1239
1277
|
process.stderr.write(`Saved ${records.length} records to ${options.output}
|
|
1240
1278
|
`);
|
|
1279
|
+
if (response.status === "failed") process.exitCode = EXIT.api;
|
|
1241
1280
|
return;
|
|
1242
1281
|
}
|
|
1243
1282
|
await emit(response, jsonMode);
|
|
1283
|
+
if (response.status === "failed") process.exitCode = EXIT.api;
|
|
1244
1284
|
}
|
|
1245
1285
|
function executionSummary(response) {
|
|
1246
1286
|
const records = response.records;
|
|
@@ -1266,6 +1306,7 @@ function executionSummary(response) {
|
|
|
1266
1306
|
"result_count",
|
|
1267
1307
|
"latency_ms",
|
|
1268
1308
|
"estimated_cost_usd",
|
|
1309
|
+
"provider_http_status",
|
|
1269
1310
|
"error"
|
|
1270
1311
|
]));
|
|
1271
1312
|
}
|
|
@@ -1303,6 +1344,7 @@ function executionSummary(response) {
|
|
|
1303
1344
|
"result_count",
|
|
1304
1345
|
"latency_ms",
|
|
1305
1346
|
"estimated_cost_usd",
|
|
1347
|
+
"provider_http_status",
|
|
1306
1348
|
"error"
|
|
1307
1349
|
]));
|
|
1308
1350
|
}
|