@youkno/edge-cli 1.21.330 → 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 +82 -3
- package/package.json +1 -1
- package/src/lib/auth.ts +95 -3
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
|
|
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
|
-
|
|
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
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
|
|
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
|
-
|
|
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
|
}
|