fedipod 1.36.6 → 1.40.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 +29 -8
- package/architecture.md +8 -0
- package/bin/fedipod.mjs +6 -0
- package/cli.md +7 -1
- package/device-agent.md +3 -3
- package/gateway.md +69 -8
- package/groups.md +2 -1
- package/gui.md +3 -1
- package/lib/client/c2s.mjs +97 -43
- package/lib/client/masto/accounts.mjs +1 -0
- package/lib/client/masto/bridge.mjs +61 -0
- package/lib/client/masto/index.mjs +4 -1
- package/lib/client/masto/oauth.mjs +1 -1
- package/lib/client/masto/statuses.mjs +3 -0
- package/lib/connections/acctfeed.mjs +13 -9
- package/lib/connections/bskyfeed.mjs +13 -9
- package/lib/connections/tagfeed.mjs +14 -9
- package/lib/core/deliver.mjs +17 -1
- package/lib/core/intake/activities.mjs +18 -2
- package/lib/core/intake/group.mjs +3 -1
- package/lib/core/intake/index.mjs +226 -48
- package/lib/core/intake/notes.mjs +12 -5
- package/lib/core/intake/verify.mjs +11 -0
- package/lib/core/lease.mjs +15 -1
- package/lib/core/place.mjs +82 -0
- package/lib/core/pod-only.mjs +4 -0
- package/lib/core/publisher/collections.mjs +37 -2
- package/lib/core/publisher/index.mjs +25 -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/scheduled.mjs +39 -0
- package/lib/core/social.mjs +62 -32
- package/lib/core/storage.mjs +67 -0
- package/lib/core/store.mjs +77 -5
- 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/account-agent.mjs +111 -0
- package/lib/gateway/copy.mjs +381 -0
- package/lib/gateway/front-core.mjs +74 -63
- package/lib/gateway/gateway-core.mjs +107 -9
- package/lib/gateway/held-mail.mjs +198 -0
- package/lib/gateway/keeper-due.mjs +39 -0
- package/lib/gateway/keeper-session.mjs +10 -0
- package/lib/gateway/keeper.mjs +72 -0
- package/lib/gateway/masto-gateway.mjs +510 -0
- package/lib/gateway/quiet.mjs +7 -2
- package/lib/gateway/relay-extras.mjs +89 -0
- package/lib/gateway/state-api.mjs +207 -0
- package/lib/gateway/token-claims.mjs +16 -0
- package/lib/pod/containers.mjs +17 -0
- package/lib/pod/location.mjs +52 -0
- package/lib/pod/notes.mjs +2 -4
- package/lib/pod/transport.mjs +214 -24
- 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 +5 -5
- package/lib/session/demo.html +1 -1
- package/lib/session/fedi-account.mjs +19 -10
- package/lib/session/package.json +2 -2
- package/package.json +2 -2
- package/run-agent.mjs +6 -16
- package/scripts/stage-site.mjs +17 -4
- package/web/admin/actors.js +2 -0
- package/web/admin/gateway.js +14 -1
- package/web/admin/index.html +13 -0
- package/web/admin/oauth-signin.mjs +1 -1
- 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 +2 -2
- package/web/app/admin-facade.mjs +12 -2
- package/web/app/agent.mjs +228 -51
- package/web/app/boot.mjs +75 -32
- package/web/app/copy-mode.mjs +223 -0
- package/web/app/dist/boot.js +498 -88
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +4016 -2539
- 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/sw-src.mjs +15 -53
- package/web/app/update.js +3 -2
- package/web/app/warm-start.mjs +115 -0
- package/web/app-signin/app-signin.mjs +78 -0
- package/web/app-signin/index.html +41 -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;
|
|
@@ -32616,6 +32635,9 @@ var PodTransport = class {
|
|
|
32616
32635
|
this.aclFlavour = null;
|
|
32617
32636
|
this._listCache = /* @__PURE__ */ new Map();
|
|
32618
32637
|
this.toPod = null;
|
|
32638
|
+
this.aclOwner = null;
|
|
32639
|
+
this.keepers = [];
|
|
32640
|
+
this.aclIfChanged = false;
|
|
32619
32641
|
}
|
|
32620
32642
|
/** Who is talking, and from where — the prefix on every log line and error. */
|
|
32621
32643
|
get label() {
|
|
@@ -32813,9 +32835,10 @@ var PodTransport = class {
|
|
|
32813
32835
|
authorize(
|
|
32814
32836
|
namedNode2(url + "#owner"),
|
|
32815
32837
|
ACL("agent"),
|
|
32816
|
-
namedNode2(this.webId),
|
|
32838
|
+
namedNode2(this.aclOwner || this.webId),
|
|
32817
32839
|
["Read", "Write", "Control"]
|
|
32818
32840
|
);
|
|
32841
|
+
(this.keepers || []).forEach((webId, i) => authorize(namedNode2(url + `#keeper${i}`), ACL("agent"), namedNode2(webId), ["Read", "Write", "Control"]));
|
|
32819
32842
|
return serialize(doc, g, url, "text/turtle");
|
|
32820
32843
|
}
|
|
32821
32844
|
async setAcl(targetUrl, publicModes, opts = {}) {
|
|
@@ -32823,9 +32846,52 @@ var PodTransport = class {
|
|
|
32823
32846
|
const url = await this.aclUrlFor(podTarget);
|
|
32824
32847
|
if (!await this.aclWritable(url)) return null;
|
|
32825
32848
|
const doc = this.aclDoc(podTarget, publicModes, { ...opts, aclUrl: url });
|
|
32826
|
-
if (opts.ifChanged && await this.aclSame(url, doc)) return { status: 304, unchanged: true };
|
|
32849
|
+
if ((opts.ifChanged || this.aclIfChanged) && await this.aclSame(url, doc)) return { status: 304, unchanged: true };
|
|
32827
32850
|
return this.put(url, doc, "text/turtle");
|
|
32828
32851
|
}
|
|
32852
|
+
/**
|
|
32853
|
+
* A rule already on the pod, stated again with this transport's owner and
|
|
32854
|
+
* keepers, keeping what it grants the public (#public), agents who may only
|
|
32855
|
+
* append (#gw…) and agents who may only read (#r…). A resource with no rule
|
|
32856
|
+
* of its own inherits one and is left alone; so is a rule with anything this
|
|
32857
|
+
* file did not write, which it cannot restate faithfully.
|
|
32858
|
+
*/
|
|
32859
|
+
async restateAcl(targetUrl) {
|
|
32860
|
+
const podTarget = this.toPod ? this.toPod(targetUrl) : targetUrl;
|
|
32861
|
+
const url = await this.aclUrlFor(podTarget);
|
|
32862
|
+
if (!await this.aclWritable(url)) return null;
|
|
32863
|
+
const res = await this.fetch(url, { headers: { accept: "text/turtle" } });
|
|
32864
|
+
if (res.status !== 200) return null;
|
|
32865
|
+
const g = graph();
|
|
32866
|
+
try {
|
|
32867
|
+
parse2(await res.text(), g, url, "text/turtle");
|
|
32868
|
+
} catch {
|
|
32869
|
+
return null;
|
|
32870
|
+
}
|
|
32871
|
+
const modes = (auth) => g.each(auth, ACL("mode"), null).map((m) => m.value.slice(ACL("").value.length));
|
|
32872
|
+
let publicModes = [];
|
|
32873
|
+
const appendAgents = [];
|
|
32874
|
+
const readAgents = [];
|
|
32875
|
+
for (const auth of g.each(null, RDF3("type"), ACL("Authorization"))) {
|
|
32876
|
+
const frag = auth.value.includes("#") ? auth.value.slice(auth.value.lastIndexOf("#") + 1) : "";
|
|
32877
|
+
if (frag === "owner" || /^keeper\d+$/u.test(frag)) continue;
|
|
32878
|
+
if (frag === "public") {
|
|
32879
|
+
publicModes = modes(auth);
|
|
32880
|
+
continue;
|
|
32881
|
+
}
|
|
32882
|
+
const agent = g.any(auth, ACL("agent"), null)?.value;
|
|
32883
|
+
if (/^gw\d+$/u.test(frag) && agent) {
|
|
32884
|
+
appendAgents.push(agent);
|
|
32885
|
+
continue;
|
|
32886
|
+
}
|
|
32887
|
+
if (/^r\d+$/u.test(frag) && agent) {
|
|
32888
|
+
readAgents.push(agent);
|
|
32889
|
+
continue;
|
|
32890
|
+
}
|
|
32891
|
+
return null;
|
|
32892
|
+
}
|
|
32893
|
+
return this.setAcl(targetUrl, publicModes, { appendAgents, readAgents, ifChanged: true });
|
|
32894
|
+
}
|
|
32829
32895
|
// Whether the pod's rule at `aclUrl` states exactly what `doc` states.
|
|
32830
32896
|
// Compared as graphs, not bytes: the pod serialises what it holds its own
|
|
32831
32897
|
// way. Every rule this file writes names its subjects, so triple sets are
|
|
@@ -32903,16 +32969,10 @@ var PodTransport = class {
|
|
|
32903
32969
|
* written back — an empty or foreign body must never become the new profile.
|
|
32904
32970
|
*/
|
|
32905
32971
|
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
32972
|
const me = namedNode2(this.webId);
|
|
32913
|
-
|
|
32914
|
-
|
|
32915
|
-
}
|
|
32973
|
+
const podBase = podBaseOfWebId(this.webId);
|
|
32974
|
+
const docs = await this.profileDocs(podBase);
|
|
32975
|
+
const says = (s, p, o) => docs.some(({ g }) => g?.holds(s, p, o));
|
|
32916
32976
|
const actor = namedNode2(actorUrl);
|
|
32917
32977
|
const wanted = [
|
|
32918
32978
|
[me, FOAF("account"), actor],
|
|
@@ -32923,17 +32983,20 @@ var PodTransport = class {
|
|
|
32923
32983
|
// WebID is what dokieli reads); only where a door exists to take it.
|
|
32924
32984
|
...outbox ? [[me, AS("outbox"), namedNode2(outbox)]] : []
|
|
32925
32985
|
];
|
|
32926
|
-
const missing = wanted.filter(([s, p, o]) => !
|
|
32927
|
-
const stale = [
|
|
32928
|
-
|
|
32929
|
-
|
|
32930
|
-
|
|
32986
|
+
const missing = wanted.filter(([s, p, o]) => !says(s, p, o));
|
|
32987
|
+
const stale = [];
|
|
32988
|
+
for (const { g } of docs) {
|
|
32989
|
+
for (const st2 of g?.statementsMatching(actor, FOAF("accountName"), null) || []) {
|
|
32990
|
+
if (st2.object.value !== accountName) stale.push([st2.subject, st2.predicate, st2.object]);
|
|
32991
|
+
}
|
|
32992
|
+
if (outbox) {
|
|
32993
|
+
for (const st2 of g?.statementsMatching(me, AS("outbox"), null) || []) {
|
|
32994
|
+
if (st2.object.value !== outbox) stale.push([st2.subject, st2.predicate, st2.object]);
|
|
32995
|
+
}
|
|
32996
|
+
}
|
|
32997
|
+
}
|
|
32931
32998
|
if (!missing.length && !stale.length) return false;
|
|
32932
|
-
|
|
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");
|
|
32999
|
+
await this.writeAboutWebId(podBase, { inserts: missing, deletes: stale });
|
|
32937
33000
|
return true;
|
|
32938
33001
|
}
|
|
32939
33002
|
/**
|
|
@@ -32972,6 +33035,151 @@ ${clauses.join(";\n")}.
|
|
|
32972
33035
|
if (res.status === 405 || res.status === 415 || res.status === 501) return false;
|
|
32973
33036
|
throw new Error(`[${this.label}] PATCH ${docUrl} \u2192 ${res.status}`);
|
|
32974
33037
|
}
|
|
33038
|
+
// ---- writing a profile or a type index: valid RDF, or nothing ----
|
|
33039
|
+
//
|
|
33040
|
+
// Two rules. A change is written only if the document it leaves behind is
|
|
33041
|
+
// valid RDF: the graph as it would be afterwards is serialised and parsed
|
|
33042
|
+
// back, and the subject that must stay described still is. And a profile
|
|
33043
|
+
// that will not take a write is not the end: the statements go to the first
|
|
33044
|
+
// document its rdfs:seeAlso names, inside the same pod, that takes them —
|
|
33045
|
+
// under the same check. A profile is what every Solid app signs in through;
|
|
33046
|
+
// one left broken breaks them all. Reading follows the same links.
|
|
33047
|
+
/** An IRI or a literal as a term, for the operations that pass plain values. */
|
|
33048
|
+
sym(iri) {
|
|
33049
|
+
try {
|
|
33050
|
+
return namedNode2(iri);
|
|
33051
|
+
} catch {
|
|
33052
|
+
throw new Error(`not written: ${iri} is not an absolute IRI`);
|
|
33053
|
+
}
|
|
33054
|
+
}
|
|
33055
|
+
literal(value) {
|
|
33056
|
+
return literal2(value);
|
|
33057
|
+
}
|
|
33058
|
+
/** A document as a graph, or null when it is not there. */
|
|
33059
|
+
async readRdf(docUrl) {
|
|
33060
|
+
const res = await this.fetch(docUrl, { headers: { accept: "text/turtle" } });
|
|
33061
|
+
if (res.status === 404 || res.status === 410) return null;
|
|
33062
|
+
if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} \u2192 ${res.status}`);
|
|
33063
|
+
const g = graph();
|
|
33064
|
+
parse2(await res.text(), g, docUrl, "text/turtle");
|
|
33065
|
+
return g;
|
|
33066
|
+
}
|
|
33067
|
+
/**
|
|
33068
|
+
* The document as it would be after the change, checked. Returns the Turtle
|
|
33069
|
+
* to write, or throws saying why nothing may be written.
|
|
33070
|
+
*/
|
|
33071
|
+
checkedRdf(g, docUrl, { inserts = [], deletes = [], mustDescribe = null }) {
|
|
33072
|
+
const doc = namedNode2(docUrl);
|
|
33073
|
+
for (const [s, p, o] of inserts) {
|
|
33074
|
+
const bad = termProblem(s) || termProblem(p) || termProblem(o) || (s?.termType === "Literal" || p?.termType !== "NamedNode" ? "a literal subject or predicate" : null);
|
|
33075
|
+
if (bad) throw new Error(`not written: ${bad}`);
|
|
33076
|
+
}
|
|
33077
|
+
const after = graph();
|
|
33078
|
+
for (const st2 of g.statementsMatching(null, null, null, doc)) after.add(st2.subject, st2.predicate, st2.object, doc);
|
|
33079
|
+
for (const [s, p, o] of deletes) for (const st2 of after.statementsMatching(s, p, o, doc)) after.remove(st2);
|
|
33080
|
+
for (const [s, p, o] of inserts) if (!after.holds(s, p, o, doc)) after.add(s, p, o, doc);
|
|
33081
|
+
let text;
|
|
33082
|
+
try {
|
|
33083
|
+
text = serialize(doc, after, docUrl, "text/turtle");
|
|
33084
|
+
} catch (e) {
|
|
33085
|
+
throw new Error(`not written: ${docUrl} would not serialise (${e.message})`);
|
|
33086
|
+
}
|
|
33087
|
+
const back = graph();
|
|
33088
|
+
try {
|
|
33089
|
+
parse2(text, back, docUrl, "text/turtle");
|
|
33090
|
+
} catch (e) {
|
|
33091
|
+
throw new Error(`not written: ${docUrl} would not parse back (${e.message})`);
|
|
33092
|
+
}
|
|
33093
|
+
if (back.statementsMatching(null, null, null, doc).length !== after.statementsMatching(null, null, null, doc).length) {
|
|
33094
|
+
throw new Error(`not written: ${docUrl} would not read back as written`);
|
|
33095
|
+
}
|
|
33096
|
+
if (mustDescribe && !back.statementsMatching(namedNode2(mustDescribe), null, null, doc).length) {
|
|
33097
|
+
throw new Error(`not written: ${docUrl} would no longer describe ${mustDescribe}`);
|
|
33098
|
+
}
|
|
33099
|
+
return text;
|
|
33100
|
+
}
|
|
33101
|
+
/**
|
|
33102
|
+
* Change one document, or refuse. `g` is the document as read (null for a
|
|
33103
|
+
* new one). A PATCH carries only these statements; where the pod cannot
|
|
33104
|
+
* patch, the whole checked document is written. Returns the write's status.
|
|
33105
|
+
*/
|
|
33106
|
+
async writeRdfChecked(docUrl, g, { inserts = [], deletes = [], mustDescribe = null }) {
|
|
33107
|
+
const current = g || graph();
|
|
33108
|
+
const doc = namedNode2(docUrl);
|
|
33109
|
+
const todo = inserts.filter(([s, p, o]) => !current.holds(s, p, o, doc));
|
|
33110
|
+
const gone = deletes.filter(([s, p, o]) => current.holds(s, p, o, doc));
|
|
33111
|
+
if (!todo.length && !gone.length) return 200;
|
|
33112
|
+
const text = this.checkedRdf(current, docUrl, { inserts: todo, deletes: gone, mustDescribe });
|
|
33113
|
+
if (g) {
|
|
33114
|
+
let res = null;
|
|
33115
|
+
try {
|
|
33116
|
+
res = await this.fetch(docUrl, {
|
|
33117
|
+
method: "PATCH",
|
|
33118
|
+
headers: { "content-type": "text/n3" },
|
|
33119
|
+
body: this.n3Patch(docUrl, todo, gone)
|
|
33120
|
+
});
|
|
33121
|
+
} catch {
|
|
33122
|
+
res = null;
|
|
33123
|
+
}
|
|
33124
|
+
if (res && res.status < 300) return res.status;
|
|
33125
|
+
if (res && ![405, 415, 501].includes(res.status)) return res.status;
|
|
33126
|
+
}
|
|
33127
|
+
const put = await this.fetch(docUrl, { method: "PUT", headers: { "content-type": "text/turtle" }, body: text });
|
|
33128
|
+
return put.status;
|
|
33129
|
+
}
|
|
33130
|
+
/**
|
|
33131
|
+
* The profile and the documents its rdfs:seeAlso names inside this pod,
|
|
33132
|
+
* each with its graph, the profile first. What any of them says about the
|
|
33133
|
+
* WebID is what the profile says.
|
|
33134
|
+
*/
|
|
33135
|
+
async profileDocs(podBase) {
|
|
33136
|
+
const profileUrl = this.webId.split("#")[0];
|
|
33137
|
+
const g = await this.readRdf(profileUrl);
|
|
33138
|
+
if (!g) throw new Error(`no profile at ${profileUrl}`);
|
|
33139
|
+
const out = [{ url: profileUrl, g, profile: true }];
|
|
33140
|
+
const seen = /* @__PURE__ */ new Set([profileUrl]);
|
|
33141
|
+
for (const st2 of g.statementsMatching(null, RDFS("seeAlso"), null)) {
|
|
33142
|
+
const url = st2.object.value.split("#")[0];
|
|
33143
|
+
if (seen.has(url) || !url.startsWith(podBase)) continue;
|
|
33144
|
+
seen.add(url);
|
|
33145
|
+
out.push({ url, g: await this.readRdf(url).catch(() => null), profile: false });
|
|
33146
|
+
}
|
|
33147
|
+
return out;
|
|
33148
|
+
}
|
|
33149
|
+
/** Every value the WebID has for `predicate`, across the profile and its seeAlso documents. */
|
|
33150
|
+
webIdValues(docs, predicate) {
|
|
33151
|
+
const me = namedNode2(this.webId);
|
|
33152
|
+
const p = namedNode2(predicate);
|
|
33153
|
+
const out = [];
|
|
33154
|
+
for (const { g } of docs) for (const st2 of g?.statementsMatching(me, p, null) || []) out.push(st2.object.value);
|
|
33155
|
+
return [...new Set(out)];
|
|
33156
|
+
}
|
|
33157
|
+
/**
|
|
33158
|
+
* Write statements about the WebID: to the profile, or — if the profile will
|
|
33159
|
+
* not take them — to the first of its seeAlso documents that will. Every
|
|
33160
|
+
* write checked. Returns the document written to, or null when nothing
|
|
33161
|
+
* needed writing; throws when none would take it.
|
|
33162
|
+
*/
|
|
33163
|
+
async writeAboutWebId(podBase, { inserts = [], deletes = [] }) {
|
|
33164
|
+
const docs = await this.profileDocs(podBase);
|
|
33165
|
+
const holds = ([s, p, o]) => docs.some(({ g }) => g?.holds(s, p, o));
|
|
33166
|
+
const todo = inserts.filter((t) => !holds(t));
|
|
33167
|
+
const gone = deletes.filter(holds);
|
|
33168
|
+
if (!todo.length && !gone.length) return null;
|
|
33169
|
+
const refusals = [];
|
|
33170
|
+
for (const d of docs) {
|
|
33171
|
+
const here = gone.filter(([s, p, o]) => d.g?.holds(s, p, o));
|
|
33172
|
+
const status = await this.writeRdfChecked(d.url, d.g, {
|
|
33173
|
+
inserts: todo,
|
|
33174
|
+
deletes: here,
|
|
33175
|
+
mustDescribe: d.profile ? this.webId : null
|
|
33176
|
+
});
|
|
33177
|
+
if (status < 300) return d.url;
|
|
33178
|
+
refusals.push(`${d.url} \u2192 ${status}`);
|
|
33179
|
+
if (status !== 401 && status !== 403) break;
|
|
33180
|
+
}
|
|
33181
|
+
throw new Error(`not written: neither the profile nor a document it names would take it (${refusals.join("; ")})`);
|
|
33182
|
+
}
|
|
32975
33183
|
};
|
|
32976
33184
|
|
|
32977
33185
|
// web/app/pod-remote.mjs
|
|
@@ -33057,15 +33265,6 @@ async function resourceExists(fetchImpl, url, { timeoutMs = OWNER_LOOKUP_MS } =
|
|
|
33057
33265
|
}
|
|
33058
33266
|
}
|
|
33059
33267
|
|
|
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
33268
|
// web/app/idb-kv.mjs
|
|
33070
33269
|
var DB = "fedipod-accounts";
|
|
33071
33270
|
function open() {
|
|
@@ -33112,10 +33311,139 @@ async function cacheOpenedKeys(actorUrl, keysRecord) {
|
|
|
33112
33311
|
}
|
|
33113
33312
|
var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
|
|
33114
33313
|
|
|
33314
|
+
// lib/pod/type-index.mjs
|
|
33315
|
+
var SOLID = "http://www.w3.org/ns/solid/terms#";
|
|
33316
|
+
var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
|
33317
|
+
var AS_ACTOR = "https://www.w3.org/ns/activitystreams#Actor";
|
|
33318
|
+
var PUBLIC_READ = ["Read"];
|
|
33319
|
+
var newIndexUrl = (podBase) => `${podBase}settings/publicTypeIndex.ttl`;
|
|
33320
|
+
async function findPublicIndex(pod, podBase) {
|
|
33321
|
+
const docs = await pod.profileDocs(podBase);
|
|
33322
|
+
return pod.webIdValues(docs, SOLID + "publicTypeIndex")[0] || null;
|
|
33323
|
+
}
|
|
33324
|
+
async function actorsIn(pod, indexUrl) {
|
|
33325
|
+
const g = await pod.readRdf(indexUrl).catch(() => null);
|
|
33326
|
+
if (!g) return [];
|
|
33327
|
+
const out = [];
|
|
33328
|
+
for (const reg of g.each(null, pod.sym(SOLID + "forClass"), pod.sym(AS_ACTOR))) {
|
|
33329
|
+
for (const inst of g.each(reg, pod.sym(SOLID + "instance"), null)) out.push(inst.value);
|
|
33330
|
+
}
|
|
33331
|
+
return [...new Set(out)];
|
|
33332
|
+
}
|
|
33333
|
+
function regFor(indexUrl, actorUrl) {
|
|
33334
|
+
let h = 0;
|
|
33335
|
+
for (const c of actorUrl) h = h * 31 + c.charCodeAt(0) >>> 0;
|
|
33336
|
+
return `${indexUrl}#actor-${h.toString(36)}`;
|
|
33337
|
+
}
|
|
33338
|
+
async function register(pod, indexUrl, actorUrl) {
|
|
33339
|
+
const g = await pod.readRdf(indexUrl);
|
|
33340
|
+
if (!g) throw new Error(`no type index at ${indexUrl}`);
|
|
33341
|
+
if ((await actorsIn(pod, indexUrl)).includes(actorUrl)) return false;
|
|
33342
|
+
const reg = pod.sym(regFor(indexUrl, actorUrl));
|
|
33343
|
+
const status = await pod.writeRdfChecked(indexUrl, g, {
|
|
33344
|
+
inserts: [
|
|
33345
|
+
[reg, pod.sym(RDF_TYPE), pod.sym(SOLID + "TypeRegistration")],
|
|
33346
|
+
[reg, pod.sym(SOLID + "forClass"), pod.sym(AS_ACTOR)],
|
|
33347
|
+
[reg, pod.sym(SOLID + "instance"), pod.sym(actorUrl)]
|
|
33348
|
+
],
|
|
33349
|
+
mustDescribe: indexUrl
|
|
33350
|
+
});
|
|
33351
|
+
if (status >= 300) throw new Error(`the type index at ${indexUrl} did not take the registration (${status})`);
|
|
33352
|
+
return true;
|
|
33353
|
+
}
|
|
33354
|
+
async function createPublicIndex(pod, podBase) {
|
|
33355
|
+
const url = newIndexUrl(podBase);
|
|
33356
|
+
const doc = pod.sym(url);
|
|
33357
|
+
const existing = await pod.readRdf(url).catch(() => null);
|
|
33358
|
+
if (!existing) {
|
|
33359
|
+
const status = await pod.writeRdfChecked(url, null, {
|
|
33360
|
+
inserts: [
|
|
33361
|
+
[doc, pod.sym(RDF_TYPE), pod.sym(SOLID + "TypeIndex")],
|
|
33362
|
+
[doc, pod.sym(RDF_TYPE), pod.sym(SOLID + "ListedDocument")]
|
|
33363
|
+
],
|
|
33364
|
+
mustDescribe: url
|
|
33365
|
+
});
|
|
33366
|
+
if (status >= 300) throw new Error(`could not make a type index at ${url} (${status})`);
|
|
33367
|
+
}
|
|
33368
|
+
await pod.setAcl(url, PUBLIC_READ);
|
|
33369
|
+
await pod.writeAboutWebId(podBase, {
|
|
33370
|
+
inserts: [[pod.sym(pod.webId), pod.sym(SOLID + "publicTypeIndex"), doc]]
|
|
33371
|
+
});
|
|
33372
|
+
return url;
|
|
33373
|
+
}
|
|
33374
|
+
async function registeredActors(pod, podBase) {
|
|
33375
|
+
const index = await findPublicIndex(pod, podBase).catch(() => null);
|
|
33376
|
+
if (!index) return [];
|
|
33377
|
+
return (await actorsIn(pod, index)).filter((a) => a.startsWith(podBase) && a.endsWith("ap/actor"));
|
|
33378
|
+
}
|
|
33379
|
+
|
|
33380
|
+
// lib/pod/location.mjs
|
|
33381
|
+
var RESERVED = /* @__PURE__ */ new Set(["profile", "settings", ".well-known"]);
|
|
33382
|
+
var SEGMENT = /^[A-Za-z0-9._-]+$/u;
|
|
33383
|
+
function podRootPath(podBase) {
|
|
33384
|
+
return new URL(podBase).pathname;
|
|
33385
|
+
}
|
|
33386
|
+
function rootFromContainer(podBase, typed, name) {
|
|
33387
|
+
const base = new URL(podBase);
|
|
33388
|
+
let path = String(typed ?? "").trim() || base.pathname;
|
|
33389
|
+
if (!path.startsWith("/")) path = "/" + path;
|
|
33390
|
+
if (!path.endsWith("/")) path += "/";
|
|
33391
|
+
if (!path.startsWith(base.pathname)) {
|
|
33392
|
+
return { problem: `that is not in your pod \u2014 your pod starts at ${base.pathname}` };
|
|
33393
|
+
}
|
|
33394
|
+
const inside = path.slice(base.pathname.length);
|
|
33395
|
+
const segments = inside.split("/").filter(Boolean);
|
|
33396
|
+
if (segments.some((s) => s === "." || s === "..")) return { problem: "a container path cannot go up with .." };
|
|
33397
|
+
if (segments.some((s) => !SEGMENT.test(s))) {
|
|
33398
|
+
return { problem: "container names are letters, digits, dots, dashes and underscores" };
|
|
33399
|
+
}
|
|
33400
|
+
if (segments.length && RESERVED.has(segments[0])) {
|
|
33401
|
+
return { problem: `${base.pathname}${segments[0]}/ belongs to your pod itself \u2014 choose another container` };
|
|
33402
|
+
}
|
|
33403
|
+
if (segments.length > 8) return { problem: "that is nested too deep \u2014 eight containers at most" };
|
|
33404
|
+
return { root: (segments.length ? segments.join("/") + "/" : "") + name };
|
|
33405
|
+
}
|
|
33406
|
+
function rootOfActor(podBase, actorUrl) {
|
|
33407
|
+
const a = String(actorUrl || "");
|
|
33408
|
+
if (!a.startsWith(podBase) || !a.endsWith("ap/actor")) return null;
|
|
33409
|
+
const root = a.slice(podBase.length, -"ap/actor".length);
|
|
33410
|
+
return root.endsWith("/") ? root : null;
|
|
33411
|
+
}
|
|
33412
|
+
|
|
33413
|
+
// lib/core/wire.mjs
|
|
33414
|
+
var DEFAULT_ROOT = "fedipod/";
|
|
33415
|
+
|
|
33416
|
+
// lib/core/place.mjs
|
|
33417
|
+
var chosenRoot = (podBase, typed) => rootFromContainer(podBase, typed, DEFAULT_ROOT);
|
|
33418
|
+
async function candidateRoots(pod, podBase) {
|
|
33419
|
+
const recorded = await registeredActors(pod, podBase).catch(() => []);
|
|
33420
|
+
const roots = recorded.map((a) => rootOfActor(podBase, a)).filter(Boolean);
|
|
33421
|
+
return [.../* @__PURE__ */ new Set([...roots, DEFAULT_ROOT])];
|
|
33422
|
+
}
|
|
33423
|
+
async function findAccount(pod, podBase, read, accept = () => true) {
|
|
33424
|
+
for (const root of await candidateRoots(pod, podBase)) {
|
|
33425
|
+
const config = await read(root).catch(() => null);
|
|
33426
|
+
if (config && accept(config)) return { root, config };
|
|
33427
|
+
}
|
|
33428
|
+
return null;
|
|
33429
|
+
}
|
|
33430
|
+
async function recordPlace(pod, podBase, actorAtPod, { create = false } = {}) {
|
|
33431
|
+
let index = await findPublicIndex(pod, podBase);
|
|
33432
|
+
if (!index) {
|
|
33433
|
+
if (!create) return "no-index";
|
|
33434
|
+
index = await createPublicIndex(pod, podBase);
|
|
33435
|
+
}
|
|
33436
|
+
return await register(pod, index, actorAtPod) ? "registered" : "already";
|
|
33437
|
+
}
|
|
33438
|
+
async function hasPublicIndex(pod, podBase) {
|
|
33439
|
+
return !!await findPublicIndex(pod, podBase).catch(() => null);
|
|
33440
|
+
}
|
|
33441
|
+
|
|
33115
33442
|
// web/app/signup.mjs
|
|
33116
|
-
var
|
|
33117
|
-
var
|
|
33118
|
-
var
|
|
33443
|
+
var actorUrlFor = (pod, root) => `${pod}${root}ap/actor`;
|
|
33444
|
+
var keysDocFor = (pod, root) => `${pod}${root}ap-state/keys.json`;
|
|
33445
|
+
var stateOf = (pod, root) => `${pod}${root}ap-state/`;
|
|
33446
|
+
var readConfigAt = (remote, pod) => (root) => readConfig(remote, { state: stateOf(pod, root) });
|
|
33119
33447
|
var HANDLE_RE = /^[a-z0-9-]{2,30}$/;
|
|
33120
33448
|
function handleProblem(handle) {
|
|
33121
33449
|
if (!handle) return "a handle is required";
|
|
@@ -33124,7 +33452,7 @@ function handleProblem(handle) {
|
|
|
33124
33452
|
return null;
|
|
33125
33453
|
}
|
|
33126
33454
|
var PROGRESS = /* @__PURE__ */ new Map();
|
|
33127
|
-
var progressKey = (webId, a) => [webId, a.handle, a.shape || "pod"].join("|");
|
|
33455
|
+
var progressKey = (webId, a) => [webId, a.handle, a.shape || "pod", a.container || ""].join("|");
|
|
33128
33456
|
async function assertFrontNameFree(frontOrigin, handle) {
|
|
33129
33457
|
const res = await fetch(
|
|
33130
33458
|
`${frontOrigin.replace(/\/$/, "")}/api/handle?handle=${encodeURIComponent(handle)}`,
|
|
@@ -33153,10 +33481,23 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33153
33481
|
ok: (note) => onStep(key2, "ok", note),
|
|
33154
33482
|
skip: (note) => onStep(key2, "skipped", note)
|
|
33155
33483
|
});
|
|
33484
|
+
const place = chosenRoot(pod, answers.container);
|
|
33485
|
+
if (place.problem) throw new Error(`Where to store it: ${place.problem}.`);
|
|
33486
|
+
const root = place.root;
|
|
33487
|
+
const remote = new BrowserRemotePod(session, { webId, role: "signup", log: () => {
|
|
33488
|
+
} });
|
|
33156
33489
|
const podStep = step("pod");
|
|
33157
33490
|
if (!prog.pod) {
|
|
33158
33491
|
podStep.running("checking your pod");
|
|
33159
|
-
if (await
|
|
33492
|
+
if (await findAccount(remote, pod, readConfigAt(remote, pod)) || await resourceExists(session.fetch, actorUrlFor(pod, root))) {
|
|
33493
|
+
throw new Error("The pod already hosts a FediPod account. If you want a second account, put it on a different pod.");
|
|
33494
|
+
}
|
|
33495
|
+
if (await resourceExists(session.fetch, `${pod}${root}`)) {
|
|
33496
|
+
throw new Error(`${new URL(pod + root).pathname} is already there and is not a FediPod account \u2014 choose another container.`);
|
|
33497
|
+
}
|
|
33498
|
+
if (!answers.createIndex && !await hasPublicIndex(remote, pod)) {
|
|
33499
|
+
throw Object.assign(new Error("Your profile names no public type index."), { code: "needs-index" });
|
|
33500
|
+
}
|
|
33160
33501
|
prog.pod = pod;
|
|
33161
33502
|
podStep.ok(pod);
|
|
33162
33503
|
} else {
|
|
@@ -33165,10 +33506,10 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33165
33506
|
const pathPod = new URL(pod).pathname !== "/";
|
|
33166
33507
|
const fronted = pathPod || wantsFront;
|
|
33167
33508
|
if (fronted && !frontOrigin) {
|
|
33168
|
-
throw new Error(`${pod} is a
|
|
33509
|
+
throw new Error(`${pod} is a suffixed pod, so its address must live at a gateway, and this page has none.`);
|
|
33169
33510
|
}
|
|
33170
33511
|
if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
33171
|
-
const actorUrl = actorUrlFor(pod);
|
|
33512
|
+
const actorUrl = actorUrlFor(pod, root);
|
|
33172
33513
|
const frontActor = fronted ? `${frontOrigin.replace(/\/$/, "")}/u/${handle}/ap/actor` : null;
|
|
33173
33514
|
let gateway = answers.gateway || prog.gateway || null;
|
|
33174
33515
|
if (frontOrigin && !gateway) {
|
|
@@ -33182,7 +33523,7 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33182
33523
|
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
33183
33524
|
// the container the agent drains, where nothing would ever read it. The
|
|
33184
33525
|
// manage surface has always sent `urls.home`; this is the same value.
|
|
33185
|
-
body: JSON.stringify({ handle, podHome: `${pod}${
|
|
33526
|
+
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl, kind: "person", fronted })
|
|
33186
33527
|
});
|
|
33187
33528
|
const d = await res.json().catch(() => ({}));
|
|
33188
33529
|
if (res.status !== 201 || !d.hmacSecret) {
|
|
@@ -33194,8 +33535,6 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33194
33535
|
} else if (frontOrigin && gateway) {
|
|
33195
33536
|
step("gateway").ok();
|
|
33196
33537
|
}
|
|
33197
|
-
const remote = new BrowserRemotePod(session, { webId, role: "signup", log: () => {
|
|
33198
|
-
} });
|
|
33199
33538
|
const keysStep = step("keys");
|
|
33200
33539
|
let keys;
|
|
33201
33540
|
if (!prog.keysStored) {
|
|
@@ -33204,12 +33543,12 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33204
33543
|
keys.mintedFor = gateway?.frontActor || actorUrl;
|
|
33205
33544
|
try {
|
|
33206
33545
|
await provisionKey(remote, {
|
|
33207
|
-
stateUrl:
|
|
33208
|
-
keysUrl: keysDocFor(pod),
|
|
33546
|
+
stateUrl: stateOf(pod, root),
|
|
33547
|
+
keysUrl: keysDocFor(pod, root),
|
|
33209
33548
|
keys
|
|
33210
33549
|
});
|
|
33211
33550
|
} 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
|
|
33551
|
+
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
33552
|
}
|
|
33214
33553
|
await cacheOpenedKeys(actorUrl, keys);
|
|
33215
33554
|
prog.keys = keys;
|
|
@@ -33221,7 +33560,7 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33221
33560
|
}
|
|
33222
33561
|
const config = {
|
|
33223
33562
|
remotePod: pod,
|
|
33224
|
-
root
|
|
33563
|
+
root,
|
|
33225
33564
|
handle,
|
|
33226
33565
|
name: handle,
|
|
33227
33566
|
issuer: String(session.issuer || "").replace(/\/+$/, ""),
|
|
@@ -33229,10 +33568,21 @@ async function signUp(answers, { session, onStep = () => {
|
|
|
33229
33568
|
...gateway ? { gateway } : {}
|
|
33230
33569
|
};
|
|
33231
33570
|
try {
|
|
33232
|
-
await writeConfig(remote, { state:
|
|
33571
|
+
await writeConfig(remote, { state: stateOf(pod, root) }, config);
|
|
33233
33572
|
} catch (e) {
|
|
33234
33573
|
throw new Error(`could not store the config on the pod (${e.message})`);
|
|
33235
33574
|
}
|
|
33575
|
+
if (!prog.placed) {
|
|
33576
|
+
const placeStep = step("place");
|
|
33577
|
+
placeStep.running("recording where your account lives");
|
|
33578
|
+
try {
|
|
33579
|
+
await recordPlace(remote, pod, actorUrl, { create: !!answers.createIndex });
|
|
33580
|
+
} catch (e) {
|
|
33581
|
+
throw new Error(`could not record where your account lives in your type index (${e.message})`);
|
|
33582
|
+
}
|
|
33583
|
+
prog.placed = true;
|
|
33584
|
+
placeStep.ok();
|
|
33585
|
+
}
|
|
33236
33586
|
PROGRESS.delete(key);
|
|
33237
33587
|
const host = gateway?.frontActor ? new URL(gateway.frontActor).host : new URL(pod).host;
|
|
33238
33588
|
return {
|
|
@@ -33249,8 +33599,9 @@ async function readAccount(session) {
|
|
|
33249
33599
|
const pod = podBaseOfWebId(session.webId);
|
|
33250
33600
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
|
|
33251
33601
|
} });
|
|
33252
|
-
const
|
|
33253
|
-
if (!
|
|
33602
|
+
const found = await findAccount(remote, pod, readConfigAt(remote, pod), (c) => (c.kind || "person") === "person");
|
|
33603
|
+
if (!found) return null;
|
|
33604
|
+
const { root, config } = found;
|
|
33254
33605
|
const frontActor = config.gateway?.frontActor || null;
|
|
33255
33606
|
let frontHost = null;
|
|
33256
33607
|
try {
|
|
@@ -33264,7 +33615,7 @@ async function readAccount(session) {
|
|
|
33264
33615
|
} catch {
|
|
33265
33616
|
podHost = "";
|
|
33266
33617
|
}
|
|
33267
|
-
return { pod, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
|
|
33618
|
+
return { pod, root, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
|
|
33268
33619
|
}
|
|
33269
33620
|
async function moveIn(answers, { session, onStep = () => {
|
|
33270
33621
|
}, frontOrigin = null } = {}) {
|
|
@@ -33280,10 +33631,10 @@ async function moveIn(answers, { session, onStep = () => {
|
|
|
33280
33631
|
const here = await readAccount(session);
|
|
33281
33632
|
if (!here?.frontActor) throw new Error("this pod holds no account at another gateway to move here");
|
|
33282
33633
|
if (here.frontHost === new URL(origin).host) throw new Error("this account already lives at this gateway");
|
|
33283
|
-
const { pod, config: old } = here;
|
|
33634
|
+
const { pod, root, config: old } = here;
|
|
33284
33635
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
|
|
33285
33636
|
} });
|
|
33286
|
-
const urls = { state:
|
|
33637
|
+
const urls = { state: stateOf(pod, root) };
|
|
33287
33638
|
const keys = await readKeys(remote, urls);
|
|
33288
33639
|
if (!keys?.rsa) throw new Error("could not read this account's signing key on the pod");
|
|
33289
33640
|
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 +33645,7 @@ async function moveIn(answers, { session, onStep = () => {
|
|
|
33294
33645
|
const res = await session.fetch(`${origin}/api/attach`, {
|
|
33295
33646
|
method: "POST",
|
|
33296
33647
|
headers: { "content-type": "application/json" },
|
|
33297
|
-
body: JSON.stringify({ handle, podHome: `${pod}${
|
|
33648
|
+
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl: actorUrlFor(pod, root), kind: old.kind || "person", fronted: true })
|
|
33298
33649
|
});
|
|
33299
33650
|
const d = await res.json().catch(() => ({}));
|
|
33300
33651
|
if (res.status !== 201 || !d.hmacSecret) throw new Error(`could not attach to this gateway (HTTP ${res.status}): ${d.error || ""}`);
|
|
@@ -33322,7 +33673,7 @@ async function moveIn(answers, { session, onStep = () => {
|
|
|
33322
33673
|
} catch (e) {
|
|
33323
33674
|
throw new Error(`could not update your account on the pod (${e.message})`);
|
|
33324
33675
|
}
|
|
33325
|
-
await cacheOpenedKeys(actorUrlFor(pod), keys);
|
|
33676
|
+
await cacheOpenedKeys(actorUrlFor(pod, root), keys);
|
|
33326
33677
|
keysStep.ok();
|
|
33327
33678
|
return { config, address: `@${handle}@${new URL(origin).host}`, movedFrom: config.movedFrom };
|
|
33328
33679
|
}
|
|
@@ -33571,6 +33922,24 @@ function solidOidcSession({ dbName = "solid-oidc-session", clientName = "Solid a
|
|
|
33571
33922
|
// web/app/oidc-session.mjs
|
|
33572
33923
|
var { beginLogin, completeLogin, getSession, signOut } = solidOidcSession({ dbName: "fedipod-oidc", clientName: "FediPod" });
|
|
33573
33924
|
|
|
33925
|
+
// web/app/warm-start.mjs
|
|
33926
|
+
var DB2 = "fedipod-warm";
|
|
33927
|
+
var WARM_GAP_MS = 5 * 6e4;
|
|
33928
|
+
var FULL_EVERY_MS = 60 * 6e4;
|
|
33929
|
+
var DRAIN_EVERY_MS = 2 * 6e4;
|
|
33930
|
+
function deleteWarm() {
|
|
33931
|
+
return new Promise((res) => {
|
|
33932
|
+
let rq;
|
|
33933
|
+
try {
|
|
33934
|
+
rq = indexedDB.deleteDatabase(DB2);
|
|
33935
|
+
} catch {
|
|
33936
|
+
res();
|
|
33937
|
+
return;
|
|
33938
|
+
}
|
|
33939
|
+
rq.onsuccess = rq.onerror = rq.onblocked = () => res();
|
|
33940
|
+
});
|
|
33941
|
+
}
|
|
33942
|
+
|
|
33574
33943
|
// web/app/boot.mjs
|
|
33575
33944
|
var REDIRECT = `${location.origin}/`;
|
|
33576
33945
|
async function bootWorker({ reset = false } = {}) {
|
|
@@ -33614,18 +33983,14 @@ async function bootWorker({ reset = false } = {}) {
|
|
|
33614
33983
|
async function readAccountState() {
|
|
33615
33984
|
const session = await getSession();
|
|
33616
33985
|
if (!session) throw new Error("Sign in first.");
|
|
33617
|
-
const
|
|
33618
|
-
|
|
33986
|
+
const here = await readAccount(session);
|
|
33987
|
+
if (!here) throw new Error(`could not find a FediPod account on ${podBaseOfWebId(session.webId)}`);
|
|
33619
33988
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
|
|
33620
33989
|
} });
|
|
33621
|
-
const urls = { state };
|
|
33622
|
-
const
|
|
33623
|
-
|
|
33624
|
-
|
|
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 };
|
|
33990
|
+
const urls = { state: `${here.pod}${here.root}ap-state/` };
|
|
33991
|
+
const doc = await readKeys(remote, urls);
|
|
33992
|
+
const actorUrl = `${here.config.remotePod || here.pod}${here.config.root || here.root}ap/actor`;
|
|
33993
|
+
return { remote, urls, cfg: here.config, doc, actorUrl };
|
|
33629
33994
|
}
|
|
33630
33995
|
window.fedipodUnlock = async (password) => {
|
|
33631
33996
|
if (!password) throw new Error("Enter your password.");
|
|
@@ -33679,18 +34044,28 @@ function parseAddress(input) {
|
|
|
33679
34044
|
if (!handle || !host || !host.includes(".")) return null;
|
|
33680
34045
|
return { handle, host };
|
|
33681
34046
|
}
|
|
33682
|
-
async function
|
|
34047
|
+
async function issuerForActor(actorUrl) {
|
|
33683
34048
|
try {
|
|
33684
|
-
const authz = await readIssuer(
|
|
34049
|
+
const authz = await readIssuer(actorUrl);
|
|
33685
34050
|
if (authz) return new URL(authz).origin;
|
|
33686
34051
|
} catch {
|
|
33687
34052
|
}
|
|
33688
|
-
const u = new URL(
|
|
33689
|
-
|
|
34053
|
+
const u = new URL(actorUrl);
|
|
34054
|
+
const own = await fetch(`${u.origin}/.well-known/openid-configuration`, { headers: { accept: "application/json" } }).catch(() => null);
|
|
34055
|
+
if (own?.ok) return u.origin;
|
|
33690
34056
|
const parent = u.host.split(".").slice(1).join(".");
|
|
33691
34057
|
return `https://${parent || u.host}`;
|
|
33692
34058
|
}
|
|
33693
|
-
async function
|
|
34059
|
+
async function actorForPodAddress(handle, host) {
|
|
34060
|
+
const res = await fetch(
|
|
34061
|
+
`https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${host}`)}`,
|
|
34062
|
+
{ headers: { accept: "application/jrd+json, application/json" } }
|
|
34063
|
+
).catch(() => null);
|
|
34064
|
+
const doc = res?.ok ? await res.json().catch(() => ({})) : {};
|
|
34065
|
+
const self2 = (doc.links || []).find((l) => l.rel === "self" && /activity\+json|ld\+json/u.test(l.type || ""));
|
|
34066
|
+
return self2?.href || `https://${host}/fedipod/ap/actor`;
|
|
34067
|
+
}
|
|
34068
|
+
async function actorForFrontedAddress(handle) {
|
|
33694
34069
|
const res = await fetch(
|
|
33695
34070
|
`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
|
|
33696
34071
|
{ headers: { accept: "application/jrd+json, application/json" } }
|
|
@@ -33700,17 +34075,15 @@ async function podForFrontedAddress(handle) {
|
|
|
33700
34075
|
const doc = await res.json().catch(() => ({}));
|
|
33701
34076
|
const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
|
|
33702
34077
|
if (!podActorId) throw new Error(`@${handle}@${location.host} lives here but names no pod to sign in to`);
|
|
33703
|
-
|
|
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);
|
|
34078
|
+
return podActorId;
|
|
33706
34079
|
}
|
|
33707
34080
|
window.fedipodSignin = async ({ address }) => {
|
|
33708
34081
|
const parsed = parseAddress(address);
|
|
33709
34082
|
if (!parsed) throw new Error("Enter your address as @you@yourpod (for example @alice@alice.solidcommunity.net).");
|
|
33710
34083
|
const bad = handleProblem(parsed.handle);
|
|
33711
34084
|
if (bad) throw new Error(bad);
|
|
33712
|
-
const
|
|
33713
|
-
const issuer = await
|
|
34085
|
+
const actor = parsed.host === location.host.toLowerCase() ? await actorForFrontedAddress(parsed.handle) : await actorForPodAddress(parsed.handle, parsed.host);
|
|
34086
|
+
const issuer = await issuerForActor(actor);
|
|
33714
34087
|
const { authorizationUrl } = await beginLogin({ issuer, redirectUri: REDIRECT });
|
|
33715
34088
|
location.href = authorizationUrl;
|
|
33716
34089
|
};
|
|
@@ -33754,6 +34127,7 @@ window.fedipodSignOut = async () => {
|
|
|
33754
34127
|
}
|
|
33755
34128
|
req.onsuccess = req.onerror = req.onblocked = () => res();
|
|
33756
34129
|
});
|
|
34130
|
+
await deleteWarm();
|
|
33757
34131
|
};
|
|
33758
34132
|
window.fedipodHandleProblem = handleProblem;
|
|
33759
34133
|
if (typeof document !== "undefined") (async () => {
|
|
@@ -33949,6 +34323,8 @@ ${e.detail}` : "");
|
|
|
33949
34323
|
$("brand").hidden = false;
|
|
33950
34324
|
$("pane-form").hidden = false;
|
|
33951
34325
|
$("signed-pod").textContent = signedPod;
|
|
34326
|
+
$("container").placeholder = podRootPath(signedPod);
|
|
34327
|
+
$("container-field").hidden = !!moveFrom;
|
|
33952
34328
|
$("movein-note").hidden = !moveFrom;
|
|
33953
34329
|
if (moveFrom) {
|
|
33954
34330
|
$("movein-from").textContent = moveFrom.address;
|
|
@@ -33999,7 +34375,12 @@ ${e.detail}` : "");
|
|
|
33999
34375
|
}
|
|
34000
34376
|
};
|
|
34001
34377
|
const shape = () => pathPod() || moveFrom ? "front" : f().shape.value;
|
|
34002
|
-
const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape() });
|
|
34378
|
+
const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape(), container: f().container.value.trim() });
|
|
34379
|
+
const previewPlace = () => {
|
|
34380
|
+
if (!signedPod) return;
|
|
34381
|
+
const r = chosenRoot(signedPod, f().container.value);
|
|
34382
|
+
$("place").textContent = r.problem ? "\u2026" : signedPod + r.root;
|
|
34383
|
+
};
|
|
34003
34384
|
const previewAddr = () => {
|
|
34004
34385
|
const handle = f().handle.value.trim().toLowerCase();
|
|
34005
34386
|
let host = "";
|
|
@@ -34030,6 +34411,7 @@ ${e.detail}` : "");
|
|
|
34030
34411
|
applyMode();
|
|
34031
34412
|
applyShape();
|
|
34032
34413
|
previewAddr();
|
|
34414
|
+
previewPlace();
|
|
34033
34415
|
});
|
|
34034
34416
|
applyMode();
|
|
34035
34417
|
const STEP_IDS = ["step-1", "step-2"];
|
|
@@ -34043,6 +34425,8 @@ ${e.detail}` : "");
|
|
|
34043
34425
|
if (n === 2) {
|
|
34044
34426
|
applyShape();
|
|
34045
34427
|
previewAddr();
|
|
34428
|
+
previewPlace();
|
|
34429
|
+
$("index-ask").hidden = true;
|
|
34046
34430
|
}
|
|
34047
34431
|
if (FOCUS[n]) $(FOCUS[n]).focus();
|
|
34048
34432
|
};
|
|
@@ -34053,6 +34437,10 @@ ${e.detail}` : "");
|
|
|
34053
34437
|
const validateStep2 = () => {
|
|
34054
34438
|
const hp = window.fedipodHandleProblem(f().handle.value.trim().toLowerCase());
|
|
34055
34439
|
if (hp) return `Fediverse handle: ${hp}`;
|
|
34440
|
+
if (!moveFrom) {
|
|
34441
|
+
const where = chosenRoot(signedPod, f().container.value).problem;
|
|
34442
|
+
if (where) return `Where to store it: ${where}`;
|
|
34443
|
+
}
|
|
34056
34444
|
return null;
|
|
34057
34445
|
};
|
|
34058
34446
|
$("create").addEventListener("click", showForm);
|
|
@@ -34088,18 +34476,15 @@ ${e.detail}` : "");
|
|
|
34088
34476
|
showLanding();
|
|
34089
34477
|
goStep(1);
|
|
34090
34478
|
});
|
|
34091
|
-
const LABELS = {
|
|
34479
|
+
const LABELS = {
|
|
34480
|
+
pod: "Checking your pod",
|
|
34481
|
+
keys: "Making your signing key",
|
|
34482
|
+
gateway: "Connecting your mail door",
|
|
34483
|
+
place: "Recording where your account lives"
|
|
34484
|
+
};
|
|
34092
34485
|
const MOVE_LABELS = { pod: "Reading your account on your pod", gateway: "Taking your address here", keys: "Moving your key and account record" };
|
|
34093
|
-
|
|
34094
|
-
|
|
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();
|
|
34486
|
+
const run = async (createIndex = false) => {
|
|
34487
|
+
const a = { ...answers(), ...createIndex ? { createIndex: true } : {} };
|
|
34103
34488
|
$("pane-form").hidden = true;
|
|
34104
34489
|
$("running").hidden = false;
|
|
34105
34490
|
$("running-title").textContent = "Setting up\u2026";
|
|
@@ -34122,10 +34507,35 @@ ${e.detail}` : "");
|
|
|
34122
34507
|
else await window.fedipodSignup({ ...a, onStep });
|
|
34123
34508
|
location.href = "/admin/client/";
|
|
34124
34509
|
} catch (err) {
|
|
34510
|
+
if (err?.code === "needs-index") {
|
|
34511
|
+
backToForm();
|
|
34512
|
+
$("index-ask").hidden = false;
|
|
34513
|
+
$("index-yes").focus();
|
|
34514
|
+
return;
|
|
34515
|
+
}
|
|
34125
34516
|
$("running-title").textContent = "Setup did not finish";
|
|
34126
34517
|
$("run-error").textContent = err.message || String(err);
|
|
34127
34518
|
$("run-actions").hidden = false;
|
|
34128
34519
|
}
|
|
34520
|
+
};
|
|
34521
|
+
$("form").addEventListener("submit", async (e) => {
|
|
34522
|
+
e.preventDefault();
|
|
34523
|
+
$("form-error").textContent = "";
|
|
34524
|
+
const e2 = validateStep2();
|
|
34525
|
+
if (e2) {
|
|
34526
|
+
$("form-error").textContent = e2;
|
|
34527
|
+
goStep(2);
|
|
34528
|
+
return;
|
|
34529
|
+
}
|
|
34530
|
+
await run();
|
|
34531
|
+
});
|
|
34532
|
+
$("index-yes").addEventListener("click", () => {
|
|
34533
|
+
$("index-ask").hidden = true;
|
|
34534
|
+
run(true);
|
|
34535
|
+
});
|
|
34536
|
+
$("index-no").addEventListener("click", () => {
|
|
34537
|
+
$("index-ask").hidden = true;
|
|
34538
|
+
$("form-error").textContent = "Sign-up stopped. Nothing was written to your pod.";
|
|
34129
34539
|
});
|
|
34130
34540
|
if (pendingIdentity) await showIdentity();
|
|
34131
34541
|
})();
|