@clipdone/cli 0.1.0 → 0.1.2
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 +6 -6
- package/bin/clipdone.js +46 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@ Install globally for normal use:
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install -g @clipdone/cli
|
|
11
|
-
clipdone
|
|
11
|
+
clipdone login
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
For one-off use without a global install:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @clipdone/cli
|
|
17
|
+
npx @clipdone/cli login
|
|
18
18
|
pnpm dlx @clipdone/cli projects
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -25,21 +25,21 @@ The CLI requires Node.js 18 or newer.
|
|
|
25
25
|
Authorize the CLI in your browser:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
clipdone
|
|
28
|
+
clipdone login
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
On remote machines or environments where the CLI cannot open a browser automatically:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
clipdone
|
|
34
|
+
clipdone login --no-open
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Check or remove the current login:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
clipdone me
|
|
41
|
-
clipdone
|
|
42
|
-
clipdone
|
|
41
|
+
clipdone logout
|
|
42
|
+
clipdone revoke
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
`logout` removes only the local token. `revoke` also revokes the current OAuth tokens with ClipDone.
|
package/bin/clipdone.js
CHANGED
|
@@ -148,6 +148,8 @@ var OAUTH_AUTHORIZE_PATH = "/api/auth/oauth2/authorize";
|
|
|
148
148
|
var OAUTH_REGISTER_PATH = "/api/auth/oauth2/register";
|
|
149
149
|
var OAUTH_TOKEN_PATH = "/api/auth/oauth2/token";
|
|
150
150
|
var OAUTH_REVOKE_PATH = "/api/auth/oauth2/revoke";
|
|
151
|
+
var OAUTH_CLIENT_REGISTRATION_VERSION = 2;
|
|
152
|
+
var OAUTH_LOOPBACK_PORTS = [59852, 59853, 59854, 59855, 59856, 59857, 59858, 59859];
|
|
151
153
|
var PRODUCTION_APP_URL = "https://app.clipdone.app";
|
|
152
154
|
var LOCAL_APP_URL = "http://localhost:3000";
|
|
153
155
|
var REQUEST_TIMEOUT_MS = 15e3;
|
|
@@ -239,16 +241,25 @@ function clearStoredAuth() {
|
|
|
239
241
|
delete config.token;
|
|
240
242
|
delete config.tokenExpiresAt;
|
|
241
243
|
delete config.oauth;
|
|
244
|
+
delete config.oauthClient;
|
|
245
|
+
writeConfig(config);
|
|
246
|
+
}
|
|
247
|
+
function clearStoredOAuthClientRegistration() {
|
|
248
|
+
const config = readConfig();
|
|
249
|
+
if (!config.oauthClient) return;
|
|
250
|
+
delete config.oauthClient;
|
|
242
251
|
writeConfig(config);
|
|
243
252
|
}
|
|
244
253
|
function readStoredOAuthClientRegistration(scope, resource) {
|
|
245
254
|
const registration = readConfig().oauthClient;
|
|
246
255
|
if (!registration || typeof registration !== "object") return null;
|
|
256
|
+
const version = Number(registration.version);
|
|
247
257
|
const clientId = normalizeString(registration.clientId);
|
|
248
258
|
const apiUrl = normalizeString(registration.apiUrl);
|
|
249
259
|
const storedScope = normalizeString(registration.scope);
|
|
250
260
|
const storedResource = normalizeString(registration.resource);
|
|
251
261
|
if (!clientId || !apiUrl || !storedScope || !storedResource) return null;
|
|
262
|
+
if (version !== OAUTH_CLIENT_REGISTRATION_VERSION) return null;
|
|
252
263
|
if (apiUrl !== apiBase()) return null;
|
|
253
264
|
if (storedScope !== normalizeString(scope)) return null;
|
|
254
265
|
if (storedResource !== normalizeString(resource)) return null;
|
|
@@ -261,6 +272,7 @@ function writeStoredOAuthClientRegistration(clientId, scope, resource) {
|
|
|
261
272
|
if (!normalizedClientId || !normalizedScope || !normalizedResource) return;
|
|
262
273
|
const config = readConfig();
|
|
263
274
|
config.oauthClient = {
|
|
275
|
+
version: OAUTH_CLIENT_REGISTRATION_VERSION,
|
|
264
276
|
clientId: normalizedClientId,
|
|
265
277
|
apiUrl: apiBase(),
|
|
266
278
|
scope: normalizedScope,
|
|
@@ -525,7 +537,7 @@ async function oauthRegisterClient({ scope }) {
|
|
|
525
537
|
},
|
|
526
538
|
body: JSON.stringify({
|
|
527
539
|
client_name: "ClipDone CLI",
|
|
528
|
-
redirect_uris:
|
|
540
|
+
redirect_uris: OAUTH_LOOPBACK_PORTS.map((port) => `http://127.0.0.1:${port}/callback`),
|
|
529
541
|
grant_types: ["authorization_code", "refresh_token"],
|
|
530
542
|
response_types: ["code"],
|
|
531
543
|
token_endpoint_auth_method: "none",
|
|
@@ -553,10 +565,14 @@ async function oauthRegisterClient({ scope }) {
|
|
|
553
565
|
return { clientId };
|
|
554
566
|
}
|
|
555
567
|
async function resolveOAuthClient(authConfig) {
|
|
568
|
+
const activeSession = readStoredOAuthSession();
|
|
556
569
|
const cached = readStoredOAuthClientRegistration(authConfig?.scope, authConfig?.resource);
|
|
557
|
-
if (cached) {
|
|
570
|
+
if (cached && activeSession?.clientId === cached.clientId) {
|
|
558
571
|
return cached;
|
|
559
572
|
}
|
|
573
|
+
if (cached) {
|
|
574
|
+
clearStoredOAuthClientRegistration();
|
|
575
|
+
}
|
|
560
576
|
const registered = await oauthRegisterClient({ scope: authConfig.scope });
|
|
561
577
|
writeStoredOAuthClientRegistration(registered.clientId, authConfig.scope, authConfig.resource);
|
|
562
578
|
return registered;
|
|
@@ -984,10 +1000,10 @@ function printHelp() {
|
|
|
984
1000
|
writeLine("Usage: clipdone <command> [options]");
|
|
985
1001
|
writeLine();
|
|
986
1002
|
writeLine("Preferred commands:");
|
|
987
|
-
writeLine("
|
|
1003
|
+
writeLine(" login Authorize this CLI in your browser");
|
|
988
1004
|
writeLine(" me Show the current CLI login");
|
|
989
|
-
writeLine("
|
|
990
|
-
writeLine("
|
|
1005
|
+
writeLine(" logout Remove the local login only");
|
|
1006
|
+
writeLine(" revoke Revoke the current CLI login");
|
|
991
1007
|
writeLine(" Login options: --no-open --print-auth-url");
|
|
992
1008
|
writeLine(" projects List projects");
|
|
993
1009
|
writeLine(" projects create [--name <name>] Create a project");
|
|
@@ -1024,7 +1040,7 @@ function printHelp() {
|
|
|
1024
1040
|
writeLine(' clipdone projects <id> upload "/path/with spaces/video (1).mp4" --type footage');
|
|
1025
1041
|
writeLine(" clipdone projects <id> process");
|
|
1026
1042
|
writeLine(" clipdone projects <id> status");
|
|
1027
|
-
writeLine(" clipdone
|
|
1043
|
+
writeLine(" clipdone login --no-open");
|
|
1028
1044
|
writeLine();
|
|
1029
1045
|
writeLine("Useful options:");
|
|
1030
1046
|
writeLine(" --help Show command help");
|
|
@@ -1097,17 +1113,36 @@ async function generateCodeChallenge(verifier) {
|
|
|
1097
1113
|
const digest = await webcrypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
1098
1114
|
return base64UrlEncode(new Uint8Array(digest));
|
|
1099
1115
|
}
|
|
1116
|
+
async function listenOnLoopbackPort() {
|
|
1117
|
+
const errors = [];
|
|
1118
|
+
for (const port of OAUTH_LOOPBACK_PORTS) {
|
|
1119
|
+
const server = http.createServer();
|
|
1120
|
+
try {
|
|
1121
|
+
await new Promise((resolve, reject) => {
|
|
1122
|
+
server.once("error", reject);
|
|
1123
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
1124
|
+
});
|
|
1125
|
+
return { server, port };
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
server.close();
|
|
1128
|
+
const code = typeof error?.code === "string" ? error.code : null;
|
|
1129
|
+
if (code === "EADDRINUSE" || code === "EACCES") {
|
|
1130
|
+
errors.push(`${port}:${code}`);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
const detail = errors.length > 0 ? ` Tried ${errors.join(", ")}.` : "";
|
|
1137
|
+
throw new Error(`Could not reserve a local callback port for ClipDone login.${detail}`);
|
|
1138
|
+
}
|
|
1100
1139
|
async function login(args) {
|
|
1101
1140
|
const authConfig = { resource: API_OAUTH_RESOURCE, scope: API_OAUTH_SCOPES.join(" ") };
|
|
1102
1141
|
const oauthClient = await resolveOAuthClient(authConfig);
|
|
1103
1142
|
const state = randomState();
|
|
1104
1143
|
const codeVerifier = generateCodeVerifier();
|
|
1105
1144
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
1106
|
-
const server =
|
|
1107
|
-
const port = await new Promise((resolve, reject) => {
|
|
1108
|
-
server.once("error", reject);
|
|
1109
|
-
server.listen(0, "127.0.0.1", () => resolve(server.address().port));
|
|
1110
|
-
});
|
|
1145
|
+
const { server, port } = await listenOnLoopbackPort();
|
|
1111
1146
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1112
1147
|
const authUrl = new URL(OAUTH_AUTHORIZE_PATH, appBase());
|
|
1113
1148
|
authUrl.searchParams.set("client_id", oauthClient.clientId);
|