fedipod 0.18.0 → 1.0.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 (324) hide show
  1. package/README.md +105 -115
  2. package/architecture.md +41 -0
  3. package/bin/fedipod.mjs +3 -5
  4. package/browser.svg +72 -0
  5. package/cli.md +9 -0
  6. package/gateway.md +160 -0
  7. package/groups.md +7 -2
  8. package/gui.md +11 -1
  9. package/installed-agent.md +97 -0
  10. package/lib/admin.mjs +3 -3
  11. package/lib/deliver.mjs +6 -0
  12. package/lib/export-collections.mjs +1 -1
  13. package/lib/front-core.mjs +272 -49
  14. package/lib/gateway-core.mjs +3 -7
  15. package/lib/httpsig.mjs +33 -2
  16. package/lib/import.mjs +1 -1
  17. package/lib/intake.mjs +335 -118
  18. package/lib/links.mjs +6 -32
  19. package/lib/mastoapi.mjs +372 -35
  20. package/lib/migrate.mjs +4 -18
  21. package/lib/pod/README.md +61 -0
  22. package/lib/pod/actor.mjs +96 -0
  23. package/lib/pod/collection.mjs +77 -0
  24. package/lib/pod/containers.mjs +136 -0
  25. package/lib/pod/discovery.mjs +46 -0
  26. package/lib/pod/featured.mjs +18 -0
  27. package/lib/pod/followers.mjs +24 -0
  28. package/lib/pod/following.mjs +10 -0
  29. package/lib/pod/http.mjs +74 -0
  30. package/lib/pod/inbox.mjs +144 -0
  31. package/lib/pod/links.mjs +35 -0
  32. package/lib/pod/media.mjs +27 -0
  33. package/lib/pod/notes.mjs +80 -0
  34. package/lib/pod/notifications.mjs +89 -0
  35. package/lib/pod/outbox.mjs +18 -0
  36. package/lib/pod/package.json +19 -0
  37. package/lib/pod/policy.mjs +58 -0
  38. package/lib/pod/private.mjs +19 -0
  39. package/lib/pod/root.mjs +63 -0
  40. package/lib/pod/state.mjs +39 -0
  41. package/lib/pod/transport.mjs +477 -0
  42. package/lib/pod/urls.mjs +71 -0
  43. package/lib/publisher.mjs +112 -183
  44. package/lib/remote.mjs +30 -395
  45. package/lib/social.mjs +18 -9
  46. package/lib/storage.mjs +2 -2
  47. package/lib/store.mjs +8 -1
  48. package/lib/tagfeed.mjs +19 -2
  49. package/lib/wire.mjs +48 -67
  50. package/package.json +14 -3
  51. package/run-agent.mjs +14 -39
  52. package/scripts/build-app.mjs +64 -0
  53. package/scripts/check-pod-calls.mjs +177 -0
  54. package/scripts/stage-site.mjs +144 -0
  55. package/web/admin/admin.js +141 -74
  56. package/web/admin/bar.css +9 -13
  57. package/web/admin/bar.js +2 -2
  58. package/web/admin/client/client.js +1 -1
  59. package/web/admin/client/index.html +5 -6
  60. package/web/admin/index.html +53 -65
  61. package/web/admin/setup/index.html +2 -1
  62. package/web/admin/setup/setup.js +1 -1
  63. package/web/admin/tokens.css +56 -0
  64. package/web/app/README.md +77 -0
  65. package/web/app/admin-facade.mjs +549 -0
  66. package/web/app/agent.mjs +490 -0
  67. package/web/app/atproto-browser.mjs +86 -0
  68. package/web/app/boot.mjs +340 -0
  69. package/web/app/deliver-relay.mjs +72 -0
  70. package/web/app/dist/boot.js +34067 -0
  71. package/web/app/dist/boot.js.map +7 -0
  72. package/web/app/dist/sw.js +53745 -0
  73. package/web/app/dist/sw.js.map +7 -0
  74. package/web/app/fediacct-browser.mjs +146 -0
  75. package/web/app/idb-kv.mjs +35 -0
  76. package/web/app/index.html +203 -0
  77. package/web/app/index.html~ +152 -0
  78. package/web/app/keys-browser.mjs +46 -0
  79. package/web/app/keystore.mjs +99 -0
  80. package/web/app/oidc-session.mjs +189 -0
  81. package/web/app/pod-auth.mjs +248 -0
  82. package/web/app/pod-remote.mjs +84 -0
  83. package/web/app/shims/_globals.mjs +2 -0
  84. package/web/app/shims/fedify-sig.mjs +77 -0
  85. package/web/app/shims/node-crypto.mjs +113 -0
  86. package/web/app/shims/node-fs.mjs +17 -0
  87. package/web/app/shims/node-path.mjs +19 -0
  88. package/web/app/shims/node-url.mjs +7 -0
  89. package/web/app/shims/prelude.js +58 -0
  90. package/web/app/shims/safefetch.mjs +58 -0
  91. package/web/app/shims/web-push.mjs +14 -0
  92. package/web/app/signup.mjs +248 -0
  93. package/web/app/site/_headers +9 -0
  94. package/web/app/site/_redirects +21 -0
  95. package/web/app/site/admin/admin.js +1181 -0
  96. package/web/app/site/admin/bar.css +60 -0
  97. package/web/app/site/admin/bar.js +61 -0
  98. package/web/app/site/admin/client/client.js +66 -0
  99. package/web/app/site/admin/client/index.html +43 -0
  100. package/web/app/site/admin/index.html +556 -0
  101. package/web/app/site/admin/setup/index.html +191 -0
  102. package/web/app/site/admin/setup/setup.js +308 -0
  103. package/web/app/site/admin/tokens.css +56 -0
  104. package/web/app/site/admin/window.css +55 -0
  105. package/web/app/site/admin/window.js +136 -0
  106. package/web/app/site/app/404.html +40 -0
  107. package/web/app/site/app/_headers +2 -0
  108. package/web/app/site/app/apple-touch-icon.png +0 -0
  109. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
  110. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
  111. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  112. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  113. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  114. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
  115. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
  116. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  117. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  118. package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
  119. package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
  120. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
  121. package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
  122. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
  123. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
  124. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
  125. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
  126. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
  127. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  128. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  129. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  130. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  131. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  132. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
  133. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  134. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  135. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  136. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  137. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  138. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  139. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  140. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  141. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  142. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  143. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
  144. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
  145. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  146. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
  147. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  148. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  149. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  150. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  151. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
  152. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  153. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
  154. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  155. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  156. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  157. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  158. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  159. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  160. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  161. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
  162. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
  163. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
  164. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  165. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  166. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  167. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  168. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
  169. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
  170. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  171. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  172. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
  173. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  174. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  175. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  176. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  177. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
  178. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  179. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  180. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
  181. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
  182. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
  183. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  184. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  185. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  186. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
  187. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  188. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
  189. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  190. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
  191. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  192. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  193. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  194. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
  195. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
  196. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  197. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
  198. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
  199. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
  200. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
  201. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  202. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  203. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  204. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  205. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  206. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
  207. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
  208. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  209. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  210. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  211. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
  212. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
  213. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  214. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
  215. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
  216. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
  217. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
  218. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
  219. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
  220. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
  221. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
  222. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
  223. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  224. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  225. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  226. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  227. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  228. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
  229. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  230. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
  231. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  232. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  233. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
  234. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  235. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  236. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  237. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  238. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  239. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  240. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  241. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
  242. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  243. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
  244. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
  245. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  246. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  247. package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
  248. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
  249. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  250. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  251. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
  252. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  253. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  254. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
  255. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
  256. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
  257. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  258. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  259. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
  260. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
  261. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
  262. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  263. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
  264. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  265. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  266. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
  267. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  268. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
  269. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  270. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  271. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  272. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  273. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  274. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
  275. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
  276. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
  277. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  278. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
  279. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
  280. package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
  281. package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
  282. package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
  283. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
  284. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  285. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  286. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  287. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
  288. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  289. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
  290. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  291. package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
  292. package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
  293. package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
  294. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  295. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  296. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  297. package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
  298. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
  299. package/web/app/site/app/compose/index.html +20 -0
  300. package/web/app/site/app/favicon.ico +0 -0
  301. package/web/app/site/app/index.html +73 -0
  302. package/web/app/site/app/logo-192.png +0 -0
  303. package/web/app/site/app/logo-512.png +0 -0
  304. package/web/app/site/app/logo-badge-72.png +0 -0
  305. package/web/app/site/app/logo-maskable-512.png +0 -0
  306. package/web/app/site/app/logo-monochrome-512.png +0 -0
  307. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  308. package/web/app/site/app/manifest.webmanifest +1 -0
  309. package/web/app/site/app/og-image-2.jpg +0 -0
  310. package/web/app/site/app/og-image.png +0 -0
  311. package/web/app/site/app/robots.txt +2 -0
  312. package/web/app/site/app/version.json +1 -0
  313. package/web/app/site/boot.js +34067 -0
  314. package/web/app/site/index.html +203 -0
  315. package/web/app/site/sw.js +53745 -0
  316. package/web/app/sw-src.mjs +205 -0
  317. package/web/front/#new-account.html# +43 -0
  318. package/web/front/admin.html +1 -81
  319. package/web/front/admin.js +85 -0
  320. package/web/front/new-account.html +1 -9
  321. package/web/front/new-account.js +13 -0
  322. package/web/front/run.html +1 -92
  323. package/web/front/run.js +107 -0
  324. package/lib/podrdf.mjs +0 -132
package/lib/wire.mjs CHANGED
@@ -6,75 +6,40 @@ export const AS_CTX = 'https://www.w3.org/ns/activitystreams';
6
6
  export const SEC_CTX = 'https://w3id.org/security/v1';
7
7
  export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
8
8
 
9
- // URLs of the actor's documents on the remote pod. Everything the agent owns
10
- // nests under ONE top-level container (`root`) so the pod stays tidy and one
11
- // pod could host several actors under different roots. Webfinger + host-meta
12
- // stay at the pod root — fediverse discovery requires the host root.
13
- // The AP URLs for an identity. Normally everything hangs off the pod home.
14
- //
15
- // When `publicBase` is given (a fronted identity, e.g.
16
- // "https://fedipod.net/u/me/"), the ADVERTISED ids — the actor and everything
17
- // under `ap/` a remote server sees — build on the fronted home instead, while
18
- // the pod-side trees (`fediverse/`, `ap-state/`) and the write targets stay on
19
- // the pod. `toPod`/`toPublic` map between the two spaces; `RemotePod` applies
20
- // `toPod` at its one request choke point, so publisher/social/intake keep
21
- // passing advertised ids and the writes still land on the pod. With no
22
- // `publicBase` the returned object is byte-identical to before — the invariant
23
- // the smoke suite pins.
9
+ // The pod LAYOUT — which container each document lives in — moved into the pod
10
+ // library (lib/pod/urls.mjs), which knows nothing about FediPod and requires
11
+ // the container root to be stated rather than defaulted. What stays here is
12
+ // FediPod's own answer to "which root", and the handle rule, both of which read
13
+ // this application's config shape.
14
+ import { apUrls as podUrls } from './pod/urls.mjs';
15
+
16
+ export { webfingerHost } from './pod/urls.mjs';
17
+
18
+ // The container the Node agent has always used. Older than the rename to
19
+ // FediPod and kept because it is what every existing Node-side pod actually
20
+ // contains; the browser build states `fedipod/` on its own configs. A config
21
+ // that names a root is always believed — this is only the answer for one that
22
+ // does not.
23
+ export const DEFAULT_ROOT = 'activitypods-js/';
24
+
24
25
  // The handle the fediverse sees: a fronted identity's name is the front's.
25
26
  export function publicHandle(config) {
26
27
  return config?.gateway?.frontActor?.match(/\/u\/([^/]+)\/ap\/actor\/?$/)?.[1]
27
28
  || config?.handle || null;
28
29
  }
29
30
 
30
- export function apUrls(remotePod, root = 'activitypods-js/', { publicBase = null } = {}) {
31
- const base = remotePod.endsWith('/') ? remotePod : remotePod + '/';
32
- const home = base + (!root || root.endsWith('/') ? root : root + '/');
33
- // The face a remote sees: the fronted home, or the pod home when unfronted.
34
- const face = publicBase ? (publicBase.endsWith('/') ? publicBase : publicBase + '/') : home;
35
- const urls = {
36
- base, home,
37
- webfinger: base + '.well-known/webfinger',
38
- actor: face + 'ap/actor',
39
- inbox: face + 'ap/inbox/',
40
- outbox: face + 'ap/outbox',
41
- followers: face + 'ap/followers',
42
- following: face + 'ap/following',
43
- notes: face + 'ap/notes/',
44
- privateNotes: face + 'ap/private/',
45
- featured: face + 'ap/featured',
46
- // FEP-1b12: the moderator roster a recipient validates announced
47
- // moderation against. Published only when moderators are configured.
48
- moderators: face + 'ap/moderators',
49
- // FEP-4ccd and FEP-c648: follows in limbo and the block list, as
50
- // collections. They live in the private container so the owner-only ACL
51
- // is inherited, not re-stated per document.
52
- pendingFollowers: face + 'ap/private/pending-followers',
53
- pendingFollowing: face + 'ap/private/pending-following',
54
- blocked: face + 'ap/private/blocked',
55
- profileHtml: face + 'ap/profile.html',
56
- // Media stays on the pod even when fronted: attachment urls are not
57
- // identity-checked by remotes, and proxying blobs would be pure cost.
58
- media: home + 'ap/media/',
59
- fediverse: home + 'fediverse/',
60
- state: home + 'ap-state/',
61
- };
62
- if (publicBase) {
63
- urls.podHome = home;
64
- urls.publicHome = face;
65
- urls.toPod = (u) => (typeof u === 'string' && u.startsWith(face) ? home + u.slice(face.length) : u);
66
- urls.toPublic = (u) => (typeof u === 'string' && u.startsWith(home) ? face + u.slice(home.length) : u);
67
- }
68
- return urls;
69
- }
70
-
71
- // The host a handle would resolve through, or null when no handle can point
72
- // here. @name@host is looked up at https://host/.well-known/webfinger, so only
73
- // a pod that owns the root of its host can answer for one; a pod living at
74
- // https://server/name/ may publish the document but nothing will ever ask.
75
- export function webfingerHost(podUrl) {
76
- const u = new URL(podUrl);
77
- return u.pathname === '/' ? u.host : null;
31
+ /**
32
+ * The AP URLs for an identity, with FediPod's default root applied.
33
+ *
34
+ * The library underneath refuses to guess a root. Every caller here goes
35
+ * through this wrapper so there is exactly ONE answer to "which container",
36
+ * which is the thing that was wrong: the browser agent decided `fedipod/` for
37
+ * its own urls while the publisher, handed a config with no `root`, decided
38
+ * `activitypods-js/` — putting an identity's state in one container and its
39
+ * published documents in another.
40
+ */
41
+ export function apUrls(remotePod, root, opts = {}) {
42
+ return podUrls(remotePod, root || DEFAULT_ROOT, opts);
78
43
  }
79
44
 
80
45
  // RFC 6415 host-meta: several fediverse implementations discover WebFinger
@@ -585,15 +550,31 @@ export function titledContent(note) {
585
550
  }
586
551
 
587
552
  // Normalize a wire Note's attachment list to { url, mediaType, description }.
553
+ // Capped: this list comes from somebody else's document and lands in a
554
+ // statuses.json row, which is rewritten whole on every change. Twenty is more
555
+ // than any client shows and more than any real post carries.
556
+ const MAX_ATTACHMENTS = 20;
557
+ const MAX_ATTACHMENT_URL = 2048;
558
+ // http(s) only: an attachment URL becomes a client's <img src> or <video src>,
559
+ // and the store has guarded avatars this way all along (safeUrl).
560
+ const attachmentUrl = (u) => {
561
+ if (!u) return null;
562
+ const s = String(u).slice(0, MAX_ATTACHMENT_URL);
563
+ try {
564
+ const p = new URL(s);
565
+ return (p.protocol === 'https:' || p.protocol === 'http:') ? s : null;
566
+ } catch { return null; }
567
+ };
568
+
588
569
  export function attachmentsOf(note) {
589
570
  const list = Array.isArray(note?.attachment) ? note.attachment : note?.attachment ? [note.attachment] : [];
590
571
  // A bare Link carries `href` and no `url` — Lemmy's link posts arrive that
591
572
  // way, and reading only `url` dropped the link the post was about.
592
- return list.map(a => ({
573
+ return list.slice(0, MAX_ATTACHMENTS).map(a => ({
593
574
  url: typeof a?.url === 'string' ? a.url : a?.url?.href || (typeof a?.href === 'string' ? a.href : undefined),
594
- mediaType: a?.mediaType || '',
595
- ...(a?.name ? { description: a.name } : {}),
596
- })).filter(a => a.url);
575
+ mediaType: String(a?.mediaType || '').slice(0, 128),
576
+ ...(a?.name ? { description: String(a.name).slice(0, 1500) } : {}),
577
+ })).map(a => ({ ...a, url: attachmentUrl(a.url) })).filter(a => a.url);
597
578
  }
598
579
 
599
580
  const HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fedipod",
3
- "version": "0.18.0",
3
+ "version": "1.0.0",
4
4
  "description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,15 +26,23 @@
26
26
  "gui.md",
27
27
  "cli.md",
28
28
  "groups.md",
29
- "architecture.svg"
29
+ "architecture.svg",
30
+ "installed-agent.md",
31
+ "architecture.md",
32
+ "gateway.md",
33
+ "browser.svg"
30
34
  ],
31
35
  "scripts": {
32
36
  "start": "node bin/fedipod.mjs up",
33
37
  "install-service": "node bin/fedipod.mjs install-service",
34
38
  "uninstall-service": "node bin/fedipod.mjs uninstall-service",
35
39
  "prepublishOnly": "npm test",
36
- "test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node scripts/verify-ui.mjs",
40
+ "test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs",
37
41
  "verify-ui": "node scripts/verify-ui.mjs",
42
+ "build:sw": "node scripts/build-app.mjs --entry web/app/sw-src.mjs --out web/app/dist/sw.js",
43
+ "build:boot": "node scripts/build-app.mjs --entry web/app/boot.mjs --out web/app/dist/boot.js",
44
+ "build:app": "npm run build:sw && npm run build:boot",
45
+ "stage": "node scripts/stage-site.mjs",
38
46
  "audit": "npm audit --omit=dev",
39
47
  "agent": "node run-agent.mjs"
40
48
  },
@@ -57,5 +65,8 @@
57
65
  "@opentelemetry/core": "^2.8.0",
58
66
  "package.json": "npm:empty-npm-package@1.0.0",
59
67
  "package-lock.json": "npm:empty-npm-package@1.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "esbuild": "^0.28.2"
60
71
  }
61
72
  }
package/run-agent.mjs CHANGED
@@ -34,7 +34,6 @@ import { ensureTrustedTls } from './lib/certs.mjs';
34
34
  import { storageFor } from './lib/storage.mjs';
35
35
  import { resolveKeys } from './lib/keys.mjs';
36
36
  import { RemotePod } from './lib/remote.mjs';
37
- import { PodRdf } from './lib/podrdf.mjs';
38
37
  import { Deliverer } from './lib/deliver.mjs';
39
38
  import { Publisher } from './lib/publisher.mjs';
40
39
  import { Intake } from './lib/intake.mjs';
@@ -69,15 +68,14 @@ export class Agent {
69
68
 
70
69
  // Where the private half lives. `privateRoot` in the credential file names a
71
70
  // container — by default a plain directory beside the credential, and a pod
72
- // on this machine if you move it there — under which the same two trees are
73
- // laid out as on the pod. Absent means on the pod, exactly as before, so an
71
+ // on this machine if you move it there. Absent means on the pod, so an
74
72
  // existing install is untouched.
75
73
  privateUrls(cred, urls = this.urls) {
76
74
  if (!cred.privateRoot) {
77
- return { state: urls.state, fediverse: urls.fediverse, archive: urls.home + 'inbox-archive/', elsewhere: false };
75
+ return { state: urls.state, archive: urls.home + 'inbox-archive/', elsewhere: false };
78
76
  }
79
77
  const base = cred.privateRoot.endsWith('/') ? cred.privateRoot : cred.privateRoot + '/';
80
- return { state: base + 'ap-state/', fediverse: base + 'fediverse/', archive: base + 'inbox-archive/', elsewhere: true };
78
+ return { state: base + 'ap-state/', archive: base + 'inbox-archive/', elsewhere: true };
81
79
  }
82
80
 
83
81
  // A container to keep the private half in. On the pod it is reached with the
@@ -146,14 +144,9 @@ export class Agent {
146
144
  await this.remote.setAcl(this.urls.home, []);
147
145
  await this.remote.setAcl(this.urls.state, []);
148
146
  if (priv.elsewhere) {
149
- for (const s of [stateStore, this.privateStorage(cred, 'fediverse')]) {
150
- const r = await s.write('.keep', '{"keep":true}\n', 'application/json');
151
- if (!r.ok) throw new Error(`private store ${s.base}.keep → ${r.why}`);
152
- }
147
+ const r = await stateStore.write('.keep', '{"keep":true}\n', 'application/json');
148
+ if (!r.ok) throw new Error(`private store ${stateStore.base}.keep → ${r.why}`);
153
149
  this.log(`private state lives at ${cred.privateRoot} (${stateStore.kind})`);
154
- } else {
155
- await this.remote.putJson(this.urls.fediverse + '.keep', { keep: true }, 'application/json');
156
- await this.remote.setAcl(this.urls.fediverse, []);
157
150
  }
158
151
  this.store.attach(stateStore);
159
152
  // Re-running setup must not destroy state set afterwards (the UI
@@ -314,7 +307,6 @@ export class Agent {
314
307
  const { rotateKeyOnce, ...rest } = cred;
315
308
  writeJsonAtomic(path.join(this.home, 'credential.json'), rest);
316
309
  }
317
- this.local = new PodRdf({ storage: this.privateStorage(cred, 'fediverse') });
318
310
  // connect() can run more than once — connectWithRetry retries after a throw,
319
311
  // and the CLI connects for real after its confirmation. Deliverer arms a
320
312
  // 60s queue timer in its constructor and Intake owns a poll timer and a
@@ -332,13 +324,10 @@ export class Agent {
332
324
  log: this.log, passive: this.viewer,
333
325
  });
334
326
  this.publisher = new Publisher({
335
- config, remote: this.remote, local: this.local, store: this.store,
327
+ config, remote: this.remote, store: this.store,
336
328
  deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem,
337
329
  assertionKey: keys.edPublicMultibase, log: this.log,
338
330
  resolveMention: (h) => resolveHandle(this, h),
339
- // Whether the fediverse tree is on the pod at all, so the ACL check does
340
- // not probe for something the default layout keeps on local disk.
341
- privateOnPod: !cred.privateRoot,
342
331
  // Inside a pod server the client surface answers on the pod's own
343
332
  // origin, so it can be advertised. Standalone it is on loopback, and
344
333
  // naming it in a world-readable actor would send clients nowhere.
@@ -347,7 +336,7 @@ export class Agent {
347
336
  // Intake is constructed even for viewers — its signed fetchAP powers
348
337
  // search/deref; start() (draining) is active-only.
349
338
  this.intake = new Intake({
350
- config, urls: this.urls, remote: this.remote, local: this.local, store: this.store,
339
+ config, urls: this.urls, remote: this.remote, store: this.store,
351
340
  deliverer: this.deliverer, publisher: this.publisher, log: this.log, lease: this.lease,
352
341
  archive: this.privateStorage(cred, 'archive'),
353
342
  push: !this.embedded, pollSeconds: this.pollSeconds || null,
@@ -581,7 +570,7 @@ export class Agent {
581
570
  this.ensureActorPublished()
582
571
  .catch(e => this.log(`actor check failed: ${e.message}`));
583
572
  }
584
- this.backfillStatuses().catch(e => this.log(`statuses backfill failed: ${e.message}`));
573
+ this.seedFollowNotifications().catch(e => this.log(`notification seeding failed: ${e.message}`));
585
574
  }
586
575
 
587
576
  // Re-read state before acting on what we hold. A viewer's cache is kept
@@ -695,27 +684,13 @@ export class Agent {
695
684
  return true;
696
685
  }
697
686
 
698
- // The statuses index is an operational mirror of the pod's RDF — rebuild it
699
- // from /fediverse/ when absent (fresh state, or state loss).
700
- async backfillStatuses() {
701
- if (!this.store.has('notifications.json')) {
702
- for (const f of this.store.getContacts().followers) {
703
- this.store.addNotification({ type: 'follow', actor: f.actor });
704
- }
705
- }
706
- if (this.store.has('statuses.json')) return;
707
- const entries = [];
708
- for (const [container, kind] of [['timeline', 'timeline'], ['posts', 'post']]) {
709
- for (const url of await this.local.listNotes(container)) {
710
- try {
711
- const n = await this.local.readNote(url);
712
- if (n.noteId) entries.push({ ...n, kind, slug: url.split('/').pop() });
713
- } catch (e) { this.log(`backfill: skipped ${url}: ${e.message}`); }
714
- }
687
+ // A first run inherits followers without ever having seen them arrive, so
688
+ // the notifications list starts empty where it should start full.
689
+ async seedFollowNotifications() {
690
+ if (this.store.has('notifications.json')) return;
691
+ for (const f of this.store.getContacts().followers) {
692
+ this.store.addNotification({ type: 'follow', actor: f.actor });
715
693
  }
716
- entries.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
717
- this.store.write('statuses.json', entries.slice(0, 1000));
718
- this.log(`backfilled ${entries.length} statuses from pod RDF`);
719
694
  }
720
695
  }
721
696
 
@@ -0,0 +1,64 @@
1
+ // build-app.mjs — bundle the in-browser agent (and its pieces) with esbuild.
2
+ //
3
+ // The agent's brain is lib/, written for Node. This bundles it for a browser by
4
+ // swapping the Node-only edges for shims (web/app/shims/) and stubbing the Node
5
+ // modules the browser agent never uses (a local HTTP server, the filesystem,
6
+ // DNS pinning). A stubbed module that is actually called throws a named error,
7
+ // so a missing port shows up loudly rather than as silent wrong behaviour.
8
+ //
9
+ // node scripts/build-app.mjs [--entry <file>] [--out <file>]
10
+ import esbuild from 'esbuild';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
+ const shim = (p) => path.join(root, 'web/app/shims', p);
17
+
18
+ // Node modules the browser agent does not use. Stubbed to an empty module so an
19
+ // unrelated transitive import does not fail the build; anything that truly needs
20
+ // one will fail at runtime with a clear message, which is the signal to port it.
21
+ const STUBBED = ['node:fs/promises', 'node:os', 'node:net', 'node:http',
22
+ 'node:https', 'node:dns', 'node:dns/promises', 'node:child_process', 'node:tls',
23
+ 'undici', 'node-forge'];
24
+
25
+ const aliasPlugin = {
26
+ name: 'fedipod-agent-aliases',
27
+ setup(build) {
28
+ build.onResolve({ filter: /^@fedify\/fedify\/sig$/ }, () => ({ path: shim('fedify-sig.mjs') }));
29
+ build.onResolve({ filter: /^node:crypto$/ }, () => ({ path: shim('node-crypto.mjs') }));
30
+ build.onResolve({ filter: /^node:path$/ }, () => ({ path: shim('node-path.mjs') }));
31
+ build.onResolve({ filter: /^node:url$/ }, () => ({ path: shim('node-url.mjs') }));
32
+ build.onResolve({ filter: /^node:fs$/ }, () => ({ path: shim('node-fs.mjs') }));
33
+ build.onResolve({ filter: /^web-push$/ }, () => ({ path: shim('web-push.mjs') }));
34
+ build.onResolve({ filter: /\/safefetch\.mjs$/ }, () => ({ path: shim('safefetch.mjs') }));
35
+ const stub = new RegExp(`^(${STUBBED.map((s) => s.replace('/', '\\/')).join('|')})$`);
36
+ build.onResolve({ filter: stub }, (args) => ({ path: args.path, namespace: 'stub' }));
37
+ build.onLoad({ filter: /.*/, namespace: 'stub' }, (args) => ({
38
+ contents: `const fail = () => { throw new Error(${JSON.stringify(args.path)} + ' is not available in the browser agent'); };`
39
+ + 'export default new Proxy({}, { get: () => fail });',
40
+ loader: 'js',
41
+ }));
42
+ },
43
+ };
44
+
45
+ export async function buildApp({ entry, out, minify = false, format = 'esm' } = {}) {
46
+ const result = await esbuild.build({
47
+ entryPoints: [entry],
48
+ bundle: true, format, platform: 'browser', target: 'es2022',
49
+ outfile: out, sourcemap: true, minify, logLevel: 'silent', metafile: true,
50
+ plugins: [aliasPlugin],
51
+ define: { 'process.env.NODE_ENV': '"production"' },
52
+ banner: { js: fs.readFileSync(shim('prelude.js'), 'utf8') },
53
+ });
54
+ return { out, bytes: Object.values(result.metafile.outputs).reduce((n, o) => n + o.bytes, 0) };
55
+ }
56
+
57
+ if (import.meta.url === `file://${process.argv[1]}`) {
58
+ const args = process.argv.slice(2);
59
+ const entry = args[args.indexOf('--entry') + 1] || path.join(root, 'web/app/agent.mjs');
60
+ const out = args[args.indexOf('--out') + 1] || path.join(root, 'web/app/dist/agent.js');
61
+ const format = args.includes('--format') ? args[args.indexOf('--format') + 1] : 'esm';
62
+ const { bytes } = await buildApp({ entry, out, format });
63
+ console.log(`built ${path.relative(root, out)} — ${Math.round(bytes / 1024)} KB`);
64
+ }
@@ -0,0 +1,177 @@
1
+ // check-pod-calls.mjs — keeps the pod library a library, and keeps pod I/O
2
+ // inside it.
3
+ //
4
+ // Two jobs. The first is that the layer does not erode: once every pod request
5
+ // goes through a named operation, one `remote.putJson(...)` added back at a
6
+ // call site is invisible in review and undoes the property quietly. The second
7
+ // is that lib/pod/ stays EXTRACTABLE — no imports above itself, no Node
8
+ // built-ins, its one dependency confined — because "we could lift this out"
9
+ // stops being true the first time something reaches upward, and nobody notices
10
+ // until they try.
11
+ //
12
+ // Comments are stripped before every check. The rules are about code; the
13
+ // files explain themselves in prose, and prose that mentions node:dns or
14
+ // names the application it grew up in is not a violation.
15
+ //
16
+ // node scripts/check-pod-calls.mjs
17
+
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const POD = path.join(root, 'lib/pod');
24
+
25
+ let failures = 0;
26
+ const fail = (msg) => { console.log(`FAIL ${msg}`); failures++; };
27
+ const pass = (msg) => console.log(`PASS ${msg}`);
28
+
29
+ const read = (p) => fs.readFileSync(p, 'utf8');
30
+ // Prose is stripped LINE BY LINE, never with a regex spanning lines. A
31
+ // `/*...*/` matcher looks obvious and is not: a `*/` inside a regex literal or
32
+ // a string ends the match early, and everything up to the next one disappears —
33
+ // which silently deleted the very code these rules exist to look at, and left
34
+ // the check passing. Every comment in this project's style occupies whole
35
+ // lines, so dropping whole comment lines is both sufficient and safe.
36
+ const code = (src) => {
37
+ const out = [];
38
+ let inBlock = false;
39
+ for (const line of src.split('\n')) {
40
+ const t = line.trim();
41
+ if (inBlock) { if (t.endsWith('*/') || t === '*/') inBlock = false; continue; }
42
+ if (t.startsWith('/*')) { if (!t.includes('*/')) inBlock = true; continue; }
43
+ if (t.startsWith('//') || t.startsWith('*')) continue;
44
+ out.push(line);
45
+ }
46
+ return out.join('\n');
47
+ };
48
+
49
+ const podFiles = fs.readdirSync(POD).filter((f) => f.endsWith('.mjs'));
50
+
51
+ // ---- 1. the layer: pod I/O lives behind a named operation ----
52
+
53
+ // The files that run the browser agent and the gateway. Node-only code is
54
+ // deliberately out of scope for this pass and must not fail the check.
55
+ const IN_SCOPE = [
56
+ 'lib/publisher.mjs', 'lib/intake.mjs', 'lib/social.mjs', 'lib/mastoapi.mjs',
57
+ 'lib/gateway-core.mjs', 'lib/front-core.mjs',
58
+ 'web/app/agent.mjs', 'web/app/signup.mjs', 'web/app/boot.mjs',
59
+ 'web/app/admin-facade.mjs', 'web/app/keys-browser.mjs',
60
+ ];
61
+ const VERBS = /\b(?:remote|pod)\??\.(put|putJson|getJson|delete|setAcl|listContainer|aclUrlFor|aclWritable|patchDocument|linkAccountInProfile|aclDoc)\(/;
62
+
63
+ for (const rel of IN_SCOPE) {
64
+ const hits = code(read(path.join(root, rel))).split('\n')
65
+ .map((l, i) => [i + 1, l]).filter(([, l]) => VERBS.test(l));
66
+ if (hits.length) {
67
+ fail(`${rel} calls a transport verb directly — that belongs in a lib/pod/ operation`);
68
+ for (const [n, l] of hits.slice(0, 5)) console.log(` ${rel}:${n} ${l.trim()}`);
69
+ }
70
+ }
71
+ if (!failures) pass('every in-scope caller reaches the pod through a named operation');
72
+
73
+ // ---- 2. the deny-list cannot be routed around ----
74
+ {
75
+ const offenders = [];
76
+ const walk = (dir) => {
77
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
78
+ const p = path.join(dir, e.name);
79
+ if (e.isDirectory()) {
80
+ if (['node_modules', 'dist', 'site', '.git', 'claude', 'phanpy', 'ui', 'packages'].includes(e.name)) continue;
81
+ walk(p);
82
+ } else if (e.name.endsWith('.mjs') && p !== path.join(POD, 'transport.mjs')) {
83
+ // A DELETE is only a problem when it is sent on a RAW fetch. One sent
84
+ // through an injected fetcher is going through the transport — that is
85
+ // what `storage.mjs` does, and is the point of it being injected.
86
+ const src = code(read(p));
87
+ for (const m of src.matchAll(/method:\s*['"]DELETE['"]/g)) {
88
+ const before = src.slice(Math.max(0, m.index - 240), m.index);
89
+ const call = before.lastIndexOf('(');
90
+ const receiver = before.slice(Math.max(0, call - 60), call + 1);
91
+ if (/(?:^|[^.\w])(?:window\.)?fetch\($/.test(receiver) || /session\.fetch\($/.test(receiver)) {
92
+ offenders.push(`${path.relative(root, p)} (raw fetch)`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ };
98
+ walk(path.join(root, 'lib'));
99
+ walk(path.join(root, 'web/app'));
100
+ if (offenders.length) fail(`a DELETE is issued outside the transport, skipping the deny-list: ${offenders.join(', ')}`);
101
+ else pass('every DELETE goes through the transport, where the deny-list is');
102
+ }
103
+
104
+ // ---- 3. the signing key is never written without its ACL ----
105
+ {
106
+ const s = code(read(path.join(root, 'web/app/signup.mjs')));
107
+ if (/keys\.json[\s\S]{0,200}method:\s*['"]PUT['"]/.test(s) || /method:\s*['"]PUT['"][\s\S]{0,200}keys\.json/.test(s)) {
108
+ fail('signup writes keys.json directly — it must go through state.provisionKey, which sets the ACL first');
109
+ } else pass('the signing key is written only by the operation that locks the container first');
110
+ }
111
+
112
+ // ---- 4. lib/pod/ reaches nothing above itself ----
113
+ {
114
+ let bad = 0;
115
+ for (const f of podFiles) {
116
+ const src = code(read(path.join(POD, f)));
117
+ for (const m of src.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
118
+ const spec = m[1];
119
+ if (spec.startsWith('../')) { fail(`lib/pod/${f} imports ${spec} — the library may not reach above itself`); bad++; }
120
+ if (spec.startsWith('node:')) { fail(`lib/pod/${f} imports ${spec} — the library must run in a service worker`); bad++; }
121
+ }
122
+ }
123
+ if (!bad) pass('lib/pod/ imports nothing above itself and no Node built-ins');
124
+ }
125
+
126
+ // ---- 5. its one dependency stays confined ----
127
+ {
128
+ const rdflibbers = podFiles.filter((f) => /from\s+['"]rdflib['"]/.test(code(read(path.join(POD, f)))));
129
+ const allowed = ['transport.mjs', 'notifications.mjs'];
130
+ const extra = rdflibbers.filter((f) => !allowed.includes(f));
131
+ if (extra.length) fail(`rdflib reached from ${extra.join(', ')} — a resource module must not pull a parser in behind it`);
132
+ else pass(`rdflib is confined to ${allowed.join(' and ')}`);
133
+
134
+ const importsTransport = podFiles
135
+ .filter((f) => f !== 'transport.mjs')
136
+ .filter((f) => /from\s+['"]\.\/transport\.mjs['"]/.test(code(read(path.join(POD, f)))));
137
+ if (importsTransport.length) fail(`${importsTransport.join(', ')} imports the transport — operations receive one, they do not reach for it`);
138
+ else pass('no resource module imports or constructs a transport');
139
+ }
140
+
141
+ // ---- 6. it does not know what application it grew up in ----
142
+ {
143
+ const named = podFiles.filter((f) => /fedipod/i.test(code(read(path.join(POD, f)))));
144
+ if (named.length) fail(`lib/pod/${named.join(', ')} names FediPod in code — the library is meant to outlive it`);
145
+ else pass('no FediPod vocabulary in library code');
146
+ }
147
+
148
+ // ---- 7. and it actually runs on its own ----
149
+ {
150
+ const { apUrls } = await import(path.join(POD, 'urls.mjs'));
151
+ const { PodTransport, protectedFromDeletion } = await import(path.join(POD, 'transport.mjs'));
152
+ const urls = apUrls('https://p.example/', 'anything/');
153
+ let threw = false;
154
+ try { apUrls('https://p.example/'); } catch { threw = true; }
155
+ if (!threw) fail('apUrls guessed a container root instead of requiring one');
156
+
157
+ const seen = [];
158
+ const pod = new PodTransport(
159
+ { fetch: async (u, i) => { seen.push(`${i?.method || 'GET'} ${u}`); return { status: 200, headers: { get: () => null } }; } },
160
+ { webId: 'https://p.example/profile/card#me', role: 'agent', runtime: 'node' },
161
+ );
162
+ const inbox = await import(path.join(POD, 'inbox.mjs'));
163
+ await inbox.writeKeep(pod, urls);
164
+ await inbox.setPosture(pod, urls, 'open');
165
+ let refused = false;
166
+ try { await pod.delete('https://p.example/profile/card'); } catch { refused = true; }
167
+ if (!refused) fail('the deny-list did not refuse a DELETE of the WebID document');
168
+ if (!seen.some((r) => r.startsWith('PUT https://p.example/anything/ap/inbox/.keep'))) {
169
+ fail(`the inbox operations did not reach the pod as expected: ${seen.join(', ')}`);
170
+ }
171
+ if (pod.label !== 'agent/node') fail(`the transport did not label itself: ${pod.label}`);
172
+ if (typeof protectedFromDeletion !== 'function') fail('protectedFromDeletion is not exported');
173
+ pass('the library imports and runs against a stub with nothing but rdflib on the path');
174
+ }
175
+
176
+ console.log(failures ? `\n${failures} failure(s)` : '\npod layer intact');
177
+ process.exit(failures ? 1 : 0);