fedipod 1.42.0 → 1.44.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.
@@ -20136,7 +20136,7 @@ var require_buffer = __commonJS({
20136
20136
  return false;
20137
20137
  }
20138
20138
  };
20139
- Buffer3.concat = function concat(list, length) {
20139
+ Buffer3.concat = function concat2(list, length) {
20140
20140
  if (!Array.isArray(list)) {
20141
20141
  throw new TypeError('"list" argument must be an Array of Buffers');
20142
20142
  }
@@ -23512,8 +23512,8 @@ var Serializer = class _Serializer {
23512
23512
  }
23513
23513
  var canUse = canUseMethod.bind(this);
23514
23514
  if ("#/".indexOf(p[p.length - 1]) >= 0) p = p.slice(0, -1);
23515
- var slash = p.lastIndexOf("/");
23516
- if (slash >= 0) p = p.slice(slash + 1);
23515
+ var slash2 = p.lastIndexOf("/");
23516
+ if (slash2 >= 0) p = p.slice(slash2 + 1);
23517
23517
  var i = 0;
23518
23518
  while (i < p.length) {
23519
23519
  if (this.prefixchars.indexOf(p[i]) >= 0) {
@@ -32544,6 +32544,52 @@ var REL = {
32544
32544
  };
32545
32545
 
32546
32546
  // lib/pod/urls.mjs
32547
+ function apUrls(remotePod, root, { publicBase = null } = {}) {
32548
+ if (!root) throw new Error("apUrls: a container root is required \u2014 the caller states it, this library does not guess");
32549
+ const base = remotePod.endsWith("/") ? remotePod : remotePod + "/";
32550
+ const home = base + (root.endsWith("/") ? root : root + "/");
32551
+ const face = publicBase ? publicBase.endsWith("/") ? publicBase : publicBase + "/" : home;
32552
+ const urls = {
32553
+ base,
32554
+ home,
32555
+ webfinger: base + ".well-known/webfinger",
32556
+ actor: face + "ap/actor",
32557
+ inbox: face + "ap/inbox/",
32558
+ outbox: face + "ap/outbox",
32559
+ followers: face + "ap/followers",
32560
+ following: face + "ap/following",
32561
+ notes: face + "ap/notes/",
32562
+ privateNotes: face + "ap/private/",
32563
+ featured: face + "ap/featured",
32564
+ // FEP-1b12: the moderator roster a recipient validates announced
32565
+ // moderation against. Published only when moderators are configured.
32566
+ moderators: face + "ap/moderators",
32567
+ // FEP-4ccd and FEP-c648: follows in limbo and the block list, as
32568
+ // collections. They live in the private container so the owner-only ACL
32569
+ // is inherited, not re-stated per document.
32570
+ pendingFollowers: face + "ap/private/pending-followers",
32571
+ pendingFollowing: face + "ap/private/pending-following",
32572
+ blocked: face + "ap/private/blocked",
32573
+ // The outbox as its owner reads it (every message, §5.1) and what the
32574
+ // owner has liked (§5.5): the owner's alone, so they sit here too — and
32575
+ // at the pod's own address even for a fronted identity, because the owner
32576
+ // reads them there, with a credential the pod checks.
32577
+ ownOutbox: home + "ap/private/outbox",
32578
+ liked: home + "ap/private/liked",
32579
+ profileHtml: face + "ap/profile.html",
32580
+ // Media stays on the pod even when fronted: attachment urls are not
32581
+ // identity-checked by remotes, and proxying blobs would be pure cost.
32582
+ media: home + "ap/media/",
32583
+ state: home + "ap-state/"
32584
+ };
32585
+ if (publicBase) {
32586
+ urls.podHome = home;
32587
+ urls.publicHome = face;
32588
+ urls.toPod = (u) => typeof u === "string" && u.startsWith(face) ? home + u.slice(face.length) : u;
32589
+ urls.toPublic = (u) => typeof u === "string" && u.startsWith(home) ? face + u.slice(home.length) : u;
32590
+ }
32591
+ return urls;
32592
+ }
32547
32593
  function podBaseOfWebId(webId) {
32548
32594
  const u = new URL(webId);
32549
32595
  u.hash = "";
@@ -33678,6 +33724,650 @@ async function moveIn(answers, { session, onStep = () => {
33678
33724
  return { config, address: `@${handle}@${new URL(origin).host}`, movedFrom: config.movedFrom };
33679
33725
  }
33680
33726
 
33727
+ // packages/fedipod-bb/src/setup.mjs
33728
+ var setup_exports = {};
33729
+ __export(setup_exports, {
33730
+ ROOT: () => ROOT,
33731
+ attachRows: () => attachRows,
33732
+ forumConfig: () => forumConfig,
33733
+ gatewayRows: () => gatewayRows,
33734
+ keepRows: () => keepRows,
33735
+ nameKeeperInRules: () => nameKeeperInRules,
33736
+ setUpForumAtGateway: () => setUpForumAtGateway,
33737
+ writeForumConfig: () => writeForumConfig
33738
+ });
33739
+
33740
+ // lib/pod/containers.mjs
33741
+ var KEEP = { keep: true };
33742
+ var KEEP_CT = "application/json";
33743
+ var keepUrl = (base) => `${base}.keep`;
33744
+ async function exists(pod, base) {
33745
+ try {
33746
+ const r = await pod.fetch(keepUrl(base), { method: "HEAD" });
33747
+ return r?.status >= 200 && r.status < 300;
33748
+ } catch {
33749
+ return false;
33750
+ }
33751
+ }
33752
+ async function provisionPublic(pod, base) {
33753
+ await pod.putJson(keepUrl(base), KEEP, KEEP_CT);
33754
+ await pod.setAcl(base, ["Read"]);
33755
+ }
33756
+ async function provisionPrivate(pod, urls) {
33757
+ if (await exists(pod, urls.state)) return false;
33758
+ await pod.putJson(keepUrl(urls.state), KEEP, KEEP_CT);
33759
+ await pod.setAcl(urls.state, []);
33760
+ await pod.setAcl(urls.home, []);
33761
+ return true;
33762
+ }
33763
+ async function restateRules(pod, urls) {
33764
+ for (const url of [urls.home, urls.state, urls.home + "ap/private/"]) await pod.setAcl(url, [], { ifChanged: true });
33765
+ for (const url of [urls.notes, urls.media]) await pod.setAcl(url, ["Read"], { ifChanged: true });
33766
+ for (const child of await pod.listContainer(urls.home + "ap/")) {
33767
+ if (!child.url.endsWith("/")) await pod.restateAcl(child.url);
33768
+ }
33769
+ }
33770
+
33771
+ // web/app/shims/node-crypto.mjs
33772
+ var K = new Uint32Array([
33773
+ 1116352408,
33774
+ 1899447441,
33775
+ 3049323471,
33776
+ 3921009573,
33777
+ 961987163,
33778
+ 1508970993,
33779
+ 2453635748,
33780
+ 2870763221,
33781
+ 3624381080,
33782
+ 310598401,
33783
+ 607225278,
33784
+ 1426881987,
33785
+ 1925078388,
33786
+ 2162078206,
33787
+ 2614888103,
33788
+ 3248222580,
33789
+ 3835390401,
33790
+ 4022224774,
33791
+ 264347078,
33792
+ 604807628,
33793
+ 770255983,
33794
+ 1249150122,
33795
+ 1555081692,
33796
+ 1996064986,
33797
+ 2554220882,
33798
+ 2821834349,
33799
+ 2952996808,
33800
+ 3210313671,
33801
+ 3336571891,
33802
+ 3584528711,
33803
+ 113926993,
33804
+ 338241895,
33805
+ 666307205,
33806
+ 773529912,
33807
+ 1294757372,
33808
+ 1396182291,
33809
+ 1695183700,
33810
+ 1986661051,
33811
+ 2177026350,
33812
+ 2456956037,
33813
+ 2730485921,
33814
+ 2820302411,
33815
+ 3259730800,
33816
+ 3345764771,
33817
+ 3516065817,
33818
+ 3600352804,
33819
+ 4094571909,
33820
+ 275423344,
33821
+ 430227734,
33822
+ 506948616,
33823
+ 659060556,
33824
+ 883997877,
33825
+ 958139571,
33826
+ 1322822218,
33827
+ 1537002063,
33828
+ 1747873779,
33829
+ 1955562222,
33830
+ 2024104815,
33831
+ 2227730452,
33832
+ 2361852424,
33833
+ 2428436474,
33834
+ 2756734187,
33835
+ 3204031479,
33836
+ 3329325298
33837
+ ]);
33838
+ var rotr = (x, n) => x >>> n | x << 32 - n;
33839
+ function sha256(bytes) {
33840
+ const l = bytes.length;
33841
+ const withOne = l + 1;
33842
+ const k = (56 - withOne % 64 + 64) % 64;
33843
+ const total = withOne + k + 8;
33844
+ const m = new Uint8Array(total);
33845
+ m.set(bytes);
33846
+ m[l] = 128;
33847
+ const bits = l * 8;
33848
+ const dv = new DataView(m.buffer);
33849
+ dv.setUint32(total - 4, bits >>> 0);
33850
+ dv.setUint32(total - 8, Math.floor(bits / 4294967296));
33851
+ const H = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
33852
+ const w = new Uint32Array(64);
33853
+ for (let i = 0; i < total; i += 64) {
33854
+ for (let t = 0; t < 16; t++) w[t] = dv.getUint32(i + t * 4);
33855
+ for (let t = 16; t < 64; t++) {
33856
+ const s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ w[t - 15] >>> 3;
33857
+ const s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ w[t - 2] >>> 10;
33858
+ w[t] = w[t - 16] + s0 + w[t - 7] + s1 >>> 0;
33859
+ }
33860
+ let [a, b, c, d, e, f, g, h] = H;
33861
+ for (let t = 0; t < 64; t++) {
33862
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
33863
+ const ch = e & f ^ ~e & g;
33864
+ const t1 = h + S1 + ch + K[t] + w[t] >>> 0;
33865
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
33866
+ const maj = a & b ^ a & c ^ b & c;
33867
+ const t2 = S0 + maj >>> 0;
33868
+ h = g;
33869
+ g = f;
33870
+ f = e;
33871
+ e = d + t1 >>> 0;
33872
+ d = c;
33873
+ c = b;
33874
+ b = a;
33875
+ a = t1 + t2 >>> 0;
33876
+ }
33877
+ H[0] = H[0] + a >>> 0;
33878
+ H[1] = H[1] + b >>> 0;
33879
+ H[2] = H[2] + c >>> 0;
33880
+ H[3] = H[3] + d >>> 0;
33881
+ H[4] = H[4] + e >>> 0;
33882
+ H[5] = H[5] + f >>> 0;
33883
+ H[6] = H[6] + g >>> 0;
33884
+ H[7] = H[7] + h >>> 0;
33885
+ }
33886
+ const out = new Uint8Array(32);
33887
+ new DataView(out.buffer).setUint32(0, H[0]);
33888
+ new DataView(out.buffer).setUint32(4, H[1]);
33889
+ new DataView(out.buffer).setUint32(8, H[2]);
33890
+ new DataView(out.buffer).setUint32(12, H[3]);
33891
+ new DataView(out.buffer).setUint32(16, H[4]);
33892
+ new DataView(out.buffer).setUint32(20, H[5]);
33893
+ new DataView(out.buffer).setUint32(24, H[6]);
33894
+ new DataView(out.buffer).setUint32(28, H[7]);
33895
+ return out;
33896
+ }
33897
+ function hmacSha256(key, msg) {
33898
+ if (key.length > 64) key = sha256(key);
33899
+ const pad = new Uint8Array(64);
33900
+ pad.set(key);
33901
+ const ipad = new Uint8Array(64);
33902
+ const opad = new Uint8Array(64);
33903
+ for (let i = 0; i < 64; i++) {
33904
+ ipad[i] = pad[i] ^ 54;
33905
+ opad[i] = pad[i] ^ 92;
33906
+ }
33907
+ const inner = sha256(concat(ipad, msg));
33908
+ return sha256(concat(opad, inner));
33909
+ }
33910
+ var concat = (a, b) => {
33911
+ const o = new Uint8Array(a.length + b.length);
33912
+ o.set(a);
33913
+ o.set(b, a.length);
33914
+ return o;
33915
+ };
33916
+ var toBytes = (data, enc2) => {
33917
+ if (data == null) return new Uint8Array(0);
33918
+ if (typeof data === "string") {
33919
+ if (enc2 === "hex") return Uint8Array.from(data.match(/.{1,2}/g) || [], (h) => parseInt(h, 16));
33920
+ if (enc2 === "base64") return Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
33921
+ return new TextEncoder().encode(data);
33922
+ }
33923
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
33924
+ };
33925
+ var encode = (bytes, enc2) => {
33926
+ if (!enc2 || enc2 === "buffer") return bytes;
33927
+ if (enc2 === "hex") return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
33928
+ const b64 = btoa(String.fromCharCode(...bytes));
33929
+ if (enc2 === "base64url") return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
33930
+ return b64;
33931
+ };
33932
+ function createHash(alg) {
33933
+ if (alg !== "sha256") throw new Error(`node-crypto shim: only sha256 is implemented (got ${alg})`);
33934
+ let acc = new Uint8Array(0);
33935
+ return { update(d, e) {
33936
+ acc = concat(acc, toBytes(d, e));
33937
+ return this;
33938
+ }, digest(enc2) {
33939
+ return encode(sha256(acc), enc2);
33940
+ } };
33941
+ }
33942
+ function createHmac(alg, key) {
33943
+ if (alg !== "sha256") throw new Error(`node-crypto shim: only hmac-sha256 is implemented (got ${alg})`);
33944
+ const k = toBytes(key);
33945
+ let acc = new Uint8Array(0);
33946
+ return { update(d, e) {
33947
+ acc = concat(acc, toBytes(d, e));
33948
+ return this;
33949
+ }, digest(enc2) {
33950
+ return encode(hmacSha256(k, acc), enc2);
33951
+ } };
33952
+ }
33953
+ function randomBytes(n) {
33954
+ const b = new Uint8Array(n);
33955
+ crypto.getRandomValues(b);
33956
+ return globalThis.Buffer ? globalThis.Buffer.from(b) : b;
33957
+ }
33958
+ var webcrypto = globalThis.crypto;
33959
+ var randomUUID = () => globalThis.crypto.randomUUID();
33960
+ function timingSafeEqual(a, b) {
33961
+ if (a.length !== b.length) return false;
33962
+ let out = 0;
33963
+ for (let i = 0; i < a.length; i++) out |= a[i] ^ b[i];
33964
+ return out === 0;
33965
+ }
33966
+ var unavailable = (name) => () => {
33967
+ throw new Error(`node:crypto ${name} is not available in the browser agent`);
33968
+ };
33969
+ var generateKeyPairSync = unavailable("generateKeyPairSync");
33970
+ var createPrivateKey = unavailable("createPrivateKey");
33971
+ var createPublicKey = unavailable("createPublicKey");
33972
+ var scryptSync = unavailable("scryptSync");
33973
+ var node_crypto_default = {
33974
+ createHash,
33975
+ createHmac,
33976
+ randomBytes,
33977
+ webcrypto,
33978
+ randomUUID,
33979
+ timingSafeEqual,
33980
+ generateKeyPairSync,
33981
+ createPrivateKey,
33982
+ createPublicKey,
33983
+ scryptSync
33984
+ };
33985
+
33986
+ // packages/fedipod-bb/src/urls.mjs
33987
+ var ROOT = "fedipod-bb/";
33988
+ var SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/u;
33989
+ var isSlug = (s) => typeof s === "string" && SLUG.test(s);
33990
+ var TID = /^[0-9]{4}-[0-9]{2}-[a-z0-9][a-z0-9-]{0,78}$/u;
33991
+ var isTid = (s) => typeof s === "string" && TID.test(s);
33992
+ var cacheKey = (postId) => node_crypto_default.createHash("sha256").update(String(postId)).digest("hex").slice(0, 16);
33993
+ function forumUrls(remotePod, root = ROOT, { publicBase = null, front = null, handle = null } = {}) {
33994
+ const origin = front ? String(front).replace(/\/$/u, "") : null;
33995
+ if (origin && !handle) throw new Error("forumUrls: a fronted forum needs its handle");
33996
+ const site = apUrls(remotePod, root, { publicBase: publicBase || (origin ? `${origin}/u/${handle}/` : null) });
33997
+ const face = site.actor.slice(0, -"ap/actor".length);
33998
+ site.front = origin;
33999
+ site.categories = face + "ap/categories";
34000
+ site.administrators = face + "ap/administrators";
34001
+ site.latest = face + "ap/latest";
34002
+ site.mod = (remotePod.endsWith("/") ? remotePod : remotePod + "/") + (root.endsWith("/") ? root : root + "/") + "mod/";
34003
+ site.siteHtml = face + "ap/site.html";
34004
+ site.root = root.endsWith("/") ? root : root + "/";
34005
+ site.category = (slug) => {
34006
+ if (!isSlug(slug)) throw new Error(`forumUrls: not a category slug (${slug})`);
34007
+ return categoryUrls(remotePod, site.root + "c/" + slug + "/", {
34008
+ publicBase: origin ? `${origin}/u/${slug}/` : publicBase ? face + "c/" + slug + "/" : null,
34009
+ forumInbox: site.inbox
34010
+ });
34011
+ };
34012
+ return site;
34013
+ }
34014
+ function categoryUrls(remotePod, root, { publicBase = null, forumInbox = null } = {}) {
34015
+ const urls = apUrls(remotePod, root, { publicBase });
34016
+ const face = urls.actor.slice(0, -"ap/actor".length);
34017
+ urls.forumInbox = forumInbox || urls.inbox;
34018
+ urls.topics = face + "ap/topics";
34019
+ urls.topicsPage = (n) => `${urls.topics}-${n}`;
34020
+ urls.topicContainer = face + "ap/topic/";
34021
+ urls.topic = (tid) => {
34022
+ if (!isTid(tid)) throw new Error(`categoryUrls: not a topic id (${tid})`);
34023
+ return urls.topicContainer + tid;
34024
+ };
34025
+ urls.topicPage = (tid, n) => `${urls.topic(tid)}-${n}`;
34026
+ urls.cache = face + "ap/cache/";
34027
+ urls.cached = (postId) => urls.cache + cacheKey(postId);
34028
+ urls.members = face + "ap/members";
34029
+ urls.categoryHtml = face + "ap/index.html";
34030
+ urls.topicHtml = (tid) => face + "ap/t/" + tid + ".html";
34031
+ return urls;
34032
+ }
34033
+
34034
+ // packages/fedipod-bb/src/provision.mjs
34035
+ async function provisionForum(remote, site, { moderatorWebIds = [] } = {}) {
34036
+ await provisionPrivate(remote, site);
34037
+ await provisionPublic(remote, site.notes);
34038
+ await provisionPublic(remote, site.media);
34039
+ await remote.putJson(site.mod + ".keep", { "@context": "https://www.w3.org/ns/activitystreams", type: "Object" }, "application/activity+json");
34040
+ await remote.setAcl(site.mod, [], { readAgents: moderatorWebIds });
34041
+ }
34042
+ async function provisionCategory(remote, cat, { memberWebIds = null } = {}) {
34043
+ await provisionPrivate(remote, cat);
34044
+ for (const base of [cat.notes, cat.media, cat.topicContainer, cat.cache]) {
34045
+ if (memberWebIds) {
34046
+ await remote.putJson(base + ".keep", { "@context": "https://www.w3.org/ns/activitystreams", type: "Object" }, "application/activity+json");
34047
+ await remote.setAcl(base, [], { readAgents: memberWebIds });
34048
+ } else {
34049
+ await provisionPublic(remote, base);
34050
+ }
34051
+ }
34052
+ }
34053
+
34054
+ // packages/fedipod-bb/src/setup.mjs
34055
+ function forumConfig(existing = {}, {
34056
+ remotePod,
34057
+ root = ROOT,
34058
+ handle,
34059
+ name,
34060
+ categories = [],
34061
+ moderators = [],
34062
+ moderatorWebIds = [],
34063
+ membersOnly = [],
34064
+ memberWebIds = {},
34065
+ approveJoins = false,
34066
+ review = false,
34067
+ replyPolicy = "open"
34068
+ }) {
34069
+ if (!isSlug(handle)) throw new Error(`not a forum handle: ${handle}`);
34070
+ const cats = categories.map((c) => typeof c === "string" ? { slug: c, name: c } : c);
34071
+ for (const c of cats) if (!isSlug(c.slug)) throw new Error(`not a category slug: ${c.slug}`);
34072
+ const was = JSON.stringify((existing.categories || []).map((c) => [c.slug, c.name]));
34073
+ const access = JSON.stringify([existing.membersOnly || [], existing.memberWebIds || {}, existing.moderatorWebIds || []]);
34074
+ const accessChanged = !!existing.handle && access !== JSON.stringify([membersOnly, memberWebIds, moderatorWebIds]);
34075
+ const renamed = !!existing.handle && ((name || handle) !== existing.name || was !== JSON.stringify(cats.map((c) => [c.slug, c.name])));
34076
+ return {
34077
+ ...existing,
34078
+ kind: "application",
34079
+ handle,
34080
+ name: name || existing.name || handle,
34081
+ ...renamed || accessChanged ? { republish: true } : {},
34082
+ ...accessChanged ? { reprovision: true } : {},
34083
+ remotePod,
34084
+ root,
34085
+ categories: cats,
34086
+ moderators,
34087
+ moderatorWebIds,
34088
+ membersOnly,
34089
+ memberWebIds,
34090
+ approveJoins,
34091
+ review,
34092
+ replyPolicy
34093
+ };
34094
+ }
34095
+ var CONFIG = "config.json";
34096
+ var serialise = (obj) => JSON.stringify(obj, null, 2) + "\n";
34097
+ async function writeConfig2(storage, config) {
34098
+ const w = await storage.write(CONFIG, serialise(config), "application/json");
34099
+ if (!w.ok) throw new Error(`the forum's config could not be written${w.why ? ` (${w.why})` : ""}`);
34100
+ }
34101
+ async function writeForumConfig(remote, storageFor, opts, { log: log2 = () => {
34102
+ } } = {}) {
34103
+ const plain = forumUrls(opts.remotePod, opts.root || ROOT);
34104
+ await provisionForum(remote, plain);
34105
+ const storage = storageFor(plain.state, (u, i) => remote.fetch(u, i));
34106
+ const had = await storage.read(CONFIG).catch(() => null);
34107
+ let existing = {};
34108
+ if (had?.ok && had.body) {
34109
+ try {
34110
+ existing = JSON.parse(had.body) || {};
34111
+ } catch {
34112
+ existing = {};
34113
+ }
34114
+ }
34115
+ const config = forumConfig(existing, opts);
34116
+ await writeConfig2(storage, config);
34117
+ log2(`forum ${config.handle} initialised with ${config.categories.length} categor${config.categories.length === 1 ? "y" : "ies"}`);
34118
+ return { storage, config, plain };
34119
+ }
34120
+ var gatewayRows = (config, plain) => [
34121
+ { handle: config.handle, podHome: plain.home, actorUrl: plain.actor, kind: "application" },
34122
+ ...(config.categories || []).map((c) => ({
34123
+ handle: c.slug,
34124
+ podHome: plain.category(c.slug).home,
34125
+ actorUrl: plain.category(c.slug).actor,
34126
+ kind: "group"
34127
+ }))
34128
+ ];
34129
+ async function attachRows({ fetch: f, front, config, plain }, { log: log2 = () => {
34130
+ } } = {}) {
34131
+ const origin = String(front).replace(/\/+$/u, "");
34132
+ const secrets = {};
34133
+ const rows = gatewayRows(config, plain);
34134
+ for (const row of rows) {
34135
+ const res = await f(`${origin}/api/attach`, {
34136
+ method: "POST",
34137
+ headers: { "content-type": "application/json" },
34138
+ body: JSON.stringify({ ...row, fronted: true, inboxUrl: plain.inbox })
34139
+ });
34140
+ const d = await res.json().catch(() => ({}));
34141
+ if (res.status !== 201 || !d.hmacSecret) throw new Error(`attach ${row.handle} at ${origin}: HTTP ${res.status}${d.error ? " " + d.error : ""}`);
34142
+ secrets[row.handle] = String(d.hmacSecret);
34143
+ log2(`attached @${row.handle}@${new URL(origin).host}`);
34144
+ }
34145
+ return { gateway: { front: origin, mode: "trust", secrets }, handles: rows.map((r) => r.handle) };
34146
+ }
34147
+ async function keepRows({ fetch: f, front, config, on = true }) {
34148
+ const origin = String(front).replace(/\/+$/u, "");
34149
+ const forumHandle = config.handle;
34150
+ const handles = [...(config.categories || []).map((c) => c.slug), forumHandle];
34151
+ let keeper = null;
34152
+ for (const handle of handles) {
34153
+ const res = await f(`${origin}/api/keeper`, {
34154
+ method: "POST",
34155
+ headers: { "content-type": "application/json" },
34156
+ body: JSON.stringify({ handle, on, ...handle === forumHandle ? {} : { forum: forumHandle } })
34157
+ });
34158
+ const d = await res.json().catch(() => ({}));
34159
+ if (!res.ok) throw new Error(`${origin} would not ${on ? "keep" : "let go of"} @${handle}: ${d.error || res.status}`);
34160
+ keeper = d.keeper || keeper;
34161
+ }
34162
+ return { keeper: on ? keeper : null, handles, on };
34163
+ }
34164
+ async function nameKeeperInRules(remote, { config, plain, keeper = null, ownerWebId = null }) {
34165
+ remote.keepers = keeper ? [keeper] : [];
34166
+ remote.aclOwner = ownerWebId || remote.aclOwner || null;
34167
+ await provisionForum(remote, plain, { moderatorWebIds: config.moderatorWebIds || [] });
34168
+ await restateRules(remote, plain);
34169
+ for (const c of config.categories || []) {
34170
+ const urls = plain.category(c.slug);
34171
+ const members = (config.membersOnly || []).includes(c.slug) ? config.memberWebIds?.[c.slug] || [] : null;
34172
+ await provisionCategory(remote, urls, { memberWebIds: members });
34173
+ await restateRules(remote, urls);
34174
+ }
34175
+ }
34176
+ async function setUpForumAtGateway({ remote, storageFor, fetch: f, front, ownerWebId, ...opts }, { log: log2 = () => {
34177
+ }, onStep = () => {
34178
+ } } = {}) {
34179
+ onStep("write", "running");
34180
+ const { storage, config, plain } = await writeForumConfig(remote, storageFor, opts, { log: log2 });
34181
+ onStep("write", "ok");
34182
+ onStep("attach", "running");
34183
+ const { gateway, handles } = await attachRows({ fetch: f, front, config, plain }, { log: log2 });
34184
+ await writeConfig2(storage, { ...config, gateway, republish: true });
34185
+ onStep("attach", "ok");
34186
+ onStep("keep", "running");
34187
+ const { keeper } = await keepRows({ fetch: f, front, config, on: true });
34188
+ await nameKeeperInRules(remote, { config, plain, keeper, ownerWebId });
34189
+ onStep("keep", "ok");
34190
+ return { handle: config.handle, front: gateway.front, handles, keeper };
34191
+ }
34192
+
34193
+ // web/app/forum-support.mjs
34194
+ var forum_support_default = setup_exports;
34195
+
34196
+ // stub:node:fs/promises
34197
+ var fail = () => {
34198
+ throw new Error("node:fs/promises is not available in the browser agent");
34199
+ };
34200
+ var promises_default = new Proxy({}, { get: () => fail });
34201
+
34202
+ // web/app/shims/_globals.mjs
34203
+ var URL2 = globalThis.URL;
34204
+ var URLSearchParams2 = globalThis.URLSearchParams;
34205
+
34206
+ // web/app/shims/node-url.mjs
34207
+ var fileURLToPath = (u) => {
34208
+ const s = typeof u === "string" ? u : u.href;
34209
+ return s.replace(/^file:\/\//, "");
34210
+ };
34211
+ var pathToFileURL = (p) => new URL("file://" + (p.startsWith("/") ? p : "/" + p));
34212
+ var node_url_default = { URL: globalThis.URL, URLSearchParams: globalThis.URLSearchParams, fileURLToPath, pathToFileURL };
34213
+
34214
+ // lib/core/storage.mjs
34215
+ var LDP2 = Namespace("http://www.w3.org/ns/ldp#");
34216
+ var slash = (u) => u.endsWith("/") ? u : u + "/";
34217
+ var HttpStorage = class {
34218
+ constructor(base, fetchImpl) {
34219
+ this.base = slash(base);
34220
+ this.fetchImpl = fetchImpl;
34221
+ }
34222
+ get kind() {
34223
+ return "pod";
34224
+ }
34225
+ // The same jail FileStorage has, and for the same reason: a path here can
34226
+ // carry `..`, encodeURI leaves both `.` and `/` alone, and fetch normalises
34227
+ // the segments away — so a name derived from remote input could name a
34228
+ // resource outside the container entirely. A prefix check is not enough on
34229
+ // its own: a concatenated URL always satisfies one.
34230
+ _url(p) {
34231
+ const u = new URL(encodeURI(p), this.base);
34232
+ if (!u.href.startsWith(this.base)) throw new Error(`path escapes the container: ${p}`);
34233
+ return u.href;
34234
+ }
34235
+ async list(sub = "", { etag } = {}) {
34236
+ const url = this._url(sub);
34237
+ const res = await this.fetchImpl(url, {
34238
+ headers: { accept: "text/turtle", ...etag ? { "if-none-match": etag } : {} }
34239
+ });
34240
+ if (res.status === 304) return { notModified: true, names: null, etag };
34241
+ if (res.status === 404) return { notModified: false, names: [], etag: null, missing: true };
34242
+ if (res.status >= 400) throw new Error(`container unreadable (HTTP ${res.status})`);
34243
+ const g = graph();
34244
+ parse2(await res.text(), g, url, "text/turtle");
34245
+ const here = namedNode2(url);
34246
+ const names = g.each(here, LDP2("contains"), null, here).map((n) => n.value).filter((u) => u.startsWith(url) && u !== url).map((u) => decodeURIComponent(u.slice(url.length)));
34247
+ return { notModified: false, names, etag: res.headers.get("etag") };
34248
+ }
34249
+ // `accept` is for callers reading something that is RDF but is wanted as it
34250
+ // was written: asking turtle-first for a JSON-LD document gets turtle back,
34251
+ // because the server is entitled to convert between two RDF syntaxes.
34252
+ async read(p, { etag, accept } = {}) {
34253
+ const res = await this.fetchImpl(this._url(p), {
34254
+ headers: {
34255
+ accept: accept || "text/turtle, application/json;q=0.9, */*;q=0.8",
34256
+ ...etag ? { "if-none-match": etag } : {}
34257
+ }
34258
+ });
34259
+ if (res.status === 304) return { ok: true, notModified: true, status: 304, body: null, etag };
34260
+ if (res.status >= 400) return { ok: false, notModified: false, status: res.status, body: null, etag: null };
34261
+ return { ok: true, notModified: false, status: res.status, body: await res.text(), etag: res.headers.get("etag") };
34262
+ }
34263
+ async write(p, body, contentType) {
34264
+ const url = this._url(p);
34265
+ try {
34266
+ const res = await this.fetchImpl(url, {
34267
+ method: "PUT",
34268
+ headers: { "content-type": contentType },
34269
+ body
34270
+ });
34271
+ if (res.status < 400) return { ok: true, retry: false, why: "" };
34272
+ const retry = res.status >= 500 || res.status === 429;
34273
+ const ra = Number(res.headers?.get?.("retry-after"));
34274
+ return {
34275
+ ok: false,
34276
+ retry,
34277
+ why: `HTTP ${res.status}`,
34278
+ retryAfterMs: Number.isFinite(ra) && ra > 0 ? ra * 1e3 : 0
34279
+ };
34280
+ } catch (e) {
34281
+ return { ok: false, retry: true, why: e.message, retryAfterMs: 0 };
34282
+ }
34283
+ }
34284
+ async remove(p) {
34285
+ const url = this._url(p);
34286
+ try {
34287
+ const res = await this.fetchImpl(url, { method: "DELETE" });
34288
+ return res.status < 400 || res.status === 404;
34289
+ } catch {
34290
+ return false;
34291
+ }
34292
+ }
34293
+ };
34294
+
34295
+ // web/app/forum-form.mjs
34296
+ var SLUG2 = /^[a-z0-9][a-z0-9-]{0,62}$/u;
34297
+ function categoriesFrom(text) {
34298
+ const lines = String(text || "").split("\n").map((l) => l.trim()).filter(Boolean);
34299
+ if (!lines.length) throw new Error("Categories: at least one is required.");
34300
+ const out = [];
34301
+ const seen = /* @__PURE__ */ new Set();
34302
+ for (const line of lines) {
34303
+ const i = line.indexOf(":");
34304
+ const slug = (i < 0 ? line : line.slice(0, i)).trim().toLowerCase();
34305
+ const name = (i < 0 ? "" : line.slice(i + 1)).trim() || slug;
34306
+ if (!SLUG2.test(slug)) throw new Error(`Categories: "${slug}" is not a short name. Letters, digits and hyphens, starting with a letter or digit.`);
34307
+ if (seen.has(slug)) throw new Error(`Categories: "${slug}" is listed twice.`);
34308
+ seen.add(slug);
34309
+ out.push({ slug, name: name.slice(0, 200) });
34310
+ }
34311
+ return out;
34312
+ }
34313
+ function moderatorFrom(text, frontOrigin) {
34314
+ const s = String(text || "").trim();
34315
+ if (!s) return null;
34316
+ if (/^https?:\/\//iu.test(s)) {
34317
+ try {
34318
+ return new URL(s).href;
34319
+ } catch {
34320
+ throw new Error("Moderator: not a valid web address.");
34321
+ }
34322
+ }
34323
+ const m = /^@?([a-z0-9][a-z0-9._-]*)@([a-z0-9.-]+(?::\d+)?)$/iu.exec(s);
34324
+ if (!m) throw new Error("Moderator: write it as @name@server, or as a web address.");
34325
+ const front = frontOrigin ? new URL(frontOrigin) : null;
34326
+ if (front && m[2].toLowerCase() === front.host.toLowerCase()) return `${front.origin}/u/${m[1].toLowerCase()}/ap/actor`;
34327
+ throw new Error(`Moderator: only an address at ${front ? front.host : "this site"} can be named here. Others can be added on the forum's settings page.`);
34328
+ }
34329
+
34330
+ // web/app/signup-forum.mjs
34331
+ var forumsOffered = () => !!forum_support_default;
34332
+ async function signUpForum({ handle, name, categories, moderator = "" }, { session, onStep = () => {
34333
+ }, frontOrigin = null } = {}) {
34334
+ if (!forum_support_default) throw new Error("This site does not host forums.");
34335
+ if (!frontOrigin) throw new Error("A forum lives at a gateway, and this page has none.");
34336
+ const bad = handleProblem(handle);
34337
+ if (bad) throw new Error(`Forum handle: ${bad}`);
34338
+ if (!String(name || "").trim()) throw new Error("A forum name is required.");
34339
+ const cats = categoriesFrom(categories);
34340
+ const mod = moderatorFrom(moderator, frontOrigin);
34341
+ if (!session?.webId || typeof session.fetch !== "function") throw new Error("sign in at the forum's pod first");
34342
+ const webId = session.webId;
34343
+ const pod = podBaseOfWebId(webId);
34344
+ const remote = new BrowserRemotePod(session, { webId, role: "signup", log: () => {
34345
+ } });
34346
+ onStep("pod", "running");
34347
+ if (await findAccount(remote, pod, readConfigAt(remote, pod))) {
34348
+ throw new Error("This pod already hosts a FediPod account. A forum needs a pod of its own.");
34349
+ }
34350
+ if (await resourceExists(session.fetch, pod + forum_support_default.ROOT)) {
34351
+ throw new Error("This pod already holds a forum.");
34352
+ }
34353
+ for (const h of [handle, ...cats.map((c) => c.slug)]) await assertFrontNameFree(frontOrigin, h);
34354
+ onStep("pod", "ok");
34355
+ const made = await forum_support_default.setUpForumAtGateway({
34356
+ remote,
34357
+ storageFor: (base, fetch2) => new HttpStorage(base, fetch2),
34358
+ fetch: (u, i) => session.fetch(u, i),
34359
+ front: frontOrigin,
34360
+ ownerWebId: webId,
34361
+ remotePod: pod,
34362
+ handle,
34363
+ name: String(name).trim(),
34364
+ categories: cats,
34365
+ moderators: mod ? [mod] : [],
34366
+ moderatorWebIds: [webId]
34367
+ }, { onStep });
34368
+ return { handle: made.handle, front: made.front, address: `@${made.handle}@${new URL(made.front).host}` };
34369
+ }
34370
+
33681
34371
  // lib/pod/actor.mjs
33682
34372
  var ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
33683
34373
  async function readIssuer(actorUrl, fetchImpl = fetch) {
@@ -33695,7 +34385,7 @@ async function readIssuer(actorUrl, fetchImpl = fetch) {
33695
34385
  var STORE = "session";
33696
34386
  var b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
33697
34387
  var enc = (o) => b64u(new TextEncoder().encode(JSON.stringify(o)));
33698
- var sha256 = (s) => crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
34388
+ var sha2562 = (s) => crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
33699
34389
  var rand = (n = 32) => b64u(crypto.getRandomValues(new Uint8Array(n)));
33700
34390
  async function dpopKey() {
33701
34391
  return crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
@@ -33778,7 +34468,7 @@ function solidOidcSession({ dbName = "solid-oidc-session", clientName = "Solid a
33778
34468
  const client_id = await registerClient(cfg, redirectUri, name);
33779
34469
  const pair = await dpopKey();
33780
34470
  const verifier = rand(48);
33781
- const challenge = b64u(await sha256(verifier));
34471
+ const challenge = b64u(await sha2562(verifier));
33782
34472
  const state = rand(16);
33783
34473
  await idbPut("pending", {
33784
34474
  issuer,
@@ -33897,7 +34587,7 @@ function solidOidcSession({ dbName = "solid-oidc-session", clientName = "Solid a
33897
34587
  };
33898
34588
  const send = async (url, init) => {
33899
34589
  const jwk = await jwkP;
33900
- const ath = b64u(await sha256(accessToken));
34590
+ const ath = b64u(await sha2562(accessToken));
33901
34591
  const headers = { ...init.headers || {}, authorization: `DPoP ${accessToken}`, dpop: await dpopProof(s.pair, jwk, init.method || "GET", url, ath) };
33902
34592
  return fetch(url, { ...init, headers });
33903
34593
  };
@@ -34027,6 +34717,11 @@ window.fedipodSignup = async ({ onStep, ...answers }) => {
34027
34717
  await signUp(answers, { session, onStep, frontOrigin: location.origin });
34028
34718
  await bootWorker({ reset: true });
34029
34719
  };
34720
+ window.fedipodForumSignup = async ({ onStep, ...answers }) => {
34721
+ const session = await getSession();
34722
+ if (!session) throw new Error("Sign in at the forum's pod first.");
34723
+ return signUpForum(answers, { session, onStep, frontOrigin: location.origin });
34724
+ };
34030
34725
  window.fedipodMoveIn = async ({ onStep, ...answers }) => {
34031
34726
  const session = await getSession();
34032
34727
  if (!session) throw new Error("Sign in at your pod first.");
@@ -34326,6 +35021,11 @@ ${e.detail}` : "");
34326
35021
  $("container").placeholder = podRootPath(signedPod);
34327
35022
  $("container-field").hidden = !!moveFrom;
34328
35023
  $("movein-note").hidden = !moveFrom;
35024
+ try {
35025
+ const m = sessionStorage.getItem("fedipod:making");
35026
+ if (m) for (const r of f().making) r.checked = r.value === m;
35027
+ } catch {
35028
+ }
34329
35029
  if (moveFrom) {
34330
35030
  $("movein-from").textContent = moveFrom.address;
34331
35031
  if (!f().handle.value) f().handle.value = moveFrom.config.handle;
@@ -34374,7 +35074,8 @@ ${e.detail}` : "");
34374
35074
  return false;
34375
35075
  }
34376
35076
  };
34377
- const shape = () => pathPod() || moveFrom ? "front" : f().shape.value;
35077
+ const making = () => forumsOffered() && f().making?.value === "forum" ? "forum" : "account";
35078
+ const shape = () => pathPod() || moveFrom || making() === "forum" ? "front" : f().shape.value;
34378
35079
  const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape(), container: f().container.value.trim() });
34379
35080
  const previewPlace = () => {
34380
35081
  if (!signedPod) return;
@@ -34392,12 +35093,26 @@ ${e.detail}` : "");
34392
35093
  $("preview").textContent = handle && host ? `@${handle}@${host}` : "@\u2026@\u2026";
34393
35094
  };
34394
35095
  const applyShape = () => {
34395
- const fixed = pathPod() || !!moveFrom;
35096
+ const fixed = pathPod() || !!moveFrom || making() === "forum";
34396
35097
  for (const r of f().shape) {
34397
35098
  if (fixed) r.checked = r.value === "front";
34398
35099
  }
34399
35100
  $("shape-group").hidden = fixed;
34400
- $("shape-hint").hidden = !fixed || !!moveFrom;
35101
+ $("shape-hint").hidden = !fixed || !!moveFrom || making() === "forum";
35102
+ };
35103
+ const applyMaking = () => {
35104
+ const forum = making() === "forum";
35105
+ $("making-hint").hidden = !forum;
35106
+ $("forum-fields").hidden = !forum;
35107
+ $("container-field").hidden = forum || !!moveFrom;
35108
+ $("identity-legend").textContent = forum ? "Your forum" : "Your Fediverse Identity";
35109
+ $("handle-label").textContent = forum ? "Forum handle" : "Fediverse handle";
35110
+ $("handle-hint").textContent = forum ? `The name in the forum's address, @handle@${location.host}. This is permanent. Letters, digits, and hyphens only.` : "The name in your Fediverse address. This is permanent. Letters, digits, and hyphens only.";
35111
+ $("handle").placeholder = forum ? "the handle part of the forum's address: @HANDLE@server" : "the handle part of your address: @HANDLE@server";
35112
+ $("signed-pod-label").textContent = forum ? "The forum's pod" : "Your pod";
35113
+ $("addr-label").textContent = forum ? "The forum's address" : "Your Fediverse address";
35114
+ $("after-hint").textContent = forum ? "The handle, the categories' short names and the pod cannot be changed afterwards. Everything else you set on the forum's settings page." : "The handle and the pod cannot be changed afterwards. Everything else \u2014 display name, bio, pictures \u2014 you set later in the client.";
35115
+ $("submit").textContent = forum ? "Create the forum" : moveFrom ? "Move your account here" : "Create your Fediverse account";
34401
35116
  };
34402
35117
  const applyMode = () => {
34403
35118
  const existing = f().mode.value === "existing";
@@ -34409,11 +35124,14 @@ ${e.detail}` : "");
34409
35124
  };
34410
35125
  for (const el of $("form").elements) for (const evt of ["input", "change"]) el.addEventListener(evt, () => {
34411
35126
  applyMode();
35127
+ applyMaking();
34412
35128
  applyShape();
34413
35129
  previewAddr();
34414
35130
  previewPlace();
34415
35131
  });
34416
35132
  applyMode();
35133
+ $("making-group").hidden = !forumsOffered();
35134
+ applyMaking();
34417
35135
  const STEP_IDS = ["step-1", "step-2"];
34418
35136
  const FOCUS = { 1: "provider", 2: "handle" };
34419
35137
  const goStep = (n) => {
@@ -34423,6 +35141,7 @@ ${e.detail}` : "");
34423
35141
  $("err-1").textContent = "";
34424
35142
  $("form-error").textContent = "";
34425
35143
  if (n === 2) {
35144
+ applyMaking();
34426
35145
  applyShape();
34427
35146
  previewAddr();
34428
35147
  previewPlace();
@@ -34436,6 +35155,17 @@ ${e.detail}` : "");
34436
35155
  };
34437
35156
  const validateStep2 = () => {
34438
35157
  const hp = window.fedipodHandleProblem(f().handle.value.trim().toLowerCase());
35158
+ if (making() === "forum") {
35159
+ if (hp) return `Forum handle: ${hp}`;
35160
+ if (!f().forumName.value.trim()) return "Forum name: a name is required.";
35161
+ try {
35162
+ categoriesFrom(f().categories.value);
35163
+ moderatorFrom(f().moderator.value, location.origin);
35164
+ } catch (e) {
35165
+ return e.message;
35166
+ }
35167
+ return null;
35168
+ }
34439
35169
  if (hp) return `Fediverse handle: ${hp}`;
34440
35170
  if (!moveFrom) {
34441
35171
  const where = chosenRoot(signedPod, f().container.value).problem;
@@ -34456,6 +35186,10 @@ ${e.detail}` : "");
34456
35186
  }
34457
35187
  $("err-1").textContent = "";
34458
35188
  $("to-2").disabled = true;
35189
+ try {
35190
+ sessionStorage.setItem("fedipod:making", making());
35191
+ } catch {
35192
+ }
34459
35193
  try {
34460
35194
  await window.fedipodPodLogin({ issuer: providerUrl() });
34461
35195
  } catch (err) {
@@ -34483,6 +35217,7 @@ ${e.detail}` : "");
34483
35217
  place: "Recording where your account lives"
34484
35218
  };
34485
35219
  const MOVE_LABELS = { pod: "Reading your account on your pod", gateway: "Taking your address here", keys: "Moving your key and account record" };
35220
+ const FORUM_LABELS = { pod: "Checking the forum's pod", write: "Writing the forum on its pod", attach: "Taking its addresses here", keep: "Making this site its keeper" };
34486
35221
  const run = async (createIndex = false) => {
34487
35222
  const a = { ...answers(), ...createIndex ? { createIndex: true } : {} };
34488
35223
  $("pane-form").hidden = true;
@@ -34490,10 +35225,12 @@ ${e.detail}` : "");
34490
35225
  $("running-title").textContent = "Setting up\u2026";
34491
35226
  $("run-error").textContent = "";
34492
35227
  $("run-actions").hidden = true;
35228
+ $("run-done").hidden = true;
34493
35229
  const steps = $("steps");
34494
35230
  steps.textContent = "";
34495
35231
  const mark = {};
34496
- const labels = moveFrom ? MOVE_LABELS : LABELS;
35232
+ const forum = making() === "forum";
35233
+ const labels = forum ? FORUM_LABELS : moveFrom ? MOVE_LABELS : LABELS;
34497
35234
  const onStep = (k, st2) => {
34498
35235
  if (!mark[k]) {
34499
35236
  const li = document.createElement("li");
@@ -34503,6 +35240,23 @@ ${e.detail}` : "");
34503
35240
  mark[k].textContent = (st2 === "ok" ? "\u2713 " : st2 === "running" ? "\u2026 " : "") + (labels[k] || k);
34504
35241
  };
34505
35242
  try {
35243
+ if (forum) {
35244
+ const made = await window.fedipodForumSignup({
35245
+ handle: a.handle,
35246
+ name: f().forumName.value,
35247
+ categories: f().categories.value,
35248
+ moderator: f().moderator.value,
35249
+ onStep
35250
+ });
35251
+ try {
35252
+ sessionStorage.removeItem("fedipod:making");
35253
+ } catch {
35254
+ }
35255
+ $("running-title").textContent = `Your forum is ready: ${made.address}`;
35256
+ $("run-done-link").href = `${location.origin}/bb/?forum=${encodeURIComponent(made.handle)}`;
35257
+ $("run-done").hidden = false;
35258
+ return;
35259
+ }
34506
35260
  if (moveFrom) await window.fedipodMoveIn({ handle: a.handle, onStep });
34507
35261
  else await window.fedipodSignup({ ...a, onStep });
34508
35262
  location.href = "/admin/client/";