@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.
Files changed (146) hide show
  1. package/AGENTS.md +13 -9
  2. package/README.md +14 -27
  3. package/api.json +8898 -9026
  4. package/api.md +370 -328
  5. package/dist/arguments.d.ts +54 -0
  6. package/dist/arguments.d.ts.map +1 -0
  7. package/dist/arguments.js +265 -0
  8. package/dist/cli-agent.d.ts +73 -9
  9. package/dist/cli-agent.d.ts.map +1 -1
  10. package/dist/cli-agent.js +331 -44
  11. package/dist/cli.js +802 -291
  12. package/dist/core/http.d.ts +162 -19
  13. package/dist/core/http.d.ts.map +1 -1
  14. package/dist/core/http.js +381 -48
  15. package/dist/core/pagination.d.ts +42 -6
  16. package/dist/core/pagination.d.ts.map +1 -1
  17. package/dist/core/pagination.js +111 -17
  18. package/dist/credential-storage.d.ts +10 -3
  19. package/dist/credential-storage.d.ts.map +1 -1
  20. package/dist/credential-storage.js +15 -6
  21. package/dist/dates.d.ts +1 -1
  22. package/dist/dates.js +1 -1
  23. package/dist/errors.d.ts +20 -84
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +20 -108
  26. package/dist/fields.d.ts +36 -0
  27. package/dist/fields.d.ts.map +1 -0
  28. package/dist/fields.js +187 -0
  29. package/dist/index.d.ts +28 -18
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +35 -25
  32. package/dist/named-credentials.d.ts +19 -0
  33. package/dist/named-credentials.d.ts.map +1 -1
  34. package/dist/named-credentials.js +81 -1
  35. package/dist/oauth-login.d.ts +8 -2
  36. package/dist/oauth-login.d.ts.map +1 -1
  37. package/dist/oauth-login.js +31 -19
  38. package/dist/oauth-request.d.ts +7 -1
  39. package/dist/oauth-request.d.ts.map +1 -1
  40. package/dist/oauth-request.js +26 -4
  41. package/dist/oauth-session.d.ts +13 -1
  42. package/dist/oauth-session.d.ts.map +1 -1
  43. package/dist/oauth-session.js +34 -18
  44. package/dist/ops.d.ts +58 -5
  45. package/dist/ops.d.ts.map +1 -1
  46. package/dist/ops.js +110 -41
  47. package/dist/polling-login.d.ts +8 -2
  48. package/dist/polling-login.d.ts.map +1 -1
  49. package/dist/polling-login.js +25 -11
  50. package/dist/resources/api-keys.d.ts +10 -7
  51. package/dist/resources/api-keys.d.ts.map +1 -1
  52. package/dist/resources/api-keys.js +10 -31
  53. package/dist/resources/deliveries.d.ts +89 -5
  54. package/dist/resources/deliveries.d.ts.map +1 -1
  55. package/dist/resources/deliveries.js +96 -19
  56. package/dist/resources/drafts.d.ts +16 -16
  57. package/dist/resources/drafts.d.ts.map +1 -1
  58. package/dist/resources/drafts.js +12 -65
  59. package/dist/resources/files.d.ts +4 -4
  60. package/dist/resources/files.d.ts.map +1 -1
  61. package/dist/resources/files.js +3 -12
  62. package/dist/resources/generations.d.ts +16 -16
  63. package/dist/resources/generations.d.ts.map +1 -1
  64. package/dist/resources/generations.js +23 -47
  65. package/dist/resources/organization.d.ts +4 -4
  66. package/dist/resources/organization.d.ts.map +1 -1
  67. package/dist/resources/organization.js +3 -10
  68. package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
  69. package/dist/resources/packages.d.ts.map +1 -0
  70. package/dist/resources/{generate.js → packages.js} +13 -29
  71. package/dist/resources/projects.d.ts +50 -50
  72. package/dist/resources/projects.d.ts.map +1 -1
  73. package/dist/resources/projects.js +60 -116
  74. package/dist/resources/releases.d.ts +22 -17
  75. package/dist/resources/releases.d.ts.map +1 -1
  76. package/dist/resources/releases.js +19 -40
  77. package/dist/resources/spec-revisions.d.ts +16 -7
  78. package/dist/resources/spec-revisions.d.ts.map +1 -1
  79. package/dist/resources/spec-revisions.js +7 -29
  80. package/dist/resources/specs.d.ts +7 -7
  81. package/dist/resources/specs.d.ts.map +1 -1
  82. package/dist/resources/specs.js +6 -34
  83. package/dist/resources/targets.d.ts +49 -49
  84. package/dist/resources/targets.d.ts.map +1 -1
  85. package/dist/resources/targets.js +59 -115
  86. package/dist/schemas.d.ts.map +1 -1
  87. package/dist/schemas.js +83 -81
  88. package/dist/search.d.ts +54 -0
  89. package/dist/search.d.ts.map +1 -0
  90. package/dist/search.js +421 -0
  91. package/dist/table.d.ts +28 -0
  92. package/dist/table.d.ts.map +1 -0
  93. package/dist/table.js +167 -0
  94. package/dist/type-docs.d.ts +61 -0
  95. package/dist/type-docs.d.ts.map +1 -0
  96. package/dist/type-docs.js +174 -0
  97. package/dist/types.d.ts +499 -339
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js +18 -18
  100. package/package.json +5 -2
  101. package/src/arguments.ts +254 -0
  102. package/src/cli-agent.ts +351 -46
  103. package/src/cli.ts +753 -266
  104. package/src/core/http.ts +457 -58
  105. package/src/core/pagination.ts +129 -18
  106. package/src/credential-storage.ts +16 -6
  107. package/src/dates.ts +1 -1
  108. package/src/errors.ts +46 -115
  109. package/src/fields.ts +167 -0
  110. package/src/index.ts +45 -28
  111. package/src/named-credentials.ts +66 -1
  112. package/src/oauth-login.ts +36 -21
  113. package/src/oauth-request.ts +32 -6
  114. package/src/oauth-session.ts +37 -19
  115. package/src/ops.ts +146 -45
  116. package/src/polling-login.ts +24 -11
  117. package/src/resources/api-keys.ts +34 -48
  118. package/src/resources/deliveries.ts +213 -32
  119. package/src/resources/drafts.ts +62 -109
  120. package/src/resources/files.ts +19 -20
  121. package/src/resources/generations.ts +61 -79
  122. package/src/resources/organization.ts +11 -16
  123. package/src/resources/{generate.ts → packages.ts} +43 -51
  124. package/src/resources/projects.ts +145 -200
  125. package/src/resources/releases.ts +50 -67
  126. package/src/resources/spec-revisions.ts +40 -49
  127. package/src/resources/specs.ts +39 -59
  128. package/src/resources/targets.ts +144 -194
  129. package/src/schemas.ts +83 -81
  130. package/src/search.ts +434 -0
  131. package/src/table.ts +167 -0
  132. package/src/type-docs.ts +205 -0
  133. package/src/types.ts +538 -357
  134. package/dist/console-login-check.d.ts +0 -21
  135. package/dist/console-login-check.d.ts.map +0 -1
  136. package/dist/console-login-check.js +0 -107
  137. package/dist/console-login-contract.d.ts +0 -45
  138. package/dist/console-login-contract.d.ts.map +0 -1
  139. package/dist/console-login-contract.js +0 -40
  140. package/dist/resources/generate.d.ts.map +0 -1
  141. package/dist/resources/publications.d.ts +0 -47
  142. package/dist/resources/publications.d.ts.map +0 -1
  143. package/dist/resources/publications.js +0 -70
  144. package/src/console-login-check.ts +0 -88
  145. package/src/console-login-contract.ts +0 -65
  146. 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
+ }
@@ -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: string;
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: string;
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;IACf,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,EAAE,MAAM,CAAC;IACf,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;AAgDD;gFACgF;AAChF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAgGhI"}
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"}
@@ -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 Error("OAuth endpoints require HTTPS without credentials or fragments; loopback HTTP is allowed for development.");
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 Error("The OAuth issuer must not contain a query.");
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 Error("OAuth discovery returned a different issuer. Check the issuer and discovery URL.");
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 Error("The provider does not advertise S256 PKCE support.");
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 Error("Browser login requires authorization and token endpoints.");
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 Error("Could not discover OAuth endpoints. Check the issuer or provide explicit authorization and token URLs.");
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 Error("Browser login requires a public client ID.");
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 Error("Organization selection requires a supported parameter and a nonempty organization ID without whitespace.");
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 Error("The OAuth redirect must be an HTTP loopback IP URL without credentials, query, or fragment.");
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
- response.writeHead(400).end("Login was not approved. Return to the terminal.", () => rejectCode(new Error("OAuth authorization was denied or cancelled. Run login again to retry.")));
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 Error("OAuth callback did not contain one authorization code.")));
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 Error("Browser login timed out or was cancelled. Run login again to retry."));
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 Error("Could not start the OAuth callback listener.");
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 Error("The authorization URL contains a conflicting organization parameter. Update the provider configuration.");
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 Error(`OAuth token exchange failed (HTTP ${response.status}). Check the public client registration and run login again.`);
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 Error("OAuth provider returned an invalid token lifetime.");
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
  };
@@ -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
- /** Error bodies and transport causes may contain credentials and are discarded. */
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;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;CAAE;AAE7G,mFAAmF;AACnF,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"}
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"}
@@ -13,7 +13,16 @@ export class OAuthResponseError extends Error {
13
13
  this.name = "OAuthResponseError";
14
14
  }
15
15
  }
16
- /** Error bodies and transport causes may contain credentials and are discarded. */
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 && !(mode === "device" && response.status === 400)) {
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
- return { status: response.status, ...(["authorization_pending", "slow_down", "access_denied", "expired_token"].includes(String(error)) && typeof error === "string" ? { error: error } : {}) };
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)
@@ -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>): Promise<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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"}
@@ -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 Error("Saved credentials have no API and profile binding. Log in again before using them.");
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 Error("The API destination changed. Log in again before using saved credentials.");
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 Error("The credential environment or profile changed. Log in again before using saved credentials.");
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 Error("The saved login has no matching API identity verification. Sign in again to verify the current account and organization.");
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 Error("Cannot read saved credentials. Check the credential store before logging in again.");
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 Error("Cannot lock the credential store.");
122
+ throw new CredentialStorageError("Cannot lock the credential store.");
117
123
  this.recoverDeadOwner(lock);
118
124
  if (Date.now() >= deadline)
119
- throw new Error("The credential store is busy. Wait for the other login or refresh to finish and retry. If its process stopped, inspect " + lock + ".");
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 Error("The saved OAuth session changed or was logged out. Run login again and retry the request.");
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 Error("The API destination changed. Log in again.");
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 Error("The configured issuer changed. Log in again.");
183
+ throw new OAuthSessionError("The configured issuer changed. Log in again.");
178
184
  if (session.configuredClientId !== (config.clientId ?? null))
179
- throw new Error("The OAuth client changed. Log in again.");
185
+ throw new OAuthSessionError("The OAuth client changed. Log in again.");
180
186
  if (!session.binding || session.binding !== sessionBinding(config))
181
- throw new Error("The API or OAuth configuration changed. Log in again before using saved credentials.");
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 Error("The previous OAuth refresh did not finish safely. Log in again; its refresh token will not be reused.");
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 Error("The saved OAuth session is invalid. Log in again.");
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
- const fresh = (session) => session.expiresAt === undefined || (Number.isFinite(session.expiresAt) && session.expiresAt > Date.now() + 60_000);
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 Error("The OAuth session expired and cannot be refreshed. Log in again.");
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 Error("The OAuth session could not be refreshed safely. Log in again.");
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
+ }