@getrouter/getrouter-cli 0.1.13 → 0.1.14

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.
@@ -43,25 +43,23 @@ export type ApiClients = {
43
43
  usageService: UsageService;
44
44
  };
45
45
 
46
- export const createApiClients = ({
46
+ const defaultFactories: ClientFactories = {
47
+ createConsumerServiceClient,
48
+ createAuthServiceClient,
49
+ createSubscriptionServiceClient,
50
+ createUsageServiceClient,
51
+ createModelServiceClient,
52
+ };
53
+
54
+ export function createApiClients({
47
55
  fetchImpl,
48
- clients,
56
+ clients: factories = defaultFactories,
49
57
  includeAuth = true,
50
58
  }: {
51
59
  fetchImpl?: typeof fetch;
52
60
  clients?: ClientFactories;
53
61
  includeAuth?: boolean;
54
- }): ApiClients => {
55
- const factories =
56
- clients ??
57
- ({
58
- createConsumerServiceClient,
59
- createAuthServiceClient,
60
- createSubscriptionServiceClient,
61
- createUsageServiceClient,
62
- createModelServiceClient,
63
- } satisfies ClientFactories);
64
-
62
+ } = {}): ApiClients {
65
63
  const handler: RequestHandler = async ({ path, method, body }) => {
66
64
  return requestJson({
67
65
  path,
@@ -79,4 +77,4 @@ export const createApiClients = ({
79
77
  subscriptionService: factories.createSubscriptionServiceClient(handler),
80
78
  usageService: factories.createUsageServiceClient(handler),
81
79
  };
82
- };
80
+ }
@@ -6,11 +6,11 @@
6
6
  * @param getNextToken - Function that extracts the next page token from the response
7
7
  * @returns Array of all items across all pages
8
8
  */
9
- export const fetchAllPages = async <TResponse, TItem>(
9
+ export async function fetchAllPages<TResponse, TItem>(
10
10
  fetchPage: (pageToken?: string) => Promise<TResponse>,
11
11
  getItems: (response: TResponse) => TItem[],
12
12
  getNextToken: (response: TResponse) => string | undefined,
13
- ): Promise<TItem[]> => {
13
+ ): Promise<TItem[]> {
14
14
  const allItems: TItem[] = [];
15
15
  let pageToken: string | undefined;
16
16
 
@@ -22,4 +22,4 @@ export const fetchAllPages = async <TResponse, TItem>(
22
22
  } while (pageToken);
23
23
 
24
24
  return allItems;
25
- };
25
+ }
@@ -1,32 +1,33 @@
1
1
  import { requestJson } from "../http/request";
2
2
 
3
- const asTrimmedString = (value: unknown): string | null => {
3
+ type ListProviderModelsOptions = {
4
+ tag?: string;
5
+ fetchImpl?: typeof fetch;
6
+ };
7
+
8
+ function asTrimmedString(value: unknown): string | null {
4
9
  if (typeof value !== "string") return null;
5
10
  const trimmed = value.trim();
6
11
  return trimmed.length > 0 ? trimmed : null;
7
- };
12
+ }
8
13
 
9
- const buildProviderModelsPath = (tag?: string) => {
14
+ function buildProviderModelsPath(tag?: string): string {
10
15
  const query = new URLSearchParams();
11
16
  if (tag) query.set("tag", tag);
12
17
  const qs = query.toString();
13
18
  return `v1/dashboard/providers/models${qs ? `?${qs}` : ""}`;
14
- };
19
+ }
15
20
 
16
- export const listProviderModels = async ({
17
- tag,
18
- fetchImpl,
19
- }: {
20
- tag?: string;
21
- fetchImpl?: typeof fetch;
22
- }): Promise<string[]> => {
21
+ export async function listProviderModels(
22
+ options: ListProviderModelsOptions,
23
+ ): Promise<string[]> {
23
24
  const res = await requestJson<{ models?: unknown }>({
24
- path: buildProviderModelsPath(tag),
25
+ path: buildProviderModelsPath(options.tag),
25
26
  method: "GET",
26
- fetchImpl,
27
+ fetchImpl: options.fetchImpl,
27
28
  maxRetries: 0,
28
29
  });
29
30
  const raw = res?.models;
30
31
  const models = Array.isArray(raw) ? raw : [];
31
32
  return models.map(asTrimmedString).filter(Boolean) as string[];
32
- };
33
+ }
@@ -33,6 +33,12 @@ export const generateAuthCode = () => {
33
33
  export const buildLoginUrl = (authCode: string) =>
34
34
  `https://getrouter.dev/auth/${authCode}`;
35
35
 
36
+ const getErrorCode = (err: unknown) => {
37
+ if (typeof err !== "object" || err === null) return undefined;
38
+ if (!("code" in err)) return undefined;
39
+ return (err as { code?: unknown }).code;
40
+ };
41
+
36
42
  const spawnBrowser = (command: string, args: string[]) => {
37
43
  try {
38
44
  const child = spawn(command, args, {
@@ -40,16 +46,13 @@ const spawnBrowser = (command: string, args: string[]) => {
40
46
  detached: true,
41
47
  });
42
48
  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
- : "";
49
+ const code = getErrorCode(err);
50
+ let reason = "";
51
+ if (code === "ENOENT") {
52
+ reason = ` (${command} not found)`;
53
+ } else if (typeof code === "string") {
54
+ reason = ` (${code})`;
55
+ }
53
56
  console.log(
54
57
  `⚠️ Unable to open browser${reason}. Please open the URL manually.`,
55
58
  );
@@ -62,15 +65,20 @@ const spawnBrowser = (command: string, args: string[]) => {
62
65
 
63
66
  export const openLoginUrl = async (url: string) => {
64
67
  try {
65
- if (process.platform === "darwin") {
66
- spawnBrowser("open", [url]);
67
- return;
68
- }
69
- if (process.platform === "win32") {
70
- spawnBrowser("cmd", ["/c", "start", "", url]);
71
- return;
72
- }
73
- spawnBrowser("xdg-open", [url]);
68
+ const platformCommands: Record<
69
+ string,
70
+ { command: string; args: string[] }
71
+ > = {
72
+ darwin: { command: "open", args: [url] },
73
+ win32: { command: "cmd", args: ["/c", "start", "", url] },
74
+ };
75
+
76
+ const entry = platformCommands[process.platform] ?? {
77
+ command: "xdg-open",
78
+ args: [url],
79
+ };
80
+
81
+ spawnBrowser(entry.command, entry.args);
74
82
  } catch {
75
83
  // best effort
76
84
  }
@@ -89,14 +97,19 @@ export const pollAuthorize = async ({
89
97
  const start = now();
90
98
  let delay = initialDelayMs;
91
99
  let attempt = 0;
100
+
101
+ const getErrorStatus = (err: unknown) => {
102
+ if (typeof err !== "object" || err === null) return undefined;
103
+ if (!("status" in err)) return undefined;
104
+ const status = (err as { status?: unknown }).status;
105
+ return typeof status === "number" ? status : undefined;
106
+ };
107
+
92
108
  while (true) {
93
109
  try {
94
110
  return await authorize({ code });
95
111
  } catch (err: unknown) {
96
- const status =
97
- typeof err === "object" && err !== null && "status" in err
98
- ? (err as { status?: number }).status
99
- : undefined;
112
+ const status = getErrorStatus(err);
100
113
  if (status === 404) {
101
114
  // keep polling
102
115
  } else if (status === 400) {
@@ -107,11 +120,13 @@ export const pollAuthorize = async ({
107
120
  throw err;
108
121
  }
109
122
  }
123
+
110
124
  if (now() - start >= timeoutMs) {
111
125
  throw new Error(
112
126
  "Login timed out. Please run getrouter auth login again.",
113
127
  );
114
128
  }
129
+
115
130
  attempt += 1;
116
131
  onRetry?.(attempt, delay);
117
132
  await sleep(delay);
@@ -10,19 +10,27 @@ type AuthStatus = {
10
10
  tokenType?: string;
11
11
  };
12
12
 
13
- const isExpired = (expiresAt: string) => {
14
- if (!expiresAt) return true;
15
- const t = Date.parse(expiresAt);
16
- if (Number.isNaN(t)) return true;
17
- return t <= Date.now();
18
- };
13
+ export function isTokenExpired(expiresAt: string, bufferMs = 0): boolean {
14
+ if (!expiresAt) {
15
+ return true;
16
+ }
17
+
18
+ const timestampMs = Date.parse(expiresAt);
19
+ if (Number.isNaN(timestampMs)) {
20
+ return true;
21
+ }
22
+
23
+ return timestampMs <= Date.now() + bufferMs;
24
+ }
19
25
 
20
- export const getAuthStatus = (): AuthStatus => {
26
+ export function getAuthStatus(): AuthStatus {
21
27
  const auth = readAuth();
22
28
  const hasTokens = Boolean(auth.accessToken && auth.refreshToken);
23
- if (!hasTokens || isExpired(auth.expiresAt)) {
29
+
30
+ if (!hasTokens || isTokenExpired(auth.expiresAt)) {
24
31
  return { status: "logged_out" };
25
32
  }
33
+
26
34
  return {
27
35
  status: "logged_in",
28
36
  expiresAt: auth.expiresAt,
@@ -30,8 +38,8 @@ export const getAuthStatus = (): AuthStatus => {
30
38
  refreshToken: auth.refreshToken,
31
39
  tokenType: auth.tokenType,
32
40
  };
33
- };
41
+ }
34
42
 
35
- export const clearAuth = () => {
43
+ export function clearAuth(): void {
36
44
  writeAuth(defaultAuthState());
37
- };
45
+ }
@@ -1,5 +1,6 @@
1
1
  import { readAuth, writeAuth } from "../config";
2
2
  import { buildApiUrl } from "../http/url";
3
+ import { isTokenExpired } from "./index";
3
4
 
4
5
  type AuthToken = {
5
6
  accessToken: string | undefined;
@@ -9,19 +10,20 @@ type AuthToken = {
9
10
 
10
11
  const EXPIRY_BUFFER_MS = 60 * 1000; // Refresh 1 minute before expiry
11
12
 
12
- export const isTokenExpiringSoon = (expiresAt: string): boolean => {
13
- if (!expiresAt) return true;
14
- const t = Date.parse(expiresAt);
15
- if (Number.isNaN(t)) return true;
16
- return t <= Date.now() + EXPIRY_BUFFER_MS;
13
+ type RefreshOptions = {
14
+ fetchImpl?: typeof fetch;
17
15
  };
18
16
 
19
- export const refreshAccessToken = async ({
20
- fetchImpl,
21
- }: {
22
- fetchImpl?: typeof fetch;
23
- }): Promise<AuthToken | null> => {
17
+ export function isTokenExpiringSoon(expiresAt: string): boolean {
18
+ return isTokenExpired(expiresAt, EXPIRY_BUFFER_MS);
19
+ }
20
+
21
+ export async function refreshAccessToken(
22
+ options: RefreshOptions,
23
+ ): Promise<AuthToken | null> {
24
+ const { fetchImpl } = options;
24
25
  const auth = readAuth();
26
+
25
27
  if (!auth.refreshToken) {
26
28
  return null;
27
29
  }
@@ -48,21 +50,24 @@ export const refreshAccessToken = async ({
48
50
  tokenType: "Bearer",
49
51
  });
50
52
  }
53
+
51
54
  return token;
52
- };
55
+ }
53
56
 
54
- export const ensureValidToken = async ({
55
- fetchImpl,
56
- }: {
57
- fetchImpl?: typeof fetch;
58
- }): Promise<boolean> => {
57
+ export async function ensureValidToken(
58
+ options: RefreshOptions,
59
+ ): Promise<boolean> {
60
+ const { fetchImpl } = options;
59
61
  const auth = readAuth();
62
+
60
63
  if (!auth.accessToken || !auth.refreshToken) {
61
64
  return false;
62
65
  }
66
+
63
67
  if (!isTokenExpiringSoon(auth.expiresAt)) {
64
68
  return true;
65
69
  }
70
+
66
71
  const refreshed = await refreshAccessToken({ fetchImpl });
67
72
  return refreshed !== null && Boolean(refreshed.accessToken);
68
- };
73
+ }
@@ -1,16 +1,16 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- const getCorruptBackupPath = (filePath: string) => {
4
+ function getCorruptBackupPath(filePath: string): string {
5
5
  const dir = path.dirname(filePath);
6
6
  const ext = path.extname(filePath);
7
7
  const base = path.basename(filePath, ext);
8
8
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
9
9
  const rand = Math.random().toString(16).slice(2, 8);
10
10
  return path.join(dir, `${base}.corrupt-${stamp}-${rand}${ext}`);
11
- };
11
+ }
12
12
 
13
- export const readJsonFile = <T = unknown>(filePath: string): T | null => {
13
+ export function readJsonFile<T = unknown>(filePath: string): T | null {
14
14
  if (!fs.existsSync(filePath)) return null;
15
15
  let raw: string;
16
16
  try {
@@ -36,9 +36,9 @@ export const readJsonFile = <T = unknown>(filePath: string): T | null => {
36
36
  }
37
37
  return null;
38
38
  }
39
- };
39
+ }
40
40
 
41
- export const writeJsonFile = (filePath: string, value: unknown) => {
41
+ export function writeJsonFile(filePath: string, value: unknown): void {
42
42
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
43
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
44
- };
44
+ }
@@ -8,24 +8,29 @@ import {
8
8
  defaultConfig,
9
9
  } from "./types";
10
10
 
11
- export const readConfig = (): ConfigFile => ({
12
- ...defaultConfig(),
13
- ...(readJsonFile<ConfigFile>(getConfigPath()) ?? {}),
14
- });
11
+ export function readConfig(): ConfigFile {
12
+ return {
13
+ ...defaultConfig(),
14
+ ...(readJsonFile<ConfigFile>(getConfigPath()) ?? {}),
15
+ };
16
+ }
15
17
 
16
- export const writeConfig = (cfg: ConfigFile) =>
18
+ export function writeConfig(cfg: ConfigFile): void {
17
19
  writeJsonFile(getConfigPath(), cfg);
20
+ }
18
21
 
19
- export const readAuth = (): AuthState => ({
20
- ...defaultAuthState(),
21
- ...(readJsonFile<AuthState>(getAuthPath()) ?? {}),
22
- });
22
+ export function readAuth(): AuthState {
23
+ return {
24
+ ...defaultAuthState(),
25
+ ...(readJsonFile<AuthState>(getAuthPath()) ?? {}),
26
+ };
27
+ }
23
28
 
24
- export const writeAuth = (auth: AuthState) => {
29
+ export function writeAuth(auth: AuthState): void {
25
30
  const authPath = getAuthPath();
26
31
  writeJsonFile(authPath, auth);
27
32
  if (process.platform !== "win32") {
28
33
  // Restrict token file permissions on Unix-like systems.
29
34
  fs.chmodSync(authPath, 0o600);
30
35
  }
31
- };
36
+ }
@@ -1,8 +1,16 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
- export const resolveConfigDir = () =>
5
- process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
4
+ export function resolveConfigDir(): string {
5
+ return (
6
+ process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter")
7
+ );
8
+ }
6
9
 
7
- export const getConfigPath = () => path.join(resolveConfigDir(), "config.json");
8
- export const getAuthPath = () => path.join(resolveConfigDir(), "auth.json");
10
+ export function getConfigPath(): string {
11
+ return path.join(resolveConfigDir(), "config.json");
12
+ }
13
+
14
+ export function getAuthPath(): string {
15
+ return path.join(resolveConfigDir(), "auth.json");
16
+ }
@@ -1,12 +1,12 @@
1
1
  const SECRET_KEYS = new Set(["accessToken", "refreshToken", "apiKey"]);
2
2
 
3
- const mask = (value: string) => {
3
+ function mask(value: string): string {
4
4
  if (!value) return "";
5
5
  if (value.length <= 8) return "****";
6
6
  return `${value.slice(0, 4)}...${value.slice(-4)}`;
7
- };
7
+ }
8
8
 
9
- export const redactSecrets = <T extends Record<string, unknown>>(obj: T): T => {
9
+ export function redactSecrets<T extends Record<string, unknown>>(obj: T): T {
10
10
  const out: Record<string, unknown> = { ...obj };
11
11
  for (const key of Object.keys(out)) {
12
12
  const value = out[key];
@@ -15,4 +15,4 @@ export const redactSecrets = <T extends Record<string, unknown>>(obj: T): T => {
15
15
  }
16
16
  }
17
17
  return out as T;
18
- };
18
+ }
@@ -10,14 +10,18 @@ export type AuthState = {
10
10
  tokenType: string;
11
11
  };
12
12
 
13
- export const defaultConfig = (): ConfigFile => ({
14
- apiBase: "https://getrouter.dev",
15
- json: false,
16
- });
13
+ export function defaultConfig(): ConfigFile {
14
+ return {
15
+ apiBase: "https://getrouter.dev",
16
+ json: false,
17
+ };
18
+ }
17
19
 
18
- export const defaultAuthState = (): AuthState => ({
19
- accessToken: "",
20
- refreshToken: "",
21
- expiresAt: "",
22
- tokenType: "Bearer",
23
- });
20
+ export function defaultAuthState(): AuthState {
21
+ return {
22
+ accessToken: "",
23
+ refreshToken: "",
24
+ expiresAt: "",
25
+ tokenType: "Bearer",
26
+ };
27
+ }
@@ -1,32 +1,40 @@
1
- export type ApiError = {
1
+ export interface ApiError {
2
2
  code?: string;
3
3
  message: string;
4
4
  details?: unknown;
5
5
  status?: number;
6
- };
6
+ }
7
7
 
8
- export const createApiError = (
8
+ export function createApiError(
9
9
  payload: unknown,
10
10
  fallbackMessage: string,
11
11
  status?: number,
12
- ) => {
12
+ ): Error & ApiError {
13
13
  const payloadObject =
14
14
  payload && typeof payload === "object"
15
15
  ? (payload as Record<string, unknown>)
16
16
  : undefined;
17
+
17
18
  const message =
18
- payloadObject && typeof payloadObject.message === "string"
19
+ typeof payloadObject?.message === "string"
19
20
  ? payloadObject.message
20
21
  : fallbackMessage;
22
+
21
23
  const err = new Error(message) as Error & ApiError;
22
- if (payloadObject && typeof payloadObject.code === "string") {
23
- err.code = payloadObject.code;
24
+
25
+ const code = payloadObject?.code;
26
+ if (typeof code === "string") {
27
+ err.code = code;
24
28
  }
25
- if (payloadObject && payloadObject.details != null) {
26
- err.details = payloadObject.details;
29
+
30
+ const details = payloadObject?.details;
31
+ if (details != null) {
32
+ err.details = details;
27
33
  }
34
+
28
35
  if (typeof status === "number") {
29
36
  err.status = status;
30
37
  }
38
+
31
39
  return err;
32
- };
40
+ }
@@ -1,7 +1,7 @@
1
1
  import { refreshAccessToken } from "../auth/refresh";
2
2
  import { readAuth } from "../config";
3
3
  import { createApiError } from "./errors";
4
- import { isServerError, withRetry } from "./retry";
4
+ import { isRetryableError, withRetry } from "./retry";
5
5
  import { buildApiUrl } from "./url";
6
6
 
7
7
  type RequestInput = {
@@ -15,50 +15,48 @@ type RequestInput = {
15
15
  _retrySleep?: (ms: number) => Promise<void>;
16
16
  };
17
17
 
18
- const getAuthCookieName = () =>
19
- process.env.GETROUTER_AUTH_COOKIE ||
20
- process.env.KRATOS_AUTH_COOKIE ||
21
- "access_token";
18
+ function getAuthCookieName(): string {
19
+ const routerCookieName = process.env.GETROUTER_AUTH_COOKIE;
20
+ if (routerCookieName) {
21
+ return routerCookieName;
22
+ }
23
+
24
+ const kratosCookieName = process.env.KRATOS_AUTH_COOKIE;
25
+ if (kratosCookieName) {
26
+ return kratosCookieName;
27
+ }
28
+
29
+ return "access_token";
30
+ }
22
31
 
23
- const buildHeaders = (accessToken?: string): Record<string, string> => {
32
+ function buildHeaders(accessToken?: string): Record<string, string> {
24
33
  const headers: Record<string, string> = {
25
34
  "Content-Type": "application/json",
26
35
  };
36
+
27
37
  if (accessToken) {
28
38
  headers.Authorization = `Bearer ${accessToken}`;
29
39
  headers.Cookie = `${getAuthCookieName()}=${accessToken}`;
30
40
  }
41
+
31
42
  return headers;
32
- };
43
+ }
33
44
 
34
- const doFetch = async (
45
+ async function doFetch(
35
46
  url: string,
36
47
  method: string,
37
48
  headers: Record<string, string>,
38
49
  body: unknown,
39
50
  fetchImpl?: typeof fetch,
40
- ): Promise<Response> => {
51
+ ): Promise<Response> {
41
52
  return (fetchImpl ?? fetch)(url, {
42
53
  method,
43
54
  headers,
44
55
  body: body == null ? undefined : JSON.stringify(body),
45
56
  });
46
- };
47
-
48
- const shouldRetryResponse = (error: unknown): boolean => {
49
- if (
50
- typeof error === "object" &&
51
- error !== null &&
52
- "status" in error &&
53
- typeof (error as { status: unknown }).status === "number"
54
- ) {
55
- return isServerError((error as { status: number }).status);
56
- }
57
- // Retry on network errors (TypeError from fetch)
58
- return error instanceof TypeError;
59
- };
57
+ }
60
58
 
61
- export const requestJson = async <T = unknown>({
59
+ export async function requestJson<T = unknown>({
62
60
  path,
63
61
  method,
64
62
  body,
@@ -66,21 +64,17 @@ export const requestJson = async <T = unknown>({
66
64
  maxRetries = 3,
67
65
  includeAuth = true,
68
66
  _retrySleep,
69
- }: RequestInput): Promise<T> => {
67
+ }: RequestInput): Promise<T> {
70
68
  return withRetry(
71
69
  async () => {
72
- const auth = includeAuth
73
- ? readAuth()
74
- : { accessToken: undefined, refreshToken: undefined };
75
70
  const url = buildApiUrl(path);
76
- const headers = includeAuth
77
- ? buildHeaders(auth.accessToken)
78
- : buildHeaders();
71
+ const auth = includeAuth ? readAuth() : undefined;
72
+ const headers = buildHeaders(auth?.accessToken);
79
73
 
80
74
  let res = await doFetch(url, method, headers, body, fetchImpl);
81
75
 
82
76
  // On 401, attempt token refresh and retry once
83
- if (includeAuth && res.status === 401 && auth.refreshToken) {
77
+ if (includeAuth && res.status === 401 && auth?.refreshToken) {
84
78
  const refreshed = await refreshAccessToken({ fetchImpl });
85
79
  if (refreshed?.accessToken) {
86
80
  const newHeaders = buildHeaders(refreshed.accessToken);
@@ -92,12 +86,13 @@ export const requestJson = async <T = unknown>({
92
86
  const payload = await res.json().catch(() => null);
93
87
  throw createApiError(payload, res.statusText, res.status);
94
88
  }
89
+
95
90
  return (await res.json()) as T;
96
91
  },
97
92
  {
98
93
  maxRetries,
99
- shouldRetry: shouldRetryResponse,
94
+ shouldRetry: isRetryableError,
100
95
  sleep: _retrySleep,
101
96
  },
102
97
  );
103
- };
98
+ }