fedipod 1.40.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +5 -0
  2. package/architecture.md +1 -3
  3. package/architecture.svg +93 -85
  4. package/gateway.md +7 -2
  5. package/lib/client/c2s.mjs +16 -5
  6. package/lib/client/masto/oauth.mjs +1 -1
  7. package/lib/connections/vault.mjs +1 -1
  8. package/lib/core/agent.mjs +720 -0
  9. package/lib/core/gateway-move.mjs +58 -0
  10. package/lib/core/keys.mjs +1 -1
  11. package/lib/core/lease.mjs +4 -3
  12. package/lib/core/publisher/index.mjs +4 -6
  13. package/lib/core/publisher/notes.mjs +73 -4
  14. package/lib/core/remote.mjs +54 -0
  15. package/lib/core/shapes/shapes-text.mjs +11 -3
  16. package/lib/core/wire.mjs +4 -1
  17. package/lib/device/admin/index.mjs +4 -11
  18. package/lib/device/admin/routes/setup.mjs +60 -5
  19. package/lib/device/admin/server.mjs +18 -5
  20. package/lib/device/cli/commands/account.mjs +1 -1
  21. package/lib/device/cli/commands/run.mjs +2 -2
  22. package/lib/device/cli/commands/service.mjs +1 -1
  23. package/lib/device/cli/commands/setup.mjs +1 -1
  24. package/lib/device/cli/commands/state.mjs +1 -1
  25. package/lib/device/cli/context.mjs +1 -1
  26. package/lib/{gateway → device}/directory.mjs +1 -1
  27. package/lib/device/gateway-move.mjs +3 -58
  28. package/lib/device/home.mjs +3 -50
  29. package/lib/{client → device}/localapi.mjs +2 -2
  30. package/lib/device/remote.mjs +3 -54
  31. package/lib/gateway/copy.mjs +7 -1
  32. package/lib/gateway/front-core.mjs +6 -16
  33. package/lib/gateway/front-pages.mjs +31 -0
  34. package/lib/gateway/keeper-session.mjs +10 -1
  35. package/lib/gateway/masto-gateway.mjs +61 -12
  36. package/lib/gateway/outbox-door.mjs +31 -0
  37. package/lib/gateway/state-api.mjs +39 -7
  38. package/lib/server/embed.mjs +4 -4
  39. package/lib/shared/files.mjs +49 -0
  40. package/lib/shared/ua.mjs +3 -0
  41. package/lib/surface/index.mjs +7 -0
  42. package/lib/{device/admin → surface}/origins.mjs +1 -1
  43. package/lib/{device/admin → surface}/routes/gateway.mjs +7 -11
  44. package/lib/{device/admin → surface}/routes/lifecycle.mjs +3 -3
  45. package/lib/{device/admin → surface}/routes/owner.mjs +4 -60
  46. package/lib/{device/admin → surface}/routes/social.mjs +1 -1
  47. package/lib/{device/admin → surface}/static.mjs +6 -1
  48. package/lib/{device/admin → surface}/surface.mjs +19 -16
  49. package/package.json +23 -5
  50. package/run-agent.mjs +18 -714
  51. package/scripts/build-app.mjs +4 -0
  52. package/scripts/check-netlify-bundles.mjs +84 -0
  53. package/scripts/check-pod-calls.mjs +42 -0
  54. package/scripts/vendor-sanitize.mjs +27 -0
  55. package/vendor/idp-grant.cjs +11 -5
  56. package/vendor/sanitize-html.cjs +11456 -0
  57. package/web/app/agent.mjs +4 -2
  58. package/web/app/copy-mode.mjs +27 -3
  59. package/web/app/dist/boot.js.map +1 -1
  60. package/web/app/dist/sw.js +110 -24
  61. package/web/app/dist/sw.js.map +4 -4
  62. package/web/app-signin/app-signin.mjs +2 -2
  63. package/web/app-signin/index.html +13 -5
  64. package/browser.svg +0 -72
  65. /package/lib/{device/admin → surface}/routes/connections.mjs +0 -0
package/README.md CHANGED
@@ -82,6 +82,11 @@ If your account turns a post down when it runs, you get a direct message from
82
82
  yourself saying what was refused and why. Signed in, your outbox shows you
83
83
  everything you've done, likes and follows included; everyone else sees only
84
84
  your public posts.
85
+ While fedipod.net keeps your account running, a post from such an app goes out
86
+ in a moment, whether or not FediPod is open.
87
+ A comment you post from dokieli names nobody to send it to, so it goes to
88
+ nobody. It is kept on your pod at its own address, where the page you
89
+ commented on points to it. Your own timeline shows it as a private post.
85
90
 
86
91
  **The manage page.** `manage`, in the bar's account group, opens it: your profile,
87
92
  aliases, the gateway, key rotation, recovering posts, parking, moving to
package/architecture.md CHANGED
@@ -28,9 +28,7 @@ work from, and which the gateway writes to the pod every fifteen minutes. See
28
28
  Community Solid Server, so anyone with a pod on that server can opt in to a
29
29
  Fediverse account fed by the server itself.
30
30
 
31
- ![The browser version](https://raw.githubusercontent.com/jeff-zucker/FediPod/main/browser.svg)
32
-
33
- ![The DeviceAgent, with a gateway](https://raw.githubusercontent.com/jeff-zucker/FediPod/main/architecture.svg)
31
+ ![FediPod: the BrowserAgent and the Gateway, with your pod, Mastodon apps and C2S apps](https://raw.githubusercontent.com/jeff-zucker/FediPod/main/architecture.svg)
34
32
 
35
33
  ## Protocol conformance
36
34
 
package/architecture.svg CHANGED
@@ -1,89 +1,97 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1290 800" width="1290" height="800" font-family="system-ui, sans-serif">
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1300 680" font-family="Arial, Helvetica, sans-serif">
2
2
  <defs>
3
- <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
4
- <path d="M0,0 L10,5 L0,10 z" fill="#555"/>
3
+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
4
+ <path d="M0,0 L10,5 L0,10 z" fill="#333"/>
5
5
  </marker>
6
6
  </defs>
7
- <style>
8
- .t { font-size: 18px; font-weight: 600; fill: #1a1a19; }
9
- .d { font-size: 16px; fill: #333; }
10
- .sub { font-size: 15px; fill: #666; }
11
- .lbl { font-size: 15px; fill: #555; }
12
- .cc { font-size: 11px; font-weight: 400; font-style: italic; fill: #555; }
13
- .arrow { stroke: #555; stroke-width: 2; fill: none; marker-end: url(#arr); }
14
- .arrow2 { stroke: #555; stroke-width: 2; fill: none; marker-end: url(#arr); marker-start: url(#arr); }
15
- </style>
16
-
17
- <rect width="1290" height="800" fill="#ffffff"/>
18
-
19
- <!-- The open social web -->
20
- <rect x="210" y="30" width="870" height="92" rx="12" fill="#eef1f8" stroke="#7a88b8"/>
21
- <text class="t" x="645" y="64" text-anchor="middle">The open social web</text>
22
- <text class="d" x="645" y="94" text-anchor="middle">other servers: deliver mail to us, read what we publish</text>
23
-
24
- <!-- FediPod gateway -->
25
- <rect x="210" y="180" width="870" height="134" rx="12" fill="#f2ece0" stroke="#b09a5e" stroke-dasharray="7 5"/>
26
- <text class="t" x="645" y="214" text-anchor="middle">FediPod gateway</text>
27
- <text class="d" x="645" y="240" text-anchor="middle">(optional — Netlify or any always on box)</text>
28
- <text class="d" x="645" y="266" text-anchor="middle">checks each delivery's signature at the door;</text>
29
- <text class="d" x="645" y="288" text-anchor="middle">drops spam and forgeries before the pod</text>
30
-
31
- <!-- Pod -->
32
- <rect x="210" y="375" width="360" height="197" rx="12" fill="#e7f2e7" stroke="#6a9a6a"/>
33
- <text class="t" x="390" y="407" text-anchor="middle">Tamara's pod</text>
34
- <text class="sub" x="390" y="431" text-anchor="middle">any https, sub-domained, always on pod</text>
35
- <text class="d" x="390" y="459" text-anchor="middle">stores &amp; serves her public</text>
36
- <text class="d" x="390" y="481" text-anchor="middle">record in S2S AS2 format —</text>
37
- <text class="d" x="390" y="503" text-anchor="middle">actor, posts, collections;</text>
38
- <text class="d" x="390" y="525" text-anchor="middle">her inbox buffers mail</text>
39
- <text class="d" x="390" y="547" text-anchor="middle">even when her agent is off</text>
40
-
41
- <!-- Agent -->
42
- <rect x="720" y="375" width="360" height="197" rx="12" fill="#fdeeee" stroke="#b87a7a"/>
43
- <text class="t" x="900" y="407" text-anchor="middle">Tamara's FediPod Agent</text>
44
- <text class="sub" x="900" y="431" text-anchor="middle">on local device</text>
45
- <text class="d" x="900" y="459" text-anchor="middle">does the thinking:</text>
46
- <text class="d" x="900" y="481" text-anchor="middle">reads the buffered mail,</text>
47
- <text class="d" x="900" y="503" text-anchor="middle">accepts follows, builds her</text>
48
- <text class="d" x="900" y="525" text-anchor="middle">timeline, signs &amp; delivers</text>
49
- <text class="d" x="900" y="547" text-anchor="middle">everything she says</text>
50
-
51
- <!-- Client -->
52
- <rect x="720" y="628" width="360" height="136" rx="12" fill="#f0e9f5" stroke="#9a7ab0"/>
53
- <text class="t" x="900" y="662" text-anchor="middle">Browser or Native App</text>
54
- <text class="sub" x="900" y="686" text-anchor="middle">(Phanpy, Whalebird, …)</text>
55
- <text class="d" x="900" y="714" text-anchor="middle">where she reads and posts;</text>
56
- <text class="d" x="900" y="736" text-anchor="middle">talks only to her agent</text>
57
-
58
- <!-- open social web -> gateway -->
59
- <path class="arrow" d="M 645 124 L 645 176"/>
60
- <text class="lbl" x="657" y="155">deliveries in</text>
61
-
62
- <!-- gateway -> pod -->
63
- <path class="arrow" d="M 390 316 L 390 371"/>
64
- <text class="lbl" x="402" y="336">verified mail</text>
65
- <text class="lbl" x="402" y="357">+ receipt</text>
66
-
67
- <!-- pod <-> agent -->
68
- <path class="arrow2" d="M 572 473 L 718 473"/>
69
- <text class="lbl" x="645" y="455" text-anchor="middle">drains mail,</text>
70
- <text class="lbl" x="645" y="497" text-anchor="middle">writes the record</text>
71
-
72
- <!-- agent <-> client -->
73
- <path class="arrow2" d="M 900 574 L 900 626"/>
74
- <text class="lbl" x="912" y="605">read / post</text>
75
-
76
- <!-- pod -> open social web: read directly, never through the gateway -->
77
- <path class="arrow" d="M 208 440 L 70 440 C 45 440, 38 400, 38 330 C 38 210, 88 100, 206 100"/>
78
- <text class="lbl" x="188" y="412" text-anchor="end">reads her record</text>
79
-
80
- <!-- agent -> open social web: delivers her own posts, never through the gateway -->
81
- <path class="arrow" d="M 1082 440 L 1220 440 C 1245 440, 1252 400, 1252 330 C 1252 210, 1202 78, 1084 78"/>
82
- <text class="lbl" x="1102" y="412">signed deliveries out</text>
83
-
84
- <text class="d" x="45" y="716">The gateway stops unwanted mail from reaching the pod.</text>
85
- <text class="d" x="45" y="740">The agent prevents mail buildup and does the mail processing.</text>
86
- <text class="d" x="45" y="764">This leaves the pod with the minimal possible load.</text>
87
-
88
- <text class="cc" x="1245" y="764" text-anchor="end">(cc) 4.0 By, Jeff Zucker, 2026</text>
7
+ <rect x="0" y="0" width="1300" height="680" fill="#ffffff"/>
8
+
9
+ <!-- boxes (draw.io palette, as browser.svg) -->
10
+ <g stroke-width="1.5">
11
+ <rect x="450" y="24" width="280" height="84" rx="6" fill="#dae8fc" stroke="#6c8ebf"/>
12
+ <rect x="260" y="175" width="650" height="215" rx="10" fill="#f5f5f5" stroke="#666666" stroke-dasharray="8 6"/>
13
+ <rect x="285" y="230" width="230" height="130" rx="6" fill="#d5e8d4" stroke="#82b366"/>
14
+ <rect x="665" y="230" width="230" height="130" rx="6" fill="#e1d5e7" stroke="#9673a6"/>
15
+ <rect x="40" y="500" width="300" height="110" rx="6" fill="#f8cecc" stroke="#b85450"/>
16
+ <rect x="780" y="500" width="300" height="110" rx="6" fill="#fff2cc" stroke="#d6b656"/>
17
+ <rect x="1040" y="250" width="220" height="95" rx="6" fill="#ffe6cc" stroke="#d79b00"/>
18
+ </g>
19
+
20
+ <!-- box labels -->
21
+ <g fill="#111" text-anchor="middle">
22
+ <text x="590" y="60" font-size="20" font-weight="bold">Open Social Web</text>
23
+ <text x="590" y="88" font-size="16">Mastodon · Pixelfed · Bluesky …</text>
24
+
25
+ <text x="280" y="206" font-size="22" font-weight="bold" text-anchor="start">FediPod</text>
26
+
27
+ <text x="400" y="266" font-size="20" font-weight="bold">BrowserAgent</text>
28
+ <text x="400" y="294" font-size="16">FediPod in any browser,</text>
29
+ <text x="400" y="318" font-size="16">with Sengi or Phanpy</text>
30
+
31
+ <text x="780" y="266" font-size="20" font-weight="bold">Gateway</text>
32
+ <text x="780" y="294" font-size="16">at fedipod.net, keeps</text>
33
+ <text x="780" y="318" font-size="16">your account running</text>
34
+ <text x="780" y="342" font-size="16">from a working copy</text>
35
+
36
+ <text x="190" y="536" font-size="20" font-weight="bold">Your Pod</text>
37
+ <text x="190" y="564" font-size="16">your key, your posts</text>
38
+ <text x="190" y="588" font-size="16">and your account's data</text>
39
+
40
+ <text x="930" y="536" font-size="20" font-weight="bold">Mastodon Apps</text>
41
+ <text x="930" y="564" font-size="16">Elk · Ivory · Tusky · Fedilab</text>
42
+ <text x="930" y="588" font-size="16">on your phone or desktop</text>
43
+
44
+ <text x="1150" y="289" font-size="20" font-weight="bold">C2S Apps</text>
45
+ <text x="1150" y="317" font-size="16">box, Dokieli</text>
46
+ </g>
47
+
48
+ <!-- arrows -->
49
+ <g stroke="#333" stroke-width="1.6" fill="none">
50
+ <!-- Open Social Web <-> BrowserAgent (ATProto, Bluesky) -->
51
+ <line x1="520" y1="108" x2="410" y2="230" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
52
+ <!-- Open Social Web <-> Gateway (ActivityPub mail) -->
53
+ <line x1="660" y1="108" x2="770" y2="230" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
54
+ <!-- BrowserAgent <-> Gateway -->
55
+ <line x1="515" y1="298" x2="665" y2="298" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
56
+ <!-- Your Pod -> Open Social Web (public posts and profile) -->
57
+ <path d="M100,500 C0,400 0,110 450,70" marker-end="url(#arrow)"/>
58
+ <!-- BrowserAgent <-> Your Pod (Solid) -->
59
+ <line x1="350" y1="360" x2="230" y2="500" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
60
+ <!-- Gateway <-> Your Pod (stores the account's data) -->
61
+ <line x1="720" y1="360" x2="340" y2="560" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
62
+ <!-- Mastodon Apps <-> Gateway -->
63
+ <line x1="900" y1="500" x2="840" y2="360" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
64
+ <!-- C2S Apps <-> Gateway -->
65
+ <line x1="1040" y1="298" x2="895" y2="298" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
66
+ </g>
67
+
68
+ <!-- arrow labels -->
69
+ <g fill="#333" font-size="16">
70
+ <text x="470" y="140" text-anchor="end" font-weight="bold" fill="#111">ATProto Protocol</text>
71
+ <text x="470" y="160" text-anchor="end">for Bluesky</text>
72
+
73
+ <text x="712" y="140" text-anchor="start" font-weight="bold" fill="#111">S2S Protocol</text>
74
+ <text x="712" y="160" text-anchor="start">mail in, posts out</text>
75
+
76
+ <text x="590" y="286" text-anchor="middle">serves the app</text>
77
+ <text x="590" y="320" text-anchor="middle">posts out</text>
78
+
79
+ <text x="968" y="286" text-anchor="middle" font-weight="bold" fill="#111">C2S Protocol</text>
80
+
81
+ <text x="85" y="250" text-anchor="start">other servers read</text>
82
+ <text x="85" y="270" text-anchor="start">your public posts</text>
83
+ <text x="85" y="290" text-anchor="start">and profile</text>
84
+
85
+ <text x="272" y="420" text-anchor="end" font-weight="bold" fill="#111">Solid Protocol</text>
86
+ <text x="272" y="440" text-anchor="end">reads &amp; writes your pod</text>
87
+
88
+ <text x="520" y="490" text-anchor="start">stores the working copy</text>
89
+ <text x="520" y="510" text-anchor="start">on your pod</text>
90
+
91
+ <text x="894" y="415" text-anchor="start" font-weight="bold" fill="#111">Mastodon client API</text>
92
+ <text x="894" y="435" text-anchor="start">sign in at fedipod.net;</text>
93
+ <text x="894" y="455" text-anchor="start">notifications to your phone</text>
94
+ </g>
95
+
96
+ <text x="1260" y="660" text-anchor="end" font-size="16" fill="#555">© 2026 Jeff Zucker · CC BY-SA 4.0</text>
89
97
  </svg>
package/gateway.md CHANGED
@@ -117,6 +117,11 @@ there, signed in at your pod. The door checks that the token is yours, puts the
117
117
  post in your inbox marked as yours, and answers with the address the post will
118
118
  have. Your agent publishes it and sends it to your followers the next time it
119
119
  runs: for a browser account, the next time you open the site.
120
+ While the gateway keeps a browser account running, it starts the account's
121
+ keeper as the post arrives, so the post goes out in a moment.
122
+ A comment from dokieli names nobody to send it to, so it is sent to nobody. It
123
+ is kept at its own address for the page it is on to point at, and only your
124
+ own outbox lists it.
120
125
 
121
126
  A post with no audience of its own goes out as a public post. A post that is
122
127
  not a note, an annotation say, is kept as the app sent it, under your name.
@@ -193,8 +198,8 @@ you connected elsewhere are never in the copy; they stay on your pod only.
193
198
  While the gateway keeps your account running, any Mastodon app can use it:
194
199
  elk.zone, Ivory, Tusky, Phanpy and the rest. In the app, give `fedipod.net` as
195
200
  your server. The app sends you to a fedipod.net page that asks for your
196
- address here and signs you in at your own pod; no password is typed on
197
- fedipod.net. The app then reads your timeline and notifications from your
201
+ Fediverse address or your WebID and signs you in at your own pod; no password
202
+ is typed on fedipod.net. The app then reads your timeline and notifications from your
198
203
  account's copy at the gateway, and posts, boosts, likes and follows as you.
199
204
 
200
205
  An app checks for new posts every minute or so while it is open; there are no
@@ -48,6 +48,10 @@ const ACTIVITY_TYPES = new Set([
48
48
 
49
49
  const arr = (v) => (v === undefined || v === null ? [] : Array.isArray(v) ? v : [v]);
50
50
  const idOf = (v) => (typeof v === 'string' ? v : v?.id || null);
51
+ // An audience address. The public collection has three valid spellings
52
+ // (ActivityPub §5.6); reading a post as JSON-LD can yield the short one.
53
+ const PUBLIC_SPELLINGS = new Set(['Public', 'as:Public']);
54
+ const audienceOf = (v) => { const id = idOf(v); return PUBLIC_SPELLINGS.has(id) ? wire.PUBLIC : id; };
51
55
 
52
56
  function readBody(req) {
53
57
  return new Promise((resolve, reject) => {
@@ -63,6 +67,9 @@ function readBody(req) {
63
67
  });
64
68
  }
65
69
 
70
+ const isAnnotation = (o) => arr(o?.type).some((t) => t === 'Annotation' || t === 'oa:Annotation'
71
+ || t === 'http://www.w3.org/ns/oa#Annotation');
72
+
66
73
  export class C2S {
67
74
  constructor({ agent, log = console.log, auth, scheme = null, mount = '' }) {
68
75
  this.agent = agent;
@@ -345,8 +352,8 @@ export class C2S {
345
352
  // every client means by a post with no audience chosen, and what a client
346
353
  // that never addresses (dokieli's annotations) needs.
347
354
  visibilityOf(activity, object) {
348
- const to = arr(activity.to ?? object?.to).map(idOf);
349
- const cc = arr(activity.cc ?? object?.cc).map(idOf);
355
+ const to = arr(activity.to ?? object?.to).map(audienceOf);
356
+ const cc = arr(activity.cc ?? object?.cc).map(audienceOf);
350
357
  // Blind copies alone are still an audience: a post for them and nobody
351
358
  // else is direct, never public.
352
359
  const blind = arr(activity.bto ?? object?.bto).length + arr(activity.bcc ?? object?.bcc).length;
@@ -386,7 +393,7 @@ export class C2S {
386
393
  // in to/cc are listed and delivered to; the ones in bto/bcc are delivered
387
394
  // to and never listed (§6).
388
395
  addressedActors(activity, object) {
389
- const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(idOf)))]
396
+ const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(audienceOf)))]
390
397
  .filter((a) => typeof a === 'string' && /^https?:\/\//u.test(a)
391
398
  && a !== wire.PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
392
399
  // `audience` is who the post is for, a group most often: delivered to and
@@ -417,12 +424,16 @@ export class C2S {
417
424
  const visibility = this.visibilityOf(activity, object);
418
425
  const { also, deliverTo } = this.addressedActors(activity, object);
419
426
  // Not a Note and not a poll: stored as sent, under this actor, and
420
- // the Create around it delivered — a Web Annotation, for one.
427
+ // the Create around it delivered — a Web Annotation, for one. An
428
+ // annotation addressed to nobody, as dokieli sends one, is delivered
429
+ // to nobody: kept at its address for the page it is on to point at.
421
430
  if (makes !== 'Note' && makes !== 'Question') {
422
431
  const asSent = raw && typeof raw === 'object' && !Array.isArray(raw)
423
432
  ? (ACTIVITY_TYPES.has(raw.type) ? (raw.object && typeof raw.object === 'object' ? raw.object : null) : raw)
424
433
  : null;
425
- const made = await agent.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo });
434
+ const unaddressed = isAnnotation(asSent || object)
435
+ && ['to', 'cc', 'bto', 'bcc', 'audience'].every((f) => !arr(activity[f] ?? object[f]).length);
436
+ const made = await agent.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo, unaddressed });
426
437
  return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
427
438
  }
428
439
  // `source.content` is the client's plain text when it sends one; bare
@@ -53,7 +53,7 @@ function sendPodSigninPage(res, client, webId, mount = '') {
53
53
  try { where = client.redirect ? new URL(client.redirect).host : ''; } catch { /* oob or blank */ }
54
54
  const who = client.name ? escapeHtml(client.name) : (where ? escapeHtml(where) : 'A client');
55
55
  asking = `<p><strong>${who}</strong> is asking to use your account`
56
- + `${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ''}.</p>`;
56
+ + `${where ? `, and you will be sent back to <code>${escapeHtml(where)}</code>` : ''}.</p>`;
57
57
  }
58
58
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' });
59
59
  res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
@@ -18,7 +18,7 @@
18
18
  // server use it before they answer.
19
19
  import fs from 'node:fs';
20
20
  import path from 'node:path';
21
- import { writeJsonAtomic } from '../device/home.mjs';
21
+ import { writeJsonAtomic } from '../shared/files.mjs';
22
22
 
23
23
  /** What every connection document in pod state is named from. */
24
24
  export const CONNECTION_PREFIX = 'conn-';