@typeship-ax/cli 0.22.0 → 0.24.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/AGENTS.md +13 -9
- package/README.md +14 -27
- package/api.json +8898 -9026
- package/api.md +370 -328
- package/dist/arguments.d.ts +54 -0
- package/dist/arguments.d.ts.map +1 -0
- package/dist/arguments.js +265 -0
- package/dist/cli-agent.d.ts +73 -9
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +331 -44
- package/dist/cli.js +802 -291
- package/dist/core/http.d.ts +162 -19
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +381 -48
- package/dist/core/pagination.d.ts +42 -6
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +111 -17
- package/dist/credential-storage.d.ts +10 -3
- package/dist/credential-storage.d.ts.map +1 -1
- package/dist/credential-storage.js +15 -6
- package/dist/dates.d.ts +1 -1
- package/dist/dates.js +1 -1
- package/dist/errors.d.ts +20 -84
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -108
- package/dist/fields.d.ts +36 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +187 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -25
- package/dist/named-credentials.d.ts +19 -0
- package/dist/named-credentials.d.ts.map +1 -1
- package/dist/named-credentials.js +81 -1
- package/dist/oauth-login.d.ts +8 -2
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +31 -19
- package/dist/oauth-request.d.ts +7 -1
- package/dist/oauth-request.d.ts.map +1 -1
- package/dist/oauth-request.js +26 -4
- package/dist/oauth-session.d.ts +13 -1
- package/dist/oauth-session.d.ts.map +1 -1
- package/dist/oauth-session.js +34 -18
- package/dist/ops.d.ts +58 -5
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +110 -41
- package/dist/polling-login.d.ts +8 -2
- package/dist/polling-login.d.ts.map +1 -1
- package/dist/polling-login.js +25 -11
- package/dist/resources/api-keys.d.ts +10 -7
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +10 -31
- package/dist/resources/deliveries.d.ts +89 -5
- package/dist/resources/deliveries.d.ts.map +1 -1
- package/dist/resources/deliveries.js +96 -19
- package/dist/resources/drafts.d.ts +16 -16
- package/dist/resources/drafts.d.ts.map +1 -1
- package/dist/resources/drafts.js +12 -65
- package/dist/resources/files.d.ts +4 -4
- package/dist/resources/files.d.ts.map +1 -1
- package/dist/resources/files.js +3 -12
- package/dist/resources/generations.d.ts +16 -16
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +23 -47
- package/dist/resources/organization.d.ts +4 -4
- package/dist/resources/organization.d.ts.map +1 -1
- package/dist/resources/organization.js +3 -10
- package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
- package/dist/resources/packages.d.ts.map +1 -0
- package/dist/resources/{generate.js → packages.js} +13 -29
- package/dist/resources/projects.d.ts +50 -50
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +60 -116
- package/dist/resources/releases.d.ts +22 -17
- package/dist/resources/releases.d.ts.map +1 -1
- package/dist/resources/releases.js +19 -40
- package/dist/resources/spec-revisions.d.ts +16 -7
- package/dist/resources/spec-revisions.d.ts.map +1 -1
- package/dist/resources/spec-revisions.js +7 -29
- package/dist/resources/specs.d.ts +7 -7
- package/dist/resources/specs.d.ts.map +1 -1
- package/dist/resources/specs.js +6 -34
- package/dist/resources/targets.d.ts +49 -49
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +59 -115
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +83 -81
- package/dist/search.d.ts +54 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +421 -0
- package/dist/table.d.ts +28 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +167 -0
- package/dist/type-docs.d.ts +61 -0
- package/dist/type-docs.d.ts.map +1 -0
- package/dist/type-docs.js +174 -0
- package/dist/types.d.ts +499 -339
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +18 -18
- package/package.json +5 -2
- package/src/arguments.ts +254 -0
- package/src/cli-agent.ts +351 -46
- package/src/cli.ts +753 -266
- package/src/core/http.ts +457 -58
- package/src/core/pagination.ts +129 -18
- package/src/credential-storage.ts +16 -6
- package/src/dates.ts +1 -1
- package/src/errors.ts +46 -115
- package/src/fields.ts +167 -0
- package/src/index.ts +45 -28
- package/src/named-credentials.ts +66 -1
- package/src/oauth-login.ts +36 -21
- package/src/oauth-request.ts +32 -6
- package/src/oauth-session.ts +37 -19
- package/src/ops.ts +146 -45
- package/src/polling-login.ts +24 -11
- package/src/resources/api-keys.ts +34 -48
- package/src/resources/deliveries.ts +213 -32
- package/src/resources/drafts.ts +62 -109
- package/src/resources/files.ts +19 -20
- package/src/resources/generations.ts +61 -79
- package/src/resources/organization.ts +11 -16
- package/src/resources/{generate.ts → packages.ts} +43 -51
- package/src/resources/projects.ts +145 -200
- package/src/resources/releases.ts +50 -67
- package/src/resources/spec-revisions.ts +40 -49
- package/src/resources/specs.ts +39 -59
- package/src/resources/targets.ts +144 -194
- package/src/schemas.ts +83 -81
- package/src/search.ts +434 -0
- package/src/table.ts +167 -0
- package/src/type-docs.ts +205 -0
- package/src/types.ts +538 -357
- package/dist/console-login-check.d.ts +0 -21
- package/dist/console-login-check.d.ts.map +0 -1
- package/dist/console-login-check.js +0 -107
- package/dist/console-login-contract.d.ts +0 -45
- package/dist/console-login-contract.d.ts.map +0 -1
- package/dist/console-login-contract.js +0 -40
- package/dist/resources/generate.d.ts.map +0 -1
- package/dist/resources/publications.d.ts +0 -47
- package/dist/resources/publications.d.ts.map +0 -1
- package/dist/resources/publications.js +0 -70
- package/src/console-login-check.ts +0 -88
- package/src/console-login-contract.ts +0 -65
- package/src/resources/publications.ts +0 -140
|
@@ -37,7 +37,7 @@ export function resolveNamedCredentials(schemes, layers) {
|
|
|
37
37
|
for (const layer of layers) {
|
|
38
38
|
for (const [name, scheme] of Object.entries(schemes)) {
|
|
39
39
|
for (const option of scheme.options) {
|
|
40
|
-
if (option === "clientCredentials")
|
|
40
|
+
if (option === "clientCredentials" || option === "refreshToken")
|
|
41
41
|
continue;
|
|
42
42
|
const value = layer.options?.[option];
|
|
43
43
|
if (value !== undefined)
|
|
@@ -57,6 +57,27 @@ export function namedCredentialAvailability(schemes, options, named) {
|
|
|
57
57
|
}
|
|
58
58
|
return options;
|
|
59
59
|
}
|
|
60
|
+
/** The schemes an interactive OAuth login session authenticates by name. When
|
|
61
|
+
* the API also declares a separate non-OAuth bearer scheme, the convenience
|
|
62
|
+
* bearer token belongs to that scheme, so the session must be passed to each
|
|
63
|
+
* OAuth scheme by name. Empty: the session is the convenience bearer token. */
|
|
64
|
+
export function oauthSessionSchemes(schemes) {
|
|
65
|
+
return Object.entries(schemes).filter(([, scheme]) => (scheme.options.includes("clientCredentials") || scheme.options.includes("refreshToken")) && !scheme.options.includes("bearerToken")).map(([name]) => name);
|
|
66
|
+
}
|
|
67
|
+
/** Whether resolved client options satisfy one complete credential alternative
|
|
68
|
+
* of an operation. Returns null when satisfied (or when no credential is
|
|
69
|
+
* required); otherwise the named alternatives and the schemes each lacks. */
|
|
70
|
+
export function missingCredentials(schemes, credentialOptions, options) {
|
|
71
|
+
const supplied = (key, value) => typeof value === "function" || (typeof value === "string" && value.length > 0)
|
|
72
|
+
|| (key === "clientCredentials" && value !== null && typeof value === "object")
|
|
73
|
+
|| (value !== null && typeof value === "object" && "username" in value && "password" in value && typeof value.username === "string" && value.username.length > 0 && typeof value.password === "string" && value.password.length > 0);
|
|
74
|
+
const named = Object.fromEntries(Object.entries((options.credentials ?? {})).filter(([name, value]) => supplied(name, value)));
|
|
75
|
+
const available = namedCredentialAvailability(schemes, new Set(Object.keys(options).filter((key) => key !== "credentials" && supplied(key, options[key]))), named);
|
|
76
|
+
if (credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => available.has(option))))
|
|
77
|
+
return null;
|
|
78
|
+
const alternatives = (credentialOptions ?? []).filter((alternative) => alternative.length > 0 && alternative.every((option) => option.startsWith("credentials."))).map((alternative) => alternative.map((option) => option.slice("credentials.".length)));
|
|
79
|
+
return { alternatives, missing: alternatives.map((alternative) => alternative.filter((name) => !available.has("credentials." + name))) };
|
|
80
|
+
}
|
|
60
81
|
/** Bound file/stdin reads before allocating a full credential document. */
|
|
61
82
|
export function readNamedCredentialsFile(input, schemes) {
|
|
62
83
|
let fd;
|
|
@@ -84,3 +105,62 @@ export function readNamedCredentialsFile(input, schemes) {
|
|
|
84
105
|
}
|
|
85
106
|
return parseNamedCredentials(Buffer.concat(chunks).toString("utf8"), schemes);
|
|
86
107
|
}
|
|
108
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
109
|
+
const TRANSPORT_HEADERS = new Set(["host", "content-length", "content-type", "transfer-encoding", "connection"]);
|
|
110
|
+
/** Extra request headers from repeated `--header "Name: value"` flags and a
|
|
111
|
+
* `<PREFIX>_HEADERS` variable (a JSON object, or one `Name: value` per line).
|
|
112
|
+
* The escape hatch for credentials a spec does not declare. Flags win over
|
|
113
|
+
* the environment; values never appear in errors. */
|
|
114
|
+
export function parseExtraHeaders(environment, flags, variable) {
|
|
115
|
+
const entries = [];
|
|
116
|
+
const line = (text, source) => {
|
|
117
|
+
const colon = text.indexOf(":");
|
|
118
|
+
if (colon <= 0)
|
|
119
|
+
throw new Error(source + " expects \"Name: value\".");
|
|
120
|
+
entries.push([text.slice(0, colon).trim(), text.slice(colon + 1).trim()]);
|
|
121
|
+
};
|
|
122
|
+
if (environment !== undefined && environment.trim()) {
|
|
123
|
+
const text = environment.trim();
|
|
124
|
+
if (text.startsWith("{")) {
|
|
125
|
+
let value;
|
|
126
|
+
try {
|
|
127
|
+
value = JSON.parse(text);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(variable + " must be a JSON object of header names to values, or one \"Name: value\" per line.");
|
|
131
|
+
}
|
|
132
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.values(value).some((entry) => typeof entry !== "string"))
|
|
133
|
+
throw new Error(variable + " must map header names to string values.");
|
|
134
|
+
entries.push(...Object.entries(value));
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
for (const part of text.split(/\r?\n/))
|
|
138
|
+
if (part.trim())
|
|
139
|
+
line(part, variable);
|
|
140
|
+
}
|
|
141
|
+
for (const flag of flags)
|
|
142
|
+
line(flag, "--header");
|
|
143
|
+
const headers = {};
|
|
144
|
+
for (const [name, value] of entries) {
|
|
145
|
+
if (!HEADER_NAME.test(name))
|
|
146
|
+
throw new Error("Header names may contain only token characters. Check --header and " + variable + ".");
|
|
147
|
+
if (TRANSPORT_HEADERS.has(name.toLowerCase()))
|
|
148
|
+
throw new Error("The " + name + " header is set by the client and cannot be overridden.");
|
|
149
|
+
if (/[\r\n\0]/.test(value))
|
|
150
|
+
throw new Error("Header values cannot contain line breaks. Check --header and " + variable + ".");
|
|
151
|
+
for (const existing of Object.keys(headers))
|
|
152
|
+
if (existing.toLowerCase() === name.toLowerCase())
|
|
153
|
+
delete headers[existing];
|
|
154
|
+
headers[name] = value;
|
|
155
|
+
}
|
|
156
|
+
return headers;
|
|
157
|
+
}
|
|
158
|
+
/** Apply extra headers last, replacing any header of the same name. */
|
|
159
|
+
export function applyExtraHeaders(target, extra) {
|
|
160
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
161
|
+
for (const existing of Object.keys(target))
|
|
162
|
+
if (existing.toLowerCase() === name.toLowerCase())
|
|
163
|
+
delete target[existing];
|
|
164
|
+
target[name] = value;
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/oauth-login.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
/** Browser OAuth login did not complete; the message names the cause. */
|
|
2
|
+
export declare class OAuthLoginError extends Error {
|
|
3
|
+
constructor(message?: string);
|
|
4
|
+
}
|
|
1
5
|
export interface OAuthLoginConfig {
|
|
2
|
-
issuer
|
|
6
|
+
/** Exact issuer, when the provider publishes metadata. Without one, the
|
|
7
|
+
* explicit authorization and token URLs from the API Spec are used. */
|
|
8
|
+
issuer?: string | null;
|
|
3
9
|
clientId: string;
|
|
4
10
|
discoveryUrl?: string;
|
|
5
11
|
authorizationUrl?: string;
|
|
@@ -19,7 +25,7 @@ export interface OAuthLoginSession {
|
|
|
19
25
|
accessToken: string;
|
|
20
26
|
refreshToken?: string;
|
|
21
27
|
expiresAt?: number;
|
|
22
|
-
issuer
|
|
28
|
+
issuer?: string;
|
|
23
29
|
clientId: string;
|
|
24
30
|
tokenUrl: string;
|
|
25
31
|
/** The actual native callback used for this exchange, including its port. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-login.d.ts","sourceRoot":"","sources":["../src/oauth-login.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"oauth-login.d.ts","sourceRoot":"","sources":["../src/oauth-login.ts"],"names":[],"mappings":"AAIA,yEAAyE;AACzE,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,CAAC,EAAE,MAAM;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B;2EACuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,YAAY,CAAC,EAAE;QAAE,SAAS,EAAE,cAAc,GAAG,iBAAiB,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqDD;gFACgF;AAChF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAmGhI"}
|
package/dist/oauth-login.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { oauthJsonRequest, OAuthResponseError } from "./oauth-request.js";
|
|
3
|
+
import { oauthJsonRequest, OAuthResponseError, providerErrorOf } from "./oauth-request.js";
|
|
4
|
+
/** Browser OAuth login did not complete; the message names the cause. */
|
|
5
|
+
export class OAuthLoginError extends Error {
|
|
6
|
+
constructor(message) { super(message); this.name = "OAuthLoginError"; }
|
|
7
|
+
}
|
|
4
8
|
function endpoint(value) {
|
|
5
9
|
const url = new URL(value);
|
|
6
10
|
const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname);
|
|
7
11
|
if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) || url.username || url.password || url.hash) {
|
|
8
|
-
throw new
|
|
12
|
+
throw new OAuthLoginError("OAuth endpoints require HTTPS without credentials or fragments; loopback HTTP is allowed for development.");
|
|
9
13
|
}
|
|
10
14
|
return url;
|
|
11
15
|
}
|
|
12
16
|
async function metadata(config, signal) {
|
|
17
|
+
// The Spec's authorizationCode flow names both endpoints; no discovery.
|
|
18
|
+
if (!config.issuer) {
|
|
19
|
+
if (config.authorizationUrl && config.tokenUrl)
|
|
20
|
+
return { authorizationUrl: endpoint(config.authorizationUrl), tokenUrl: endpoint(config.tokenUrl).href };
|
|
21
|
+
throw new OAuthLoginError("Browser login needs the provider's authorization and token URLs, or an issuer that publishes them.");
|
|
22
|
+
}
|
|
13
23
|
const issuer = endpoint(config.issuer);
|
|
14
24
|
if (issuer.search)
|
|
15
|
-
throw new
|
|
25
|
+
throw new OAuthLoginError("The OAuth issuer must not contain a query.");
|
|
16
26
|
const path = issuer.pathname.replace(/\/$/, "");
|
|
17
27
|
const urls = config.discoveryUrl ? [config.discoveryUrl] : [
|
|
18
28
|
issuer.origin + "/.well-known/oauth-authorization-server" + path,
|
|
@@ -32,14 +42,14 @@ async function metadata(config, signal) {
|
|
|
32
42
|
continue;
|
|
33
43
|
const data = response.data;
|
|
34
44
|
if (data.issuer !== config.issuer)
|
|
35
|
-
throw new
|
|
45
|
+
throw new OAuthLoginError("OAuth discovery returned a different issuer. Check the issuer and discovery URL.");
|
|
36
46
|
if (Array.isArray(data.code_challenge_methods_supported) && !data.code_challenge_methods_supported.includes("S256")) {
|
|
37
|
-
throw new
|
|
47
|
+
throw new OAuthLoginError("The provider does not advertise S256 PKCE support.");
|
|
38
48
|
}
|
|
39
49
|
const authorizationUrl = config.authorizationUrl ?? data.authorization_endpoint;
|
|
40
50
|
const tokenUrl = config.tokenUrl ?? data.token_endpoint;
|
|
41
51
|
if (typeof authorizationUrl !== "string" || typeof tokenUrl !== "string")
|
|
42
|
-
throw new
|
|
52
|
+
throw new OAuthLoginError("Browser login requires authorization and token endpoints.");
|
|
43
53
|
return {
|
|
44
54
|
authorizationUrl: endpoint(authorizationUrl), tokenUrl: endpoint(tokenUrl).href,
|
|
45
55
|
...(typeof data.revocation_endpoint === "string" ? { revocationUrl: endpoint(data.revocation_endpoint).href } : {}),
|
|
@@ -51,21 +61,21 @@ async function metadata(config, signal) {
|
|
|
51
61
|
return {
|
|
52
62
|
authorizationUrl: endpoint(config.authorizationUrl), tokenUrl: endpoint(config.tokenUrl).href,
|
|
53
63
|
};
|
|
54
|
-
throw new
|
|
64
|
+
throw new OAuthLoginError("Could not discover OAuth endpoints. Check the issuer or provide explicit authorization and token URLs.");
|
|
55
65
|
}
|
|
56
66
|
/** Authorization Code + S256 PKCE for a public native client. The listener
|
|
57
67
|
* binds only a loopback IP, verifies state/issuer, and closes on every path. */
|
|
58
68
|
export async function oauthBrowserLogin(config, interaction) {
|
|
59
69
|
if (!config.clientId.trim())
|
|
60
|
-
throw new
|
|
70
|
+
throw new OAuthLoginError("Browser login requires a public client ID.");
|
|
61
71
|
if (config.organization && (!["organization", "organization_id"].includes(config.organization.parameter) || typeof config.organization.id !== "string" || !config.organization.id || config.organization.id.length > 512 || /\s|[\u0000-\u001F\u007F]/.test(config.organization.id)))
|
|
62
|
-
throw new
|
|
72
|
+
throw new OAuthLoginError("Organization selection requires a supported parameter and a nonempty organization ID without whitespace.");
|
|
63
73
|
const signal = AbortSignal.any([interaction.signal ?? new AbortController().signal, AbortSignal.timeout(interaction.timeoutMs ?? 600_000)]);
|
|
64
74
|
signal.throwIfAborted();
|
|
65
75
|
const endpoints = await metadata(config, signal);
|
|
66
76
|
const redirect = new URL(config.redirectUri ?? "http://127.0.0.1/callback");
|
|
67
77
|
if (redirect.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(redirect.hostname) || redirect.username || redirect.password || redirect.search || redirect.hash) {
|
|
68
|
-
throw new
|
|
78
|
+
throw new OAuthLoginError("The OAuth redirect must be an HTTP loopback IP URL without credentials, query, or fragment.");
|
|
69
79
|
}
|
|
70
80
|
const verifier = randomBytes(32).toString("base64url");
|
|
71
81
|
const state = randomBytes(32).toString("base64url");
|
|
@@ -100,25 +110,27 @@ export async function oauthBrowserLogin(config, interaction) {
|
|
|
100
110
|
return;
|
|
101
111
|
}
|
|
102
112
|
const returnedIssuer = incoming.searchParams.get("iss");
|
|
103
|
-
if (returnedIssuer !== null && (incoming.searchParams.getAll("iss").length !== 1 || returnedIssuer !== config.issuer)) {
|
|
113
|
+
if (returnedIssuer !== null && config.issuer && (incoming.searchParams.getAll("iss").length !== 1 || returnedIssuer !== config.issuer)) {
|
|
104
114
|
response.writeHead(400).end("The login issuer did not match.");
|
|
105
115
|
return;
|
|
106
116
|
}
|
|
107
117
|
accepted = true;
|
|
108
118
|
if (incoming.searchParams.has("error")) {
|
|
109
|
-
|
|
119
|
+
const provider = providerErrorOf({ error: incoming.searchParams.get("error"), error_description: incoming.searchParams.get("error_description") ?? undefined });
|
|
120
|
+
const denied = incoming.searchParams.get("error") === "access_denied";
|
|
121
|
+
response.writeHead(400).end("Login was not approved. Return to the terminal.", () => rejectCode(new OAuthLoginError((provider ? "The provider returned " + provider + ". " : "") + (denied ? "OAuth authorization was denied or cancelled. Run login again to retry." : "Check the client ID, the registered redirect URI and the requested scopes, then run login again."))));
|
|
110
122
|
return;
|
|
111
123
|
}
|
|
112
124
|
const code = incoming.searchParams.get("code");
|
|
113
125
|
if (!code || incoming.searchParams.getAll("code").length !== 1) {
|
|
114
|
-
response.writeHead(400).end("The provider returned no authorization code.", () => rejectCode(new
|
|
126
|
+
response.writeHead(400).end("The provider returned no authorization code.", () => rejectCode(new OAuthLoginError("OAuth callback did not contain one authorization code.")));
|
|
115
127
|
return;
|
|
116
128
|
}
|
|
117
129
|
response.end("Authorization received. Return to the terminal to finish signing in.", () => resolveCode(code));
|
|
118
130
|
});
|
|
119
131
|
server.headersTimeout = 10_000;
|
|
120
132
|
server.requestTimeout = 10_000;
|
|
121
|
-
const aborted = () => rejectCode(new
|
|
133
|
+
const aborted = () => rejectCode(new OAuthLoginError("Browser login timed out or was cancelled. Run login again to retry."));
|
|
122
134
|
signal.addEventListener("abort", aborted, { once: true });
|
|
123
135
|
try {
|
|
124
136
|
await new Promise((resolve, reject) => {
|
|
@@ -127,14 +139,14 @@ export async function oauthBrowserLogin(config, interaction) {
|
|
|
127
139
|
});
|
|
128
140
|
const address = server.address();
|
|
129
141
|
if (!address || typeof address === "string")
|
|
130
|
-
throw new
|
|
142
|
+
throw new OAuthLoginError("Could not start the OAuth callback listener.");
|
|
131
143
|
redirect.port = String(address.port);
|
|
132
144
|
signal.throwIfAborted();
|
|
133
145
|
const authorization = new URL(endpoints.authorizationUrl);
|
|
134
146
|
if (config.organization) {
|
|
135
147
|
const other = config.organization.parameter === "organization" ? "organization_id" : "organization";
|
|
136
148
|
if (authorization.searchParams.has(other))
|
|
137
|
-
throw new
|
|
149
|
+
throw new OAuthLoginError("The authorization URL contains a conflicting organization parameter. Update the provider configuration.");
|
|
138
150
|
authorization.searchParams.set(config.organization.parameter, config.organization.id);
|
|
139
151
|
}
|
|
140
152
|
for (const [key, value] of Object.entries({ response_type: "code", client_id: config.clientId, redirect_uri: redirect.href, state, code_challenge: challenge, code_challenge_method: "S256", ...(config.scopes?.length ? { scope: config.scopes.join(" ") } : {}), ...(config.audience ? { audience: config.audience } : {}), ...(config.resource ? { resource: config.resource } : {}) }))
|
|
@@ -149,15 +161,15 @@ export async function oauthBrowserLogin(config, interaction) {
|
|
|
149
161
|
});
|
|
150
162
|
const data = response.data;
|
|
151
163
|
if (response.status !== 200 || !data || typeof data.access_token !== "string" || !data.access_token || typeof data.token_type !== "string" || data.token_type.toLowerCase() !== "bearer") {
|
|
152
|
-
throw new
|
|
164
|
+
throw new OAuthLoginError(`OAuth token exchange failed (HTTP ${response.status}${response.providerError ? ": " + response.providerError : ""}). Check the public client registration and run login again.`);
|
|
153
165
|
}
|
|
154
166
|
if (data.expires_in !== undefined && (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0))
|
|
155
|
-
throw new
|
|
167
|
+
throw new OAuthLoginError("OAuth provider returned an invalid token lifetime.");
|
|
156
168
|
return {
|
|
157
169
|
accessToken: data.access_token,
|
|
158
170
|
...(typeof data.refresh_token === "string" && data.refresh_token ? { refreshToken: data.refresh_token } : {}),
|
|
159
171
|
...(typeof data.expires_in === "number" ? { expiresAt: Date.now() + data.expires_in * 1000 } : {}),
|
|
160
|
-
issuer: config.issuer, clientId: config.clientId, tokenUrl: endpoints.tokenUrl, redirectUri: redirect.href,
|
|
172
|
+
...(config.issuer ? { issuer: config.issuer } : {}), clientId: config.clientId, tokenUrl: endpoints.tokenUrl, redirectUri: redirect.href,
|
|
161
173
|
...(endpoints.revocationUrl ? { revocationUrl: endpoints.revocationUrl } : {}),
|
|
162
174
|
...(typeof data.scope === "string" ? { scopes: data.scope.split(/\s+/).filter(Boolean) } : {}),
|
|
163
175
|
};
|
package/dist/oauth-request.d.ts
CHANGED
|
@@ -7,8 +7,14 @@ interface AuthenticationResponse {
|
|
|
7
7
|
status: number;
|
|
8
8
|
data?: Record<string, unknown>;
|
|
9
9
|
error?: DeviceOAuthError;
|
|
10
|
+
/** RFC 6749 `error` and `error_description` from a 400 or 401 body, as
|
|
11
|
+
* "code: description". Only those two fields, printable ASCII, bounded. */
|
|
12
|
+
providerError?: string;
|
|
10
13
|
}
|
|
11
|
-
/**
|
|
14
|
+
/** The provider's standard error fields, never any other body content. */
|
|
15
|
+
export declare function providerErrorOf(data: Record<string, unknown>): string | undefined;
|
|
16
|
+
/** Transport causes and error bodies beyond the standard OAuth `error` and
|
|
17
|
+
* `error_description` fields may contain credentials and are discarded. */
|
|
12
18
|
export declare function oauthJsonRequest(url: string | URL, init?: RequestInit, timeoutMs?: number): Promise<AuthenticationResponse>;
|
|
13
19
|
/** Device polling needs four protocol errors. Return only their exact names,
|
|
14
20
|
* never descriptions, error URIs, arbitrary error codes or other body fields. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-request.d.ts","sourceRoot":"","sources":["../src/oauth-request.ts"],"names":[],"mappings":"AACA,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,WAAW,GAAG,WAAW,GAAG,oBAAoB,GAAG,kBAAkB,CAAC;gBAC5F,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC;CAY7C;AAED,MAAM,MAAM,gBAAgB,GAAG,uBAAuB,GAAG,WAAW,GAAG,eAAe,GAAG,eAAe,CAAC;AACzG,UAAU,sBAAsB;
|
|
1
|
+
{"version":3,"file":"oauth-request.d.ts","sourceRoot":"","sources":["../src/oauth-request.ts"],"names":[],"mappings":"AACA,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,WAAW,GAAG,WAAW,GAAG,oBAAoB,GAAG,kBAAkB,CAAC;gBAC5F,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC;CAY7C;AAED,MAAM,MAAM,gBAAgB,GAAG,uBAAuB,GAAG,WAAW,GAAG,eAAe,GAAG,eAAe,CAAC;AACzG,UAAU,sBAAsB;IAC9B,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzE;+EAC2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAKjF;AAED;2EAC2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,GAAE,WAAgB,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAE/H;AACD;iFACiF;AACjF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,GAAE,WAAgB,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAEjI;AACD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,GAAE,WAAgB,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAE7H"}
|
package/dist/oauth-request.js
CHANGED
|
@@ -13,7 +13,16 @@ export class OAuthResponseError extends Error {
|
|
|
13
13
|
this.name = "OAuthResponseError";
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/** The provider's standard error fields, never any other body content. */
|
|
17
|
+
export function providerErrorOf(data) {
|
|
18
|
+
const code = typeof data.error === "string" && /^[\x20-\x21\x23-\x5b\x5d-\x7e]{1,64}$/.test(data.error) ? data.error : undefined;
|
|
19
|
+
if (!code)
|
|
20
|
+
return undefined;
|
|
21
|
+
const description = typeof data.error_description === "string" ? data.error_description.replace(/[^\x20-\x7e]/g, " ").trim().slice(0, 300) : "";
|
|
22
|
+
return description ? code + ": " + description : code;
|
|
23
|
+
}
|
|
24
|
+
/** Transport causes and error bodies beyond the standard OAuth `error` and
|
|
25
|
+
* `error_description` fields may contain credentials and are discarded. */
|
|
17
26
|
export function oauthJsonRequest(url, init = {}, timeoutMs = 30_000) {
|
|
18
27
|
return boundedRequest(url, init, timeoutMs, "json");
|
|
19
28
|
}
|
|
@@ -34,6 +43,9 @@ async function boundedRequest(url, init, timeoutMs, mode) {
|
|
|
34
43
|
let reader;
|
|
35
44
|
let timer;
|
|
36
45
|
let interrupted;
|
|
46
|
+
// An error status is already an answer: its body only adds the provider's
|
|
47
|
+
// explanation, so a stalled or malformed error body never becomes a failure.
|
|
48
|
+
let errorStatus;
|
|
37
49
|
let rejectDeadline;
|
|
38
50
|
const deadline = new Promise((_, reject) => { rejectDeadline = reject; });
|
|
39
51
|
const stop = (code) => { interrupted ??= code; rejectDeadline(new OAuthResponseError(interrupted)); controller.abort(); };
|
|
@@ -56,10 +68,12 @@ async function boundedRequest(url, init, timeoutMs, mode) {
|
|
|
56
68
|
void response.body?.cancel().catch(() => { });
|
|
57
69
|
throw new OAuthResponseError("request_failed");
|
|
58
70
|
}
|
|
59
|
-
if (mode === "status" || response.status !== 200 &&
|
|
71
|
+
if (mode === "status" || response.status !== 200 && response.status !== 400 && response.status !== 401) {
|
|
60
72
|
void response.body?.cancel().catch(() => { });
|
|
61
73
|
return { status: response.status };
|
|
62
74
|
}
|
|
75
|
+
if (response.status !== 200)
|
|
76
|
+
errorStatus = response.status;
|
|
63
77
|
if (!response.body)
|
|
64
78
|
throw new OAuthResponseError("invalid_response");
|
|
65
79
|
reader = response.body.getReader();
|
|
@@ -90,17 +104,25 @@ async function boundedRequest(url, init, timeoutMs, mode) {
|
|
|
90
104
|
data = JSON.parse(text);
|
|
91
105
|
}
|
|
92
106
|
catch {
|
|
107
|
+
if (response.status !== 200)
|
|
108
|
+
return { status: response.status };
|
|
93
109
|
throw new OAuthResponseError("invalid_response");
|
|
94
110
|
}
|
|
95
|
-
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
111
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
112
|
+
if (response.status !== 200)
|
|
113
|
+
return { status: response.status };
|
|
96
114
|
throw new OAuthResponseError("invalid_response");
|
|
115
|
+
}
|
|
97
116
|
if (response.status !== 200) {
|
|
98
117
|
const error = data.error;
|
|
99
|
-
|
|
118
|
+
const providerError = providerErrorOf(data);
|
|
119
|
+
return { status: response.status, ...(providerError ? { providerError } : {}), ...(mode === "device" && ["authorization_pending", "slow_down", "access_denied", "expired_token"].includes(String(error)) && typeof error === "string" ? { error: error } : {}) };
|
|
100
120
|
}
|
|
101
121
|
return { status: response.status, data: data };
|
|
102
122
|
}
|
|
103
123
|
catch (error) {
|
|
124
|
+
if (errorStatus !== undefined && interrupted !== "cancelled")
|
|
125
|
+
return { status: errorStatus };
|
|
104
126
|
if (interrupted)
|
|
105
127
|
throw new OAuthResponseError(interrupted);
|
|
106
128
|
if (error instanceof OAuthResponseError)
|
package/dist/oauth-session.d.ts
CHANGED
|
@@ -70,7 +70,13 @@ export interface CredentialStore {
|
|
|
70
70
|
read(): StoredCredentials | null;
|
|
71
71
|
withLock<T>(work: (store: LockedCredentials) => Promise<T>): Promise<T>;
|
|
72
72
|
}
|
|
73
|
+
/** The OS credential store or the saved credential file cannot be used. */
|
|
73
74
|
export declare class CredentialStorageError extends Error {
|
|
75
|
+
constructor(message?: string);
|
|
76
|
+
}
|
|
77
|
+
/** A saved login cannot be used as is; the remedy is to sign in again. */
|
|
78
|
+
export declare class OAuthSessionError extends Error {
|
|
79
|
+
constructor(message?: string);
|
|
74
80
|
}
|
|
75
81
|
export interface CredentialCodec {
|
|
76
82
|
readonly name: string;
|
|
@@ -101,6 +107,12 @@ export declare class FileCredentialStore implements CredentialStore {
|
|
|
101
107
|
/** Re-read on every request attempt; only one process may rotate the token.
|
|
102
108
|
* A failed or interrupted exchange requires login, since the server may have
|
|
103
109
|
* consumed its refresh token even when no response reached this process. */
|
|
104
|
-
export declare function oauthSessionToken(store: CredentialStore, config: SessionConfiguration, sessionId: string, tokenParams?: Record<string, string
|
|
110
|
+
export declare function oauthSessionToken(store: CredentialStore, config: SessionConfiguration, sessionId: string, tokenParams?: Record<string, string>, rejected?: string): Promise<string>;
|
|
111
|
+
/** A saved-session token for the request runtime: resolved before every
|
|
112
|
+
* attempt, and after a 401 (invalidate) the next resolve forces one refresh
|
|
113
|
+
* of the token the API rejected. */
|
|
114
|
+
export declare function sessionCredential(resolve: (rejected?: string) => Promise<string>): (() => Promise<string>) & {
|
|
115
|
+
invalidate(): void;
|
|
116
|
+
};
|
|
105
117
|
export {};
|
|
106
118
|
//# sourceMappingURL=oauth-session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-session.d.ts","sourceRoot":"","sources":["../src/oauth-session.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"oauth-session.d.ts","sourceRoot":"","sources":["../src/oauth-session.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth-session.js
CHANGED
|
@@ -6,11 +6,11 @@ import { oauthJsonRequest } from "./oauth-request.js";
|
|
|
6
6
|
export function assertCredentialDestination(credentials, destination) {
|
|
7
7
|
const saved = credentials.destination;
|
|
8
8
|
if (!saved)
|
|
9
|
-
throw new
|
|
9
|
+
throw new OAuthSessionError("Saved credentials have no API and profile binding. Log in again before using them.");
|
|
10
10
|
if (new URL(saved.apiBaseUrl).href !== new URL(destination.apiBaseUrl).href)
|
|
11
|
-
throw new
|
|
11
|
+
throw new OAuthSessionError("The API destination changed. Log in again before using saved credentials.");
|
|
12
12
|
if ((saved.environment ?? null) !== (destination.environment ?? null) || (saved.profile ?? null) !== (destination.profile ?? null))
|
|
13
|
-
throw new
|
|
13
|
+
throw new OAuthSessionError("The credential environment or profile changed. Log in again before using saved credentials.");
|
|
14
14
|
}
|
|
15
15
|
/** Bind minimal identity evidence to this credential set and its configured read. */
|
|
16
16
|
export function credentialIdentityBinding(credentials, identity) {
|
|
@@ -24,7 +24,7 @@ export function assertStoredIdentity(credentials, identity) {
|
|
|
24
24
|
if (!identity)
|
|
25
25
|
return;
|
|
26
26
|
if (!credentials.identity?.values || credentials.identity.binding !== credentialIdentityBinding(credentials, identity) || Object.keys(identity.fields).some((kind) => !Object.hasOwn(credentials.identity.values, kind))) {
|
|
27
|
-
throw new
|
|
27
|
+
throw new OAuthSessionError("The saved login has no matching API identity verification. Sign in again to verify the current account and organization.");
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/** Bind the login to effective nonsecret settings. Scope ordering is irrelevant. */
|
|
@@ -49,7 +49,13 @@ function alive(pid) {
|
|
|
49
49
|
return code(error) !== "ESRCH";
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/** The OS credential store or the saved credential file cannot be used. */
|
|
52
53
|
export class CredentialStorageError extends Error {
|
|
54
|
+
constructor(message) { super(message); this.name = "CredentialStorageError"; }
|
|
55
|
+
}
|
|
56
|
+
/** A saved login cannot be used as is; the remedy is to sign in again. */
|
|
57
|
+
export class OAuthSessionError extends Error {
|
|
58
|
+
constructor(message) { super(message); this.name = "OAuthSessionError"; }
|
|
53
59
|
}
|
|
54
60
|
/** Private file adapter. All writers (login, refresh and logout) use the same
|
|
55
61
|
* cross-process lock. Credential storage can be replaced independently of OAuth. */
|
|
@@ -78,7 +84,7 @@ export class FileCredentialStore {
|
|
|
78
84
|
return null;
|
|
79
85
|
if (error instanceof CredentialStorageError)
|
|
80
86
|
throw error;
|
|
81
|
-
throw new
|
|
87
|
+
throw new CredentialStorageError("Cannot read saved credentials. Check the credential store before logging in again.");
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
write(credentials) {
|
|
@@ -113,10 +119,10 @@ export class FileCredentialStore {
|
|
|
113
119
|
}
|
|
114
120
|
catch (error) {
|
|
115
121
|
if (code(error) !== "EEXIST")
|
|
116
|
-
throw new
|
|
122
|
+
throw new CredentialStorageError("Cannot lock the credential store.");
|
|
117
123
|
this.recoverDeadOwner(lock);
|
|
118
124
|
if (Date.now() >= deadline)
|
|
119
|
-
throw new
|
|
125
|
+
throw new CredentialStorageError("The credential store is busy. Wait for the other login or refresh to finish and retry. If its process stopped, inspect " + lock + ".");
|
|
120
126
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
121
127
|
}
|
|
122
128
|
}
|
|
@@ -170,27 +176,29 @@ export class FileCredentialStore {
|
|
|
170
176
|
function validSession(credentials, config, sessionId, allowPending = false) {
|
|
171
177
|
const session = credentials?.oauth;
|
|
172
178
|
if (!session || !session.sessionId || session.sessionId !== sessionId)
|
|
173
|
-
throw new
|
|
179
|
+
throw new OAuthSessionError("The saved OAuth session changed or was logged out. Run login again and retry the request.");
|
|
174
180
|
if (session.apiBaseUrl && new URL(session.apiBaseUrl).href !== new URL(config.apiBaseUrl).href)
|
|
175
|
-
throw new
|
|
181
|
+
throw new OAuthSessionError("The API destination changed. Log in again.");
|
|
176
182
|
if (session.issuer && config.issuer && session.issuer !== config.issuer)
|
|
177
|
-
throw new
|
|
183
|
+
throw new OAuthSessionError("The configured issuer changed. Log in again.");
|
|
178
184
|
if (session.configuredClientId !== (config.clientId ?? null))
|
|
179
|
-
throw new
|
|
185
|
+
throw new OAuthSessionError("The OAuth client changed. Log in again.");
|
|
180
186
|
if (!session.binding || session.binding !== sessionBinding(config))
|
|
181
|
-
throw new
|
|
187
|
+
throw new OAuthSessionError("The API or OAuth configuration changed. Log in again before using saved credentials.");
|
|
182
188
|
if (session.refreshPending && !allowPending)
|
|
183
|
-
throw new
|
|
189
|
+
throw new OAuthSessionError("The previous OAuth refresh did not finish safely. Log in again; its refresh token will not be reused.");
|
|
184
190
|
if (typeof session.accessToken !== "string" || !session.accessToken)
|
|
185
|
-
throw new
|
|
191
|
+
throw new OAuthSessionError("The saved OAuth session is invalid. Log in again.");
|
|
186
192
|
assertStoredIdentity(credentials, config.identity);
|
|
187
193
|
return session;
|
|
188
194
|
}
|
|
189
195
|
/** Re-read on every request attempt; only one process may rotate the token.
|
|
190
196
|
* A failed or interrupted exchange requires login, since the server may have
|
|
191
197
|
* consumed its refresh token even when no response reached this process. */
|
|
192
|
-
export async function oauthSessionToken(store, config, sessionId, tokenParams = {}) {
|
|
193
|
-
|
|
198
|
+
export async function oauthSessionToken(store, config, sessionId, tokenParams = {}, rejected) {
|
|
199
|
+
// A token the API just rejected (401) is refreshed once even if unexpired;
|
|
200
|
+
// another process may already have replaced it, which is fine.
|
|
201
|
+
const fresh = (session) => session.accessToken !== rejected && (session.expiresAt === undefined || (Number.isFinite(session.expiresAt) && session.expiresAt > Date.now() + 60_000));
|
|
194
202
|
const first = validSession(store.read(), config, sessionId, true);
|
|
195
203
|
if (!first.refreshPending && fresh(first))
|
|
196
204
|
return first.accessToken;
|
|
@@ -200,7 +208,7 @@ export async function oauthSessionToken(store, config, sessionId, tokenParams =
|
|
|
200
208
|
if (fresh(session))
|
|
201
209
|
return session.accessToken;
|
|
202
210
|
if (!session.refreshToken || !session.tokenUrl || !session.clientId)
|
|
203
|
-
throw new
|
|
211
|
+
throw new OAuthSessionError(session.accessToken === rejected ? "The API rejected the saved OAuth session and it has no refresh token. Log in again." : "The OAuth session expired and cannot be refreshed. Log in again.");
|
|
204
212
|
const endpoint = new URL(session.tokenUrl);
|
|
205
213
|
if ((endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname))) || endpoint.username || endpoint.password || endpoint.hash)
|
|
206
214
|
throw new Error("The OAuth token endpoint requires HTTPS; loopback HTTP is allowed for development.");
|
|
@@ -238,7 +246,15 @@ export async function oauthSessionToken(store, config, sessionId, tokenParams =
|
|
|
238
246
|
}
|
|
239
247
|
catch {
|
|
240
248
|
// Never expose an error body, a refresh token, or fetch's nested cause.
|
|
241
|
-
throw new
|
|
249
|
+
throw new OAuthSessionError("The OAuth session could not be refreshed safely. Log in again.");
|
|
242
250
|
}
|
|
243
251
|
});
|
|
244
252
|
}
|
|
253
|
+
/** A saved-session token for the request runtime: resolved before every
|
|
254
|
+
* attempt, and after a 401 (invalidate) the next resolve forces one refresh
|
|
255
|
+
* of the token the API rejected. */
|
|
256
|
+
export function sessionCredential(resolve) {
|
|
257
|
+
let last, rejected;
|
|
258
|
+
const token = async () => { const previous = rejected; rejected = undefined; last = await resolve(previous); return last; };
|
|
259
|
+
return Object.assign(token, { invalidate() { rejected = last; } });
|
|
260
|
+
}
|