@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,55 @@
1
+ import type { Context, RequestContext } from '@fedify/fedify';
2
+ import { Application, Group, Organization, Person, Service } from '@fedify/vocab';
3
+ import type { Actor } from '@fedify/vocab';
4
+ import type { SiteSettings } from '../admin/settings.ts';
5
+ /**
6
+ * The vocabulary class behind each of {@link ACTOR_TYPES}.
7
+ *
8
+ * The setting is a string, because it is a form field and a SQLite row; this
9
+ * is where it becomes the class the actor document is built from.
10
+ */
11
+ export declare const ACTOR_CLASSES: {
12
+ readonly Person: typeof Person;
13
+ readonly Organization: typeof Organization;
14
+ readonly Service: typeof Service;
15
+ readonly Group: typeof Group;
16
+ readonly Application: typeof Application;
17
+ };
18
+ /** What {@link siteActor} needs beyond the Fedify context. */
19
+ export interface SiteActorOptions {
20
+ /** The settings the profile is built from. */
21
+ settings: SiteSettings;
22
+ /**
23
+ * The base URL actually in effect, which the settings' own may not be: it is
24
+ * what the avatar's path is resolved against. Defaults to the setting.
25
+ */
26
+ baseUrl?: string | undefined;
27
+ }
28
+ /**
29
+ * The site's avatar as the absolute URL an `icon` carries, or `undefined` when
30
+ * the site has none.
31
+ *
32
+ * The setting holds what the upload endpoint handed back —
33
+ * `/uploads/2026/09/me.png` — because that is where the site serves the file
34
+ * and where an Eleventy build of the same content copies it through to. A peer
35
+ * reading the actor has no site to resolve that against, so it is made
36
+ * absolute here. An avatar already given as an absolute URL is left alone.
37
+ */
38
+ export declare function avatarUrl(avatar: string, baseUrl: string): string | undefined;
39
+ /**
40
+ * The site's ActivityPub actor, as doc-4 describes it: one actor for the whole
41
+ * site, whose profile fields are the settings.
42
+ *
43
+ * Every URL comes from the Fedify context rather than from string
44
+ * concatenation, so the ids stay in step with the paths the dispatchers are
45
+ * registered under. The keys come from the key pairs dispatcher: `publicKey`
46
+ * is the RSA one Mastodon verifies HTTP Signatures with, `assertionMethods`
47
+ * are the multikeys FEP-8b32 proofs are checked against.
48
+ */
49
+ export declare function siteActor(context: RequestContext<unknown> | Context<unknown>, identifier: string, options: SiteActorOptions): Promise<Actor>;
50
+ /**
51
+ * The class for an actor type, falling back to the default when the stored
52
+ * setting is one this version does not know.
53
+ */
54
+ export declare function actorClassFor(actorType: string): (typeof ACTOR_CLASSES)[keyof typeof ACTOR_CLASSES];
55
+ //# sourceMappingURL=actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../../src/federation/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAa,KAAK,EAAS,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAG3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;;;CAMuD,CAAC;AAElF,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,QAAQ,EAAE,YAAY,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAW7E;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,EACnD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,KAAK,CAAC,CA8BhB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAChB,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAGpD"}
@@ -0,0 +1,101 @@
1
+ import { Application, Endpoints, Group, Image, Organization, Person, Service } from '@fedify/vocab';
2
+ import { ACTOR_TYPES, DEFAULT_SITE_SETTINGS } from '../admin/settings.js';
3
+ import { absoluteUrl } from '../web/negotiate.js';
4
+ /**
5
+ * The vocabulary class behind each of {@link ACTOR_TYPES}.
6
+ *
7
+ * The setting is a string, because it is a form field and a SQLite row; this
8
+ * is where it becomes the class the actor document is built from.
9
+ */
10
+ export const ACTOR_CLASSES = {
11
+ Person,
12
+ Organization,
13
+ Service,
14
+ Group,
15
+ Application,
16
+ };
17
+ /**
18
+ * The site's avatar as the absolute URL an `icon` carries, or `undefined` when
19
+ * the site has none.
20
+ *
21
+ * The setting holds what the upload endpoint handed back —
22
+ * `/uploads/2026/09/me.png` — because that is where the site serves the file
23
+ * and where an Eleventy build of the same content copies it through to. A peer
24
+ * reading the actor has no site to resolve that against, so it is made
25
+ * absolute here. An avatar already given as an absolute URL is left alone.
26
+ */
27
+ export function avatarUrl(avatar, baseUrl) {
28
+ const value = avatar.trim();
29
+ if (value === '')
30
+ return undefined;
31
+ if (urlOrNull(value) !== null)
32
+ return value;
33
+ if (baseUrl === '')
34
+ return undefined;
35
+ try {
36
+ return absoluteUrl(value, baseUrl);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /**
43
+ * The site's ActivityPub actor, as doc-4 describes it: one actor for the whole
44
+ * site, whose profile fields are the settings.
45
+ *
46
+ * Every URL comes from the Fedify context rather than from string
47
+ * concatenation, so the ids stay in step with the paths the dispatchers are
48
+ * registered under. The keys come from the key pairs dispatcher: `publicKey`
49
+ * is the RSA one Mastodon verifies HTTP Signatures with, `assertionMethods`
50
+ * are the multikeys FEP-8b32 proofs are checked against.
51
+ */
52
+ export async function siteActor(context, identifier, options) {
53
+ const { settings } = options;
54
+ // The base URL in effect, which is what the profile's own URL and the
55
+ // avatar's are built on; the setting is the fallback for a caller that has
56
+ // no resolved config in hand.
57
+ const baseUrl = options.baseUrl ?? settings.baseUrl;
58
+ const icon = avatarUrl(settings.avatar, baseUrl);
59
+ const keys = await context.getActorKeyPairs(identifier);
60
+ const ActorClass = actorClassFor(settings.actorType);
61
+ return new ActorClass({
62
+ id: context.getActorUri(identifier),
63
+ preferredUsername: settings.actorHandle,
64
+ name: settings.title,
65
+ summary: settings.tagline === '' ? null : settings.tagline,
66
+ url: urlOrNull(baseUrl),
67
+ icon: icon === undefined ? null : new Image({ url: new URL(icon) }),
68
+ inbox: context.getInboxUri(identifier),
69
+ outbox: context.getOutboxUri(identifier),
70
+ followers: context.getFollowersUri(identifier),
71
+ following: context.getFollowingUri(identifier),
72
+ endpoints: new Endpoints({ sharedInbox: context.getInboxUri() }),
73
+ publicKey: keys[0]?.cryptographicKey ?? null,
74
+ assertionMethods: keys.map((key) => key.multikey),
75
+ // A blog actor accepts every follower, and says so, rather than leaving a
76
+ // client to guess whether a follow will ever be answered.
77
+ manuallyApprovesFollowers: false,
78
+ discoverable: true,
79
+ indexable: true,
80
+ });
81
+ }
82
+ /**
83
+ * The class for an actor type, falling back to the default when the stored
84
+ * setting is one this version does not know.
85
+ */
86
+ export function actorClassFor(actorType) {
87
+ const known = ACTOR_TYPES.includes(actorType) ? actorType : DEFAULT_SITE_SETTINGS.actorType;
88
+ return ACTOR_CLASSES[known] ?? Person;
89
+ }
90
+ /** A URL, or `null` when the string is empty or is not one. */
91
+ function urlOrNull(value) {
92
+ if (value === '')
93
+ return null;
94
+ try {
95
+ return new URL(value);
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ //# sourceMappingURL=actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.js","sourceRoot":"","sources":["../../src/federation/actor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAGpG,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,MAAM;IACN,YAAY;IACZ,OAAO;IACP,KAAK;IACL,WAAW;CACoE,CAAC;AAalF;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,OAAe;IACvD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACnC,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAErC,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAmD,EACnD,UAAkB,EAClB,OAAyB;IAEzB,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAC7B,sEAAsE;IACtE,2EAA2E;IAC3E,8BAA8B;IAC9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC;IACpD,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAErD,OAAO,IAAI,UAAU,CAAC;QACpB,EAAE,EAAE,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC;QACnC,iBAAiB,EAAE,QAAQ,CAAC,WAAW;QACvC,IAAI,EAAE,QAAQ,CAAC,KAAK;QACpB,OAAO,EAAE,QAAQ,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO;QAC1D,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC;QACvB,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACnE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC;QACtC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC;QACxC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC;QAC9C,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC;QAC9C,SAAS,EAAE,IAAI,SAAS,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;QAChE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,gBAAgB,IAAI,IAAI;QAC5C,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC;QACjD,0EAA0E;QAC1E,0DAA0D;QAC1D,yBAAyB,EAAE,KAAK;QAChC,YAAY,EAAE,IAAI;QAClB,SAAS,EAAE,IAAI;KAChB,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,SAAiB;IAEjB,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,SAAS,CAAC;IAC5F,OAAO,aAAa,CAAC,KAAmC,CAAC,IAAI,MAAM,CAAC;AACtE,CAAC;AAED,+DAA+D;AAC/D,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,83 @@
1
+ import type { Context } from '@fedify/fedify';
2
+ import { Article, Create, Delete, Update } from '@fedify/vocab';
3
+ import type { Document } from '../content/document.ts';
4
+ import type { FederationContextData } from './federation.ts';
5
+ /** The media type an `Article`'s `source` is labelled with. */
6
+ export declare const SOURCE_MEDIA_TYPE = "text/markdown";
7
+ /**
8
+ * Whether a document is one of the objects this site federates.
9
+ *
10
+ * doc-4 federates published posts and nothing else: pages are standing
11
+ * content with no place in a timeline, and a draft or a trashed post is not
12
+ * public at all. This is the one rule, so the object dispatcher, the outbox
13
+ * and the permalink all agree about what exists.
14
+ */
15
+ export declare function isFederatedDocument(document: Document): boolean;
16
+ /**
17
+ * One post as the `Article` doc-4 describes.
18
+ *
19
+ * The id comes from the Fedify context rather than from string concatenation,
20
+ * so it stays in step with the path the object dispatcher is registered under,
21
+ * and it is built from the slug rather than from the permalink, so moving a
22
+ * post does not mint a second object. `url` is the permalink, which is the
23
+ * page a human should land on.
24
+ *
25
+ * `source` carries the Markdown the file holds, so a peer that wants to quote
26
+ * or re-render the post has the text rather than only the rendering of it.
27
+ */
28
+ export declare function postArticle(context: Context<FederationContextData>, document: Document): Article;
29
+ /**
30
+ * The `Create` that announces a post: what the outbox lists, and what delivery
31
+ * sends.
32
+ *
33
+ * Its addressing is the article's, because an activity a follower cannot see
34
+ * is an object it will never learn about. The id is a fragment of the
35
+ * article's, so the same post always produces the same activity id.
36
+ */
37
+ export declare function postCreateActivity(context: Context<FederationContextData>, document: Document): Create;
38
+ /**
39
+ * The `Update` that announces an edit to a post followers already hold.
40
+ *
41
+ * The whole article goes with it rather than a diff, because that is all
42
+ * ActivityPub offers and all a peer can apply. Its id carries the document's
43
+ * hash, so two edits are two activities and the same edit delivered twice is
44
+ * one; see {@link updateActivityId}.
45
+ */
46
+ export declare function postUpdateActivity(context: Context<FederationContextData>, document: Document): Update;
47
+ /**
48
+ * The `Delete` that withdraws a post: a `Tombstone` where the `Article` was.
49
+ *
50
+ * doc-4 sends this when a post becomes a draft, is trashed or is deleted, and
51
+ * all three look the same from outside — the object is gone and the copy every
52
+ * follower holds should go with it. The `Tombstone` keeps the object's id and
53
+ * says what it used to be, which is what lets a peer that never held the
54
+ * article recognise what it is being told about.
55
+ *
56
+ * The document is the one as it was before it went, because after a delete
57
+ * there is no other.
58
+ */
59
+ export declare function postDeleteActivity(context: Context<FederationContextData>, document: Document, deleted: string): Delete;
60
+ /**
61
+ * A post's ActivityStreams object id: the one written into its front matter if
62
+ * it has been federated, and the one its slug implies if it has not.
63
+ *
64
+ * The stored id is what makes a rename invisible to a follower. Ids are minted
65
+ * from the slug, so a post renamed after it was announced would otherwise
66
+ * become a second object and be delivered as a `Create` all over again; doc-4
67
+ * asks the `activitypub.id` key to prevent exactly that. The object dispatcher
68
+ * resolves a stored id back to its post, so the old URL keeps answering.
69
+ */
70
+ export declare function articleObjectId(context: Context<FederationContextData>, document: Document): URL;
71
+ /**
72
+ * An ISO 8601 date as the `Temporal.Instant` the vocabulary takes, or
73
+ * `undefined` when there is no usable date.
74
+ *
75
+ * The value comes from `@js-temporal/polyfill` because Node does not ship
76
+ * `Temporal` yet, and Fedify recognises a polyfilled instant by its
77
+ * `Symbol.toStringTag` rather than by its class. The cast is the one place
78
+ * that costs: the polyfill's declarations and TypeScript's `esnext.temporal`
79
+ * lib describe the same object with types that are not assignable to one
80
+ * another.
81
+ */
82
+ export declare function toInstant(value: string | undefined): Temporal.Instant | undefined;
83
+ //# sourceMappingURL=article.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"article.d.ts","sourceRoot":"","sources":["../../src/federation/article.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,MAAM,EACN,MAAM,EAKN,MAAM,EACP,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAIvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAI7D,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAE/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAwBhG;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,EACvC,QAAQ,EAAE,QAAQ,GACjB,MAAM,CAWR;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,EACvC,QAAQ,EAAE,QAAQ,GACjB,MAAM,CAWR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,EACvC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,GACd,MAAM,CAcR;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAWhG;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC,OAAO,GAAG,SAAS,CAOjF"}
@@ -0,0 +1,169 @@
1
+ import { Article, Create, Delete, Hashtag, PUBLIC_COLLECTION, Source, Tombstone, Update, } from '@fedify/vocab';
2
+ import { Temporal as TemporalPolyfill } from '@js-temporal/polyfill';
3
+ import { isPublicDocument } from '../web/documents.js';
4
+ import { absoluteUrl } from '../web/negotiate.js';
5
+ import { tagHref } from '../web/routes.js';
6
+ import { SITE_ACTOR_IDENTIFIER } from './keys.js';
7
+ import { createActivityId, deleteActivityId, updateActivityId } from './paths.js';
8
+ /** The media type an `Article`'s `source` is labelled with. */
9
+ export const SOURCE_MEDIA_TYPE = 'text/markdown';
10
+ /**
11
+ * Whether a document is one of the objects this site federates.
12
+ *
13
+ * doc-4 federates published posts and nothing else: pages are standing
14
+ * content with no place in a timeline, and a draft or a trashed post is not
15
+ * public at all. This is the one rule, so the object dispatcher, the outbox
16
+ * and the permalink all agree about what exists.
17
+ */
18
+ export function isFederatedDocument(document) {
19
+ return document.type === 'post' && isPublicDocument(document);
20
+ }
21
+ /**
22
+ * One post as the `Article` doc-4 describes.
23
+ *
24
+ * The id comes from the Fedify context rather than from string concatenation,
25
+ * so it stays in step with the path the object dispatcher is registered under,
26
+ * and it is built from the slug rather than from the permalink, so moving a
27
+ * post does not mint a second object. `url` is the permalink, which is the
28
+ * page a human should land on.
29
+ *
30
+ * `source` carries the Markdown the file holds, so a peer that wants to quote
31
+ * or re-render the post has the text rather than only the rendering of it.
32
+ */
33
+ export function postArticle(context, document) {
34
+ const { baseUrl } = context.data.config;
35
+ return new Article({
36
+ id: articleObjectId(context, document),
37
+ url: new URL(absoluteUrl(document.permalink, baseUrl)),
38
+ name: document.title,
39
+ content: document.html,
40
+ source: new Source({ content: document.body, mediaType: SOURCE_MEDIA_TYPE }),
41
+ published: toInstant(document.date) ?? null,
42
+ updated: toInstant(document.updated) ?? null,
43
+ attribution: context.getActorUri(SITE_ACTOR_IDENTIFIER),
44
+ // Public addressing, as a blog post is: anybody may fetch it, and every
45
+ // follower is told about it.
46
+ to: PUBLIC_COLLECTION,
47
+ cc: context.getFollowersUri(SITE_ACTOR_IDENTIFIER),
48
+ tags: document.tags.map((tag) => new Hashtag({
49
+ name: `#${tag}`,
50
+ href: new URL(absoluteUrl(tagHref(tag, 0), baseUrl)),
51
+ })),
52
+ });
53
+ }
54
+ /**
55
+ * The `Create` that announces a post: what the outbox lists, and what delivery
56
+ * sends.
57
+ *
58
+ * Its addressing is the article's, because an activity a follower cannot see
59
+ * is an object it will never learn about. The id is a fragment of the
60
+ * article's, so the same post always produces the same activity id.
61
+ */
62
+ export function postCreateActivity(context, document) {
63
+ const article = postArticle(context, document);
64
+ return new Create({
65
+ id: createActivityId(articleObjectId(context, document)),
66
+ actor: context.getActorUri(SITE_ACTOR_IDENTIFIER),
67
+ object: article,
68
+ published: toInstant(document.date) ?? null,
69
+ to: PUBLIC_COLLECTION,
70
+ cc: context.getFollowersUri(SITE_ACTOR_IDENTIFIER),
71
+ });
72
+ }
73
+ /**
74
+ * The `Update` that announces an edit to a post followers already hold.
75
+ *
76
+ * The whole article goes with it rather than a diff, because that is all
77
+ * ActivityPub offers and all a peer can apply. Its id carries the document's
78
+ * hash, so two edits are two activities and the same edit delivered twice is
79
+ * one; see {@link updateActivityId}.
80
+ */
81
+ export function postUpdateActivity(context, document) {
82
+ const article = postArticle(context, document);
83
+ return new Update({
84
+ id: updateActivityId(articleObjectId(context, document), revisionOf(document)),
85
+ actor: context.getActorUri(SITE_ACTOR_IDENTIFIER),
86
+ object: article,
87
+ published: toInstant(document.updated ?? document.date) ?? null,
88
+ to: PUBLIC_COLLECTION,
89
+ cc: context.getFollowersUri(SITE_ACTOR_IDENTIFIER),
90
+ });
91
+ }
92
+ /**
93
+ * The `Delete` that withdraws a post: a `Tombstone` where the `Article` was.
94
+ *
95
+ * doc-4 sends this when a post becomes a draft, is trashed or is deleted, and
96
+ * all three look the same from outside — the object is gone and the copy every
97
+ * follower holds should go with it. The `Tombstone` keeps the object's id and
98
+ * says what it used to be, which is what lets a peer that never held the
99
+ * article recognise what it is being told about.
100
+ *
101
+ * The document is the one as it was before it went, because after a delete
102
+ * there is no other.
103
+ */
104
+ export function postDeleteActivity(context, document, deleted) {
105
+ const objectId = articleObjectId(context, document);
106
+ return new Delete({
107
+ id: deleteActivityId(objectId, deleted),
108
+ actor: context.getActorUri(SITE_ACTOR_IDENTIFIER),
109
+ object: new Tombstone({
110
+ id: objectId,
111
+ formerType: Article,
112
+ deleted: toInstant(deleted) ?? null,
113
+ }),
114
+ to: PUBLIC_COLLECTION,
115
+ cc: context.getFollowersUri(SITE_ACTOR_IDENTIFIER),
116
+ });
117
+ }
118
+ /**
119
+ * A post's ActivityStreams object id: the one written into its front matter if
120
+ * it has been federated, and the one its slug implies if it has not.
121
+ *
122
+ * The stored id is what makes a rename invisible to a follower. Ids are minted
123
+ * from the slug, so a post renamed after it was announced would otherwise
124
+ * become a second object and be delivered as a `Create` all over again; doc-4
125
+ * asks the `activitypub.id` key to prevent exactly that. The object dispatcher
126
+ * resolves a stored id back to its post, so the old URL keeps answering.
127
+ */
128
+ export function articleObjectId(context, document) {
129
+ const stored = document.activitypub?.id;
130
+ if (stored !== undefined && stored !== '') {
131
+ try {
132
+ return new URL(stored);
133
+ }
134
+ catch {
135
+ // A hand-written `activitypub.id` that is not a URL is not an id. Fall
136
+ // back to the derived one rather than failing the delivery.
137
+ }
138
+ }
139
+ return context.getObjectUri(Article, { slug: document.slug });
140
+ }
141
+ /**
142
+ * Which revision of a document an `Update` is announcing.
143
+ *
144
+ * The content hash: it changes with every real edit and with nothing else, so
145
+ * it names the revision without a clock and without a counter to keep.
146
+ */
147
+ function revisionOf(document) {
148
+ return document.hash.slice(0, 16);
149
+ }
150
+ /**
151
+ * An ISO 8601 date as the `Temporal.Instant` the vocabulary takes, or
152
+ * `undefined` when there is no usable date.
153
+ *
154
+ * The value comes from `@js-temporal/polyfill` because Node does not ship
155
+ * `Temporal` yet, and Fedify recognises a polyfilled instant by its
156
+ * `Symbol.toStringTag` rather than by its class. The cast is the one place
157
+ * that costs: the polyfill's declarations and TypeScript's `esnext.temporal`
158
+ * lib describe the same object with types that are not assignable to one
159
+ * another.
160
+ */
161
+ export function toInstant(value) {
162
+ if (value === undefined)
163
+ return undefined;
164
+ const date = new Date(value);
165
+ if (Number.isNaN(date.getTime()))
166
+ return undefined;
167
+ return TemporalPolyfill.Instant.fromEpochMilliseconds(date.getTime());
168
+ }
169
+ //# sourceMappingURL=article.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"article.js","sourceRoot":"","sources":["../../src/federation/article.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import type { AdminStore, Delivery, Follower } from '../admin/store.ts';
2
+ import type { ResolvedConfig } from '../config.ts';
3
+ import type { ContentStore } from '../content/store.ts';
4
+ import type { DocumentChange } from '../content/sync.ts';
5
+ import type { SiteFederation } from './federation.ts';
6
+ /** What one activity's delivery came to, follower by follower. */
7
+ export interface DeliveryReport {
8
+ /** The activity that went out. */
9
+ readonly activityId: string;
10
+ /** `Create`, `Update` or `Delete`. */
11
+ readonly activityType: string;
12
+ /**
13
+ * The ActivityStreams id of what it was about: a post, or the site's own
14
+ * actor when the profile itself was what moved.
15
+ */
16
+ readonly objectId: string;
17
+ /** One row per follower, as recorded. Empty when nobody follows the site. */
18
+ readonly deliveries: readonly Delivery[];
19
+ }
20
+ /** Where a delivery service reports what it could not do. `console` will do. */
21
+ export interface DeliveryLogger {
22
+ warn(message: string): void;
23
+ }
24
+ /** What {@link createDeliveryService} needs. */
25
+ export interface CreateDeliveryServiceOptions {
26
+ /** The site's federation object, which mints the ids and signs the requests. */
27
+ federation: SiteFederation;
28
+ /** Followers and the delivery log. */
29
+ admin: AdminStore;
30
+ /** The content index, which the dispatchers read through the context. */
31
+ store: ContentStore;
32
+ /** Config after defaults and environment overrides. */
33
+ config: ResolvedConfig;
34
+ /** Where failures are reported. Defaults to `console`. */
35
+ logger?: DeliveryLogger | undefined;
36
+ }
37
+ /**
38
+ * Sends a site's posts to its followers, and remembers how that went.
39
+ *
40
+ * Deliveries run one after another on a queue of their own rather than at
41
+ * once, because ActivityPub has no way of saying that a `Create` came before
42
+ * the `Update` that follows it: a peer that receives them out of order keeps
43
+ * the wrong version.
44
+ */
45
+ export interface DeliveryService {
46
+ /**
47
+ * Consider one index change, and deliver whatever it implies. Resolves once
48
+ * the change has been recorded in the file — not once the activity has been
49
+ * delivered, which is what {@link DeliveryService.settled} is for.
50
+ */
51
+ handle(change: DocumentChange): Promise<void>;
52
+ /**
53
+ * Tell the followers that the site's own profile has moved: an `Update`
54
+ * whose object is the actor, which is how a peer learns that the name, the
55
+ * summary or the avatar it cached is out of date.
56
+ *
57
+ * `undefined` when nobody follows the site, in which case nothing is built
58
+ * and nothing is recorded: there is no cached profile anywhere to refresh.
59
+ */
60
+ updateActor(): Promise<DeliveryReport | undefined>;
61
+ /**
62
+ * Send a recorded activity again, to every follower the site has now.
63
+ *
64
+ * decision-5 accepts that an activity queued when the process exits is lost
65
+ * and promises this as the way back. `undefined` means no such activity was
66
+ * ever sent.
67
+ */
68
+ redeliver(activityId: string): Promise<DeliveryReport | undefined>;
69
+ /** Resolve once every queued delivery has finished, however it finished. */
70
+ settled(): Promise<void>;
71
+ }
72
+ /**
73
+ * Build the delivery service for one site.
74
+ *
75
+ * doc-4's delivery table is the whole of the policy: a post that becomes
76
+ * published is a `Create`, an edit to a published post is an `Update`, and a
77
+ * post that stops being published — draft, trashed or deleted, which are one
78
+ * thing from outside — is a `Delete` of a `Tombstone`.
79
+ *
80
+ * Nothing is delivered for a full scan. The boot scan reports a cold index as
81
+ * a directory full of creations, so federating them would announce the whole
82
+ * archive to every follower every time somebody deleted the database.
83
+ */
84
+ export declare function createDeliveryService(options: CreateDeliveryServiceOptions): DeliveryService;
85
+ /**
86
+ * Followers keyed by the inbox one POST to which reaches them: the instance's
87
+ * shared inbox where it published one, and the follower's own where it did not.
88
+ */
89
+ export declare function groupByInbox(followers: readonly Follower[]): Map<string, Follower[]>;
90
+ //# sourceMappingURL=delivery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../../src/federation/delivery.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAkB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAUzD,OAAO,KAAK,EAAyB,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAK7E,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sCAAsC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAC;CAC1C;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,gDAAgD;AAChD,MAAM,WAAW,4BAA4B;IAC3C,gFAAgF;IAChF,UAAU,EAAE,cAAc,CAAC;IAC3B,sCAAsC;IACtC,KAAK,EAAE,UAAU,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,YAAY,CAAC;IACpB,uDAAuD;IACvD,MAAM,EAAE,cAAc,CAAC;IACvB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C;;;;;;;OAOG;IACH,WAAW,IAAI,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACnD;;;;;;OAMG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACnE,4EAA4E;IAC5E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,eAAe,CAkR5F;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAWpF"}