@hardfin/cli 0.1.1-dev.41 → 0.1.1-dev.42
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 +15 -0
- package/dist/cli.js +4 -4
- package/dist/{program-C1-jZp6K.js → program-CqO1St9H.js} +283 -228
- package/package.json +1 -1
- package/skills/hardfin-cli/SKILL.md +12 -8
package/README.md
CHANGED
|
@@ -109,6 +109,17 @@ stderr, so a pipe still carries only the result.
|
|
|
109
109
|
| `requestId` | The API's request ID, or null when the request never reached it |
|
|
110
110
|
| `status` | The HTTP status, or null for a failure caught before any request is made |
|
|
111
111
|
|
|
112
|
+
Three codes cover failures that are easy to mistake for something else.
|
|
113
|
+
|
|
114
|
+
| `code` | Happens when |
|
|
115
|
+
| --------------------- | ------------------------------------------------------------------------------------------- |
|
|
116
|
+
| `unexpected_response` | A 200 whose body is not the API's JSON, such as a proxy's page. It is never an empty result |
|
|
117
|
+
| `timed_out` | The server did not begin to answer within 60 seconds, or 300 for an upload |
|
|
118
|
+
| `file_error` | This machine refused a path the command was given |
|
|
119
|
+
|
|
120
|
+
A change that timed out may still have been made, and the message says so. Read the record
|
|
121
|
+
before sending the change again.
|
|
122
|
+
|
|
112
123
|
An agent branches on `code`. A code is never renamed and never changes meaning, while the
|
|
113
124
|
message may be reworded at any time. `hardfin agent-guide` lists every code with what it
|
|
114
125
|
means, and `src/output/failure.ts` defines them.
|
|
@@ -442,6 +453,10 @@ Nothing secret is printed. An API key and a refresh token are each reported as a
|
|
|
442
453
|
`sha256:` fingerprint, which identifies a credential across two machines without disclosing
|
|
443
454
|
it.
|
|
444
455
|
|
|
456
|
+
`api.authenticated` is `true` only when the API answered who the credential is. A 401 or a
|
|
457
|
+
403 makes it `false`, and `hardfin status` then exits with code 4. It is `null` when the API
|
|
458
|
+
could not be asked, because no answer says nothing about the credential.
|
|
459
|
+
|
|
445
460
|
`refreshExpiresAt` is what the server said when it issued the token. A sign in stored before
|
|
446
461
|
the server reported one falls back to Hardfin's 90-day rule, and `refreshExpiryIsEstimated`
|
|
447
462
|
says which of the two you are looking at.
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c2c38fd6-1925-561f-baf9-51faf8c4c4b8")}catch(e){}}();
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
module.enableCompileCache?.();
|
|
@@ -14,7 +14,7 @@ process.on("unhandledRejection", (error) => void toCrash(error));
|
|
|
14
14
|
try {
|
|
15
15
|
const answer = await toQuickAnswer(process.argv.slice(2));
|
|
16
16
|
if (answer === void 0) {
|
|
17
|
-
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-
|
|
17
|
+
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-CqO1St9H.js");
|
|
18
18
|
try {
|
|
19
19
|
await toCli().parseAsync(process.argv);
|
|
20
20
|
} catch (error) {
|
|
@@ -33,7 +33,7 @@ async function toCrash(error) {
|
|
|
33
33
|
isCrashing = true;
|
|
34
34
|
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
35
35
|
try {
|
|
36
|
-
const { toReported } = await import("./program-
|
|
36
|
+
const { toReported } = await import("./program-CqO1St9H.js");
|
|
37
37
|
await toReported(error, false, void 0, "fatal");
|
|
38
38
|
} finally {
|
|
39
39
|
process.exit(1);
|
|
@@ -51,4 +51,4 @@ async function toQuickAnswer(args) {
|
|
|
51
51
|
export {};
|
|
52
52
|
|
|
53
53
|
//# sourceMappingURL=cli.js.map
|
|
54
|
-
//# debugId=
|
|
54
|
+
//# debugId=c2c38fd6-1925-561f-baf9-51faf8c4c4b8
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0a793a7f-2311-5aaf-b27d-0877574e2d18")}catch(e){}}();
|
|
4
4
|
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BN3gyBpR.js";
|
|
5
5
|
import { n as toAnswer$1, r as toCompletion, t as PATH_VALUES } from "./completion-tree-DLC6ARLR.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -261,30 +261,208 @@ function toOrigin(apiUrl) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
//#endregion
|
|
264
|
-
//#region src/
|
|
264
|
+
//#region src/http/reach.ts
|
|
265
|
+
/** Unreachable is a request that never reached a server, so there is no answer to report. */
|
|
266
|
+
var Unreachable = class extends ExpectedFailure {
|
|
267
|
+
constructor(message) {
|
|
268
|
+
super(message);
|
|
269
|
+
this.name = "Unreachable";
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const OFFLINE = "Couldn't connect to Hardfin. Check your internet connection and try again.";
|
|
273
|
+
const INSECURE = "Couldn't connect to Hardfin securely, so nothing was sent. A work network or security software may be blocking the connection. If this keeps happening, contact your IT team.";
|
|
274
|
+
/** The reasons Node gives, by the code on the error behind fetch's own. */
|
|
275
|
+
const REASONS = {
|
|
276
|
+
ECONNREFUSED: {
|
|
277
|
+
message: OFFLINE,
|
|
278
|
+
detail: "connection refused"
|
|
279
|
+
},
|
|
280
|
+
ENOTFOUND: {
|
|
281
|
+
message: OFFLINE,
|
|
282
|
+
detail: "address not found"
|
|
283
|
+
},
|
|
284
|
+
EAI_AGAIN: {
|
|
285
|
+
message: OFFLINE,
|
|
286
|
+
detail: "address lookup failed"
|
|
287
|
+
},
|
|
288
|
+
ETIMEDOUT: {
|
|
289
|
+
message: OFFLINE,
|
|
290
|
+
detail: "timed out"
|
|
291
|
+
},
|
|
292
|
+
UND_ERR_CONNECT_TIMEOUT: {
|
|
293
|
+
message: OFFLINE,
|
|
294
|
+
detail: "timed out"
|
|
295
|
+
},
|
|
296
|
+
EHOSTUNREACH: {
|
|
297
|
+
message: OFFLINE,
|
|
298
|
+
detail: "no route to host"
|
|
299
|
+
},
|
|
300
|
+
ENETUNREACH: {
|
|
301
|
+
message: OFFLINE,
|
|
302
|
+
detail: "network unreachable"
|
|
303
|
+
},
|
|
304
|
+
ECONNRESET: {
|
|
305
|
+
message: OFFLINE,
|
|
306
|
+
detail: "connection closed"
|
|
307
|
+
},
|
|
308
|
+
UND_ERR_SOCKET: {
|
|
309
|
+
message: OFFLINE,
|
|
310
|
+
detail: "connection closed"
|
|
311
|
+
},
|
|
312
|
+
DEPTH_ZERO_SELF_SIGNED_CERT: {
|
|
313
|
+
message: INSECURE,
|
|
314
|
+
detail: "certificate not trusted"
|
|
315
|
+
},
|
|
316
|
+
SELF_SIGNED_CERT_IN_CHAIN: {
|
|
317
|
+
message: INSECURE,
|
|
318
|
+
detail: "certificate not trusted"
|
|
319
|
+
},
|
|
320
|
+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: {
|
|
321
|
+
message: INSECURE,
|
|
322
|
+
detail: "certificate not trusted"
|
|
323
|
+
},
|
|
324
|
+
UNABLE_TO_GET_ISSUER_CERT_LOCALLY: {
|
|
325
|
+
message: INSECURE,
|
|
326
|
+
detail: "certificate not trusted"
|
|
327
|
+
},
|
|
328
|
+
CERT_HAS_EXPIRED: {
|
|
329
|
+
message: INSECURE,
|
|
330
|
+
detail: "certificate expired"
|
|
331
|
+
},
|
|
332
|
+
ERR_TLS_CERT_ALTNAME_INVALID: {
|
|
333
|
+
message: INSECURE,
|
|
334
|
+
detail: "certificate is for another host"
|
|
335
|
+
},
|
|
336
|
+
ERR_SSL_WRONG_VERSION_NUMBER: {
|
|
337
|
+
message: OFFLINE,
|
|
338
|
+
detail: "not an HTTPS server"
|
|
339
|
+
},
|
|
340
|
+
EPROTO: {
|
|
341
|
+
message: OFFLINE,
|
|
342
|
+
detail: "not an HTTPS server"
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
/** How long a server may take to begin its answer, before the request is given up on. */
|
|
346
|
+
const ANSWER_TIMEOUT_MS = 6e4;
|
|
265
347
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
348
|
+
* send is fetch, with a failure to connect reported in words anyone can act on. It gives up
|
|
349
|
+
* on a server that has not begun to answer in time, because a caller with no person watching
|
|
350
|
+
* would otherwise wait on a stalled connection for as long as it stayed open.
|
|
268
351
|
*/
|
|
269
|
-
function
|
|
270
|
-
const
|
|
271
|
-
|
|
352
|
+
async function send(url, init, timeoutMs = ANSWER_TIMEOUT_MS) {
|
|
353
|
+
const deadline = new AbortController();
|
|
354
|
+
const timer = setTimeout(() => {
|
|
355
|
+
deadline.abort();
|
|
356
|
+
}, timeoutMs);
|
|
272
357
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
358
|
+
return await fetch(url, {
|
|
359
|
+
...init,
|
|
360
|
+
signal: init?.signal ? AbortSignal.any([init.signal, deadline.signal]) : deadline.signal
|
|
361
|
+
});
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (deadline.signal.aborted) throw toTimedOut(url, init?.method ?? "GET", timeoutMs);
|
|
364
|
+
throw toUnreachable(url, error);
|
|
365
|
+
} finally {
|
|
366
|
+
clearTimeout(timer);
|
|
282
367
|
}
|
|
283
368
|
}
|
|
284
|
-
/**
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
return
|
|
369
|
+
/** toTimedOut describes a request given up on, and says when it may have gone through anyway. */
|
|
370
|
+
function toTimedOut(url, method, timeoutMs) {
|
|
371
|
+
const waited = `${new URL(url).host} did not answer within ${timeoutMs / 1e3} seconds`;
|
|
372
|
+
return new TimedOut(["GET", "HEAD"].includes(method.toUpperCase()) ? waited : `${waited}. The change may still have been made, so read the record before sending it again`);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* toUnreachable describes a request that could not connect. Fetch reports every such failure
|
|
376
|
+
* as "fetch failed", and the reason is on the error it wraps. The person reads a plain
|
|
377
|
+
* message, and the line after it names the reason and host for whoever helps them.
|
|
378
|
+
*/
|
|
379
|
+
function toUnreachable(url, error) {
|
|
380
|
+
const host = new URL(url).host;
|
|
381
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
382
|
+
const code = cause instanceof Error ? cause.code : void 0;
|
|
383
|
+
const reason = (error instanceof Error && error.name === "TimeoutError" ? REASONS["ETIMEDOUT"] : code === void 0 ? void 0 : REASONS[code]) ?? {
|
|
384
|
+
message: OFFLINE,
|
|
385
|
+
detail: cause instanceof Error ? cause.message : error instanceof Error ? error.message : String(error)
|
|
386
|
+
};
|
|
387
|
+
return new Unreachable(`${reason.message}\nDetails: ${reason.detail} (${host})`);
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/auth/grant.ts
|
|
391
|
+
const TokenResponse = z.looseObject({
|
|
392
|
+
access_token: z.string(),
|
|
393
|
+
token_type: z.string(),
|
|
394
|
+
expires_in: z.number().optional(),
|
|
395
|
+
refresh_token: z.string().optional(),
|
|
396
|
+
refresh_token_expires_in: z.number().optional(),
|
|
397
|
+
scope: z.string().optional()
|
|
398
|
+
});
|
|
399
|
+
/** GrantFailure is a token request the authorization server refused. */
|
|
400
|
+
var GrantFailure = class extends ExpectedFailure {
|
|
401
|
+
code;
|
|
402
|
+
constructor(code, description) {
|
|
403
|
+
super(description ? `${code}: ${description}` : code);
|
|
404
|
+
this.name = "GrantFailure";
|
|
405
|
+
this.code = code;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
/** SignInStopped is a sign in the person ended themselves, in a way other than Ctrl-C. */
|
|
409
|
+
var SignInStopped = class extends ExpectedFailure {
|
|
410
|
+
constructor(message) {
|
|
411
|
+
super(message);
|
|
412
|
+
this.name = "SignInStopped";
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
/** toTokensFromCode trades an authorization code for tokens. */
|
|
416
|
+
async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
417
|
+
return await toTokens(tokenUrl, {
|
|
418
|
+
grant_type: "authorization_code",
|
|
419
|
+
client_id: clientId,
|
|
420
|
+
code,
|
|
421
|
+
redirect_uri: redirectUri,
|
|
422
|
+
code_verifier: pkce.verifier
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/** toTokensFromRefresh trades a refresh token for a fresh pair. */
|
|
426
|
+
async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
|
|
427
|
+
return await toTokens(tokenUrl, {
|
|
428
|
+
grant_type: "refresh_token",
|
|
429
|
+
client_id: clientId,
|
|
430
|
+
refresh_token: refreshToken
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/** revoke tells the server to forget a token, so signing out reaches every machine. */
|
|
434
|
+
async function revoke(revocationUrl, clientId, token) {
|
|
435
|
+
await send(revocationUrl, {
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
438
|
+
body: new URLSearchParams({
|
|
439
|
+
client_id: clientId,
|
|
440
|
+
token,
|
|
441
|
+
token_type_hint: "refresh_token"
|
|
442
|
+
})
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
|
|
446
|
+
async function toTokens(url, form) {
|
|
447
|
+
const response = await send(url, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers: {
|
|
450
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
451
|
+
Accept: "application/json"
|
|
452
|
+
},
|
|
453
|
+
body: new URLSearchParams(form)
|
|
454
|
+
});
|
|
455
|
+
const body = await response.json().catch(() => void 0);
|
|
456
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
457
|
+
const parsed = TokenResponse.safeParse(body);
|
|
458
|
+
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
459
|
+
return {
|
|
460
|
+
accessToken: parsed.data.access_token,
|
|
461
|
+
refreshToken: parsed.data.refresh_token,
|
|
462
|
+
refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
|
|
463
|
+
scope: parsed.data.scope,
|
|
464
|
+
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
465
|
+
};
|
|
288
466
|
}
|
|
289
467
|
//#endregion
|
|
290
468
|
//#region src/credential/store.ts
|
|
@@ -418,111 +596,6 @@ function writeFile(held) {
|
|
|
418
596
|
renameSync(pending, path);
|
|
419
597
|
}
|
|
420
598
|
//#endregion
|
|
421
|
-
//#region src/http/reach.ts
|
|
422
|
-
/** Unreachable is a request that never reached a server, so there is no answer to report. */
|
|
423
|
-
var Unreachable = class extends ExpectedFailure {
|
|
424
|
-
constructor(message) {
|
|
425
|
-
super(message);
|
|
426
|
-
this.name = "Unreachable";
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
const OFFLINE = "Couldn't connect to Hardfin. Check your internet connection and try again.";
|
|
430
|
-
const INSECURE = "Couldn't connect to Hardfin securely, so nothing was sent. A work network or security software may be blocking the connection. If this keeps happening, contact your IT team.";
|
|
431
|
-
/** The reasons Node gives, by the code on the error behind fetch's own. */
|
|
432
|
-
const REASONS = {
|
|
433
|
-
ECONNREFUSED: {
|
|
434
|
-
message: OFFLINE,
|
|
435
|
-
detail: "connection refused"
|
|
436
|
-
},
|
|
437
|
-
ENOTFOUND: {
|
|
438
|
-
message: OFFLINE,
|
|
439
|
-
detail: "address not found"
|
|
440
|
-
},
|
|
441
|
-
EAI_AGAIN: {
|
|
442
|
-
message: OFFLINE,
|
|
443
|
-
detail: "address lookup failed"
|
|
444
|
-
},
|
|
445
|
-
ETIMEDOUT: {
|
|
446
|
-
message: OFFLINE,
|
|
447
|
-
detail: "timed out"
|
|
448
|
-
},
|
|
449
|
-
UND_ERR_CONNECT_TIMEOUT: {
|
|
450
|
-
message: OFFLINE,
|
|
451
|
-
detail: "timed out"
|
|
452
|
-
},
|
|
453
|
-
EHOSTUNREACH: {
|
|
454
|
-
message: OFFLINE,
|
|
455
|
-
detail: "no route to host"
|
|
456
|
-
},
|
|
457
|
-
ENETUNREACH: {
|
|
458
|
-
message: OFFLINE,
|
|
459
|
-
detail: "network unreachable"
|
|
460
|
-
},
|
|
461
|
-
ECONNRESET: {
|
|
462
|
-
message: OFFLINE,
|
|
463
|
-
detail: "connection closed"
|
|
464
|
-
},
|
|
465
|
-
UND_ERR_SOCKET: {
|
|
466
|
-
message: OFFLINE,
|
|
467
|
-
detail: "connection closed"
|
|
468
|
-
},
|
|
469
|
-
DEPTH_ZERO_SELF_SIGNED_CERT: {
|
|
470
|
-
message: INSECURE,
|
|
471
|
-
detail: "certificate not trusted"
|
|
472
|
-
},
|
|
473
|
-
SELF_SIGNED_CERT_IN_CHAIN: {
|
|
474
|
-
message: INSECURE,
|
|
475
|
-
detail: "certificate not trusted"
|
|
476
|
-
},
|
|
477
|
-
UNABLE_TO_VERIFY_LEAF_SIGNATURE: {
|
|
478
|
-
message: INSECURE,
|
|
479
|
-
detail: "certificate not trusted"
|
|
480
|
-
},
|
|
481
|
-
UNABLE_TO_GET_ISSUER_CERT_LOCALLY: {
|
|
482
|
-
message: INSECURE,
|
|
483
|
-
detail: "certificate not trusted"
|
|
484
|
-
},
|
|
485
|
-
CERT_HAS_EXPIRED: {
|
|
486
|
-
message: INSECURE,
|
|
487
|
-
detail: "certificate expired"
|
|
488
|
-
},
|
|
489
|
-
ERR_TLS_CERT_ALTNAME_INVALID: {
|
|
490
|
-
message: INSECURE,
|
|
491
|
-
detail: "certificate is for another host"
|
|
492
|
-
},
|
|
493
|
-
ERR_SSL_WRONG_VERSION_NUMBER: {
|
|
494
|
-
message: OFFLINE,
|
|
495
|
-
detail: "not an HTTPS server"
|
|
496
|
-
},
|
|
497
|
-
EPROTO: {
|
|
498
|
-
message: OFFLINE,
|
|
499
|
-
detail: "not an HTTPS server"
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
/** send is fetch, with a failure to connect reported in words anyone can act on. */
|
|
503
|
-
async function send(url, init) {
|
|
504
|
-
try {
|
|
505
|
-
return await fetch(url, init);
|
|
506
|
-
} catch (error) {
|
|
507
|
-
throw toUnreachable(url, error);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* toUnreachable describes a request that could not connect. Fetch reports every such failure
|
|
512
|
-
* as "fetch failed", and the reason is on the error it wraps. The person reads a plain
|
|
513
|
-
* message, and the line after it names the reason and host for whoever helps them.
|
|
514
|
-
*/
|
|
515
|
-
function toUnreachable(url, error) {
|
|
516
|
-
const host = new URL(url).host;
|
|
517
|
-
const cause = error instanceof Error ? error.cause : void 0;
|
|
518
|
-
const code = cause instanceof Error ? cause.code : void 0;
|
|
519
|
-
const reason = (error instanceof Error && error.name === "TimeoutError" ? REASONS["ETIMEDOUT"] : code === void 0 ? void 0 : REASONS[code]) ?? {
|
|
520
|
-
message: OFFLINE,
|
|
521
|
-
detail: cause instanceof Error ? cause.message : error instanceof Error ? error.message : String(error)
|
|
522
|
-
};
|
|
523
|
-
return new Unreachable(`${reason.message}\nDetails: ${reason.detail} (${host})`);
|
|
524
|
-
}
|
|
525
|
-
//#endregion
|
|
526
599
|
//#region src/auth/metadata.ts
|
|
527
600
|
const METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
528
601
|
const AuthorizationServerMetadata = z.looseObject({
|
|
@@ -591,75 +664,30 @@ function keep(issuer, document) {
|
|
|
591
664
|
} catch {}
|
|
592
665
|
}
|
|
593
666
|
//#endregion
|
|
594
|
-
//#region src/auth/
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
667
|
+
//#region src/auth/jwt.ts
|
|
668
|
+
/**
|
|
669
|
+
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
670
|
+
* signature, because the API is what decides whether a token is good.
|
|
671
|
+
*/
|
|
672
|
+
function toClaims(token) {
|
|
673
|
+
const payload = token.split(".")[1];
|
|
674
|
+
if (!payload) return {};
|
|
675
|
+
try {
|
|
676
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
677
|
+
return {
|
|
678
|
+
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
679
|
+
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
680
|
+
scopes: toScopes(decoded),
|
|
681
|
+
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
682
|
+
};
|
|
683
|
+
} catch {
|
|
684
|
+
return {};
|
|
610
685
|
}
|
|
611
|
-
};
|
|
612
|
-
/** toTokensFromCode trades an authorization code for tokens. */
|
|
613
|
-
async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
614
|
-
return await toTokens(tokenUrl, {
|
|
615
|
-
grant_type: "authorization_code",
|
|
616
|
-
client_id: clientId,
|
|
617
|
-
code,
|
|
618
|
-
redirect_uri: redirectUri,
|
|
619
|
-
code_verifier: pkce.verifier
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
/** toTokensFromRefresh trades a refresh token for a fresh pair. */
|
|
623
|
-
async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
|
|
624
|
-
return await toTokens(tokenUrl, {
|
|
625
|
-
grant_type: "refresh_token",
|
|
626
|
-
client_id: clientId,
|
|
627
|
-
refresh_token: refreshToken
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
/** revoke tells the server to forget a token, so signing out reaches every machine. */
|
|
631
|
-
async function revoke(revocationUrl, clientId, token) {
|
|
632
|
-
await send(revocationUrl, {
|
|
633
|
-
method: "POST",
|
|
634
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
635
|
-
body: new URLSearchParams({
|
|
636
|
-
client_id: clientId,
|
|
637
|
-
token,
|
|
638
|
-
token_type_hint: "refresh_token"
|
|
639
|
-
})
|
|
640
|
-
});
|
|
641
686
|
}
|
|
642
|
-
/**
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
headers: {
|
|
647
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
648
|
-
Accept: "application/json"
|
|
649
|
-
},
|
|
650
|
-
body: new URLSearchParams(form)
|
|
651
|
-
});
|
|
652
|
-
const body = await response.json().catch(() => void 0);
|
|
653
|
-
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
654
|
-
const parsed = TokenResponse.safeParse(body);
|
|
655
|
-
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
656
|
-
return {
|
|
657
|
-
accessToken: parsed.data.access_token,
|
|
658
|
-
refreshToken: parsed.data.refresh_token,
|
|
659
|
-
refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
|
|
660
|
-
scope: parsed.data.scope,
|
|
661
|
-
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
662
|
-
};
|
|
687
|
+
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
688
|
+
function toScopes(decoded) {
|
|
689
|
+
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
690
|
+
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
663
691
|
}
|
|
664
692
|
//#endregion
|
|
665
693
|
//#region src/credential/lock.ts
|
|
@@ -916,6 +944,18 @@ var RequestFailure = class extends ExpectedFailure {
|
|
|
916
944
|
this.requestId = requestId;
|
|
917
945
|
}
|
|
918
946
|
};
|
|
947
|
+
/**
|
|
948
|
+
* UnexpectedAnswer is a successful response that is not the API's envelope. A captive portal
|
|
949
|
+
* or a proxy answers 200 with a page of its own, which must not pass for an empty result.
|
|
950
|
+
*/
|
|
951
|
+
var UnexpectedAnswer = class extends ExpectedFailure {
|
|
952
|
+
constructor(host, contentType) {
|
|
953
|
+
super(`${host} answered with ${contentType ?? "something"} that is not the API's JSON, so nothing can be read from it. A proxy, a firewall or a network sign-in page may be answering in its place`);
|
|
954
|
+
this.name = "UnexpectedAnswer";
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
/** How long an upload may take to be answered, which is longer than any other request. */
|
|
958
|
+
const UPLOAD_TIMEOUT_MS = 3e5;
|
|
919
959
|
/** request calls one /v2 endpoint and returns the envelope it answered with. */
|
|
920
960
|
async function request$1(options) {
|
|
921
961
|
const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
|
|
@@ -931,13 +971,14 @@ async function request$1(options) {
|
|
|
931
971
|
signal: options.signal,
|
|
932
972
|
headers,
|
|
933
973
|
body: options.form ?? (options.body === void 0 ? void 0 : JSON.stringify(options.body))
|
|
934
|
-
});
|
|
974
|
+
}, options.form ? UPLOAD_TIMEOUT_MS : void 0);
|
|
935
975
|
if (options.downloads && response.ok) return { data: {
|
|
936
976
|
bytes: Buffer.from(await response.arrayBuffer()),
|
|
937
977
|
contentType: response.headers.get("content-type"),
|
|
938
978
|
fileName: toFileName(response.headers.get("content-disposition"))
|
|
939
979
|
} };
|
|
940
|
-
const
|
|
980
|
+
const text = await response.text();
|
|
981
|
+
const envelope = toEnvelope(text);
|
|
941
982
|
if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
|
|
942
983
|
error: toStatusMessage(response.status),
|
|
943
984
|
statusCode: response.status
|
|
@@ -949,6 +990,7 @@ async function request$1(options) {
|
|
|
949
990
|
statusCode: response.status
|
|
950
991
|
}], envelope?.metadata?.requestId);
|
|
951
992
|
}
|
|
993
|
+
if (envelope === void 0 && text.trim() !== "") throw new UnexpectedAnswer(url.host, response.headers.get("content-type"));
|
|
952
994
|
return envelope ?? { data: null };
|
|
953
995
|
}
|
|
954
996
|
/** toFileName reads the name a download was offered under, when the server names one. */
|
|
@@ -962,7 +1004,8 @@ function toLeadingSlash(path) {
|
|
|
962
1004
|
function toEnvelope(text) {
|
|
963
1005
|
if (text.trim() === "") return;
|
|
964
1006
|
try {
|
|
965
|
-
|
|
1007
|
+
const parsed = JSON.parse(text);
|
|
1008
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
966
1009
|
} catch {
|
|
967
1010
|
return;
|
|
968
1011
|
}
|
|
@@ -973,6 +1016,36 @@ function toStatusMessage(status) {
|
|
|
973
1016
|
return `the request failed with status ${status}`;
|
|
974
1017
|
}
|
|
975
1018
|
//#endregion
|
|
1019
|
+
//#region src/report/classify.ts
|
|
1020
|
+
/**
|
|
1021
|
+
* The codes Node gives when the machine refuses something: permissions, a full or read-only
|
|
1022
|
+
* disk, a path that is missing or of the wrong kind, and a reader that closed the pipe.
|
|
1023
|
+
*/
|
|
1024
|
+
const ENVIRONMENT_CODES = /* @__PURE__ */ new Set([
|
|
1025
|
+
"EACCES",
|
|
1026
|
+
"EPERM",
|
|
1027
|
+
"ENOSPC",
|
|
1028
|
+
"EROFS",
|
|
1029
|
+
"ENOENT",
|
|
1030
|
+
"EISDIR",
|
|
1031
|
+
"ENOTDIR",
|
|
1032
|
+
"EEXIST",
|
|
1033
|
+
"EPIPE"
|
|
1034
|
+
]);
|
|
1035
|
+
/**
|
|
1036
|
+
* isReportable answers whether an error is a defect in the CLI. Everything the CLI anticipated
|
|
1037
|
+
* extends ExpectedFailure, so a failure nobody has classified yet is reported until someone
|
|
1038
|
+
* decides it is expected.
|
|
1039
|
+
*/
|
|
1040
|
+
function isReportable(error) {
|
|
1041
|
+
return !(error instanceof ExpectedFailure) && !isMachineRefusal(error);
|
|
1042
|
+
}
|
|
1043
|
+
/** isMachineRefusal answers whether an error is the machine refusing a file or a folder. */
|
|
1044
|
+
function isMachineRefusal(error) {
|
|
1045
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
1046
|
+
return typeof code === "string" && ENVIRONMENT_CODES.has(code);
|
|
1047
|
+
}
|
|
1048
|
+
//#endregion
|
|
976
1049
|
//#region src/output/failure.ts
|
|
977
1050
|
/** FAILURE_CODES is every code a failure can carry, and none is ever renamed or given a new meaning. */
|
|
978
1051
|
const FAILURE_CODES = {
|
|
@@ -986,6 +1059,7 @@ const FAILURE_CODES = {
|
|
|
986
1059
|
not_confirmed: "The change was not confirmed. Pass --yes to confirm it without a terminal",
|
|
987
1060
|
shell_unavailable: "The shell did not say where its startup file is",
|
|
988
1061
|
invalid_config: "A config file or setting cannot be read",
|
|
1062
|
+
file_error: "This machine refused a file or folder the command needed, such as a path that does not exist",
|
|
989
1063
|
not_signed_in: "No usable credential. Run hardfin login, or set HARDFIN_API_KEY",
|
|
990
1064
|
sign_in_failed: "Signing in did not finish",
|
|
991
1065
|
unreachable: "The CLI could not connect to the server",
|
|
@@ -1001,6 +1075,7 @@ const FAILURE_CODES = {
|
|
|
1001
1075
|
rate_limited: "Too many requests. Wait, then try again (HTTP 429)",
|
|
1002
1076
|
server_error: "The API failed to answer (HTTP 5xx)",
|
|
1003
1077
|
api_error: "The API refused the request with another status",
|
|
1078
|
+
unexpected_response: "The answer was not the API's JSON, as when a proxy or a sign-in page answers in its place",
|
|
1004
1079
|
internal_error: "A problem in hardfin itself, which has been reported"
|
|
1005
1080
|
};
|
|
1006
1081
|
/** toStatusCode names a refusal by the HTTP status the API answered with. */
|
|
@@ -1025,6 +1100,9 @@ function toErrorCode(error) {
|
|
|
1025
1100
|
if (error instanceof Unreachable) return "unreachable";
|
|
1026
1101
|
if (error instanceof TimedOut) return "timed_out";
|
|
1027
1102
|
if (error instanceof LockHeld) return "busy";
|
|
1103
|
+
if (error instanceof UnexpectedAnswer) return "unexpected_response";
|
|
1104
|
+
if (error instanceof GrantFailure || error instanceof DiscoveryFailure || error instanceof SignInStopped) return "sign_in_failed";
|
|
1105
|
+
if (isMachineRefusal(error)) return "file_error";
|
|
1028
1106
|
return "internal_error";
|
|
1029
1107
|
}
|
|
1030
1108
|
//#endregion
|
|
@@ -2496,7 +2574,7 @@ async function runLogin(input) {
|
|
|
2496
2574
|
}
|
|
2497
2575
|
return await toSignedIn(input, metadata, await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce));
|
|
2498
2576
|
} catch (error) {
|
|
2499
|
-
if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
|
|
2577
|
+
if (error instanceof DiscoveryFailure || error instanceof GrantFailure || error instanceof SignInStopped) {
|
|
2500
2578
|
writeFailure("sign_in_failed", error.message, input.isJSON);
|
|
2501
2579
|
return ExitCode.ERROR;
|
|
2502
2580
|
}
|
|
@@ -2542,7 +2620,7 @@ async function runDeviceLogin(input, metadata, scope) {
|
|
|
2542
2620
|
*/
|
|
2543
2621
|
async function toDeviceTokens(approved, typed) {
|
|
2544
2622
|
return await Promise.race([approved, typed.then(() => {
|
|
2545
|
-
throw new
|
|
2623
|
+
throw new SignInStopped("Sign in stopped. The code goes on the page in your browser, not here. Run hardfin login to try again.");
|
|
2546
2624
|
})]);
|
|
2547
2625
|
}
|
|
2548
2626
|
/** toSignedIn stores what a sign in issued, whichever flow issued it. */
|
|
@@ -3210,6 +3288,8 @@ function toServerReport(metadata) {
|
|
|
3210
3288
|
scopes: metadata.scopes_supported ?? []
|
|
3211
3289
|
};
|
|
3212
3290
|
}
|
|
3291
|
+
/** The statuses the API answers when it will not take the credential. */
|
|
3292
|
+
const REFUSED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
3213
3293
|
/** toApiReport asks the API who this credential is, which is the only authoritative answer. */
|
|
3214
3294
|
async function toApiReport(resolved, options) {
|
|
3215
3295
|
const settings = resolved.settings;
|
|
@@ -3222,6 +3302,7 @@ async function toApiReport(resolved, options) {
|
|
|
3222
3302
|
};
|
|
3223
3303
|
const claims = credential.kind === "access token" ? toClaims(credential.value.replace(/^Bearer /, "")) : {};
|
|
3224
3304
|
let isReachable = true;
|
|
3305
|
+
let isAuthenticated = true;
|
|
3225
3306
|
const identity = await request$1({
|
|
3226
3307
|
apiUrl: settings.apiUrl,
|
|
3227
3308
|
credential,
|
|
@@ -3229,12 +3310,13 @@ async function toApiReport(resolved, options) {
|
|
|
3229
3310
|
path: "/token",
|
|
3230
3311
|
signal: options.signal
|
|
3231
3312
|
}).then((envelope) => envelope.data).catch((error) => {
|
|
3232
|
-
isReachable = !(error instanceof Unreachable);
|
|
3313
|
+
isReachable = !(error instanceof Unreachable || error instanceof TimedOut);
|
|
3314
|
+
isAuthenticated = error instanceof RequestFailure && REFUSED_STATUSES.has(error.status) ? false : null;
|
|
3233
3315
|
return { unavailable: error instanceof Error ? error.message : String(error) };
|
|
3234
3316
|
});
|
|
3235
3317
|
return {
|
|
3236
3318
|
reachable: isReachable,
|
|
3237
|
-
authenticated:
|
|
3319
|
+
authenticated: isAuthenticated,
|
|
3238
3320
|
url: settings.apiUrl,
|
|
3239
3321
|
accessTokenExpiresAt: claims.expiresAt === void 0 ? null : new Date(claims.expiresAt).toISOString(),
|
|
3240
3322
|
accessTokenScopes: claims.scopes ?? [],
|
|
@@ -3300,33 +3382,6 @@ function toLines(report, depth = 0) {
|
|
|
3300
3382
|
return lines;
|
|
3301
3383
|
}
|
|
3302
3384
|
//#endregion
|
|
3303
|
-
//#region src/report/classify.ts
|
|
3304
|
-
/**
|
|
3305
|
-
* The codes Node gives when the machine refuses something: permissions, a full or read-only
|
|
3306
|
-
* disk, a path that is missing or of the wrong kind, and a reader that closed the pipe.
|
|
3307
|
-
*/
|
|
3308
|
-
const ENVIRONMENT_CODES = /* @__PURE__ */ new Set([
|
|
3309
|
-
"EACCES",
|
|
3310
|
-
"EPERM",
|
|
3311
|
-
"ENOSPC",
|
|
3312
|
-
"EROFS",
|
|
3313
|
-
"ENOENT",
|
|
3314
|
-
"EISDIR",
|
|
3315
|
-
"ENOTDIR",
|
|
3316
|
-
"EEXIST",
|
|
3317
|
-
"EPIPE"
|
|
3318
|
-
]);
|
|
3319
|
-
/**
|
|
3320
|
-
* isReportable answers whether an error is a defect in the CLI. Everything the CLI anticipated
|
|
3321
|
-
* extends ExpectedFailure, so a failure nobody has classified yet is reported until someone
|
|
3322
|
-
* decides it is expected.
|
|
3323
|
-
*/
|
|
3324
|
-
function isReportable(error) {
|
|
3325
|
-
if (error instanceof ExpectedFailure) return false;
|
|
3326
|
-
const code = error instanceof Error ? error.code : void 0;
|
|
3327
|
-
return !(typeof code === "string" && ENVIRONMENT_CODES.has(code));
|
|
3328
|
-
}
|
|
3329
|
-
//#endregion
|
|
3330
3385
|
//#region src/report/report.ts
|
|
3331
3386
|
/** True once a report has been sent or tried, which the entry point reads before it exits. */
|
|
3332
3387
|
let isAttempted = false;
|
|
@@ -6634,5 +6689,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6634
6689
|
//#endregion
|
|
6635
6690
|
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6636
6691
|
|
|
6637
|
-
//# sourceMappingURL=program-
|
|
6638
|
-
//# debugId=
|
|
6692
|
+
//# sourceMappingURL=program-CqO1St9H.js.map
|
|
6693
|
+
//# debugId=0a793a7f-2311-5aaf-b27d-0877574e2d18
|
package/package.json
CHANGED
|
@@ -124,13 +124,17 @@ The exit code says what kind of failure it was.
|
|
|
124
124
|
|
|
125
125
|
The error is JSON on stderr. Branch on its `code`, never on the message.
|
|
126
126
|
|
|
127
|
-
| `code` | Do this
|
|
128
|
-
| --------------------------------------------------- |
|
|
129
|
-
| `not_signed_in`, `unauthorized` | Sign in again
|
|
130
|
-
| `invalid_flag`, `missing_flag`, `invalid_arguments` | Read `--help` for that command
|
|
131
|
-
| `not_found` | Check you passed a key, not an ID, where the command wants a key
|
|
132
|
-
| `bad_request`, `invalid_request`, `conflict` | Read `errors`, which names the field
|
|
133
|
-
| `rate_limited` | Wait a minute, then retry once
|
|
134
|
-
| `unreachable`, `server_error` | Tell the person, and include `requestId` if there is one
|
|
127
|
+
| `code` | Do this |
|
|
128
|
+
| --------------------------------------------------- | ------------------------------------------------------------------ |
|
|
129
|
+
| `not_signed_in`, `unauthorized` | Sign in again |
|
|
130
|
+
| `invalid_flag`, `missing_flag`, `invalid_arguments` | Read `--help` for that command |
|
|
131
|
+
| `not_found` | Check you passed a key, not an ID, where the command wants a key |
|
|
132
|
+
| `bad_request`, `invalid_request`, `conflict` | Read `errors`, which names the field |
|
|
133
|
+
| `rate_limited` | Wait a minute, then retry once |
|
|
134
|
+
| `unreachable`, `server_error` | Tell the person, and include `requestId` if there is one |
|
|
135
|
+
| `timed_out` | For a change, read the record first, because it may have been made |
|
|
136
|
+
| `unexpected_response` | Tell the person a proxy or network page answered, not Hardfin |
|
|
137
|
+
| `file_error` | Check the path you gave exists and can be read or written |
|
|
138
|
+
| `sign_in_failed` | Tell the person, and have them run `hardfin login` |
|
|
135
139
|
|
|
136
140
|
`hardfin agent-guide` lists every code.
|