ymmv-cli 0.1.4 → 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 +101 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,11 @@ 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(() => ({}));
|
|
@@ -30,7 +34,10 @@ async function mintYmmvToken(accessToken) {
|
|
|
30
34
|
async function revokeYmmvToken(token) {
|
|
31
35
|
const res = await fetch(`${BASE}/api/v1/auth/logout`, {
|
|
32
36
|
method: "POST",
|
|
33
|
-
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"
|
|
34
41
|
});
|
|
35
42
|
if (!res.ok) throw new Error(`logout failed: ${res.status}`);
|
|
36
43
|
const body = await res.json().catch(() => ({}));
|
|
@@ -112,6 +119,74 @@ function diff(mine, theirs) {
|
|
|
112
119
|
// ../shared/dist/github.js
|
|
113
120
|
var GITHUB_CLIENT_ID = "Ov23liMoD29eizQcN1KZ";
|
|
114
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
|
+
|
|
115
190
|
// ../shared/dist/reserved.js
|
|
116
191
|
var RESERVED_ROUTES = ["api", "login", "logout"];
|
|
117
192
|
var CLI_VERBS = ["login", "logout", "set", "delete", "view", "help"];
|
|
@@ -122,9 +197,6 @@ function isValidHandle(handle) {
|
|
|
122
197
|
return handle.length >= 1 && handle.length <= 39 && HANDLE_RE.test(handle);
|
|
123
198
|
}
|
|
124
199
|
|
|
125
|
-
// ../shared/dist/types.js
|
|
126
|
-
var SCHEMA_VERSION = 1;
|
|
127
|
-
|
|
128
200
|
// src/token-store.ts
|
|
129
201
|
import { randomUUID } from "crypto";
|
|
130
202
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
@@ -135,7 +207,10 @@ function tokenFilePath() {
|
|
|
135
207
|
}
|
|
136
208
|
async function saveToken(data) {
|
|
137
209
|
const path = tokenFilePath();
|
|
138
|
-
|
|
210
|
+
const dir = dirname(path);
|
|
211
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
212
|
+
if (process.platform !== "win32") await chmod(dir, 448).catch(() => {
|
|
213
|
+
});
|
|
139
214
|
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
140
215
|
const stored = { base: BASE, token: data.token, handle: data.handle };
|
|
141
216
|
try {
|
|
@@ -198,6 +273,8 @@ async function pollForToken(dc, deps = {}) {
|
|
|
198
273
|
const now = deps.now ?? Date.now;
|
|
199
274
|
let interval = dc.interval || 5;
|
|
200
275
|
const deadline = now() + dc.expires_in * 1e3;
|
|
276
|
+
let transientFailures = 0;
|
|
277
|
+
const MAX_TRANSIENT_FAILURES = 5;
|
|
201
278
|
while (now() < deadline) {
|
|
202
279
|
await sleep(interval * 1e3);
|
|
203
280
|
const res = await doFetch(TOKEN_URL, {
|
|
@@ -209,7 +286,23 @@ async function pollForToken(dc, deps = {}) {
|
|
|
209
286
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
210
287
|
})
|
|
211
288
|
});
|
|
212
|
-
|
|
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;
|
|
213
306
|
if (tok.access_token) return tok.access_token;
|
|
214
307
|
switch (tok.error) {
|
|
215
308
|
case "authorization_pending":
|
|
@@ -308,7 +401,7 @@ async function fetchProfileJson(handle) {
|
|
|
308
401
|
if (!res.ok) {
|
|
309
402
|
throw new Error(`fetch failed: ${res.status} ${await res.text()}`);
|
|
310
403
|
}
|
|
311
|
-
return await res.json();
|
|
404
|
+
return parseProfile(await res.json());
|
|
312
405
|
}
|
|
313
406
|
async function deleteProfile() {
|
|
314
407
|
const cred = await ensureLogin();
|