@youkno/edge-cli 1.21.331 → 1.21.332

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/dist/lib/auth.js CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ShellAuthMethodError = void 0;
6
7
  exports.listLogins = listLogins;
7
8
  exports.setDefaultLogin = setDefaultLogin;
8
9
  exports.logout = logout;
@@ -161,6 +162,65 @@ function extractEmailFromToken(accessToken) {
161
162
  }
162
163
  return "unknown@local";
163
164
  }
165
+ /**
166
+ * The server's `errorCode` for "you signed in a way the shell does not accept". Mirrors
167
+ * `ApiErrorCode.SHELL_AUTH_METHOD_NOT_ALLOWED`. Matched on the number rather than the message so a
168
+ * reworded refusal does not silently stop being recognised here.
169
+ */
170
+ const ERROR_CODE_SHELL_AUTH_METHOD = 403006;
171
+ /**
172
+ * The RFC 7807 body an Edge API error carries, or undefined if this one did not send one (a proxy
173
+ * error page, an HTML 502). Never throws -- a failure to parse the explanation of a failure must not
174
+ * replace it with a parse error.
175
+ */
176
+ function parseProblemDetail(body) {
177
+ try {
178
+ const parsed = JSON.parse(body);
179
+ return parsed && typeof parsed === "object" ? parsed : undefined;
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ }
185
+ class AuthEndpointError extends Error {
186
+ status;
187
+ problem;
188
+ constructor(message, status, problem) {
189
+ super(message);
190
+ this.status = status;
191
+ this.problem = problem;
192
+ this.name = "AuthEndpointError";
193
+ }
194
+ }
195
+ /**
196
+ * The server refused the sign-in METHOD rather than the identity: this session was created with, or
197
+ * is being used from, a login the shell does not accept.
198
+ *
199
+ * Its own type because it is the one auth failure the operator can act on immediately, and because
200
+ * it must survive `resolveAccessToken`'s "a failed refresh is not fatal" handling, which otherwise
201
+ * swallows it and falls through to an access token that is already dead.
202
+ */
203
+ class ShellAuthMethodError extends Error {
204
+ constructor(message) {
205
+ super(message);
206
+ this.name = "ShellAuthMethodError";
207
+ }
208
+ }
209
+ exports.ShellAuthMethodError = ShellAuthMethodError;
210
+ /**
211
+ * The refusal as a clean error, or undefined when this failure is something else.
212
+ *
213
+ * The server has already written the sentence the operator needs -- which methods are accepted, and
214
+ * that they should log in again -- so it is surfaced as-is rather than buried inside
215
+ * `Auth refresh failed (403): {"type":...}`. Matched on errorCode, never on the prose.
216
+ */
217
+ function asShellAuthMethodError(err, hint) {
218
+ if (!(err instanceof AuthEndpointError) || err.problem?.errorCode !== ERROR_CODE_SHELL_AUTH_METHOD) {
219
+ return undefined;
220
+ }
221
+ const detail = err.problem.detail ?? err.problem.title ?? "Shell access requires a different sign-in method.";
222
+ return new ShellAuthMethodError(`${detail}${hint}`);
223
+ }
164
224
  async function requestTokenEndpoint(cfg, endpoint, payload) {
165
225
  const url = `${cfg.baseUrl.replace(/\/$/, "")}/api/v1/auth/${endpoint}`;
166
226
  const response = await fetch(url, {
@@ -173,7 +233,7 @@ async function requestTokenEndpoint(cfg, endpoint, payload) {
173
233
  });
174
234
  if (!response.ok) {
175
235
  const body = await response.text();
176
- throw new Error(`Auth ${endpoint} failed (${response.status}): ${body}`);
236
+ throw new AuthEndpointError(`Auth ${endpoint} failed (${response.status}): ${body}`, response.status, parseProblemDetail(body));
177
237
  }
178
238
  if (endpoint === "logout") {
179
239
  return undefined;
@@ -229,6 +289,10 @@ async function exchangeCallbackToken(cfg, callbackToken) {
229
289
  return await authExchangeFirebaseToken(cfg, callbackToken);
230
290
  }
231
291
  catch (err) {
292
+ const refusal = asShellAuthMethodError(err, "");
293
+ if (refusal) {
294
+ throw refusal;
295
+ }
232
296
  const reason = err instanceof Error ? err.message : String(err);
233
297
  // The identity hint only fits a 401. On a network error or a 5xx, telling someone to check
234
298
  // which account they used is misdirection -- the same species of misleading report this
@@ -543,7 +607,16 @@ async function refreshDefaultToken(cfg) {
543
607
  if (!account?.refreshToken) {
544
608
  return undefined;
545
609
  }
546
- const refreshed = await authRefreshToken(cfg, account.refreshToken);
610
+ let refreshed;
611
+ try {
612
+ refreshed = await authRefreshToken(cfg, account.refreshToken);
613
+ }
614
+ catch (err) {
615
+ // The common case for this refusal is not a fresh login but an existing session on the day
616
+ // the gate ships: the server revokes it on first refresh, and without this the operator gets
617
+ // the raw ProblemDetail JSON after two wasted round-trips.
618
+ throw asShellAuthMethodError(err, `\nRun 'edge-cli --product ${cfg.product} login' to sign in again.`) ?? err;
619
+ }
547
620
  const merged = {
548
621
  accessToken: refreshed.accessToken,
549
622
  refreshToken: refreshed.refreshToken || account.refreshToken,
@@ -580,7 +653,13 @@ async function resolveAccessToken(cfg, forceRefresh = false, skipValidate = fals
580
653
  accessToken = refreshed;
581
654
  }
582
655
  }
583
- catch {
656
+ catch (err) {
657
+ // A failed refresh is ordinarily not fatal -- the existing token may still have a minute
658
+ // on it. The method refusal is different: the session is revoked server-side, so falling
659
+ // through here only trades one clear error for a confusing 401 from whatever comes next.
660
+ if (err instanceof ShellAuthMethodError) {
661
+ throw err;
662
+ }
584
663
  // continue with existing token
585
664
  }
586
665
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youkno/edge-cli",
3
- "version": "1.21.331",
3
+ "version": "1.21.332",
4
4
  "description": "Cross-platform Edge CLI",
5
5
  "bin": "dist/index.js",
6
6
  "publishConfig": {
package/src/lib/auth.ts CHANGED
@@ -205,6 +205,74 @@ function extractEmailFromToken(accessToken: string): string {
205
205
  return "unknown@local";
206
206
  }
207
207
 
208
+ /**
209
+ * The server's `errorCode` for "you signed in a way the shell does not accept". Mirrors
210
+ * `ApiErrorCode.SHELL_AUTH_METHOD_NOT_ALLOWED`. Matched on the number rather than the message so a
211
+ * reworded refusal does not silently stop being recognised here.
212
+ */
213
+ const ERROR_CODE_SHELL_AUTH_METHOD = 403006;
214
+
215
+ interface ProblemDetail {
216
+ title?: string;
217
+ detail?: string;
218
+ errorCode?: number;
219
+ }
220
+
221
+ /**
222
+ * The RFC 7807 body an Edge API error carries, or undefined if this one did not send one (a proxy
223
+ * error page, an HTML 502). Never throws -- a failure to parse the explanation of a failure must not
224
+ * replace it with a parse error.
225
+ */
226
+ function parseProblemDetail(body: string): ProblemDetail | undefined {
227
+ try {
228
+ const parsed = JSON.parse(body);
229
+ return parsed && typeof parsed === "object" ? (parsed as ProblemDetail) : undefined;
230
+ } catch {
231
+ return undefined;
232
+ }
233
+ }
234
+
235
+ class AuthEndpointError extends Error {
236
+ constructor(
237
+ message: string,
238
+ readonly status: number,
239
+ readonly problem?: ProblemDetail
240
+ ) {
241
+ super(message);
242
+ this.name = "AuthEndpointError";
243
+ }
244
+ }
245
+
246
+ /**
247
+ * The server refused the sign-in METHOD rather than the identity: this session was created with, or
248
+ * is being used from, a login the shell does not accept.
249
+ *
250
+ * Its own type because it is the one auth failure the operator can act on immediately, and because
251
+ * it must survive `resolveAccessToken`'s "a failed refresh is not fatal" handling, which otherwise
252
+ * swallows it and falls through to an access token that is already dead.
253
+ */
254
+ export class ShellAuthMethodError extends Error {
255
+ constructor(message: string) {
256
+ super(message);
257
+ this.name = "ShellAuthMethodError";
258
+ }
259
+ }
260
+
261
+ /**
262
+ * The refusal as a clean error, or undefined when this failure is something else.
263
+ *
264
+ * The server has already written the sentence the operator needs -- which methods are accepted, and
265
+ * that they should log in again -- so it is surfaced as-is rather than buried inside
266
+ * `Auth refresh failed (403): {"type":...}`. Matched on errorCode, never on the prose.
267
+ */
268
+ function asShellAuthMethodError(err: unknown, hint: string): ShellAuthMethodError | undefined {
269
+ if (!(err instanceof AuthEndpointError) || err.problem?.errorCode !== ERROR_CODE_SHELL_AUTH_METHOD) {
270
+ return undefined;
271
+ }
272
+ const detail = err.problem.detail ?? err.problem.title ?? "Shell access requires a different sign-in method.";
273
+ return new ShellAuthMethodError(`${detail}${hint}`);
274
+ }
275
+
208
276
  async function requestTokenEndpoint(
209
277
  cfg: EffectiveConfig,
210
278
  endpoint: "exchange" | "refresh" | "logout",
@@ -222,7 +290,11 @@ async function requestTokenEndpoint(
222
290
 
223
291
  if (!response.ok) {
224
292
  const body = await response.text();
225
- throw new Error(`Auth ${endpoint} failed (${response.status}): ${body}`);
293
+ throw new AuthEndpointError(
294
+ `Auth ${endpoint} failed (${response.status}): ${body}`,
295
+ response.status,
296
+ parseProblemDetail(body)
297
+ );
226
298
  }
227
299
 
228
300
  if (endpoint === "logout") {
@@ -287,6 +359,11 @@ async function exchangeCallbackToken(cfg: EffectiveConfig, callbackToken: string
287
359
  try {
288
360
  return await authExchangeFirebaseToken(cfg, callbackToken);
289
361
  } catch (err) {
362
+ const refusal = asShellAuthMethodError(err, "");
363
+ if (refusal) {
364
+ throw refusal;
365
+ }
366
+
290
367
  const reason = err instanceof Error ? err.message : String(err);
291
368
  // The identity hint only fits a 401. On a network error or a 5xx, telling someone to check
292
369
  // which account they used is misdirection -- the same species of misleading report this
@@ -646,7 +723,16 @@ async function refreshDefaultToken(cfg: EffectiveConfig): Promise<string | undef
646
723
  return undefined;
647
724
  }
648
725
 
649
- const refreshed = await authRefreshToken(cfg, account.refreshToken);
726
+ let refreshed: TokenResponse;
727
+ try {
728
+ refreshed = await authRefreshToken(cfg, account.refreshToken);
729
+ } catch (err) {
730
+ // The common case for this refusal is not a fresh login but an existing session on the day
731
+ // the gate ships: the server revokes it on first refresh, and without this the operator gets
732
+ // the raw ProblemDetail JSON after two wasted round-trips.
733
+ throw asShellAuthMethodError(err, `\nRun 'edge-cli --product ${cfg.product} login' to sign in again.`) ?? err;
734
+ }
735
+
650
736
  const merged: TokenResponse = {
651
737
  accessToken: refreshed.accessToken,
652
738
  refreshToken: refreshed.refreshToken || account.refreshToken,
@@ -686,7 +772,13 @@ export async function resolveAccessToken(cfg: EffectiveConfig, forceRefresh = fa
686
772
  if (refreshed) {
687
773
  accessToken = refreshed;
688
774
  }
689
- } catch {
775
+ } catch (err) {
776
+ // A failed refresh is ordinarily not fatal -- the existing token may still have a minute
777
+ // on it. The method refusal is different: the session is revoked server-side, so falling
778
+ // through here only trades one clear error for a confusing 401 from whatever comes next.
779
+ if (err instanceof ShellAuthMethodError) {
780
+ throw err;
781
+ }
690
782
  // continue with existing token
691
783
  }
692
784
  }