@pipeup/share 0.0.0-stage → 0.5.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/dist/cli.js ADDED
@@ -0,0 +1,1196 @@
1
+ #!/usr/bin/env node
2
+
3
+ // share/src/cli.ts
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+
6
+ // kit/src/encoding.ts
7
+ var utf8 = (text2) => new TextEncoder().encode(text2);
8
+ var text = (bytes) => new TextDecoder().decode(bytes);
9
+ function toB64u(bytes) {
10
+ let s = "";
11
+ for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
12
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
13
+ }
14
+ function fromB64u(s) {
15
+ const std = s.replace(/-/g, "+").replace(/_/g, "/");
16
+ const bin = atob(std + "=".repeat((4 - std.length % 4) % 4));
17
+ return Uint8Array.from(bin, (c) => c.charCodeAt(0));
18
+ }
19
+ function toB64(bytes) {
20
+ let s = "";
21
+ for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
22
+ return btoa(s);
23
+ }
24
+ function fromB64(s) {
25
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
26
+ }
27
+ var randomBytes = (n) => crypto.getRandomValues(new Uint8Array(n));
28
+ async function sha256(data) {
29
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
30
+ }
31
+
32
+ // kit/src/derive.ts
33
+ var salt = (docId) => utf8(`pipeup/v1/doc:${docId}`);
34
+ async function hkdf(ikm, docId, info, bits) {
35
+ const base = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
36
+ return crypto.subtle.deriveBits(
37
+ { name: "HKDF", hash: "SHA-256", salt: salt(docId), info: utf8(info) },
38
+ base,
39
+ bits
40
+ );
41
+ }
42
+ function ladder(docId, room) {
43
+ return {
44
+ docId,
45
+ key: async (p2) => crypto.subtle.importKey("raw", await hkdf(room, docId, `pipeup/v1/${p2}/key`, 256), "AES-GCM", false, [
46
+ "encrypt",
47
+ "decrypt"
48
+ ]),
49
+ addr: async (p2) => toB64u(new Uint8Array(await hkdf(room, docId, `pipeup/v1/${p2}/address`, 128))),
50
+ aad: (p2) => `pipeup/v1/${p2}:${docId}`
51
+ };
52
+ }
53
+
54
+ // kit/src/envelope.ts
55
+ var MAX_INFLATED = 1e6;
56
+ var MAX_OPS = 1e3;
57
+ async function pipe(data, t2, cap = Infinity) {
58
+ const reader = new Blob([data]).stream().pipeThrough(t2).getReader();
59
+ const chunks2 = [];
60
+ let total = 0;
61
+ for (; ; ) {
62
+ const { done, value } = await reader.read();
63
+ if (done) break;
64
+ total += value.length;
65
+ if (total > cap) {
66
+ void reader.cancel();
67
+ throw new Error("too large");
68
+ }
69
+ chunks2.push(value);
70
+ }
71
+ const out = new Uint8Array(total);
72
+ let at = 0;
73
+ for (const c of chunks2) {
74
+ out.set(c, at);
75
+ at += c.length;
76
+ }
77
+ return out;
78
+ }
79
+ var seal = (l, p2, ops) => sealJson(l, p2, { pipeup: 1, doc: l.docId, sealed: false, ops });
80
+ async function sealJson(l, p2, value) {
81
+ const plain = utf8(JSON.stringify(value));
82
+ const iv = randomBytes(12);
83
+ const body = await crypto.subtle.encrypt(
84
+ { name: "AES-GCM", iv, additionalData: utf8(l.aad(p2)) },
85
+ await l.key(p2),
86
+ await pipe(plain, new CompressionStream("deflate-raw"))
87
+ );
88
+ const out = new Uint8Array(13 + body.byteLength);
89
+ out[0] = 1;
90
+ out.set(iv, 1);
91
+ out.set(new Uint8Array(body), 13);
92
+ return out;
93
+ }
94
+ async function openJson(l, p2, bytes) {
95
+ try {
96
+ if (bytes.length < 13 + 16 || bytes[0] !== 1) return null;
97
+ const plain = await crypto.subtle.decrypt(
98
+ { name: "AES-GCM", iv: bytes.slice(1, 13), additionalData: utf8(l.aad(p2)) },
99
+ await l.key(p2),
100
+ bytes.slice(13)
101
+ );
102
+ return JSON.parse(
103
+ text(await pipe(new Uint8Array(plain), new DecompressionStream("deflate-raw"), MAX_INFLATED))
104
+ );
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ function asBatch(l, value) {
110
+ const file = value;
111
+ if (file?.pipeup !== 1 || file.doc !== l.docId || !Array.isArray(file.ops) || file.ops.length > MAX_OPS)
112
+ return null;
113
+ let newer = 0;
114
+ for (const op of file.ops) {
115
+ const v = op?.body?.v;
116
+ if (typeof v === "number" && v !== 1) newer++;
117
+ }
118
+ return { ops: file.ops, newer };
119
+ }
120
+ async function open(l, p2, bytes) {
121
+ return asBatch(l, await openJson(l, p2, bytes));
122
+ }
123
+ var toText = (bytes) => `pu1.${toB64u(bytes)}`;
124
+ var fromText = (s) => {
125
+ if (!/^pu1\.[A-Za-z0-9_-]+$/.test(s)) return null;
126
+ try {
127
+ return fromB64u(s.slice(4));
128
+ } catch {
129
+ return null;
130
+ }
131
+ };
132
+
133
+ // kit/src/transport.ts
134
+ var RetryAfter = class extends Error {
135
+ /** `limit`: the service's standing rule (one post per this long), so later sends keep at least this gap. */
136
+ constructor(seconds, limit = false) {
137
+ super(`try again in ${seconds} s`);
138
+ this.seconds = seconds;
139
+ this.limit = limit;
140
+ this.name = "RetryAfter";
141
+ }
142
+ };
143
+
144
+ // kit/src/schnorr.ts
145
+ var P = 2n ** 256n - 2n ** 32n - 977n;
146
+
147
+ // ../pipeup/dist/pipeup.core.js
148
+ function f(t2) {
149
+ let e = "";
150
+ for (let r of t2) e += String.fromCharCode(r);
151
+ return btoa(e).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
152
+ }
153
+ function g(t2) {
154
+ let e = atob(t2.replace(/-/g, "+").replace(/_/g, "/")), r = new Uint8Array(e.length);
155
+ for (let n = 0; n < e.length; n++) r[n] = e.charCodeAt(n);
156
+ return r;
157
+ }
158
+ function J(t2 = 16) {
159
+ return f(crypto.getRandomValues(new Uint8Array(t2)));
160
+ }
161
+ function m(t2) {
162
+ return new TextEncoder().encode(t2);
163
+ }
164
+ function x(t2) {
165
+ if (t2 === null || typeof t2 != "object") return JSON.stringify(t2);
166
+ if (Array.isArray(t2)) return "[" + t2.map(x).join(",") + "]";
167
+ let e = t2;
168
+ return "{" + Object.keys(e).filter((r) => e[r] !== void 0).sort().map((r) => JSON.stringify(r) + ":" + x(e[r])).join(",") + "}";
169
+ }
170
+ var Te = /^[A-Za-z0-9_-]{43}$/;
171
+ var Ie = 512;
172
+ var w = /* @__PURE__ */ new Map();
173
+ async function R() {
174
+ let t2 = await crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign", "verify"]), e = new Uint8Array(await crypto.subtle.exportKey("raw", t2.publicKey));
175
+ return { publicKey: f(e), privateKey: t2.privateKey };
176
+ }
177
+ async function S(t2, e) {
178
+ let r = await crypto.subtle.sign({ name: "Ed25519" }, t2.privateKey, e);
179
+ return f(new Uint8Array(r));
180
+ }
181
+ async function Q(t2, e, r) {
182
+ if (!Te.test(t2)) return false;
183
+ try {
184
+ if (f(g(t2)) !== t2) return false;
185
+ let n = w.get(t2);
186
+ if (!n) {
187
+ w.size >= Ie && w.delete(w.keys().next().value), n = crypto.subtle.importKey("raw", g(t2), { name: "Ed25519" }, true, ["verify"]), w.set(t2, n);
188
+ let o = n;
189
+ o.catch(() => {
190
+ w.get(t2) === o && w.delete(t2);
191
+ });
192
+ }
193
+ return await crypto.subtle.verify({ name: "Ed25519" }, await n, g(r), e);
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+ var k = 1e4;
199
+ var Ce = 2 ** 18;
200
+ var D = 2 ** 40;
201
+ var L = 2 ** 47;
202
+ var p = { doc: 64, path: 2e3, snapshot: 500, anchorId: 200, quote: 1e4, context: 64, viewEntries: 16, viewKey: 64, viewValue: 200 };
203
+ var Me = /^[0-9a-f]{8}$/;
204
+ var F = /^[A-Za-z0-9_-]{43}$/;
205
+ var $e = /^[A-Za-z0-9_-]{43}$/;
206
+ var Ne = /* @__PURE__ */ new Set(["create", "reply", "edit", "delete", "resolve", "reopen"]);
207
+ async function Y(t2, e) {
208
+ if (t2.author !== e.publicKey) throw new Error("pipeup: an op must be signed by its author");
209
+ return { body: t2, sig: await S(e, m(x(t2))) };
210
+ }
211
+ var h = (t2, e) => typeof t2 == "string" && t2.length <= e;
212
+ var O = (t2) => !!t2 && typeof t2 == "object";
213
+ var V = (t2) => typeof t2 == "number" && Number.isFinite(t2) && t2 >= 0 && t2 <= 1;
214
+ function B(t2) {
215
+ if (!O(t2)) return false;
216
+ let { id: e, path: r, fingerprint: n, snapshot: o, quote: i, point: s, view: a } = t2;
217
+ if (!h(r, p.path) || !h(o, p.snapshot) || typeof n != "string" || !Me.test(n) || e !== void 0 && (!h(e, p.anchorId) || e === "") || i !== void 0 && (!O(i) || !h(i.exact, p.quote) || i.exact === "" || !h(i.prefix, p.context) || !h(i.suffix, p.context)) || s !== void 0 && (!O(s) || !V(s.x) || !V(s.y))) return false;
218
+ if (a !== void 0) {
219
+ if (!O(a) || Array.isArray(a)) return false;
220
+ let d = Object.entries(a);
221
+ if (d.length > p.viewEntries || !d.every(([c, l]) => c.length <= p.viewKey && h(l, p.viewValue))) return false;
222
+ }
223
+ return true;
224
+ }
225
+ function W(t2) {
226
+ if (!O(t2)) return false;
227
+ let { body: e, sig: r } = t2;
228
+ if (typeof r != "string" || !O(e) || JSON.stringify({ body: e, sig: r }).length > Ce) return false;
229
+ let n = e;
230
+ return !(n.v !== 1 || typeof n.id != "string" || !F.test(n.id) || !Ne.has(n.kind) || !h(n.doc, p.doc) || n.doc === "" || typeof n.thread != "string" || !F.test(n.thread) || typeof n.author != "string" || !$e.test(n.author) || typeof n.name != "string" || n.name.length > 80 || n.name !== n.name.trim() || typeof n.clock != "number" || !Number.isSafeInteger(n.clock) || n.clock < 1 || n.clock > D || typeof n.at != "number" || !Number.isSafeInteger(n.at) || n.at < 0 || n.at > L || n.text !== void 0 && (!h(n.text, k) || n.text !== n.text.trim()) || n.target !== void 0 && (typeof n.target != "string" || !F.test(n.target)) || (n.kind === "create" || n.kind === "reply" || n.kind === "edit") && !n.text || (n.kind === "reply" || n.kind === "edit" || n.kind === "delete") && !n.target || n.kind === "create" && (n.id !== n.thread || !B(n.anchor)));
231
+ }
232
+ function ee(t2) {
233
+ return Q(t2.body.author, m(x(t2.body)), t2.sig);
234
+ }
235
+ function te(t2, e) {
236
+ return t2.body.clock !== e.body.clock ? t2.body.clock - e.body.clock : t2.body.at !== e.body.at ? t2.body.at - e.body.at : t2.body.author !== e.body.author ? t2.body.author < e.body.author ? -1 : 1 : t2.body.id !== e.body.id ? t2.body.id < e.body.id ? -1 : 1 : 0;
237
+ }
238
+ function re(t2) {
239
+ let e = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
240
+ for (let { body: o } of [...t2].sort(te)) {
241
+ if (o.name && n.set(o.author, o.name), o.kind === "create") {
242
+ if (e.has(o.id) || !o.anchor || o.text === void 0) continue;
243
+ let a = ne(o.id, o.author, o.name, o.text, o.at);
244
+ e.set(o.id, { id: o.id, anchor: o.anchor, root: a, resolved: false }), r.set(o.id, { comment: a, thread: o.id });
245
+ continue;
246
+ }
247
+ let i = e.get(o.thread);
248
+ if (!i) continue;
249
+ if (o.kind === "resolve" || o.kind === "reopen") {
250
+ i.resolved = o.kind === "resolve";
251
+ continue;
252
+ }
253
+ let s = o.target ? r.get(o.target) : void 0;
254
+ if (!(!s || s.thread !== o.thread)) {
255
+ if (o.kind === "reply") {
256
+ if (r.has(o.id) || o.text === void 0) continue;
257
+ let a = ne(o.id, o.author, o.name, o.text, o.at);
258
+ i.root.replies.push(a), r.set(o.id, { comment: a, thread: o.thread });
259
+ continue;
260
+ }
261
+ o.author !== s.comment.author || s.comment.deleted || (o.kind === "edit" && o.text !== void 0 && (s.comment.text = o.text, s.comment.edited = true), o.kind === "delete" && (s.comment.text = "", s.comment.deleted = true));
262
+ }
263
+ }
264
+ for (let { comment: o } of r.values()) o.name || (o.unnamed = true, o.name = n.get(o.author) ?? "");
265
+ return [...e.values()];
266
+ }
267
+ function ne(t2, e, r, n, o) {
268
+ return { id: t2, author: e, name: r, text: n, at: o, edited: false, deleted: false, replies: [] };
269
+ }
270
+ async function _(t2) {
271
+ let e = { ...t2, id: void 0, thread: t2.kind === "create" ? void 0 : t2.thread }, r = await crypto.subtle.digest("SHA-256", m(x(e)));
272
+ return f(new Uint8Array(r));
273
+ }
274
+ var T = class {
275
+ constructor(e) {
276
+ this.doc = e;
277
+ }
278
+ ops = /* @__PURE__ */ new Map();
279
+ clock = 0;
280
+ createAt = /* @__PURE__ */ new Map();
281
+ threadAt = /* @__PURE__ */ new Map();
282
+ get size() {
283
+ return this.ops.size;
284
+ }
285
+ all() {
286
+ return [...this.ops.values()];
287
+ }
288
+ threads() {
289
+ return re(this.ops.values());
290
+ }
291
+ async add(e) {
292
+ let r = [];
293
+ for (let n of e) {
294
+ if (!W(n) || n.body.doc !== this.doc || this.ops.has(n.body.id) || n.body.id !== await _(n.body) || !await ee(n)) continue;
295
+ let o = { body: n.body, sig: n.sig };
296
+ this.ops.set(o.body.id, o), this.clock = Math.max(this.clock, o.body.clock), o.body.kind === "create" && this.createAt.set(o.body.author, Math.max(this.createAt.get(o.body.author) ?? 0, o.body.at)), this.threadAt.set(o.body.thread, Math.max(this.threadAt.get(o.body.thread) ?? 0, o.body.at)), r.push(o);
297
+ }
298
+ return r;
299
+ }
300
+ async append(e, r, n) {
301
+ let o = { v: 1, ...n, id: "", thread: n.kind === "create" ? "" : n.thread, doc: this.doc, author: e.publicKey, name: r, clock: Math.min(this.clock + 1, D), at: Math.min(Math.max(Date.now(), n.kind === "create" ? (this.createAt.get(e.publicKey) ?? 0) + 1 : (this.threadAt.get(n.thread) ?? 0) + 1), L) };
302
+ o.id = await _(o);
303
+ let i = this.ops.get(o.id);
304
+ if (i) return i;
305
+ o.kind === "create" && (o.thread = o.id);
306
+ let [s] = await this.add([await Y(o, e)]);
307
+ if (!s) throw new Error("pipeup: could not record this change");
308
+ return s;
309
+ }
310
+ };
311
+ var Re = /^([A-Za-z0-9_-]{16}):([A-Za-z0-9_-]{43})$/;
312
+ function Fe() {
313
+ return `${J(12)}:${f(crypto.getRandomValues(new Uint8Array(32)))}`;
314
+ }
315
+ async function De(t2) {
316
+ let e = Re.exec(t2.trim()), r = e?.[1], n = e?.[2];
317
+ if (!r || !n) throw new Error('pipeup: data-pipeup-doc must look like "<id>:<key>", as made by newDocumentAttribute()');
318
+ let o = await crypto.subtle.importKey("raw", g(n), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
319
+ return { id: r, key: o };
320
+ }
321
+ async function oe(t2, e, r) {
322
+ let n = crypto.getRandomValues(new Uint8Array(12)), o = await crypto.subtle.encrypt({ name: "AES-GCM", iv: n, additionalData: m(r) }, t2, m(e));
323
+ return { iv: f(n), data: f(new Uint8Array(o)) };
324
+ }
325
+ async function ie(t2, e, r) {
326
+ try {
327
+ let n = await crypto.subtle.decrypt({ name: "AES-GCM", iv: g(e.iv), additionalData: m(r) }, t2, g(e.data));
328
+ return new TextDecoder().decode(n);
329
+ } catch {
330
+ throw new Error("pipeup: this feedback could not be opened with this document");
331
+ }
332
+ }
333
+ var Le = 5e6;
334
+ var se = (t2) => `pipeup-file:${t2}`;
335
+ async function ae(t2, e, r) {
336
+ let n = r ? { pipeup: 1, doc: t2, sealed: true, ...await oe(r, JSON.stringify(e), se(t2)) } : { pipeup: 1, doc: t2, sealed: false, ops: e };
337
+ return JSON.stringify(n, null, 2);
338
+ }
339
+ async function de(t2, e, r) {
340
+ if (t2.length > Le) throw new Error("pipeup: this feedback file is too large");
341
+ let n;
342
+ try {
343
+ n = JSON.parse(t2);
344
+ } catch {
345
+ throw new Error("pipeup: this is not an Pipeup feedback file");
346
+ }
347
+ if (!n || typeof n != "object" || n.pipeup !== 1 || typeof n.doc != "string") throw new Error("pipeup: this is not an Pipeup feedback file");
348
+ if (n.doc !== e) throw new Error("pipeup: this feedback is for a different document");
349
+ if (!n.sealed) return Array.isArray(n.ops) ? n.ops : [];
350
+ if (!r) throw new Error("pipeup: this feedback is sealed, and this page has no document key");
351
+ if (typeof n.iv != "string" || typeof n.data != "string") throw new Error("pipeup: this is not an Pipeup feedback file");
352
+ let o;
353
+ try {
354
+ o = await ie(r, { iv: n.iv, data: n.data }, se(e));
355
+ } catch {
356
+ throw new Error("pipeup: this feedback could not be opened with this document's key");
357
+ }
358
+ let i;
359
+ try {
360
+ i = JSON.parse(o);
361
+ } catch {
362
+ throw new Error("pipeup: this is not an Pipeup feedback file");
363
+ }
364
+ return Array.isArray(i) ? i : [];
365
+ }
366
+ var Be = 5e3;
367
+ var _e = /^[a-z][a-z0-9-]{1,23}\/[a-z][a-z0-9-]{0,31}$/;
368
+ var ce = 80;
369
+ var E = class extends Error {
370
+ constructor(r, n, o = 1) {
371
+ super("pipeup: shown here, but not saved yet");
372
+ this.id = r;
373
+ this.cause = n;
374
+ this.count = o;
375
+ this.name = "UnsavedChangeError";
376
+ }
377
+ };
378
+ var K = class t {
379
+ constructor(e, r) {
380
+ this.log = e;
381
+ this.options = r;
382
+ this.displayName = r.name.trim() ? pe(r.name) : "";
383
+ }
384
+ listeners = /* @__PURE__ */ new Set();
385
+ cache = [];
386
+ comments = /* @__PURE__ */ new Map();
387
+ displayName;
388
+ e = Promise.resolve();
389
+ unsaved = [];
390
+ static async open(e) {
391
+ if (typeof e.doc != "string" || e.doc === "" || e.doc.length > p.doc) throw new Error(`pipeup: the document id must be 1 to ${p.doc} characters`);
392
+ let r = new T(e.doc), n = await e.store.load(e.doc), o = n.length - (await r.add(n)).length;
393
+ o && console.warn(`pipeup: ${o} saved change${o === 1 ? "" : "s"} on this page couldn't be read; kept as saved`);
394
+ let i = new t(r, e);
395
+ return i.refresh(false), i;
396
+ }
397
+ get id() {
398
+ return this.options.doc;
399
+ }
400
+ get me() {
401
+ return this.options.identity.publicKey;
402
+ }
403
+ get name() {
404
+ return this.displayName;
405
+ }
406
+ set name(e) {
407
+ this.displayName = pe(e), this.refresh(true, [], "");
408
+ }
409
+ threads() {
410
+ return this.cache;
411
+ }
412
+ ops() {
413
+ return this.log.all();
414
+ }
415
+ onChange(e) {
416
+ return this.listeners.add(e), () => this.listeners.delete(e);
417
+ }
418
+ comment(e, r) {
419
+ return this.enqueue(() => this.write({ kind: "create", anchor: e, text: U(r) }));
420
+ }
421
+ reply(e, r) {
422
+ return this.enqueue(() => this.write({ kind: "reply", thread: this.find(e).thread, target: e, text: U(r) }));
423
+ }
424
+ edit(e, r) {
425
+ return this.enqueue(async () => {
426
+ let { thread: n } = this.mine(e);
427
+ await this.write({ kind: "edit", thread: n, target: e, text: U(r) });
428
+ });
429
+ }
430
+ remove(e) {
431
+ return this.enqueue(async () => {
432
+ let { thread: r } = this.mine(e);
433
+ await this.write({ kind: "delete", thread: r, target: e });
434
+ });
435
+ }
436
+ resolve(e) {
437
+ return this.enqueue(async () => {
438
+ await this.write({ kind: "resolve", thread: this.thread(e) });
439
+ });
440
+ }
441
+ reopen(e) {
442
+ return this.enqueue(async () => {
443
+ await this.write({ kind: "reopen", thread: this.thread(e) });
444
+ });
445
+ }
446
+ sign(e, r) {
447
+ if (!_e.test(e)) throw new Error('pipeup: a signing purpose looks like "<add-on id>/<name>"');
448
+ return S(this.options.identity, m(`pipeup:${e}
449
+ ${r}`));
450
+ }
451
+ flush() {
452
+ return this.enqueue(() => this.persist());
453
+ }
454
+ exportFile() {
455
+ return ae(this.options.doc, this.log.all(), this.options.key);
456
+ }
457
+ async importFile(e) {
458
+ return this.merge(await de(e, this.options.doc, this.options.key), "file");
459
+ }
460
+ merge(e, r) {
461
+ return e.length > Be ? Promise.reject(new RangeError("pipeup: too many changes to merge at once")) : this.enqueue(async () => {
462
+ let n = await this.log.add([...e]);
463
+ this.unsaved.push(...n), n.length > 0 && this.refresh(true, n, r);
464
+ try {
465
+ await this.persist();
466
+ } catch (o) {
467
+ throw n.length > 0 ? new E(n[0].body.id, o, n.length) : o;
468
+ }
469
+ return n.length;
470
+ });
471
+ }
472
+ enqueue(e) {
473
+ let r = this.e.then(e);
474
+ return this.e = r.catch(() => {
475
+ }), r;
476
+ }
477
+ async write(e) {
478
+ let r = await this.log.append(this.options.identity, this.displayName, e);
479
+ this.unsaved.push(r), this.refresh(true, [r], "local");
480
+ try {
481
+ await this.persist();
482
+ } catch (n) {
483
+ throw new E(r.body.id, n);
484
+ }
485
+ return r.body.id;
486
+ }
487
+ async persist() {
488
+ if (this.unsaved.length === 0) return;
489
+ let e = [...this.unsaved];
490
+ await this.options.store.append(this.options.doc, e), this.unsaved = this.unsaved.filter((r) => !e.includes(r));
491
+ }
492
+ refresh(e, r = [], n = "") {
493
+ this.cache = this.log.threads(), this.comments.clear();
494
+ let o = (i, s) => {
495
+ i.unnamed && i.author === this.me && this.displayName && (i.name = this.displayName), this.comments.set(i.id, { comment: i, thread: s });
496
+ for (let a of i.replies) o(a, s);
497
+ };
498
+ for (let i of this.cache) o(i.root, i.id);
499
+ if (e) for (let i of this.listeners) try {
500
+ i(this.cache, r, n);
501
+ } catch (s) {
502
+ globalThis.reportError?.(s);
503
+ }
504
+ }
505
+ find(e) {
506
+ let r = this.comments.get(e);
507
+ if (!r) throw new Error("pipeup: there is no comment with that id");
508
+ return r;
509
+ }
510
+ mine(e) {
511
+ let r = this.find(e);
512
+ if (r.comment.author !== this.me) throw new Error("pipeup: only the writer can change a comment");
513
+ return r;
514
+ }
515
+ thread(e) {
516
+ if (!this.cache.some((r) => r.id === e)) throw new Error("pipeup: there is no thread with that id");
517
+ return e;
518
+ }
519
+ };
520
+ function U(t2) {
521
+ let e = t2.trim();
522
+ if (!e) throw new Error("pipeup: a comment needs some words");
523
+ if (e.length > k) throw new Error(`pipeup: comments are limited to ${k} characters`);
524
+ return e;
525
+ }
526
+ function pe(t2) {
527
+ let e = t2.replace(/\s+/g, " ").trim();
528
+ if (!e || e.length > ce) throw new Error(`pipeup: choose a name of 1 to ${ce} characters`);
529
+ return e;
530
+ }
531
+ var q = class {
532
+ docs = /* @__PURE__ */ new Map();
533
+ profile = null;
534
+ async load(e) {
535
+ return [...this.docs.get(e)?.values() ?? []];
536
+ }
537
+ async append(e, r) {
538
+ let n = this.docs.get(e);
539
+ n || this.docs.set(e, n = /* @__PURE__ */ new Map());
540
+ for (let o of r) n.set(o.body.id, o);
541
+ }
542
+ async loadProfile() {
543
+ return this.profile;
544
+ }
545
+ async saveProfile(e) {
546
+ this.profile = e;
547
+ }
548
+ async saveProfileIfAbsent(e) {
549
+ return this.profile || (this.profile = e), this.profile;
550
+ }
551
+ };
552
+
553
+ // share/src/address.ts
554
+ var NOT_SET = "sharing isn't set up for this page";
555
+ var NOT_AN_ADDRESS = "data-pipeup-share isn't a sharing address";
556
+ var LOCAL = /^(localhost|127\.0\.0\.1|\[::1\])$/;
557
+ var KEY = /^[A-Za-z0-9_-]{43}$/;
558
+ var off = (reason) => ({ kind: "off", reason });
559
+ function parseShare(value, local = false) {
560
+ const text2 = value?.trim();
561
+ if (!text2) return off(NOT_SET);
562
+ let url;
563
+ try {
564
+ url = new URL(text2);
565
+ } catch {
566
+ return off(NOT_AN_ADDRESS);
567
+ }
568
+ if (url.protocol !== "https:" && !(local && url.protocol === "http:" && LOCAL.test(url.hostname)))
569
+ return off(NOT_AN_ADDRESS);
570
+ const at = url.origin + url.pathname;
571
+ const frag = url.hash.slice(1);
572
+ if (frag.startsWith("pm1.")) {
573
+ const [key, token, ...rest] = frag.slice(4).split(".");
574
+ if (!KEY.test(key ?? "") || rest.length || token !== void 0 && !/^[A-Za-z0-9_-]+$/.test(token))
575
+ return off(NOT_AN_ADDRESS);
576
+ if (url.search || !/\/m\/[A-Za-z0-9_-]{22,64}$/.test(at)) return off(NOT_AN_ADDRESS);
577
+ return { kind: "mailbox", mailbox: at, key: fromB64u(key), ...token ? { token } : {} };
578
+ }
579
+ const paste = /^\?([A-Za-z0-9_-]{8,64})$/.exec(url.search)?.[1];
580
+ if (!paste || !KEY.test(frag)) return off(NOT_AN_ADDRESS);
581
+ return { kind: "privatebin", base: at, paste, key: fromB64u(frag) };
582
+ }
583
+
584
+ // share/src/backend.ts
585
+ var Problem = class extends Error {
586
+ constructor(kind) {
587
+ super(kind);
588
+ this.kind = kind;
589
+ this.name = "Problem";
590
+ }
591
+ };
592
+ var ACCEPT = { Accept: "application/json" };
593
+ var GET = (f2, url, signal) => f2(url, { headers: ACCEPT, credentials: "omit", signal });
594
+ var POST = (body) => ({
595
+ method: "POST",
596
+ headers: ACCEPT,
597
+ credentials: "omit",
598
+ body: JSON.stringify(body)
599
+ });
600
+ var refusal = (status) => {
601
+ const kind = { 403: "closed", 404: "gone", 507: "full" }[status];
602
+ return kind && new Problem(kind);
603
+ };
604
+ async function json(res) {
605
+ const t2 = await res.text();
606
+ const v = t2.length < 1e7 && JSON.parse(t2);
607
+ if (!v || typeof v !== "object") throw new Error("the answer was not usable");
608
+ return [v, t2.length];
609
+ }
610
+ function chunks(ops, max = 9e5) {
611
+ const out = [];
612
+ let size = 0;
613
+ for (const op of ops) {
614
+ size += JSON.stringify(op).length;
615
+ if (!out.length || size > max || out.at(-1).length >= 500) {
616
+ out.push([]);
617
+ size = JSON.stringify(op).length;
618
+ }
619
+ out.at(-1).push(op);
620
+ }
621
+ return out;
622
+ }
623
+ var sleep = (ms, signal) => new Promise((resolve) => {
624
+ const t2 = setTimeout(resolve, ms);
625
+ signal?.addEventListener(
626
+ "abort",
627
+ () => {
628
+ clearTimeout(t2);
629
+ resolve();
630
+ },
631
+ { once: true }
632
+ );
633
+ });
634
+ var retryAfter = (res, fallback = 10) => {
635
+ const n = Number(res.headers.get("Retry-After"));
636
+ return n > 0 ? Math.min(n, 3600) : fallback;
637
+ };
638
+ var idsOf = (ops) => ops.map((o) => o?.body?.id).filter((i) => typeof i === "string");
639
+
640
+ // share/src/mailbox.ts
641
+ var MAX_BODY = 524288;
642
+ var MAX_PAGES = 1e3;
643
+ var batchId = async (ct) => toB64u(await sha256(utf8(ct))).slice(0, 22);
644
+ var Mailbox = class {
645
+ constructor(a, l, f2, signal) {
646
+ this.a = a;
647
+ this.l = l;
648
+ this.f = f2;
649
+ this.signal = signal;
650
+ }
651
+ ids = /* @__PURE__ */ new Set();
652
+ expires = null;
653
+ moved = false;
654
+ big = false;
655
+ cursor = "";
656
+ seen = /* @__PURE__ */ new Set();
657
+ memo = null;
658
+ async read() {
659
+ const out = [];
660
+ let reset = 0;
661
+ for (let pages = 0; pages < MAX_PAGES; ) {
662
+ const url = `${this.a.mailbox}/ops${this.cursor ? `?since=${encodeURIComponent(this.cursor)}` : ""}`;
663
+ const r = await GET(this.f, url, this.signal);
664
+ if (r.status === 404) throw new Problem("gone");
665
+ if (r.status === 409 && reset++ < 2) {
666
+ this.cursor = "";
667
+ continue;
668
+ }
669
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
670
+ pages++;
671
+ const [j] = await json(r);
672
+ for (const b of Array.isArray(j.batches) ? j.batches : []) {
673
+ if (typeof b?.id !== "string" || typeof b.ct !== "string" || this.seen.has(b.id)) continue;
674
+ this.seen.add(b.id);
675
+ const bytes = fromText(b.ct);
676
+ const o = bytes && await open(this.l, "share", bytes);
677
+ if (!o) continue;
678
+ out.push(o);
679
+ for (const id of idsOf(o.ops)) this.ids.add(id);
680
+ }
681
+ if (typeof j.next === "string") this.cursor = j.next;
682
+ this.expires = typeof j.expires === "string" ? Date.parse(j.expires) || null : null;
683
+ if (j.more !== true) break;
684
+ }
685
+ return out;
686
+ }
687
+ /** The POST for these ops. A retry of the same ops reuses its sealed text and id, so the server collapses it. */
688
+ async req(ops) {
689
+ const key = ops.map((o) => o.body.id).join();
690
+ if (this.memo?.key !== key) {
691
+ const ct2 = toText(await seal(this.l, "share", ops));
692
+ this.memo = { key, ct: ct2, id: await batchId(ct2) };
693
+ }
694
+ const { ct, id } = this.memo;
695
+ return [`${this.a.mailbox}/ops`, POST({ id, ct, token: this.a.token })];
696
+ }
697
+ async post(ops) {
698
+ for (const part of chunks(ops)) await this.one(part);
699
+ }
700
+ async one(part) {
701
+ const [url, init] = await this.req(part);
702
+ const split = async () => {
703
+ const mid = part.length >> 1;
704
+ await this.one(part.slice(0, mid));
705
+ await this.one(part.slice(mid));
706
+ };
707
+ if (init.body.length > MAX_BODY && part.length > 1) return split();
708
+ const r = await this.f(url, { ...init, signal: this.signal });
709
+ const s = r.status;
710
+ if (s === 200 || s === 201 || s === 400) return;
711
+ if (s === 413 && part.length > 1) return split();
712
+ const refused = refusal(s);
713
+ if (refused) throw refused;
714
+ if (s === 429) {
715
+ const [body] = await json(r).catch(() => [{}]);
716
+ const n = Number(body.retryAfter);
717
+ throw new RetryAfter(r.headers.get("Retry-After") ? retryAfter(r) : n > 0 ? Math.min(n, 3600) : 10);
718
+ }
719
+ throw new Error(`HTTP ${s}`);
720
+ }
721
+ };
722
+
723
+ // share/src/privatebin.ts
724
+ var ROLL_BYTES = 256e3;
725
+ var MAX_HOPS = 16;
726
+ var NEXT = /^[A-Za-z0-9_-]{8,64}$/;
727
+ var spec = () => [toB64(randomBytes(16)), toB64(randomBytes(8)), 1e5, 256, 128, "aes", "gcm", "none"];
728
+ var commentBody = (paste, sealed) => ({
729
+ v: 2,
730
+ adata: spec(),
731
+ ct: toB64(sealed),
732
+ pasteid: paste,
733
+ parentid: paste
734
+ });
735
+ async function answer(res) {
736
+ if (res.status === 429) throw new RetryAfter(retryAfter(res));
737
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
738
+ const [j] = await json(res);
739
+ if (j.status === 0) return j;
740
+ const n = Number(/\d+/.exec(String(j.message ?? ""))?.[0]);
741
+ throw new RetryAfter(n >= 1 && n <= 3600 ? n : 30, true);
742
+ }
743
+ async function createPaste(f2, base, l, expire = "never", signal) {
744
+ const ct = toB64(await seal(l, "share", []));
745
+ const j = await answer(
746
+ await f2(base, { ...POST({ v: 2, adata: [spec(), "plaintext", 1, 0], ct, meta: { expire } }), signal })
747
+ );
748
+ if (typeof j.id !== "string" || typeof j.deletetoken !== "string")
749
+ throw new Error("the answer had no paste id");
750
+ return { id: j.id, deleteToken: j.deletetoken };
751
+ }
752
+ async function deletePaste(f2, base, id, token) {
753
+ return f2(base, POST({ pasteid: id, deletetoken: token }));
754
+ }
755
+ async function readPaste(f2, base, id, signal) {
756
+ const r = await GET(f2, `${base}?${id}`, signal);
757
+ if (r.status === 404) return null;
758
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
759
+ const [j, size] = await json(r);
760
+ if (j.status !== 0) return null;
761
+ const ttl = j.meta?.time_to_live;
762
+ return { j, size, ttl: typeof ttl === "number" && ttl > 0 ? ttl * 1e3 : null };
763
+ }
764
+ var PrivateBin = class {
765
+ constructor(a, l, f2, signal, gen, onGen = () => {
766
+ }) {
767
+ this.a = a;
768
+ this.l = l;
769
+ this.f = f2;
770
+ this.signal = signal;
771
+ this.onGen = onGen;
772
+ this.head = gen || a.paste;
773
+ this.trail.push(a.paste);
774
+ }
775
+ ids = /* @__PURE__ */ new Set();
776
+ expires = null;
777
+ moved = false;
778
+ big = false;
779
+ /** The newest generation seen. */
780
+ head;
781
+ /** Every generation moved through, oldest first (the command line lists them). */
782
+ trail = [];
783
+ /** Comment id -> the `next` it carries (null: none): a comment is opened once. */
784
+ seen = /* @__PURE__ */ new Map();
785
+ async scan(comments, out) {
786
+ let next;
787
+ for (const c of Array.isArray(comments) ? comments : []) {
788
+ if (typeof c?.id !== "string" || typeof c.ct !== "string") continue;
789
+ let n = this.seen.get(c.id);
790
+ if (n === void 0) {
791
+ n = null;
792
+ let bytes = null;
793
+ try {
794
+ if (c.ct.length < 14e5) bytes = fromB64(c.ct);
795
+ } catch {
796
+ }
797
+ const v = bytes && await openJson(this.l, "share", bytes);
798
+ const nx = v?.next;
799
+ const b = typeof nx === "string" ? null : asBatch(this.l, v);
800
+ if (typeof nx === "string" && NEXT.test(nx)) n = nx;
801
+ else if (b) {
802
+ out.push(b);
803
+ for (const id of idsOf(b.ops)) this.ids.add(id);
804
+ }
805
+ this.seen.set(c.id, n);
806
+ }
807
+ next ??= n ?? void 0;
808
+ }
809
+ return next;
810
+ }
811
+ /** Reads the newest generation, following `next` records up to 16 hops; falls back to the file's own address. */
812
+ async read() {
813
+ const out = [];
814
+ let id = this.head;
815
+ let fell = false;
816
+ let got = false;
817
+ for (let hops = 0; ; ) {
818
+ const r = await readPaste(this.f, this.a.base, id, this.signal);
819
+ if (!r) {
820
+ if (got) break;
821
+ if (!fell && id !== this.a.paste) {
822
+ fell = true;
823
+ id = this.a.paste;
824
+ continue;
825
+ }
826
+ throw new Problem("gone");
827
+ }
828
+ got = true;
829
+ if (id !== this.head) {
830
+ this.head = id;
831
+ this.trail.push(id);
832
+ this.ids.clear();
833
+ this.moved = true;
834
+ this.onGen(id);
835
+ }
836
+ const comments = r.j.comments;
837
+ const next = await this.scan(comments, out);
838
+ this.expires = r.ttl && Date.now() + r.ttl;
839
+ const first = (Array.isArray(comments) ? comments[0]?.ct?.length : 0) ?? 0;
840
+ this.big = !next && r.size > ROLL_BYTES && r.size > 2 * first;
841
+ if (!next || ++hops >= MAX_HOPS) break;
842
+ id = next;
843
+ }
844
+ return out;
845
+ }
846
+ ask(paste, sealed) {
847
+ return [this.a.base, POST(commentBody(paste, sealed))];
848
+ }
849
+ async req(ops) {
850
+ return this.ask(this.head, await seal(this.l, "share", ops));
851
+ }
852
+ go(url, init) {
853
+ return this.f(url, { ...init, signal: this.signal });
854
+ }
855
+ async post(ops) {
856
+ for (const part of chunks(ops)) await answer(await this.go(...await this.req(part)));
857
+ }
858
+ /**
859
+ * Starts a new generation: a new paste holding `ops`, then a sealed `next` in the old one. The earliest `next`
860
+ * wins; a loser deletes its own paste. Returns what the final read found.
861
+ */
862
+ async roll(ops) {
863
+ const patiently2 = async (run2) => {
864
+ for (let tries = 0; ; tries++) {
865
+ try {
866
+ return await run2();
867
+ } catch (e) {
868
+ if (!(e instanceof RetryAfter) || tries > 5 || this.signal?.aborted) throw e;
869
+ await sleep(e.seconds * 1e3 * (1 + Math.random() * 0.2), this.signal);
870
+ }
871
+ }
872
+ };
873
+ const old = this.head;
874
+ const made = await patiently2(() => createPaste(this.f, this.a.base, this.l, "never", this.signal));
875
+ for (const part of chunks(ops))
876
+ await patiently2(
877
+ async () => answer(await this.go(...this.ask(made.id, await seal(this.l, "share", part))))
878
+ );
879
+ await patiently2(
880
+ async () => answer(await this.go(...this.ask(old, await sealJson(this.l, "share", { next: made.id }))))
881
+ );
882
+ const got = await this.read();
883
+ if (this.head !== made.id)
884
+ await deletePaste(this.f, this.a.base, made.id, made.deleteToken).catch(() => {
885
+ });
886
+ return got;
887
+ }
888
+ };
889
+
890
+ // share/src/cli-lib.ts
891
+ var HTML_TAG = /<html\b(?:[^>"']|"[^"]*"|'[^']*')*>/i;
892
+ var attr = (name) => new RegExp(`(\\s)${name}(\\s*=\\s*)("[^"]*"|'[^']*'|[^\\s>"']+)`, "i");
893
+ function readAttribute(html, name) {
894
+ const tag = HTML_TAG.exec(html)?.[0];
895
+ const m2 = tag && attr(name).exec(tag);
896
+ if (!m2) return null;
897
+ return m2[3].replace(/^["']|["']$/g, "").replace(/&quot;/g, '"').replace(/&amp;/g, "&");
898
+ }
899
+ function setAttribute(html, name, value) {
900
+ const m2 = HTML_TAG.exec(html);
901
+ if (!m2) throw new Error("the page has no <html> tag to put it on");
902
+ const tag = m2[0];
903
+ const escaped = value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
904
+ const old = attr(name).exec(tag);
905
+ let next;
906
+ if (old) {
907
+ const q2 = old[3][0] === "'" ? "'" : '"';
908
+ const v = q2 === "'" ? value.replace(/&/g, "&amp;").replace(/'/g, "&#39;") : escaped;
909
+ next = tag.slice(0, old.index) + `${old[1]}${name}${old[2]}${q2}${v}${q2}` + tag.slice(old.index + old[0].length);
910
+ } else {
911
+ const at = tag.length - 1;
912
+ next = `${tag.slice(0, at)} ${name}="${escaped}"${tag.slice(at)}`;
913
+ }
914
+ return html.slice(0, m2.index) + next + html.slice(m2.index + tag.length);
915
+ }
916
+ var DAY = 864e5;
917
+ var dateWords = (ms) => new Date(ms).toLocaleDateString("en-GB", {
918
+ day: "numeric",
919
+ month: "long",
920
+ year: "numeric",
921
+ timeZone: "UTC"
922
+ });
923
+ function spanWords(ms) {
924
+ const n = (x2, unit) => `${x2} ${unit}${x2 === 1 ? "" : "s"}`;
925
+ if (ms < 36e5) return n(Math.max(1, Math.round(ms / 6e4)), "minute");
926
+ if (ms < 2 * DAY) return n(Math.round(ms / 36e5), "hour");
927
+ if (ms < 6.5 * DAY) return n(Math.round(ms / DAY), "day");
928
+ if (ms < 60 * DAY) return n(Math.round(ms / (7 * DAY)), "week");
929
+ if (ms < 730 * DAY) return n(Math.round(ms / (30 * DAY)), "month");
930
+ return n(Math.round(ms / (365 * DAY)), "year");
931
+ }
932
+ function keepWords(host, ttlMs, now = Date.now()) {
933
+ if (ttlMs === null) return `${host} keeps this shared copy until it is deleted.`;
934
+ return `${host} keeps this shared copy for ${spanWords(ttlMs)}, until ${dateWords(now + ttlMs)}.`;
935
+ }
936
+ var WITH_VALUE = /* @__PURE__ */ new Set(["server", "from", "key"]);
937
+ function parseArgs(argv) {
938
+ const flags = /* @__PURE__ */ new Map();
939
+ const rest = [];
940
+ for (let i = 0; i < argv.length; i++) {
941
+ const a = argv[i];
942
+ if (a === "-h") flags.set("help", true);
943
+ else if (a.startsWith("--")) {
944
+ const [name, inline] = a.slice(2).split(/=(.*)/s, 2);
945
+ if (WITH_VALUE.has(name)) {
946
+ const v = inline ?? argv[++i];
947
+ if (v === void 0 || v.startsWith("--")) throw new UsageError(`--${name} needs a value`);
948
+ flags.set(name, v);
949
+ } else if (["mailbox", "new-doc", "help"].includes(name)) flags.set(name, true);
950
+ else throw new UsageError(`I don't know the option --${name}`);
951
+ } else rest.push(a);
952
+ }
953
+ return { command: rest[0], file: rest[1], flags };
954
+ }
955
+ var UsageError = class extends Error {
956
+ };
957
+
958
+ // share/src/cli-run.ts
959
+ var HELP = `Share a Pipeup page's comments through an encrypted service.
960
+
961
+ Usage:
962
+ share create <page.html> --server <url> [--mailbox] [--from <feedback.json>] [--new-doc]
963
+ share stop <page.html> --key <stop key>
964
+ share update <page.html>
965
+
966
+ create Makes a shared copy and writes its address into the page (data-pipeup-share), changing nothing else.
967
+ --server a PrivateBin instance (https://paste.example.org/), or with --mailbox your own mailbox server
968
+ --mailbox use the HTTP mailbox contract; set PIPEUP_MAILBOX_CREATE_TOKEN in the environment if the
969
+ server wants one (it is never read from the command line)
970
+ --from post the comments of a feedback file into the shared copy
971
+ --new-doc give the page a document identity (data-pipeup-doc) if it has none
972
+ stop Deletes the shared copy. --key is the stop key that "create" printed.
973
+ update Points the page at the newest copy, when PrivateBin has moved on to a newer one.
974
+
975
+ Exit codes: 0 done, 1 the service or the file failed, 2 the command wasn't understood or needs something first.
976
+ `;
977
+ var wait = (io, ms) => io.wait ? io.wait(ms) : sleep(ms);
978
+ async function patiently(io, run2) {
979
+ for (let tries = 0; ; tries++) {
980
+ try {
981
+ return await run2();
982
+ } catch (e) {
983
+ if (!(e instanceof RetryAfter) || tries > 8) throw e;
984
+ await wait(io, e.seconds * 1e3 + 500);
985
+ }
986
+ }
987
+ }
988
+ function server(value, mailbox) {
989
+ if (typeof value !== "string") throw new UsageError("--server needs the address of the service");
990
+ const a = parseShare(
991
+ mailbox ? `${value.replace(/\/+$/, "")}/m/${"x".repeat(22)}#pm1.${"x".repeat(43)}` : `${value}?${"x".repeat(16)}#${"x".repeat(43)}`,
992
+ true
993
+ );
994
+ if (a.kind === "off")
995
+ throw new UsageError("--server must be an https: address (http://localhost is allowed for development)");
996
+ const u = new URL(value);
997
+ return mailbox ? value.replace(/\/+$/, "") : u.origin + u.pathname;
998
+ }
999
+ async function feedbackOps(file, docAttr, io) {
1000
+ const { id, key } = await De(docAttr);
1001
+ const doc = await K.open({
1002
+ doc: id,
1003
+ key,
1004
+ store: new q(),
1005
+ identity: await R(),
1006
+ name: ""
1007
+ });
1008
+ try {
1009
+ await doc.importFile(await io.read(file));
1010
+ } catch (e) {
1011
+ throw new Error(
1012
+ `I couldn't read ${file} as this page's feedback: ${e.message.replace(/^pipeup: /, "")}`
1013
+ );
1014
+ }
1015
+ return [...doc.ops()];
1016
+ }
1017
+ async function create(file, flags, io) {
1018
+ const mailbox = flags.has("mailbox");
1019
+ const base = server(flags.get("server"), mailbox);
1020
+ let html = await io.read(file);
1021
+ let docAttr = readAttribute(html, "data-pipeup-doc");
1022
+ if (!docAttr) {
1023
+ if (!flags.has("new-doc")) {
1024
+ io.err(
1025
+ `${file} has no data-pipeup-doc, so it has no document identity, and sharing needs one.
1026
+ Run the same command with --new-doc and I will write a fresh one into the page.
1027
+ `
1028
+ );
1029
+ throw new UsageError("the page needs a document identity");
1030
+ }
1031
+ docAttr = Fe();
1032
+ html = setAttribute(html, "data-pipeup-doc", docAttr);
1033
+ io.out("Gave the page a new document identity (data-pipeup-doc).");
1034
+ }
1035
+ const { id } = await De(docAttr).catch(() => {
1036
+ throw new UsageError("data-pipeup-doc in the page isn't a valid document identity");
1037
+ });
1038
+ const from = typeof flags.get("from") === "string" ? flags.get("from") : null;
1039
+ const ops = from ? await feedbackOps(from, docAttr, io) : [];
1040
+ const room = randomBytes(32);
1041
+ const key = toB64u(room);
1042
+ const l = ladder(id, room);
1043
+ const had = readAttribute(html, "data-pipeup-share");
1044
+ let address;
1045
+ let stopKey;
1046
+ let backend;
1047
+ const host = new URL(base).host;
1048
+ if (mailbox) {
1049
+ const create2 = io.env.PIPEUP_MAILBOX_CREATE_TOKEN;
1050
+ const res = await io.fetch(`${base}/m`, {
1051
+ method: "POST",
1052
+ headers: create2 ? { Authorization: `Bearer ${create2}` } : {},
1053
+ body: JSON.stringify({ v: 1 })
1054
+ });
1055
+ const j = await res.json().catch(() => ({}));
1056
+ if (res.status === 401 || res.status === 403)
1057
+ throw new Error(
1058
+ `${host} wouldn't make a mailbox. It wants a create token: set PIPEUP_MAILBOX_CREATE_TOKEN and run again.`
1059
+ );
1060
+ if (res.status !== 201 || typeof j.mailbox !== "string" || typeof j.stop !== "string")
1061
+ throw new Error(`${host} wouldn't make a mailbox (it answered ${res.status}).`);
1062
+ const token = typeof j.token === "string" ? j.token : null;
1063
+ address = `${base}/m/${j.mailbox}#pm1.${key}${token ? `.${token}` : ""}`;
1064
+ stopKey = j.stop;
1065
+ backend = new Mailbox({ mailbox: `${base}/m/${j.mailbox}`, ...token ? { token } : {} }, l, io.fetch);
1066
+ const until = typeof j.expires === "string" ? Date.parse(j.expires) : NaN;
1067
+ const keep = j.keep === "last-write" ? ", and longer each time a comment is added" : "";
1068
+ io.out(
1069
+ Number.isNaN(until) ? `${host} keeps this shared copy until it is deleted.` : `${host} keeps this shared copy until ${dateWords(until)}${keep}.`
1070
+ );
1071
+ const lim = j.limits;
1072
+ if (lim?.mailbox) io.out(`It holds up to ${Math.round(lim.mailbox / 1048576)} MB of comments.`);
1073
+ } else {
1074
+ const made = await patiently(io, () => createPaste(io.fetch, base, l, "never")).catch((e) => {
1075
+ throw new Error(`${host} wouldn't make a shared copy: ${e.message}`);
1076
+ });
1077
+ address = `${base}?${made.id}#${key}`;
1078
+ stopKey = made.deleteToken;
1079
+ backend = new PrivateBin({ base, paste: made.id }, l, io.fetch);
1080
+ const back = await readPaste(io.fetch, base, made.id).catch(() => null);
1081
+ if (!back)
1082
+ throw new Error(
1083
+ `${host} made the shared copy but I couldn't read it back, so I haven't touched the page.`
1084
+ );
1085
+ io.out(keepWords(host, back.ttl));
1086
+ }
1087
+ if (ops.length) {
1088
+ await patiently(io, () => backend.post(ops));
1089
+ io.out(`Put ${ops.length} comment${ops.length === 1 ? "" : "s"} from ${from} into the shared copy.`);
1090
+ }
1091
+ await io.write(file, setAttribute(html, "data-pipeup-share", address));
1092
+ io.out(`Wrote data-pipeup-share into ${file}; nothing else in the page changed.`);
1093
+ if (had)
1094
+ io.out("It replaced the earlier sharing address: people with the old file keep reading the old copy.");
1095
+ io.out("");
1096
+ io.out(`Stop key: ${stopKey}`);
1097
+ io.out("Keep this private; never put it in the page. You need it to stop sharing:");
1098
+ io.out(` share stop ${file} --key <stop key>`);
1099
+ }
1100
+ async function addressOf(file, io) {
1101
+ const html = await io.read(file);
1102
+ const a = parseShare(readAttribute(html, "data-pipeup-share"), true);
1103
+ if (a.kind === "off") throw new UsageError(`${file}: ${a.reason}`);
1104
+ const docAttr = readAttribute(html, "data-pipeup-doc");
1105
+ const doc = docAttr ? (await De(docAttr).catch(() => null))?.id : null;
1106
+ if (!doc) throw new UsageError(`${file} has no valid data-pipeup-doc`);
1107
+ return { html, a, l: ladder(doc, a.key) };
1108
+ }
1109
+ async function stop(file, flags, io) {
1110
+ const key = flags.get("key");
1111
+ if (typeof key !== "string") throw new UsageError("stop needs --key with the stop key that create printed");
1112
+ const { a, l } = await addressOf(file, io);
1113
+ if (a.kind === "mailbox") {
1114
+ const res = await io.fetch(a.mailbox, { method: "DELETE", headers: { Authorization: `Bearer ${key}` } });
1115
+ if (res.status === 403) throw new Error("That isn't this mailbox's stop key, so nothing was deleted.");
1116
+ if (res.status === 404) throw new Error("The shared copy is already gone.");
1117
+ if (res.status !== 204 && res.status !== 200)
1118
+ throw new Error(`The server answered ${res.status}, so I can't say it was deleted.`);
1119
+ io.out("Deleted the shared copy. Everyone keeps the comments they already have.");
1120
+ } else {
1121
+ const b = new PrivateBin(a, l, io.fetch);
1122
+ const later = [];
1123
+ await b.read().catch(() => {
1124
+ });
1125
+ for (const id of b.trail.slice(1))
1126
+ later.push({ id, ttl: (await readPaste(io.fetch, a.base, id).catch(() => null))?.ttl ?? null });
1127
+ const res = await deletePaste(io.fetch, a.base, a.paste, key);
1128
+ const j = await res.json().catch(() => ({}));
1129
+ if (j.status !== 0)
1130
+ throw new Error(
1131
+ "That isn't this shared copy's stop key (or it is already gone), so nothing was deleted."
1132
+ );
1133
+ io.out(
1134
+ "Deleted the shared copy the page's address points to. Everyone keeps the comments they already have."
1135
+ );
1136
+ if (later.length) {
1137
+ io.err(
1138
+ `${later.length} newer cop${later.length === 1 ? "y was" : "ies were"} made when the first one grew. This key can't delete ${later.length === 1 ? "it" : "them"}; ${later.length === 1 ? "it stays" : "they stay"} until the service removes ${later.length === 1 ? "it" : "them"}:`
1139
+ );
1140
+ for (const g2 of later)
1141
+ io.err(
1142
+ ` ${g2.id}: ${g2.ttl === null ? "no expiry set" : `until about ${dateWords(Date.now() + g2.ttl)}`}`
1143
+ );
1144
+ }
1145
+ }
1146
+ io.out(
1147
+ "The page still carries its old address; remove data-pipeup-share if it should say nothing about sharing."
1148
+ );
1149
+ }
1150
+ async function update(file, io) {
1151
+ const { html, a, l } = await addressOf(file, io);
1152
+ if (a.kind === "mailbox") return io.out("A mailbox has no newer copies, so there is nothing to update.");
1153
+ const b = new PrivateBin(a, l, io.fetch);
1154
+ await b.read().catch((e) => {
1155
+ throw new Error(
1156
+ `I couldn't read the shared copy: ${e.message === "gone" ? "it is gone" : e.message}`
1157
+ );
1158
+ });
1159
+ if (b.head === a.paste) return io.out("The page already points at the newest copy.");
1160
+ const fragment = new URL(readAttribute(html, "data-pipeup-share")).hash;
1161
+ await io.write(file, setAttribute(html, "data-pipeup-share", `${a.base}?${b.head}${fragment}`));
1162
+ io.out(`Pointed ${file} at the newest copy, so new reviewers don't have to walk through the older ones.`);
1163
+ }
1164
+ async function run(argv, io) {
1165
+ try {
1166
+ const { command, file, flags } = parseArgs(argv);
1167
+ if (flags.has("help") || !command || command === "help") {
1168
+ io.out(HELP);
1169
+ return flags.has("help") || command === "help" ? 0 : 2;
1170
+ }
1171
+ if (!["create", "stop", "update"].includes(command))
1172
+ throw new UsageError(`I don't know the command "${command}"`);
1173
+ if (!file) throw new UsageError(`${command} needs the page, such as: share ${command} page.html`);
1174
+ if (command === "create") await create(file, flags, io);
1175
+ else if (command === "stop") await stop(file, flags, io);
1176
+ else await update(file, io);
1177
+ return 0;
1178
+ } catch (e) {
1179
+ if (e instanceof UsageError) {
1180
+ io.err(`${e.message.replace(/\.$/, "")}. Try: share --help`);
1181
+ return 2;
1182
+ }
1183
+ io.err(e.message);
1184
+ return 1;
1185
+ }
1186
+ }
1187
+
1188
+ // share/src/cli.ts
1189
+ process.exitCode = await run(process.argv.slice(2), {
1190
+ fetch: (i, n) => fetch(i, n),
1191
+ env: process.env,
1192
+ read: (p2) => readFile(p2, "utf8"),
1193
+ write: (p2, t2) => writeFile(p2, t2),
1194
+ out: (t2) => console.log(t2),
1195
+ err: (t2) => console.error(t2)
1196
+ });