ymmv-cli 0.1.3 → 0.1.5
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/cli.js +111 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,13 +11,22 @@ async function mintYmmvToken(accessToken) {
|
|
|
11
11
|
const res = await fetch(`${BASE}/api/v1/auth/token`, {
|
|
12
12
|
method: "POST",
|
|
13
13
|
headers: { "content-type": "application/json" },
|
|
14
|
-
body: JSON.stringify({ access_token: accessToken })
|
|
14
|
+
body: JSON.stringify({ access_token: accessToken }),
|
|
15
|
+
// Never follow a redirect: a 30x must fail (the existing `!res.ok` guard rejects the resulting
|
|
16
|
+
// opaqueredirect), not re-POST the GitHub access_token to the redirect target or read a
|
|
17
|
+
// redirected 200 as a successful mint. Mirrors publish/delete in api.ts.
|
|
18
|
+
redirect: "manual"
|
|
15
19
|
});
|
|
16
20
|
if (!res.ok) {
|
|
17
21
|
const body = await res.json().catch(() => ({}));
|
|
18
22
|
if (res.status === 503) {
|
|
19
23
|
throw new Error(body.message ?? "GitHub is unavailable \u2014 run `ymmv login` again shortly.");
|
|
20
24
|
}
|
|
25
|
+
if (res.status === 429) {
|
|
26
|
+
const retry = res.headers.get("retry-after");
|
|
27
|
+
const msg = body.message ?? "too many login attempts \u2014 slow down and try again shortly";
|
|
28
|
+
throw new Error(retry ? `${msg} (retry in ${retry}s)` : msg);
|
|
29
|
+
}
|
|
21
30
|
throw new Error(`login failed: ${res.status} ${body.error ?? ""}`.trim());
|
|
22
31
|
}
|
|
23
32
|
return await res.json();
|
|
@@ -25,7 +34,10 @@ async function mintYmmvToken(accessToken) {
|
|
|
25
34
|
async function revokeYmmvToken(token) {
|
|
26
35
|
const res = await fetch(`${BASE}/api/v1/auth/logout`, {
|
|
27
36
|
method: "POST",
|
|
28
|
-
headers: { authorization: `Bearer ${token}` }
|
|
37
|
+
headers: { authorization: `Bearer ${token}` },
|
|
38
|
+
// Never follow a redirect: a 30x→200 must not read as a successful revoke (which would delete the
|
|
39
|
+
// local file while the server token stays live). Same guard as mint + publish/delete.
|
|
40
|
+
redirect: "manual"
|
|
29
41
|
});
|
|
30
42
|
if (!res.ok) throw new Error(`logout failed: ${res.status}`);
|
|
31
43
|
const body = await res.json().catch(() => ({}));
|
|
@@ -104,6 +116,77 @@ function diff(mine, theirs) {
|
|
|
104
116
|
};
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
// ../shared/dist/github.js
|
|
120
|
+
var GITHUB_CLIENT_ID = "Ov23liMoD29eizQcN1KZ";
|
|
121
|
+
|
|
122
|
+
// ../shared/dist/types.js
|
|
123
|
+
var SCHEMA_VERSION = 1;
|
|
124
|
+
|
|
125
|
+
// ../shared/dist/parse.js
|
|
126
|
+
var MAX_PARSE_ENTRIES = 256;
|
|
127
|
+
var MAX_PARSE_EXTRAS = 256;
|
|
128
|
+
var MAX_PARSE_VALUE = 4096;
|
|
129
|
+
var MAX_PARSE_LABEL = 4096;
|
|
130
|
+
var ProfileParseError = class extends Error {
|
|
131
|
+
constructor(message) {
|
|
132
|
+
super(message);
|
|
133
|
+
this.name = "ProfileParseError";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function isRecord(value) {
|
|
137
|
+
return typeof value === "object" && value !== null;
|
|
138
|
+
}
|
|
139
|
+
function parseProfile(raw) {
|
|
140
|
+
if (!isRecord(raw))
|
|
141
|
+
throw new ProfileParseError("profile is not an object");
|
|
142
|
+
if (raw.schema_version !== SCHEMA_VERSION) {
|
|
143
|
+
throw new ProfileParseError(`unsupported schema_version: ${String(raw.schema_version)}`);
|
|
144
|
+
}
|
|
145
|
+
if (typeof raw.handle !== "string")
|
|
146
|
+
throw new ProfileParseError("handle is not a string");
|
|
147
|
+
if (typeof raw.updated_at !== "string")
|
|
148
|
+
throw new ProfileParseError("updated_at is not a string");
|
|
149
|
+
if (!Array.isArray(raw.entries))
|
|
150
|
+
throw new ProfileParseError("entries is not an array");
|
|
151
|
+
if (raw.entries.length > MAX_PARSE_ENTRIES) {
|
|
152
|
+
throw new ProfileParseError(`too many entries (>${MAX_PARSE_ENTRIES})`);
|
|
153
|
+
}
|
|
154
|
+
const entries = [];
|
|
155
|
+
for (let i = 0; i < raw.entries.length; i++) {
|
|
156
|
+
const entry = raw.entries[i];
|
|
157
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.value !== "string") {
|
|
158
|
+
throw new ProfileParseError(`entry ${i} is not {key,value} strings`);
|
|
159
|
+
}
|
|
160
|
+
if (entry.value.length > MAX_PARSE_VALUE) {
|
|
161
|
+
throw new ProfileParseError(`entry ${i} value exceeds ${MAX_PARSE_VALUE} chars`);
|
|
162
|
+
}
|
|
163
|
+
entries.push({ key: entry.key, value: entry.value });
|
|
164
|
+
}
|
|
165
|
+
if (!Array.isArray(raw.extras))
|
|
166
|
+
throw new ProfileParseError("extras is not an array");
|
|
167
|
+
if (raw.extras.length > MAX_PARSE_EXTRAS) {
|
|
168
|
+
throw new ProfileParseError(`too many extras (>${MAX_PARSE_EXTRAS})`);
|
|
169
|
+
}
|
|
170
|
+
const extras = [];
|
|
171
|
+
for (let i = 0; i < raw.extras.length; i++) {
|
|
172
|
+
const extra = raw.extras[i];
|
|
173
|
+
if (!isRecord(extra) || typeof extra.label !== "string" || typeof extra.value !== "string") {
|
|
174
|
+
throw new ProfileParseError(`extra ${i} is not {label,value} strings`);
|
|
175
|
+
}
|
|
176
|
+
if (extra.label.length > MAX_PARSE_LABEL || extra.value.length > MAX_PARSE_VALUE) {
|
|
177
|
+
throw new ProfileParseError(`extra ${i} exceeds the length ceiling`);
|
|
178
|
+
}
|
|
179
|
+
extras.push({ label: extra.label, value: extra.value });
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
schema_version: SCHEMA_VERSION,
|
|
183
|
+
handle: raw.handle,
|
|
184
|
+
entries,
|
|
185
|
+
extras,
|
|
186
|
+
updated_at: raw.updated_at
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
107
190
|
// ../shared/dist/reserved.js
|
|
108
191
|
var RESERVED_ROUTES = ["api", "login", "logout"];
|
|
109
192
|
var CLI_VERBS = ["login", "logout", "set", "delete", "view", "help"];
|
|
@@ -114,9 +197,6 @@ function isValidHandle(handle) {
|
|
|
114
197
|
return handle.length >= 1 && handle.length <= 39 && HANDLE_RE.test(handle);
|
|
115
198
|
}
|
|
116
199
|
|
|
117
|
-
// ../shared/dist/types.js
|
|
118
|
-
var SCHEMA_VERSION = 1;
|
|
119
|
-
|
|
120
200
|
// src/token-store.ts
|
|
121
201
|
import { randomUUID } from "crypto";
|
|
122
202
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
@@ -127,7 +207,10 @@ function tokenFilePath() {
|
|
|
127
207
|
}
|
|
128
208
|
async function saveToken(data) {
|
|
129
209
|
const path = tokenFilePath();
|
|
130
|
-
|
|
210
|
+
const dir = dirname(path);
|
|
211
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
212
|
+
if (process.platform !== "win32") await chmod(dir, 448).catch(() => {
|
|
213
|
+
});
|
|
131
214
|
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
132
215
|
const stored = { base: BASE, token: data.token, handle: data.handle };
|
|
133
216
|
try {
|
|
@@ -169,7 +252,6 @@ async function peekBase() {
|
|
|
169
252
|
}
|
|
170
253
|
|
|
171
254
|
// src/device-flow.ts
|
|
172
|
-
var CLIENT_ID = "Ov23liMoD29eizQcN1KZ";
|
|
173
255
|
var DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
174
256
|
var TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
175
257
|
var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -178,7 +260,7 @@ async function requestDeviceCode(deps = {}) {
|
|
|
178
260
|
const res = await doFetch(DEVICE_CODE_URL, {
|
|
179
261
|
method: "POST",
|
|
180
262
|
headers: { accept: "application/json" },
|
|
181
|
-
body: new URLSearchParams({ client_id:
|
|
263
|
+
body: new URLSearchParams({ client_id: GITHUB_CLIENT_ID })
|
|
182
264
|
});
|
|
183
265
|
if (!res.ok) {
|
|
184
266
|
throw new Error(`device code request failed: ${res.status} ${await res.text()}`);
|
|
@@ -191,18 +273,36 @@ async function pollForToken(dc, deps = {}) {
|
|
|
191
273
|
const now = deps.now ?? Date.now;
|
|
192
274
|
let interval = dc.interval || 5;
|
|
193
275
|
const deadline = now() + dc.expires_in * 1e3;
|
|
276
|
+
let transientFailures = 0;
|
|
277
|
+
const MAX_TRANSIENT_FAILURES = 5;
|
|
194
278
|
while (now() < deadline) {
|
|
195
279
|
await sleep(interval * 1e3);
|
|
196
280
|
const res = await doFetch(TOKEN_URL, {
|
|
197
281
|
method: "POST",
|
|
198
282
|
headers: { accept: "application/json" },
|
|
199
283
|
body: new URLSearchParams({
|
|
200
|
-
client_id:
|
|
284
|
+
client_id: GITHUB_CLIENT_ID,
|
|
201
285
|
device_code: dc.device_code,
|
|
202
286
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
203
287
|
})
|
|
204
288
|
});
|
|
205
|
-
|
|
289
|
+
let tok;
|
|
290
|
+
if (res.ok) {
|
|
291
|
+
try {
|
|
292
|
+
tok = await res.json();
|
|
293
|
+
} catch {
|
|
294
|
+
tok = void 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (tok === void 0) {
|
|
298
|
+
if (++transientFailures >= MAX_TRANSIENT_FAILURES) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"GitHub isn't responding to the login poll \u2014 check your connection and run `ymmv login` again."
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
transientFailures = 0;
|
|
206
306
|
if (tok.access_token) return tok.access_token;
|
|
207
307
|
switch (tok.error) {
|
|
208
308
|
case "authorization_pending":
|
|
@@ -301,7 +401,7 @@ async function fetchProfileJson(handle) {
|
|
|
301
401
|
if (!res.ok) {
|
|
302
402
|
throw new Error(`fetch failed: ${res.status} ${await res.text()}`);
|
|
303
403
|
}
|
|
304
|
-
return await res.json();
|
|
404
|
+
return parseProfile(await res.json());
|
|
305
405
|
}
|
|
306
406
|
async function deleteProfile() {
|
|
307
407
|
const cred = await ensureLogin();
|