fedipod-server 0.10.0 → 0.12.1

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 (416) hide show
  1. package/README.md +15 -6
  2. package/dist/claims.js +1 -1
  3. package/dist/handler.d.ts +7 -1
  4. package/dist/handler.js +58 -12
  5. package/dist/handler.jsonld +5 -1
  6. package/dist/store-pod.js +18 -4
  7. package/lib/{c2s.mjs → client/c2s.mjs} +8 -3
  8. package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
  9. package/lib/client/masto/accounts.mjs +264 -0
  10. package/lib/client/masto/body.mjs +69 -0
  11. package/lib/client/masto/index.mjs +183 -0
  12. package/lib/client/masto/instance.mjs +104 -0
  13. package/lib/client/masto/media.mjs +133 -0
  14. package/lib/client/masto/oauth.mjs +599 -0
  15. package/lib/client/masto/render.mjs +459 -0
  16. package/lib/client/masto/statuses.mjs +331 -0
  17. package/lib/client/masto/timelines.mjs +316 -0
  18. package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
  19. package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
  20. package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
  21. package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
  22. package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
  23. package/lib/{import.mjs → connections/import.mjs} +2 -2
  24. package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +21 -4
  25. package/lib/connections/vault.mjs +114 -0
  26. package/lib/core/as2.mjs +124 -0
  27. package/lib/core/contexts/activitystreams.json +379 -0
  28. package/lib/core/contexts/did-v1.json +57 -0
  29. package/lib/core/contexts/fep-5711.json +36 -0
  30. package/lib/core/contexts/gotosocial.json +86 -0
  31. package/lib/core/contexts/identity-v1.json +152 -0
  32. package/lib/core/contexts/index.mjs +45 -0
  33. package/lib/core/contexts/join-lemmy.json +33 -0
  34. package/lib/core/contexts/joinmastodon.json +28 -0
  35. package/lib/core/contexts/map.json +16 -0
  36. package/lib/core/contexts/miscellany.json +19 -0
  37. package/lib/core/contexts/schemaorg.json +8845 -0
  38. package/lib/core/contexts/security-data-integrity-v1.json +78 -0
  39. package/lib/core/contexts/security-data-integrity-v2.json +81 -0
  40. package/lib/core/contexts/security-multikey-v1.json +35 -0
  41. package/lib/core/contexts/security-v1.json +74 -0
  42. package/lib/core/contexts/webfinger.json +10 -0
  43. package/lib/{deliver.mjs → core/deliver.mjs} +8 -2
  44. package/lib/core/intake/activities.mjs +437 -0
  45. package/lib/core/intake/activity.mjs +240 -0
  46. package/lib/core/intake/channel.mjs +144 -0
  47. package/lib/core/intake/group.mjs +222 -0
  48. package/lib/core/intake/index.mjs +629 -0
  49. package/lib/core/intake/notes.mjs +288 -0
  50. package/lib/core/intake/verify.mjs +141 -0
  51. package/lib/{keys.mjs → core/keys.mjs} +1 -1
  52. package/lib/core/publisher/collections.mjs +229 -0
  53. package/lib/core/publisher/index.mjs +421 -0
  54. package/lib/core/publisher/notes.mjs +188 -0
  55. package/lib/core/publisher/questions.mjs +233 -0
  56. package/lib/core/publisher/restore.mjs +196 -0
  57. package/lib/core/shapes/activitystreams.ttl +129 -0
  58. package/lib/core/shapes/index.mjs +107 -0
  59. package/lib/core/shapes/shapes-text.mjs +13 -0
  60. package/lib/{social.mjs → core/social.mjs} +19 -10
  61. package/lib/{storage.mjs → core/storage.mjs} +2 -2
  62. package/lib/{store.mjs → core/store.mjs} +12 -1
  63. package/lib/{wire.mjs → core/wire.mjs} +48 -67
  64. package/lib/device/admin/index.mjs +13 -0
  65. package/lib/device/admin/origins.mjs +35 -0
  66. package/lib/device/admin/routes/connections.mjs +144 -0
  67. package/lib/device/admin/routes/gateway.mjs +199 -0
  68. package/lib/device/admin/routes/lifecycle.mjs +191 -0
  69. package/lib/device/admin/routes/owner.mjs +322 -0
  70. package/lib/device/admin/routes/setup.mjs +393 -0
  71. package/lib/device/admin/routes/social.mjs +188 -0
  72. package/lib/device/admin/server.mjs +95 -0
  73. package/lib/device/admin/static.mjs +244 -0
  74. package/lib/device/admin/surface.mjs +274 -0
  75. package/lib/device/cli/commands/account.mjs +586 -0
  76. package/lib/device/cli/commands/run.mjs +278 -0
  77. package/lib/device/cli/commands/service.mjs +221 -0
  78. package/lib/device/cli/commands/setup.mjs +410 -0
  79. package/lib/device/cli/commands/state.mjs +559 -0
  80. package/lib/device/cli/context.mjs +288 -0
  81. package/lib/{export-collections.mjs → device/export-collections.mjs} +1 -1
  82. package/lib/{migrate.mjs → device/migrate.mjs} +5 -19
  83. package/lib/device/remote.mjs +54 -0
  84. package/lib/{setup.mjs → device/setup.mjs} +3 -3
  85. package/lib/{update.mjs → device/update.mjs} +1 -1
  86. package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
  87. package/lib/{front-core.mjs → gateway/front-core.mjs} +272 -49
  88. package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +3 -7
  89. package/lib/{httpsig.mjs → gateway/httpsig.mjs} +34 -3
  90. package/lib/pod/README.md +61 -0
  91. package/lib/pod/actor.mjs +96 -0
  92. package/lib/pod/collection.mjs +77 -0
  93. package/lib/pod/containers.mjs +136 -0
  94. package/lib/pod/discovery.mjs +46 -0
  95. package/lib/pod/featured.mjs +18 -0
  96. package/lib/pod/followers.mjs +24 -0
  97. package/lib/pod/following.mjs +10 -0
  98. package/lib/pod/http.mjs +74 -0
  99. package/lib/pod/inbox.mjs +144 -0
  100. package/lib/{links.mjs → pod/links.mjs} +1 -1
  101. package/lib/pod/media.mjs +27 -0
  102. package/lib/pod/notes.mjs +80 -0
  103. package/lib/pod/notifications.mjs +89 -0
  104. package/lib/pod/outbox.mjs +18 -0
  105. package/lib/pod/package.json +19 -0
  106. package/lib/pod/policy.mjs +58 -0
  107. package/lib/pod/private.mjs +19 -0
  108. package/lib/pod/root.mjs +63 -0
  109. package/lib/pod/state.mjs +39 -0
  110. package/lib/{remote.mjs → pod/transport.mjs} +217 -159
  111. package/lib/pod/urls.mjs +71 -0
  112. package/lib/{embed.mjs → server/embed.mjs} +149 -22
  113. package/lib/shared/links.mjs +9 -0
  114. package/lib/{ua.mjs → shared/ua.mjs} +1 -1
  115. package/package.json +1 -1
  116. package/run-agent.mjs +47 -64
  117. package/web/admin/actors.js +145 -0
  118. package/web/admin/bar.css +9 -13
  119. package/web/admin/bar.js +2 -2
  120. package/web/admin/client/client.js +1 -1
  121. package/web/admin/client/index.html +5 -6
  122. package/web/admin/common.js +23 -0
  123. package/web/admin/connections.js +112 -0
  124. package/web/admin/gateway.js +111 -0
  125. package/web/admin/group.js +258 -0
  126. package/web/admin/index.html +60 -66
  127. package/web/admin/record.js +378 -0
  128. package/web/admin/setup/index.html +3 -1
  129. package/web/admin/setup/setup.js +2 -13
  130. package/web/admin/tokens.css +56 -0
  131. package/web/admin/upkeep.js +170 -0
  132. package/web/app/README.md +77 -0
  133. package/web/app/admin-facade.mjs +549 -0
  134. package/web/app/agent.mjs +490 -0
  135. package/web/app/atproto-browser.mjs +86 -0
  136. package/web/app/boot.mjs +340 -0
  137. package/web/app/deliver-relay.mjs +72 -0
  138. package/web/app/dist/boot.js +34067 -0
  139. package/web/app/dist/boot.js.map +7 -0
  140. package/web/app/dist/sw.js +70014 -0
  141. package/web/app/dist/sw.js.map +7 -0
  142. package/web/app/fediacct-browser.mjs +146 -0
  143. package/web/app/idb-kv.mjs +35 -0
  144. package/web/app/index.html +203 -0
  145. package/web/app/index.html~ +152 -0
  146. package/web/app/keys-browser.mjs +46 -0
  147. package/web/app/keystore.mjs +99 -0
  148. package/web/app/oidc-session.mjs +189 -0
  149. package/web/app/pod-auth.mjs +248 -0
  150. package/web/app/pod-remote.mjs +84 -0
  151. package/web/app/shims/_globals.mjs +2 -0
  152. package/web/app/shims/fedify-sig.mjs +77 -0
  153. package/web/app/shims/node-crypto.mjs +113 -0
  154. package/web/app/shims/node-fs.mjs +17 -0
  155. package/web/app/shims/node-path.mjs +19 -0
  156. package/web/app/shims/node-url.mjs +7 -0
  157. package/web/app/shims/prelude.js +58 -0
  158. package/web/app/shims/safefetch.mjs +58 -0
  159. package/web/app/shims/shapes-text.mjs +8 -0
  160. package/web/app/shims/web-push.mjs +14 -0
  161. package/web/app/signup.mjs +248 -0
  162. package/web/app/site/_headers +9 -0
  163. package/web/app/site/_redirects +21 -0
  164. package/web/app/site/admin/actors.js +145 -0
  165. package/web/app/site/admin/bar.css +60 -0
  166. package/web/app/site/admin/bar.js +61 -0
  167. package/web/app/site/admin/client/client.js +66 -0
  168. package/web/app/site/admin/client/index.html +43 -0
  169. package/web/app/site/admin/common.js +23 -0
  170. package/web/app/site/admin/connections.js +112 -0
  171. package/web/app/site/admin/gateway.js +111 -0
  172. package/web/app/site/admin/group.js +258 -0
  173. package/web/app/site/admin/index.html +562 -0
  174. package/web/app/site/admin/record.js +378 -0
  175. package/web/app/site/admin/setup/index.html +192 -0
  176. package/web/app/site/admin/setup/setup.js +297 -0
  177. package/web/app/site/admin/tokens.css +56 -0
  178. package/web/app/site/admin/upkeep.js +170 -0
  179. package/web/app/site/admin/window.css +55 -0
  180. package/web/app/site/admin/window.js +136 -0
  181. package/web/app/site/app/404.html +40 -0
  182. package/web/app/site/app/_headers +2 -0
  183. package/web/app/site/app/apple-touch-icon.png +0 -0
  184. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
  185. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
  186. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  187. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  188. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  189. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
  190. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
  191. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  192. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  193. package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
  194. package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
  195. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
  196. package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
  197. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
  198. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
  199. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
  200. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
  201. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
  202. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  203. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  204. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  205. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  206. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  207. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
  208. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  209. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  210. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  211. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  212. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  213. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  214. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  215. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  216. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  217. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  218. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
  219. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
  220. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  221. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
  222. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  223. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  224. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  225. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  226. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
  227. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  228. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
  229. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  230. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  231. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  232. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  233. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  234. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  235. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  236. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
  237. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
  238. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
  239. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  240. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  241. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  242. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  243. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
  244. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
  245. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  246. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  247. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
  248. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  249. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  250. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  251. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  252. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
  253. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  254. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  255. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
  256. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
  257. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
  258. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  259. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  260. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  261. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
  262. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  263. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
  264. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  265. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
  266. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  267. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  268. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  269. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
  270. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
  271. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  272. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
  273. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
  274. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
  275. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
  276. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  277. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  278. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  279. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  280. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  281. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
  282. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
  283. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  284. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  285. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  286. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
  287. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
  288. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  289. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
  290. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
  291. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
  292. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
  293. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
  294. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
  295. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
  296. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
  297. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
  298. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  299. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  300. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  301. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  302. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  303. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
  304. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  305. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
  306. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  307. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  308. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
  309. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  310. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  311. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  312. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  313. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  314. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  315. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  316. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
  317. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  318. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
  319. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
  320. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  321. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  322. package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
  323. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
  324. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  325. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  326. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
  327. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  328. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  329. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
  330. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
  331. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
  332. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  333. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  334. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
  335. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
  336. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
  337. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  338. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
  339. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  340. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  341. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
  342. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  343. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
  344. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  345. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  346. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  347. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  348. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  349. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
  350. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
  351. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
  352. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  353. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
  354. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
  355. package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
  356. package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
  357. package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
  358. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
  359. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  360. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  361. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  362. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
  363. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  364. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
  365. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  366. package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
  367. package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
  368. package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
  369. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  370. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  371. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  372. package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
  373. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
  374. package/web/app/site/app/compose/index.html +20 -0
  375. package/web/app/site/app/favicon.ico +0 -0
  376. package/web/app/site/app/index.html +73 -0
  377. package/web/app/site/app/logo-192.png +0 -0
  378. package/web/app/site/app/logo-512.png +0 -0
  379. package/web/app/site/app/logo-badge-72.png +0 -0
  380. package/web/app/site/app/logo-maskable-512.png +0 -0
  381. package/web/app/site/app/logo-monochrome-512.png +0 -0
  382. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  383. package/web/app/site/app/manifest.webmanifest +1 -0
  384. package/web/app/site/app/og-image-2.jpg +0 -0
  385. package/web/app/site/app/og-image.png +0 -0
  386. package/web/app/site/app/robots.txt +2 -0
  387. package/web/app/site/app/version.json +1 -0
  388. package/web/app/site/boot.js +34067 -0
  389. package/web/app/site/index.html +203 -0
  390. package/web/app/site/sw.js +70014 -0
  391. package/web/app/sw-src.mjs +220 -0
  392. package/web/front/#new-account.html# +43 -0
  393. package/web/front/admin.html +1 -81
  394. package/web/front/admin.js +85 -0
  395. package/web/front/new-account.html +1 -9
  396. package/web/front/new-account.js +13 -0
  397. package/web/front/run.html +1 -92
  398. package/web/front/run.js +107 -0
  399. package/lib/admin.mjs +0 -1913
  400. package/lib/intake.mjs +0 -1764
  401. package/lib/mastoapi.mjs +0 -1947
  402. package/lib/podrdf.mjs +0 -132
  403. package/lib/publisher.mjs +0 -1263
  404. package/web/admin/admin.js +0 -1114
  405. /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
  406. /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
  407. /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
  408. /package/lib/{lease.mjs → core/lease.mjs} +0 -0
  409. /package/lib/{polls.mjs → core/polls.mjs} +0 -0
  410. /package/lib/{proof.mjs → core/proof.mjs} +0 -0
  411. /package/lib/{account.mjs → device/account.mjs} +0 -0
  412. /package/lib/{certs.mjs → device/certs.mjs} +0 -0
  413. /package/lib/{home.mjs → device/home.mjs} +0 -0
  414. /package/lib/{ports.mjs → device/ports.mjs} +0 -0
  415. /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
  416. /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
@@ -1,11 +1,20 @@
1
- // remote.mjs — authenticated I/O against the remote pod, via CSS
2
- // client-credentials + DPoP (vendor/idp-grant.cjs, extracted from
3
- // data-kitchen's "remember this IdP" machinery — plain Node, no Electron).
1
+ // transport.mjs — every authenticated request this library makes to a pod, and
2
+ // the manners that must hold for all of them.
3
+ //
4
+ // The session is injected and never built here: a Node agent hands in a DPoP
5
+ // client-credentials grant, a browser hands in a Solid-OIDC session, a pod
6
+ // server hands in a shim straight onto its own store. That is what lets one
7
+ // implementation serve all three, and it is why this file has no `import` of
8
+ // anything with a runtime.
9
+ //
10
+ // Two copies of this used to exist — lib/remote.mjs and web/app/pod-remote.mjs
11
+ // — hand-kept in sync, with the browser copy quietly behind on five separate
12
+ // hardenings. The subclasses below now add only what genuinely differs: how the
13
+ // session is obtained, and whether a throttled pod is waited out or refused.
4
14
 
5
- import { createRequire } from 'node:module';
6
- import path from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
15
  import * as $rdf from 'rdflib';
16
+ import { readCapped, retryAfterMs } from './http.mjs';
17
+ import { linkTargets, REL } from './links.mjs';
9
18
 
10
19
  const LDP = $rdf.Namespace('http://www.w3.org/ns/ldp#');
11
20
  const DC = $rdf.Namespace('http://purl.org/dc/terms/');
@@ -15,24 +24,8 @@ const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#');
15
24
  const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/');
16
25
  const AS = $rdf.Namespace('https://www.w3.org/ns/activitystreams#');
17
26
 
18
- const require = createRequire(import.meta.url);
19
- const vendorDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../vendor');
20
- const { mintCredential, discoverTokenEndpoint, createGrantSession, revokeCredentialViaAccount } =
21
- require(path.join(vendorDir, 'idp-grant.cjs'));
22
-
23
- export { mintCredential, discoverTokenEndpoint, revokeCredentialViaAccount };
24
-
25
- // A pod that answers 429 or 503 is asking to be left alone; Retry-After says
26
- // for how long. Every request to this pod goes through one cooldown, so a
27
- // server under strain gets silence rather than each timer discovering the
28
- // refusal separately. Requests during the window fail fast — no socket is
29
- // opened — and the callers' existing retry paths take it from there.
30
- // The parser is shared with the outbound path; see lib/safefetch.mjs.
31
- import { retryAfterMs, readCapped } from './safefetch.mjs';
32
- import { linkTargets, REL } from './links.mjs';
33
-
34
27
  // A pod whose access rules are ACP policies, not WAC authorizations. This
35
- // agent writes WAC; over an ACP resource that would be noise where the pod's
28
+ // library writes WAC; over an ACP resource that would be noise where the pod's
36
29
  // real rules used to be, so it stops instead.
37
30
  const ACP_NS = 'http://www.w3.org/ns/solid/acp#';
38
31
 
@@ -40,13 +33,19 @@ const ACP_NS = 'http://www.w3.org/ns/solid/acp#';
40
33
  // hands; reading it whole must still have a ceiling.
41
34
  const LISTING_MAX_BYTES = 10 * 1024 * 1024;
42
35
 
36
+ // No Retry-After header still means "not now". Sixty seconds is the answer
37
+ // when the server declined to give one.
38
+ const COOLDOWN_DEFAULT_MS = 60_000;
39
+
40
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
41
+
43
42
  // A pod's ability to BE a pod. Not data, and never ours to remove: delete one
44
43
  // of these and the pod does not degrade, it stops — and it cannot be repaired
45
44
  // by the tool that broke it, because that tool can no longer authenticate. A
46
45
  // pod was crippled exactly this way.
47
46
  //
48
- // Here rather than in any one script, because every DELETE this project sends
49
- // goes through RemotePod.delete — so this is the one place that cannot be
47
+ // Here rather than in any one caller, because every DELETE this library sends
48
+ // goes through PodTransport.fetch — so this is the one place that cannot be
50
49
  // routed around. A DENY-list, not an allow-prefix: the next script will have a
51
50
  // different prefix and the same list of things it must never touch.
52
51
  const PROTECTED = [
@@ -70,100 +69,160 @@ export function protectedFromDeletion(url) {
70
69
  if (p === '/' || p === '') throw new Error(`refusing to DELETE the pod root: ${url}`);
71
70
  for (const [re, why] of PROTECTED) {
72
71
  if (re.test(p)) {
73
- throw new Error(`refusing to DELETE ${url} — ${why}. Deny-list: lib/remote.mjs.`);
72
+ throw new Error(`refusing to DELETE ${url} — ${why}. Deny-list: lib/pod/transport.mjs.`);
74
73
  }
75
74
  }
76
75
  }
77
76
 
78
- export class RemotePod {
79
- constructor(credential, { log = () => {}, home = null, session = null } = {}) {
80
- // An injected session replaces the credential-backed one, and everything
81
- // below it — the cooldown, the deny-list, the url map — is unchanged. That
82
- // is how the agent runs inside a pod server: same class, no token to mint.
83
- this.session = session || createGrantSession(credential, home ? {
84
- backoffFile: path.join(home, 'backoff.json'),
85
- tokenFile: path.join(home, 'token.json'),
86
- } : {});
87
- this.webId = credential.webId;
77
+ export class PodTransport {
78
+ /**
79
+ * @param {object} session `{ fetch(url, init) }` — injected, never built here
80
+ * @param {object} opts
81
+ * @param {string} opts.webId who this transport acts as; the ACL owner
82
+ * @param {string} [opts.role] 'agent' | 'gateway' | 'signup' — what is talking
83
+ * @param {string} [opts.runtime] 'node' | 'browser' — where it is talking from
84
+ * @param {string} [opts.cooldownMode] 'refuse' fails fast for the window;
85
+ * 'wait' sleeps it out and continues
86
+ * @param {number} [opts.maxCooldownMs] ceiling on a Retry-After we will honour
87
+ */
88
+ constructor(session, {
89
+ webId, log = () => {}, role = 'agent', runtime = 'node',
90
+ cooldownMode = 'refuse', maxCooldownMs = 30 * 60_000,
91
+ } = {}) {
92
+ this.session = session;
93
+ this.webId = webId;
94
+ this.log = log;
95
+ this.role = role;
96
+ this.runtime = runtime;
97
+ this.cooldownMode = cooldownMode;
98
+ this.maxCooldownMs = maxCooldownMs;
88
99
  this.pausedUntil = 0;
89
100
  this.probeCount = 0;
90
- this.log = log;
91
101
  // Where each resource's access control lives, as the pod itself said. WAC
92
102
  // forbids working it out from the resource's own URL, so it is asked for
93
103
  // and remembered rather than assembled.
94
104
  this.aclUrls = new Map();
95
105
  this.aclFlavour = null; // null until the first write asks what this pod speaks
106
+ this._listCache = new Map();
96
107
  // A fronted identity advertises ids on a shared domain but writes to the
97
- // pod. run-agent installs the fronted→pod mapping here, so every request
98
- // built from an advertised id lands on the pod — one choke point, and
99
- // publisher/social/intake stay unaware. Unset → identity, byte-for-byte
100
- // the unfronted behaviour.
108
+ // pod. The caller installs the advertised→pod mapping here, so every
109
+ // request built from an advertised id lands on the pod — one choke point.
110
+ // Unset → identity, byte-for-byte the unfronted behaviour.
101
111
  this.toPod = null;
102
112
  }
103
113
 
104
- // Install the advertised→pod url map for a fronted identity (urls.toPod).
114
+ /** Who is talking, and from where — the prefix on every log line and error. */
115
+ get label() { return `${this.role}/${this.runtime}`; }
116
+
117
+ /** One line for a boot log: what this transport is and what it points at. */
118
+ describe() {
119
+ return `${this.role} over a ${this.runtime} session → ${this.webId?.split('#')[0] || 'unknown'}`;
120
+ }
121
+
122
+ /** Install the advertised→pod url map for a fronted identity (urls.toPod). */
105
123
  setUrlMap(fn) { this.toPod = typeof fn === 'function' ? fn : null; }
106
124
 
107
- async warmup() { return this.session.warmup(); }
125
+ async warmup() { return this.session.warmup?.(); }
108
126
 
109
- // What we have asked of this pod, for /status and for answering an operator.
127
+ /** What we have asked of this pod, for a status page or an operator. */
110
128
  stats() {
111
129
  return {
112
- ...this.session.stats(),
130
+ role: this.role,
131
+ runtime: this.runtime,
113
132
  probes: this.probeCount,
114
133
  pausedFor: Math.max(0, Math.round((this.pausedUntil - Date.now()) / 1000)),
115
134
  };
116
135
  }
117
136
 
118
- // A deliberately CREDENTIAL-FREE request: it asks what a stranger would see,
119
- // so it cannot go through session.fetch without answering a different
120
- // question. It still belongs to this pod, though — it opens a socket to it
121
- // and takes one of its workers — so it observes the same Retry-After window
122
- // and is counted. Ten of these ride on every profile save, and none of them
123
- // used to appear in /status, which made podRequests under-report by design.
124
- async probe(url, init = {}) {
137
+ /**
138
+ * Observe a throttling answer and arm the pod-wide cooldown.
139
+ *
140
+ * One pause for the whole pod, not one per request: without it every
141
+ * in-flight call rides its own ladder into a server that has already said it
142
+ * is overloaded, which is how a throttle becomes a stampede. A 429 or 503
143
+ * with NO Retry-After still arms the window — the browser copy used to arm
144
+ * nothing at all in that case, which is most cases.
145
+ */
146
+ _observe(res) {
147
+ if (res.status !== 429 && res.status !== 503) return res;
148
+ // What the server ASKED for is always honoured. What to do when it asked
149
+ // for nothing depends on what the cooldown costs here: a caller that
150
+ // REFUSES for the window loses nothing by assuming a minute, while one that
151
+ // WAITS it out would freeze a person's tab for a minute over a throttle
152
+ // that clears in seconds — so there, absence arms nothing and the caller's
153
+ // own retry ladder paces it instead.
154
+ const asked = retryAfterMs(res, this.maxCooldownMs);
155
+ const ms = asked ?? (this.cooldownMode === 'refuse' ? COOLDOWN_DEFAULT_MS : 0);
156
+ if (!ms) return res;
157
+ this.pausedUntil = Date.now() + ms;
158
+ this.log(`[${this.label}] pod returned ${res.status}${asked ? ', Retry-After' : ''} — `
159
+ + `holding all requests for ${Math.round(ms / 1000)}s`);
160
+ return res;
161
+ }
162
+
163
+ /** Honour an armed cooldown: fail fast, or wait it out, as configured. */
164
+ async _cooldownGate() {
125
165
  const left = this.pausedUntil - Date.now();
126
- if (left > 0) {
127
- throw new Error(`pod asked us to back off — ${Math.ceil(left / 1000)}s left of its Retry-After`);
166
+ if (left <= 0) return;
167
+ if (this.cooldownMode === 'wait') {
168
+ await sleep(Math.min(left, this.maxCooldownMs));
169
+ return;
128
170
  }
171
+ throw new Error(`[${this.label}] pod asked us to back off — ${Math.ceil(left / 1000)}s left of its Retry-After`);
172
+ }
173
+
174
+ /**
175
+ * The seam a subclass overrides to add transport behaviour — the browser's
176
+ * retry ladder lives here. Overriding THIS rather than `fetch` is deliberate:
177
+ * an override cannot skip the url map, the cooldown accounting or the
178
+ * deletion deny-list, because those are in `fetch` above it.
179
+ */
180
+ async _send(url, init) { return this.session.fetch(url, init); }
181
+
182
+ /**
183
+ * A deliberately CREDENTIAL-FREE request: it asks what a stranger would see,
184
+ * so it cannot go through the session without answering a different question.
185
+ * It still belongs to this pod, though — it opens a socket to it and takes
186
+ * one of its workers — so it observes the same cooldown and is counted.
187
+ *
188
+ * No url map: a probe asks about the ADVERTISED face, which is the whole
189
+ * point of asking.
190
+ */
191
+ async probe(url, init = {}) {
192
+ await this._cooldownGate();
129
193
  this.probeCount++;
130
- const res = await fetch(url, init);
131
- if (res.status === 429 || res.status === 503) {
132
- const ms = retryAfterMs(res) ?? 60_000;
133
- this.pausedUntil = Date.now() + ms;
134
- this.log(`pod returned ${res.status} to an unauthenticated probe — pausing for ${Math.round(ms / 1000)}s`);
135
- }
136
- return res;
194
+ return this._observe(await fetch(url, init));
137
195
  }
138
196
 
197
+ /**
198
+ * Every authenticated request. Nothing below this reaches the session
199
+ * directly: this is the only place the cooldown is both OBSERVED and ARMED,
200
+ * the url map applied, and the deny-list enforced.
201
+ */
139
202
  async fetch(url, init) {
140
- const left = this.pausedUntil - Date.now();
141
- if (left > 0) {
142
- throw new Error(`pod asked us to back off — ${Math.ceil(left / 1000)}s left of its Retry-After`);
143
- }
144
- // Fronted identity: an advertised id maps to its pod location here, so the
145
- // request lands on the pod. A no-op for a pod-native id or an unfronted
146
- // identity (toPod unset).
203
+ await this._cooldownGate();
147
204
  if (this.toPod) url = this.toPod(url);
148
- const res = await this.session.fetch(url, init);
149
- if (res.status === 429 || res.status === 503) {
150
- const ms = retryAfterMs(res) ?? 60_000; // no header: our own cooldown
151
- this.pausedUntil = Date.now() + ms;
152
- this.log(`pod returned ${res.status} — pausing all requests for ${Math.round(ms / 1000)}s`);
205
+ if (String(init?.method || '').toUpperCase() === 'DELETE') this._guardDelete(url);
206
+ return this._observe(await this._send(url, init));
207
+ }
208
+
209
+ /** Both refusals a DELETE must pass, wherever it was issued from. */
210
+ _guardDelete(url) {
211
+ protectedFromDeletion(url);
212
+ // The pattern list knows the usual name for an access-control document.
213
+ // One the pod named itself is just as fatal to remove. Tolerating an
214
+ // absent map matters: this now runs for EVERY delete, including one issued
215
+ // through `fetch` by a caller holding a minimally-built transport.
216
+ for (const acl of this.aclUrls?.values() ?? []) {
217
+ if (acl === url) throw new Error(`refusing to DELETE an access-control document: ${url}`);
153
218
  }
154
- return res;
155
219
  }
156
220
 
157
- // Everything below goes through this.fetch, never session.fetch directly:
158
- // that is the only place the Retry-After cooldown is both OBSERVED and ARMED.
159
- // Writing straight to the session meant a pod could answer 429 to every PUT
160
- // and DELETE we sent without any of them ever being recorded as a refusal,
161
- // and meant a cooldown armed by a read did not stop a publish burst.
162
221
  async put(url, body, contentType) {
163
222
  const res = await this.fetch(url, {
164
223
  method: 'PUT', headers: { 'content-type': contentType }, body,
165
224
  });
166
- if (res.status >= 400) throw new Error(`PUT ${url} → ${res.status}`);
225
+ if (res.status >= 400) throw new Error(`[${this.label}] PUT ${url} → ${res.status}`);
167
226
  // Writing a document is usually the step before setting its access, and
168
227
  // the answer to the write already says where that lives. Taking it here
169
228
  // spares the extra request the ACL write would otherwise make.
@@ -171,6 +230,26 @@ export class RemotePod {
171
230
  return res;
172
231
  }
173
232
 
233
+ async putJson(url, obj, contentType = 'application/activity+json') {
234
+ return this.put(url, JSON.stringify(obj), contentType);
235
+ }
236
+
237
+ // A read that FAILED is not a document that is ABSENT. Returning null for
238
+ // both let a 429 read as "no replies yet", and the caller then rewrote the
239
+ // collection from empty — erasing every reply already recorded. Only a real
240
+ // 404/410 is absence; anything else throws and the caller retries later.
241
+ async getJson(url) {
242
+ const res = await this.fetch(url, { headers: { accept: '*/*' } });
243
+ if (res.status === 404 || res.status === 410) return null;
244
+ if (res.status >= 400) throw new Error(`[${this.label}] GET ${url} → ${res.status}`);
245
+ return res.json().catch(() => null);
246
+ }
247
+
248
+ async delete(url) {
249
+ const res = await this.fetch(url, { method: 'DELETE' });
250
+ return res.status < 400 || res.status === 404;
251
+ }
252
+
174
253
  /** Remember an access-control location the pod volunteered on a response. */
175
254
  noteAclLink(url, res) {
176
255
  if (this.aclUrls.has(url)) return;
@@ -213,7 +292,7 @@ export class RemotePod {
213
292
  || st.object.value.startsWith(ACP_NS));
214
293
  if (acp) {
215
294
  this.aclFlavour = false;
216
- this.log('this pod states access as ACP policies, which this agent does not write — '
295
+ this.log(`[${this.label}] this pod states access as ACP policies, which this library does not write — `
217
296
  + 'its access rules are left exactly as they are, and nothing here is published private');
218
297
  }
219
298
  }
@@ -221,36 +300,51 @@ export class RemotePod {
221
300
  return this.aclFlavour;
222
301
  }
223
302
 
224
- async putJson(url, obj, contentType = 'application/activity+json') {
225
- return this.put(url, JSON.stringify(obj), contentType);
226
- }
227
-
228
- // A read that FAILED is not a document that is ABSENT. Returning null for
229
- // both let a 429 read as "no replies yet", and the caller then rewrote the
230
- // collection from empty — erasing every reply already recorded. Only a real
231
- // 404/410 is absence; anything else throws and the caller retries later.
232
- async getJson(url) {
233
- const res = await this.fetch(url, { headers: { accept: '*/*' } });
234
- if (res.status === 404 || res.status === 410) return null;
235
- if (res.status >= 400) throw new Error(`GET ${url} → ${res.status}`);
236
- return res.json().catch(() => null);
303
+ // WAC doc granting the public `publicModes` on target, owner full control.
304
+ // An empty publicModes list yields an owner-only document.
305
+ //
306
+ // Built and serialised by rdflib, like every other document written here.
307
+ // This is the highest-consequence RDF in the project: an ACL that comes out
308
+ // malformed, or naming the wrong subject, either locks the owner out or
309
+ // leaves the private trees world-readable. $rdf.sym() also throws on an
310
+ // illegal IRI, so a pod URL with something odd in it fails here rather than
311
+ // silently producing a document that means something else.
312
+ aclDoc(targetUrl, publicModes, { appendAgents = [], aclUrl = null } = {}) {
313
+ const url = aclUrl || targetUrl + '.acl';
314
+ const doc = $rdf.sym(url);
315
+ const target = $rdf.sym(targetUrl);
316
+ const g = $rdf.graph();
317
+ const authorize = (subject, agentPred, agent, modes) => {
318
+ g.add(subject, RDF('type'), ACL('Authorization'), doc);
319
+ g.add(subject, agentPred, agent, doc);
320
+ g.add(subject, ACL('accessTo'), target, doc);
321
+ g.add(subject, ACL('default'), target, doc);
322
+ for (const m of modes) g.add(subject, ACL('mode'), ACL(m), doc);
323
+ };
324
+ if (publicModes.length) {
325
+ authorize($rdf.sym(url + '#public'), ACL('agentClass'), FOAF('Agent'), publicModes);
326
+ }
327
+ // Named agents granted Append but not public: this is how the inbox is
328
+ // locked to a gateway — public loses Append, the gateway's WebID keeps it.
329
+ appendAgents.forEach((webId, i) =>
330
+ authorize($rdf.sym(url + `#gw${i}`), ACL('agent'), $rdf.sym(webId), ['Append']));
331
+ authorize($rdf.sym(url + '#owner'), ACL('agent'), $rdf.sym(this.webId),
332
+ ['Read', 'Write', 'Control']);
333
+ return $rdf.serialize(doc, g, url, 'text/turtle');
237
334
  }
238
335
 
239
- async delete(url) {
240
- protectedFromDeletion(url);
241
- // The pattern list above knows the usual name for an access-control
242
- // document. One the pod named itself is just as fatal to remove.
243
- for (const acl of this.aclUrls.values()) {
244
- if (acl === url) throw new Error(`refusing to DELETE an access-control document: ${url}`);
245
- }
246
- const res = await this.fetch(url, { method: 'DELETE' });
247
- return res.status < 400 || res.status === 404;
336
+ async setAcl(targetUrl, publicModes, opts = {}) {
337
+ const url = await this.aclUrlFor(targetUrl);
338
+ if (!await this.aclWritable(url)) return null;
339
+ return this.put(url, this.aclDoc(targetUrl, publicModes, { ...opts, aclUrl: url }), 'text/turtle');
248
340
  }
249
341
 
250
342
  // Child documents of an LDP container (URLs under it, excluding aux docs).
251
- // Revalidated: the inbox is polled every two minutes and is usually
343
+ // Revalidated: the inbox is polled every couple of minutes and is usually
252
344
  // unchanged, so ask conditionally and let the server answer 304.
253
345
  async listContainer(url) {
346
+ // Lazily, not only in the constructor: a caller may hold a transport built
347
+ // with Object.create for a test, setting just the few fields it drives.
254
348
  this._listCache ||= new Map();
255
349
  const known = this._listCache.get(url);
256
350
  const res = await this.fetch(url, {
@@ -260,18 +354,21 @@ export class RemotePod {
260
354
  if (res.status >= 400) return [];
261
355
  // rdflib, not a regex: a container listing is RDF, and the gap between
262
356
  // "any angle-bracketed thing in the document" and "what this container
263
- // actually contains" is where quiet bugs live. claude/plans/no-regex-rdf.md
357
+ // actually contains" is where quiet bugs live.
264
358
  //
265
359
  // CSS puts dc:modified and posix:size on every child of the same listing,
266
- // so age and weight cost nothing extra — which is what lets the drain work
360
+ // so age and weight cost nothing extra — which is what lets a drain work
267
361
  // oldest-first and lets a backlog be measured without reading any of it.
268
362
  let body;
269
363
  try {
270
364
  body = await readCapped(res, LISTING_MAX_BYTES);
271
365
  } catch (e) {
272
- this.log(`listing at ${url}: ${e.message} — using the last known listing`);
366
+ this.log(`[${this.label}] listing at ${url}: ${e.message} — using the last known listing`);
273
367
  return known?.children ?? [];
274
368
  }
369
+ // A parse failure is a real fault, not a cache hit: it propagates. The
370
+ // browser copy used to swallow it and hand back a stale listing, which
371
+ // turns a corrupt container into silently missing work.
275
372
  const g = $rdf.graph();
276
373
  $rdf.parse(body, g, url, 'text/turtle');
277
374
  const here = $rdf.sym(url);
@@ -289,65 +386,26 @@ export class RemotePod {
289
386
  modified: g.any(child, DC('modified'), null, here)?.value || null,
290
387
  });
291
388
  }
292
- // Oldest first. An LDP listing is a set, so without this the drain works
293
- // in whatever order the graph happened to parse — a mention from last week
389
+ // Oldest first. An LDP listing is a set, so without this a drain works in
390
+ // whatever order the graph happened to parse — a mention from last week
294
391
  // after one from today, and no way to make progress predictable.
295
392
  list.sort((a, b) => String(a.modified || '').localeCompare(String(b.modified || '')));
296
393
  this._listCache.set(url, { etag: res.headers.get('etag'), children: list });
297
394
  return list;
298
395
  }
299
396
 
300
- // WAC doc granting the public `publicModes` on target, owner full control.
301
- // An empty publicModes list yields an owner-only document.
302
- //
303
- // Built and serialised by rdflib, like every other document this project
304
- // writes — see claude/plans/no-regex-rdf.md. This was the last site still
305
- // assembling Turtle from template literals, and it is the highest-consequence
306
- // RDF here: an ACL that comes out malformed, or naming the wrong subject,
307
- // either locks the owner out or leaves the private trees world-readable.
308
- // $rdf.sym() also throws on an illegal IRI, so a pod URL with something odd
309
- // in it fails here rather than silently producing a document that means
310
- // something else.
311
- aclDoc(targetUrl, publicModes, { appendAgents = [], aclUrl = null } = {}) {
312
- const url = aclUrl || targetUrl + '.acl';
313
- const doc = $rdf.sym(url);
314
- const target = $rdf.sym(targetUrl);
315
- const g = $rdf.graph();
316
- const authorize = (subject, agentPred, agent, modes) => {
317
- g.add(subject, RDF('type'), ACL('Authorization'), doc);
318
- g.add(subject, agentPred, agent, doc);
319
- g.add(subject, ACL('accessTo'), target, doc);
320
- g.add(subject, ACL('default'), target, doc);
321
- for (const m of modes) g.add(subject, ACL('mode'), ACL(m), doc);
322
- };
323
- if (publicModes.length) {
324
- authorize($rdf.sym(url + '#public'), ACL('agentClass'), FOAF('Agent'), publicModes);
325
- }
326
- // Named agents granted Append but not public: this is how the inbox is
327
- // locked to a gateway — public loses Append, the gateway's WebID keeps it.
328
- appendAgents.forEach((webId, i) =>
329
- authorize($rdf.sym(url + `#gw${i}`), ACL('agent'), $rdf.sym(webId), ['Append']));
330
- authorize($rdf.sym(url + '#owner'), ACL('agent'), $rdf.sym(this.webId),
331
- ['Read', 'Write', 'Control']);
332
- return $rdf.serialize(doc, g, url, 'text/turtle');
333
- }
334
-
335
- async setAcl(targetUrl, publicModes, opts = {}) {
336
- const url = await this.aclUrlFor(targetUrl);
337
- if (!await this.aclWritable(url)) return null;
338
- return this.put(url, this.aclDoc(targetUrl, publicModes, { ...opts, aclUrl: url }), 'text/turtle');
339
- }
340
-
341
- // The WebID profile advertises the actor as an account:
342
- // <webId> foaf:account <actor> .
343
- // <actor> a foaf:OnlineAccount, as:Person|as:Group ; foaf:accountName "@handle@host" .
344
- // Read–check–write through rdflib. Returns false when the profile already
345
- // says all of it. The parsed graph must mention the WebID before anything is
346
- // written back — an empty or foreign body must never become the new profile.
397
+ /**
398
+ * The WebID profile advertises the actor as an account:
399
+ * <webId> foaf:account <actor> .
400
+ * <actor> a foaf:OnlineAccount, as:Person|as:Group ; foaf:accountName "@handle@host" .
401
+ * Read–check–write through rdflib. Returns false when the profile already
402
+ * says all of it. The parsed graph must mention the WebID before anything is
403
+ * written back — an empty or foreign body must never become the new profile.
404
+ */
347
405
  async linkAccountInProfile({ actorUrl, accountName, kind = 'person' }) {
348
406
  const docUrl = this.webId.split('#')[0];
349
407
  const res = await this.fetch(docUrl, { headers: { accept: 'text/turtle' } });
350
- if (res.status >= 400) throw new Error(`GET ${docUrl} → ${res.status}`);
408
+ if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} → ${res.status}`);
351
409
  const g = $rdf.graph();
352
410
  $rdf.parse(await res.text(), g, docUrl, 'text/turtle');
353
411
  const doc = $rdf.sym(docUrl);
@@ -370,7 +428,7 @@ export class RemotePod {
370
428
  // A patch touches these statements and nothing else. Rewriting the whole
371
429
  // profile re-serialises statements that are not ours — the OIDC issuer
372
430
  // among them — and a server is entitled to refuse a write that would.
373
- const deletes = stale.map(st => [ st.subject, st.predicate, st.object ]);
431
+ const deletes = stale.map(st => [st.subject, st.predicate, st.object]);
374
432
  if (await this.patchDocument(docUrl, missing, deletes)) return true;
375
433
  for (const st of stale) g.remove(st);
376
434
  for (const [s, p, o] of missing) g.add(s, p, o, doc);
@@ -414,6 +472,6 @@ export class RemotePod {
414
472
  // remove is not there any more — is a real answer, and rewriting the whole
415
473
  // document over the top of it would destroy whatever changed it.
416
474
  if (res.status === 405 || res.status === 415 || res.status === 501) return false;
417
- throw new Error(`PATCH ${docUrl} → ${res.status}`);
475
+ throw new Error(`[${this.label}] PATCH ${docUrl} → ${res.status}`);
418
476
  }
419
477
  }
@@ -0,0 +1,71 @@
1
+ // urls.mjs — where an ActivityPub actor's documents live on a Solid pod.
2
+ //
3
+ // Everything the actor owns nests under ONE top-level container (`root`), so
4
+ // the pod stays tidy and one pod could host several actors under different
5
+ // roots. WebFinger and host-meta are the exception: fediverse discovery only
6
+ // ever looks at the host root, so they sit at the pod base.
7
+ //
8
+ // `root` is REQUIRED. It used to default, and the default disagreed with what
9
+ // one of the two callers actually passed — which is the kind of bug that puts
10
+ // an actor's state in one container and its published documents in another.
11
+ // A library has no business having an opinion about which container an
12
+ // application uses; the application states it.
13
+ //
14
+ // When `publicBase` is given — an identity whose ADVERTISED ids live somewhere
15
+ // else, e.g. "https://front.example/u/me/" — the ids a remote server sees build
16
+ // on that instead, while the pod-side trees and every write target stay on the
17
+ // pod. `toPod`/`toPublic` map between the two spaces, and a transport applies
18
+ // `toPod` at its one request choke point, so callers keep passing advertised
19
+ // ids and the writes still land on the pod. With no `publicBase` the returned
20
+ // object is byte-identical to the unfronted form — an invariant worth keeping,
21
+ // because it is what lets the fronted case be tested against the plain one.
22
+
23
+ export function apUrls(remotePod, root, { publicBase = null } = {}) {
24
+ if (!root) throw new Error('apUrls: a container root is required — the caller states it, this library does not guess');
25
+ const base = remotePod.endsWith('/') ? remotePod : remotePod + '/';
26
+ const home = base + (root.endsWith('/') ? root : root + '/');
27
+ // The face a remote sees: the fronted home, or the pod home when unfronted.
28
+ const face = publicBase ? (publicBase.endsWith('/') ? publicBase : publicBase + '/') : home;
29
+ const urls = {
30
+ base, home,
31
+ webfinger: base + '.well-known/webfinger',
32
+ actor: face + 'ap/actor',
33
+ inbox: face + 'ap/inbox/',
34
+ outbox: face + 'ap/outbox',
35
+ followers: face + 'ap/followers',
36
+ following: face + 'ap/following',
37
+ notes: face + 'ap/notes/',
38
+ privateNotes: face + 'ap/private/',
39
+ featured: face + 'ap/featured',
40
+ // FEP-1b12: the moderator roster a recipient validates announced
41
+ // moderation against. Published only when moderators are configured.
42
+ moderators: face + 'ap/moderators',
43
+ // FEP-4ccd and FEP-c648: follows in limbo and the block list, as
44
+ // collections. They live in the private container so the owner-only ACL
45
+ // is inherited, not re-stated per document.
46
+ pendingFollowers: face + 'ap/private/pending-followers',
47
+ pendingFollowing: face + 'ap/private/pending-following',
48
+ blocked: face + 'ap/private/blocked',
49
+ profileHtml: face + 'ap/profile.html',
50
+ // Media stays on the pod even when fronted: attachment urls are not
51
+ // identity-checked by remotes, and proxying blobs would be pure cost.
52
+ media: home + 'ap/media/',
53
+ state: home + 'ap-state/',
54
+ };
55
+ if (publicBase) {
56
+ urls.podHome = home;
57
+ urls.publicHome = face;
58
+ urls.toPod = (u) => (typeof u === 'string' && u.startsWith(face) ? home + u.slice(face.length) : u);
59
+ urls.toPublic = (u) => (typeof u === 'string' && u.startsWith(home) ? face + u.slice(home.length) : u);
60
+ }
61
+ return urls;
62
+ }
63
+
64
+ // The host a handle would resolve through, or null when no handle can point
65
+ // here. @name@host is looked up at https://host/.well-known/webfinger, so only
66
+ // a pod that owns the root of its host can answer for one; a pod living at
67
+ // https://server/name/ may publish the document but nothing will ever ask.
68
+ export function webfingerHost(podUrl) {
69
+ const u = new URL(podUrl);
70
+ return u.pathname === '/' ? u.host : null;
71
+ }