@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,289 @@
1
+ import { Activity, getTypeId, PUBLIC_COLLECTION, Update } from '@fedify/vocab';
2
+ import { readSiteSettings } from '../admin/settings.js';
3
+ import { saveDocument } from '../content/save.js';
4
+ import { documentContent } from '../content/writer.js';
5
+ import { siteActor } from './actor.js';
6
+ import { articleObjectId, isFederatedDocument, postCreateActivity, postDeleteActivity, postUpdateActivity, } from './article.js';
7
+ import { followerRecipient } from './followers.js';
8
+ import { SITE_ACTOR_IDENTIFIER } from './keys.js';
9
+ import { postObjectId, updateActivityId } from './paths.js';
10
+ /**
11
+ * Build the delivery service for one site.
12
+ *
13
+ * doc-4's delivery table is the whole of the policy: a post that becomes
14
+ * published is a `Create`, an edit to a published post is an `Update`, and a
15
+ * post that stops being published — draft, trashed or deleted, which are one
16
+ * thing from outside — is a `Delete` of a `Tombstone`.
17
+ *
18
+ * Nothing is delivered for a full scan. The boot scan reports a cold index as
19
+ * a directory full of creations, so federating them would announce the whole
20
+ * archive to every follower every time somebody deleted the database.
21
+ */
22
+ export function createDeliveryService(options) {
23
+ const { federation, admin, store, config } = options;
24
+ const logger = options.logger ?? console;
25
+ /**
26
+ * Whether a delivery is over by the time `sendActivity` resolves.
27
+ *
28
+ * With no queue it is: Fedify posts to each inbox and waits. With one, all
29
+ * that is known is that the activity was accepted for delivery, and the
30
+ * queue retries out of band without reporting back — which is the difference
31
+ * between a `sent` row and a `queued` one.
32
+ */
33
+ const synchronous = config.federation.queue === null;
34
+ // Deliveries are chained rather than run in parallel, and the chain never
35
+ // rejects: a failure is recorded against the followers it happened to and
36
+ // the next activity still goes out.
37
+ let chain = Promise.resolve();
38
+ function enqueue(task) {
39
+ const result = chain.then(task);
40
+ chain = result.then(ignore, ignore);
41
+ return result;
42
+ }
43
+ /** A context outside any request, which is where a delivery happens from. */
44
+ function deliveryContext() {
45
+ return federation.createContext(new URL(config.baseUrl), { admin, store, config });
46
+ }
47
+ /**
48
+ * Write `activitypub.id` and `activitypub.published` into the post's file,
49
+ * and answer with the document as it now reads.
50
+ *
51
+ * This happens before the first activity goes out, so the id the follower is
52
+ * handed is the id the file will keep however often the post is renamed
53
+ * afterwards (doc-4). It is written through the same
54
+ * {@link saveDocument} the admin writes through, which corrects the index in
55
+ * the same breath — so the watcher's re-read of the file finds a hash that
56
+ * already matches and this write federates nothing of its own.
57
+ */
58
+ async function stamp(document) {
59
+ const existing = document.activitypub;
60
+ const id = existing?.id ?? postObjectId(document.slug, config.baseUrl);
61
+ const published = existing?.published ?? document.date ?? new Date().toISOString();
62
+ if (existing?.id === id && existing.published === published)
63
+ return document;
64
+ return await saveDocument({
65
+ contentDir: config.contentDir,
66
+ store,
67
+ path: document.path,
68
+ content: { ...documentContent(document), activitypub: { id, published } },
69
+ });
70
+ }
71
+ /**
72
+ * Record an activity and fan it out to the followers.
73
+ *
74
+ * The activity is stored before it is sent, not after, so an activity whose
75
+ * delivery is interrupted is still one an admin can re-send.
76
+ */
77
+ async function send(context, activity, about) {
78
+ const activityId = activity.id?.href;
79
+ if (activityId === undefined) {
80
+ throw new TypeError('An activity cannot be delivered without an id.');
81
+ }
82
+ const objectId = articleObjectId(context, about).href;
83
+ const activityType = typeNameOf(activity);
84
+ const json = await activity.toJsonLd({
85
+ format: 'compact',
86
+ contextLoader: context.contextLoader,
87
+ });
88
+ admin.putOutboundActivity({
89
+ activityId,
90
+ activityType,
91
+ objectId,
92
+ slug: about.slug,
93
+ json: JSON.stringify(json),
94
+ });
95
+ return await fanOut(context, activity, { activityId, activityType, objectId });
96
+ }
97
+ /**
98
+ * Deliver one activity to every follower, one inbox at a time.
99
+ *
100
+ * Fedify's own `sendActivity(…, 'followers', …)` would reach the same
101
+ * inboxes in one call, but it answers `void`: there would be no way of
102
+ * saying which follower did not get it, which is the thing decision-5
103
+ * promised to record. So the followers are grouped by the inbox they share —
104
+ * one POST still serves a whole instance — and each group's outcome is
105
+ * written against every follower behind it.
106
+ */
107
+ async function fanOut(context, activity, about) {
108
+ const deliveries = [];
109
+ for (const [inboxId, members] of groupByInbox(admin.listFollowers())) {
110
+ let status = synchronous ? 'sent' : 'queued';
111
+ let error = null;
112
+ try {
113
+ await context.sendActivity({ identifier: SITE_ACTOR_IDENTIFIER }, members.map(followerRecipient), activity,
114
+ // The object id keeps a post's activities in order per server, so a
115
+ // follower cannot be shown an Update of something it has not been
116
+ // told about yet.
117
+ { preferSharedInbox: true, orderingKey: about.objectId });
118
+ }
119
+ catch (thrown) {
120
+ status = 'failed';
121
+ error = messageOf(thrown);
122
+ logger.warn(`Could not deliver ${about.activityId} to ${inboxId}: ${error}`);
123
+ }
124
+ for (const member of members) {
125
+ deliveries.push(admin.recordDelivery({
126
+ activityId: about.activityId,
127
+ actorId: member.actorId,
128
+ inboxId,
129
+ status,
130
+ error,
131
+ }));
132
+ }
133
+ }
134
+ return { ...about, deliveries };
135
+ }
136
+ return {
137
+ async handle(change) {
138
+ // A full scan is a rebuild of the index, not news about the site.
139
+ if (change.origin === 'scan')
140
+ return;
141
+ const before = federatedOrUndefined(change.previous);
142
+ const after = federatedOrUndefined(change.next);
143
+ if (before === undefined && after === undefined)
144
+ return;
145
+ const context = deliveryContext();
146
+ if (after === undefined) {
147
+ // Unpublished, trashed or deleted; `before` is the post as it last
148
+ // stood, and the only version there is to build a Tombstone from.
149
+ if (before === undefined)
150
+ return;
151
+ const deleted = new Date().toISOString();
152
+ queue(() => send(context, postDeleteActivity(context, before, deleted), before));
153
+ return;
154
+ }
155
+ {
156
+ const stamped = await stamp(after);
157
+ if (before === undefined) {
158
+ queue(() => send(context, postCreateActivity(context, stamped), stamped));
159
+ return;
160
+ }
161
+ // Almost always the same id, because the stamped one follows the post
162
+ // through a rename. When it is not — a post federated before this
163
+ // version, renamed since — the old object is withdrawn rather than
164
+ // left standing under an id nothing answers to any more.
165
+ const previousId = articleObjectId(context, before).href;
166
+ if (previousId === articleObjectId(context, stamped).href) {
167
+ queue(() => send(context, postUpdateActivity(context, stamped), stamped));
168
+ return;
169
+ }
170
+ const deleted = new Date().toISOString();
171
+ queue(() => send(context, postDeleteActivity(context, before, deleted), before));
172
+ queue(() => send(context, postCreateActivity(context, stamped), stamped));
173
+ }
174
+ },
175
+ async updateActor() {
176
+ // Building the actor loads — and on a cold database generates — the key
177
+ // pairs, so a site nobody follows does not pay for an activity that has
178
+ // nowhere to go.
179
+ if (admin.countFollowers() === 0)
180
+ return undefined;
181
+ return await enqueue(async () => {
182
+ const context = deliveryContext();
183
+ const actorId = context.getActorUri(SITE_ACTOR_IDENTIFIER);
184
+ const actor = await siteActor(context, SITE_ACTOR_IDENTIFIER, {
185
+ settings: readSiteSettings(admin),
186
+ baseUrl: config.baseUrl,
187
+ });
188
+ // The revision is the moment rather than a hash of the profile: an
189
+ // avatar removed and put back is the same profile twice, and both
190
+ // times the followers have to be told rather than recognise an id
191
+ // they have already seen and skip it.
192
+ const activityId = updateActivityId(actorId, new Date().toISOString());
193
+ const activity = new Update({
194
+ id: activityId,
195
+ actor: actorId,
196
+ object: actor,
197
+ to: PUBLIC_COLLECTION,
198
+ cc: context.getFollowersUri(SITE_ACTOR_IDENTIFIER),
199
+ });
200
+ const about = {
201
+ activityId: activityId.href,
202
+ activityType: 'Update',
203
+ // The object is the actor, not a post, and there is no slug to
204
+ // record: that is what keeps an actor Update out of the federation
205
+ // screen's per-post delivery table.
206
+ objectId: actorId.href,
207
+ };
208
+ admin.putOutboundActivity({
209
+ ...about,
210
+ slug: null,
211
+ json: JSON.stringify(await activity.toJsonLd({ format: 'compact', contextLoader: context.contextLoader })),
212
+ });
213
+ return await fanOut(context, activity, about);
214
+ }).catch((thrown) => {
215
+ // A profile update is a side effect of saving the settings, and losing
216
+ // it must not lose the save.
217
+ logger.warn(`The actor update could not be delivered: ${messageOf(thrown)}`);
218
+ return undefined;
219
+ });
220
+ },
221
+ async redeliver(activityId) {
222
+ const stored = admin.getOutboundActivity(activityId);
223
+ if (stored === undefined)
224
+ return undefined;
225
+ return await enqueue(async () => {
226
+ const context = deliveryContext();
227
+ const activity = await Activity.fromJsonLd(JSON.parse(stored.json), {
228
+ contextLoader: context.contextLoader,
229
+ documentLoader: context.documentLoader,
230
+ });
231
+ return await fanOut(context, activity, {
232
+ activityId: stored.activityId,
233
+ activityType: stored.activityType,
234
+ objectId: stored.objectId,
235
+ });
236
+ });
237
+ },
238
+ settled() {
239
+ return chain.then(ignore);
240
+ },
241
+ };
242
+ /**
243
+ * Put one delivery on the queue and stop caring about its result.
244
+ *
245
+ * Nothing is waiting for a delivery that a change set off, so a rejection
246
+ * here has nowhere to go but the log; the per-follower failures are already
247
+ * in the database by the time one gets this far.
248
+ */
249
+ function queue(task) {
250
+ enqueue(task).catch((thrown) => {
251
+ logger.warn(`A delivery failed: ${messageOf(thrown)}`);
252
+ });
253
+ }
254
+ }
255
+ /**
256
+ * Followers keyed by the inbox one POST to which reaches them: the instance's
257
+ * shared inbox where it published one, and the follower's own where it did not.
258
+ */
259
+ export function groupByInbox(followers) {
260
+ const groups = new Map();
261
+ for (const follower of followers) {
262
+ const inbox = follower.sharedInboxId ?? follower.inboxId;
263
+ const group = groups.get(inbox);
264
+ if (group === undefined)
265
+ groups.set(inbox, [follower]);
266
+ else
267
+ group.push(follower);
268
+ }
269
+ return groups;
270
+ }
271
+ /** The document, when it is one this site federates, and `undefined` otherwise. */
272
+ function federatedOrUndefined(document) {
273
+ if (document === undefined)
274
+ return undefined;
275
+ return isFederatedDocument(document) ? document : undefined;
276
+ }
277
+ /** An activity's short type name — `Create`, `Update`, `Delete` — from its type URI. */
278
+ function typeNameOf(activity) {
279
+ const typeId = getTypeId(activity);
280
+ return typeId.hash === '' ? typeId.href : typeId.hash.slice(1);
281
+ }
282
+ function messageOf(error) {
283
+ return error instanceof Error ? error.message : String(error);
284
+ }
285
+ function ignore() {
286
+ // Deliberately empty: the chain must not reject, and a failure has already
287
+ // been recorded against the followers it happened to.
288
+ }
289
+ //# sourceMappingURL=delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.js","sourceRoot":"","sources":["../../src/federation/delivery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ import type { Federation, FederationOptions } from '@fedify/fedify';
2
+ import type { AdminStore } from '../admin/store.ts';
3
+ import type { FederationOverrides, ResolvedConfig } from '../config.ts';
4
+ import type { Document } from '../content/document.ts';
5
+ import type { ContentStore } from '../content/store.ts';
6
+ /** The `software.name` this CMS reports in NodeInfo. Lower case, as the schema demands. */
7
+ export declare const SOFTWARE_NAME = "geekity-cms";
8
+ /**
9
+ * How many activities one page of the outbox holds.
10
+ *
11
+ * A constant rather than the site's `postsPerPage`, because an outbox cursor
12
+ * is a URL a peer may come back to: changing the setting would silently move
13
+ * every page boundary underneath whoever is walking the collection.
14
+ */
15
+ export declare const OUTBOX_PAGE_SIZE = 20;
16
+ /** What every Fedify dispatcher in the CMS is handed. */
17
+ export interface FederationContextData {
18
+ /** Settings and, through them, the actor's profile and its keys. */
19
+ readonly admin: AdminStore;
20
+ /** The content index, which the outbox and the post objects are built from. */
21
+ readonly store: ContentStore;
22
+ /** Config after defaults and environment overrides. */
23
+ readonly config: ResolvedConfig;
24
+ }
25
+ /** A CMS federation object, with the context data its dispatchers expect. */
26
+ export type SiteFederation = Federation<FederationContextData>;
27
+ /**
28
+ * What {@link createSiteFederation} takes: the site's base URL, and the
29
+ * {@link FederationOverrides} a site may swap the defaults for.
30
+ */
31
+ export interface CreateSiteFederationOptions extends FederationOverrides {
32
+ /**
33
+ * The site's base URL. Fedify otherwise mints ids from `request.url`, which
34
+ * behind a proxy is the internal address rather than the one peers
35
+ * dereference, so this is the one option with no sensible default.
36
+ *
37
+ * Only its origin is used; see {@link federationOrigin}.
38
+ */
39
+ baseUrl: string;
40
+ /** Anything else Fedify takes, for a site that needs to reach past this factory. */
41
+ federationOptions?: Partial<FederationOptions<FederationContextData>> | undefined;
42
+ }
43
+ /**
44
+ * Build the site's `Federation` object: the actor, its keys, the post objects,
45
+ * the outbox, WebFinger and NodeInfo.
46
+ *
47
+ * It is a factory rather than a module-level singleton so the KV store and the
48
+ * queue are arguments (decision-5), so a test can build one per data directory,
49
+ * and so a site that outgrows the in-memory pair changes a call rather than
50
+ * this file. The followers come out of SQLite; the following collection is
51
+ * empty, and doc-4 says it always will be.
52
+ */
53
+ export declare function createSiteFederation(options: CreateSiteFederationOptions): SiteFederation;
54
+ /**
55
+ * The published post a slug names, or `undefined`.
56
+ *
57
+ * Slugs are not unique across years, and the index answers with the newest
58
+ * match. That is the same rule the admin's own slug lookup follows, so a
59
+ * collision resolves the same way wherever it is asked about.
60
+ */
61
+ export declare function federatedPost(store: ContentStore, slug: string): Document | undefined;
62
+ /**
63
+ * The post whose ActivityStreams object is served at `/ap/posts/{slug}`.
64
+ *
65
+ * Nearly always the post with that slug. The exception is a post renamed after
66
+ * it was announced: it keeps the id its old slug minted, written into
67
+ * `activitypub.id` (doc-4), so the old URL has to keep answering or every
68
+ * follower's copy points at a 404. That case is found by searching the
69
+ * archive for the post holding the id, which is a walk rather than an index
70
+ * lookup — the front matter is the source of truth (decision-1) and the index
71
+ * has no column for it — but it is only reached when the slug itself does not
72
+ * answer.
73
+ */
74
+ export declare function federatedObject(store: ContentStore, slug: string, baseUrl: string): Document | undefined;
75
+ //# sourceMappingURL=federation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../../src/federation/federation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAA6B,MAAM,gBAAgB,CAAC;AAI/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAmBxD,2FAA2F;AAC3F,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,yDAAyD;AACzD,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;CACjC;AAED,6EAA6E;AAC7E,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,qBAAqB,CAAC,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,2BAA4B,SAAQ,mBAAmB;IACtE;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,qBAAqB,CAAC,CAAC,GAAG,SAAS,CAAC;CACnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,cAAc,CAoHzF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAGrF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,QAAQ,GAAG,SAAS,CAUtB"}
@@ -0,0 +1,190 @@
1
+ import { createRequire } from 'node:module';
2
+ import { createFederation, InProcessMessageQueue, MemoryKvStore } from '@fedify/fedify';
3
+ import { Announce, Article, Create, Delete, Follow, Like, Undo } from '@fedify/vocab';
4
+ import { readSiteSettings } from '../admin/settings.js';
5
+ import { siteActor } from './actor.js';
6
+ import { isFederatedDocument, postArticle, postCreateActivity } from './article.js';
7
+ import { followersPage, lastFollowersCursor } from './followers.js';
8
+ import { handleDelete, handleFollow, handleLoggedActivity, handleUndo } from './inbox.js';
9
+ import { loadActorKeyPairs, SITE_ACTOR_IDENTIFIER } from './keys.js';
10
+ import { ACTOR_PATH, federationOrigin, FOLLOWERS_PATH, FOLLOWING_PATH, INBOX_PATH, NODEINFO_PATH, OUTBOX_PATH, POST_OBJECT_PATH, postObjectId, SHARED_INBOX_PATH, } from './paths.js';
11
+ /** The `software.name` this CMS reports in NodeInfo. Lower case, as the schema demands. */
12
+ export const SOFTWARE_NAME = 'geekity-cms';
13
+ /**
14
+ * How many activities one page of the outbox holds.
15
+ *
16
+ * A constant rather than the site's `postsPerPage`, because an outbox cursor
17
+ * is a URL a peer may come back to: changing the setting would silently move
18
+ * every page boundary underneath whoever is walking the collection.
19
+ */
20
+ export const OUTBOX_PAGE_SIZE = 20;
21
+ /**
22
+ * Build the site's `Federation` object: the actor, its keys, the post objects,
23
+ * the outbox, WebFinger and NodeInfo.
24
+ *
25
+ * It is a factory rather than a module-level singleton so the KV store and the
26
+ * queue are arguments (decision-5), so a test can build one per data directory,
27
+ * and so a site that outgrows the in-memory pair changes a call rather than
28
+ * this file. The followers come out of SQLite; the following collection is
29
+ * empty, and doc-4 says it always will be.
30
+ */
31
+ export function createSiteFederation(options) {
32
+ const federation = createFederation({
33
+ kv: options.kv ?? new MemoryKvStore(),
34
+ origin: federationOrigin(options.baseUrl),
35
+ // `null` is not the same as absent: it asks for no queue at all, so an
36
+ // activity is handled and delivered inside the request that carried it.
37
+ // Leaving the key off entirely is how Fedify is told that, which is why
38
+ // this is a spread rather than a value.
39
+ ...(options.queue === null ? {} : { queue: options.queue ?? new InProcessMessageQueue() }),
40
+ ...(options.allowPrivateAddress === undefined
41
+ ? {}
42
+ : { allowPrivateAddress: options.allowPrivateAddress }),
43
+ ...options.federationOptions,
44
+ });
45
+ federation
46
+ .setActorDispatcher(ACTOR_PATH, async (context, identifier) => {
47
+ if (identifier !== SITE_ACTOR_IDENTIFIER)
48
+ return null;
49
+ const settings = readSiteSettings(context.data.admin);
50
+ return await siteActor(context, identifier, {
51
+ settings,
52
+ baseUrl: context.data.config.baseUrl,
53
+ });
54
+ })
55
+ // WebFinger asks for a username; this is what turns the handle somebody
56
+ // typed into the identifier the URLs are built from, and what makes the
57
+ // handle a setting rather than part of the actor's id.
58
+ .mapHandle((context, username) => username === readSiteSettings(context.data.admin).actorHandle ? SITE_ACTOR_IDENTIFIER : null)
59
+ .setKeyPairsDispatcher(async (context, identifier) => identifier === SITE_ACTOR_IDENTIFIER
60
+ ? await loadActorKeyPairs(context.data.admin, identifier)
61
+ : []);
62
+ // A post's ActivityStreams object. `null` is a 404, which is what a draft, a
63
+ // trashed post, a page and a slug that names nothing all get: the object
64
+ // dispatcher is the only thing that decides whether an object exists, so it
65
+ // cannot disagree with the outbox about it.
66
+ federation.setObjectDispatcher(Article, POST_OBJECT_PATH, (context, values) => {
67
+ const document = federatedObject(context.data.store, values.slug, context.data.config.baseUrl);
68
+ return document === undefined ? null : postArticle(context, document);
69
+ });
70
+ federation
71
+ .setOutboxDispatcher(OUTBOX_PATH, (context, identifier, cursor) => identifier === SITE_ACTOR_IDENTIFIER ? outboxPage(context, cursor) : null)
72
+ .setCounter((context, identifier) => identifier === SITE_ACTOR_IDENTIFIER ? context.data.store.counts().posts : null)
73
+ // Paging is cursor-based, and the cursor is an offset, so the first page
74
+ // is always the start of the archive and the last is wherever the archive
75
+ // currently ends.
76
+ .setFirstCursor((_context, identifier) => (identifier === SITE_ACTOR_IDENTIFIER ? '0' : null))
77
+ .setLastCursor((context, identifier) => {
78
+ if (identifier !== SITE_ACTOR_IDENTIFIER)
79
+ return null;
80
+ const total = context.data.store.counts().posts;
81
+ return String(total === 0 ? 0 : Math.floor((total - 1) / OUTBOX_PAGE_SIZE) * OUTBOX_PAGE_SIZE);
82
+ });
83
+ // The followers, straight out of SQLite and paged like the outbox. Fedify
84
+ // renders each one as its actor id, and hands the same rows — inbox and
85
+ // shared inbox included — to `ctx.sendActivity(…, 'followers', …)`, so this
86
+ // one dispatcher is both what a peer reads and where delivery fans out to.
87
+ federation
88
+ .setFollowersDispatcher(FOLLOWERS_PATH, (context, identifier, cursor) => identifier === SITE_ACTOR_IDENTIFIER ? followersPage(context, cursor) : null)
89
+ .setCounter((context, identifier) => identifier === SITE_ACTOR_IDENTIFIER ? context.data.admin.countFollowers() : null)
90
+ .setFirstCursor((_context, identifier) => (identifier === SITE_ACTOR_IDENTIFIER ? '0' : null))
91
+ .setLastCursor((context, identifier) => identifier === SITE_ACTOR_IDENTIFIER
92
+ ? lastFollowersCursor(context.data.admin.countFollowers())
93
+ : null);
94
+ // Always empty, and always will be: doc-4 says the site follows nobody.
95
+ federation.setFollowingDispatcher(FOLLOWING_PATH, (_context, identifier) => identifier === SITE_ACTOR_IDENTIFIER ? { items: [] } : null);
96
+ // The inbox, personal and shared. Fedify has already verified the signature
97
+ // by the time a listener runs — an unsigned or badly signed delivery never
98
+ // reaches one — so a handler may trust that the activity's actor really sent
99
+ // it. An activity of a type not listed here is answered 202 and dropped,
100
+ // which is what doc-4 asks for everything past these five.
101
+ federation
102
+ .setInboxListeners(INBOX_PATH, SHARED_INBOX_PATH)
103
+ .on(Follow, handleFollow)
104
+ .on(Undo, handleUndo)
105
+ .on(Delete, handleDelete)
106
+ .on(Like, handleLoggedActivity)
107
+ .on(Announce, handleLoggedActivity)
108
+ .on(Create, handleLoggedActivity);
109
+ federation.setNodeInfoDispatcher(NODEINFO_PATH, (context) => {
110
+ const counts = context.data.store.counts();
111
+ return {
112
+ software: { name: SOFTWARE_NAME, version: packageVersion() },
113
+ protocols: ['activitypub'],
114
+ // A CMS is not a signup service; the admin makes accounts by hand.
115
+ openRegistrations: false,
116
+ usage: {
117
+ users: { total: context.data.admin.countUsers() },
118
+ localPosts: counts.posts,
119
+ localComments: 0,
120
+ },
121
+ };
122
+ });
123
+ return federation;
124
+ }
125
+ /**
126
+ * The published post a slug names, or `undefined`.
127
+ *
128
+ * Slugs are not unique across years, and the index answers with the newest
129
+ * match. That is the same rule the admin's own slug lookup follows, so a
130
+ * collision resolves the same way wherever it is asked about.
131
+ */
132
+ export function federatedPost(store, slug) {
133
+ const document = store.getBySlug(slug);
134
+ return document !== undefined && isFederatedDocument(document) ? document : undefined;
135
+ }
136
+ /**
137
+ * The post whose ActivityStreams object is served at `/ap/posts/{slug}`.
138
+ *
139
+ * Nearly always the post with that slug. The exception is a post renamed after
140
+ * it was announced: it keeps the id its old slug minted, written into
141
+ * `activitypub.id` (doc-4), so the old URL has to keep answering or every
142
+ * follower's copy points at a 404. That case is found by searching the
143
+ * archive for the post holding the id, which is a walk rather than an index
144
+ * lookup — the front matter is the source of truth (decision-1) and the index
145
+ * has no column for it — but it is only reached when the slug itself does not
146
+ * answer.
147
+ */
148
+ export function federatedObject(store, slug, baseUrl) {
149
+ const objectId = postObjectId(slug, baseUrl);
150
+ const direct = federatedPost(store, slug);
151
+ if (direct !== undefined) {
152
+ const stored = direct.activitypub?.id;
153
+ if (stored === undefined || stored === objectId)
154
+ return direct;
155
+ }
156
+ return store.listPosts().find((post) => post.activitypub?.id === objectId);
157
+ }
158
+ /**
159
+ * One page of the outbox: the `Create` that announced each post, newest first.
160
+ *
161
+ * The cursor is the offset into the archive as a decimal string. That keeps a
162
+ * page URL meaning what it meant when it was minted, which a cursor derived
163
+ * from the site's page size would not: the size is a setting somebody may
164
+ * change between two requests.
165
+ */
166
+ function outboxPage(context, cursor) {
167
+ const offset = cursorOffset(cursor);
168
+ const { store } = context.data;
169
+ const documents = store.listPosts({ limit: OUTBOX_PAGE_SIZE, offset });
170
+ const total = store.counts().posts;
171
+ const next = offset + OUTBOX_PAGE_SIZE;
172
+ return {
173
+ items: documents.map((document) => postCreateActivity(context, document)),
174
+ nextCursor: next < total ? String(next) : null,
175
+ prevCursor: offset <= 0 ? null : String(Math.max(0, offset - OUTBOX_PAGE_SIZE)),
176
+ };
177
+ }
178
+ /** A cursor as an offset. Anything unreadable starts at the beginning. */
179
+ function cursorOffset(cursor) {
180
+ if (cursor === null || !/^[0-9]+$/.test(cursor))
181
+ return 0;
182
+ return Number(cursor);
183
+ }
184
+ /** The published version of `@geekity/cms`, which NodeInfo reports. */
185
+ function packageVersion() {
186
+ const require = createRequire(import.meta.url);
187
+ const manifest = require('../../package.json');
188
+ return manifest.version ?? '0.0.0';
189
+ }
190
+ //# sourceMappingURL=federation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federation.js","sourceRoot":"","sources":["../../src/federation/federation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { Context, PageItems } from '@fedify/fedify';
2
+ import type { Recipient } from '@fedify/vocab';
3
+ import type { Follower } from '../admin/store.ts';
4
+ import type { FederationContextData } from './federation.ts';
5
+ /**
6
+ * How many followers one page of the collection holds.
7
+ *
8
+ * A constant for the same reason the outbox's is: a page URL is something a
9
+ * peer may come back to, and deriving the size from a setting would move every
10
+ * page boundary the moment somebody changed it.
11
+ */
12
+ export declare const FOLLOWERS_PAGE_SIZE = 20;
13
+ /**
14
+ * A stored follower as the {@link Recipient} Fedify delivers to.
15
+ *
16
+ * The shared inbox is offered when the follower published one, because that is
17
+ * what lets a post addressed to a hundred followers on one instance become one
18
+ * POST instead of a hundred. Fedify renders a recipient in the collection as
19
+ * its `id` alone, so the inbox columns cost the collection nothing and are
20
+ * what `ctx.sendActivity(…, 'followers', …)` needs.
21
+ */
22
+ export declare function followerRecipient(follower: Follower): Recipient;
23
+ /**
24
+ * The followers collection, newest follow first: one page of it, or all of it.
25
+ *
26
+ * A cursor is the offset into the list as a decimal string, exactly as the
27
+ * outbox's is, so the two collections page the same way and a cursor keeps
28
+ * meaning what it meant when it was minted.
29
+ *
30
+ * A `null` cursor is not the first page: it is Fedify asking for the whole
31
+ * collection at once, which is what it does before fanning an activity out to
32
+ * the followers. Answering it with a page would strand every follower past the
33
+ * first one silently, so this answers with all of them. The collection a peer
34
+ * reads never arrives here with `null`, because the first cursor is registered
35
+ * and a registered first cursor is what makes the collection paged.
36
+ */
37
+ export declare function followersPage(context: Context<FederationContextData>, cursor: string | null): PageItems<Recipient>;
38
+ /**
39
+ * The cursor of the last page of the followers collection: the offset the
40
+ * final whole page starts at, and `0` for a site nobody follows yet.
41
+ */
42
+ export declare function lastFollowersCursor(total: number): string;
43
+ //# sourceMappingURL=followers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"followers.d.ts","sourceRoot":"","sources":["../../src/federation/followers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAQ/D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,EACvC,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,SAAS,CAAC,SAAS,CAAC,CAiBtB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIzD"}