@getrouter/getrouter-cli 0.1.7 → 0.1.9
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/bin.mjs +32 -21
- package/package.json +1 -1
- package/src/cmd/auth.ts +1 -1
- package/src/core/api/client.ts +3 -0
- package/src/core/auth/device.ts +30 -15
- package/src/core/http/request.ts +9 -3
- package/tests/http/request.test.ts +30 -0
package/dist/bin.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { randomInt } from "node:crypto";
|
|
|
8
8
|
import prompts from "prompts";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "0.1.
|
|
11
|
+
var version = "0.1.9";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
@@ -369,12 +369,15 @@ const shouldRetryResponse = (error) => {
|
|
|
369
369
|
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") return isServerError(error.status);
|
|
370
370
|
return error instanceof TypeError;
|
|
371
371
|
};
|
|
372
|
-
const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries = 3, _retrySleep }) => {
|
|
372
|
+
const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries = 3, includeAuth = true, _retrySleep }) => {
|
|
373
373
|
return withRetry(async () => {
|
|
374
|
-
const auth = readAuth()
|
|
374
|
+
const auth = includeAuth ? readAuth() : {
|
|
375
|
+
accessToken: void 0,
|
|
376
|
+
refreshToken: void 0
|
|
377
|
+
};
|
|
375
378
|
const url = buildApiUrl(path$1);
|
|
376
|
-
let res = await doFetch(url, method, buildHeaders(auth.accessToken), body, fetchImpl);
|
|
377
|
-
if (res.status === 401 && auth.refreshToken) {
|
|
379
|
+
let res = await doFetch(url, method, includeAuth ? buildHeaders(auth.accessToken) : buildHeaders(), body, fetchImpl);
|
|
380
|
+
if (includeAuth && res.status === 401 && auth.refreshToken) {
|
|
378
381
|
const refreshed = await refreshAccessToken({ fetchImpl });
|
|
379
382
|
if (refreshed?.accessToken) res = await doFetch(url, method, buildHeaders(refreshed.accessToken), body, fetchImpl);
|
|
380
383
|
}
|
|
@@ -389,7 +392,7 @@ const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries =
|
|
|
389
392
|
|
|
390
393
|
//#endregion
|
|
391
394
|
//#region src/core/api/client.ts
|
|
392
|
-
const createApiClients = ({ fetchImpl, clients }) => {
|
|
395
|
+
const createApiClients = ({ fetchImpl, clients, includeAuth = true }) => {
|
|
393
396
|
const factories = clients ?? {
|
|
394
397
|
createConsumerServiceClient,
|
|
395
398
|
createAuthServiceClient,
|
|
@@ -402,7 +405,8 @@ const createApiClients = ({ fetchImpl, clients }) => {
|
|
|
402
405
|
path: path$1,
|
|
403
406
|
method,
|
|
404
407
|
body: body ? JSON.parse(body) : void 0,
|
|
405
|
-
fetchImpl
|
|
408
|
+
fetchImpl,
|
|
409
|
+
includeAuth
|
|
406
410
|
});
|
|
407
411
|
};
|
|
408
412
|
return {
|
|
@@ -446,31 +450,38 @@ const generateAuthCode = () => {
|
|
|
446
450
|
return out;
|
|
447
451
|
};
|
|
448
452
|
const buildLoginUrl = (authCode) => `https://getrouter.dev/auth/${authCode}`;
|
|
453
|
+
const spawnBrowser = (command, args) => {
|
|
454
|
+
try {
|
|
455
|
+
const child = spawn(command, args, {
|
|
456
|
+
stdio: "ignore",
|
|
457
|
+
detached: true
|
|
458
|
+
});
|
|
459
|
+
child.on("error", (err) => {
|
|
460
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
|
|
461
|
+
const reason = code === "ENOENT" ? ` (${command} not found)` : code ? ` (${code})` : "";
|
|
462
|
+
console.log(`⚠️ Unable to open browser${reason}. Please open the URL manually.`);
|
|
463
|
+
});
|
|
464
|
+
child.unref();
|
|
465
|
+
} catch {
|
|
466
|
+
console.log("⚠️ Unable to open browser. Please open the URL manually.");
|
|
467
|
+
}
|
|
468
|
+
};
|
|
449
469
|
const openLoginUrl = async (url) => {
|
|
450
470
|
try {
|
|
451
471
|
if (process.platform === "darwin") {
|
|
452
|
-
|
|
453
|
-
stdio: "ignore",
|
|
454
|
-
detached: true
|
|
455
|
-
}).unref();
|
|
472
|
+
spawnBrowser("open", [url]);
|
|
456
473
|
return;
|
|
457
474
|
}
|
|
458
475
|
if (process.platform === "win32") {
|
|
459
|
-
|
|
476
|
+
spawnBrowser("cmd", [
|
|
460
477
|
"/c",
|
|
461
478
|
"start",
|
|
462
479
|
"",
|
|
463
480
|
url
|
|
464
|
-
]
|
|
465
|
-
stdio: "ignore",
|
|
466
|
-
detached: true
|
|
467
|
-
}).unref();
|
|
481
|
+
]);
|
|
468
482
|
return;
|
|
469
483
|
}
|
|
470
|
-
|
|
471
|
-
stdio: "ignore",
|
|
472
|
-
detached: true
|
|
473
|
-
}).unref();
|
|
484
|
+
spawnBrowser("xdg-open", [url]);
|
|
474
485
|
} catch {}
|
|
475
486
|
};
|
|
476
487
|
const pollAuthorize = async ({ authorize, code, timeoutMs = 300 * 1e3, initialDelayMs = 1e3, maxDelayMs = 1e4, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = () => Date.now(), onRetry }) => {
|
|
@@ -498,7 +509,7 @@ const pollAuthorize = async ({ authorize, code, timeoutMs = 300 * 1e3, initialDe
|
|
|
498
509
|
//#region src/cmd/auth.ts
|
|
499
510
|
const registerAuthCommands = (program) => {
|
|
500
511
|
program.command("login").description("Login with device flow").action(async () => {
|
|
501
|
-
const { authService } = createApiClients({});
|
|
512
|
+
const { authService } = createApiClients({ includeAuth: false });
|
|
502
513
|
const authCode = generateAuthCode();
|
|
503
514
|
const url = buildLoginUrl(authCode);
|
|
504
515
|
console.log("🔐 To authenticate, visit:");
|
package/package.json
CHANGED
package/src/cmd/auth.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const registerAuthCommands = (program: Command) => {
|
|
|
14
14
|
.command("login")
|
|
15
15
|
.description("Login with device flow")
|
|
16
16
|
.action(async () => {
|
|
17
|
-
const { authService } = createApiClients({});
|
|
17
|
+
const { authService } = createApiClients({ includeAuth: false });
|
|
18
18
|
const authCode = generateAuthCode();
|
|
19
19
|
const url = buildLoginUrl(authCode);
|
|
20
20
|
console.log("🔐 To authenticate, visit:");
|
package/src/core/api/client.ts
CHANGED
|
@@ -46,9 +46,11 @@ export type ApiClients = {
|
|
|
46
46
|
export const createApiClients = ({
|
|
47
47
|
fetchImpl,
|
|
48
48
|
clients,
|
|
49
|
+
includeAuth = true,
|
|
49
50
|
}: {
|
|
50
51
|
fetchImpl?: typeof fetch;
|
|
51
52
|
clients?: ClientFactories;
|
|
53
|
+
includeAuth?: boolean;
|
|
52
54
|
}): ApiClients => {
|
|
53
55
|
const factories =
|
|
54
56
|
clients ??
|
|
@@ -66,6 +68,7 @@ export const createApiClients = ({
|
|
|
66
68
|
method,
|
|
67
69
|
body: body ? JSON.parse(body) : undefined,
|
|
68
70
|
fetchImpl,
|
|
71
|
+
includeAuth,
|
|
69
72
|
});
|
|
70
73
|
};
|
|
71
74
|
|
package/src/core/auth/device.ts
CHANGED
|
@@ -33,29 +33,44 @@ export const generateAuthCode = () => {
|
|
|
33
33
|
export const buildLoginUrl = (authCode: string) =>
|
|
34
34
|
`https://getrouter.dev/auth/${authCode}`;
|
|
35
35
|
|
|
36
|
+
const spawnBrowser = (command: string, args: string[]) => {
|
|
37
|
+
try {
|
|
38
|
+
const child = spawn(command, args, {
|
|
39
|
+
stdio: "ignore",
|
|
40
|
+
detached: true,
|
|
41
|
+
});
|
|
42
|
+
child.on("error", (err) => {
|
|
43
|
+
const code =
|
|
44
|
+
typeof err === "object" && err !== null && "code" in err
|
|
45
|
+
? (err as { code?: string }).code
|
|
46
|
+
: undefined;
|
|
47
|
+
const reason =
|
|
48
|
+
code === "ENOENT"
|
|
49
|
+
? ` (${command} not found)`
|
|
50
|
+
: code
|
|
51
|
+
? ` (${code})`
|
|
52
|
+
: "";
|
|
53
|
+
console.log(
|
|
54
|
+
`⚠️ Unable to open browser${reason}. Please open the URL manually.`,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
child.unref();
|
|
58
|
+
} catch {
|
|
59
|
+
console.log("⚠️ Unable to open browser. Please open the URL manually.");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
36
63
|
export const openLoginUrl = async (url: string) => {
|
|
37
64
|
try {
|
|
38
65
|
if (process.platform === "darwin") {
|
|
39
|
-
|
|
40
|
-
stdio: "ignore",
|
|
41
|
-
detached: true,
|
|
42
|
-
});
|
|
43
|
-
child.unref();
|
|
66
|
+
spawnBrowser("open", [url]);
|
|
44
67
|
return;
|
|
45
68
|
}
|
|
46
69
|
if (process.platform === "win32") {
|
|
47
|
-
|
|
48
|
-
stdio: "ignore",
|
|
49
|
-
detached: true,
|
|
50
|
-
});
|
|
51
|
-
child.unref();
|
|
70
|
+
spawnBrowser("cmd", ["/c", "start", "", url]);
|
|
52
71
|
return;
|
|
53
72
|
}
|
|
54
|
-
|
|
55
|
-
stdio: "ignore",
|
|
56
|
-
detached: true,
|
|
57
|
-
});
|
|
58
|
-
child.unref();
|
|
73
|
+
spawnBrowser("xdg-open", [url]);
|
|
59
74
|
} catch {
|
|
60
75
|
// best effort
|
|
61
76
|
}
|
package/src/core/http/request.ts
CHANGED
|
@@ -10,6 +10,7 @@ type RequestInput = {
|
|
|
10
10
|
body?: unknown;
|
|
11
11
|
fetchImpl?: typeof fetch;
|
|
12
12
|
maxRetries?: number;
|
|
13
|
+
includeAuth?: boolean;
|
|
13
14
|
/** For testing: override the sleep function used for retry delays */
|
|
14
15
|
_retrySleep?: (ms: number) => Promise<void>;
|
|
15
16
|
};
|
|
@@ -63,18 +64,23 @@ export const requestJson = async <T = unknown>({
|
|
|
63
64
|
body,
|
|
64
65
|
fetchImpl,
|
|
65
66
|
maxRetries = 3,
|
|
67
|
+
includeAuth = true,
|
|
66
68
|
_retrySleep,
|
|
67
69
|
}: RequestInput): Promise<T> => {
|
|
68
70
|
return withRetry(
|
|
69
71
|
async () => {
|
|
70
|
-
const auth =
|
|
72
|
+
const auth = includeAuth
|
|
73
|
+
? readAuth()
|
|
74
|
+
: { accessToken: undefined, refreshToken: undefined };
|
|
71
75
|
const url = buildApiUrl(path);
|
|
72
|
-
const headers =
|
|
76
|
+
const headers = includeAuth
|
|
77
|
+
? buildHeaders(auth.accessToken)
|
|
78
|
+
: buildHeaders();
|
|
73
79
|
|
|
74
80
|
let res = await doFetch(url, method, headers, body, fetchImpl);
|
|
75
81
|
|
|
76
82
|
// On 401, attempt token refresh and retry once
|
|
77
|
-
if (res.status === 401 && auth.refreshToken) {
|
|
83
|
+
if (includeAuth && res.status === 401 && auth.refreshToken) {
|
|
78
84
|
const refreshed = await refreshAccessToken({ fetchImpl });
|
|
79
85
|
if (refreshed?.accessToken) {
|
|
80
86
|
const newHeaders = buildHeaders(refreshed.accessToken);
|
|
@@ -51,6 +51,36 @@ describe("requestJson", () => {
|
|
|
51
51
|
expect(headers.Cookie).toBe("access_token=t");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it("skips auth headers when includeAuth is false", async () => {
|
|
55
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
56
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(dir, "auth.json"),
|
|
59
|
+
JSON.stringify({ accessToken: "t" }),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const fetchSpy = vi.fn(
|
|
63
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
64
|
+
({
|
|
65
|
+
ok: true,
|
|
66
|
+
json: async () => ({ ok: true }),
|
|
67
|
+
}) as Response,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await requestJson({
|
|
71
|
+
path: "/v1/test",
|
|
72
|
+
method: "GET",
|
|
73
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
74
|
+
includeAuth: false,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const call = fetchSpy.mock.calls[0] as Parameters<typeof fetch> | undefined;
|
|
78
|
+
const init = call?.[1];
|
|
79
|
+
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
80
|
+
expect(headers.Authorization).toBeUndefined();
|
|
81
|
+
expect(headers.Cookie).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
54
84
|
it("uses GETROUTER_AUTH_COOKIE when set", async () => {
|
|
55
85
|
process.env.GETROUTER_AUTH_COOKIE = "router_auth";
|
|
56
86
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|