@asteroidai/cli 1.9.27 → 1.9.30
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/dist/asteroid.js +2 -2
- package/dist/core.js +1 -1
- package/package.json +3 -8
- package/src/account.ts +0 -501
- package/src/args.ts +0 -39
- package/src/auth/credentials.ts +0 -179
- package/src/auth/store.ts +0 -253
- package/src/auth/workos.ts +0 -171
- package/src/commands.ts +0 -13
- package/src/config.ts +0 -125
- package/src/core.ts +0 -41
- package/src/generated/commands.ts +0 -4772
- package/src/http.ts +0 -115
- package/src/main.ts +0 -49
- package/src/output.test.ts +0 -71
- package/src/output.ts +0 -176
- package/src/program.test.ts +0 -155
- package/src/program.ts +0 -293
- package/src/reference.ts +0 -195
- package/src/request.test.ts +0 -164
- package/src/request.ts +0 -252
- package/src/run.test.ts +0 -651
- package/src/run.ts +0 -235
- package/src/sync/activities.ts +0 -80
- package/src/sync/commands.ts +0 -63
- package/src/sync/execution-pull.ts +0 -273
- package/src/sync/node-files.ts +0 -47
- package/src/sync/paths.ts +0 -45
- package/src/sync/run.ts +0 -136
- package/src/sync/sync.test.ts +0 -502
- package/src/sync/types.ts +0 -58
- package/src/sync/workflow-pull.ts +0 -185
- package/src/sync/workflow-push.ts +0 -161
- package/src/types.ts +0 -46
- package/src/ui.ts +0 -35
package/src/auth/credentials.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Config,
|
|
3
|
-
type Env,
|
|
4
|
-
findOrg,
|
|
5
|
-
mergeOrgs,
|
|
6
|
-
type Org,
|
|
7
|
-
orgLabel,
|
|
8
|
-
saveConfig,
|
|
9
|
-
} from "../config";
|
|
10
|
-
import { clean } from "../output";
|
|
11
|
-
import { CredentialStore, type Login, type StaffToken } from "./store";
|
|
12
|
-
import { refresh, tokenClaims, WorkosError } from "./workos";
|
|
13
|
-
|
|
14
|
-
export class AuthError extends Error {}
|
|
15
|
-
|
|
16
|
-
/** The sign-in is fine, but the person is not a member of the org. */
|
|
17
|
-
export class NotMemberError extends AuthError {}
|
|
18
|
-
|
|
19
|
-
export const API_KEY_OVERRIDES =
|
|
20
|
-
"ASTEROID_API_KEY is set, so the CLI uses that key and ignores `asteroid login`.";
|
|
21
|
-
export const UNSET_API_KEY =
|
|
22
|
-
"Run `unset ASTEROID_API_KEY` to use your login instead.";
|
|
23
|
-
|
|
24
|
-
export interface Auth {
|
|
25
|
-
kind: "api-key" | "bearer";
|
|
26
|
-
headers: Record<string, string>;
|
|
27
|
-
org?: Org;
|
|
28
|
-
expiresAt?: number;
|
|
29
|
-
/** Set when the token is a staff token from `asteroid use <org> --reason`. */
|
|
30
|
-
staff?: { grantId: string; reason: string };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** An Asteroid sign-in. It only decides which hints to show: the server checks the admin list. */
|
|
34
|
-
export function isStaffLogin(login: Login | undefined): boolean {
|
|
35
|
-
return login?.email?.toLowerCase().endsWith("@asteroid.ai") ?? false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** The `--reason` hint, for staff only. Empty for everyone else. */
|
|
39
|
-
export function staffHint(ref: string, staff: boolean): string {
|
|
40
|
-
return staff
|
|
41
|
-
? ` Asteroid staff: run \`asteroid use ${clean(ref)} --reason <why>\`.`
|
|
42
|
-
: "";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function staffAuth(held: StaffToken): Auth {
|
|
46
|
-
return {
|
|
47
|
-
kind: "bearer",
|
|
48
|
-
headers: { Authorization: `Bearer ${held.token}` },
|
|
49
|
-
org: held.org,
|
|
50
|
-
expiresAt: held.expiresAt,
|
|
51
|
-
staff: { grantId: held.grantId, reason: held.reason },
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function orgFromToken(token: string): Org | undefined {
|
|
56
|
-
const claims = tokenClaims(token);
|
|
57
|
-
if (!claims.org_id) return undefined;
|
|
58
|
-
return {
|
|
59
|
-
workosId: claims.org_id,
|
|
60
|
-
id: claims.org?.id,
|
|
61
|
-
name: claims.org?.name,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function rememberOrg(env: Env, config: Config, org: Org | undefined) {
|
|
66
|
-
if (!org) return;
|
|
67
|
-
config.orgs = mergeOrgs(config.orgs, [org]);
|
|
68
|
-
if (!config.org || config.org.workosId === org.workosId) {
|
|
69
|
-
config.org = config.orgs.find((o) => o.workosId === org.workosId);
|
|
70
|
-
}
|
|
71
|
-
saveConfig(env, config);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function bearer(token: string, expiresAt: number): Auth {
|
|
75
|
-
return {
|
|
76
|
-
kind: "bearer",
|
|
77
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
78
|
-
org: orgFromToken(token),
|
|
79
|
-
expiresAt,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const SSO_OR_MFA =
|
|
84
|
-
/^(sso_required|mfa_.*|organization_authentication_methods_required)$/;
|
|
85
|
-
|
|
86
|
-
function refreshError(error: WorkosError, org: Org, login: Login): AuthError {
|
|
87
|
-
if (error.code === "invalid_grant") {
|
|
88
|
-
return new AuthError("your sign-in has expired. Run `asteroid login`.");
|
|
89
|
-
}
|
|
90
|
-
if (SSO_OR_MFA.test(error.code)) {
|
|
91
|
-
return new AuthError(
|
|
92
|
-
`${orgLabel(org)} requires SSO or MFA. Run \`asteroid login\` and sign in to it again.`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
if (error.code === "organization_membership_not_found") {
|
|
96
|
-
return new NotMemberError(
|
|
97
|
-
`you are not a member of ${orgLabel(org)}.${staffHint(org.id ?? org.workosId, isStaffLogin(login))}`,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
return new AuthError(
|
|
101
|
-
`signing in to ${orgLabel(org)} failed: ${error.message}`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function resolveAuth(
|
|
106
|
-
env: Env,
|
|
107
|
-
config: Config,
|
|
108
|
-
orgRef: string | undefined,
|
|
109
|
-
fetchImpl: typeof fetch = fetch,
|
|
110
|
-
): Promise<Auth> {
|
|
111
|
-
if (env.apiKey)
|
|
112
|
-
return { kind: "api-key", headers: { "X-Api-Key": env.apiKey } };
|
|
113
|
-
|
|
114
|
-
const store = new CredentialStore(env);
|
|
115
|
-
const login = await store.readLogin();
|
|
116
|
-
if (!login) {
|
|
117
|
-
throw new AuthError(
|
|
118
|
-
"not signed in. Run `asteroid login`, or set ASTEROID_API_KEY.",
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const target = orgRef ?? config.org?.workosId;
|
|
123
|
-
const held = target ? await store.staffToken(target) : undefined;
|
|
124
|
-
if (held) {
|
|
125
|
-
if (held.expiresAt - 30_000 > Date.now()) return staffAuth(held);
|
|
126
|
-
throw new AuthError(
|
|
127
|
-
`your staff access to ${orgLabel(held.org)} has expired.${staffHint(held.org.id ?? held.org.workosId, true)}`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const org = orgRef ? findOrg(orgRef, config.orgs ?? []) : config.org;
|
|
132
|
-
if (orgRef && !org) {
|
|
133
|
-
throw new AuthError(
|
|
134
|
-
`unknown organization ${clean(orgRef)}. Run \`asteroid orgs\` to list yours.${staffHint(orgRef, isStaffLogin(login))}`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
if (!org) {
|
|
138
|
-
throw new AuthError(
|
|
139
|
-
"no organization. Run `asteroid use <org>`, or pass --org.",
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const cached = store.cachedToken(org.workosId);
|
|
144
|
-
if (cached) return bearer(cached.accessToken, cached.expiresAt);
|
|
145
|
-
|
|
146
|
-
return store.withRefreshLock(async () => {
|
|
147
|
-
const fresh = store.cachedToken(org.workosId);
|
|
148
|
-
if (fresh) return bearer(fresh.accessToken, fresh.expiresAt);
|
|
149
|
-
const login = await store.readLogin();
|
|
150
|
-
if (!login) {
|
|
151
|
-
throw new AuthError("not signed in. Run `asteroid login`.");
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
const session = await refresh(
|
|
155
|
-
{
|
|
156
|
-
clientId: env.workosClientId,
|
|
157
|
-
apiUrl: env.workosApiUrl,
|
|
158
|
-
fetch: fetchImpl,
|
|
159
|
-
},
|
|
160
|
-
login.refreshToken,
|
|
161
|
-
org.workosId,
|
|
162
|
-
);
|
|
163
|
-
await store.writeLogin({ ...login, refreshToken: session.refreshToken });
|
|
164
|
-
const expiresAt =
|
|
165
|
-
(tokenClaims(session.accessToken).exp ?? 0) * 1000 ||
|
|
166
|
-
Date.now() + 60_000;
|
|
167
|
-
const auth = bearer(session.accessToken, expiresAt);
|
|
168
|
-
store.cacheToken(auth.org?.workosId ?? org.workosId, {
|
|
169
|
-
accessToken: session.accessToken,
|
|
170
|
-
expiresAt,
|
|
171
|
-
});
|
|
172
|
-
rememberOrg(env, config, auth.org);
|
|
173
|
-
return auth;
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (error instanceof WorkosError) throw refreshError(error, org, login);
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}
|
package/src/auth/store.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import lockfile from "proper-lockfile";
|
|
4
|
-
|
|
5
|
-
import { type Env, type Org, readJson, writePrivateJson } from "../config";
|
|
6
|
-
import { WorkosError } from "./workos";
|
|
7
|
-
|
|
8
|
-
const SERVICE = "asteroid-cli";
|
|
9
|
-
const LOCK_STALE_MS = 30_000;
|
|
10
|
-
const ACCOUNT = "refresh-token";
|
|
11
|
-
const STAFF_ACCOUNT = "staff-tokens";
|
|
12
|
-
|
|
13
|
-
export interface Login {
|
|
14
|
-
refreshToken: string;
|
|
15
|
-
email?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface CachedToken {
|
|
19
|
-
accessToken: string;
|
|
20
|
-
expiresAt: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** A staff token for one org that is not yours. It lasts 8 h and is audited. */
|
|
24
|
-
export interface StaffToken {
|
|
25
|
-
token: string;
|
|
26
|
-
grantId: string;
|
|
27
|
-
org: Org;
|
|
28
|
-
reason: string;
|
|
29
|
-
expiresAt: number;
|
|
30
|
-
/** The default org before `asteroid use` took staff access. */
|
|
31
|
-
from?: Org;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type StaffTokens = Record<string, StaffToken>;
|
|
35
|
-
|
|
36
|
-
interface Keyring {
|
|
37
|
-
get(): string | null;
|
|
38
|
-
set(value: string): void;
|
|
39
|
-
delete(): void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function keyring(account = ACCOUNT): Promise<Keyring | undefined> {
|
|
43
|
-
if (process.env.ASTEROID_NO_KEYCHAIN) return undefined;
|
|
44
|
-
try {
|
|
45
|
-
const { Entry } = await import("@napi-rs/keyring");
|
|
46
|
-
const entry = new Entry(SERVICE, account);
|
|
47
|
-
entry.getPassword();
|
|
48
|
-
return {
|
|
49
|
-
get: () => entry.getPassword() ?? null,
|
|
50
|
-
set: (value) => entry.setPassword(value),
|
|
51
|
-
delete: () => {
|
|
52
|
-
entry.deletePassword();
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
} catch {
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function tryGet(ring: Keyring): { ok: boolean; value: string | null } {
|
|
61
|
-
try {
|
|
62
|
-
return { ok: true, value: ring.get() };
|
|
63
|
-
} catch {
|
|
64
|
-
return { ok: false, value: null };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function trySet(ring: Keyring, value: string | null): boolean {
|
|
69
|
-
try {
|
|
70
|
-
if (value === null) ring.delete();
|
|
71
|
-
else ring.set(value);
|
|
72
|
-
return true;
|
|
73
|
-
} catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export class CredentialStore {
|
|
79
|
-
constructor(private readonly env: Env) {}
|
|
80
|
-
|
|
81
|
-
private get loginFile() {
|
|
82
|
-
return join(this.env.configDir, "credentials.json");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private get tokenFile() {
|
|
86
|
-
return join(this.env.configDir, "tokens.json");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private get staffFile() {
|
|
90
|
-
return join(this.env.configDir, "staff-tokens.json");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async readLogin(): Promise<Login | undefined> {
|
|
94
|
-
const ring = await keyring();
|
|
95
|
-
const raw = ring ? tryGet(ring).value : null;
|
|
96
|
-
if (raw) return JSON.parse(raw) as Login;
|
|
97
|
-
return readJson<Login | undefined>(this.loginFile, undefined);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async writeLogin(login: Login): Promise<"keychain" | "file"> {
|
|
101
|
-
const ring = await keyring();
|
|
102
|
-
if (ring && trySet(ring, JSON.stringify(login))) {
|
|
103
|
-
rmSync(this.loginFile, { force: true });
|
|
104
|
-
return "keychain";
|
|
105
|
-
}
|
|
106
|
-
const stale = ring ? tryGet(ring) : { ok: true, value: null };
|
|
107
|
-
if (!stale.ok || (stale.value && !trySet(ring!, null))) {
|
|
108
|
-
throw new WorkosError(
|
|
109
|
-
"keychain",
|
|
110
|
-
"could not update the sign-in in the OS keychain. Unlock it, or run `asteroid logout` and sign in again.",
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
writePrivateJson(this.loginFile, login);
|
|
114
|
-
return "file";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async clear(): Promise<boolean> {
|
|
118
|
-
const ring = await keyring();
|
|
119
|
-
const current = ring ? tryGet(ring) : { ok: true, value: null };
|
|
120
|
-
const cleared = current.ok && (!current.value || trySet(ring!, null));
|
|
121
|
-
rmSync(this.loginFile, { force: true });
|
|
122
|
-
rmSync(this.tokenFile, { force: true });
|
|
123
|
-
const staffCleared = await this.withStaffLock(() =>
|
|
124
|
-
this.writeStaffTokens({}),
|
|
125
|
-
);
|
|
126
|
-
return staffCleared && cleared;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Every staff token held, expired ones included. */
|
|
130
|
-
async staffTokens(): Promise<StaffToken[]> {
|
|
131
|
-
const ring = await keyring(STAFF_ACCOUNT);
|
|
132
|
-
const raw = ring ? tryGet(ring).value : null;
|
|
133
|
-
const tokens = raw
|
|
134
|
-
? (JSON.parse(raw) as StaffTokens)
|
|
135
|
-
: readJson<StaffTokens>(this.staffFile, {});
|
|
136
|
-
return Object.values(tokens);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** The staff token for an org, matched by WorkOS id, id or name. */
|
|
140
|
-
async staffToken(ref: string): Promise<StaffToken | undefined> {
|
|
141
|
-
const lower = ref.toLowerCase();
|
|
142
|
-
return (await this.staffTokens()).find(
|
|
143
|
-
({ org }) =>
|
|
144
|
-
org.workosId.toLowerCase() === lower ||
|
|
145
|
-
org.id?.toLowerCase() === lower ||
|
|
146
|
-
org.name?.toLowerCase() === lower,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Saves a staff token and drops expired ones. `replaces` is dropped too. */
|
|
151
|
-
async saveStaffToken(token: StaffToken, replaces?: string) {
|
|
152
|
-
await this.withStaffLock(async () => {
|
|
153
|
-
const tokens: StaffTokens = {};
|
|
154
|
-
for (const held of await this.staffTokens())
|
|
155
|
-
if (held.expiresAt > Date.now() && held.grantId !== replaces)
|
|
156
|
-
tokens[held.org.workosId] = held;
|
|
157
|
-
tokens[token.org.workosId] = token;
|
|
158
|
-
if (!(await this.writeStaffTokens(tokens)))
|
|
159
|
-
throw new WorkosError(
|
|
160
|
-
"keychain",
|
|
161
|
-
"could not save the staff token in the OS keychain. Unlock it and try again.",
|
|
162
|
-
);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async dropStaffToken(grantId: string) {
|
|
167
|
-
await this.withStaffLock(async () => {
|
|
168
|
-
const tokens: StaffTokens = {};
|
|
169
|
-
for (const held of await this.staffTokens())
|
|
170
|
-
if (held.grantId !== grantId) tokens[held.org.workosId] = held;
|
|
171
|
-
if (!(await this.writeStaffTokens(tokens)))
|
|
172
|
-
throw new WorkosError(
|
|
173
|
-
"keychain",
|
|
174
|
-
"could not remove the staff token from the OS keychain. Unlock it and try again.",
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private async writeStaffTokens(tokens: StaffTokens): Promise<boolean> {
|
|
180
|
-
const empty = Object.keys(tokens).length === 0;
|
|
181
|
-
const ring = await keyring(STAFF_ACCOUNT);
|
|
182
|
-
if (ring && trySet(ring, empty ? null : JSON.stringify(tokens))) {
|
|
183
|
-
rmSync(this.staffFile, { force: true });
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
const stale = ring ? tryGet(ring) : { ok: true, value: null };
|
|
187
|
-
if (!stale.ok || (stale.value && !trySet(ring!, null))) return false;
|
|
188
|
-
if (empty) rmSync(this.staffFile, { force: true });
|
|
189
|
-
else writePrivateJson(this.staffFile, tokens);
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async withRefreshLock<T>(work: () => Promise<T>): Promise<T> {
|
|
194
|
-
return this.withLock(
|
|
195
|
-
"refresh",
|
|
196
|
-
"another asteroid command is refreshing the sign-in.",
|
|
197
|
-
work,
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Two commands at once must not lose a grant they could still revoke. */
|
|
202
|
-
private async withStaffLock<T>(work: () => Promise<T>): Promise<T> {
|
|
203
|
-
return this.withLock(
|
|
204
|
-
"staff",
|
|
205
|
-
"another asteroid command is saving staff access.",
|
|
206
|
-
work,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
private async withLock<T>(
|
|
211
|
-
name: string,
|
|
212
|
-
busy: string,
|
|
213
|
-
work: () => Promise<T>,
|
|
214
|
-
): Promise<T> {
|
|
215
|
-
mkdirSync(this.env.configDir, { recursive: true, mode: 0o700 });
|
|
216
|
-
const lockfilePath = join(this.env.configDir, `${name}.lock`);
|
|
217
|
-
let release: () => Promise<void>;
|
|
218
|
-
try {
|
|
219
|
-
// Each lock names its own target: proper-lockfile tracks locks by target.
|
|
220
|
-
release = await lockfile.lock(join(this.env.configDir, name), {
|
|
221
|
-
lockfilePath,
|
|
222
|
-
realpath: false,
|
|
223
|
-
stale: LOCK_STALE_MS,
|
|
224
|
-
retries: { retries: 60, minTimeout: 100, maxTimeout: 500 },
|
|
225
|
-
});
|
|
226
|
-
} catch {
|
|
227
|
-
throw new WorkosError(
|
|
228
|
-
"locked",
|
|
229
|
-
`${busy} Try again, or delete ${lockfilePath} if none is running.`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
try {
|
|
233
|
-
return await work();
|
|
234
|
-
} finally {
|
|
235
|
-
await release();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
cachedToken(orgId: string): CachedToken | undefined {
|
|
240
|
-
const tokens = readJson<Record<string, CachedToken>>(this.tokenFile, {});
|
|
241
|
-
const token = tokens[orgId];
|
|
242
|
-
return token && token.expiresAt - 30_000 > Date.now() ? token : undefined;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
cacheToken(orgId: string, token: CachedToken) {
|
|
246
|
-
const tokens = readJson<Record<string, CachedToken>>(this.tokenFile, {});
|
|
247
|
-
for (const [id, cached] of Object.entries(tokens)) {
|
|
248
|
-
if (cached.expiresAt <= Date.now()) delete tokens[id];
|
|
249
|
-
}
|
|
250
|
-
tokens[orgId] = token;
|
|
251
|
-
writePrivateJson(this.tokenFile, tokens);
|
|
252
|
-
}
|
|
253
|
-
}
|
package/src/auth/workos.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
export interface DeviceAuthorization {
|
|
2
|
-
device_code: string;
|
|
3
|
-
user_code: string;
|
|
4
|
-
verification_uri: string;
|
|
5
|
-
verification_uri_complete?: string;
|
|
6
|
-
expires_in: number;
|
|
7
|
-
interval: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface WorkosOrg {
|
|
11
|
-
id: string;
|
|
12
|
-
name: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface Session {
|
|
16
|
-
accessToken: string;
|
|
17
|
-
refreshToken: string;
|
|
18
|
-
organizationId?: string;
|
|
19
|
-
email?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class WorkosError extends Error {
|
|
23
|
-
constructor(
|
|
24
|
-
readonly code: string,
|
|
25
|
-
message: string,
|
|
26
|
-
readonly body: Record<string, unknown> = {},
|
|
27
|
-
) {
|
|
28
|
-
super(message);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface WorkosClient {
|
|
33
|
-
clientId: string;
|
|
34
|
-
apiUrl: string;
|
|
35
|
-
fetch?: typeof fetch;
|
|
36
|
-
sleep?: (ms: number) => Promise<void>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const AUTHENTICATE = "/user_management/authenticate";
|
|
40
|
-
const TIMEOUT_MS = 15_000;
|
|
41
|
-
|
|
42
|
-
async function post(
|
|
43
|
-
client: WorkosClient,
|
|
44
|
-
path: string,
|
|
45
|
-
body: Record<string, string>,
|
|
46
|
-
): Promise<Record<string, unknown>> {
|
|
47
|
-
const response = await (client.fetch ?? fetch)(`${client.apiUrl}${path}`, {
|
|
48
|
-
method: "POST",
|
|
49
|
-
headers: { Accept: "application/json" },
|
|
50
|
-
body: new URLSearchParams({ client_id: client.clientId, ...body }),
|
|
51
|
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
52
|
-
});
|
|
53
|
-
const json = (await response.json().catch(() => ({}))) as Record<
|
|
54
|
-
string,
|
|
55
|
-
unknown
|
|
56
|
-
>;
|
|
57
|
-
if (!response.ok) {
|
|
58
|
-
const code = String(json.error ?? json.code ?? response.status);
|
|
59
|
-
const message = String(json.error_description ?? json.message ?? code);
|
|
60
|
-
throw new WorkosError(code, message, json);
|
|
61
|
-
}
|
|
62
|
-
return json;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function pollOnce(client: WorkosClient, deviceCode: string) {
|
|
66
|
-
return post(client, AUTHENTICATE, {
|
|
67
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
68
|
-
device_code: deviceCode,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function selectOrg(
|
|
73
|
-
client: WorkosClient,
|
|
74
|
-
pendingAuthenticationToken: string,
|
|
75
|
-
organizationId: string,
|
|
76
|
-
) {
|
|
77
|
-
return post(client, AUTHENTICATE, {
|
|
78
|
-
grant_type: "urn:workos:oauth:grant-type:organization-selection",
|
|
79
|
-
pending_authentication_token: pendingAuthenticationToken,
|
|
80
|
-
organization_id: organizationId,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function session(json: Record<string, unknown>): Session {
|
|
85
|
-
const user = json.user as { email?: string } | undefined;
|
|
86
|
-
return {
|
|
87
|
-
accessToken: String(json.access_token),
|
|
88
|
-
refreshToken: String(json.refresh_token),
|
|
89
|
-
organizationId: (json.organization_id as string | undefined) ?? undefined,
|
|
90
|
-
email: user?.email,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function authorizeDevice(
|
|
95
|
-
client: WorkosClient,
|
|
96
|
-
): Promise<DeviceAuthorization> {
|
|
97
|
-
return (await post(
|
|
98
|
-
client,
|
|
99
|
-
"/user_management/authorize/device",
|
|
100
|
-
{},
|
|
101
|
-
)) as unknown as DeviceAuthorization;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function pollDevice(
|
|
105
|
-
client: WorkosClient,
|
|
106
|
-
device: DeviceAuthorization,
|
|
107
|
-
chooseOrg: (orgs: WorkosOrg[]) => Promise<WorkosOrg>,
|
|
108
|
-
): Promise<Session> {
|
|
109
|
-
const sleep = client.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
110
|
-
let interval = (device.interval || 5) * 1000;
|
|
111
|
-
const deadline = Date.now() + device.expires_in * 1000;
|
|
112
|
-
while (Date.now() < deadline) {
|
|
113
|
-
await sleep(interval);
|
|
114
|
-
try {
|
|
115
|
-
return session(await pollOnce(client, device.device_code));
|
|
116
|
-
} catch (error) {
|
|
117
|
-
if (!(error instanceof WorkosError)) throw error;
|
|
118
|
-
if (error.code === "authorization_pending") continue;
|
|
119
|
-
if (error.code === "slow_down") {
|
|
120
|
-
interval += 5000;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (error.code === "organization_selection_required") {
|
|
124
|
-
const orgs =
|
|
125
|
-
(error.body.organizations as WorkosOrg[] | undefined) ?? [];
|
|
126
|
-
const org = await chooseOrg(orgs);
|
|
127
|
-
return session(
|
|
128
|
-
await selectOrg(
|
|
129
|
-
client,
|
|
130
|
-
String(error.body.pending_authentication_token),
|
|
131
|
-
org.id,
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
throw new WorkosError(
|
|
139
|
-
"expired_token",
|
|
140
|
-
"the sign-in code expired; run asteroid login again",
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function refresh(
|
|
145
|
-
client: WorkosClient,
|
|
146
|
-
refreshToken: string,
|
|
147
|
-
organizationId?: string,
|
|
148
|
-
): Promise<Session> {
|
|
149
|
-
return session(
|
|
150
|
-
await post(client, AUTHENTICATE, {
|
|
151
|
-
grant_type: "refresh_token",
|
|
152
|
-
refresh_token: refreshToken,
|
|
153
|
-
...(organizationId ? { organization_id: organizationId } : {}),
|
|
154
|
-
}),
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export interface Claims {
|
|
159
|
-
exp?: number;
|
|
160
|
-
org_id?: string;
|
|
161
|
-
org?: { id?: string; name?: string };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function tokenClaims(token: string): Claims {
|
|
165
|
-
try {
|
|
166
|
-
const payload = token.split(".")[1];
|
|
167
|
-
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
168
|
-
} catch {
|
|
169
|
-
return {};
|
|
170
|
-
}
|
|
171
|
-
}
|
package/src/commands.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { COMMANDS } from "./generated/commands";
|
|
2
|
-
import { SYNC_COMMANDS } from "./sync/commands";
|
|
3
|
-
import type { CliCommand } from "./types";
|
|
4
|
-
|
|
5
|
-
export function withSyncCommands(
|
|
6
|
-
commands: readonly CliCommand[],
|
|
7
|
-
): CliCommand[] {
|
|
8
|
-
return [...commands, ...SYNC_COMMANDS].sort((a, b) =>
|
|
9
|
-
a.command.localeCompare(b.command),
|
|
10
|
-
);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const ALL_COMMANDS = withSyncCommands(COMMANDS);
|