fedipod-server 0.28.1 → 0.29.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.
Files changed (61) hide show
  1. package/README.md +27 -28
  2. package/config/server.json +22 -0
  3. package/dist/claims.js +3 -2
  4. package/dist/directory.d.ts +2 -0
  5. package/dist/handler.d.ts +4 -1
  6. package/dist/handler.js +32 -3
  7. package/dist/handler.jsonld +4 -0
  8. package/dist/store-pod.js +37 -0
  9. package/lib/client/c2s.mjs +97 -43
  10. package/lib/client/masto/accounts.mjs +1 -0
  11. package/lib/core/deliver.mjs +17 -1
  12. package/lib/core/intake/activities.mjs +18 -2
  13. package/lib/core/intake/group.mjs +3 -1
  14. package/lib/core/intake/index.mjs +33 -2
  15. package/lib/core/intake/notes.mjs +12 -5
  16. package/lib/core/place.mjs +82 -0
  17. package/lib/core/publisher/collections.mjs +37 -2
  18. package/lib/core/publisher/index.mjs +23 -1
  19. package/lib/core/publisher/notes.mjs +73 -7
  20. package/lib/core/publisher/own.mjs +143 -0
  21. package/lib/core/publisher/questions.mjs +5 -3
  22. package/lib/core/social.mjs +62 -32
  23. package/lib/core/store.mjs +11 -1
  24. package/lib/core/wire.mjs +51 -15
  25. package/lib/device/admin/routes/lifecycle.mjs +1 -1
  26. package/lib/device/admin/routes/setup.mjs +17 -1
  27. package/lib/device/cli/commands/setup.mjs +50 -8
  28. package/lib/device/cli/context.mjs +1 -1
  29. package/lib/device/migrate.mjs +1 -1
  30. package/lib/device/setup.mjs +45 -8
  31. package/lib/gateway/front-core.mjs +54 -58
  32. package/lib/gateway/gateway-core.mjs +87 -8
  33. package/lib/gateway/relay-extras.mjs +89 -0
  34. package/lib/gateway/token-claims.mjs +16 -0
  35. package/lib/pod/location.mjs +52 -0
  36. package/lib/pod/notes.mjs +2 -4
  37. package/lib/pod/transport.mjs +171 -22
  38. package/lib/pod/type-index.mjs +101 -0
  39. package/lib/pod/urls.mjs +6 -0
  40. package/lib/server/embed.mjs +7 -6
  41. package/lib/session/fedi-account.mjs +19 -10
  42. package/lib/session/package.json +1 -1
  43. package/package.json +1 -1
  44. package/run-agent.mjs +2 -0
  45. package/web/admin/actors.js +2 -0
  46. package/web/admin/index.html +9 -0
  47. package/web/admin/record.js +4 -1
  48. package/web/admin/setup/index.html +17 -1
  49. package/web/admin/setup/setup.js +18 -5
  50. package/web/app/README.md +1 -1
  51. package/web/app/admin-facade.mjs +1 -1
  52. package/web/app/agent.mjs +11 -2
  53. package/web/app/boot.mjs +72 -32
  54. package/web/app/dist/boot.js +430 -86
  55. package/web/app/dist/boot.js.map +4 -4
  56. package/web/app/dist/sw.js +3294 -2648
  57. package/web/app/dist/sw.js.map +4 -4
  58. package/web/app/index.html +16 -0
  59. package/web/app/signup.mjs +65 -26
  60. package/web/front/run.html +7 -1
  61. package/web/front/run.js +30 -4
@@ -16984,7 +16984,7 @@ var require_fromRdf = __commonJS({
16984
16984
  RDF_FIRST,
16985
16985
  RDF_REST,
16986
16986
  RDF_NIL,
16987
- RDF_TYPE,
16987
+ RDF_TYPE: RDF_TYPE2,
16988
16988
  // RDF_PLAIN_LITERAL,
16989
16989
  // RDF_XML_LITERAL,
16990
16990
  RDF_JSON_LITERAL,
@@ -17043,7 +17043,7 @@ var require_fromRdf = __commonJS({
17043
17043
  if (objectIsNode && !(objectNodeId in nodeMap)) {
17044
17044
  nodeMap[objectNodeId] = { "@id": objectNodeId };
17045
17045
  }
17046
- if (p === RDF_TYPE && !useRdfType && objectIsNode) {
17046
+ if (p === RDF_TYPE2 && !useRdfType && objectIsNode) {
17047
17047
  _addValue(node, "@type", objectNodeId, { propertyIsArray: true });
17048
17048
  continue;
17049
17049
  }
@@ -17292,7 +17292,7 @@ var require_toRdf = __commonJS({
17292
17292
  RDF_FIRST,
17293
17293
  RDF_REST,
17294
17294
  RDF_NIL,
17295
- RDF_TYPE,
17295
+ RDF_TYPE: RDF_TYPE2,
17296
17296
  // RDF_PLAIN_LITERAL,
17297
17297
  // RDF_XML_LITERAL,
17298
17298
  RDF_JSON_LITERAL,
@@ -17350,7 +17350,7 @@ var require_toRdf = __commonJS({
17350
17350
  for (let property of properties) {
17351
17351
  const items = node[property];
17352
17352
  if (property === "@type") {
17353
- property = RDF_TYPE;
17353
+ property = RDF_TYPE2;
17354
17354
  } else if (isKeyword(property)) {
17355
17355
  continue;
17356
17356
  }
@@ -32543,6 +32543,15 @@ var REL = {
32543
32543
  owner: "http://www.w3.org/ns/solid/terms#owner"
32544
32544
  };
32545
32545
 
32546
+ // lib/pod/urls.mjs
32547
+ function podBaseOfWebId(webId) {
32548
+ const u = new URL(webId);
32549
+ u.hash = "";
32550
+ u.search = "";
32551
+ const dir = u.pathname.replace(/profile\/card$/u, "").replace(/[^/]*$/u, "");
32552
+ return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
32553
+ }
32554
+
32546
32555
  // lib/pod/transport.mjs
32547
32556
  var LDP = Namespace("http://www.w3.org/ns/ldp#");
32548
32557
  var DC = Namespace("http://purl.org/dc/terms/");
@@ -32551,6 +32560,16 @@ var RDF3 = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#");
32551
32560
  var ACL = Namespace("http://www.w3.org/ns/auth/acl#");
32552
32561
  var FOAF = Namespace("http://xmlns.com/foaf/0.1/");
32553
32562
  var AS = Namespace("https://www.w3.org/ns/activitystreams#");
32563
+ var RDFS = Namespace("http://www.w3.org/2000/01/rdf-schema#");
32564
+ function termProblem(t) {
32565
+ if (t?.termType === "Literal") return null;
32566
+ if (t?.termType !== "NamedNode") return `${t?.value ?? t} is not an IRI or a literal`;
32567
+ try {
32568
+ return /^https?:$/.test(new URL(t.value).protocol) ? null : `${t.value} is not an http(s) IRI`;
32569
+ } catch {
32570
+ return `${t.value} is not an absolute IRI`;
32571
+ }
32572
+ }
32554
32573
  var ACP_NS = "http://www.w3.org/ns/solid/acp#";
32555
32574
  var LISTING_MAX_BYTES = 10 * 1024 * 1024;
32556
32575
  var COOLDOWN_DEFAULT_MS = 6e4;
@@ -32903,16 +32922,10 @@ var PodTransport = class {
32903
32922
  * written back — an empty or foreign body must never become the new profile.
32904
32923
  */
32905
32924
  async linkAccountInProfile({ actorUrl, accountName, kind = "person", outbox = null }) {
32906
- const docUrl = this.webId.split("#")[0];
32907
- const res = await this.fetch(docUrl, { headers: { accept: "text/turtle" } });
32908
- if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} \u2192 ${res.status}`);
32909
- const g = graph();
32910
- parse2(await res.text(), g, docUrl, "text/turtle");
32911
- const doc = namedNode2(docUrl);
32912
32925
  const me = namedNode2(this.webId);
32913
- if (!g.statementsMatching(me, null, null, doc).length) {
32914
- throw new Error(`profile at ${docUrl} does not mention ${this.webId} \u2014 not rewriting it`);
32915
- }
32926
+ const podBase = podBaseOfWebId(this.webId);
32927
+ const docs = await this.profileDocs(podBase);
32928
+ const says = (s, p, o) => docs.some(({ g }) => g?.holds(s, p, o));
32916
32929
  const actor = namedNode2(actorUrl);
32917
32930
  const wanted = [
32918
32931
  [me, FOAF("account"), actor],
@@ -32923,17 +32936,20 @@ var PodTransport = class {
32923
32936
  // WebID is what dokieli reads); only where a door exists to take it.
32924
32937
  ...outbox ? [[me, AS("outbox"), namedNode2(outbox)]] : []
32925
32938
  ];
32926
- const missing = wanted.filter(([s, p, o]) => !g.holds(s, p, o, doc));
32927
- const stale = [
32928
- ...g.statementsMatching(actor, FOAF("accountName"), null, doc).filter((st2) => st2.object.value !== accountName),
32929
- ...outbox ? g.statementsMatching(me, AS("outbox"), null, doc).filter((st2) => st2.object.value !== outbox) : []
32930
- ];
32939
+ const missing = wanted.filter(([s, p, o]) => !says(s, p, o));
32940
+ const stale = [];
32941
+ for (const { g } of docs) {
32942
+ for (const st2 of g?.statementsMatching(actor, FOAF("accountName"), null) || []) {
32943
+ if (st2.object.value !== accountName) stale.push([st2.subject, st2.predicate, st2.object]);
32944
+ }
32945
+ if (outbox) {
32946
+ for (const st2 of g?.statementsMatching(me, AS("outbox"), null) || []) {
32947
+ if (st2.object.value !== outbox) stale.push([st2.subject, st2.predicate, st2.object]);
32948
+ }
32949
+ }
32950
+ }
32931
32951
  if (!missing.length && !stale.length) return false;
32932
- const deletes = stale.map((st2) => [st2.subject, st2.predicate, st2.object]);
32933
- if (await this.patchDocument(docUrl, missing, deletes)) return true;
32934
- for (const st2 of stale) g.remove(st2);
32935
- for (const [s, p, o] of missing) g.add(s, p, o, doc);
32936
- await this.put(docUrl, serialize(doc, g, docUrl, "text/turtle"), "text/turtle");
32952
+ await this.writeAboutWebId(podBase, { inserts: missing, deletes: stale });
32937
32953
  return true;
32938
32954
  }
32939
32955
  /**
@@ -32972,6 +32988,151 @@ ${clauses.join(";\n")}.
32972
32988
  if (res.status === 405 || res.status === 415 || res.status === 501) return false;
32973
32989
  throw new Error(`[${this.label}] PATCH ${docUrl} \u2192 ${res.status}`);
32974
32990
  }
32991
+ // ---- writing a profile or a type index: valid RDF, or nothing ----
32992
+ //
32993
+ // Two rules. A change is written only if the document it leaves behind is
32994
+ // valid RDF: the graph as it would be afterwards is serialised and parsed
32995
+ // back, and the subject that must stay described still is. And a profile
32996
+ // that will not take a write is not the end: the statements go to the first
32997
+ // document its rdfs:seeAlso names, inside the same pod, that takes them —
32998
+ // under the same check. A profile is what every Solid app signs in through;
32999
+ // one left broken breaks them all. Reading follows the same links.
33000
+ /** An IRI or a literal as a term, for the operations that pass plain values. */
33001
+ sym(iri) {
33002
+ try {
33003
+ return namedNode2(iri);
33004
+ } catch {
33005
+ throw new Error(`not written: ${iri} is not an absolute IRI`);
33006
+ }
33007
+ }
33008
+ literal(value) {
33009
+ return literal2(value);
33010
+ }
33011
+ /** A document as a graph, or null when it is not there. */
33012
+ async readRdf(docUrl) {
33013
+ const res = await this.fetch(docUrl, { headers: { accept: "text/turtle" } });
33014
+ if (res.status === 404 || res.status === 410) return null;
33015
+ if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} \u2192 ${res.status}`);
33016
+ const g = graph();
33017
+ parse2(await res.text(), g, docUrl, "text/turtle");
33018
+ return g;
33019
+ }
33020
+ /**
33021
+ * The document as it would be after the change, checked. Returns the Turtle
33022
+ * to write, or throws saying why nothing may be written.
33023
+ */
33024
+ checkedRdf(g, docUrl, { inserts = [], deletes = [], mustDescribe = null }) {
33025
+ const doc = namedNode2(docUrl);
33026
+ for (const [s, p, o] of inserts) {
33027
+ const bad = termProblem(s) || termProblem(p) || termProblem(o) || (s?.termType === "Literal" || p?.termType !== "NamedNode" ? "a literal subject or predicate" : null);
33028
+ if (bad) throw new Error(`not written: ${bad}`);
33029
+ }
33030
+ const after = graph();
33031
+ for (const st2 of g.statementsMatching(null, null, null, doc)) after.add(st2.subject, st2.predicate, st2.object, doc);
33032
+ for (const [s, p, o] of deletes) for (const st2 of after.statementsMatching(s, p, o, doc)) after.remove(st2);
33033
+ for (const [s, p, o] of inserts) if (!after.holds(s, p, o, doc)) after.add(s, p, o, doc);
33034
+ let text;
33035
+ try {
33036
+ text = serialize(doc, after, docUrl, "text/turtle");
33037
+ } catch (e) {
33038
+ throw new Error(`not written: ${docUrl} would not serialise (${e.message})`);
33039
+ }
33040
+ const back = graph();
33041
+ try {
33042
+ parse2(text, back, docUrl, "text/turtle");
33043
+ } catch (e) {
33044
+ throw new Error(`not written: ${docUrl} would not parse back (${e.message})`);
33045
+ }
33046
+ if (back.statementsMatching(null, null, null, doc).length !== after.statementsMatching(null, null, null, doc).length) {
33047
+ throw new Error(`not written: ${docUrl} would not read back as written`);
33048
+ }
33049
+ if (mustDescribe && !back.statementsMatching(namedNode2(mustDescribe), null, null, doc).length) {
33050
+ throw new Error(`not written: ${docUrl} would no longer describe ${mustDescribe}`);
33051
+ }
33052
+ return text;
33053
+ }
33054
+ /**
33055
+ * Change one document, or refuse. `g` is the document as read (null for a
33056
+ * new one). A PATCH carries only these statements; where the pod cannot
33057
+ * patch, the whole checked document is written. Returns the write's status.
33058
+ */
33059
+ async writeRdfChecked(docUrl, g, { inserts = [], deletes = [], mustDescribe = null }) {
33060
+ const current = g || graph();
33061
+ const doc = namedNode2(docUrl);
33062
+ const todo = inserts.filter(([s, p, o]) => !current.holds(s, p, o, doc));
33063
+ const gone = deletes.filter(([s, p, o]) => current.holds(s, p, o, doc));
33064
+ if (!todo.length && !gone.length) return 200;
33065
+ const text = this.checkedRdf(current, docUrl, { inserts: todo, deletes: gone, mustDescribe });
33066
+ if (g) {
33067
+ let res = null;
33068
+ try {
33069
+ res = await this.fetch(docUrl, {
33070
+ method: "PATCH",
33071
+ headers: { "content-type": "text/n3" },
33072
+ body: this.n3Patch(docUrl, todo, gone)
33073
+ });
33074
+ } catch {
33075
+ res = null;
33076
+ }
33077
+ if (res && res.status < 300) return res.status;
33078
+ if (res && ![405, 415, 501].includes(res.status)) return res.status;
33079
+ }
33080
+ const put = await this.fetch(docUrl, { method: "PUT", headers: { "content-type": "text/turtle" }, body: text });
33081
+ return put.status;
33082
+ }
33083
+ /**
33084
+ * The profile and the documents its rdfs:seeAlso names inside this pod,
33085
+ * each with its graph, the profile first. What any of them says about the
33086
+ * WebID is what the profile says.
33087
+ */
33088
+ async profileDocs(podBase) {
33089
+ const profileUrl = this.webId.split("#")[0];
33090
+ const g = await this.readRdf(profileUrl);
33091
+ if (!g) throw new Error(`no profile at ${profileUrl}`);
33092
+ const out = [{ url: profileUrl, g, profile: true }];
33093
+ const seen = /* @__PURE__ */ new Set([profileUrl]);
33094
+ for (const st2 of g.statementsMatching(null, RDFS("seeAlso"), null)) {
33095
+ const url = st2.object.value.split("#")[0];
33096
+ if (seen.has(url) || !url.startsWith(podBase)) continue;
33097
+ seen.add(url);
33098
+ out.push({ url, g: await this.readRdf(url).catch(() => null), profile: false });
33099
+ }
33100
+ return out;
33101
+ }
33102
+ /** Every value the WebID has for `predicate`, across the profile and its seeAlso documents. */
33103
+ webIdValues(docs, predicate) {
33104
+ const me = namedNode2(this.webId);
33105
+ const p = namedNode2(predicate);
33106
+ const out = [];
33107
+ for (const { g } of docs) for (const st2 of g?.statementsMatching(me, p, null) || []) out.push(st2.object.value);
33108
+ return [...new Set(out)];
33109
+ }
33110
+ /**
33111
+ * Write statements about the WebID: to the profile, or — if the profile will
33112
+ * not take them — to the first of its seeAlso documents that will. Every
33113
+ * write checked. Returns the document written to, or null when nothing
33114
+ * needed writing; throws when none would take it.
33115
+ */
33116
+ async writeAboutWebId(podBase, { inserts = [], deletes = [] }) {
33117
+ const docs = await this.profileDocs(podBase);
33118
+ const holds = ([s, p, o]) => docs.some(({ g }) => g?.holds(s, p, o));
33119
+ const todo = inserts.filter((t) => !holds(t));
33120
+ const gone = deletes.filter(holds);
33121
+ if (!todo.length && !gone.length) return null;
33122
+ const refusals = [];
33123
+ for (const d of docs) {
33124
+ const here = gone.filter(([s, p, o]) => d.g?.holds(s, p, o));
33125
+ const status = await this.writeRdfChecked(d.url, d.g, {
33126
+ inserts: todo,
33127
+ deletes: here,
33128
+ mustDescribe: d.profile ? this.webId : null
33129
+ });
33130
+ if (status < 300) return d.url;
33131
+ refusals.push(`${d.url} \u2192 ${status}`);
33132
+ if (status !== 401 && status !== 403) break;
33133
+ }
33134
+ throw new Error(`not written: neither the profile nor a document it names would take it (${refusals.join("; ")})`);
33135
+ }
32975
33136
  };
32976
33137
 
32977
33138
  // web/app/pod-remote.mjs
@@ -33057,15 +33218,6 @@ async function resourceExists(fetchImpl, url, { timeoutMs = OWNER_LOOKUP_MS } =
33057
33218
  }
33058
33219
  }
33059
33220
 
33060
- // lib/pod/urls.mjs
33061
- function podBaseOfWebId(webId) {
33062
- const u = new URL(webId);
33063
- u.hash = "";
33064
- u.search = "";
33065
- const dir = u.pathname.replace(/profile\/card$/u, "").replace(/[^/]*$/u, "");
33066
- return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
33067
- }
33068
-
33069
33221
  // web/app/idb-kv.mjs
33070
33222
  var DB = "fedipod-accounts";
33071
33223
  function open() {
@@ -33112,10 +33264,139 @@ async function cacheOpenedKeys(actorUrl, keysRecord) {
33112
33264
  }
33113
33265
  var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
33114
33266
 
33267
+ // lib/pod/type-index.mjs
33268
+ var SOLID = "http://www.w3.org/ns/solid/terms#";
33269
+ var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
33270
+ var AS_ACTOR = "https://www.w3.org/ns/activitystreams#Actor";
33271
+ var PUBLIC_READ = ["Read"];
33272
+ var newIndexUrl = (podBase) => `${podBase}settings/publicTypeIndex.ttl`;
33273
+ async function findPublicIndex(pod, podBase) {
33274
+ const docs = await pod.profileDocs(podBase);
33275
+ return pod.webIdValues(docs, SOLID + "publicTypeIndex")[0] || null;
33276
+ }
33277
+ async function actorsIn(pod, indexUrl) {
33278
+ const g = await pod.readRdf(indexUrl).catch(() => null);
33279
+ if (!g) return [];
33280
+ const out = [];
33281
+ for (const reg of g.each(null, pod.sym(SOLID + "forClass"), pod.sym(AS_ACTOR))) {
33282
+ for (const inst of g.each(reg, pod.sym(SOLID + "instance"), null)) out.push(inst.value);
33283
+ }
33284
+ return [...new Set(out)];
33285
+ }
33286
+ function regFor(indexUrl, actorUrl) {
33287
+ let h = 0;
33288
+ for (const c of actorUrl) h = h * 31 + c.charCodeAt(0) >>> 0;
33289
+ return `${indexUrl}#actor-${h.toString(36)}`;
33290
+ }
33291
+ async function register(pod, indexUrl, actorUrl) {
33292
+ const g = await pod.readRdf(indexUrl);
33293
+ if (!g) throw new Error(`no type index at ${indexUrl}`);
33294
+ if ((await actorsIn(pod, indexUrl)).includes(actorUrl)) return false;
33295
+ const reg = pod.sym(regFor(indexUrl, actorUrl));
33296
+ const status = await pod.writeRdfChecked(indexUrl, g, {
33297
+ inserts: [
33298
+ [reg, pod.sym(RDF_TYPE), pod.sym(SOLID + "TypeRegistration")],
33299
+ [reg, pod.sym(SOLID + "forClass"), pod.sym(AS_ACTOR)],
33300
+ [reg, pod.sym(SOLID + "instance"), pod.sym(actorUrl)]
33301
+ ],
33302
+ mustDescribe: indexUrl
33303
+ });
33304
+ if (status >= 300) throw new Error(`the type index at ${indexUrl} did not take the registration (${status})`);
33305
+ return true;
33306
+ }
33307
+ async function createPublicIndex(pod, podBase) {
33308
+ const url = newIndexUrl(podBase);
33309
+ const doc = pod.sym(url);
33310
+ const existing = await pod.readRdf(url).catch(() => null);
33311
+ if (!existing) {
33312
+ const status = await pod.writeRdfChecked(url, null, {
33313
+ inserts: [
33314
+ [doc, pod.sym(RDF_TYPE), pod.sym(SOLID + "TypeIndex")],
33315
+ [doc, pod.sym(RDF_TYPE), pod.sym(SOLID + "ListedDocument")]
33316
+ ],
33317
+ mustDescribe: url
33318
+ });
33319
+ if (status >= 300) throw new Error(`could not make a type index at ${url} (${status})`);
33320
+ }
33321
+ await pod.setAcl(url, PUBLIC_READ);
33322
+ await pod.writeAboutWebId(podBase, {
33323
+ inserts: [[pod.sym(pod.webId), pod.sym(SOLID + "publicTypeIndex"), doc]]
33324
+ });
33325
+ return url;
33326
+ }
33327
+ async function registeredActors(pod, podBase) {
33328
+ const index = await findPublicIndex(pod, podBase).catch(() => null);
33329
+ if (!index) return [];
33330
+ return (await actorsIn(pod, index)).filter((a) => a.startsWith(podBase) && a.endsWith("ap/actor"));
33331
+ }
33332
+
33333
+ // lib/pod/location.mjs
33334
+ var RESERVED = /* @__PURE__ */ new Set(["profile", "settings", ".well-known"]);
33335
+ var SEGMENT = /^[A-Za-z0-9._-]+$/u;
33336
+ function podRootPath(podBase) {
33337
+ return new URL(podBase).pathname;
33338
+ }
33339
+ function rootFromContainer(podBase, typed, name) {
33340
+ const base = new URL(podBase);
33341
+ let path = String(typed ?? "").trim() || base.pathname;
33342
+ if (!path.startsWith("/")) path = "/" + path;
33343
+ if (!path.endsWith("/")) path += "/";
33344
+ if (!path.startsWith(base.pathname)) {
33345
+ return { problem: `that is not in your pod \u2014 your pod starts at ${base.pathname}` };
33346
+ }
33347
+ const inside = path.slice(base.pathname.length);
33348
+ const segments = inside.split("/").filter(Boolean);
33349
+ if (segments.some((s) => s === "." || s === "..")) return { problem: "a container path cannot go up with .." };
33350
+ if (segments.some((s) => !SEGMENT.test(s))) {
33351
+ return { problem: "container names are letters, digits, dots, dashes and underscores" };
33352
+ }
33353
+ if (segments.length && RESERVED.has(segments[0])) {
33354
+ return { problem: `${base.pathname}${segments[0]}/ belongs to your pod itself \u2014 choose another container` };
33355
+ }
33356
+ if (segments.length > 8) return { problem: "that is nested too deep \u2014 eight containers at most" };
33357
+ return { root: (segments.length ? segments.join("/") + "/" : "") + name };
33358
+ }
33359
+ function rootOfActor(podBase, actorUrl) {
33360
+ const a = String(actorUrl || "");
33361
+ if (!a.startsWith(podBase) || !a.endsWith("ap/actor")) return null;
33362
+ const root = a.slice(podBase.length, -"ap/actor".length);
33363
+ return root.endsWith("/") ? root : null;
33364
+ }
33365
+
33366
+ // lib/core/wire.mjs
33367
+ var DEFAULT_ROOT = "fedipod/";
33368
+
33369
+ // lib/core/place.mjs
33370
+ var chosenRoot = (podBase, typed) => rootFromContainer(podBase, typed, DEFAULT_ROOT);
33371
+ async function candidateRoots(pod, podBase) {
33372
+ const recorded = await registeredActors(pod, podBase).catch(() => []);
33373
+ const roots = recorded.map((a) => rootOfActor(podBase, a)).filter(Boolean);
33374
+ return [.../* @__PURE__ */ new Set([...roots, DEFAULT_ROOT])];
33375
+ }
33376
+ async function findAccount(pod, podBase, read, accept = () => true) {
33377
+ for (const root of await candidateRoots(pod, podBase)) {
33378
+ const config = await read(root).catch(() => null);
33379
+ if (config && accept(config)) return { root, config };
33380
+ }
33381
+ return null;
33382
+ }
33383
+ async function recordPlace(pod, podBase, actorAtPod, { create = false } = {}) {
33384
+ let index = await findPublicIndex(pod, podBase);
33385
+ if (!index) {
33386
+ if (!create) return "no-index";
33387
+ index = await createPublicIndex(pod, podBase);
33388
+ }
33389
+ return await register(pod, index, actorAtPod) ? "registered" : "already";
33390
+ }
33391
+ async function hasPublicIndex(pod, podBase) {
33392
+ return !!await findPublicIndex(pod, podBase).catch(() => null);
33393
+ }
33394
+
33115
33395
  // web/app/signup.mjs
33116
- var AP_ROOT = "fedipod/";
33117
- var actorUrlFor = (pod) => `${pod}${AP_ROOT}ap/actor`;
33118
- var keysDocFor = (pod) => `${pod}${AP_ROOT}ap-state/keys.json`;
33396
+ var actorUrlFor = (pod, root) => `${pod}${root}ap/actor`;
33397
+ var keysDocFor = (pod, root) => `${pod}${root}ap-state/keys.json`;
33398
+ var stateOf = (pod, root) => `${pod}${root}ap-state/`;
33399
+ var readConfigAt = (remote, pod) => (root) => readConfig(remote, { state: stateOf(pod, root) });
33119
33400
  var HANDLE_RE = /^[a-z0-9-]{2,30}$/;
33120
33401
  function handleProblem(handle) {
33121
33402
  if (!handle) return "a handle is required";
@@ -33124,7 +33405,7 @@ function handleProblem(handle) {
33124
33405
  return null;
33125
33406
  }
33126
33407
  var PROGRESS = /* @__PURE__ */ new Map();
33127
- var progressKey = (webId, a) => [webId, a.handle, a.shape || "pod"].join("|");
33408
+ var progressKey = (webId, a) => [webId, a.handle, a.shape || "pod", a.container || ""].join("|");
33128
33409
  async function assertFrontNameFree(frontOrigin, handle) {
33129
33410
  const res = await fetch(
33130
33411
  `${frontOrigin.replace(/\/$/, "")}/api/handle?handle=${encodeURIComponent(handle)}`,
@@ -33153,10 +33434,23 @@ async function signUp(answers, { session, onStep = () => {
33153
33434
  ok: (note) => onStep(key2, "ok", note),
33154
33435
  skip: (note) => onStep(key2, "skipped", note)
33155
33436
  });
33437
+ const place = chosenRoot(pod, answers.container);
33438
+ if (place.problem) throw new Error(`Where to store it: ${place.problem}.`);
33439
+ const root = place.root;
33440
+ const remote = new BrowserRemotePod(session, { webId, role: "signup", log: () => {
33441
+ } });
33156
33442
  const podStep = step("pod");
33157
33443
  if (!prog.pod) {
33158
33444
  podStep.running("checking your pod");
33159
- if (await resourceExists(session.fetch, actorUrlFor(pod))) throw new Error("The pod already hosts a FediPod account. If you want a second account, put it on a different pod.");
33445
+ if (await findAccount(remote, pod, readConfigAt(remote, pod)) || await resourceExists(session.fetch, actorUrlFor(pod, root))) {
33446
+ throw new Error("The pod already hosts a FediPod account. If you want a second account, put it on a different pod.");
33447
+ }
33448
+ if (await resourceExists(session.fetch, `${pod}${root}`)) {
33449
+ throw new Error(`${new URL(pod + root).pathname} is already there and is not a FediPod account \u2014 choose another container.`);
33450
+ }
33451
+ if (!answers.createIndex && !await hasPublicIndex(remote, pod)) {
33452
+ throw Object.assign(new Error("Your profile names no public type index."), { code: "needs-index" });
33453
+ }
33160
33454
  prog.pod = pod;
33161
33455
  podStep.ok(pod);
33162
33456
  } else {
@@ -33165,10 +33459,10 @@ async function signUp(answers, { session, onStep = () => {
33165
33459
  const pathPod = new URL(pod).pathname !== "/";
33166
33460
  const fronted = pathPod || wantsFront;
33167
33461
  if (fronted && !frontOrigin) {
33168
- throw new Error(`${pod} is a suffix-based host, so its address must live at a gateway, and this page has none.`);
33462
+ throw new Error(`${pod} is a suffixed pod, so its address must live at a gateway, and this page has none.`);
33169
33463
  }
33170
33464
  if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
33171
- const actorUrl = actorUrlFor(pod);
33465
+ const actorUrl = actorUrlFor(pod, root);
33172
33466
  const frontActor = fronted ? `${frontOrigin.replace(/\/$/, "")}/u/${handle}/ap/actor` : null;
33173
33467
  let gateway = answers.gateway || prog.gateway || null;
33174
33468
  if (frontOrigin && !gateway) {
@@ -33182,7 +33476,7 @@ async function signUp(answers, { session, onStep = () => {
33182
33476
  // bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
33183
33477
  // the container the agent drains, where nothing would ever read it. The
33184
33478
  // manage surface has always sent `urls.home`; this is the same value.
33185
- body: JSON.stringify({ handle, podHome: `${pod}${AP_ROOT}`, actorUrl, kind: "person", fronted })
33479
+ body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl, kind: "person", fronted })
33186
33480
  });
33187
33481
  const d = await res.json().catch(() => ({}));
33188
33482
  if (res.status !== 201 || !d.hmacSecret) {
@@ -33194,8 +33488,6 @@ async function signUp(answers, { session, onStep = () => {
33194
33488
  } else if (frontOrigin && gateway) {
33195
33489
  step("gateway").ok();
33196
33490
  }
33197
- const remote = new BrowserRemotePod(session, { webId, role: "signup", log: () => {
33198
- } });
33199
33491
  const keysStep = step("keys");
33200
33492
  let keys;
33201
33493
  if (!prog.keysStored) {
@@ -33204,12 +33496,12 @@ async function signUp(answers, { session, onStep = () => {
33204
33496
  keys.mintedFor = gateway?.frontActor || actorUrl;
33205
33497
  try {
33206
33498
  await provisionKey(remote, {
33207
- stateUrl: `${pod}${AP_ROOT}ap-state/`,
33208
- keysUrl: keysDocFor(pod),
33499
+ stateUrl: stateOf(pod, root),
33500
+ keysUrl: keysDocFor(pod, root),
33209
33501
  keys
33210
33502
  });
33211
33503
  } catch (e) {
33212
- throw new Error(`could not store the signing key on the pod (${e.message}). You are signed in as ${webId} \u2014 that WebID must own ${pod} and its ${AP_ROOT} must be writable by it.`);
33504
+ throw new Error(`could not store the signing key on the pod (${e.message}). You are signed in as ${webId} \u2014 that WebID must own ${pod} and ${root} in it must be writable by it.`);
33213
33505
  }
33214
33506
  await cacheOpenedKeys(actorUrl, keys);
33215
33507
  prog.keys = keys;
@@ -33221,7 +33513,7 @@ async function signUp(answers, { session, onStep = () => {
33221
33513
  }
33222
33514
  const config = {
33223
33515
  remotePod: pod,
33224
- root: AP_ROOT,
33516
+ root,
33225
33517
  handle,
33226
33518
  name: handle,
33227
33519
  issuer: String(session.issuer || "").replace(/\/+$/, ""),
@@ -33229,10 +33521,21 @@ async function signUp(answers, { session, onStep = () => {
33229
33521
  ...gateway ? { gateway } : {}
33230
33522
  };
33231
33523
  try {
33232
- await writeConfig(remote, { state: `${pod}${AP_ROOT}ap-state/` }, config);
33524
+ await writeConfig(remote, { state: stateOf(pod, root) }, config);
33233
33525
  } catch (e) {
33234
33526
  throw new Error(`could not store the config on the pod (${e.message})`);
33235
33527
  }
33528
+ if (!prog.placed) {
33529
+ const placeStep = step("place");
33530
+ placeStep.running("recording where your account lives");
33531
+ try {
33532
+ await recordPlace(remote, pod, actorUrl, { create: !!answers.createIndex });
33533
+ } catch (e) {
33534
+ throw new Error(`could not record where your account lives in your type index (${e.message})`);
33535
+ }
33536
+ prog.placed = true;
33537
+ placeStep.ok();
33538
+ }
33236
33539
  PROGRESS.delete(key);
33237
33540
  const host = gateway?.frontActor ? new URL(gateway.frontActor).host : new URL(pod).host;
33238
33541
  return {
@@ -33249,8 +33552,9 @@ async function readAccount(session) {
33249
33552
  const pod = podBaseOfWebId(session.webId);
33250
33553
  const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
33251
33554
  } });
33252
- const config = await readConfig(remote, { state: `${pod}${AP_ROOT}ap-state/` }).catch(() => null);
33253
- if (!config) return null;
33555
+ const found = await findAccount(remote, pod, readConfigAt(remote, pod), (c) => (c.kind || "person") === "person");
33556
+ if (!found) return null;
33557
+ const { root, config } = found;
33254
33558
  const frontActor = config.gateway?.frontActor || null;
33255
33559
  let frontHost = null;
33256
33560
  try {
@@ -33264,7 +33568,7 @@ async function readAccount(session) {
33264
33568
  } catch {
33265
33569
  podHost = "";
33266
33570
  }
33267
- return { pod, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
33571
+ return { pod, root, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
33268
33572
  }
33269
33573
  async function moveIn(answers, { session, onStep = () => {
33270
33574
  }, frontOrigin = null } = {}) {
@@ -33280,10 +33584,10 @@ async function moveIn(answers, { session, onStep = () => {
33280
33584
  const here = await readAccount(session);
33281
33585
  if (!here?.frontActor) throw new Error("this pod holds no account at another gateway to move here");
33282
33586
  if (here.frontHost === new URL(origin).host) throw new Error("this account already lives at this gateway");
33283
- const { pod, config: old } = here;
33587
+ const { pod, root, config: old } = here;
33284
33588
  const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
33285
33589
  } });
33286
- const urls = { state: `${pod}${AP_ROOT}ap-state/` };
33590
+ const urls = { state: stateOf(pod, root) };
33287
33591
  const keys = await readKeys(remote, urls);
33288
33592
  if (!keys?.rsa) throw new Error("could not read this account's signing key on the pod");
33289
33593
  if (isKeyEnvelope(keys)) throw new Error(`this account's key is still under a password from an earlier version \u2014 sign in once at ${here.frontHost} first, then come back`);
@@ -33294,7 +33598,7 @@ async function moveIn(answers, { session, onStep = () => {
33294
33598
  const res = await session.fetch(`${origin}/api/attach`, {
33295
33599
  method: "POST",
33296
33600
  headers: { "content-type": "application/json" },
33297
- body: JSON.stringify({ handle, podHome: `${pod}${AP_ROOT}`, actorUrl: actorUrlFor(pod), kind: old.kind || "person", fronted: true })
33601
+ body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl: actorUrlFor(pod, root), kind: old.kind || "person", fronted: true })
33298
33602
  });
33299
33603
  const d = await res.json().catch(() => ({}));
33300
33604
  if (res.status !== 201 || !d.hmacSecret) throw new Error(`could not attach to this gateway (HTTP ${res.status}): ${d.error || ""}`);
@@ -33322,7 +33626,7 @@ async function moveIn(answers, { session, onStep = () => {
33322
33626
  } catch (e) {
33323
33627
  throw new Error(`could not update your account on the pod (${e.message})`);
33324
33628
  }
33325
- await cacheOpenedKeys(actorUrlFor(pod), keys);
33629
+ await cacheOpenedKeys(actorUrlFor(pod, root), keys);
33326
33630
  keysStep.ok();
33327
33631
  return { config, address: `@${handle}@${new URL(origin).host}`, movedFrom: config.movedFrom };
33328
33632
  }
@@ -33614,18 +33918,14 @@ async function bootWorker({ reset = false } = {}) {
33614
33918
  async function readAccountState() {
33615
33919
  const session = await getSession();
33616
33920
  if (!session) throw new Error("Sign in first.");
33617
- const podFromWebId = podBaseOfWebId(session.webId);
33618
- const state = `${podFromWebId}${AP_ROOT}ap-state/`;
33921
+ const here = await readAccount(session);
33922
+ if (!here) throw new Error(`could not find a FediPod account on ${podBaseOfWebId(session.webId)}`);
33619
33923
  const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
33620
33924
  } });
33621
- const urls = { state };
33622
- const [cfg, doc] = await Promise.all([
33623
- readConfig(remote, urls),
33624
- readKeys(remote, urls)
33625
- ]);
33626
- if (!cfg) throw new Error(`could not read this account's config under ${state}`);
33627
- const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
33628
- return { remote, urls, cfg, doc, actorUrl };
33925
+ const urls = { state: `${here.pod}${here.root}ap-state/` };
33926
+ const doc = await readKeys(remote, urls);
33927
+ const actorUrl = `${here.config.remotePod || here.pod}${here.config.root || here.root}ap/actor`;
33928
+ return { remote, urls, cfg: here.config, doc, actorUrl };
33629
33929
  }
33630
33930
  window.fedipodUnlock = async (password) => {
33631
33931
  if (!password) throw new Error("Enter your password.");
@@ -33679,18 +33979,28 @@ function parseAddress(input) {
33679
33979
  if (!handle || !host || !host.includes(".")) return null;
33680
33980
  return { handle, host };
33681
33981
  }
33682
- async function issuerForPod(pod) {
33982
+ async function issuerForActor(actorUrl) {
33683
33983
  try {
33684
- const authz = await readIssuer(`${pod}${AP_ROOT}ap/actor`);
33984
+ const authz = await readIssuer(actorUrl);
33685
33985
  if (authz) return new URL(authz).origin;
33686
33986
  } catch {
33687
33987
  }
33688
- const u = new URL(pod);
33689
- if (u.pathname !== "/") return u.origin;
33988
+ const u = new URL(actorUrl);
33989
+ const own = await fetch(`${u.origin}/.well-known/openid-configuration`, { headers: { accept: "application/json" } }).catch(() => null);
33990
+ if (own?.ok) return u.origin;
33690
33991
  const parent = u.host.split(".").slice(1).join(".");
33691
33992
  return `https://${parent || u.host}`;
33692
33993
  }
33693
- async function podForFrontedAddress(handle) {
33994
+ async function actorForPodAddress(handle, host) {
33995
+ const res = await fetch(
33996
+ `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${host}`)}`,
33997
+ { headers: { accept: "application/jrd+json, application/json" } }
33998
+ ).catch(() => null);
33999
+ const doc = res?.ok ? await res.json().catch(() => ({})) : {};
34000
+ const self2 = (doc.links || []).find((l) => l.rel === "self" && /activity\+json|ld\+json/u.test(l.type || ""));
34001
+ return self2?.href || `https://${host}/fedipod/ap/actor`;
34002
+ }
34003
+ async function actorForFrontedAddress(handle) {
33694
34004
  const res = await fetch(
33695
34005
  `/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
33696
34006
  { headers: { accept: "application/jrd+json, application/json" } }
@@ -33700,17 +34010,15 @@ async function podForFrontedAddress(handle) {
33700
34010
  const doc = await res.json().catch(() => ({}));
33701
34011
  const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
33702
34012
  if (!podActorId) throw new Error(`@${handle}@${location.host} lives here but names no pod to sign in to`);
33703
- const tail = `${AP_ROOT}ap/actor`;
33704
- if (!podActorId.endsWith(tail)) throw new Error(`the pod actor ${podActorId} is not where a FediPod pod keeps one`);
33705
- return podActorId.slice(0, -tail.length);
34013
+ return podActorId;
33706
34014
  }
33707
34015
  window.fedipodSignin = async ({ address }) => {
33708
34016
  const parsed = parseAddress(address);
33709
34017
  if (!parsed) throw new Error("Enter your address as @you@yourpod (for example @alice@alice.solidcommunity.net).");
33710
34018
  const bad = handleProblem(parsed.handle);
33711
34019
  if (bad) throw new Error(bad);
33712
- const pod = parsed.host === location.host.toLowerCase() ? await podForFrontedAddress(parsed.handle) : `https://${parsed.host}/`;
33713
- const issuer = await issuerForPod(pod);
34020
+ const actor = parsed.host === location.host.toLowerCase() ? await actorForFrontedAddress(parsed.handle) : await actorForPodAddress(parsed.handle, parsed.host);
34021
+ const issuer = await issuerForActor(actor);
33714
34022
  const { authorizationUrl } = await beginLogin({ issuer, redirectUri: REDIRECT });
33715
34023
  location.href = authorizationUrl;
33716
34024
  };
@@ -33949,6 +34257,8 @@ ${e.detail}` : "");
33949
34257
  $("brand").hidden = false;
33950
34258
  $("pane-form").hidden = false;
33951
34259
  $("signed-pod").textContent = signedPod;
34260
+ $("container").placeholder = podRootPath(signedPod);
34261
+ $("container-field").hidden = !!moveFrom;
33952
34262
  $("movein-note").hidden = !moveFrom;
33953
34263
  if (moveFrom) {
33954
34264
  $("movein-from").textContent = moveFrom.address;
@@ -33999,7 +34309,12 @@ ${e.detail}` : "");
33999
34309
  }
34000
34310
  };
34001
34311
  const shape = () => pathPod() || moveFrom ? "front" : f().shape.value;
34002
- const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape() });
34312
+ const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape(), container: f().container.value.trim() });
34313
+ const previewPlace = () => {
34314
+ if (!signedPod) return;
34315
+ const r = chosenRoot(signedPod, f().container.value);
34316
+ $("place").textContent = r.problem ? "\u2026" : signedPod + r.root;
34317
+ };
34003
34318
  const previewAddr = () => {
34004
34319
  const handle = f().handle.value.trim().toLowerCase();
34005
34320
  let host = "";
@@ -34030,6 +34345,7 @@ ${e.detail}` : "");
34030
34345
  applyMode();
34031
34346
  applyShape();
34032
34347
  previewAddr();
34348
+ previewPlace();
34033
34349
  });
34034
34350
  applyMode();
34035
34351
  const STEP_IDS = ["step-1", "step-2"];
@@ -34043,6 +34359,8 @@ ${e.detail}` : "");
34043
34359
  if (n === 2) {
34044
34360
  applyShape();
34045
34361
  previewAddr();
34362
+ previewPlace();
34363
+ $("index-ask").hidden = true;
34046
34364
  }
34047
34365
  if (FOCUS[n]) $(FOCUS[n]).focus();
34048
34366
  };
@@ -34053,6 +34371,10 @@ ${e.detail}` : "");
34053
34371
  const validateStep2 = () => {
34054
34372
  const hp = window.fedipodHandleProblem(f().handle.value.trim().toLowerCase());
34055
34373
  if (hp) return `Fediverse handle: ${hp}`;
34374
+ if (!moveFrom) {
34375
+ const where = chosenRoot(signedPod, f().container.value).problem;
34376
+ if (where) return `Where to store it: ${where}`;
34377
+ }
34056
34378
  return null;
34057
34379
  };
34058
34380
  $("create").addEventListener("click", showForm);
@@ -34088,18 +34410,15 @@ ${e.detail}` : "");
34088
34410
  showLanding();
34089
34411
  goStep(1);
34090
34412
  });
34091
- const LABELS = { pod: "Checking your pod", keys: "Making your signing key", gateway: "Connecting your mail door" };
34413
+ const LABELS = {
34414
+ pod: "Checking your pod",
34415
+ keys: "Making your signing key",
34416
+ gateway: "Connecting your mail door",
34417
+ place: "Recording where your account lives"
34418
+ };
34092
34419
  const MOVE_LABELS = { pod: "Reading your account on your pod", gateway: "Taking your address here", keys: "Moving your key and account record" };
34093
- $("form").addEventListener("submit", async (e) => {
34094
- e.preventDefault();
34095
- $("form-error").textContent = "";
34096
- const e2 = validateStep2();
34097
- if (e2) {
34098
- $("form-error").textContent = e2;
34099
- goStep(2);
34100
- return;
34101
- }
34102
- const a = answers();
34420
+ const run = async (createIndex = false) => {
34421
+ const a = { ...answers(), ...createIndex ? { createIndex: true } : {} };
34103
34422
  $("pane-form").hidden = true;
34104
34423
  $("running").hidden = false;
34105
34424
  $("running-title").textContent = "Setting up\u2026";
@@ -34122,10 +34441,35 @@ ${e.detail}` : "");
34122
34441
  else await window.fedipodSignup({ ...a, onStep });
34123
34442
  location.href = "/admin/client/";
34124
34443
  } catch (err) {
34444
+ if (err?.code === "needs-index") {
34445
+ backToForm();
34446
+ $("index-ask").hidden = false;
34447
+ $("index-yes").focus();
34448
+ return;
34449
+ }
34125
34450
  $("running-title").textContent = "Setup did not finish";
34126
34451
  $("run-error").textContent = err.message || String(err);
34127
34452
  $("run-actions").hidden = false;
34128
34453
  }
34454
+ };
34455
+ $("form").addEventListener("submit", async (e) => {
34456
+ e.preventDefault();
34457
+ $("form-error").textContent = "";
34458
+ const e2 = validateStep2();
34459
+ if (e2) {
34460
+ $("form-error").textContent = e2;
34461
+ goStep(2);
34462
+ return;
34463
+ }
34464
+ await run();
34465
+ });
34466
+ $("index-yes").addEventListener("click", () => {
34467
+ $("index-ask").hidden = true;
34468
+ run(true);
34469
+ });
34470
+ $("index-no").addEventListener("click", () => {
34471
+ $("index-ask").hidden = true;
34472
+ $("form-error").textContent = "Sign-up stopped. Nothing was written to your pod.";
34129
34473
  });
34130
34474
  if (pendingIdentity) await showIdentity();
34131
34475
  })();