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.
- package/lib/api/api.js +99 -31
- package/lib/api/fetcher.js +31 -27
- package/lib/api/schemas.js +6 -0
- package/lib/auth/auth.js +135 -97
- package/lib/auth/keychain.js +86 -43
- package/lib/build/server/generate-server-entry.js +6 -6
- package/lib/build.js +2 -2
- package/lib/commands/build/build-javascript.js +1 -1
- package/lib/commands/build/validate-typescript.js +1 -1
- package/lib/commands/build.js +2 -2
- package/lib/commands/dev/boot.js +9 -9
- package/lib/commands/dev/bundle-javascript.js +1 -1
- package/lib/commands/dev/onboarding.js +3 -3
- package/lib/commands/dev/prepare-build-contexts.js +1 -1
- package/lib/commands/dev/upload.js +4 -21
- package/lib/commands/dev.js +5 -4
- package/lib/commands/init/create-project.js +7 -39
- package/lib/commands/init.js +6 -14
- package/lib/commands/login.js +8 -2
- package/lib/commands/logout.js +8 -2
- package/lib/commands/version/create/bundle-javascript.js +3 -3
- package/lib/commands/version/create.js +14 -14
- package/lib/commands/version/list.js +4 -4
- package/lib/commands/whoami.js +7 -8
- package/lib/errors.js +1 -0
- package/lib/print-errors.js +165 -0
- package/lib/spinners/determine-workspace.spinner.js +3 -26
- package/lib/spinners/get-app-info.spinner.js +2 -2
- package/lib/spinners/get-app-slug-from-package-json.js +1 -20
- package/lib/spinners/get-versions.spinner.js +2 -2
- package/lib/util/copy-with-replace.js +2 -2
- package/lib/util/create-directory.js +1 -1
- package/lib/util/load-attio-cli-version.js +1 -26
- package/lib/util/upload-bundle.js +1 -1
- package/package.json +1 -1
- package/lib/auth/ensure-authed.js +0 -14
- package/lib/commands/dev/start-graphql-server.js +0 -32
- package/lib/commands/init/ask-language.js +0 -11
- package/lib/templates/javascript/.env +0 -12
- package/lib/templates/javascript/eslint.config.js +0 -48
- package/lib/templates/javascript/package.json +0 -26
- package/lib/templates/javascript/src/assets/icon.png +0 -0
- package/lib/templates/javascript/src/cat-fact.jsx +0 -16
- package/lib/templates/javascript/src/get-cat-fact.server.js +0 -6
- package/lib/templates/javascript/src/hello-world-action.jsx +0 -17
- package/lib/templates/javascript/src/hello-world-dialog.jsx +0 -26
- package/lib/templates/typescript/.eslintignore +0 -3
- package/lib/templates/typescript/src/assets/icon.png +0 -0
- /package/lib/{templates/typescript → template}/.env +0 -0
- /package/lib/{templates/javascript → template}/.eslintignore +0 -0
- /package/lib/{templates/common → template}/README.md +0 -0
- /package/lib/{templates/typescript → template}/eslint.config.js +0 -0
- /package/lib/{templates/typescript → template}/package.json +0 -0
- /package/lib/{templates/common → template}/src/assets/icon.png +0 -0
- /package/lib/{templates/typescript → template}/src/cat-fact.tsx +0 -0
- /package/lib/{templates/typescript → template}/src/get-cat-fact.server.ts +0 -0
- /package/lib/{templates/typescript → template}/src/hello-world-action.tsx +0 -0
- /package/lib/{templates/typescript → template}/src/hello-world-dialog.tsx +0 -0
- /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
|
|
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(
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
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",
|
|
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
|
|
166
|
+
export const api = new ApiImpl();
|
package/lib/api/fetcher.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
}
|
package/lib/api/schemas.js
CHANGED
|
@@ -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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
31
|
+
return this.authenticate();
|
|
58
32
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
82
|
-
|
|
38
|
+
const existingToken = existingTokenResult.value;
|
|
39
|
+
if (existingToken !== null && existingToken.expires_at > Date.now()) {
|
|
40
|
+
return complete(existingToken.access_token);
|
|
83
41
|
}
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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();
|