attio 0.0.1-experimental.20250408 → 0.0.1-experimental.20250414

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 (59) hide show
  1. package/lib/api/api.js +99 -31
  2. package/lib/api/fetcher.js +31 -27
  3. package/lib/api/schemas.js +6 -0
  4. package/lib/auth/auth.js +135 -97
  5. package/lib/auth/keychain.js +86 -43
  6. package/lib/build/server/generate-server-entry.js +6 -6
  7. package/lib/build.js +2 -2
  8. package/lib/commands/build/build-javascript.js +1 -1
  9. package/lib/commands/build/validate-typescript.js +1 -1
  10. package/lib/commands/build.js +2 -2
  11. package/lib/commands/dev/boot.js +9 -9
  12. package/lib/commands/dev/bundle-javascript.js +1 -1
  13. package/lib/commands/dev/onboarding.js +3 -3
  14. package/lib/commands/dev/prepare-build-contexts.js +1 -1
  15. package/lib/commands/dev/upload.js +4 -21
  16. package/lib/commands/dev.js +5 -4
  17. package/lib/commands/init/create-project.js +7 -39
  18. package/lib/commands/init.js +6 -14
  19. package/lib/commands/login.js +8 -2
  20. package/lib/commands/logout.js +8 -2
  21. package/lib/commands/version/create/bundle-javascript.js +3 -3
  22. package/lib/commands/version/create.js +14 -14
  23. package/lib/commands/version/list.js +4 -4
  24. package/lib/commands/whoami.js +7 -8
  25. package/lib/errors.js +1 -0
  26. package/lib/print-errors.js +165 -0
  27. package/lib/spinners/determine-workspace.spinner.js +3 -26
  28. package/lib/spinners/get-app-info.spinner.js +2 -2
  29. package/lib/spinners/get-app-slug-from-package-json.js +1 -20
  30. package/lib/spinners/get-versions.spinner.js +2 -2
  31. package/lib/util/copy-with-replace.js +2 -2
  32. package/lib/util/create-directory.js +1 -1
  33. package/lib/util/load-attio-cli-version.js +1 -26
  34. package/lib/util/upload-bundle.js +1 -1
  35. package/package.json +1 -1
  36. package/lib/auth/ensure-authed.js +0 -14
  37. package/lib/commands/dev/start-graphql-server.js +0 -32
  38. package/lib/commands/init/ask-language.js +0 -11
  39. package/lib/templates/javascript/.env +0 -12
  40. package/lib/templates/javascript/eslint.config.js +0 -48
  41. package/lib/templates/javascript/package.json +0 -26
  42. package/lib/templates/javascript/src/assets/icon.png +0 -0
  43. package/lib/templates/javascript/src/cat-fact.jsx +0 -16
  44. package/lib/templates/javascript/src/get-cat-fact.server.js +0 -6
  45. package/lib/templates/javascript/src/hello-world-action.jsx +0 -17
  46. package/lib/templates/javascript/src/hello-world-dialog.jsx +0 -26
  47. package/lib/templates/typescript/.eslintignore +0 -3
  48. package/lib/templates/typescript/src/assets/icon.png +0 -0
  49. /package/lib/{templates/typescript → template}/.env +0 -0
  50. /package/lib/{templates/javascript → template}/.eslintignore +0 -0
  51. /package/lib/{templates/common → template}/README.md +0 -0
  52. /package/lib/{templates/typescript → template}/eslint.config.js +0 -0
  53. /package/lib/{templates/typescript → template}/package.json +0 -0
  54. /package/lib/{templates/common → template}/src/assets/icon.png +0 -0
  55. /package/lib/{templates/typescript → template}/src/cat-fact.tsx +0 -0
  56. /package/lib/{templates/typescript → template}/src/get-cat-fact.server.ts +0 -0
  57. /package/lib/{templates/typescript → template}/src/hello-world-action.tsx +0 -0
  58. /package/lib/{templates/typescript → template}/src/hello-world-dialog.tsx +0 -0
  59. /package/lib/{templates/typescript → template}/tsconfig.json +0 -0
package/lib/api/api.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import { Fetcher } from "./fetcher.js";
2
2
  import { APP } from "../env.js";
3
3
  import { isErrored, complete, errored } from "@attio/fetchable";
4
- import { whoamiSchema, listDevWorkspacesResponseSchema, createVersionSchema, appInfoSchema, completeBundleUploadSchema, startUploadSchema, createDevVersionSchema, installationSchema, versionsSchema, TEST_WORKSPACES, TEST_APP_INFO, } from "./schemas.js";
5
- class CliApi {
4
+ import { whoamiSchema, listDevWorkspacesResponseSchema, createVersionSchema, appInfoSchema, completeBundleUploadSchema, startUploadSchema, createDevVersionSchema, installationSchema, versionsSchema, TEST_WORKSPACES, TEST_APP_INFO, tokenResponseSchema, } from "./schemas.js";
5
+ class ApiImpl {
6
6
  _fetcher;
7
7
  constructor() {
8
8
  this._fetcher = new Fetcher();
9
9
  }
10
10
  async whoami() {
11
- const result = await this._fetcher.get(`${APP}/api/auth/whoami`, whoamiSchema);
11
+ const result = await this._fetcher.get({
12
+ path: `${APP}/api/auth/whoami`,
13
+ schema: whoamiSchema,
14
+ });
12
15
  if (isErrored(result)) {
13
- return errored({ code: "WHOAMI_ERROR", fetcherError: result.error });
16
+ return errored({ code: "WHOAMI_ERROR", error: result.error });
14
17
  }
15
18
  return complete(result.value.user);
16
19
  }
@@ -18,19 +21,26 @@ class CliApi {
18
21
  if (process.env.NODE_ENV === "test") {
19
22
  return complete(TEST_WORKSPACES);
20
23
  }
21
- const result = await this._fetcher.get("/dev-workspaces", listDevWorkspacesResponseSchema);
24
+ const result = await this._fetcher.get({
25
+ path: "/dev-workspaces",
26
+ schema: listDevWorkspacesResponseSchema,
27
+ });
22
28
  if (isErrored(result)) {
23
- return errored({ code: "FETCH_WORKSPACES_ERROR", fetcherError: result.error });
29
+ return errored({ code: "FETCH_WORKSPACES_ERROR", error: result.error });
24
30
  }
25
31
  return complete(result.value.workspaces);
26
32
  }
27
33
  async createVersion({ appId, major, cliVersion, }) {
28
- const result = await this._fetcher.post(`/apps/${appId}/prod-versions`, {
29
- major,
30
- cli_version: cliVersion,
31
- }, createVersionSchema);
34
+ const result = await this._fetcher.post({
35
+ path: `/apps/${appId}/prod-versions`,
36
+ body: {
37
+ major,
38
+ cli_version: cliVersion,
39
+ },
40
+ schema: createVersionSchema,
41
+ });
32
42
  if (isErrored(result)) {
33
- return errored({ code: "CREATE_VERSION_ERROR", fetcherError: result.error });
43
+ return errored({ code: "CREATE_VERSION_ERROR", error: result.error });
34
44
  }
35
45
  return result;
36
46
  }
@@ -38,61 +48,119 @@ class CliApi {
38
48
  if (process.env.NODE_ENV === "test") {
39
49
  return complete(TEST_APP_INFO.app);
40
50
  }
41
- const result = await this._fetcher.get(`/apps/by-slug/${appSlug}`, appInfoSchema);
51
+ const result = await this._fetcher.get({
52
+ path: `/apps/by-slug/${appSlug}`,
53
+ schema: appInfoSchema,
54
+ });
42
55
  if (isErrored(result)) {
43
- return errored({ code: "FETCH_APP_INFO_ERROR", fetcherError: result.error });
56
+ return errored({ code: "FETCH_APP_INFO_ERROR", error: result.error });
44
57
  }
45
58
  return complete(result.value.app);
46
59
  }
47
60
  async completeBundleUpload({ appId, devVersionId, bundleId, }) {
48
- const result = await this._fetcher.post(`/apps/${appId}/dev-versions/${devVersionId}/bundles/${bundleId}/complete`, {}, completeBundleUploadSchema);
61
+ const result = await this._fetcher.post({
62
+ path: `/apps/${appId}/dev-versions/${devVersionId}/bundles/${bundleId}/complete`,
63
+ body: {},
64
+ schema: completeBundleUploadSchema,
65
+ });
49
66
  if (isErrored(result)) {
50
- return errored({ code: "COMPLETE_BUNDLE_UPLOAD_ERROR", fetcherError: result.error });
67
+ return errored({ code: "COMPLETE_BUNDLE_UPLOAD_ERROR", error: result.error });
51
68
  }
52
69
  return complete(undefined);
53
70
  }
54
71
  async completeProdBundleUpload({ appId, bundleId, major, minor, }) {
55
- const result = await this._fetcher.post(`/apps/${appId}/prod-versions/${major}/${minor}/bundles/${bundleId}/complete`, {}, completeBundleUploadSchema);
72
+ const result = await this._fetcher.post({
73
+ path: `/apps/${appId}/prod-versions/${major}/${minor}/bundles/${bundleId}/complete`,
74
+ body: {},
75
+ schema: completeBundleUploadSchema,
76
+ });
56
77
  if (isErrored(result)) {
57
- return errored({ code: "COMPLETE_PROD_BUNDLE_UPLOAD_ERROR", fetcherError: result.error });
78
+ return errored({ code: "COMPLETE_PROD_BUNDLE_UPLOAD_ERROR", error: result.error });
58
79
  }
59
80
  return complete(undefined);
60
81
  }
61
82
  async startUpload({ appId, devVersionId, }) {
62
- const result = await this._fetcher.post(`/apps/${appId}/dev-versions/${devVersionId}/bundles`, {}, startUploadSchema);
83
+ const result = await this._fetcher.post({
84
+ path: `/apps/${appId}/dev-versions/${devVersionId}/bundles`,
85
+ body: {},
86
+ schema: startUploadSchema,
87
+ });
63
88
  if (isErrored(result)) {
64
- return errored({ code: "START_UPLOAD_ERROR", fetcherError: result.error });
89
+ return errored({ code: "START_UPLOAD_ERROR", error: result.error });
65
90
  }
66
91
  return result;
67
92
  }
68
93
  async createDevVersion({ appId, cliVersion, targetWorkspaceId, environmentVariables, }) {
69
- const result = await this._fetcher.post(`/apps/${appId}/dev-versions`, {
70
- major: 1,
71
- target_workspace_id: targetWorkspaceId,
72
- environment_variables: environmentVariables,
73
- cli_version: cliVersion,
74
- }, createDevVersionSchema);
94
+ const result = await this._fetcher.post({
95
+ path: `/apps/${appId}/dev-versions`,
96
+ body: {
97
+ major: 1,
98
+ target_workspace_id: targetWorkspaceId,
99
+ environment_variables: environmentVariables,
100
+ cli_version: cliVersion,
101
+ },
102
+ schema: createDevVersionSchema,
103
+ });
75
104
  if (isErrored(result)) {
76
- return errored({ code: "CREATE_DEV_VERSION_ERROR", fetcherError: result.error });
105
+ return errored({ code: "CREATE_DEV_VERSION_ERROR", error: result.error });
77
106
  }
78
107
  return result;
79
108
  }
80
109
  async fetchInstallation({ appId, workspaceId, }) {
81
- const result = await this._fetcher.get(`/apps/${appId}/workspace/${workspaceId}/dev-installation`, installationSchema);
110
+ const result = await this._fetcher.get({
111
+ path: `/apps/${appId}/workspace/${workspaceId}/dev-installation`,
112
+ schema: installationSchema,
113
+ });
82
114
  if (isErrored(result)) {
83
115
  if (result.error.code === "HTTP_ERROR" && result.error.status === 404) {
84
116
  return complete(null);
85
117
  }
86
- return errored({ code: "FETCH_INSTALLATION_ERROR", fetcherError: result.error });
118
+ return errored({ code: "FETCH_INSTALLATION_ERROR", error: result.error });
87
119
  }
88
120
  return result;
89
121
  }
90
122
  async fetchVersions(appId) {
91
- const result = await this._fetcher.get(`/apps/${appId}/prod-versions`, versionsSchema);
123
+ const result = await this._fetcher.get({
124
+ path: `/apps/${appId}/prod-versions`,
125
+ schema: versionsSchema,
126
+ });
92
127
  if (isErrored(result)) {
93
- return errored({ code: "FETCH_VERSIONS_ERROR", fetcherError: result.error });
128
+ return errored({ code: "FETCH_VERSIONS_ERROR", error: result.error });
94
129
  }
95
130
  return complete(result.value.app_prod_versions);
96
131
  }
132
+ async exchangeToken({ receivedCode, verifierString, redirectUri, clientId, }) {
133
+ const params = new URLSearchParams();
134
+ params.append("grant_type", "authorization_code");
135
+ params.append("code", receivedCode);
136
+ params.append("client_id", clientId);
137
+ params.append("redirect_uri", redirectUri);
138
+ params.append("code_verifier", verifierString);
139
+ const result = await this._fetcher.post({
140
+ path: `${APP}/oidc/token`,
141
+ body: params,
142
+ schema: tokenResponseSchema,
143
+ authenticated: "Not Authenticated",
144
+ });
145
+ if (isErrored(result)) {
146
+ return errored({ code: "GET_TOKEN_ERROR", error: result.error });
147
+ }
148
+ return result;
149
+ }
150
+ async refreshToken({ refreshToken, clientId, }) {
151
+ const params = new URLSearchParams();
152
+ params.append("grant_type", "refresh_token");
153
+ params.append("refresh_token", refreshToken);
154
+ params.append("client_id", clientId);
155
+ const result = await this._fetcher.post({
156
+ path: `${APP}/oidc/token`,
157
+ body: params,
158
+ schema: tokenResponseSchema,
159
+ });
160
+ if (isErrored(result)) {
161
+ return errored({ code: "REFRESH_TOKEN_ERROR", error: result.error });
162
+ }
163
+ return result;
164
+ }
97
165
  }
98
- export const API = new CliApi();
166
+ export const api = new ApiImpl();
@@ -1,17 +1,24 @@
1
- import { complete, errored } from "@attio/fetchable";
1
+ import { complete, errored, isErrored } from "@attio/fetchable";
2
2
  import { API } from "../env.js";
3
- import { ensureAuthed } from "../auth/ensure-authed.js";
4
- import chalk from "chalk";
3
+ import { authenticator } from "../auth/auth.js";
5
4
  export class Fetcher {
6
- async _fetch(path, fetchOptions, schema) {
7
- const token = await ensureAuthed();
5
+ async _fetch({ path, fetchOptions, schema, authenticated, }) {
6
+ const tokenResult = authenticated === "Authenticated" ? await authenticator.ensureAuthed() : complete(null);
7
+ if (isErrored(tokenResult)) {
8
+ return errored({
9
+ code: "UNAUTHORIZED",
10
+ error: tokenResult.error,
11
+ });
12
+ }
13
+ const token = tokenResult.value;
8
14
  try {
9
15
  const response = await fetch(path.startsWith("https") ? path : `${API}${path}`, {
10
16
  ...fetchOptions,
11
17
  headers: {
12
18
  "x-attio-platform": "developer-cli",
13
- "Authorization": `Bearer ${token}`,
19
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
14
20
  "Content-Type": "application/json",
21
+ ...fetchOptions.headers,
15
22
  },
16
23
  });
17
24
  if (!response.ok) {
@@ -39,27 +46,24 @@ export class Fetcher {
39
46
  });
40
47
  }
41
48
  }
42
- async get(path, schema) {
43
- return this._fetch(path, { method: "GET" }, schema);
44
- }
45
- async post(path, body, schema) {
46
- return this._fetch(path, {
47
- method: "POST",
48
- body: JSON.stringify(body),
49
- }, schema);
49
+ async get({ path, schema, headers = {}, authenticated = "Authenticated", }) {
50
+ return this._fetch({ path, fetchOptions: { method: "GET", headers }, schema, authenticated });
50
51
  }
51
- }
52
- export function printFetcherError(message, error) {
53
- process.stderr.write(`${chalk.red("✖ ")}${message}\n`);
54
- switch (error.code) {
55
- case "HTTP_ERROR":
56
- process.stderr.write(`HTTP Error (${error.status}): ${error.message}\n`);
57
- break;
58
- case "INVALID_RESPONSE":
59
- process.stderr.write(`Invalid response: ${error.message}\n`);
60
- break;
61
- case "NETWORK_ERROR":
62
- process.stderr.write(`Network error: ${error.message}\n`);
63
- break;
52
+ async post({ path, body, schema, headers = {}, authenticated = "Authenticated", }) {
53
+ return this._fetch({
54
+ path,
55
+ fetchOptions: {
56
+ method: "POST",
57
+ body: body instanceof URLSearchParams ? body.toString() : JSON.stringify(body),
58
+ headers: {
59
+ "Content-Type": body instanceof URLSearchParams
60
+ ? "application/x-www-form-urlencoded"
61
+ : "application/json",
62
+ ...headers,
63
+ },
64
+ },
65
+ schema,
66
+ authenticated,
67
+ });
64
68
  }
65
69
  }
@@ -74,3 +74,9 @@ export const versionSchema = z.object({
74
74
  export const versionsSchema = z.object({
75
75
  app_prod_versions: z.array(versionSchema),
76
76
  });
77
+ export const tokenResponseSchema = z.object({
78
+ access_token: z.string(),
79
+ token_type: z.literal("Bearer"),
80
+ refresh_token: z.string(),
81
+ expires_in: z.number(),
82
+ });
package/lib/auth/auth.js CHANGED
@@ -4,118 +4,156 @@ import { randomBytes, createHash } from "crypto";
4
4
  import open from "open";
5
5
  import { APP } from "../env.js";
6
6
  import { findAvailablePort } from "../util/find-available-port.js";
7
- import { loadAuthToken, saveAuthToken, deleteAuthToken } from "./keychain.js";
8
- import { z } from "zod";
9
- import { hardExit } from "../util/hard-exit.js";
10
- import { API } from "../api/api.js";
11
- import { isComplete } from "@attio/fetchable";
12
- const CLIENT_ID = "f881c6f1-82d7-48a5-a581-649596167845";
13
- const tokenResponseSchema = z.object({
14
- access_token: z.string(),
15
- token_type: z.string(),
16
- expires_in: z.number(),
17
- });
18
- async function exchangeCodeForToken(code, codeVerifier, redirectUri) {
19
- const tokenUrl = `${APP}/oidc/token`;
20
- const params = new URLSearchParams();
21
- params.append("grant_type", "authorization_code");
22
- params.append("code", code);
23
- params.append("client_id", CLIENT_ID);
24
- params.append("redirect_uri", redirectUri);
25
- params.append("code_verifier", codeVerifier);
26
- try {
27
- const response = await fetch(tokenUrl, {
28
- method: "POST",
29
- headers: {
30
- "Content-Type": "application/x-www-form-urlencoded",
31
- },
32
- body: params.toString(),
33
- });
34
- if (!response.ok) {
35
- const errorText = await response.text();
36
- return hardExit(`Failed to exchange code for token: ${errorText} (status: ${response.status})`);
7
+ import { keychain } from "./keychain.js";
8
+ import { api } from "../api/api.js";
9
+ import { isErrored, complete, errored } from "@attio/fetchable";
10
+ import { printFetcherError, printKeychainError } from "../print-errors.js";
11
+ class AuthenticatorImpl {
12
+ clientId = "f881c6f1-82d7-48a5-a581-649596167845";
13
+ refreshTimeout = null;
14
+ async ensureAuthed() {
15
+ const existingTokenResult = await keychain.load();
16
+ if (isErrored(existingTokenResult)) {
17
+ return this.promptToAuthenticate();
37
18
  }
38
- const data = await response.json();
39
- const result = tokenResponseSchema.safeParse(data);
40
- if (!result.success) {
41
- return hardExit("Invalid token response");
19
+ const existingToken = existingTokenResult.value;
20
+ if (existingToken === null || existingToken.expires_at < Date.now()) {
21
+ return this.promptToAuthenticate();
42
22
  }
43
- return result.data;
44
- }
45
- catch (error) {
46
- return hardExit(`Failed to exchange code for token: ${error instanceof Error ? error.message : String(error)}`);
23
+ this.scheduleRefresh(existingToken);
24
+ return complete(existingToken.access_token);
47
25
  }
48
- }
49
- export async function auth() {
50
- const existingTokenResult = await loadAuthToken();
51
- if (existingTokenResult !== null) {
52
- const userResult = await API.whoami();
53
- const user = isComplete(userResult) ? userResult.value : null;
54
- if (user !== null) {
55
- return existingTokenResult;
26
+ async promptToAuthenticate() {
27
+ if (process.env.NODE_ENV !== "test") {
28
+ process.stdout.write("You need to log in with Attio. Press Enter to continue...\n\n");
29
+ await new Promise((resolve) => process.stdin.once("data", resolve));
56
30
  }
57
- await deleteAuthToken();
31
+ return this.authenticate();
58
32
  }
59
- const verifier = randomBytes(32);
60
- const verifierString = verifier.toString("base64url");
61
- const hash = createHash("sha256");
62
- hash.update(verifier);
63
- const challenge = hash.digest();
64
- const challengeString = challenge.toString("base64url");
65
- const state = randomBytes(32).toString("base64url");
66
- const port = await findAvailablePort(3000, 65000);
67
- const redirectUri = `http://localhost:${port}`;
68
- let resolveToken;
69
- const tokenPromise = new Promise((resolve) => {
70
- resolveToken = resolve;
71
- });
72
- const app = new Hono();
73
- let serverRef;
74
- app.get("/", async (c) => {
75
- const query = c.req.query();
76
- const receivedCode = query.authorization_code;
77
- const receivedState = query.state;
78
- if (receivedState !== state) {
79
- hardExit("State mismatch - possible CSRF attack");
33
+ async authenticate() {
34
+ const existingTokenResult = await keychain.load();
35
+ if (isErrored(existingTokenResult)) {
36
+ return existingTokenResult;
80
37
  }
81
- if (!receivedCode) {
82
- hardExit("No authorization code received");
38
+ const existingToken = existingTokenResult.value;
39
+ if (existingToken !== null && existingToken.expires_at > Date.now()) {
40
+ return complete(existingToken.access_token);
83
41
  }
84
- const tokenResult = await exchangeCodeForToken(receivedCode, verifierString, redirectUri);
85
- setTimeout(() => {
86
- serverRef.close();
87
- }, 1000);
88
- resolveToken(tokenResult.access_token);
89
- return c.html(`
42
+ const verifier = randomBytes(32);
43
+ const verifierString = verifier.toString("base64url");
44
+ const hash = createHash("sha256");
45
+ hash.update(verifier);
46
+ const challenge = hash.digest();
47
+ const challengeString = challenge.toString("base64url");
48
+ const state = randomBytes(32).toString("base64url");
49
+ const port = await findAvailablePort(3000, 65000);
50
+ const redirectUri = `http://localhost:${port}`;
51
+ let resolveAsyncResult;
52
+ const asyncResult = new Promise((resolve) => {
53
+ resolveAsyncResult = resolve;
54
+ });
55
+ const app = new Hono();
56
+ let serverRef;
57
+ app.get("/", async (c) => {
58
+ const query = c.req.query();
59
+ const receivedCode = query.authorization_code;
60
+ const receivedState = query.state;
61
+ if (receivedState !== state) {
62
+ resolveAsyncResult(errored({ code: "OAUTH_STATE_MISMATCH" }));
63
+ }
64
+ if (!receivedCode) {
65
+ resolveAsyncResult(errored({ code: "NO_AUTHORIZATION_CODE" }));
66
+ }
67
+ const tokenResult = await api.exchangeToken({
68
+ receivedCode,
69
+ verifierString,
70
+ redirectUri,
71
+ clientId: this.clientId,
72
+ });
73
+ setTimeout(() => {
74
+ serverRef.close();
75
+ resolveAsyncResult(tokenResult);
76
+ }, 1_000);
77
+ return c.html(`
90
78
  <html>
91
79
  <body>
92
80
  <script>window.location.href = '${APP}/authorized';</script>
93
81
  </body>
94
82
  </html>
95
83
  `);
96
- });
97
- serverRef = serve({
98
- fetch: app.fetch,
99
- port,
100
- });
101
- try {
102
- const authUrl = new URL(`${APP}/oidc/authorize`);
103
- authUrl.searchParams.append("scope", "openid");
104
- authUrl.searchParams.append("response_type", "code");
105
- authUrl.searchParams.append("client_id", CLIENT_ID);
106
- authUrl.searchParams.append("redirect_uri", redirectUri);
107
- authUrl.searchParams.append("state", state);
108
- authUrl.searchParams.append("code_challenge", challengeString);
109
- authUrl.searchParams.append("code_challenge_method", "S256");
110
- await open(authUrl.toString());
111
- const token = await tokenPromise;
112
- if (token) {
84
+ });
85
+ serverRef = serve({
86
+ fetch: app.fetch,
87
+ port,
88
+ });
89
+ try {
90
+ const authUrl = new URL(`${APP}/oidc/authorize`);
91
+ authUrl.searchParams.append("scope", "openid offline_access");
92
+ authUrl.searchParams.append("response_type", "code");
93
+ authUrl.searchParams.append("client_id", this.clientId);
94
+ authUrl.searchParams.append("redirect_uri", redirectUri);
95
+ authUrl.searchParams.append("state", state);
96
+ authUrl.searchParams.append("code_challenge", challengeString);
97
+ authUrl.searchParams.append("code_challenge_method", "S256");
98
+ await open(authUrl.toString());
99
+ const tokenResult = await asyncResult;
100
+ if (isErrored(tokenResult)) {
101
+ return tokenResult;
102
+ }
103
+ const token = tokenResult.value;
113
104
  process.stdout.write("🔑 Saving new token to keychain\n");
114
- await saveAuthToken(token);
105
+ const keychainToken = {
106
+ access_token: token.access_token,
107
+ refresh_token: token.refresh_token,
108
+ token_type: token.token_type,
109
+ expires_at: Date.now() + token.expires_in * 1000,
110
+ };
111
+ const saveResult = await keychain.save(keychainToken);
112
+ if (isErrored(saveResult)) {
113
+ return saveResult;
114
+ }
115
+ this.scheduleRefresh(keychainToken);
116
+ return complete(token.access_token);
117
+ }
118
+ finally {
119
+ serverRef.close();
115
120
  }
116
- return token;
117
121
  }
118
- finally {
119
- serverRef.close();
122
+ scheduleRefresh(token) {
123
+ if (this.refreshTimeout !== null) {
124
+ clearTimeout(this.refreshTimeout);
125
+ }
126
+ this.refreshTimeout = setTimeout(async () => await this.refreshToken(token), Math.max(0, token.expires_at - Date.now() - 5_000));
127
+ }
128
+ async refreshToken(token) {
129
+ const refreshTokenResult = await api.refreshToken({
130
+ refreshToken: token.refresh_token,
131
+ clientId: this.clientId,
132
+ });
133
+ if (isErrored(refreshTokenResult)) {
134
+ printFetcherError("Error refreshing token", refreshTokenResult.error);
135
+ return;
136
+ }
137
+ const refreshedToken = refreshTokenResult.value;
138
+ const keychainToken = {
139
+ access_token: refreshedToken.access_token,
140
+ refresh_token: refreshedToken.refresh_token,
141
+ token_type: refreshedToken.token_type,
142
+ expires_at: Date.now() + refreshedToken.expires_in * 1000,
143
+ };
144
+ const saveResult = await keychain.save(keychainToken);
145
+ if (isErrored(saveResult)) {
146
+ printKeychainError(saveResult.error);
147
+ return;
148
+ }
149
+ this.scheduleRefresh(keychainToken);
150
+ }
151
+ async logout() {
152
+ if (this.refreshTimeout !== null) {
153
+ clearTimeout(this.refreshTimeout);
154
+ this.refreshTimeout = null;
155
+ }
156
+ await keychain.delete();
120
157
  }
121
158
  }
159
+ export const authenticator = new AuthenticatorImpl();