passalong 0.20.0 → 0.21.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/package.json +1 -1
- package/src/api.js +24 -10
- package/src/passalong.js +59 -15
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -120,6 +120,19 @@ export function loggedIn() {
|
|
|
120
120
|
return Boolean(token());
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Every login this machine holds, as `{ token, api }` for a call's `as`: each kept account and the
|
|
125
|
+
* one in use, once each. For work that belongs to all of them, like `tidy`, rather than to one.
|
|
126
|
+
*/
|
|
127
|
+
export function logins() {
|
|
128
|
+
const all = Object.values(readAccounts())
|
|
129
|
+
.filter((a) => a.token)
|
|
130
|
+
.map((a) => ({ token: a.token, api: a.api || readConfig().api || DEFAULT_API }));
|
|
131
|
+
if (token()) all.push({ token: token(), api: baseUrl() });
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
return all.filter((a) => !seen.has(`${a.token} ${a.api}`) && seen.add(`${a.token} ${a.api}`));
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
/** The team this account works in. It belongs to the account, not the machine. */
|
|
124
137
|
export function currentTeam() {
|
|
125
138
|
const p = picked();
|
|
@@ -149,12 +162,12 @@ export { nameFor };
|
|
|
149
162
|
|
|
150
163
|
async function call(
|
|
151
164
|
path,
|
|
152
|
-
{ method = "GET", body, auth = true, raw = false, uploadName = "" } = {},
|
|
165
|
+
{ method = "GET", body, auth = true, raw = false, uploadName = "", as = null } = {},
|
|
153
166
|
) {
|
|
154
167
|
const headers = { ...CLIENT };
|
|
155
168
|
if (auth) {
|
|
156
|
-
if (needsChoice()) throw new ApiError(409, choiceMessage());
|
|
157
|
-
const t = token();
|
|
169
|
+
if (!as && needsChoice()) throw new ApiError(409, choiceMessage());
|
|
170
|
+
const t = as ? as.token : token();
|
|
158
171
|
if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
|
|
159
172
|
headers.authorization = `Bearer ${t}`;
|
|
160
173
|
}
|
|
@@ -166,9 +179,10 @@ async function call(
|
|
|
166
179
|
: "application/json";
|
|
167
180
|
}
|
|
168
181
|
if (uploadName) headers["x-file-name"] = uploadName.replace(/[^\x20-\x7e]/g, "").slice(0, 120);
|
|
182
|
+
const base = as ? as.api.replace(/\/$/, "") : baseUrl();
|
|
169
183
|
let res;
|
|
170
184
|
try {
|
|
171
|
-
res = await fetch(`${
|
|
185
|
+
res = await fetch(`${base}${path}`, {
|
|
172
186
|
method,
|
|
173
187
|
headers,
|
|
174
188
|
body:
|
|
@@ -179,7 +193,7 @@ async function call(
|
|
|
179
193
|
: JSON.stringify(body),
|
|
180
194
|
});
|
|
181
195
|
} catch (err) {
|
|
182
|
-
throw new ApiError(0, `could not reach ${
|
|
196
|
+
throw new ApiError(0, `could not reach ${base} (${err.message})`);
|
|
183
197
|
}
|
|
184
198
|
if (!res.ok) {
|
|
185
199
|
const text = await res.text();
|
|
@@ -379,10 +393,10 @@ export const inbox = () => call("/v1/inbox");
|
|
|
379
393
|
export const createReport = (body) => call("/v1/reports", { method: "POST", body });
|
|
380
394
|
/** One report and the issues filed under it, grouped by product area. */
|
|
381
395
|
export const report = (id) => call(`/v1/reports/${encodeURIComponent(id)}`);
|
|
382
|
-
export const get = (id, { peek = false } = {}) =>
|
|
383
|
-
call(`/v1/guides/${id}${q({ peek: peek ? "1" : "" })}`, { raw: true });
|
|
396
|
+
export const get = (id, { peek = false, as = null } = {}) =>
|
|
397
|
+
call(`/v1/guides/${id}${q({ peek: peek ? "1" : "" })}`, { raw: true, as });
|
|
384
398
|
/** One page of every guide this account can read, for `export`. Records no pull. */
|
|
385
|
-
export const exportPage = (after = "") => call(`/v1/export${q({ after })}
|
|
399
|
+
export const exportPage = (after = "", as = null) => call(`/v1/export${q({ after })}`, { as });
|
|
386
400
|
/**
|
|
387
401
|
* A guide's follow-ups, one level down. `markdown` adds each one's content, oldest first and
|
|
388
402
|
* capped server-side; neither form counts as opening them.
|
|
@@ -485,7 +499,7 @@ export async function uploadFile(bytes, type, name = "") {
|
|
|
485
499
|
try {
|
|
486
500
|
res = await fetch(`${baseUrl()}/v1/attachments`, { method: "POST", headers, body: bytes });
|
|
487
501
|
} catch (err) {
|
|
488
|
-
throw new ApiError(0, `could not reach ${
|
|
502
|
+
throw new ApiError(0, `could not reach ${base} (${err.message})`);
|
|
489
503
|
}
|
|
490
504
|
if (!res.ok) {
|
|
491
505
|
const body = await res.text();
|
|
@@ -509,7 +523,7 @@ export async function uploadShot(bytes, type, name = "") {
|
|
|
509
523
|
try {
|
|
510
524
|
res = await fetch(`${baseUrl()}/v1/shots`, { method: "POST", headers, body: bytes });
|
|
511
525
|
} catch (err) {
|
|
512
|
-
throw new ApiError(0, `could not reach ${
|
|
526
|
+
throw new ApiError(0, `could not reach ${base} (${err.message})`);
|
|
513
527
|
}
|
|
514
528
|
if (!res.ok) {
|
|
515
529
|
const body = await res.text();
|
package/src/passalong.js
CHANGED
|
@@ -1181,26 +1181,70 @@ export async function exportAll(dir) {
|
|
|
1181
1181
|
});
|
|
1182
1182
|
}
|
|
1183
1183
|
|
|
1184
|
+
// The host this product answered on before passalong.dev. It serves the same data, so a copy
|
|
1185
|
+
// whose url names it belongs to the default API.
|
|
1186
|
+
const OLD_HOST = "passalong.kreativekorna.com";
|
|
1187
|
+
const hostOf = (url) => {
|
|
1188
|
+
try {
|
|
1189
|
+
const { host, hostname } = new URL(url);
|
|
1190
|
+
if (["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return "";
|
|
1191
|
+
return host === OLD_HOST ? new URL(api.DEFAULT_API).host : host;
|
|
1192
|
+
} catch {
|
|
1193
|
+
return "";
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
// Raised whenever tidy learns to remove something it used to keep, so machines that tidied run it once more.
|
|
1197
|
+
const TIDIED = 2;
|
|
1198
|
+
|
|
1184
1199
|
/**
|
|
1185
1200
|
* Once per machine, signed in: drop the local copies the server says it holds. They were written
|
|
1186
|
-
* by every share, pull and take before `keep()`, and nothing ever removed one.
|
|
1187
|
-
*
|
|
1201
|
+
* by every share, pull and take before `keep()`, and nothing ever removed one. Every login on the
|
|
1202
|
+
* machine is asked, since a copy may be any of theirs. A copy whose `url:` is on a login's own host
|
|
1203
|
+
* and that the server answers 404 for is a guide since deleted, and goes too. Anything else — no
|
|
1204
|
+
* url, localhost, another host, or an answer that was neither 200 nor 404 — is left where it is,
|
|
1205
|
+
* and an export that failed leaves the flag unset so the next start tries again.
|
|
1188
1206
|
*/
|
|
1189
1207
|
export async function tidy() {
|
|
1190
|
-
|
|
1191
|
-
|
|
1208
|
+
const logins = api.logins();
|
|
1209
|
+
if (!logins.length || store.readConfig().tidied === TIDIED) return 0;
|
|
1210
|
+
const local = new Map(store.list().map((g) => [g.meta.id, hostOf(g.meta.url)]));
|
|
1192
1211
|
let removed = 0;
|
|
1193
|
-
let
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1212
|
+
let sure = true;
|
|
1213
|
+
const drop = (id) => {
|
|
1214
|
+
if (!local.delete(id)) return;
|
|
1215
|
+
store.remove(id);
|
|
1216
|
+
removed++;
|
|
1217
|
+
};
|
|
1218
|
+
for (const as of logins) {
|
|
1219
|
+
try {
|
|
1220
|
+
let after = "";
|
|
1221
|
+
do {
|
|
1222
|
+
const page = await api.exportPage(after, as);
|
|
1223
|
+
for (const g of page.guides) drop(g.id);
|
|
1224
|
+
after = page.next;
|
|
1225
|
+
} while (after && local.size);
|
|
1226
|
+
} catch {
|
|
1227
|
+
sure = false;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const said = new Map();
|
|
1231
|
+
for (const as of logins) {
|
|
1232
|
+
const host = hostOf(as.api);
|
|
1233
|
+
const ids = [...local].filter(([, h]) => h && h === host).map(([id]) => id);
|
|
1234
|
+
// Eight at a time: a few hundred copies is a few seconds, not a few hundred open requests.
|
|
1235
|
+
for (let i = 0; i < ids.length; i += 8)
|
|
1236
|
+
await Promise.all(
|
|
1237
|
+
ids.slice(i, i + 8).map(async (id) => {
|
|
1238
|
+
const status = await api.get(id, { peek: true, as }).then(
|
|
1239
|
+
() => 200,
|
|
1240
|
+
(err) => err.status ?? 0,
|
|
1241
|
+
);
|
|
1242
|
+
said.set(id, [...(said.get(id) ?? []), status]);
|
|
1243
|
+
}),
|
|
1244
|
+
);
|
|
1203
1245
|
}
|
|
1204
|
-
|
|
1246
|
+
for (const [id, statuses] of said)
|
|
1247
|
+
if (statuses.includes(200) || statuses.every((s) => s === 404)) drop(id);
|
|
1248
|
+
if (sure) store.writeConfig({ tidied: TIDIED });
|
|
1205
1249
|
return removed;
|
|
1206
1250
|
}
|