seedyn 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 +7 -3
- package/package.json +1 -1
- package/src/browser-auth.mjs +105 -0
- package/src/cli.mjs +47 -2
- package/src/config.mjs +1 -1
package/README.md
CHANGED
|
@@ -5,13 +5,17 @@ no runtime dependencies and requires Node.js 22.12 or newer.
|
|
|
5
5
|
|
|
6
6
|
## Setup
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Open Seedyn in your browser, approve the requested upload scopes, and let the
|
|
9
|
+
CLI save the new key:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npx seedyn auth
|
|
12
|
+
npx seedyn auth login
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
The browser encrypts the new credential to the waiting CLI process. The server
|
|
16
|
+
cannot recover it from the login request. To store an existing key instead, run
|
|
17
|
+
`npx seedyn auth set` and paste it into the hidden prompt.
|
|
18
|
+
|
|
15
19
|
Credentials are written to `$XDG_CONFIG_HOME/seedyn/config.json`, or
|
|
16
20
|
`~/.config/seedyn/config.json` when `XDG_CONFIG_HOME` is unset. The directory is
|
|
17
21
|
mode `0700` and the file is mode `0600`. `SEEDYN_CONFIG_PATH` can select a
|
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { generateKeyPairSync, privateDecrypt } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { CliError } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
function delay(milliseconds) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function json(response) {
|
|
10
|
+
try {
|
|
11
|
+
return await response.json();
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function responseError(response, body) {
|
|
18
|
+
const message = body?.error?.message;
|
|
19
|
+
return new CliError(
|
|
20
|
+
typeof message === "string"
|
|
21
|
+
? message
|
|
22
|
+
: `Seedyn authentication failed with HTTP ${response.status}.`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function browserLogin({ apiUrl, version, openBrowser }) {
|
|
27
|
+
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
28
|
+
modulusLength: 2_048,
|
|
29
|
+
});
|
|
30
|
+
const publicKeyPem = publicKey
|
|
31
|
+
.export({ type: "spki", format: "pem" })
|
|
32
|
+
.toString();
|
|
33
|
+
|
|
34
|
+
let startResponse;
|
|
35
|
+
try {
|
|
36
|
+
startResponse = await fetch(`${apiUrl}/api/cli-auth/start`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"User-Agent": `seedyn/${version}`,
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({ publicKey: publicKeyPem }),
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new CliError(`Could not reach ${apiUrl}.`, { cause: error });
|
|
46
|
+
}
|
|
47
|
+
const started = await json(startResponse);
|
|
48
|
+
if (!startResponse.ok) throw responseError(startResponse, started);
|
|
49
|
+
if (
|
|
50
|
+
typeof started?.requestId !== "string" ||
|
|
51
|
+
typeof started?.pollSecret !== "string" ||
|
|
52
|
+
typeof started?.verificationUrl !== "string" ||
|
|
53
|
+
typeof started?.expiresAt !== "string"
|
|
54
|
+
) {
|
|
55
|
+
throw new CliError("Seedyn returned an invalid authentication response.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`Open this page to connect the CLI:\n${started.verificationUrl}`);
|
|
59
|
+
if (openBrowser(started.verificationUrl)) {
|
|
60
|
+
console.log("Opened the page in your browser.");
|
|
61
|
+
}
|
|
62
|
+
console.log("Waiting for approval...");
|
|
63
|
+
|
|
64
|
+
const expiresAt = Date.parse(started.expiresAt);
|
|
65
|
+
const intervalMs = Math.max(
|
|
66
|
+
1_000,
|
|
67
|
+
Math.min(10_000, Number(started.intervalSeconds || 2) * 1_000),
|
|
68
|
+
);
|
|
69
|
+
while (Date.now() < expiresAt) {
|
|
70
|
+
await delay(intervalMs);
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
response = await fetch(
|
|
74
|
+
`${apiUrl}/api/cli-auth/poll/${encodeURIComponent(started.requestId)}`,
|
|
75
|
+
{
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${started.pollSecret}`,
|
|
78
|
+
"User-Agent": `seedyn/${version}`,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const body = await json(response);
|
|
86
|
+
if (response.status === 202) continue;
|
|
87
|
+
if (!response.ok) throw responseError(response, body);
|
|
88
|
+
if (typeof body?.encryptedApiKey !== "string") {
|
|
89
|
+
throw new CliError("Seedyn returned an invalid encrypted credential.");
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return privateDecrypt(
|
|
93
|
+
{ key: privateKey, oaepHash: "sha256" },
|
|
94
|
+
Buffer.from(body.encryptedApiKey, "base64"),
|
|
95
|
+
).toString("utf8");
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new CliError("The CLI could not decrypt the new credential.", {
|
|
98
|
+
cause: error,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new CliError(
|
|
103
|
+
"The login request expired. Run `seedyn auth login` again.",
|
|
104
|
+
);
|
|
105
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
validateApiUrl,
|
|
13
13
|
} from "./config.mjs";
|
|
14
14
|
import { CliError } from "./errors.mjs";
|
|
15
|
+
import { browserLogin } from "./browser-auth.mjs";
|
|
15
16
|
import { copyUrl, openUrl, uploadFile } from "./upload.mjs";
|
|
16
17
|
|
|
17
18
|
const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
@@ -73,6 +74,42 @@ async function authCommand(args) {
|
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
if (action === "login") {
|
|
78
|
+
const { values, positionals } = parseArgs({
|
|
79
|
+
args: args.slice(1),
|
|
80
|
+
allowPositionals: true,
|
|
81
|
+
strict: true,
|
|
82
|
+
options: {
|
|
83
|
+
"api-url": { type: "string" },
|
|
84
|
+
"no-open": { type: "boolean" },
|
|
85
|
+
help: { type: "boolean", short: "h" },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (values.help) {
|
|
89
|
+
console.log(AUTH_LOGIN_HELP);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (positionals.length > 0) {
|
|
93
|
+
throw new CliError("auth login takes no arguments.");
|
|
94
|
+
}
|
|
95
|
+
const apiUrl = validateApiUrl(
|
|
96
|
+
values["api-url"] ||
|
|
97
|
+
process.env.SEEDYN_API_URL ||
|
|
98
|
+
"https://seedyn.dave.tips",
|
|
99
|
+
);
|
|
100
|
+
const key = validateApiKey(
|
|
101
|
+
await browserLogin({
|
|
102
|
+
apiUrl,
|
|
103
|
+
version: VERSION,
|
|
104
|
+
openBrowser: values["no-open"] ? () => false : openUrl,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
const result = await saveAuthentication({ apiKey: key, apiUrl });
|
|
108
|
+
console.log(`Seedyn credentials saved to ${result.file}`);
|
|
109
|
+
console.log(`API key: ${displayApiKey(key)}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
if (action === "remove") {
|
|
77
114
|
if (args.length > 1) throw new CliError("auth remove takes no arguments.");
|
|
78
115
|
console.log(
|
|
@@ -249,7 +286,7 @@ Upload any file and receive its durable URL.
|
|
|
249
286
|
Usage:
|
|
250
287
|
seedyn <file> [options]
|
|
251
288
|
seedyn upload <file> [options]
|
|
252
|
-
seedyn auth <set|status|remove>
|
|
289
|
+
seedyn auth <login|set|status|remove>
|
|
253
290
|
|
|
254
291
|
Examples:
|
|
255
292
|
seedyn ./image.png --copy
|
|
@@ -279,11 +316,19 @@ Options:
|
|
|
279
316
|
const AUTH_HELP = `Usage: seedyn auth <command>
|
|
280
317
|
|
|
281
318
|
Commands:
|
|
319
|
+
login Create a key through the Seedyn website
|
|
282
320
|
set [api-key] Store a key with owner-only permissions
|
|
283
321
|
status Show config and redacted credential status
|
|
284
322
|
remove Remove the stored API key
|
|
285
323
|
|
|
286
|
-
|
|
324
|
+
Run "seedyn auth login" for the guided browser flow.`;
|
|
325
|
+
|
|
326
|
+
const AUTH_LOGIN_HELP = `Usage: seedyn auth login [options]
|
|
327
|
+
|
|
328
|
+
Options:
|
|
329
|
+
--api-url <origin> Use and save a custom Seedyn application origin
|
|
330
|
+
--no-open Print the approval URL without opening a browser
|
|
331
|
+
-h, --help Show this help`;
|
|
287
332
|
|
|
288
333
|
const AUTH_SET_HELP = `Usage: seedyn auth set [api-key] [options]
|
|
289
334
|
|
package/src/config.mjs
CHANGED
|
@@ -112,7 +112,7 @@ export async function resolveAuthentication(
|
|
|
112
112
|
options.apiKey || environment.SEEDYN_API_KEY || config.apiKey;
|
|
113
113
|
if (!candidate) {
|
|
114
114
|
throw new CliError(
|
|
115
|
-
"Missing Seedyn API key. Run `seedyn auth
|
|
115
|
+
"Missing Seedyn API key. Run `seedyn auth login`, `seedyn auth set`, or set SEEDYN_API_KEY.",
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
return { apiKey: validateApiKey(candidate), apiUrl };
|