fedipod-server 0.28.0 → 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.
- package/README.md +27 -28
- package/config/server.json +22 -0
- package/dist/claims.js +3 -2
- package/dist/directory.d.ts +2 -0
- package/dist/handler.d.ts +4 -1
- package/dist/handler.js +32 -3
- package/dist/handler.jsonld +4 -0
- package/dist/store-pod.js +37 -0
- package/lib/client/c2s.mjs +97 -43
- package/lib/client/masto/accounts.mjs +1 -0
- package/lib/connections/bskyfeed.mjs +6 -0
- package/lib/core/deliver.mjs +68 -22
- package/lib/core/intake/activities.mjs +18 -2
- package/lib/core/intake/group.mjs +3 -1
- package/lib/core/intake/index.mjs +50 -6
- package/lib/core/intake/notes.mjs +12 -5
- package/lib/core/lease.mjs +7 -7
- package/lib/core/place.mjs +82 -0
- package/lib/core/publisher/collections.mjs +37 -2
- package/lib/core/publisher/index.mjs +23 -1
- package/lib/core/publisher/notes.mjs +73 -7
- package/lib/core/publisher/own.mjs +143 -0
- package/lib/core/publisher/questions.mjs +5 -3
- package/lib/core/social.mjs +62 -32
- package/lib/core/store.mjs +36 -6
- package/lib/core/wire.mjs +51 -15
- package/lib/device/admin/routes/lifecycle.mjs +1 -1
- package/lib/device/admin/routes/setup.mjs +17 -1
- package/lib/device/cli/commands/setup.mjs +50 -8
- package/lib/device/cli/context.mjs +1 -1
- package/lib/device/migrate.mjs +1 -1
- package/lib/device/setup.mjs +45 -8
- package/lib/gateway/caches.mjs +36 -0
- package/lib/gateway/front-core.mjs +80 -111
- package/lib/gateway/gateway-core.mjs +91 -9
- package/lib/gateway/headers.mjs +49 -0
- package/lib/gateway/quiet.mjs +7 -2
- package/lib/gateway/relay-extras.mjs +89 -0
- package/lib/gateway/token-claims.mjs +16 -0
- package/lib/pod/containers.mjs +5 -1
- package/lib/pod/inbox.mjs +22 -3
- package/lib/pod/location.mjs +52 -0
- package/lib/pod/notes.mjs +2 -4
- package/lib/pod/transport.mjs +207 -25
- package/lib/pod/type-index.mjs +101 -0
- package/lib/pod/urls.mjs +6 -0
- package/lib/server/embed.mjs +7 -6
- package/lib/session/README.md +14 -0
- package/lib/session/fedi-account.mjs +73 -2
- package/lib/session/package.json +10 -2
- package/package.json +1 -1
- package/run-agent.mjs +18 -0
- package/web/admin/actors.js +2 -0
- package/web/admin/index.html +9 -0
- package/web/admin/record.js +4 -1
- package/web/admin/setup/index.html +17 -1
- package/web/admin/setup/setup.js +18 -5
- package/web/app/README.md +1 -1
- package/web/app/admin-facade.mjs +1 -1
- package/web/app/agent.mjs +24 -10
- package/web/app/boot.mjs +72 -32
- package/web/app/deliver-relay.mjs +47 -7
- package/web/app/dist/boot.js +466 -89
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +2792 -1996
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/index.html +16 -0
- package/web/app/signup.mjs +65 -26
- package/web/app/update.js +4 -0
- package/web/front/run.html +7 -1
- package/web/front/run.js +30 -4
package/web/app/dist/boot.js
CHANGED
|
@@ -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 ===
|
|
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 =
|
|
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;
|
|
@@ -32822,7 +32841,29 @@ var PodTransport = class {
|
|
|
32822
32841
|
const podTarget = this.toPod ? this.toPod(targetUrl) : targetUrl;
|
|
32823
32842
|
const url = await this.aclUrlFor(podTarget);
|
|
32824
32843
|
if (!await this.aclWritable(url)) return null;
|
|
32825
|
-
|
|
32844
|
+
const doc = this.aclDoc(podTarget, publicModes, { ...opts, aclUrl: url });
|
|
32845
|
+
if (opts.ifChanged && await this.aclSame(url, doc)) return { status: 304, unchanged: true };
|
|
32846
|
+
return this.put(url, doc, "text/turtle");
|
|
32847
|
+
}
|
|
32848
|
+
// Whether the pod's rule at `aclUrl` states exactly what `doc` states.
|
|
32849
|
+
// Compared as graphs, not bytes: the pod serialises what it holds its own
|
|
32850
|
+
// way. Every rule this file writes names its subjects, so triple sets are
|
|
32851
|
+
// enough; anything unreadable or with blank nodes reads as different.
|
|
32852
|
+
async aclSame(aclUrl, doc) {
|
|
32853
|
+
try {
|
|
32854
|
+
const res = await this.fetch(aclUrl, { headers: { accept: "text/turtle" } });
|
|
32855
|
+
if (res.status !== 200) return false;
|
|
32856
|
+
const triples = (text) => {
|
|
32857
|
+
const g = graph();
|
|
32858
|
+
parse2(text, g, aclUrl, "text/turtle");
|
|
32859
|
+
if (g.statements.some((st2) => st2.subject.termType === "BlankNode" || st2.object.termType === "BlankNode")) return null;
|
|
32860
|
+
return g.statements.map((st2) => `${st2.subject.value} ${st2.predicate.value} ${st2.object.value}`).sort().join("\n");
|
|
32861
|
+
};
|
|
32862
|
+
const theirs = triples(await res.text());
|
|
32863
|
+
return theirs !== null && theirs === triples(doc);
|
|
32864
|
+
} catch {
|
|
32865
|
+
return false;
|
|
32866
|
+
}
|
|
32826
32867
|
}
|
|
32827
32868
|
// Child documents of an LDP container (URLs under it, excluding aux docs).
|
|
32828
32869
|
// Revalidated: the inbox is polled every couple of minutes and is usually
|
|
@@ -32846,10 +32887,15 @@ var PodTransport = class {
|
|
|
32846
32887
|
parse2(body, g, url, "text/turtle");
|
|
32847
32888
|
const here = namedNode2(url);
|
|
32848
32889
|
const seen = /* @__PURE__ */ new Set();
|
|
32890
|
+
const receipts = /* @__PURE__ */ new Set();
|
|
32849
32891
|
const list = [];
|
|
32850
32892
|
for (const child of g.each(here, LDP("contains"), null, here)) {
|
|
32851
32893
|
const u = child.value;
|
|
32852
|
-
if (
|
|
32894
|
+
if (u.endsWith(".receipt.json")) {
|
|
32895
|
+
receipts.add(u);
|
|
32896
|
+
continue;
|
|
32897
|
+
}
|
|
32898
|
+
if (!u.startsWith(url) || u === url || /\.(acl|meta)$/.test(u) || seen.has(u)) continue;
|
|
32853
32899
|
seen.add(u);
|
|
32854
32900
|
list.push({
|
|
32855
32901
|
url: u,
|
|
@@ -32857,10 +32903,16 @@ var PodTransport = class {
|
|
|
32857
32903
|
modified: g.any(child, DC("modified"), null, here)?.value || null
|
|
32858
32904
|
});
|
|
32859
32905
|
}
|
|
32906
|
+
for (const item of list) item.receipt = receipts.has(item.url + ".receipt.json");
|
|
32907
|
+
const orphans = [...receipts].filter((r) => !seen.has(r.slice(0, -".receipt.json".length)));
|
|
32860
32908
|
list.sort((a, b) => String(a.modified || "").localeCompare(String(b.modified || "")));
|
|
32861
|
-
this._listCache.set(url, { etag: res.headers.get("etag"), children: list });
|
|
32909
|
+
this._listCache.set(url, { etag: res.headers.get("etag"), children: list, orphans });
|
|
32862
32910
|
return list;
|
|
32863
32911
|
}
|
|
32912
|
+
/** Receipts in the last listing of `url` whose item is gone. */
|
|
32913
|
+
orphanReceipts(url) {
|
|
32914
|
+
return this._listCache?.get(url)?.orphans ?? [];
|
|
32915
|
+
}
|
|
32864
32916
|
/**
|
|
32865
32917
|
* The WebID profile advertises the actor as an account:
|
|
32866
32918
|
* <webId> foaf:account <actor> .
|
|
@@ -32870,16 +32922,10 @@ var PodTransport = class {
|
|
|
32870
32922
|
* written back — an empty or foreign body must never become the new profile.
|
|
32871
32923
|
*/
|
|
32872
32924
|
async linkAccountInProfile({ actorUrl, accountName, kind = "person", outbox = null }) {
|
|
32873
|
-
const docUrl = this.webId.split("#")[0];
|
|
32874
|
-
const res = await this.fetch(docUrl, { headers: { accept: "text/turtle" } });
|
|
32875
|
-
if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} \u2192 ${res.status}`);
|
|
32876
|
-
const g = graph();
|
|
32877
|
-
parse2(await res.text(), g, docUrl, "text/turtle");
|
|
32878
|
-
const doc = namedNode2(docUrl);
|
|
32879
32925
|
const me = namedNode2(this.webId);
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
}
|
|
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));
|
|
32883
32929
|
const actor = namedNode2(actorUrl);
|
|
32884
32930
|
const wanted = [
|
|
32885
32931
|
[me, FOAF("account"), actor],
|
|
@@ -32890,17 +32936,20 @@ var PodTransport = class {
|
|
|
32890
32936
|
// WebID is what dokieli reads); only where a door exists to take it.
|
|
32891
32937
|
...outbox ? [[me, AS("outbox"), namedNode2(outbox)]] : []
|
|
32892
32938
|
];
|
|
32893
|
-
const missing = wanted.filter(([s, p, o]) => !
|
|
32894
|
-
const stale = [
|
|
32895
|
-
|
|
32896
|
-
|
|
32897
|
-
|
|
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
|
+
}
|
|
32898
32951
|
if (!missing.length && !stale.length) return false;
|
|
32899
|
-
|
|
32900
|
-
if (await this.patchDocument(docUrl, missing, deletes)) return true;
|
|
32901
|
-
for (const st2 of stale) g.remove(st2);
|
|
32902
|
-
for (const [s, p, o] of missing) g.add(s, p, o, doc);
|
|
32903
|
-
await this.put(docUrl, serialize(doc, g, docUrl, "text/turtle"), "text/turtle");
|
|
32952
|
+
await this.writeAboutWebId(podBase, { inserts: missing, deletes: stale });
|
|
32904
32953
|
return true;
|
|
32905
32954
|
}
|
|
32906
32955
|
/**
|
|
@@ -32939,6 +32988,151 @@ ${clauses.join(";\n")}.
|
|
|
32939
32988
|
if (res.status === 405 || res.status === 415 || res.status === 501) return false;
|
|
32940
32989
|
throw new Error(`[${this.label}] PATCH ${docUrl} \u2192 ${res.status}`);
|
|
32941
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
|
+
}
|
|
32942
33136
|
};
|
|
32943
33137
|
|
|
32944
33138
|
// web/app/pod-remote.mjs
|
|
@@ -33024,15 +33218,6 @@ async function resourceExists(fetchImpl, url, { timeoutMs = OWNER_LOOKUP_MS } =
|
|
|
33024
33218
|
}
|
|
33025
33219
|
}
|
|
33026
33220
|
|
|
33027
|
-
// lib/pod/urls.mjs
|
|
33028
|
-
function podBaseOfWebId(webId) {
|
|
33029
|
-
const u = new URL(webId);
|
|
33030
|
-
u.hash = "";
|
|
33031
|
-
u.search = "";
|
|
33032
|
-
const dir = u.pathname.replace(/profile\/card$/u, "").replace(/[^/]*$/u, "");
|
|
33033
|
-
return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
|
|
33034
|
-
}
|
|
33035
|
-
|
|
33036
33221
|
// web/app/idb-kv.mjs
|
|
33037
33222
|
var DB = "fedipod-accounts";
|
|
33038
33223
|
function open() {
|
|
@@ -33079,10 +33264,139 @@ async function cacheOpenedKeys(actorUrl, keysRecord) {
|
|
|
33079
33264
|
}
|
|
33080
33265
|
var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
|
|
33081
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
|
+
|
|
33082
33395
|
// web/app/signup.mjs
|
|
33083
|
-
var
|
|
33084
|
-
var
|
|
33085
|
-
var
|
|
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) });
|
|
33086
33400
|
var HANDLE_RE = /^[a-z0-9-]{2,30}$/;
|
|
33087
33401
|
function handleProblem(handle) {
|
|
33088
33402
|
if (!handle) return "a handle is required";
|
|
@@ -33091,7 +33405,7 @@ function handleProblem(handle) {
|
|
|
33091
33405
|
return null;
|
|
33092
33406
|
}
|
|
33093
33407
|
var PROGRESS = /* @__PURE__ */ new Map();
|
|
33094
|
-
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("|");
|
|
33095
33409
|
async function assertFrontNameFree(frontOrigin, handle) {
|
|
33096
33410
|
const res = await fetch(
|
|
33097
33411
|
`${frontOrigin.replace(/\/$/, "")}/api/handle?handle=${encodeURIComponent(handle)}`,
|
|
@@ -33120,10 +33434,23 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33120
33434
|
ok: (note) => onStep(key2, "ok", note),
|
|
33121
33435
|
skip: (note) => onStep(key2, "skipped", note)
|
|
33122
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
|
+
} });
|
|
33123
33442
|
const podStep = step("pod");
|
|
33124
33443
|
if (!prog.pod) {
|
|
33125
33444
|
podStep.running("checking your pod");
|
|
33126
|
-
if (await
|
|
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
|
+
}
|
|
33127
33454
|
prog.pod = pod;
|
|
33128
33455
|
podStep.ok(pod);
|
|
33129
33456
|
} else {
|
|
@@ -33132,10 +33459,10 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33132
33459
|
const pathPod = new URL(pod).pathname !== "/";
|
|
33133
33460
|
const fronted = pathPod || wantsFront;
|
|
33134
33461
|
if (fronted && !frontOrigin) {
|
|
33135
|
-
throw new Error(`${pod} is a
|
|
33462
|
+
throw new Error(`${pod} is a suffixed pod, so its address must live at a gateway, and this page has none.`);
|
|
33136
33463
|
}
|
|
33137
33464
|
if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
33138
|
-
const actorUrl = actorUrlFor(pod);
|
|
33465
|
+
const actorUrl = actorUrlFor(pod, root);
|
|
33139
33466
|
const frontActor = fronted ? `${frontOrigin.replace(/\/$/, "")}/u/${handle}/ap/actor` : null;
|
|
33140
33467
|
let gateway = answers.gateway || prog.gateway || null;
|
|
33141
33468
|
if (frontOrigin && !gateway) {
|
|
@@ -33149,7 +33476,7 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33149
33476
|
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
33150
33477
|
// the container the agent drains, where nothing would ever read it. The
|
|
33151
33478
|
// manage surface has always sent `urls.home`; this is the same value.
|
|
33152
|
-
body: JSON.stringify({ handle, podHome: `${pod}${
|
|
33479
|
+
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl, kind: "person", fronted })
|
|
33153
33480
|
});
|
|
33154
33481
|
const d = await res.json().catch(() => ({}));
|
|
33155
33482
|
if (res.status !== 201 || !d.hmacSecret) {
|
|
@@ -33161,8 +33488,6 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33161
33488
|
} else if (frontOrigin && gateway) {
|
|
33162
33489
|
step("gateway").ok();
|
|
33163
33490
|
}
|
|
33164
|
-
const remote = new BrowserRemotePod(session, { webId, role: "signup", log: () => {
|
|
33165
|
-
} });
|
|
33166
33491
|
const keysStep = step("keys");
|
|
33167
33492
|
let keys;
|
|
33168
33493
|
if (!prog.keysStored) {
|
|
@@ -33171,12 +33496,12 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33171
33496
|
keys.mintedFor = gateway?.frontActor || actorUrl;
|
|
33172
33497
|
try {
|
|
33173
33498
|
await provisionKey(remote, {
|
|
33174
|
-
stateUrl:
|
|
33175
|
-
keysUrl: keysDocFor(pod),
|
|
33499
|
+
stateUrl: stateOf(pod, root),
|
|
33500
|
+
keysUrl: keysDocFor(pod, root),
|
|
33176
33501
|
keys
|
|
33177
33502
|
});
|
|
33178
33503
|
} catch (e) {
|
|
33179
|
-
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
|
|
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.`);
|
|
33180
33505
|
}
|
|
33181
33506
|
await cacheOpenedKeys(actorUrl, keys);
|
|
33182
33507
|
prog.keys = keys;
|
|
@@ -33188,7 +33513,7 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33188
33513
|
}
|
|
33189
33514
|
const config = {
|
|
33190
33515
|
remotePod: pod,
|
|
33191
|
-
root
|
|
33516
|
+
root,
|
|
33192
33517
|
handle,
|
|
33193
33518
|
name: handle,
|
|
33194
33519
|
issuer: String(session.issuer || "").replace(/\/+$/, ""),
|
|
@@ -33196,10 +33521,21 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33196
33521
|
...gateway ? { gateway } : {}
|
|
33197
33522
|
};
|
|
33198
33523
|
try {
|
|
33199
|
-
await writeConfig(remote, { state:
|
|
33524
|
+
await writeConfig(remote, { state: stateOf(pod, root) }, config);
|
|
33200
33525
|
} catch (e) {
|
|
33201
33526
|
throw new Error(`could not store the config on the pod (${e.message})`);
|
|
33202
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
|
+
}
|
|
33203
33539
|
PROGRESS.delete(key);
|
|
33204
33540
|
const host = gateway?.frontActor ? new URL(gateway.frontActor).host : new URL(pod).host;
|
|
33205
33541
|
return {
|
|
@@ -33216,8 +33552,9 @@ async function readAccount(session) {
|
|
|
33216
33552
|
const pod = podBaseOfWebId(session.webId);
|
|
33217
33553
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
|
|
33218
33554
|
} });
|
|
33219
|
-
const
|
|
33220
|
-
if (!
|
|
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;
|
|
33221
33558
|
const frontActor = config.gateway?.frontActor || null;
|
|
33222
33559
|
let frontHost = null;
|
|
33223
33560
|
try {
|
|
@@ -33231,7 +33568,7 @@ async function readAccount(session) {
|
|
|
33231
33568
|
} catch {
|
|
33232
33569
|
podHost = "";
|
|
33233
33570
|
}
|
|
33234
|
-
return { pod, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
|
|
33571
|
+
return { pod, root, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
|
|
33235
33572
|
}
|
|
33236
33573
|
async function moveIn(answers, { session, onStep = () => {
|
|
33237
33574
|
}, frontOrigin = null } = {}) {
|
|
@@ -33247,10 +33584,10 @@ async function moveIn(answers, { session, onStep = () => {
|
|
|
33247
33584
|
const here = await readAccount(session);
|
|
33248
33585
|
if (!here?.frontActor) throw new Error("this pod holds no account at another gateway to move here");
|
|
33249
33586
|
if (here.frontHost === new URL(origin).host) throw new Error("this account already lives at this gateway");
|
|
33250
|
-
const { pod, config: old } = here;
|
|
33587
|
+
const { pod, root, config: old } = here;
|
|
33251
33588
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
|
|
33252
33589
|
} });
|
|
33253
|
-
const urls = { state:
|
|
33590
|
+
const urls = { state: stateOf(pod, root) };
|
|
33254
33591
|
const keys = await readKeys(remote, urls);
|
|
33255
33592
|
if (!keys?.rsa) throw new Error("could not read this account's signing key on the pod");
|
|
33256
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`);
|
|
@@ -33261,7 +33598,7 @@ async function moveIn(answers, { session, onStep = () => {
|
|
|
33261
33598
|
const res = await session.fetch(`${origin}/api/attach`, {
|
|
33262
33599
|
method: "POST",
|
|
33263
33600
|
headers: { "content-type": "application/json" },
|
|
33264
|
-
body: JSON.stringify({ handle, podHome: `${pod}${
|
|
33601
|
+
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl: actorUrlFor(pod, root), kind: old.kind || "person", fronted: true })
|
|
33265
33602
|
});
|
|
33266
33603
|
const d = await res.json().catch(() => ({}));
|
|
33267
33604
|
if (res.status !== 201 || !d.hmacSecret) throw new Error(`could not attach to this gateway (HTTP ${res.status}): ${d.error || ""}`);
|
|
@@ -33289,7 +33626,7 @@ async function moveIn(answers, { session, onStep = () => {
|
|
|
33289
33626
|
} catch (e) {
|
|
33290
33627
|
throw new Error(`could not update your account on the pod (${e.message})`);
|
|
33291
33628
|
}
|
|
33292
|
-
await cacheOpenedKeys(actorUrlFor(pod), keys);
|
|
33629
|
+
await cacheOpenedKeys(actorUrlFor(pod, root), keys);
|
|
33293
33630
|
keysStep.ok();
|
|
33294
33631
|
return { config, address: `@${handle}@${new URL(origin).host}`, movedFrom: config.movedFrom };
|
|
33295
33632
|
}
|
|
@@ -33581,18 +33918,14 @@ async function bootWorker({ reset = false } = {}) {
|
|
|
33581
33918
|
async function readAccountState() {
|
|
33582
33919
|
const session = await getSession();
|
|
33583
33920
|
if (!session) throw new Error("Sign in first.");
|
|
33584
|
-
const
|
|
33585
|
-
|
|
33921
|
+
const here = await readAccount(session);
|
|
33922
|
+
if (!here) throw new Error(`could not find a FediPod account on ${podBaseOfWebId(session.webId)}`);
|
|
33586
33923
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
|
|
33587
33924
|
} });
|
|
33588
|
-
const urls = { state };
|
|
33589
|
-
const
|
|
33590
|
-
|
|
33591
|
-
|
|
33592
|
-
]);
|
|
33593
|
-
if (!cfg) throw new Error(`could not read this account's config under ${state}`);
|
|
33594
|
-
const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
|
|
33595
|
-
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 };
|
|
33596
33929
|
}
|
|
33597
33930
|
window.fedipodUnlock = async (password) => {
|
|
33598
33931
|
if (!password) throw new Error("Enter your password.");
|
|
@@ -33646,18 +33979,28 @@ function parseAddress(input) {
|
|
|
33646
33979
|
if (!handle || !host || !host.includes(".")) return null;
|
|
33647
33980
|
return { handle, host };
|
|
33648
33981
|
}
|
|
33649
|
-
async function
|
|
33982
|
+
async function issuerForActor(actorUrl) {
|
|
33650
33983
|
try {
|
|
33651
|
-
const authz = await readIssuer(
|
|
33984
|
+
const authz = await readIssuer(actorUrl);
|
|
33652
33985
|
if (authz) return new URL(authz).origin;
|
|
33653
33986
|
} catch {
|
|
33654
33987
|
}
|
|
33655
|
-
const u = new URL(
|
|
33656
|
-
|
|
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;
|
|
33657
33991
|
const parent = u.host.split(".").slice(1).join(".");
|
|
33658
33992
|
return `https://${parent || u.host}`;
|
|
33659
33993
|
}
|
|
33660
|
-
async function
|
|
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) {
|
|
33661
34004
|
const res = await fetch(
|
|
33662
34005
|
`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
|
|
33663
34006
|
{ headers: { accept: "application/jrd+json, application/json" } }
|
|
@@ -33667,17 +34010,15 @@ async function podForFrontedAddress(handle) {
|
|
|
33667
34010
|
const doc = await res.json().catch(() => ({}));
|
|
33668
34011
|
const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
|
|
33669
34012
|
if (!podActorId) throw new Error(`@${handle}@${location.host} lives here but names no pod to sign in to`);
|
|
33670
|
-
|
|
33671
|
-
if (!podActorId.endsWith(tail)) throw new Error(`the pod actor ${podActorId} is not where a FediPod pod keeps one`);
|
|
33672
|
-
return podActorId.slice(0, -tail.length);
|
|
34013
|
+
return podActorId;
|
|
33673
34014
|
}
|
|
33674
34015
|
window.fedipodSignin = async ({ address }) => {
|
|
33675
34016
|
const parsed = parseAddress(address);
|
|
33676
34017
|
if (!parsed) throw new Error("Enter your address as @you@yourpod (for example @alice@alice.solidcommunity.net).");
|
|
33677
34018
|
const bad = handleProblem(parsed.handle);
|
|
33678
34019
|
if (bad) throw new Error(bad);
|
|
33679
|
-
const
|
|
33680
|
-
const issuer = await
|
|
34020
|
+
const actor = parsed.host === location.host.toLowerCase() ? await actorForFrontedAddress(parsed.handle) : await actorForPodAddress(parsed.handle, parsed.host);
|
|
34021
|
+
const issuer = await issuerForActor(actor);
|
|
33681
34022
|
const { authorizationUrl } = await beginLogin({ issuer, redirectUri: REDIRECT });
|
|
33682
34023
|
location.href = authorizationUrl;
|
|
33683
34024
|
};
|
|
@@ -33916,6 +34257,8 @@ ${e.detail}` : "");
|
|
|
33916
34257
|
$("brand").hidden = false;
|
|
33917
34258
|
$("pane-form").hidden = false;
|
|
33918
34259
|
$("signed-pod").textContent = signedPod;
|
|
34260
|
+
$("container").placeholder = podRootPath(signedPod);
|
|
34261
|
+
$("container-field").hidden = !!moveFrom;
|
|
33919
34262
|
$("movein-note").hidden = !moveFrom;
|
|
33920
34263
|
if (moveFrom) {
|
|
33921
34264
|
$("movein-from").textContent = moveFrom.address;
|
|
@@ -33966,7 +34309,12 @@ ${e.detail}` : "");
|
|
|
33966
34309
|
}
|
|
33967
34310
|
};
|
|
33968
34311
|
const shape = () => pathPod() || moveFrom ? "front" : f().shape.value;
|
|
33969
|
-
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
|
+
};
|
|
33970
34318
|
const previewAddr = () => {
|
|
33971
34319
|
const handle = f().handle.value.trim().toLowerCase();
|
|
33972
34320
|
let host = "";
|
|
@@ -33997,6 +34345,7 @@ ${e.detail}` : "");
|
|
|
33997
34345
|
applyMode();
|
|
33998
34346
|
applyShape();
|
|
33999
34347
|
previewAddr();
|
|
34348
|
+
previewPlace();
|
|
34000
34349
|
});
|
|
34001
34350
|
applyMode();
|
|
34002
34351
|
const STEP_IDS = ["step-1", "step-2"];
|
|
@@ -34010,6 +34359,8 @@ ${e.detail}` : "");
|
|
|
34010
34359
|
if (n === 2) {
|
|
34011
34360
|
applyShape();
|
|
34012
34361
|
previewAddr();
|
|
34362
|
+
previewPlace();
|
|
34363
|
+
$("index-ask").hidden = true;
|
|
34013
34364
|
}
|
|
34014
34365
|
if (FOCUS[n]) $(FOCUS[n]).focus();
|
|
34015
34366
|
};
|
|
@@ -34020,6 +34371,10 @@ ${e.detail}` : "");
|
|
|
34020
34371
|
const validateStep2 = () => {
|
|
34021
34372
|
const hp = window.fedipodHandleProblem(f().handle.value.trim().toLowerCase());
|
|
34022
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
|
+
}
|
|
34023
34378
|
return null;
|
|
34024
34379
|
};
|
|
34025
34380
|
$("create").addEventListener("click", showForm);
|
|
@@ -34055,18 +34410,15 @@ ${e.detail}` : "");
|
|
|
34055
34410
|
showLanding();
|
|
34056
34411
|
goStep(1);
|
|
34057
34412
|
});
|
|
34058
|
-
const LABELS = {
|
|
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
|
+
};
|
|
34059
34419
|
const MOVE_LABELS = { pod: "Reading your account on your pod", gateway: "Taking your address here", keys: "Moving your key and account record" };
|
|
34060
|
-
|
|
34061
|
-
|
|
34062
|
-
$("form-error").textContent = "";
|
|
34063
|
-
const e2 = validateStep2();
|
|
34064
|
-
if (e2) {
|
|
34065
|
-
$("form-error").textContent = e2;
|
|
34066
|
-
goStep(2);
|
|
34067
|
-
return;
|
|
34068
|
-
}
|
|
34069
|
-
const a = answers();
|
|
34420
|
+
const run = async (createIndex = false) => {
|
|
34421
|
+
const a = { ...answers(), ...createIndex ? { createIndex: true } : {} };
|
|
34070
34422
|
$("pane-form").hidden = true;
|
|
34071
34423
|
$("running").hidden = false;
|
|
34072
34424
|
$("running-title").textContent = "Setting up\u2026";
|
|
@@ -34089,10 +34441,35 @@ ${e.detail}` : "");
|
|
|
34089
34441
|
else await window.fedipodSignup({ ...a, onStep });
|
|
34090
34442
|
location.href = "/admin/client/";
|
|
34091
34443
|
} catch (err) {
|
|
34444
|
+
if (err?.code === "needs-index") {
|
|
34445
|
+
backToForm();
|
|
34446
|
+
$("index-ask").hidden = false;
|
|
34447
|
+
$("index-yes").focus();
|
|
34448
|
+
return;
|
|
34449
|
+
}
|
|
34092
34450
|
$("running-title").textContent = "Setup did not finish";
|
|
34093
34451
|
$("run-error").textContent = err.message || String(err);
|
|
34094
34452
|
$("run-actions").hidden = false;
|
|
34095
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.";
|
|
34096
34473
|
});
|
|
34097
34474
|
if (pendingIdentity) await showIdentity();
|
|
34098
34475
|
})();
|