fedipod-server 0.17.0 → 0.18.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.
@@ -110,20 +110,30 @@ a { color: var(--accent); }
110
110
  </section>
111
111
 
112
112
  <!-- Shown only on a browser that has signed in but has never opened the signing
113
- key. The key is kept on the pod encrypted under the account password, so
114
- the pod's host cannot sign as you; a browser opens it once and remembers. -->
113
+ key. The key is kept on the pod encrypted under the sign-up password, so
114
+ the pod's host cannot sign as you; a browser opens it once and remembers.
115
+ The second block is for someone who no longer has that password: a new
116
+ key, locked under the password they use now. -->
115
117
  <section id="unlock" hidden>
116
118
  <h2>Unlock your signing key</h2>
117
- <p>Your signing key is stored on your pod, locked under your account password,
118
- so nobody who can read your pod — your pod's host included — can post as you.
119
- This browser has not opened it yet.</p>
119
+ <p>Your signing key is stored on your pod, locked under the password you gave
120
+ when you signed up, so nobody who can read your pod — your pod's host included
121
+ — can post as you. This browser has not opened it yet.</p>
120
122
  <p class="row">
121
123
  <input type="password" id="unlock-password" autocomplete="current-password"
122
- placeholder="Your account password" aria-label="Your account password">
124
+ placeholder="Your password" aria-label="Your password">
123
125
  <button type="button" class="primary" id="unlock-go">unlock</button>
124
126
  </p>
125
127
  <p class="err" id="unlock-error" role="alert"></p>
126
128
  <p class="hint">This browser only — it is not sent anywhere.</p>
129
+ <h2>Changed your pod password, or lost the old one?</h2>
130
+ <p>Make a new signing key, locked under the password you use now. Your account,
131
+ address, posts and followers stay as they are; other servers learn the new key
132
+ the next time they hear from you.</p>
133
+ <p><button type="button" id="unlock-newkey">make a new signing key</button></p>
134
+ <p id="unlock-newkey-confirm" hidden>This replaces the key on your pod. Type the
135
+ password you use now in the field above, then
136
+ <button type="button" id="unlock-newkey-go">make the new key</button></p>
127
137
  </section>
128
138
 
129
139
 
@@ -277,6 +277,8 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
277
277
 
278
278
  <section id="pane-identity" hidden>
279
279
  <dl id="facts"></dl>
280
+ <!-- State documents the agent could not read on its last load, if any. -->
281
+ <p class="err" id="state-skipped" role="alert" hidden></p>
280
282
  <!-- Declared here, and render() puts these on the kind row — the row they
281
283
  belong to is generated, so this is the only place they can be written
282
284
  down. Each reads as the setting it would change, so what it shows IS the
@@ -145,8 +145,16 @@ const INBOX_PROMPT_AT = 500;
145
145
  let dismissed = false;
146
146
 
147
147
  async function renderInbox() {
148
- if (dismissed) return;
149
148
  const { json: st } = await api('/status');
149
+ // A state document the last load could not read is a timeline or a contact
150
+ // list quietly missing; the page says which, under the facts.
151
+ const skipped = st?.stateSkipped || [];
152
+ const line = $('state-skipped');
153
+ line.hidden = !skipped.length;
154
+ line.textContent = skipped.length
155
+ ? `${skipped.length} state document${skipped.length === 1 ? '' : 's'} could not be read on the last load: ${skipped.join(', ')}.`
156
+ : '';
157
+ if (dismissed) return;
150
158
  const box = st?.inbox;
151
159
  const panel = $('pane-inbox');
152
160
  if (!box || box.count < INBOX_PROMPT_AT) { panel.hidden = true; return; }
@@ -33075,7 +33075,7 @@ var PodTransport = class {
33075
33075
  * says all of it. The parsed graph must mention the WebID before anything is
33076
33076
  * written back — an empty or foreign body must never become the new profile.
33077
33077
  */
33078
- async linkAccountInProfile({ actorUrl, accountName, kind = "person" }) {
33078
+ async linkAccountInProfile({ actorUrl, accountName, kind = "person", outbox = null }) {
33079
33079
  const docUrl = this.webId.split("#")[0];
33080
33080
  const res = await this.fetch(docUrl, { headers: { accept: "text/turtle" } });
33081
33081
  if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} \u2192 ${res.status}`);
@@ -33091,10 +33091,16 @@ var PodTransport = class {
33091
33091
  [me, FOAF("account"), actor],
33092
33092
  [actor, RDF3("type"), FOAF("OnlineAccount")],
33093
33093
  [actor, RDF3("type"), kind === "group" ? AS("Group") : AS("Person")],
33094
- [actor, FOAF("accountName"), literal2(accountName)]
33094
+ [actor, FOAF("accountName"), literal2(accountName)],
33095
+ // Where a Solid client posts on this person's behalf (`as:outbox` on the
33096
+ // WebID is what dokieli reads); only where a door exists to take it.
33097
+ ...outbox ? [[me, AS("outbox"), namedNode2(outbox)]] : []
33095
33098
  ];
33096
33099
  const missing = wanted.filter(([s, p, o]) => !g.holds(s, p, o, doc));
33097
- const stale = g.statementsMatching(actor, FOAF("accountName"), null, doc).filter((st2) => st2.object.value !== accountName);
33100
+ const stale = [
33101
+ ...g.statementsMatching(actor, FOAF("accountName"), null, doc).filter((st2) => st2.object.value !== accountName),
33102
+ ...outbox ? g.statementsMatching(me, AS("outbox"), null, doc).filter((st2) => st2.object.value !== outbox) : []
33103
+ ];
33098
33104
  if (!missing.length && !stale.length) return false;
33099
33105
  const deletes = stale.map((st2) => [st2.subject, st2.predicate, st2.object]);
33100
33106
  if (await this.patchDocument(docUrl, missing, deletes)) return true;
@@ -33202,12 +33208,54 @@ var BrowserRemotePod = class extends PodTransport {
33202
33208
  // lib/pod/state.mjs
33203
33209
  var readWrappedKeys = (pod, urls) => pod.getJson(urls.state + "keys.json");
33204
33210
  var readConfig = (pod, urls) => pod.getJson(urls.state + "config.json");
33211
+ var writeWrappedKeys = (pod, urls, envelope) => pod.putJson(urls.state + "keys.json", envelope, "application/json");
33205
33212
  var writeConfig = (pod, urls, config) => pod.putJson(urls.state + "config.json", config, "application/json");
33206
33213
  async function provisionKey(pod, { stateUrl, keysUrl, envelope }) {
33207
33214
  await pod.setAcl(stateUrl, []);
33208
33215
  await pod.putJson(keysUrl, envelope, "application/json");
33209
33216
  }
33210
33217
 
33218
+ // lib/pod/root.mjs
33219
+ var OWNER_LOOKUP_MS = 5e3;
33220
+ var PUBLIC_DOC_MAX_BYTES = 1024 * 1024;
33221
+ async function podLayout(fetchImpl, providerOrigin, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
33222
+ let origin;
33223
+ try {
33224
+ origin = new URL(providerOrigin).origin;
33225
+ } catch {
33226
+ return null;
33227
+ }
33228
+ let res;
33229
+ try {
33230
+ res = await fetchImpl(
33231
+ `${origin}/.well-known/solid`,
33232
+ { headers: { accept: "text/turtle" }, signal: AbortSignal.timeout(timeoutMs) }
33233
+ );
33234
+ } catch {
33235
+ return null;
33236
+ }
33237
+ if (res.status === 501) return "host";
33238
+ if (res.status !== 200) return null;
33239
+ let body = "";
33240
+ try {
33241
+ body = await readCapped(res, 64 * 1024);
33242
+ } catch {
33243
+ return null;
33244
+ }
33245
+ return /ns\/pim\/space#Storage|pim:Storage/u.test(body) ? "path" : null;
33246
+ }
33247
+ async function resourceExists(fetchImpl, url, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
33248
+ try {
33249
+ const res = await fetchImpl(
33250
+ url,
33251
+ { headers: { accept: "application/activity+json" }, signal: AbortSignal.timeout(timeoutMs) }
33252
+ );
33253
+ return !!res && res.status === 200;
33254
+ } catch {
33255
+ return false;
33256
+ }
33257
+ }
33258
+
33211
33259
  // web/app/idb-kv.mjs
33212
33260
  var DB = "fedipod-accounts";
33213
33261
  function open() {
@@ -33310,6 +33358,7 @@ async function signUp(answers, { onStep = () => {
33310
33358
  acct.running("checking your pod");
33311
33359
  const head = await fetch(brought, { method: "HEAD" }).catch(() => null);
33312
33360
  if (!head || head.status >= 400) throw new Error(`the pod at ${brought} did not answer (HTTP ${head?.status || "no response"})`);
33361
+ if (await resourceExists(fetch, actorUrlFor(brought))) throw new Error("The pod already hosts a FediPod account. If you want a second account, put it on a different pod.");
33313
33362
  prog.pod = brought;
33314
33363
  acct.skip("using the pod you brought");
33315
33364
  }
@@ -33450,36 +33499,6 @@ function podBaseOfWebId(webId) {
33450
33499
  return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
33451
33500
  }
33452
33501
 
33453
- // lib/pod/root.mjs
33454
- var OWNER_LOOKUP_MS = 5e3;
33455
- var PUBLIC_DOC_MAX_BYTES = 1024 * 1024;
33456
- async function podLayout(fetchImpl, providerOrigin, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
33457
- let origin;
33458
- try {
33459
- origin = new URL(providerOrigin).origin;
33460
- } catch {
33461
- return null;
33462
- }
33463
- let res;
33464
- try {
33465
- res = await fetchImpl(
33466
- `${origin}/.well-known/solid`,
33467
- { headers: { accept: "text/turtle" }, signal: AbortSignal.timeout(timeoutMs) }
33468
- );
33469
- } catch {
33470
- return null;
33471
- }
33472
- if (res.status === 501) return "host";
33473
- if (res.status !== 200) return null;
33474
- let body = "";
33475
- try {
33476
- body = await readCapped(res, 64 * 1024);
33477
- } catch {
33478
- return null;
33479
- }
33480
- return /ns\/pim\/space#Storage|pim:Storage/u.test(body) ? "path" : null;
33481
- }
33482
-
33483
33502
  // web/app/oidc-session.mjs
33484
33503
  var DB2 = "fedipod-oidc";
33485
33504
  var STORE = "session";
@@ -33725,8 +33744,7 @@ async function bootWorker({ reset = false } = {}) {
33725
33744
  worker.postMessage({ type: "boot", frontOrigin: location.origin });
33726
33745
  await booted;
33727
33746
  }
33728
- window.fedipodUnlock = async (password) => {
33729
- if (!password) throw new Error("Enter your account password.");
33747
+ async function readAccountState() {
33730
33748
  const session = await getSession();
33731
33749
  if (!session) throw new Error("Sign in first.");
33732
33750
  const podFromWebId = podBaseOfWebId(session.webId);
@@ -33738,13 +33756,28 @@ window.fedipodUnlock = async (password) => {
33738
33756
  readConfig(remote, urls),
33739
33757
  readWrappedKeys(remote, urls)
33740
33758
  ]);
33741
- if (!cfg || !doc) throw new Error(`could not read this account's config and key under ${state}`);
33759
+ if (!cfg) throw new Error(`could not read this account's config under ${state}`);
33760
+ const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
33761
+ return { remote, urls, cfg, doc, actorUrl };
33762
+ }
33763
+ window.fedipodUnlock = async (password) => {
33764
+ if (!password) throw new Error("Enter your password.");
33765
+ const { doc, actorUrl } = await readAccountState();
33766
+ if (!doc) throw new Error("could not read this account's key on the pod");
33742
33767
  if (!isKeyEnvelope(doc)) throw new Error("this account's key is not locked \u2014 nothing to unlock");
33743
33768
  const rec = await unwrapKeys(doc, password);
33744
- const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
33745
33769
  await cacheOpenedKeys(actorUrl, rec);
33746
33770
  await bootWorker();
33747
33771
  };
33772
+ window.fedipodNewKey = async (password) => {
33773
+ if (!password) throw new Error("Enter the password you use for your pod now.");
33774
+ const { remote, urls, cfg, actorUrl } = await readAccountState();
33775
+ const keys = await generateKeys();
33776
+ keys.mintedFor = cfg.gateway?.frontActor || actorUrl;
33777
+ await writeWrappedKeys(remote, urls, await wrapKeys(keys, password));
33778
+ await cacheOpenedKeys(actorUrl, keys);
33779
+ await bootWorker();
33780
+ };
33748
33781
  window.fedipodSignup = async ({ onStep, ...answers }) => {
33749
33782
  await signUp(answers, { onStep, frontOrigin: location.origin });
33750
33783
  const { authorizationUrl } = await beginLogin({ issuer: answers.issuer, redirectUri: REDIRECT });
@@ -33849,6 +33882,25 @@ if (typeof document !== "undefined") (async () => {
33849
33882
  $("unlock-password")?.addEventListener("keydown", (e) => {
33850
33883
  if (e.key === "Enter") doUnlock();
33851
33884
  });
33885
+ $("unlock-newkey")?.addEventListener("click", () => {
33886
+ $("unlock-newkey-confirm").hidden = false;
33887
+ $("unlock-password").focus();
33888
+ });
33889
+ const doNewKey = async () => {
33890
+ $("unlock-error").textContent = "";
33891
+ const btn = $("unlock-newkey-go");
33892
+ btn.disabled = true;
33893
+ try {
33894
+ await window.fedipodNewKey($("unlock-password").value);
33895
+ $("unlock-password").value = "";
33896
+ location.href = "/admin/client/";
33897
+ } catch (err) {
33898
+ $("unlock-error").textContent = err.message || String(err);
33899
+ btn.disabled = false;
33900
+ $("unlock-password").select();
33901
+ }
33902
+ };
33903
+ $("unlock-newkey-go")?.addEventListener("click", doNewKey);
33852
33904
  if (params.has("signout") || params.has("add")) {
33853
33905
  if (params.has("signout")) {
33854
33906
  try {
@@ -110,20 +110,30 @@ a { color: var(--accent); }
110
110
  </section>
111
111
 
112
112
  <!-- Shown only on a browser that has signed in but has never opened the signing
113
- key. The key is kept on the pod encrypted under the account password, so
114
- the pod's host cannot sign as you; a browser opens it once and remembers. -->
113
+ key. The key is kept on the pod encrypted under the sign-up password, so
114
+ the pod's host cannot sign as you; a browser opens it once and remembers.
115
+ The second block is for someone who no longer has that password: a new
116
+ key, locked under the password they use now. -->
115
117
  <section id="unlock" hidden>
116
118
  <h2>Unlock your signing key</h2>
117
- <p>Your signing key is stored on your pod, locked under your account password,
118
- so nobody who can read your pod — your pod's host included — can post as you.
119
- This browser has not opened it yet.</p>
119
+ <p>Your signing key is stored on your pod, locked under the password you gave
120
+ when you signed up, so nobody who can read your pod — your pod's host included
121
+ — can post as you. This browser has not opened it yet.</p>
120
122
  <p class="row">
121
123
  <input type="password" id="unlock-password" autocomplete="current-password"
122
- placeholder="Your account password" aria-label="Your account password">
124
+ placeholder="Your password" aria-label="Your password">
123
125
  <button type="button" class="primary" id="unlock-go">unlock</button>
124
126
  </p>
125
127
  <p class="err" id="unlock-error" role="alert"></p>
126
128
  <p class="hint">This browser only — it is not sent anywhere.</p>
129
+ <h2>Changed your pod password, or lost the old one?</h2>
130
+ <p>Make a new signing key, locked under the password you use now. Your account,
131
+ address, posts and followers stay as they are; other servers learn the new key
132
+ the next time they hear from you.</p>
133
+ <p><button type="button" id="unlock-newkey">make a new signing key</button></p>
134
+ <p id="unlock-newkey-confirm" hidden>This replaces the key on your pod. Type the
135
+ password you use now in the field above, then
136
+ <button type="button" id="unlock-newkey-go">make the new key</button></p>
127
137
  </section>
128
138
 
129
139