attio 0.0.1-experimental.20250409 → 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 +2 -2
- package/lib/api/fetcher.js +2 -2
- package/lib/auth/auth.js +21 -14
- package/lib/auth/keychain.js +16 -12
- package/lib/commands/dev/boot.js +2 -2
- package/lib/commands/dev/onboarding.js +3 -3
- package/lib/commands/dev/upload.js +3 -3
- package/lib/commands/login.js +2 -2
- package/lib/commands/logout.js +2 -2
- package/lib/commands/version/create.js +3 -3
- package/lib/commands/whoami.js +4 -4
- package/lib/spinners/determine-workspace.spinner.js +2 -2
- package/lib/spinners/get-app-info.spinner.js +2 -2
- package/lib/spinners/get-versions.spinner.js +2 -2
- package/package.json +1 -1
package/lib/api/api.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Fetcher } from "./fetcher.js";
|
|
|
2
2
|
import { APP } from "../env.js";
|
|
3
3
|
import { isErrored, complete, errored } from "@attio/fetchable";
|
|
4
4
|
import { whoamiSchema, listDevWorkspacesResponseSchema, createVersionSchema, appInfoSchema, completeBundleUploadSchema, startUploadSchema, createDevVersionSchema, installationSchema, versionsSchema, TEST_WORKSPACES, TEST_APP_INFO, tokenResponseSchema, } from "./schemas.js";
|
|
5
|
-
class
|
|
5
|
+
class ApiImpl {
|
|
6
6
|
_fetcher;
|
|
7
7
|
constructor() {
|
|
8
8
|
this._fetcher = new Fetcher();
|
|
@@ -163,4 +163,4 @@ class CliApi {
|
|
|
163
163
|
return result;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
export const
|
|
166
|
+
export const api = new ApiImpl();
|
package/lib/api/fetcher.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { complete, errored, isErrored } from "@attio/fetchable";
|
|
2
2
|
import { API } from "../env.js";
|
|
3
|
-
import {
|
|
3
|
+
import { authenticator } from "../auth/auth.js";
|
|
4
4
|
export class Fetcher {
|
|
5
5
|
async _fetch({ path, fetchOptions, schema, authenticated, }) {
|
|
6
|
-
const tokenResult = authenticated === "Authenticated" ? await
|
|
6
|
+
const tokenResult = authenticated === "Authenticated" ? await authenticator.ensureAuthed() : complete(null);
|
|
7
7
|
if (isErrored(tokenResult)) {
|
|
8
8
|
return errored({
|
|
9
9
|
code: "UNAUTHORIZED",
|
package/lib/auth/auth.js
CHANGED
|
@@ -4,20 +4,20 @@ 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 {
|
|
7
|
+
import { keychain } from "./keychain.js";
|
|
8
|
+
import { api } from "../api/api.js";
|
|
9
9
|
import { isErrored, complete, errored } from "@attio/fetchable";
|
|
10
10
|
import { printFetcherError, printKeychainError } from "../print-errors.js";
|
|
11
11
|
class AuthenticatorImpl {
|
|
12
12
|
clientId = "f881c6f1-82d7-48a5-a581-649596167845";
|
|
13
13
|
refreshTimeout = null;
|
|
14
14
|
async ensureAuthed() {
|
|
15
|
-
const existingTokenResult = await
|
|
15
|
+
const existingTokenResult = await keychain.load();
|
|
16
16
|
if (isErrored(existingTokenResult)) {
|
|
17
17
|
return this.promptToAuthenticate();
|
|
18
18
|
}
|
|
19
19
|
const existingToken = existingTokenResult.value;
|
|
20
|
-
if (existingToken === null || existingToken.
|
|
20
|
+
if (existingToken === null || existingToken.expires_at < Date.now()) {
|
|
21
21
|
return this.promptToAuthenticate();
|
|
22
22
|
}
|
|
23
23
|
this.scheduleRefresh(existingToken);
|
|
@@ -31,12 +31,12 @@ class AuthenticatorImpl {
|
|
|
31
31
|
return this.authenticate();
|
|
32
32
|
}
|
|
33
33
|
async authenticate() {
|
|
34
|
-
const existingTokenResult = await
|
|
34
|
+
const existingTokenResult = await keychain.load();
|
|
35
35
|
if (isErrored(existingTokenResult)) {
|
|
36
36
|
return existingTokenResult;
|
|
37
37
|
}
|
|
38
38
|
const existingToken = existingTokenResult.value;
|
|
39
|
-
if (existingToken !== null && existingToken.
|
|
39
|
+
if (existingToken !== null && existingToken.expires_at > Date.now()) {
|
|
40
40
|
return complete(existingToken.access_token);
|
|
41
41
|
}
|
|
42
42
|
const verifier = randomBytes(32);
|
|
@@ -64,7 +64,7 @@ class AuthenticatorImpl {
|
|
|
64
64
|
if (!receivedCode) {
|
|
65
65
|
resolveAsyncResult(errored({ code: "NO_AUTHORIZATION_CODE" }));
|
|
66
66
|
}
|
|
67
|
-
const tokenResult = await
|
|
67
|
+
const tokenResult = await api.exchangeToken({
|
|
68
68
|
receivedCode,
|
|
69
69
|
verifierString,
|
|
70
70
|
redirectUri,
|
|
@@ -106,9 +106,9 @@ class AuthenticatorImpl {
|
|
|
106
106
|
access_token: token.access_token,
|
|
107
107
|
refresh_token: token.refresh_token,
|
|
108
108
|
token_type: token.token_type,
|
|
109
|
-
|
|
109
|
+
expires_at: Date.now() + token.expires_in * 1000,
|
|
110
110
|
};
|
|
111
|
-
const saveResult = await
|
|
111
|
+
const saveResult = await keychain.save(keychainToken);
|
|
112
112
|
if (isErrored(saveResult)) {
|
|
113
113
|
return saveResult;
|
|
114
114
|
}
|
|
@@ -123,10 +123,10 @@ class AuthenticatorImpl {
|
|
|
123
123
|
if (this.refreshTimeout !== null) {
|
|
124
124
|
clearTimeout(this.refreshTimeout);
|
|
125
125
|
}
|
|
126
|
-
this.refreshTimeout = setTimeout(async () => await this.refreshToken(token), Math.max(0, token.
|
|
126
|
+
this.refreshTimeout = setTimeout(async () => await this.refreshToken(token), Math.max(0, token.expires_at - Date.now() - 5_000));
|
|
127
127
|
}
|
|
128
128
|
async refreshToken(token) {
|
|
129
|
-
const refreshTokenResult = await
|
|
129
|
+
const refreshTokenResult = await api.refreshToken({
|
|
130
130
|
refreshToken: token.refresh_token,
|
|
131
131
|
clientId: this.clientId,
|
|
132
132
|
});
|
|
@@ -139,14 +139,21 @@ class AuthenticatorImpl {
|
|
|
139
139
|
access_token: refreshedToken.access_token,
|
|
140
140
|
refresh_token: refreshedToken.refresh_token,
|
|
141
141
|
token_type: refreshedToken.token_type,
|
|
142
|
-
|
|
142
|
+
expires_at: Date.now() + refreshedToken.expires_in * 1000,
|
|
143
143
|
};
|
|
144
|
-
const saveResult = await
|
|
144
|
+
const saveResult = await keychain.save(keychainToken);
|
|
145
145
|
if (isErrored(saveResult)) {
|
|
146
146
|
printKeychainError(saveResult.error);
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
this.scheduleRefresh(keychainToken);
|
|
150
150
|
}
|
|
151
|
+
async logout() {
|
|
152
|
+
if (this.refreshTimeout !== null) {
|
|
153
|
+
clearTimeout(this.refreshTimeout);
|
|
154
|
+
this.refreshTimeout = null;
|
|
155
|
+
}
|
|
156
|
+
await keychain.delete();
|
|
157
|
+
}
|
|
151
158
|
}
|
|
152
|
-
export const
|
|
159
|
+
export const authenticator = new AuthenticatorImpl();
|
package/lib/auth/keychain.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { complete, errored } from "@attio/fetchable";
|
|
1
|
+
import { complete, errored, isErrored } from "@attio/fetchable";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
const authTokenSchema = z.object({
|
|
4
4
|
access_token: z.string(),
|
|
5
5
|
refresh_token: z.string(),
|
|
6
6
|
token_type: z.literal("Bearer"),
|
|
7
|
-
|
|
7
|
+
expires_at: z.number(),
|
|
8
8
|
});
|
|
9
9
|
class KeytarKeychain {
|
|
10
10
|
SERVICE_NAME = "attio-cli";
|
|
@@ -43,17 +43,21 @@ class KeytarKeychain {
|
|
|
43
43
|
jsonToken = JSON.parse(unparsedToken);
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
error
|
|
49
|
-
}
|
|
46
|
+
const deleteResult = await this.delete();
|
|
47
|
+
if (isErrored(deleteResult)) {
|
|
48
|
+
return errored({ code: "LOAD_KEYCHAIN_ERROR", error: deleteResult.error });
|
|
49
|
+
}
|
|
50
|
+
console.debug("Wiped keychain entry due to invalid JSON");
|
|
51
|
+
return complete(null);
|
|
50
52
|
}
|
|
51
53
|
const parsedToken = authTokenSchema.safeParse(jsonToken);
|
|
52
54
|
if (!parsedToken.success) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
error:
|
|
56
|
-
}
|
|
55
|
+
const deleteResult = await this.delete();
|
|
56
|
+
if (isErrored(deleteResult)) {
|
|
57
|
+
return errored({ code: "LOAD_KEYCHAIN_ERROR", error: deleteResult.error });
|
|
58
|
+
}
|
|
59
|
+
console.debug("Wiped keychain entry due to schema mismatch");
|
|
60
|
+
return complete(null);
|
|
57
61
|
}
|
|
58
62
|
return complete(parsedToken.data);
|
|
59
63
|
}
|
|
@@ -76,7 +80,7 @@ class TestKeychain {
|
|
|
76
80
|
access_token: "TEST",
|
|
77
81
|
refresh_token: "TEST",
|
|
78
82
|
token_type: "Bearer",
|
|
79
|
-
|
|
83
|
+
expires_at: Number.MAX_SAFE_INTEGER,
|
|
80
84
|
};
|
|
81
85
|
async save(_token) {
|
|
82
86
|
return complete(undefined);
|
|
@@ -88,4 +92,4 @@ class TestKeychain {
|
|
|
88
92
|
return complete(undefined);
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
|
-
export const
|
|
95
|
+
export const keychain = process.env.NODE_ENV === "test" ? new TestKeychain() : new KeytarKeychain();
|
package/lib/commands/dev/boot.js
CHANGED
|
@@ -3,7 +3,7 @@ import { printFetcherError, printCliVersionError, printPackageJsonError, printDe
|
|
|
3
3
|
import { getAppSlugFromPackageJson } from "../../spinners/get-app-slug-from-package-json.js";
|
|
4
4
|
import { determineWorkspace } from "../../spinners/determine-workspace.spinner.js";
|
|
5
5
|
import { loadAttioCliVersion } from "../../util/load-attio-cli-version.js";
|
|
6
|
-
import {
|
|
6
|
+
import { api } from "../../api/api.js";
|
|
7
7
|
import { getAppInfo } from "../../spinners/get-app-info.spinner.js";
|
|
8
8
|
import { loadEnv } from "../../util/load-env.js";
|
|
9
9
|
export async function boot({ workspaceSlug }) {
|
|
@@ -32,7 +32,7 @@ export async function boot({ workspaceSlug }) {
|
|
|
32
32
|
process.exit(1);
|
|
33
33
|
}
|
|
34
34
|
const cliVersion = cliVersionResult.value;
|
|
35
|
-
const devVersionResult = await
|
|
35
|
+
const devVersionResult = await api.createDevVersion({
|
|
36
36
|
appId: appInfo.app_id,
|
|
37
37
|
cliVersion,
|
|
38
38
|
targetWorkspaceId: workspace.workspace_id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { listenForKey } from "../../util/listen-for-key.js";
|
|
2
2
|
import open from "open";
|
|
3
3
|
import { APP } from "../../env.js";
|
|
4
|
-
import {
|
|
4
|
+
import { api } from "../../api/api.js";
|
|
5
5
|
import { isErrored } from "@attio/fetchable";
|
|
6
6
|
function prompt() {
|
|
7
7
|
process.stdout.write(`🚨 IMPORTANT: You will need to install your app in your workspace. Press "i" to open the app settings page, and then click "Install".\n\n`);
|
|
@@ -11,7 +11,7 @@ export function onboarding({ appId, appSlug, workspace, }) {
|
|
|
11
11
|
open(`${APP}/${workspace.slug}/settings/apps/${appSlug}`);
|
|
12
12
|
});
|
|
13
13
|
const poll = async () => {
|
|
14
|
-
const installationResult = await
|
|
14
|
+
const installationResult = await api.fetchInstallation({
|
|
15
15
|
appId,
|
|
16
16
|
workspaceId: workspace.workspace_id,
|
|
17
17
|
});
|
|
@@ -23,7 +23,7 @@ export function onboarding({ appId, appSlug, workspace, }) {
|
|
|
23
23
|
prompt();
|
|
24
24
|
while (!installation) {
|
|
25
25
|
await new Promise((resolve) => setTimeout(resolve, 60_000));
|
|
26
|
-
const installationResult = await
|
|
26
|
+
const installationResult = await api.fetchInstallation({
|
|
27
27
|
appId,
|
|
28
28
|
workspaceId: workspace.workspace_id,
|
|
29
29
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import notifier from "node-notifier";
|
|
2
2
|
import { spinnerify } from "../../util/spinner.js";
|
|
3
3
|
import { uploadBundle } from "../../util/upload-bundle.js";
|
|
4
|
-
import {
|
|
4
|
+
import { api } from "../../api/api.js";
|
|
5
5
|
import { isErrored, complete, combineAsync } from "@attio/fetchable";
|
|
6
6
|
export async function upload({ contents, devVersionId, appId, }) {
|
|
7
7
|
return await spinnerify("Uploading...", () => `Upload complete at ${new Date().toLocaleTimeString()}`, async () => {
|
|
8
|
-
const startUploadResult = await
|
|
8
|
+
const startUploadResult = await api.startUpload({
|
|
9
9
|
appId,
|
|
10
10
|
devVersionId,
|
|
11
11
|
});
|
|
@@ -21,7 +21,7 @@ export async function upload({ contents, devVersionId, appId, }) {
|
|
|
21
21
|
if (isErrored(uploadResults)) {
|
|
22
22
|
return uploadResults;
|
|
23
23
|
}
|
|
24
|
-
const completeBundleUploadResult = await
|
|
24
|
+
const completeBundleUploadResult = await api.completeBundleUpload({
|
|
25
25
|
appId,
|
|
26
26
|
devVersionId,
|
|
27
27
|
bundleId,
|
package/lib/commands/login.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import { authenticator } from "../auth/auth.js";
|
|
3
3
|
import { isErrored } from "@attio/fetchable";
|
|
4
4
|
import { printAuthenticationError } from "../print-errors.js";
|
|
5
5
|
export const login = new Command("login")
|
|
6
6
|
.description("Authenticate with Attio")
|
|
7
7
|
.action(async () => {
|
|
8
|
-
const result = await
|
|
8
|
+
const result = await authenticator.authenticate();
|
|
9
9
|
if (isErrored(result)) {
|
|
10
10
|
printAuthenticationError(result.error);
|
|
11
11
|
process.exit(1);
|
package/lib/commands/logout.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import { keychain } from "../auth/keychain.js";
|
|
3
3
|
import { printKeychainError } from "../print-errors.js";
|
|
4
4
|
import { isErrored } from "@attio/fetchable";
|
|
5
5
|
export const logout = new Command("logout").description("Log out from Attio").action(async () => {
|
|
6
|
-
const result = await
|
|
6
|
+
const result = await keychain.delete();
|
|
7
7
|
if (isErrored(result)) {
|
|
8
8
|
printKeychainError(result.error);
|
|
9
9
|
process.exit(1);
|
|
@@ -10,7 +10,7 @@ import { combineAsync, isErrored } from "@attio/fetchable";
|
|
|
10
10
|
import { getAppSlugFromPackageJson } from "../../spinners/get-app-slug-from-package-json.js";
|
|
11
11
|
import { bundleJavaScript } from "./create/bundle-javascript.js";
|
|
12
12
|
import { printBuildContextError } from "../dev/prepare-build-contexts.js";
|
|
13
|
-
import {
|
|
13
|
+
import { api } from "../../api/api.js";
|
|
14
14
|
import { printJsError } from "../../util/typescript.js";
|
|
15
15
|
export const versionCreate = new Command("create")
|
|
16
16
|
.description("Create a new unpublished version of your Attio app")
|
|
@@ -59,7 +59,7 @@ export const versionCreate = new Command("create")
|
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
const cliVersion = cliVersionResult.value;
|
|
62
|
-
const versionResult = await
|
|
62
|
+
const versionResult = await api.createVersion({
|
|
63
63
|
appId: appInfo.app_id,
|
|
64
64
|
major: versions.length === 0
|
|
65
65
|
? 1
|
|
@@ -86,7 +86,7 @@ export const versionCreate = new Command("create")
|
|
|
86
86
|
process.exit(1);
|
|
87
87
|
}
|
|
88
88
|
const version = versionResult.value;
|
|
89
|
-
const signingResult = await spinnerify("Signing bundles...", "Bundles signed", async () => await
|
|
89
|
+
const signingResult = await spinnerify("Signing bundles...", "Bundles signed", async () => await api.completeProdBundleUpload({
|
|
90
90
|
appId: appInfo.app_id,
|
|
91
91
|
major: version.major,
|
|
92
92
|
minor: version.minor,
|
package/lib/commands/whoami.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { keychain } from "../auth/keychain.js";
|
|
3
|
+
import { api } from "../api/api.js";
|
|
4
4
|
import { isErrored } from "@attio/fetchable";
|
|
5
5
|
import { printFetcherError } from "../print-errors.js";
|
|
6
6
|
export const whoami = new Command()
|
|
7
7
|
.name("whoami")
|
|
8
8
|
.description("Identify the current user")
|
|
9
9
|
.action(async () => {
|
|
10
|
-
const tokenResult = await
|
|
10
|
+
const tokenResult = await keychain.load();
|
|
11
11
|
if (isErrored(tokenResult) || tokenResult.value === null) {
|
|
12
12
|
process.stdout.write("🔒 Not logged in.\n");
|
|
13
13
|
process.exit(0);
|
|
14
14
|
}
|
|
15
|
-
const result = await
|
|
15
|
+
const result = await api.whoami();
|
|
16
16
|
if (isErrored(result)) {
|
|
17
17
|
printFetcherError("Error fetching user", result.error);
|
|
18
18
|
process.exit(1);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { select } from "@inquirer/prompts";
|
|
2
2
|
import { spinnerify } from "../util/spinner.js";
|
|
3
|
-
import {
|
|
3
|
+
import { api } from "../api/api.js";
|
|
4
4
|
import { isErrored, complete, errored } from "@attio/fetchable";
|
|
5
5
|
export async function determineWorkspace(workspaceSlug) {
|
|
6
|
-
const workspacesResult = await spinnerify("Loading workspaces...", "Workspaces loaded", async () => await
|
|
6
|
+
const workspacesResult = await spinnerify("Loading workspaces...", "Workspaces loaded", async () => await api.fetchWorkspaces());
|
|
7
7
|
if (isErrored(workspacesResult)) {
|
|
8
8
|
return workspacesResult;
|
|
9
9
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { api } from "../api/api.js";
|
|
2
2
|
import { spinnerify } from "../util/spinner.js";
|
|
3
3
|
export async function getAppInfo(appSlug) {
|
|
4
4
|
return await spinnerify("Loading app information...", (app) => `App found: ${app.title}`, async () => {
|
|
5
|
-
return await
|
|
5
|
+
return await api.fetchAppInfo(appSlug);
|
|
6
6
|
});
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spinnerify } from "../util/spinner.js";
|
|
2
|
-
import {
|
|
2
|
+
import { api } from "../api/api.js";
|
|
3
3
|
export async function getVersions(appInfo) {
|
|
4
|
-
return await spinnerify("Loading versions...", "Versions loaded", async () => await
|
|
4
|
+
return await spinnerify("Loading versions...", "Versions loaded", async () => await api.fetchVersions(appInfo.app_id));
|
|
5
5
|
}
|