@trayai/tray-sync-cli 1.0.13 → 1.0.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.
Files changed (135) hide show
  1. package/dist/api/resources/me.js +14 -0
  2. package/dist/commands/api/api.js +4 -0
  3. package/dist/commands/api/login.js +91 -0
  4. package/dist/commands/api/logout.js +39 -0
  5. package/dist/commands/api/project.js +2 -2
  6. package/dist/commands/api/shared.js +10 -4
  7. package/dist/commands/api/workflow.js +4 -4
  8. package/dist/lib/apiAuth.js +25 -2
  9. package/dist/lib/apiSessionStore.js +104 -0
  10. package/dist/lib/browserOpener.js +24 -0
  11. package/dist/lib/dotenvEdit.js +31 -0
  12. package/dist/lib/oauth.js +125 -0
  13. package/dist/lib/oauthCallbackServer.js +97 -0
  14. package/dist/lib/oauthRefresh.js +65 -0
  15. package/dist/lib/region.js +1 -0
  16. package/node_modules/bundle-name/index.js +5 -0
  17. package/node_modules/bundle-name/license +9 -0
  18. package/node_modules/bundle-name/package.json +45 -0
  19. package/node_modules/bundle-name/readme.md +23 -0
  20. package/node_modules/default-browser/index.d.ts +29 -0
  21. package/node_modules/default-browser/index.js +34 -0
  22. package/node_modules/default-browser/license +9 -0
  23. package/node_modules/default-browser/package.json +54 -0
  24. package/node_modules/default-browser/readme.md +24 -0
  25. package/node_modules/default-browser/windows.js +59 -0
  26. package/node_modules/default-browser-id/index.js +25 -0
  27. package/node_modules/default-browser-id/license +9 -0
  28. package/node_modules/default-browser-id/package.json +41 -0
  29. package/node_modules/default-browser-id/readme.md +19 -0
  30. package/node_modules/define-lazy-prop/index.d.ts +31 -0
  31. package/node_modules/define-lazy-prop/index.js +18 -0
  32. package/node_modules/define-lazy-prop/license +9 -0
  33. package/node_modules/define-lazy-prop/package.json +51 -0
  34. package/node_modules/define-lazy-prop/readme.md +55 -0
  35. package/node_modules/dotenv/CHANGELOG.md +643 -0
  36. package/node_modules/dotenv/LICENSE +23 -0
  37. package/node_modules/dotenv/README-es.md +757 -0
  38. package/node_modules/dotenv/README.md +812 -0
  39. package/node_modules/dotenv/SECURITY.md +1 -0
  40. package/node_modules/dotenv/config.d.ts +1 -0
  41. package/node_modules/dotenv/config.js +9 -0
  42. package/node_modules/dotenv/lib/cli-options.js +17 -0
  43. package/node_modules/dotenv/lib/env-options.js +28 -0
  44. package/node_modules/dotenv/lib/main.d.ts +179 -0
  45. package/node_modules/dotenv/lib/main.js +423 -0
  46. package/node_modules/dotenv/package.json +62 -0
  47. package/node_modules/dotenv/skills/dotenv/SKILL.md +200 -0
  48. package/node_modules/dotenv/skills/dotenvx/SKILL.md +118 -0
  49. package/node_modules/graceful-fs/LICENSE +15 -0
  50. package/node_modules/graceful-fs/README.md +143 -0
  51. package/node_modules/graceful-fs/clone.js +23 -0
  52. package/node_modules/graceful-fs/graceful-fs.js +448 -0
  53. package/node_modules/graceful-fs/legacy-streams.js +118 -0
  54. package/node_modules/graceful-fs/package.json +53 -0
  55. package/node_modules/graceful-fs/polyfills.js +355 -0
  56. package/node_modules/is-docker/cli.js +5 -0
  57. package/node_modules/is-docker/index.d.ts +13 -0
  58. package/node_modules/is-docker/index.js +29 -0
  59. package/node_modules/is-docker/license +9 -0
  60. package/node_modules/is-docker/package.json +44 -0
  61. package/node_modules/is-docker/readme.md +27 -0
  62. package/node_modules/is-in-ssh/index.d.ts +15 -0
  63. package/node_modules/is-in-ssh/index.js +7 -0
  64. package/node_modules/is-in-ssh/license +9 -0
  65. package/node_modules/is-in-ssh/package.json +43 -0
  66. package/node_modules/is-in-ssh/readme.md +43 -0
  67. package/node_modules/is-inside-container/cli.js +5 -0
  68. package/node_modules/is-inside-container/index.d.ts +13 -0
  69. package/node_modules/is-inside-container/index.js +23 -0
  70. package/node_modules/is-inside-container/license +9 -0
  71. package/node_modules/is-inside-container/package.json +51 -0
  72. package/node_modules/is-inside-container/readme.md +36 -0
  73. package/node_modules/is-wsl/index.d.ts +15 -0
  74. package/node_modules/is-wsl/index.js +36 -0
  75. package/node_modules/is-wsl/license +9 -0
  76. package/node_modules/is-wsl/package.json +58 -0
  77. package/node_modules/is-wsl/readme.md +21 -0
  78. package/node_modules/open/index.d.ts +158 -0
  79. package/node_modules/open/index.js +425 -0
  80. package/node_modules/open/license +9 -0
  81. package/node_modules/open/package.json +70 -0
  82. package/node_modules/open/readme.md +197 -0
  83. package/node_modules/open/xdg-open +1267 -0
  84. package/node_modules/powershell-utils/index.d.ts +175 -0
  85. package/node_modules/powershell-utils/index.js +79 -0
  86. package/node_modules/powershell-utils/license +9 -0
  87. package/node_modules/powershell-utils/package.json +47 -0
  88. package/node_modules/powershell-utils/readme.md +209 -0
  89. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  90. package/node_modules/proper-lockfile/LICENSE +21 -0
  91. package/node_modules/proper-lockfile/README.md +183 -0
  92. package/node_modules/proper-lockfile/index.js +40 -0
  93. package/node_modules/proper-lockfile/lib/adapter.js +85 -0
  94. package/node_modules/proper-lockfile/lib/lockfile.js +342 -0
  95. package/node_modules/proper-lockfile/lib/mtime-precision.js +55 -0
  96. package/node_modules/proper-lockfile/package.json +71 -0
  97. package/node_modules/retry/.npmignore +3 -0
  98. package/node_modules/retry/.travis.yml +15 -0
  99. package/node_modules/retry/License +21 -0
  100. package/node_modules/retry/Makefile +18 -0
  101. package/node_modules/retry/README.md +227 -0
  102. package/node_modules/retry/equation.gif +0 -0
  103. package/node_modules/retry/example/dns.js +31 -0
  104. package/node_modules/retry/example/stop.js +40 -0
  105. package/node_modules/retry/index.js +1 -0
  106. package/node_modules/retry/lib/retry.js +100 -0
  107. package/node_modules/retry/lib/retry_operation.js +158 -0
  108. package/node_modules/retry/package.json +32 -0
  109. package/node_modules/retry/test/common.js +10 -0
  110. package/node_modules/retry/test/integration/test-forever.js +24 -0
  111. package/node_modules/retry/test/integration/test-retry-operation.js +258 -0
  112. package/node_modules/retry/test/integration/test-retry-wrap.js +101 -0
  113. package/node_modules/retry/test/integration/test-timeouts.js +69 -0
  114. package/node_modules/run-applescript/index.d.ts +66 -0
  115. package/node_modules/run-applescript/index.js +37 -0
  116. package/node_modules/run-applescript/license +9 -0
  117. package/node_modules/run-applescript/package.json +42 -0
  118. package/node_modules/run-applescript/readme.md +78 -0
  119. package/node_modules/signal-exit/LICENSE.txt +16 -0
  120. package/node_modules/signal-exit/README.md +39 -0
  121. package/node_modules/signal-exit/index.js +202 -0
  122. package/node_modules/signal-exit/package.json +38 -0
  123. package/node_modules/signal-exit/signals.js +53 -0
  124. package/node_modules/wsl-utils/index.d.ts +180 -0
  125. package/node_modules/wsl-utils/index.js +145 -0
  126. package/node_modules/wsl-utils/license +9 -0
  127. package/node_modules/wsl-utils/node_modules/powershell-utils/index.d.ts +122 -0
  128. package/node_modules/wsl-utils/node_modules/powershell-utils/index.js +58 -0
  129. package/node_modules/wsl-utils/node_modules/powershell-utils/license +9 -0
  130. package/node_modules/wsl-utils/node_modules/powershell-utils/package.json +47 -0
  131. package/node_modules/wsl-utils/node_modules/powershell-utils/readme.md +153 -0
  132. package/node_modules/wsl-utils/package.json +48 -0
  133. package/node_modules/wsl-utils/readme.md +223 -0
  134. package/node_modules/wsl-utils/utilities.js +19 -0
  135. package/package.json +8 -1
@@ -0,0 +1,14 @@
1
+ import { AuthError } from "../errors.js";
2
+ export async function getMe(client) {
3
+ try {
4
+ const body = (await client.request("GET", "/v1/me"));
5
+ const user = body.data ?? body;
6
+ return { id: user.id, email: user.email, name: user.name };
7
+ }
8
+ catch (err) {
9
+ if (err instanceof AuthError)
10
+ return null;
11
+ throw err;
12
+ }
13
+ }
14
+ //# sourceMappingURL=me.js.map
@@ -1,7 +1,11 @@
1
1
  import { Command } from "commander";
2
2
  import { projectCommand } from "./project.js";
3
3
  import { workflowCommand } from "./workflow.js";
4
+ import { loginCommand } from "./login.js";
5
+ import { logoutCommand } from "./logout.js";
4
6
  export const apiCommand = new Command("api").description("Direct, authenticated access to the Tray API");
5
7
  apiCommand.addCommand(projectCommand);
6
8
  apiCommand.addCommand(workflowCommand);
9
+ apiCommand.addCommand(loginCommand);
10
+ apiCommand.addCommand(logoutCommand);
7
11
  //# sourceMappingURL=api.js.map
@@ -0,0 +1,91 @@
1
+ import { Command } from "commander";
2
+ import { parseRegion } from "../../lib/regionParsing.js";
3
+ import { generatePkcePair, generateState, authorizeUrl, exchangeAuthorizationCode, oauthClientId, OAUTH_SCOPE, apiResource } from "../../lib/oauth.js";
4
+ import { startCallbackServer, DEFAULT_CALLBACK_TIMEOUT_MS } from "../../lib/oauthCallbackServer.js";
5
+ import { openInBrowser } from "../../lib/browserOpener.js";
6
+ import { toApiSession } from "../../lib/oauthRefresh.js";
7
+ import { createApiSessionStore } from "../../lib/apiSessionStore.js";
8
+ import { getMe } from "../../api/resources/me.js";
9
+ import { createTrayClientForRegion } from "../../api/trayClient.js";
10
+ import { FAILURE_ICON, SUCCESS_ICON } from "../../views/icons.js";
11
+ export async function runApiLogin(deps, options) {
12
+ const { stdout, stderr } = deps;
13
+ if (!deps.isInteractive()) {
14
+ stderr(`${FAILURE_ICON} "tray api login" needs an interactive terminal: it opens a browser and waits for you to sign in. Run it directly in your terminal.`);
15
+ return 1;
16
+ }
17
+ const { region } = options;
18
+ const { verifier, challenge } = generatePkcePair();
19
+ const state = generateState();
20
+ const controller = new AbortController();
21
+ const onSigint = () => controller.abort();
22
+ const server = await deps.startCallback({
23
+ expectedState: state,
24
+ timeoutMs: DEFAULT_CALLBACK_TIMEOUT_MS,
25
+ signal: controller.signal,
26
+ });
27
+ process.once("SIGINT", onSigint);
28
+ try {
29
+ const url = new URL(authorizeUrl(region));
30
+ url.searchParams.set("response_type", "code");
31
+ url.searchParams.set("client_id", oauthClientId(region));
32
+ url.searchParams.set("redirect_uri", server.redirectUri);
33
+ url.searchParams.set("scope", OAUTH_SCOPE);
34
+ url.searchParams.set("resource", apiResource(region));
35
+ url.searchParams.set("state", state);
36
+ url.searchParams.set("code_challenge", challenge);
37
+ url.searchParams.set("code_challenge_method", "S256");
38
+ const authorizeUrlString = url.toString();
39
+ try {
40
+ await deps.openBrowser(authorizeUrlString);
41
+ stdout(`\n Opened your browser to sign in to Tray (${region}).`);
42
+ stdout(` Didn't open? Visit:\n\n ${authorizeUrlString}\n`);
43
+ }
44
+ catch {
45
+ stdout(`\n Couldn't open a browser automatically. To sign in, visit:\n\n ${authorizeUrlString}\n`);
46
+ }
47
+ stdout(" Waiting for you to sign in... (Ctrl-C to cancel)");
48
+ const code = await server.waitForCode();
49
+ const pair = await deps.exchangeCode({
50
+ region,
51
+ code,
52
+ redirectUri: server.redirectUri,
53
+ codeVerifier: verifier,
54
+ });
55
+ const client = deps.createClient(region, pair.accessToken);
56
+ const user = await getMe(client);
57
+ if (user === null) {
58
+ stderr(`${FAILURE_ICON} Tray rejected the token from the browser sign-in. Run "tray api login" to try again.`);
59
+ return 1;
60
+ }
61
+ await deps.store.withLock(() => deps.store.set(toApiSession(region, pair, deps.now())));
62
+ const label = user.email ?? user.name ?? user.id ?? "your account";
63
+ stdout(`${SUCCESS_ICON} Logged in as ${label} (${region}).`);
64
+ return 0;
65
+ }
66
+ finally {
67
+ process.removeListener("SIGINT", onSigint);
68
+ await server.close();
69
+ }
70
+ }
71
+ function realDeps() {
72
+ return {
73
+ env: process.env,
74
+ stdout: (line) => console.log(line),
75
+ stderr: (line) => console.error(line),
76
+ store: createApiSessionStore(),
77
+ createClient: createTrayClientForRegion,
78
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
79
+ openBrowser: openInBrowser,
80
+ startCallback: startCallbackServer,
81
+ exchangeCode: exchangeAuthorizationCode,
82
+ now: Date.now,
83
+ };
84
+ }
85
+ export const loginCommand = new Command("login")
86
+ .description("Sign in via browser (OAuth2) and store the token for tray api commands")
87
+ .requiredOption("-r, --region <region>", "Tray region to sign in to", parseRegion)
88
+ .action(async (opts) => {
89
+ process.exitCode = await runApiLogin(realDeps(), { region: opts.region });
90
+ });
91
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1,39 @@
1
+ import { Command } from "commander";
2
+ import { revokeToken } from "../../lib/oauth.js";
3
+ import { createApiSessionStore } from "../../lib/apiSessionStore.js";
4
+ import { SUCCESS_ICON, INFO_ICON } from "../../views/icons.js";
5
+ export async function runApiLogout(deps) {
6
+ const { stdout, store } = deps;
7
+ const session = await store.withLock(async () => {
8
+ const current = await store.get();
9
+ await store.clear();
10
+ return current;
11
+ });
12
+ if (!session) {
13
+ stdout(`${INFO_ICON} Not logged in.`);
14
+ return 0;
15
+ }
16
+ if (session.method === "oauth") {
17
+ try {
18
+ await deps.revokeToken(session.region, session.token);
19
+ }
20
+ catch {
21
+ }
22
+ }
23
+ stdout(`${SUCCESS_ICON} Logged out (${session.region}).`);
24
+ return 0;
25
+ }
26
+ function realDeps() {
27
+ return {
28
+ stdout: (line) => console.log(line),
29
+ stderr: (line) => console.error(line),
30
+ store: createApiSessionStore(),
31
+ revokeToken,
32
+ };
33
+ }
34
+ export const logoutCommand = new Command("logout")
35
+ .description("Log out of tray api (remove the stored token and revoke it server-side)")
36
+ .action(async () => {
37
+ process.exitCode = await runApiLogout(realDeps());
38
+ });
39
+ //# sourceMappingURL=logout.js.map
@@ -21,7 +21,7 @@ function parseProjectId(value) {
21
21
  return value;
22
22
  }
23
23
  export async function runProjectRollback(deps, options) {
24
- const client = apiClientFor(deps, options);
24
+ const client = await apiClientFor(deps, options);
25
25
  await rollbackProject(client, options.projectId, options.versionNumber);
26
26
  if (options.format === OutputFormats.JSON) {
27
27
  printConfirmationJson(deps.stdout, {
@@ -36,7 +36,7 @@ export async function runProjectRollback(deps, options) {
36
36
  return 0;
37
37
  }
38
38
  export async function runProjectVersionList(deps, options) {
39
- const client = apiClientFor(deps, options);
39
+ const client = await apiClientFor(deps, options);
40
40
  const result = await listProjectVersions(client, options.projectId, {
41
41
  last: options.limit ?? DEFAULT_VERSION_LIST_LIMIT,
42
42
  });
@@ -1,5 +1,6 @@
1
1
  import { createTrayClientForRegion } from "../../api/trayClient.js";
2
- import { resolveApiRegion, resolveApiToken } from "../../lib/apiAuth.js";
2
+ import { resolveApiCredentials } from "../../lib/apiAuth.js";
3
+ import { createApiSessionStore } from "../../lib/apiSessionStore.js";
3
4
  import { parseRegion } from "../../lib/regionParsing.js";
4
5
  import { OutputFormats, parseFormat } from "../../lib/outputFormat.js";
5
6
  import { FAILURE_ICON } from "../../views/icons.js";
@@ -9,6 +10,7 @@ export function realDeps() {
9
10
  createClient: createTrayClientForRegion,
10
11
  stdout: (line) => console.log(line),
11
12
  stderr: (line) => console.error(line),
13
+ apiSessionStore: createApiSessionStore(),
12
14
  };
13
15
  }
14
16
  export function addCommonApiOptions(command) {
@@ -27,9 +29,13 @@ export async function runApiAction(deps, run) {
27
29
  process.exitCode = 1;
28
30
  }
29
31
  }
30
- export function apiClientFor(deps, options) {
31
- const token = resolveApiToken({ tokenFlag: options.token, env: deps.env });
32
- const region = resolveApiRegion({ regionFlag: options.region, env: deps.env });
32
+ export async function apiClientFor(deps, options) {
33
+ const { token, region } = await resolveApiCredentials({
34
+ tokenFlag: options.token,
35
+ regionFlag: options.region,
36
+ env: deps.env,
37
+ store: deps.apiSessionStore,
38
+ });
33
39
  return deps.createClient(region, token);
34
40
  }
35
41
  //# sourceMappingURL=shared.js.map
@@ -9,7 +9,7 @@ import { FAILURE_ICON } from "../../views/icons.js";
9
9
  import { COMMAND_COLOR } from "../../views/help.js";
10
10
  import { addCommonApiOptions, apiClientFor, realDeps, runApiAction, } from "./shared.js";
11
11
  async function runWorkflowSetEnabled(deps, options, enabled) {
12
- const client = apiClientFor(deps, options);
12
+ const client = await apiClientFor(deps, options);
13
13
  await setWorkflowEnabled(client, options.workflowId, enabled);
14
14
  if (options.format === OutputFormats.JSON) {
15
15
  printConfirmationJson(deps.stdout, { workflowId: options.workflowId, enabled });
@@ -26,7 +26,7 @@ export async function runWorkflowDisable(deps, options) {
26
26
  return runWorkflowSetEnabled(deps, options, false);
27
27
  }
28
28
  export async function runWorkflowShow(deps, options) {
29
- const client = apiClientFor(deps, options);
29
+ const client = await apiClientFor(deps, options);
30
30
  const workflow = await getWorkflow(client, options.workflowId);
31
31
  if (options.format === OutputFormats.JSON) {
32
32
  deps.stdout(JSON.stringify(workflow, null, 2));
@@ -37,7 +37,7 @@ export async function runWorkflowShow(deps, options) {
37
37
  return 0;
38
38
  }
39
39
  export async function runWorkflowDelete(deps, options) {
40
- const client = apiClientFor(deps, options);
40
+ const client = await apiClientFor(deps, options);
41
41
  await deleteWorkflow(client, options.workflowId);
42
42
  if (options.format === OutputFormats.JSON) {
43
43
  printConfirmationJson(deps.stdout, { workflowId: options.workflowId, deleted: true });
@@ -52,7 +52,7 @@ export async function runWorkflowEdit(deps, options) {
52
52
  deps.stderr(`${FAILURE_ICON} Nothing to edit — payload has no fields.`);
53
53
  return 1;
54
54
  }
55
- const client = apiClientFor(deps, options);
55
+ const client = await apiClientFor(deps, options);
56
56
  await updateWorkflowUnversioned(client, options.workflowId, options.payload);
57
57
  if (options.format === OutputFormats.JSON) {
58
58
  printConfirmationJson(deps.stdout, { workflowId: options.workflowId, edited: true });
@@ -1,12 +1,15 @@
1
+ import ansis from "ansis";
1
2
  import { REGIONS, PUBLIC_REGIONS } from "./regionParsing.js";
2
3
  import { describeRegion } from "./regionDisplay.js";
4
+ import { createApiSessionStore } from "./apiSessionStore.js";
5
+ import { getValidApiSession } from "./oauthRefresh.js";
3
6
  export function resolveApiToken(options) {
4
7
  const { tokenFlag, env = process.env } = options;
5
8
  if (tokenFlag)
6
9
  return tokenFlag;
7
10
  if (env.TRAY_API_TOKEN)
8
11
  return env.TRAY_API_TOKEN;
9
- throw new Error("No API token provided. Pass -t/--token <token> or set TRAY_API_TOKEN.");
12
+ throw new Error(`Not authenticated. Run ${ansis.bold("tray api login -r <region>")} to sign in, pass ${ansis.bold("-t/--token <token>")}, or set the ${ansis.bold("TRAY_API_TOKEN")} environment variable in your terminal.`);
10
13
  }
11
14
  export function resolveApiRegion(options) {
12
15
  const { regionFlag, env = process.env } = options;
@@ -19,9 +22,29 @@ export function resolveApiRegion(options) {
19
22
  }
20
23
  return envRegion;
21
24
  }
22
- throw new Error("No region provided. Pass -r/--region <region> or set TRAY_API_REGION.");
25
+ throw new Error(`No region provided. Run ${ansis.bold("tray api login -r <region>")} to sign in, pass ${ansis.bold("-r/--region <region>")}, or set the ${ansis.bold("TRAY_API_REGION")} environment variable in your terminal.`);
23
26
  }
24
27
  export function isApiEnabled(env = process.env) {
25
28
  return env.TRAY_CLI_API_ENABLE === "true";
26
29
  }
30
+ export async function resolveApiCredentials(options) {
31
+ const { tokenFlag, regionFlag, env = process.env } = options;
32
+ if (tokenFlag && regionFlag) {
33
+ return { token: tokenFlag, region: regionFlag };
34
+ }
35
+ if (tokenFlag || regionFlag) {
36
+ return {
37
+ token: resolveApiToken({ tokenFlag, env }),
38
+ region: resolveApiRegion({ regionFlag, env }),
39
+ };
40
+ }
41
+ const store = options.store ?? createApiSessionStore();
42
+ const session = await getValidApiSession(store);
43
+ if (session)
44
+ return session;
45
+ return {
46
+ token: resolveApiToken({ env }),
47
+ region: resolveApiRegion({ env }),
48
+ };
49
+ }
27
50
  //# sourceMappingURL=apiAuth.js.map
@@ -0,0 +1,104 @@
1
+ import { mkdir, open, readFile, rename, unlink } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { join, dirname } from "node:path";
5
+ import { lock } from "proper-lockfile";
6
+ import { REGIONS } from "./regionParsing.js";
7
+ import { upsertDotEnv, removeDotEnvKey, readDotEnvKey } from "./dotenvEdit.js";
8
+ const KEY_TOKEN = "TCLI_TOKEN";
9
+ const KEY_REGION = "TCLI_REGION";
10
+ const KEY_REFRESH_TOKEN = "TCLI_REFRESH_TOKEN";
11
+ const KEY_EXPIRES_AT = "TCLI_TOKEN_EXPIRES_AT";
12
+ const KEY_METHOD = "TCLI_TOKEN_METHOD";
13
+ export function defaultApiSessionPath(env = process.env) {
14
+ return env.TRAY_CONFIG_PATH ?? join(homedir(), ".tray.config");
15
+ }
16
+ function writeOptionalKey(source, key, value) {
17
+ return value !== undefined ? upsertDotEnv(source, key, value) : removeDotEnvKey(source, key);
18
+ }
19
+ async function readFileOrEmpty(path) {
20
+ try {
21
+ return await readFile(path, "utf-8");
22
+ }
23
+ catch (err) {
24
+ if (err.code === "ENOENT")
25
+ return "";
26
+ throw err;
27
+ }
28
+ }
29
+ async function writeFileAtomic(path, content) {
30
+ await mkdir(dirname(path), { recursive: true });
31
+ const tmp = `${path}.${randomUUID()}.tmp`;
32
+ try {
33
+ const fh = await open(tmp, "w", 0o600);
34
+ try {
35
+ await fh.writeFile(content, "utf-8");
36
+ await fh.sync();
37
+ }
38
+ finally {
39
+ await fh.close();
40
+ }
41
+ await rename(tmp, path);
42
+ }
43
+ catch (err) {
44
+ await unlink(tmp).catch(() => { });
45
+ throw err;
46
+ }
47
+ }
48
+ export function createApiSessionStore(path = defaultApiSessionPath()) {
49
+ async function setSession(session) {
50
+ let source = await readFileOrEmpty(path);
51
+ source = upsertDotEnv(source, KEY_TOKEN, session.token);
52
+ source = upsertDotEnv(source, KEY_REGION, session.region);
53
+ source = writeOptionalKey(source, KEY_REFRESH_TOKEN, session.refreshToken);
54
+ source = writeOptionalKey(source, KEY_EXPIRES_AT, session.expiresAt !== undefined ? String(session.expiresAt) : undefined);
55
+ source = writeOptionalKey(source, KEY_METHOD, session.method);
56
+ await writeFileAtomic(path, source);
57
+ }
58
+ return {
59
+ async get() {
60
+ const source = await readFileOrEmpty(path);
61
+ const token = readDotEnvKey(source, KEY_TOKEN);
62
+ const regionRaw = readDotEnvKey(source, KEY_REGION);
63
+ if (!token || !regionRaw || !REGIONS.includes(regionRaw))
64
+ return null;
65
+ const region = regionRaw;
66
+ const refreshToken = readDotEnvKey(source, KEY_REFRESH_TOKEN) ?? undefined;
67
+ const expiresAtRaw = readDotEnvKey(source, KEY_EXPIRES_AT);
68
+ let expiresAt;
69
+ if (expiresAtRaw) {
70
+ const parsed = Number(expiresAtRaw);
71
+ if (Number.isFinite(parsed))
72
+ expiresAt = parsed;
73
+ }
74
+ const methodRaw = readDotEnvKey(source, KEY_METHOD);
75
+ const method = methodRaw === "oauth" ? "oauth" : undefined;
76
+ return { token, region, refreshToken, expiresAt, method };
77
+ },
78
+ set: setSession,
79
+ async clear() {
80
+ let source = await readFileOrEmpty(path);
81
+ source = removeDotEnvKey(source, KEY_TOKEN);
82
+ source = removeDotEnvKey(source, KEY_REGION);
83
+ source = removeDotEnvKey(source, KEY_REFRESH_TOKEN);
84
+ source = removeDotEnvKey(source, KEY_EXPIRES_AT);
85
+ source = removeDotEnvKey(source, KEY_METHOD);
86
+ await writeFileAtomic(path, source);
87
+ },
88
+ async withLock(fn) {
89
+ await mkdir(dirname(path), { recursive: true });
90
+ const release = await lock(path, {
91
+ realpath: false,
92
+ stale: 60_000,
93
+ retries: { retries: 35, minTimeout: 100, maxTimeout: 2_000 },
94
+ });
95
+ try {
96
+ return await fn();
97
+ }
98
+ finally {
99
+ await release();
100
+ }
101
+ },
102
+ };
103
+ }
104
+ //# sourceMappingURL=apiSessionStore.js.map
@@ -0,0 +1,24 @@
1
+ export class BrowserOpenError extends Error {
2
+ cause;
3
+ constructor(message, cause) {
4
+ super(message);
5
+ this.cause = cause;
6
+ this.name = "BrowserOpenError";
7
+ }
8
+ }
9
+ export async function openInBrowser(url) {
10
+ let open;
11
+ try {
12
+ open = (await import("open")).default;
13
+ }
14
+ catch (err) {
15
+ throw new BrowserOpenError("Could not load the browser opener. Open the printed URL manually.", err);
16
+ }
17
+ try {
18
+ await open(url);
19
+ }
20
+ catch (err) {
21
+ throw new BrowserOpenError("Could not open a browser automatically. Open the printed URL manually.", err);
22
+ }
23
+ }
24
+ //# sourceMappingURL=browserOpener.js.map
@@ -0,0 +1,31 @@
1
+ import { parse as dotenvParse } from "dotenv";
2
+ function detectEol(source) {
3
+ return source.includes("\r\n") ? "\r\n" : "\n";
4
+ }
5
+ function escapeKey(key) {
6
+ return key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
+ }
8
+ export function upsertDotEnv(source, key, value) {
9
+ if (/[\r\n]/.test(value)) {
10
+ throw new Error(`Refusing to write a value containing a newline for ${key}`);
11
+ }
12
+ const eol = detectEol(source);
13
+ const line = `${key}=${value}`;
14
+ const re = new RegExp(`^${escapeKey(key)}=.*$`, "m");
15
+ if (re.test(source)) {
16
+ return source.replace(re, line);
17
+ }
18
+ if (source.length === 0)
19
+ return line + eol;
20
+ const endsWithNewline = source.endsWith("\n") || source.endsWith("\r");
21
+ return source + (endsWithNewline ? "" : eol) + line + eol;
22
+ }
23
+ export function removeDotEnvKey(source, key) {
24
+ const re = new RegExp(`^${escapeKey(key)}=.*(?:\\r?\\n)?`, "m");
25
+ return source.replace(re, "");
26
+ }
27
+ export function readDotEnvKey(source, key) {
28
+ const parsed = dotenvParse(source);
29
+ return Object.prototype.hasOwnProperty.call(parsed, key) ? parsed[key] : null;
30
+ }
31
+ //# sourceMappingURL=dotenvEdit.js.map
@@ -0,0 +1,125 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { urlsForRegion } from "./region.js";
3
+ import { NetworkError, IncompatibleApiError } from "../api/errors.js";
4
+ const AUTHORIZE_PATH = "/server/oauth2/authorize";
5
+ const TOKEN_PATH = "/server/oauth2/token";
6
+ const REVOKE_PATH = "/server/oauth2/revoke";
7
+ export const OAUTH_SCOPE = "api:full";
8
+ export function oauthClientId(region) {
9
+ return `${urlsForRegion(region).authBaseUrl}/client/oauth2/tray-cli-client-metadata`;
10
+ }
11
+ export function authorizeUrl(region) {
12
+ return `${urlsForRegion(region).authBaseUrl}${AUTHORIZE_PATH}`;
13
+ }
14
+ export function tokenEndpoint(region) {
15
+ return `${urlsForRegion(region).authBaseUrl}${TOKEN_PATH}`;
16
+ }
17
+ export function revokeEndpoint(region) {
18
+ return `${urlsForRegion(region).authBaseUrl}${REVOKE_PATH}`;
19
+ }
20
+ export function generatePkcePair() {
21
+ const verifier = randomBytes(32).toString("base64url");
22
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
23
+ return { verifier, challenge };
24
+ }
25
+ export function generateState() {
26
+ return randomBytes(16).toString("base64url");
27
+ }
28
+ export function apiResource(region) {
29
+ const api = urlsForRegion(region).apiBaseUrl;
30
+ return api.endsWith("/") ? api : `${api}/`;
31
+ }
32
+ export class OAuthTokenError extends Error {
33
+ errorCode;
34
+ status;
35
+ constructor(message, errorCode, status) {
36
+ super(message);
37
+ this.errorCode = errorCode;
38
+ this.status = status;
39
+ this.name = "OAuthTokenError";
40
+ }
41
+ get requiresReauth() {
42
+ return this.errorCode === "invalid_grant";
43
+ }
44
+ }
45
+ function isOAuthErrorBody(value) {
46
+ return (typeof value === "object" &&
47
+ value !== null &&
48
+ typeof value.error === "string");
49
+ }
50
+ function isTokenResponse(value) {
51
+ if (typeof value !== "object" || value === null)
52
+ return false;
53
+ const body = value;
54
+ return (typeof body.access_token === "string" &&
55
+ body.access_token.length > 0 &&
56
+ typeof body.refresh_token === "string" &&
57
+ body.refresh_token.length > 0 &&
58
+ typeof body.expires_in === "number" &&
59
+ body.expires_in > 0);
60
+ }
61
+ async function requestTokenPair(endpoint, form, fetchImpl) {
62
+ let response;
63
+ try {
64
+ response = await fetchImpl(endpoint, {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
67
+ body: new URLSearchParams(form),
68
+ signal: AbortSignal.timeout(30_000),
69
+ });
70
+ }
71
+ catch (err) {
72
+ throw new NetworkError(`Request to ${endpoint} failed: ${err.message}`);
73
+ }
74
+ const text = await response.text();
75
+ let body;
76
+ try {
77
+ body = text === "" ? undefined : JSON.parse(text);
78
+ }
79
+ catch {
80
+ throw new IncompatibleApiError(`Could not parse response from ${endpoint}`);
81
+ }
82
+ if (!response.ok) {
83
+ if (isOAuthErrorBody(body)) {
84
+ throw new OAuthTokenError(`Tray login service rejected the request (${body.error}${body.error_description ? `: ${body.error_description}` : ""}).`, body.error, response.status);
85
+ }
86
+ throw new OAuthTokenError(`Tray login service returned HTTP ${response.status}.`, undefined, response.status);
87
+ }
88
+ if (!isTokenResponse(body)) {
89
+ throw new OAuthTokenError("Tray login service returned an unexpected token response.");
90
+ }
91
+ return {
92
+ accessToken: body.access_token,
93
+ refreshToken: body.refresh_token,
94
+ expiresInSeconds: body.expires_in,
95
+ };
96
+ }
97
+ export function exchangeAuthorizationCode(params) {
98
+ return requestTokenPair(tokenEndpoint(params.region), {
99
+ grant_type: "authorization_code",
100
+ code: params.code,
101
+ redirect_uri: params.redirectUri,
102
+ client_id: oauthClientId(params.region),
103
+ code_verifier: params.codeVerifier,
104
+ }, params.fetchImpl ?? fetch);
105
+ }
106
+ export function refreshAccessToken(params) {
107
+ return requestTokenPair(tokenEndpoint(params.region), {
108
+ grant_type: "refresh_token",
109
+ refresh_token: params.refreshToken,
110
+ client_id: oauthClientId(params.region),
111
+ }, params.fetchImpl ?? fetch);
112
+ }
113
+ export async function revokeToken(region, token, fetchImpl = fetch) {
114
+ await fetchImpl(revokeEndpoint(region), {
115
+ method: "POST",
116
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
117
+ body: new URLSearchParams({
118
+ token,
119
+ token_type_hint: "access_token",
120
+ client_id: oauthClientId(region),
121
+ }),
122
+ signal: AbortSignal.timeout(15_000),
123
+ });
124
+ }
125
+ //# sourceMappingURL=oauth.js.map