@anyship/cli 0.1.0 → 0.2.0
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/README.md +22 -10
- package/package.json +1 -1
- package/src/config.mjs +3 -4
- package/src/deploy-directory.mjs +24 -7
- package/src/login.mjs +13 -6
- package/src/mcp-server.mjs +20 -6
- package/src/oauth.mjs +162 -0
package/README.md
CHANGED
|
@@ -21,15 +21,24 @@ never hand-pick files.
|
|
|
21
21
|
|
|
22
22
|
### `anyship login`
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
Run **`anyship login`** with no arguments and it opens your browser to sign in
|
|
25
|
+
(OAuth) — the same login you use for the anyship connector, no key required. It
|
|
26
|
+
stores the resulting token in `~/.anyship/config.json` (mode `0600`), refreshing
|
|
27
|
+
it automatically. This is the common path now that most people connect with
|
|
28
|
+
browser login.
|
|
29
|
+
|
|
30
|
+
Prefer an API key (CI, scripts)? Pass one and it's validated + stored instead:
|
|
27
31
|
|
|
28
32
|
```bash
|
|
29
|
-
anyship login
|
|
30
|
-
anyship
|
|
33
|
+
anyship login # browser login (OAuth) — no key
|
|
34
|
+
anyship login --key anyship_sk_… # or an API key: echo $KEY | anyship login --with-token
|
|
35
|
+
anyship logout # remove stored credentials
|
|
31
36
|
```
|
|
32
37
|
|
|
38
|
+
`deploy` and `mcp` then authenticate automatically. If neither a key nor a stored
|
|
39
|
+
token is present, `deploy` triggers the browser login on first use; the `mcp`
|
|
40
|
+
server uses only stored auth (run `anyship login` once first).
|
|
41
|
+
|
|
33
42
|
### `anyship deploy [path]`
|
|
34
43
|
|
|
35
44
|
Archive and deploy a project directory (defaults to `.`). anyship detects the
|
|
@@ -60,11 +69,14 @@ files and never handles your API key.
|
|
|
60
69
|
|
|
61
70
|
## Configuration
|
|
62
71
|
|
|
63
|
-
|
|
72
|
+
Auth resolves in this order — an **API key** if you have one, otherwise **OAuth**:
|
|
64
73
|
|
|
65
|
-
1.
|
|
66
|
-
2.
|
|
67
|
-
3. stored
|
|
74
|
+
1. `--key` flag
|
|
75
|
+
2. `ANYSHIP_API_KEY` env
|
|
76
|
+
3. a stored API key (`~/.anyship/config.json`)
|
|
68
77
|
4. an `mcp_servers.anyship` entry in `~/.codex/config.toml`
|
|
78
|
+
5. a stored **OAuth token** (`~/.anyship/config.json`, auto-refreshed)
|
|
79
|
+
6. otherwise → browser login (interactive commands only)
|
|
69
80
|
|
|
70
|
-
|
|
81
|
+
The control-plane URL comes from `--api`, `ANYSHIP_API_URL`, the stored config, or
|
|
82
|
+
the default. Requires Node.js 18+.
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -13,13 +13,12 @@ export function anyshipConfigPath(home = homedir()) {
|
|
|
13
13
|
return join(anyshipConfigDir(home), "config.json");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// Returns the whole stored object (apiKey, apiUrl, oauth, …) so writes preserve
|
|
17
|
+
// every field — the OAuth token block must survive an apiKey/apiUrl patch.
|
|
16
18
|
export function readAnyshipConfig(home = homedir()) {
|
|
17
19
|
try {
|
|
18
20
|
const parsed = JSON.parse(readFileSync(anyshipConfigPath(home), "utf8"));
|
|
19
|
-
return {
|
|
20
|
-
apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
|
|
21
|
-
apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : undefined,
|
|
22
|
-
};
|
|
21
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
23
22
|
} catch {
|
|
24
23
|
return {};
|
|
25
24
|
}
|
package/src/deploy-directory.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { basename, join, relative, resolve } from "node:path";
|
|
6
6
|
import { zipSync } from "fflate";
|
|
7
7
|
import { readAnyshipConfig } from "./config.mjs";
|
|
8
|
+
import { browserLogin, storedAccessToken } from "./oauth.mjs";
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_API = "https://drydock.anyship.workers.dev";
|
|
10
11
|
|
|
@@ -130,6 +131,25 @@ export function apiUrl(args, config = codexAnyshipConfig(), stored = readAnyship
|
|
|
130
131
|
).replace(/\/+$/, "");
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
// Bearer for control-plane calls. Precedence: an API key (flag / env / stored /
|
|
135
|
+
// codex config) → a stored OAuth token (refreshed if expiring) → an interactive
|
|
136
|
+
// browser login. The no-key OAuth path is the common one now that most users
|
|
137
|
+
// connect anyship with browser login. Pass opts.interactive === false to fail
|
|
138
|
+
// instead of opening a browser (CI / non-interactive).
|
|
139
|
+
export async function resolveBearer(args, io, opts = {}) {
|
|
140
|
+
const stored = readAnyshipConfig(opts.home);
|
|
141
|
+
const key = apiKey(args, undefined, stored);
|
|
142
|
+
if (key) return key;
|
|
143
|
+
const api = apiUrl(args, undefined, stored);
|
|
144
|
+
const existing = await storedAccessToken(api, { home: opts.home, fetch: opts.fetch });
|
|
145
|
+
if (existing) return existing;
|
|
146
|
+
if (opts.interactive === false) {
|
|
147
|
+
throw new Error("not authenticated — set ANYSHIP_API_KEY or run `anyship login`");
|
|
148
|
+
}
|
|
149
|
+
const oauth = await browserLogin(api, io, { home: opts.home, fetch: opts.fetch });
|
|
150
|
+
return oauth.accessToken;
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
function pathParts(rel) {
|
|
134
154
|
return rel.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
135
155
|
}
|
|
@@ -220,12 +240,9 @@ export async function deployDirectory(args, hooks = {}) {
|
|
|
220
240
|
|
|
221
241
|
if (args.dryRun) return { dryRun: true, ...summary };
|
|
222
242
|
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
throw new Error("missing Anyship API key; set ANYSHIP_API_KEY or configure global mcp_servers.anyship");
|
|
227
|
-
}
|
|
228
|
-
const api = apiUrl(args, config);
|
|
243
|
+
const api = apiUrl(args);
|
|
244
|
+
const io = hooks.io ?? { stdout: () => {}, stderr: (t) => process.stderr.write(`${t}\n`) };
|
|
245
|
+
const token = await resolveBearer(args, io, { interactive: hooks.interactive });
|
|
229
246
|
hooks.onUpload?.({ ...summary, api });
|
|
230
247
|
|
|
231
248
|
const form = new FormData();
|
|
@@ -235,7 +252,7 @@ export async function deployDirectory(args, hooks = {}) {
|
|
|
235
252
|
|
|
236
253
|
const res = await fetch(`${api}/api/deploy-from-archive`, {
|
|
237
254
|
method: "POST",
|
|
238
|
-
headers: { Authorization: `Bearer ${
|
|
255
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
239
256
|
body: form,
|
|
240
257
|
});
|
|
241
258
|
const text = await res.text();
|
package/src/login.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// no --env and no token in shell history.
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { DEFAULT_API } from "./deploy-directory.mjs";
|
|
7
|
+
import { browserLogin } from "./oauth.mjs";
|
|
7
8
|
import {
|
|
8
9
|
anyshipConfigPath,
|
|
9
10
|
clearAnyshipConfig,
|
|
@@ -66,17 +67,17 @@ function promptSecret(query, input, output) {
|
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// An explicitly-supplied key (flag / env / piped). Returns "" when none — that's
|
|
71
|
+
// the signal to fall back to browser-login OAuth.
|
|
69
72
|
async function resolveKey(args, deps) {
|
|
70
73
|
if (args.key) return args.key.trim();
|
|
74
|
+
if (process.env.ANYSHIP_API_KEY) return process.env.ANYSHIP_API_KEY.trim();
|
|
71
75
|
const input = deps.input ?? process.stdin;
|
|
72
|
-
const output = deps.output ?? process.stdout;
|
|
73
76
|
// Piped input (scripts, `echo $KEY | anyship login --with-token`).
|
|
74
77
|
if (args.withToken || !input.isTTY) {
|
|
75
78
|
const piped = await readAll(input);
|
|
76
79
|
if (piped.trim()) return piped.trim();
|
|
77
80
|
}
|
|
78
|
-
// Interactive, hidden.
|
|
79
|
-
if (input.isTTY) return (await promptSecret("Paste your anyship API key (anyship_sk_…): ", input, output)).trim();
|
|
80
81
|
return "";
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -108,13 +109,19 @@ export async function runLogin(argv, io = defaultIo(), deps = {}) {
|
|
|
108
109
|
io.stdout(loginUsage());
|
|
109
110
|
return 0;
|
|
110
111
|
}
|
|
112
|
+
const api = (args.api || process.env.ANYSHIP_API_URL || readAnyshipConfig(deps.home).apiUrl || DEFAULT_API).replace(/\/+$/, "");
|
|
111
113
|
const key = await resolveKey(args, deps);
|
|
112
|
-
|
|
114
|
+
|
|
115
|
+
// No key supplied → browser-login OAuth (the common case). A key → validate + store.
|
|
116
|
+
if (!key) {
|
|
117
|
+
await browserLogin(api, io, deps);
|
|
118
|
+
io.stdout("✅ Signed in to anyship. `anyship deploy` and `anyship mcp` are now authenticated.");
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
113
122
|
if (!/^anyship_sk_/.test(key)) {
|
|
114
123
|
io.stderr("warning: keys usually start with 'anyship_sk_'; continuing anyway");
|
|
115
124
|
}
|
|
116
|
-
const api = (args.api || process.env.ANYSHIP_API_URL || readAnyshipConfig(deps.home).apiUrl || DEFAULT_API).replace(/\/+$/, "");
|
|
117
|
-
|
|
118
125
|
const who = await validateKey(api, key, deps.fetch ?? fetch);
|
|
119
126
|
// Only persist a non-default URL so the default can still shift centrally.
|
|
120
127
|
const patch = { apiKey: key };
|
package/src/mcp-server.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// whatever tools the platform exposes — only `deploy` is handled locally.
|
|
11
11
|
|
|
12
12
|
import { deployDirectory, apiKey, apiUrl, codexAnyshipConfig } from "./deploy-directory.mjs";
|
|
13
|
+
import { storedAccessToken } from "./oauth.mjs";
|
|
13
14
|
import { createInterface } from "node:readline";
|
|
14
15
|
import { resolve } from "node:path";
|
|
15
16
|
|
|
@@ -51,15 +52,28 @@ const LOCAL_DEPLOY_TOOL = {
|
|
|
51
52
|
// limitation — irrelevant here, where deploy reads the disk directly.
|
|
52
53
|
const LOCAL_OVERRIDDEN = new Set(["deploy", "deploy_archive", "cli_instructions"]);
|
|
53
54
|
|
|
54
|
-
//
|
|
55
|
+
// Bearer for the remote proxy: an API key (test-injected, env, stored, or codex
|
|
56
|
+
// config) or a stored OAuth token from a prior `anyship login`. This background
|
|
57
|
+
// server never opens a browser — the user authenticates once with `anyship login`
|
|
58
|
+
// (which does the browser flow); here we only use what's already stored.
|
|
59
|
+
async function remoteBearer(deps) {
|
|
60
|
+
const config = deps.config ?? codexAnyshipConfig();
|
|
61
|
+
const key = deps.apiKey ? deps.apiKey({}, config) : apiKey({}, config);
|
|
62
|
+
if (key) return key;
|
|
63
|
+
const api = deps.apiUrl ? deps.apiUrl({}, config) : apiUrl({}, config);
|
|
64
|
+
const oauthTok = await storedAccessToken(api);
|
|
65
|
+
if (oauthTok) return oauthTok;
|
|
66
|
+
throw new Error("not authenticated — run `anyship login` first");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Forward a JSON-RPC call to the remote control-plane /mcp.
|
|
55
70
|
async function remoteProxy(method, params, deps) {
|
|
56
71
|
const config = deps.config ?? codexAnyshipConfig();
|
|
57
|
-
const api = deps.apiUrl
|
|
58
|
-
const
|
|
59
|
-
if (!key) throw new Error("missing Anyship API key; start this server with ANYSHIP_API_KEY set");
|
|
72
|
+
const api = deps.apiUrl ? deps.apiUrl({}, config) : apiUrl({}, config);
|
|
73
|
+
const token = await remoteBearer(deps);
|
|
60
74
|
const res = await (deps.fetch ?? fetch)(`${api}/mcp`, {
|
|
61
75
|
method: "POST",
|
|
62
|
-
headers: { Authorization: `Bearer ${
|
|
76
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
63
77
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
64
78
|
});
|
|
65
79
|
const data = await res.json().catch(() => null);
|
|
@@ -76,7 +90,7 @@ async function localDeploy(args, deps) {
|
|
|
76
90
|
? JSON.stringify(args.env)
|
|
77
91
|
: undefined;
|
|
78
92
|
const runDeploy = deps.deployDirectory ?? deployDirectory;
|
|
79
|
-
const result = await runDeploy({ project, path, env });
|
|
93
|
+
const result = await runDeploy({ project, path, env }, { interactive: false });
|
|
80
94
|
const root = resolve(path);
|
|
81
95
|
const message = result?.response?.message ?? "Deploying. Poll deployment_status for progress.";
|
|
82
96
|
const depId = result?.response?.deployment?.id;
|
package/src/oauth.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Browser-login OAuth for the CLI — the same flow an MCP client runs, so a user
|
|
2
|
+
// who connected anyship with browser login (no API key) can also use the CLI.
|
|
3
|
+
// Loopback Authorization Code + PKCE: discover the auth server, dynamically
|
|
4
|
+
// register, open the browser, catch the redirect on 127.0.0.1, exchange the code
|
|
5
|
+
// for tokens, and store them in ~/.anyship/config.json. Refreshes on expiry.
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { readAnyshipConfig, writeAnyshipConfig } from "./config.mjs";
|
|
10
|
+
|
|
11
|
+
const b64url = (b) => Buffer.from(b).toString("base64url");
|
|
12
|
+
|
|
13
|
+
async function discover(api, fetchImpl) {
|
|
14
|
+
const res = await fetchImpl(`${api}/.well-known/oauth-authorization-server`);
|
|
15
|
+
if (!res.ok) throw new Error(`OAuth discovery failed at ${api} (HTTP ${res.status})`);
|
|
16
|
+
const meta = await res.json();
|
|
17
|
+
if (!meta?.authorization_endpoint || !meta?.token_endpoint || !meta?.registration_endpoint) {
|
|
18
|
+
throw new Error(`${api} does not advertise the OAuth endpoints needed for login`);
|
|
19
|
+
}
|
|
20
|
+
return meta;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function openBrowser(url) {
|
|
24
|
+
const p = process.platform;
|
|
25
|
+
const [cmd, args] =
|
|
26
|
+
p === "darwin" ? ["open", [url]] : p === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
27
|
+
try {
|
|
28
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
29
|
+
} catch {
|
|
30
|
+
/* headless / no browser — the URL is printed for manual use */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Loopback server on an ephemeral port; resolves with the OAuth redirect params.
|
|
35
|
+
function startCallbackServer() {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let resolveCode;
|
|
38
|
+
const waitForCode = new Promise((r) => (resolveCode = r));
|
|
39
|
+
const server = createServer((req, res) => {
|
|
40
|
+
const u = new URL(req.url, "http://127.0.0.1");
|
|
41
|
+
if (u.pathname !== "/callback") {
|
|
42
|
+
res.writeHead(404);
|
|
43
|
+
res.end();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
47
|
+
res.end(
|
|
48
|
+
"<!doctype html><body style='font:16px/1.5 system-ui;text-align:center;padding:48px'>" +
|
|
49
|
+
"<h2>anyship — signed in ✓</h2><p>You can close this tab and return to your terminal.</p>",
|
|
50
|
+
);
|
|
51
|
+
resolveCode({ code: u.searchParams.get("code"), state: u.searchParams.get("state") });
|
|
52
|
+
});
|
|
53
|
+
server.listen(0, "127.0.0.1", () => resolve({ server, port: server.address().port, waitForCode }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Run the full interactive browser login and persist the tokens. Returns the
|
|
58
|
+
// stored oauth block.
|
|
59
|
+
export async function browserLogin(api, io = defaultIo(), deps = {}) {
|
|
60
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
61
|
+
const meta = await discover(api, fetchImpl);
|
|
62
|
+
|
|
63
|
+
const { server, port, waitForCode } = await startCallbackServer();
|
|
64
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
65
|
+
try {
|
|
66
|
+
const reg = await fetchImpl(meta.registration_endpoint, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "content-type": "application/json" },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
client_name: "Anyship CLI",
|
|
71
|
+
redirect_uris: [redirectUri],
|
|
72
|
+
token_endpoint_auth_method: "none",
|
|
73
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
74
|
+
response_types: ["code"],
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const client = await reg.json().catch(() => null);
|
|
78
|
+
if (!client?.client_id) throw new Error("OAuth client registration failed");
|
|
79
|
+
|
|
80
|
+
const verifier = b64url(randomBytes(32));
|
|
81
|
+
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
82
|
+
const state = b64url(randomBytes(16));
|
|
83
|
+
const authUrl = `${meta.authorization_endpoint}?${new URLSearchParams({
|
|
84
|
+
response_type: "code",
|
|
85
|
+
client_id: client.client_id,
|
|
86
|
+
redirect_uri: redirectUri,
|
|
87
|
+
scope: "openid profile email offline_access",
|
|
88
|
+
state,
|
|
89
|
+
code_challenge: challenge,
|
|
90
|
+
code_challenge_method: "S256",
|
|
91
|
+
})}`;
|
|
92
|
+
io.stderr(`Opening your browser to sign in to anyship…\nIf it doesn't open, visit:\n ${authUrl}`);
|
|
93
|
+
openBrowser(authUrl);
|
|
94
|
+
|
|
95
|
+
const got = await waitForCode;
|
|
96
|
+
if (got.state !== state) throw new Error("OAuth state mismatch — aborting");
|
|
97
|
+
if (!got.code) throw new Error("no authorization code returned");
|
|
98
|
+
|
|
99
|
+
const tok = await fetchImpl(meta.token_endpoint, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
102
|
+
body: new URLSearchParams({
|
|
103
|
+
grant_type: "authorization_code",
|
|
104
|
+
code: got.code,
|
|
105
|
+
redirect_uri: redirectUri,
|
|
106
|
+
client_id: client.client_id,
|
|
107
|
+
code_verifier: verifier,
|
|
108
|
+
}).toString(),
|
|
109
|
+
});
|
|
110
|
+
const t = await tok.json().catch(() => null);
|
|
111
|
+
if (!t?.access_token) throw new Error("token exchange failed");
|
|
112
|
+
|
|
113
|
+
const oauth = {
|
|
114
|
+
apiUrl: api,
|
|
115
|
+
clientId: client.client_id,
|
|
116
|
+
accessToken: t.access_token,
|
|
117
|
+
refreshToken: t.refresh_token || null,
|
|
118
|
+
expiresAt: t.expires_in ? Date.now() + t.expires_in * 1000 : null,
|
|
119
|
+
};
|
|
120
|
+
writeAnyshipConfig({ oauth }, deps.home);
|
|
121
|
+
io.stderr("Signed in to anyship.");
|
|
122
|
+
return oauth;
|
|
123
|
+
} finally {
|
|
124
|
+
server.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Return a valid stored access token (refreshing if near expiry), or null if
|
|
129
|
+
// there's no usable OAuth token for this api.
|
|
130
|
+
export async function storedAccessToken(api, deps = {}) {
|
|
131
|
+
const oauth = readAnyshipConfig(deps.home).oauth;
|
|
132
|
+
if (!oauth?.accessToken || oauth.apiUrl !== api) return null;
|
|
133
|
+
if (!oauth.expiresAt || oauth.expiresAt - Date.now() > 60_000) return oauth.accessToken;
|
|
134
|
+
if (!oauth.refreshToken) return null;
|
|
135
|
+
|
|
136
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
137
|
+
const meta = await discover(api, fetchImpl).catch(() => null);
|
|
138
|
+
if (!meta?.token_endpoint) return null;
|
|
139
|
+
const res = await fetchImpl(meta.token_endpoint, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
142
|
+
body: new URLSearchParams({
|
|
143
|
+
grant_type: "refresh_token",
|
|
144
|
+
refresh_token: oauth.refreshToken,
|
|
145
|
+
client_id: oauth.clientId,
|
|
146
|
+
}).toString(),
|
|
147
|
+
}).catch(() => null);
|
|
148
|
+
const t = res && (await res.json().catch(() => null));
|
|
149
|
+
if (!t?.access_token) return null;
|
|
150
|
+
const updated = {
|
|
151
|
+
...oauth,
|
|
152
|
+
accessToken: t.access_token,
|
|
153
|
+
refreshToken: t.refresh_token || oauth.refreshToken,
|
|
154
|
+
expiresAt: t.expires_in ? Date.now() + t.expires_in * 1000 : null,
|
|
155
|
+
};
|
|
156
|
+
writeAnyshipConfig({ oauth: updated }, deps.home);
|
|
157
|
+
return updated.accessToken;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function defaultIo() {
|
|
161
|
+
return { stdout: (t) => console.log(t), stderr: (t) => console.error(t) };
|
|
162
|
+
}
|