@geekity/cms 0.3.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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +870 -0
  3. package/admin/layouts/_flash.njk +5 -0
  4. package/admin/layouts/base.njk +27 -0
  5. package/admin/layouts/dashboard.njk +78 -0
  6. package/admin/layouts/document-conflict.njk +49 -0
  7. package/admin/layouts/document-editor.njk +167 -0
  8. package/admin/layouts/document-list.njk +92 -0
  9. package/admin/layouts/federation.njk +145 -0
  10. package/admin/layouts/login.njk +23 -0
  11. package/admin/layouts/placeholder.njk +10 -0
  12. package/admin/layouts/settings.njk +116 -0
  13. package/admin/layouts/setup.njk +27 -0
  14. package/admin/layouts/shell.njk +37 -0
  15. package/admin/layouts/users.njk +91 -0
  16. package/admin/static/admin.css +726 -0
  17. package/admin/static/editor.js +30 -0
  18. package/dist/admin/assets.d.ts +26 -0
  19. package/dist/admin/assets.d.ts.map +1 -0
  20. package/dist/admin/assets.js +38 -0
  21. package/dist/admin/assets.js.map +1 -0
  22. package/dist/admin/credentials.d.ts +28 -0
  23. package/dist/admin/credentials.d.ts.map +1 -0
  24. package/dist/admin/credentials.js +39 -0
  25. package/dist/admin/credentials.js.map +1 -0
  26. package/dist/admin/documents.d.ts +134 -0
  27. package/dist/admin/documents.d.ts.map +1 -0
  28. package/dist/admin/documents.js +613 -0
  29. package/dist/admin/documents.js.map +1 -0
  30. package/dist/admin/federation.d.ts +194 -0
  31. package/dist/admin/federation.d.ts.map +1 -0
  32. package/dist/admin/federation.js +299 -0
  33. package/dist/admin/federation.js.map +1 -0
  34. package/dist/admin/flash.d.ts +24 -0
  35. package/dist/admin/flash.d.ts.map +1 -0
  36. package/dist/admin/flash.js +31 -0
  37. package/dist/admin/flash.js.map +1 -0
  38. package/dist/admin/index.d.ts +23 -0
  39. package/dist/admin/index.d.ts.map +1 -0
  40. package/dist/admin/index.js +15 -0
  41. package/dist/admin/index.js.map +1 -0
  42. package/dist/admin/passwords.d.ts +49 -0
  43. package/dist/admin/passwords.d.ts.map +1 -0
  44. package/dist/admin/passwords.js +122 -0
  45. package/dist/admin/passwords.js.map +1 -0
  46. package/dist/admin/preview.d.ts +18 -0
  47. package/dist/admin/preview.d.ts.map +1 -0
  48. package/dist/admin/preview.js +83 -0
  49. package/dist/admin/preview.js.map +1 -0
  50. package/dist/admin/routes.d.ts +54 -0
  51. package/dist/admin/routes.d.ts.map +1 -0
  52. package/dist/admin/routes.js +383 -0
  53. package/dist/admin/routes.js.map +1 -0
  54. package/dist/admin/session.d.ts +44 -0
  55. package/dist/admin/session.d.ts.map +1 -0
  56. package/dist/admin/session.js +69 -0
  57. package/dist/admin/session.js.map +1 -0
  58. package/dist/admin/settings.d.ts +194 -0
  59. package/dist/admin/settings.d.ts.map +1 -0
  60. package/dist/admin/settings.js +470 -0
  61. package/dist/admin/settings.js.map +1 -0
  62. package/dist/admin/store.d.ts +407 -0
  63. package/dist/admin/store.d.ts.map +1 -0
  64. package/dist/admin/store.js +704 -0
  65. package/dist/admin/store.js.map +1 -0
  66. package/dist/admin/templates.d.ts +36 -0
  67. package/dist/admin/templates.d.ts.map +1 -0
  68. package/dist/admin/templates.js +43 -0
  69. package/dist/admin/templates.js.map +1 -0
  70. package/dist/admin/uploads.d.ts +96 -0
  71. package/dist/admin/uploads.d.ts.map +1 -0
  72. package/dist/admin/uploads.js +179 -0
  73. package/dist/admin/uploads.js.map +1 -0
  74. package/dist/admin/users.d.ts +95 -0
  75. package/dist/admin/users.d.ts.map +1 -0
  76. package/dist/admin/users.js +233 -0
  77. package/dist/admin/users.js.map +1 -0
  78. package/dist/cli.d.ts +85 -0
  79. package/dist/cli.d.ts.map +1 -0
  80. package/dist/cli.js +424 -0
  81. package/dist/cli.js.map +1 -0
  82. package/dist/config.d.ts +172 -0
  83. package/dist/config.d.ts.map +1 -0
  84. package/dist/config.js +184 -0
  85. package/dist/config.js.map +1 -0
  86. package/dist/content/document.d.ts +66 -0
  87. package/dist/content/document.d.ts.map +1 -0
  88. package/dist/content/document.js +13 -0
  89. package/dist/content/document.js.map +1 -0
  90. package/dist/content/index.d.ts +17 -0
  91. package/dist/content/index.d.ts.map +1 -0
  92. package/dist/content/index.js +10 -0
  93. package/dist/content/index.js.map +1 -0
  94. package/dist/content/markdown.d.ts +3 -0
  95. package/dist/content/markdown.d.ts.map +1 -0
  96. package/dist/content/markdown.js +39 -0
  97. package/dist/content/markdown.js.map +1 -0
  98. package/dist/content/media.d.ts +65 -0
  99. package/dist/content/media.d.ts.map +1 -0
  100. package/dist/content/media.js +143 -0
  101. package/dist/content/media.js.map +1 -0
  102. package/dist/content/parser.d.ts +24 -0
  103. package/dist/content/parser.d.ts.map +1 -0
  104. package/dist/content/parser.js +165 -0
  105. package/dist/content/parser.js.map +1 -0
  106. package/dist/content/save.d.ts +60 -0
  107. package/dist/content/save.d.ts.map +1 -0
  108. package/dist/content/save.js +103 -0
  109. package/dist/content/save.js.map +1 -0
  110. package/dist/content/slug.d.ts +25 -0
  111. package/dist/content/slug.d.ts.map +1 -0
  112. package/dist/content/slug.js +75 -0
  113. package/dist/content/slug.js.map +1 -0
  114. package/dist/content/store.d.ts +133 -0
  115. package/dist/content/store.d.ts.map +1 -0
  116. package/dist/content/store.js +411 -0
  117. package/dist/content/store.js.map +1 -0
  118. package/dist/content/sync.d.ts +146 -0
  119. package/dist/content/sync.d.ts.map +1 -0
  120. package/dist/content/sync.js +421 -0
  121. package/dist/content/sync.js.map +1 -0
  122. package/dist/content/writer.d.ts +37 -0
  123. package/dist/content/writer.d.ts.map +1 -0
  124. package/dist/content/writer.js +97 -0
  125. package/dist/content/writer.js.map +1 -0
  126. package/dist/env.d.ts +48 -0
  127. package/dist/env.d.ts.map +1 -0
  128. package/dist/env.js +2 -0
  129. package/dist/env.js.map +1 -0
  130. package/dist/federation/actor.d.ts +55 -0
  131. package/dist/federation/actor.d.ts.map +1 -0
  132. package/dist/federation/actor.js +101 -0
  133. package/dist/federation/actor.js.map +1 -0
  134. package/dist/federation/article.d.ts +83 -0
  135. package/dist/federation/article.d.ts.map +1 -0
  136. package/dist/federation/article.js +169 -0
  137. package/dist/federation/article.js.map +1 -0
  138. package/dist/federation/delivery.d.ts +90 -0
  139. package/dist/federation/delivery.d.ts.map +1 -0
  140. package/dist/federation/delivery.js +289 -0
  141. package/dist/federation/delivery.js.map +1 -0
  142. package/dist/federation/federation.d.ts +75 -0
  143. package/dist/federation/federation.d.ts.map +1 -0
  144. package/dist/federation/federation.js +190 -0
  145. package/dist/federation/federation.js.map +1 -0
  146. package/dist/federation/followers.d.ts +43 -0
  147. package/dist/federation/followers.d.ts.map +1 -0
  148. package/dist/federation/followers.js +69 -0
  149. package/dist/federation/followers.js.map +1 -0
  150. package/dist/federation/inbox.d.ts +68 -0
  151. package/dist/federation/inbox.d.ts.map +1 -0
  152. package/dist/federation/inbox.js +171 -0
  153. package/dist/federation/inbox.js.map +1 -0
  154. package/dist/federation/index.d.ts +14 -0
  155. package/dist/federation/index.d.ts.map +1 -0
  156. package/dist/federation/index.js +10 -0
  157. package/dist/federation/index.js.map +1 -0
  158. package/dist/federation/keys.d.ts +27 -0
  159. package/dist/federation/keys.d.ts.map +1 -0
  160. package/dist/federation/keys.js +60 -0
  161. package/dist/federation/keys.js.map +1 -0
  162. package/dist/federation/mount.d.ts +23 -0
  163. package/dist/federation/mount.d.ts.map +1 -0
  164. package/dist/federation/mount.js +71 -0
  165. package/dist/federation/mount.js.map +1 -0
  166. package/dist/federation/paths.d.ts +85 -0
  167. package/dist/federation/paths.d.ts.map +1 -0
  168. package/dist/federation/paths.js +97 -0
  169. package/dist/federation/paths.js.map +1 -0
  170. package/dist/index.d.ts +106 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +148 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/init.d.ts +71 -0
  175. package/dist/init.d.ts.map +1 -0
  176. package/dist/init.js +125 -0
  177. package/dist/init.js.map +1 -0
  178. package/dist/web/assets.d.ts +65 -0
  179. package/dist/web/assets.d.ts.map +1 -0
  180. package/dist/web/assets.js +152 -0
  181. package/dist/web/assets.js.map +1 -0
  182. package/dist/web/context.d.ts +121 -0
  183. package/dist/web/context.d.ts.map +1 -0
  184. package/dist/web/context.js +112 -0
  185. package/dist/web/context.js.map +1 -0
  186. package/dist/web/documents.d.ts +28 -0
  187. package/dist/web/documents.d.ts.map +1 -0
  188. package/dist/web/documents.js +39 -0
  189. package/dist/web/documents.js.map +1 -0
  190. package/dist/web/feeds.d.ts +122 -0
  191. package/dist/web/feeds.d.ts.map +1 -0
  192. package/dist/web/feeds.js +229 -0
  193. package/dist/web/feeds.js.map +1 -0
  194. package/dist/web/index.d.ts +17 -0
  195. package/dist/web/index.d.ts.map +1 -0
  196. package/dist/web/index.js +10 -0
  197. package/dist/web/index.js.map +1 -0
  198. package/dist/web/negotiate.d.ts +219 -0
  199. package/dist/web/negotiate.d.ts.map +1 -0
  200. package/dist/web/negotiate.js +409 -0
  201. package/dist/web/negotiate.js.map +1 -0
  202. package/dist/web/pagination.d.ts +48 -0
  203. package/dist/web/pagination.d.ts.map +1 -0
  204. package/dist/web/pagination.js +30 -0
  205. package/dist/web/pagination.js.map +1 -0
  206. package/dist/web/render.d.ts +72 -0
  207. package/dist/web/render.d.ts.map +1 -0
  208. package/dist/web/render.js +73 -0
  209. package/dist/web/render.js.map +1 -0
  210. package/dist/web/routes.d.ts +31 -0
  211. package/dist/web/routes.d.ts.map +1 -0
  212. package/dist/web/routes.js +366 -0
  213. package/dist/web/routes.js.map +1 -0
  214. package/dist/web/templates.d.ts +34 -0
  215. package/dist/web/templates.d.ts.map +1 -0
  216. package/dist/web/templates.js +106 -0
  217. package/dist/web/templates.js.map +1 -0
  218. package/package.json +68 -0
  219. package/templates/site/content/_data/site.json +8 -0
  220. package/templates/site/content/pages/about.md +11 -0
  221. package/templates/site/content/pages/pages.json +3 -0
  222. package/templates/site/content/posts/2026-01-01-hello-world.md +34 -0
  223. package/templates/site/content/posts/posts.json +4 -0
  224. package/templates/site/geekity.config.ts +11 -0
  225. package/templates/site/gitignore +3 -0
  226. package/templates/site/server.ts +20 -0
  227. package/templates/site/tsconfig.json +18 -0
  228. package/themes/default/README.md +148 -0
  229. package/themes/default/layouts/404.njk +9 -0
  230. package/themes/default/layouts/base.njk +59 -0
  231. package/themes/default/layouts/home.njk +9 -0
  232. package/themes/default/layouts/page.njk +13 -0
  233. package/themes/default/layouts/post.njk +30 -0
  234. package/themes/default/layouts/tag.njk +19 -0
  235. package/themes/default/partials/pagination.njk +14 -0
  236. package/themes/default/partials/post-list.njk +24 -0
  237. package/themes/default/partials/tags.njk +12 -0
  238. package/themes/default/static/style.css +327 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * How many followers one page of the collection holds.
3
+ *
4
+ * A constant for the same reason the outbox's is: a page URL is something a
5
+ * peer may come back to, and deriving the size from a setting would move every
6
+ * page boundary the moment somebody changed it.
7
+ */
8
+ export const FOLLOWERS_PAGE_SIZE = 20;
9
+ /**
10
+ * A stored follower as the {@link Recipient} Fedify delivers to.
11
+ *
12
+ * The shared inbox is offered when the follower published one, because that is
13
+ * what lets a post addressed to a hundred followers on one instance become one
14
+ * POST instead of a hundred. Fedify renders a recipient in the collection as
15
+ * its `id` alone, so the inbox columns cost the collection nothing and are
16
+ * what `ctx.sendActivity(…, 'followers', …)` needs.
17
+ */
18
+ export function followerRecipient(follower) {
19
+ return {
20
+ id: new URL(follower.actorId),
21
+ inboxId: new URL(follower.inboxId),
22
+ endpoints: {
23
+ sharedInbox: follower.sharedInboxId === null ? null : new URL(follower.sharedInboxId),
24
+ },
25
+ };
26
+ }
27
+ /**
28
+ * The followers collection, newest follow first: one page of it, or all of it.
29
+ *
30
+ * A cursor is the offset into the list as a decimal string, exactly as the
31
+ * outbox's is, so the two collections page the same way and a cursor keeps
32
+ * meaning what it meant when it was minted.
33
+ *
34
+ * A `null` cursor is not the first page: it is Fedify asking for the whole
35
+ * collection at once, which is what it does before fanning an activity out to
36
+ * the followers. Answering it with a page would strand every follower past the
37
+ * first one silently, so this answers with all of them. The collection a peer
38
+ * reads never arrives here with `null`, because the first cursor is registered
39
+ * and a registered first cursor is what makes the collection paged.
40
+ */
41
+ export function followersPage(context, cursor) {
42
+ const { admin } = context.data;
43
+ if (cursor === null) {
44
+ return { items: admin.listFollowers().map(followerRecipient) };
45
+ }
46
+ const offset = cursorOffset(cursor);
47
+ const followers = admin.listFollowers({ limit: FOLLOWERS_PAGE_SIZE, offset });
48
+ const total = admin.countFollowers();
49
+ const next = offset + FOLLOWERS_PAGE_SIZE;
50
+ return {
51
+ items: followers.map(followerRecipient),
52
+ nextCursor: next < total ? String(next) : null,
53
+ prevCursor: offset <= 0 ? null : String(Math.max(0, offset - FOLLOWERS_PAGE_SIZE)),
54
+ };
55
+ }
56
+ /**
57
+ * The cursor of the last page of the followers collection: the offset the
58
+ * final whole page starts at, and `0` for a site nobody follows yet.
59
+ */
60
+ export function lastFollowersCursor(total) {
61
+ return String(total === 0 ? 0 : Math.floor((total - 1) / FOLLOWERS_PAGE_SIZE) * FOLLOWERS_PAGE_SIZE);
62
+ }
63
+ /** A cursor as an offset. Anything unreadable starts at the beginning. */
64
+ function cursorOffset(cursor) {
65
+ if (cursor === null || !/^[0-9]+$/.test(cursor))
66
+ return 0;
67
+ return Number(cursor);
68
+ }
69
+ //# sourceMappingURL=followers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"followers.js","sourceRoot":"","sources":["../../src/federation/followers.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAkB;IAClD,OAAO;QACL,EAAE,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC;QAC7B,OAAO,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC;QAClC,SAAS,EAAE;YACT,WAAW,EAAE,QAAQ,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC;SACtF;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAuC,EACvC,MAAqB;IAErB,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAE/B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;IACjE,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,KAAK,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAG,KAAK,CAAC,cAAc,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,MAAM,GAAG,mBAAmB,CAAC;IAE1C,OAAO;QACL,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACvC,UAAU,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;QAC9C,UAAU,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC,CAAC;KACnF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,OAAO,MAAM,CACX,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,mBAAmB,CAAC,GAAG,mBAAmB,CACtF,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,SAAS,YAAY,CAAC,MAAqB;IACzC,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,CAAC;IAC1D,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { InboxContext } from '@fedify/fedify';
2
+ import { Follow } from '@fedify/vocab';
3
+ import type { Activity, Actor, Announce, Create, Delete, Like, Undo } from '@fedify/vocab';
4
+ import type { NewFollower } from '../admin/store.ts';
5
+ import type { FederationContextData } from './federation.ts';
6
+ /** What an inbox handler is handed: a Fedify context over the CMS's stores. */
7
+ export type SiteInboxContext = InboxContext<FederationContextData>;
8
+ /**
9
+ * Handle a `Follow`: store the follower and reply `Accept`.
10
+ *
11
+ * A `Follow` of anything but the site actor is ignored rather than refused —
12
+ * a peer that asked to follow a post has not done anything wrong, it has
13
+ * merely addressed something that does not accept followers. The reply goes
14
+ * to the follower's own inbox rather than through the followers collection,
15
+ * because at this moment the follow is not yet a fact for anybody but us.
16
+ *
17
+ * Following twice is a no-op beyond refreshing the stored profile: the store
18
+ * is keyed by actor id and keeps the original follow time, so a redelivered
19
+ * or repeated `Follow` cannot turn one follower into two.
20
+ */
21
+ export declare function handleFollow(context: SiteInboxContext, follow: Follow): Promise<void>;
22
+ /**
23
+ * Handle an `Undo`: an undone `Follow` removes the follower.
24
+ *
25
+ * Only the actor that sent the `Follow` may undo it. Without that check any
26
+ * signed actor could unfollow the site on somebody else's behalf, since the
27
+ * activity carrying the instruction is signed by its sender and says nothing
28
+ * about who the follow belonged to.
29
+ */
30
+ export declare function handleUndo(context: SiteInboxContext, undo: Undo): Promise<void>;
31
+ /**
32
+ * Handle a `Delete`: an actor deleting itself stops being a follower.
33
+ *
34
+ * The only `Delete` acted on is the one whose object is its own actor, which
35
+ * is how an instance announces that an account is gone. A `Delete` of a note
36
+ * or an article is logged and left alone; the site holds no copy of a remote
37
+ * object to remove.
38
+ */
39
+ export declare function handleDelete(context: SiteInboxContext, activity: Delete): Promise<void>;
40
+ /**
41
+ * Handle a `Like`, an `Announce` or a `Create`: record it and do nothing else.
42
+ *
43
+ * doc-4 keeps these so a later phase can surface likes, boosts and comments.
44
+ * Nothing about them is interpreted here, because what that phase will want
45
+ * out of an activity is not knowable yet — which is also why the row keeps the
46
+ * whole JSON-LD rather than a few columns of it.
47
+ */
48
+ export declare function handleLoggedActivity(context: SiteInboxContext, activity: Like | Announce | Create): Promise<void>;
49
+ /**
50
+ * Write one inbound activity to the log.
51
+ *
52
+ * Every handled activity goes through here, the follow traffic included, so
53
+ * the log is a complete record of what the inbox was told rather than of what
54
+ * the inbox ignored. An activity with no actor is dropped: Fedify has already
55
+ * refused anything whose signature does not match its actor, so an activity
56
+ * without one is malformed rather than anonymous.
57
+ */
58
+ export declare function logActivity(context: SiteInboxContext, activity: Activity): Promise<void>;
59
+ /**
60
+ * A remote actor as the follower row that describes it, or `undefined` when
61
+ * it is not deliverable.
62
+ *
63
+ * An actor with no id or no inbox is dropped rather than stored: a follower
64
+ * the site could never deliver to is a row that would only ever fail, and
65
+ * both are the minimum ActivityPub asks of an actor.
66
+ */
67
+ export declare function followerFrom(context: SiteInboxContext, actor: Actor): Promise<NewFollower | undefined>;
68
+ //# sourceMappingURL=inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/federation/inbox.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAU,MAAM,EAAa,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAQ,IAAI,EAAE,MAAM,eAAe,CAAC;AAEjG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG7D,+EAA+E;AAC/E,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B3F;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrF;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ7F;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,IAAI,GAAG,QAAQ,GAAG,MAAM,GACjC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9F;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAclC"}
@@ -0,0 +1,171 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { Accept, Follow, getTypeId } from '@fedify/vocab';
3
+ import { SITE_ACTOR_IDENTIFIER } from './keys.js';
4
+ /**
5
+ * Handle a `Follow`: store the follower and reply `Accept`.
6
+ *
7
+ * A `Follow` of anything but the site actor is ignored rather than refused —
8
+ * a peer that asked to follow a post has not done anything wrong, it has
9
+ * merely addressed something that does not accept followers. The reply goes
10
+ * to the follower's own inbox rather than through the followers collection,
11
+ * because at this moment the follow is not yet a fact for anybody but us.
12
+ *
13
+ * Following twice is a no-op beyond refreshing the stored profile: the store
14
+ * is keyed by actor id and keeps the original follow time, so a redelivered
15
+ * or repeated `Follow` cannot turn one follower into two.
16
+ */
17
+ export async function handleFollow(context, follow) {
18
+ await logActivity(context, follow);
19
+ if (follow.id === null || follow.objectId === null)
20
+ return;
21
+ // `parseUri` is what decides whether the object is this actor: comparing
22
+ // strings would have to know how Fedify spells the actor's URL, and this
23
+ // asks Fedify instead.
24
+ const parsed = context.parseUri(follow.objectId);
25
+ if (parsed?.type !== 'actor' || parsed.identifier !== SITE_ACTOR_IDENTIFIER)
26
+ return;
27
+ const actor = await follow.getActor(dereference(context));
28
+ if (actor === null)
29
+ return;
30
+ const follower = await followerFrom(context, actor);
31
+ if (follower === undefined)
32
+ return;
33
+ context.data.admin.putFollower(follower);
34
+ await context.sendActivity({ identifier: SITE_ACTOR_IDENTIFIER }, actor, new Accept({
35
+ // A fresh id every time: an actor may follow, unfollow and follow again,
36
+ // and those are three activities rather than one repeated.
37
+ id: new URL(`#accept/${randomUUID()}`, context.getActorUri(SITE_ACTOR_IDENTIFIER)),
38
+ actor: follow.objectId,
39
+ object: follow,
40
+ }));
41
+ }
42
+ /**
43
+ * Handle an `Undo`: an undone `Follow` removes the follower.
44
+ *
45
+ * Only the actor that sent the `Follow` may undo it. Without that check any
46
+ * signed actor could unfollow the site on somebody else's behalf, since the
47
+ * activity carrying the instruction is signed by its sender and says nothing
48
+ * about who the follow belonged to.
49
+ */
50
+ export async function handleUndo(context, undo) {
51
+ await logActivity(context, undo);
52
+ const object = await undo.getObject(dereference(context));
53
+ if (!(object instanceof Follow))
54
+ return;
55
+ const undoer = undo.actorId;
56
+ const follower = object.actorId;
57
+ if (undoer === null || follower === null || undoer.href !== follower.href)
58
+ return;
59
+ context.data.admin.deleteFollower(follower.href);
60
+ }
61
+ /**
62
+ * Handle a `Delete`: an actor deleting itself stops being a follower.
63
+ *
64
+ * The only `Delete` acted on is the one whose object is its own actor, which
65
+ * is how an instance announces that an account is gone. A `Delete` of a note
66
+ * or an article is logged and left alone; the site holds no copy of a remote
67
+ * object to remove.
68
+ */
69
+ export async function handleDelete(context, activity) {
70
+ await logActivity(context, activity);
71
+ const actor = activity.actorId;
72
+ const object = activity.objectId;
73
+ if (actor === null || object === null || actor.href !== object.href)
74
+ return;
75
+ context.data.admin.deleteFollower(object.href);
76
+ }
77
+ /**
78
+ * Handle a `Like`, an `Announce` or a `Create`: record it and do nothing else.
79
+ *
80
+ * doc-4 keeps these so a later phase can surface likes, boosts and comments.
81
+ * Nothing about them is interpreted here, because what that phase will want
82
+ * out of an activity is not knowable yet — which is also why the row keeps the
83
+ * whole JSON-LD rather than a few columns of it.
84
+ */
85
+ export async function handleLoggedActivity(context, activity) {
86
+ await logActivity(context, activity);
87
+ }
88
+ /**
89
+ * Write one inbound activity to the log.
90
+ *
91
+ * Every handled activity goes through here, the follow traffic included, so
92
+ * the log is a complete record of what the inbox was told rather than of what
93
+ * the inbox ignored. An activity with no actor is dropped: Fedify has already
94
+ * refused anything whose signature does not match its actor, so an activity
95
+ * without one is malformed rather than anonymous.
96
+ */
97
+ export async function logActivity(context, activity) {
98
+ const actorId = activity.actorId;
99
+ if (actorId === null)
100
+ return;
101
+ const json = await activity.toJsonLd({
102
+ format: 'compact',
103
+ contextLoader: context.contextLoader,
104
+ });
105
+ context.data.admin.logInboxActivity({
106
+ activityId: activity.id?.href ?? null,
107
+ activityType: activityTypeName(activity),
108
+ actorId: actorId.href,
109
+ objectId: activity.objectId?.href ?? null,
110
+ json: JSON.stringify(json),
111
+ });
112
+ }
113
+ /**
114
+ * A remote actor as the follower row that describes it, or `undefined` when
115
+ * it is not deliverable.
116
+ *
117
+ * An actor with no id or no inbox is dropped rather than stored: a follower
118
+ * the site could never deliver to is a row that would only ever fail, and
119
+ * both are the minimum ActivityPub asks of an actor.
120
+ */
121
+ export async function followerFrom(context, actor) {
122
+ if (actor.id === null || actor.inboxId === null)
123
+ return undefined;
124
+ const icon = await actor.getIcon({ ...dereference(context), suppressError: true });
125
+ return {
126
+ actorId: actor.id.href,
127
+ inboxId: actor.inboxId.href,
128
+ sharedInboxId: actor.endpoints?.sharedInbox?.href ?? null,
129
+ handle: actorHandle(actor),
130
+ name: actor.name === null ? null : actor.name.toString(),
131
+ iconUrl: linkHref(icon?.url ?? null) ?? actor.iconId?.href ?? null,
132
+ url: linkHref(actor.url) ?? null,
133
+ };
134
+ }
135
+ /**
136
+ * `@name@host` for an actor, built from what the actor document says rather
137
+ * than from WebFinger.
138
+ *
139
+ * A canonical handle would take a WebFinger round trip per follow to confirm
140
+ * the host, and this is display data: the id is what identifies the follower,
141
+ * and the id is what every other part of the system keys on.
142
+ */
143
+ function actorHandle(actor) {
144
+ const username = actor.preferredUsername;
145
+ if (username === null || actor.id === null)
146
+ return null;
147
+ return `@${username.toString()}@${actor.id.host}`;
148
+ }
149
+ /** A `URL` or a `Link` as a plain href, or `null`. */
150
+ function linkHref(value) {
151
+ if (value === null)
152
+ return null;
153
+ return value instanceof URL ? value.href : (value.href?.href ?? null);
154
+ }
155
+ /**
156
+ * The short ActivityStreams type name — `Follow`, `Like`, `Announce` — that the
157
+ * log stores.
158
+ *
159
+ * Taken from the type IRI rather than from the class name, which a bundler is
160
+ * free to rename, or from the JSON, which spells a type outside the core
161
+ * vocabulary as a full IRI.
162
+ */
163
+ function activityTypeName(activity) {
164
+ const typeId = getTypeId(activity);
165
+ return typeId.hash === '' ? typeId.href : typeId.hash.slice(1);
166
+ }
167
+ /** The loaders a vocabulary getter needs to follow a link off this context. */
168
+ function dereference(context) {
169
+ return { documentLoader: context.documentLoader, contextLoader: context.contextLoader };
170
+ }
171
+ //# sourceMappingURL=inbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/federation/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAK1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAKlD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAyB,EAAE,MAAc;IAC1E,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,EAAE,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO;IAE3D,yEAAyE;IACzE,yEAAyE;IACzE,uBAAuB;IACvB,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,MAAM,EAAE,IAAI,KAAK,OAAO,IAAI,MAAM,CAAC,UAAU,KAAK,qBAAqB;QAAE,OAAO;IAEpF,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO;IAC3B,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACpD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO;IAEnC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEzC,MAAM,OAAO,CAAC,YAAY,CACxB,EAAE,UAAU,EAAE,qBAAqB,EAAE,EACrC,KAAK,EACL,IAAI,MAAM,CAAC;QACT,yEAAyE;QACzE,2DAA2D;QAC3D,EAAE,EAAE,IAAI,GAAG,CAAC,WAAW,UAAU,EAAE,EAAE,EAAE,OAAO,CAAC,WAAW,CAAC,qBAAqB,CAAC,CAAC;QAClF,KAAK,EAAE,MAAM,CAAC,QAAQ;QACtB,MAAM,EAAE,MAAM;KACf,CAAC,CACH,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAyB,EAAE,IAAU;IACpE,MAAM,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAEjC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,IAAI,CAAC,CAAC,MAAM,YAAY,MAAM,CAAC;QAAE,OAAO;IAExC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;IAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC;IAChC,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI;QAAE,OAAO;IAElF,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAyB,EAAE,QAAgB;IAC5E,MAAM,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAErC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC;IAC/B,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC;IACjC,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI;QAAE,OAAO;IAE5E,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAyB,EACzB,QAAkC;IAElC,MAAM,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAyB,EAAE,QAAkB;IAC7E,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IACjC,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO;IAE7B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC;QACnC,MAAM,EAAE,SAAS;QACjB,aAAa,EAAE,OAAO,CAAC,aAAa;KACrC,CAAC,CAAC;IAEH,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC;QAClC,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,IAAI,IAAI;QACrC,YAAY,EAAE,gBAAgB,CAAC,QAAQ,CAAC;QACxC,OAAO,EAAE,OAAO,CAAC,IAAI;QACrB,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI;QACzC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAyB,EACzB,KAAY;IAEZ,IAAI,KAAK,CAAC,EAAE,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAElE,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAEnF,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI;QACtB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI;QAC3B,aAAa,EAAE,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI,IAAI;QACzD,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC;QAC1B,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE;QACxD,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI;QAClE,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI;KACjC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,WAAW,CAAC,KAAY;IAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,iBAAiB,CAAC;IACzC,IAAI,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,EAAE,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACxD,OAAO,IAAI,QAAQ,CAAC,QAAQ,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,sDAAsD;AACtD,SAAS,QAAQ,CAAC,KAAwB;IACxC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CAAC,QAAkB;IAC1C,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,+EAA+E;AAC/E,SAAS,WAAW,CAAC,OAAyB;IAI5C,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;AAC1F,CAAC"}
@@ -0,0 +1,14 @@
1
+ export { ACTOR_CLASSES, actorClassFor, avatarUrl, siteActor } from './actor.ts';
2
+ export type { SiteActorOptions } from './actor.ts';
3
+ export { articleObjectId, isFederatedDocument, postArticle, postCreateActivity, postDeleteActivity, postUpdateActivity, SOURCE_MEDIA_TYPE, toInstant, } from './article.ts';
4
+ export { createDeliveryService, groupByInbox } from './delivery.ts';
5
+ export type { CreateDeliveryServiceOptions, DeliveryLogger, DeliveryReport, DeliveryService, } from './delivery.ts';
6
+ export { createSiteFederation, federatedObject, federatedPost, OUTBOX_PAGE_SIZE, SOFTWARE_NAME, } from './federation.ts';
7
+ export type { CreateSiteFederationOptions, FederationContextData, SiteFederation, } from './federation.ts';
8
+ export { followerRecipient, followersPage, FOLLOWERS_PAGE_SIZE, lastFollowersCursor, } from './followers.ts';
9
+ export { followerFrom, handleDelete, handleFollow, handleLoggedActivity, handleUndo, logActivity, } from './inbox.ts';
10
+ export type { SiteInboxContext } from './inbox.ts';
11
+ export { loadActorKeyPairs, SITE_ACTOR_IDENTIFIER } from './keys.ts';
12
+ export { mountFederation } from './mount.ts';
13
+ export { ACTOR_PATH, createActivityId, deleteActivityId, FEDERATION_PREFIX, federationOrigin, FOLLOWERS_PATH, FOLLOWING_PATH, INBOX_PATH, NODEINFO_PATH, OUTBOX_PATH, POST_OBJECT_PATH, postObjectId, postObjectPath, SHARED_INBOX_PATH, updateActivityId, } from './paths.ts';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/federation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAChF,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACpE,YAAY,EACV,4BAA4B,EAC5B,cAAc,EACd,cAAc,EACd,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,UAAU,EACV,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ export { ACTOR_CLASSES, actorClassFor, avatarUrl, siteActor } from './actor.js';
2
+ export { articleObjectId, isFederatedDocument, postArticle, postCreateActivity, postDeleteActivity, postUpdateActivity, SOURCE_MEDIA_TYPE, toInstant, } from './article.js';
3
+ export { createDeliveryService, groupByInbox } from './delivery.js';
4
+ export { createSiteFederation, federatedObject, federatedPost, OUTBOX_PAGE_SIZE, SOFTWARE_NAME, } from './federation.js';
5
+ export { followerRecipient, followersPage, FOLLOWERS_PAGE_SIZE, lastFollowersCursor, } from './followers.js';
6
+ export { followerFrom, handleDelete, handleFollow, handleLoggedActivity, handleUndo, logActivity, } from './inbox.js';
7
+ export { loadActorKeyPairs, SITE_ACTOR_IDENTIFIER } from './keys.js';
8
+ export { mountFederation } from './mount.js';
9
+ export { ACTOR_PATH, createActivityId, deleteActivityId, FEDERATION_PREFIX, federationOrigin, FOLLOWERS_PATH, FOLLOWING_PATH, INBOX_PATH, NODEINFO_PATH, OUTBOX_PATH, POST_OBJECT_PATH, postObjectId, postObjectPath, SHARED_INBOX_PATH, updateActivityId, } from './paths.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/federation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEhF,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAOpE,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,UAAU,EACV,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { AdminStore } from '../admin/store.ts';
2
+ /**
3
+ * The site actor's internal identifier.
4
+ *
5
+ * Fedify keys actors by an identifier rather than by handle, and the two are
6
+ * deliberately not the same thing here: the handle is a setting somebody may
7
+ * change, while the identifier is what the actor's id, its inbox, its outbox
8
+ * and its stored keys are all built from. Renaming `@blog@example.com` to
9
+ * `@notes@example.com` therefore leaves every URL — and every follower — alone.
10
+ */
11
+ export declare const SITE_ACTOR_IDENTIFIER = "actor";
12
+ /**
13
+ * The actor's key pairs, generated on the first call and read from SQLite on
14
+ * every one after that.
15
+ *
16
+ * Both of {@link ACTOR_KEY_ALGORITHMS} are returned, in that order, which is
17
+ * the order Fedify wants them in: the RSA pair signs HTTP Signatures for
18
+ * Mastodon and friends, the Ed25519 pair signs FEP-8b32 integrity proofs.
19
+ *
20
+ * A stored pair that will not import — a half-written row, a key from a JWK
21
+ * shape that has since changed — is replaced rather than thrown, so a damaged
22
+ * table heals on the next boot instead of wedging every boot after it. That
23
+ * costs the site its old key for that algorithm, which is why the two are
24
+ * stored and healed independently.
25
+ */
26
+ export declare function loadActorKeyPairs(store: AdminStore, identifier: string): Promise<CryptoKeyPair[]>;
27
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/federation/keys.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,UAAU,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,EAAE,CAAC,CAY1B"}
@@ -0,0 +1,60 @@
1
+ import { exportJwk, generateCryptoKeyPair, importJwk } from '@fedify/fedify';
2
+ import { ACTOR_KEY_ALGORITHMS } from '../admin/store.js';
3
+ /**
4
+ * The site actor's internal identifier.
5
+ *
6
+ * Fedify keys actors by an identifier rather than by handle, and the two are
7
+ * deliberately not the same thing here: the handle is a setting somebody may
8
+ * change, while the identifier is what the actor's id, its inbox, its outbox
9
+ * and its stored keys are all built from. Renaming `@blog@example.com` to
10
+ * `@notes@example.com` therefore leaves every URL — and every follower — alone.
11
+ */
12
+ export const SITE_ACTOR_IDENTIFIER = 'actor';
13
+ /**
14
+ * The actor's key pairs, generated on the first call and read from SQLite on
15
+ * every one after that.
16
+ *
17
+ * Both of {@link ACTOR_KEY_ALGORITHMS} are returned, in that order, which is
18
+ * the order Fedify wants them in: the RSA pair signs HTTP Signatures for
19
+ * Mastodon and friends, the Ed25519 pair signs FEP-8b32 integrity proofs.
20
+ *
21
+ * A stored pair that will not import — a half-written row, a key from a JWK
22
+ * shape that has since changed — is replaced rather than thrown, so a damaged
23
+ * table heals on the next boot instead of wedging every boot after it. That
24
+ * costs the site its old key for that algorithm, which is why the two are
25
+ * stored and healed independently.
26
+ */
27
+ export async function loadActorKeyPairs(store, identifier) {
28
+ const stored = new Map(store.listActorKeys(identifier).map((key) => [key.algorithm, key]));
29
+ const pairs = [];
30
+ for (const algorithm of ACTOR_KEY_ALGORITHMS) {
31
+ const row = stored.get(algorithm);
32
+ const imported = row === undefined ? undefined : await importPair(row.privateJwk, row.publicJwk);
33
+ pairs.push(imported ?? (await generatePair(store, identifier, algorithm)));
34
+ }
35
+ return pairs;
36
+ }
37
+ /** A stored pair as `CryptoKey`s, or `undefined` when it will not import. */
38
+ async function importPair(privateJwk, publicJwk) {
39
+ try {
40
+ return {
41
+ privateKey: await importJwk(JSON.parse(privateJwk), 'private'),
42
+ publicKey: await importJwk(JSON.parse(publicJwk), 'public'),
43
+ };
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ /** A fresh pair, written to the store before it is handed back. */
50
+ async function generatePair(store, identifier, algorithm) {
51
+ const pair = await generateCryptoKeyPair(algorithm);
52
+ store.putActorKey({
53
+ identifier,
54
+ algorithm,
55
+ privateJwk: JSON.stringify(await exportJwk(pair.privateKey)),
56
+ publicJwk: JSON.stringify(await exportJwk(pair.publicKey)),
57
+ });
58
+ return pair;
59
+ }
60
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/federation/keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAiB,EACjB,UAAkB;IAElB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IAC3F,MAAM,KAAK,GAAoB,EAAE,CAAC;IAElC,KAAK,MAAM,SAAS,IAAI,oBAAoB,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAClC,MAAM,QAAQ,GACZ,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;QAClF,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,6EAA6E;AAC7E,KAAK,UAAU,UAAU,CACvB,UAAkB,EAClB,SAAiB;IAEjB,IAAI,CAAC;QACH,OAAO;YACL,UAAU,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAe,EAAE,SAAS,CAAC;YAC5E,SAAS,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAe,EAAE,QAAQ,CAAC;SAC1E,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,KAAK,UAAU,YAAY,CACzB,KAAiB,EACjB,UAAkB,EAClB,SAA4B;IAE5B,MAAM,IAAI,GAAG,MAAM,qBAAqB,CAAC,SAAS,CAAC,CAAC;IACpD,KAAK,CAAC,WAAW,CAAC;QAChB,UAAU;QACV,SAAS;QACT,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5D,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;KAC3D,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { Hono } from 'hono';
2
+ import type { GeekityEnv } from '../env.ts';
3
+ import type { SiteFederation } from './federation.ts';
4
+ /**
5
+ * Put Fedify in front of the rest of the app.
6
+ *
7
+ * The middleware only answers the paths Fedify registered — the actor, its
8
+ * collections, the post objects, WebFinger and NodeInfo; everything else falls
9
+ * through to `next()` and reaches the admin and the public site as before.
10
+ * That is why it is mounted first: the public site claims every unmatched path
11
+ * in its not-found handler, so anything registered after it never sees a
12
+ * request.
13
+ *
14
+ * A post permalink asked for as ActivityStreams is answered here too, by
15
+ * {@link activityStreamsDocument}, because Fedify only knows about the URLs it
16
+ * minted and a permalink is not one of them.
17
+ *
18
+ * The dispatchers are handed the stores off the Hono context rather than
19
+ * closing over them, so one federation object could serve more than one CMS
20
+ * and so a dispatcher is never reading a store the request did not come with.
21
+ */
22
+ export declare function mountFederation(app: Hono<GeekityEnv>, federation: SiteFederation): void;
23
+ //# sourceMappingURL=mount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../../src/federation/mount.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAW,IAAI,EAAqB,MAAM,MAAM,CAAC;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAK5C,OAAO,KAAK,EAAyB,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE7E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,UAAU,EAAE,cAAc,GAAG,IAAI,CAoBvF"}
@@ -0,0 +1,71 @@
1
+ import { respondWithObject } from '@fedify/fedify';
2
+ import { federation as federationMiddleware } from '@fedify/hono';
3
+ import { publicDocumentAt } from '../web/documents.js';
4
+ import { prefersActivityStreams } from '../web/negotiate.js';
5
+ import { requestPath } from '../web/routes.js';
6
+ import { isFederatedDocument, postArticle } from './article.js';
7
+ /**
8
+ * Put Fedify in front of the rest of the app.
9
+ *
10
+ * The middleware only answers the paths Fedify registered — the actor, its
11
+ * collections, the post objects, WebFinger and NodeInfo; everything else falls
12
+ * through to `next()` and reaches the admin and the public site as before.
13
+ * That is why it is mounted first: the public site claims every unmatched path
14
+ * in its not-found handler, so anything registered after it never sees a
15
+ * request.
16
+ *
17
+ * A post permalink asked for as ActivityStreams is answered here too, by
18
+ * {@link activityStreamsDocument}, because Fedify only knows about the URLs it
19
+ * minted and a permalink is not one of them.
20
+ *
21
+ * The dispatchers are handed the stores off the Hono context rather than
22
+ * closing over them, so one federation object could serve more than one CMS
23
+ * and so a dispatcher is never reading a store the request did not come with.
24
+ */
25
+ export function mountFederation(app, federation) {
26
+ // `@fedify/hono` types its context as the two properties it actually reads,
27
+ // which is looser than Hono's own; the cast is what puts `c.var` back.
28
+ app.use('*', federationMiddleware(federation, (context) => {
29
+ const c = context;
30
+ return contextData(c);
31
+ }));
32
+ // Named, and typed as a middleware, so the Hono context arrives with its
33
+ // path parameters resolved rather than as `any`.
34
+ const activityStreams = async (c, next) => {
35
+ const response = await activityStreamsDocument(c, federation);
36
+ if (response !== undefined)
37
+ return response;
38
+ await next();
39
+ };
40
+ app.use('*', activityStreams);
41
+ }
42
+ /**
43
+ * The `Article` for the post at this URL, when the request asked for
44
+ * ActivityStreams and the URL is a published post's permalink.
45
+ *
46
+ * doc-4 wants a peer that dereferences a link somebody shared — which is the
47
+ * permalink, not the object id — to land on the object rather than on a page
48
+ * it cannot read. The article is built through the same
49
+ * {@link postArticle} the object dispatcher uses, off a context Fedify makes
50
+ * for this request, so the two answers cannot drift apart; its `id` is the
51
+ * object URL either way, so a peer stores one object however it arrived.
52
+ *
53
+ * Anything else — a browser, a `.md` or `.json` request, a page, a listing —
54
+ * gets `undefined` and falls through to the public site untouched.
55
+ */
56
+ async function activityStreamsDocument(c, federation) {
57
+ if (!prefersActivityStreams(c.req.header('accept')))
58
+ return undefined;
59
+ const document = publicDocumentAt(c.var.store, requestPath(c));
60
+ if (document === undefined || !isFederatedDocument(document))
61
+ return undefined;
62
+ const context = federation.createContext(c.req.raw, contextData(c));
63
+ return await respondWithObject(postArticle(context, document), {
64
+ contextLoader: context.contextLoader,
65
+ });
66
+ }
67
+ /** What the dispatchers are handed, off the Hono context. */
68
+ function contextData(c) {
69
+ return { admin: c.var.admin, store: c.var.store, config: c.var.config };
70
+ }
71
+ //# sourceMappingURL=mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../../src/federation/mount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAIlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,eAAe,CAAC,GAAqB,EAAE,UAA0B;IAC/E,4EAA4E;IAC5E,uEAAuE;IACvE,GAAG,CAAC,GAAG,CACL,GAAG,EACH,oBAAoB,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE;QAC3C,MAAM,CAAC,GAAG,OAAyC,CAAC;QACpD,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CACH,CAAC;IAEF,yEAAyE;IACzE,iDAAiD;IACjD,MAAM,eAAe,GAAkC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvE,MAAM,QAAQ,GAAG,MAAM,uBAAuB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC9D,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC;QAC5C,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;IAEF,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,uBAAuB,CACpC,CAAsB,EACtB,UAA0B;IAE1B,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IAEtE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IAE/E,MAAM,OAAO,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,OAAO,MAAM,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE;QAC7D,aAAa,EAAE,OAAO,CAAC,aAAa;KACrC,CAAC,CAAC;AACL,CAAC;AAED,6DAA6D;AAC7D,SAAS,WAAW,CAAC,CAAsB;IACzC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;AAC1E,CAAC"}