@geekity/cms 0.7.0 → 0.9.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 (216) hide show
  1. package/README.md +445 -63
  2. package/admin/layouts/shell.njk +13 -6
  3. package/admin/pages/comments/all.njk +3 -0
  4. package/admin/pages/error.njk +11 -0
  5. package/admin/pages/settings/discussion.njk +53 -0
  6. package/admin/pages/settings/email.njk +20 -0
  7. package/admin/pages/tools/personal-data.njk +67 -0
  8. package/admin/pages/users/edit.njk +1 -1
  9. package/admin/static/admin.css +69 -15
  10. package/dist/admin/comments.d.ts +5 -0
  11. package/dist/admin/comments.d.ts.map +1 -1
  12. package/dist/admin/comments.js +13 -0
  13. package/dist/admin/comments.js.map +1 -1
  14. package/dist/admin/documents.d.ts.map +1 -1
  15. package/dist/admin/documents.js +58 -31
  16. package/dist/admin/documents.js.map +1 -1
  17. package/dist/admin/headers.d.ts +14 -5
  18. package/dist/admin/headers.d.ts.map +1 -1
  19. package/dist/admin/headers.js +18 -6
  20. package/dist/admin/headers.js.map +1 -1
  21. package/dist/admin/index.d.ts +1 -1
  22. package/dist/admin/index.d.ts.map +1 -1
  23. package/dist/admin/index.js +1 -1
  24. package/dist/admin/index.js.map +1 -1
  25. package/dist/admin/menu.d.ts.map +1 -1
  26. package/dist/admin/menu.js +5 -1
  27. package/dist/admin/menu.js.map +1 -1
  28. package/dist/admin/personal-data.d.ts +35 -0
  29. package/dist/admin/personal-data.d.ts.map +1 -0
  30. package/dist/admin/personal-data.js +90 -0
  31. package/dist/admin/personal-data.js.map +1 -0
  32. package/dist/admin/routes.d.ts.map +1 -1
  33. package/dist/admin/routes.js +23 -4
  34. package/dist/admin/routes.js.map +1 -1
  35. package/dist/admin/session.d.ts +42 -1
  36. package/dist/admin/session.d.ts.map +1 -1
  37. package/dist/admin/session.js +74 -4
  38. package/dist/admin/session.js.map +1 -1
  39. package/dist/admin/settings-discussion.d.ts.map +1 -1
  40. package/dist/admin/settings-discussion.js +9 -1
  41. package/dist/admin/settings-discussion.js.map +1 -1
  42. package/dist/admin/settings-email.d.ts.map +1 -1
  43. package/dist/admin/settings-email.js +10 -1
  44. package/dist/admin/settings-email.js.map +1 -1
  45. package/dist/admin/settings.d.ts +39 -0
  46. package/dist/admin/settings.d.ts.map +1 -1
  47. package/dist/admin/settings.js +144 -0
  48. package/dist/admin/settings.js.map +1 -1
  49. package/dist/admin/store.d.ts +30 -0
  50. package/dist/admin/store.d.ts.map +1 -1
  51. package/dist/admin/store.js +68 -5
  52. package/dist/admin/store.js.map +1 -1
  53. package/dist/admin/templates.d.ts +2 -0
  54. package/dist/admin/templates.d.ts.map +1 -1
  55. package/dist/admin/templates.js +2 -0
  56. package/dist/admin/templates.js.map +1 -1
  57. package/dist/avatars/avatars.d.ts +112 -0
  58. package/dist/avatars/avatars.d.ts.map +1 -0
  59. package/dist/avatars/avatars.js +259 -0
  60. package/dist/avatars/avatars.js.map +1 -0
  61. package/dist/avatars/index.d.ts +4 -0
  62. package/dist/avatars/index.d.ts.map +1 -0
  63. package/dist/avatars/index.js +3 -0
  64. package/dist/avatars/index.js.map +1 -0
  65. package/dist/avatars/routes.d.ts +11 -0
  66. package/dist/avatars/routes.d.ts.map +1 -0
  67. package/dist/avatars/routes.js +33 -0
  68. package/dist/avatars/routes.js.map +1 -0
  69. package/dist/cli.d.ts +1 -1
  70. package/dist/cli.d.ts.map +1 -1
  71. package/dist/cli.js +128 -3
  72. package/dist/cli.js.map +1 -1
  73. package/dist/comments/records.d.ts +15 -0
  74. package/dist/comments/records.d.ts.map +1 -1
  75. package/dist/comments/records.js +51 -1
  76. package/dist/comments/records.js.map +1 -1
  77. package/dist/comments/routes.d.ts.map +1 -1
  78. package/dist/comments/routes.js +7 -1
  79. package/dist/comments/routes.js.map +1 -1
  80. package/dist/config.d.ts +37 -0
  81. package/dist/config.d.ts.map +1 -1
  82. package/dist/config.js +58 -0
  83. package/dist/config.js.map +1 -1
  84. package/dist/contact/index.d.ts +1 -1
  85. package/dist/contact/index.d.ts.map +1 -1
  86. package/dist/contact/index.js +1 -1
  87. package/dist/contact/index.js.map +1 -1
  88. package/dist/contact/records.d.ts +8 -2
  89. package/dist/contact/records.d.ts.map +1 -1
  90. package/dist/contact/records.js +13 -6
  91. package/dist/contact/records.js.map +1 -1
  92. package/dist/content/document.d.ts +10 -2
  93. package/dist/content/document.d.ts.map +1 -1
  94. package/dist/content/document.js +1 -0
  95. package/dist/content/document.js.map +1 -1
  96. package/dist/content/parser.d.ts.map +1 -1
  97. package/dist/content/parser.js +13 -0
  98. package/dist/content/parser.js.map +1 -1
  99. package/dist/content/store.d.ts +13 -2
  100. package/dist/content/store.d.ts.map +1 -1
  101. package/dist/content/store.js +45 -1
  102. package/dist/content/store.js.map +1 -1
  103. package/dist/content/writer.d.ts.map +1 -1
  104. package/dist/content/writer.js +4 -0
  105. package/dist/content/writer.js.map +1 -1
  106. package/dist/env.d.ts +18 -0
  107. package/dist/env.d.ts.map +1 -1
  108. package/dist/federation/article.d.ts +9 -0
  109. package/dist/federation/article.d.ts.map +1 -1
  110. package/dist/federation/article.js +24 -2
  111. package/dist/federation/article.js.map +1 -1
  112. package/dist/federation/federation.d.ts.map +1 -1
  113. package/dist/federation/federation.js +24 -6
  114. package/dist/federation/federation.js.map +1 -1
  115. package/dist/federation/inbox.d.ts +17 -3
  116. package/dist/federation/inbox.d.ts.map +1 -1
  117. package/dist/federation/inbox.js +72 -6
  118. package/dist/federation/inbox.js.map +1 -1
  119. package/dist/federation/mount.d.ts.map +1 -1
  120. package/dist/federation/mount.js +8 -3
  121. package/dist/federation/mount.js.map +1 -1
  122. package/dist/federation/paths.d.ts +5 -0
  123. package/dist/federation/paths.d.ts.map +1 -1
  124. package/dist/federation/paths.js +5 -0
  125. package/dist/federation/paths.js.map +1 -1
  126. package/dist/federation/quotes.d.ts +58 -0
  127. package/dist/federation/quotes.d.ts.map +1 -0
  128. package/dist/federation/quotes.js +89 -0
  129. package/dist/federation/quotes.js.map +1 -0
  130. package/dist/federation/replies.d.ts +23 -0
  131. package/dist/federation/replies.d.ts.map +1 -1
  132. package/dist/federation/replies.js +47 -10
  133. package/dist/federation/replies.js.map +1 -1
  134. package/dist/federation/wordpress.d.ts.map +1 -1
  135. package/dist/federation/wordpress.js +5 -4
  136. package/dist/federation/wordpress.js.map +1 -1
  137. package/dist/index.d.ts +23 -2
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +64 -7
  140. package/dist/index.js.map +1 -1
  141. package/dist/maintenance.d.ts +47 -0
  142. package/dist/maintenance.d.ts.map +1 -0
  143. package/dist/maintenance.js +78 -0
  144. package/dist/maintenance.js.map +1 -0
  145. package/dist/notifications/index.d.ts +1 -1
  146. package/dist/notifications/index.d.ts.map +1 -1
  147. package/dist/notifications/index.js +1 -1
  148. package/dist/notifications/index.js.map +1 -1
  149. package/dist/notifications/optouts.d.ts +6 -0
  150. package/dist/notifications/optouts.d.ts.map +1 -1
  151. package/dist/notifications/optouts.js +17 -0
  152. package/dist/notifications/optouts.js.map +1 -1
  153. package/dist/privacy/erase.d.ts +45 -0
  154. package/dist/privacy/erase.d.ts.map +1 -0
  155. package/dist/privacy/erase.js +77 -0
  156. package/dist/privacy/erase.js.map +1 -0
  157. package/dist/privacy/policy.d.ts +32 -0
  158. package/dist/privacy/policy.d.ts.map +1 -0
  159. package/dist/privacy/policy.js +37 -0
  160. package/dist/privacy/policy.js.map +1 -0
  161. package/dist/privacy/retention.d.ts +79 -0
  162. package/dist/privacy/retention.d.ts.map +1 -0
  163. package/dist/privacy/retention.js +133 -0
  164. package/dist/privacy/retention.js.map +1 -0
  165. package/dist/web/conversation.d.ts +9 -1
  166. package/dist/web/conversation.d.ts.map +1 -1
  167. package/dist/web/conversation.js +69 -39
  168. package/dist/web/conversation.js.map +1 -1
  169. package/dist/web/documents.d.ts +9 -0
  170. package/dist/web/documents.d.ts.map +1 -1
  171. package/dist/web/documents.js +31 -0
  172. package/dist/web/documents.js.map +1 -1
  173. package/dist/web/errors.d.ts +19 -0
  174. package/dist/web/errors.d.ts.map +1 -0
  175. package/dist/web/errors.js +93 -0
  176. package/dist/web/errors.js.map +1 -0
  177. package/dist/web/health.d.ts +6 -0
  178. package/dist/web/health.d.ts.map +1 -1
  179. package/dist/web/health.js +5 -1
  180. package/dist/web/health.js.map +1 -1
  181. package/dist/web/index.d.ts +5 -0
  182. package/dist/web/index.d.ts.map +1 -1
  183. package/dist/web/index.js +4 -0
  184. package/dist/web/index.js.map +1 -1
  185. package/dist/web/maintenance.d.ts +18 -0
  186. package/dist/web/maintenance.d.ts.map +1 -0
  187. package/dist/web/maintenance.js +113 -0
  188. package/dist/web/maintenance.js.map +1 -0
  189. package/dist/web/redirects.d.ts +75 -0
  190. package/dist/web/redirects.d.ts.map +1 -0
  191. package/dist/web/redirects.js +200 -0
  192. package/dist/web/redirects.js.map +1 -0
  193. package/dist/web/render.d.ts +9 -0
  194. package/dist/web/render.d.ts.map +1 -1
  195. package/dist/web/render.js +19 -0
  196. package/dist/web/render.js.map +1 -1
  197. package/dist/web/routes.d.ts.map +1 -1
  198. package/dist/web/routes.js +57 -0
  199. package/dist/web/routes.js.map +1 -1
  200. package/dist/web/well-known.d.ts +21 -0
  201. package/dist/web/well-known.d.ts.map +1 -0
  202. package/dist/web/well-known.js +66 -0
  203. package/dist/web/well-known.js.map +1 -0
  204. package/dist/webmention/fetch-public.d.ts +34 -0
  205. package/dist/webmention/fetch-public.d.ts.map +1 -0
  206. package/dist/webmention/fetch-public.js +101 -0
  207. package/dist/webmention/fetch-public.js.map +1 -0
  208. package/dist/webmention/reply-context.d.ts.map +1 -1
  209. package/dist/webmention/reply-context.js +12 -89
  210. package/dist/webmention/reply-context.js.map +1 -1
  211. package/package.json +1 -1
  212. package/templates/site/content/_data/site.json +7 -1
  213. package/templates/site/content/pages/privacy.md +127 -0
  214. package/themes/default/README.md +58 -19
  215. package/themes/default/layouts/500.njk +8 -0
  216. package/themes/default/layouts/503.njk +8 -0
@@ -0,0 +1,78 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readFileIfPresentSync, writeFileAtomically } from './files/atomic.js';
4
+ /**
5
+ * Maintenance mode (TASK-130) is a file, `data/maintenance.json`, that the
6
+ * running site checks for and `geekity maintenance on` and `off` write and
7
+ * remove. A file rather than a setting so that turning it on needs neither a
8
+ * restart nor a working admin, and under `data/` rather than `content/`
9
+ * because it is operational state: `content/` is published and committed, and
10
+ * a flag that travelled with it would take down every checkout of the site.
11
+ */
12
+ export const MAINTENANCE_FILE = 'maintenance.json';
13
+ /** How long the running site trusts its last look at the file. */
14
+ const RECHECK_MS = 1000;
15
+ export function maintenanceFile(dataDir) {
16
+ return path.join(dataDir, MAINTENANCE_FILE);
17
+ }
18
+ /** Turn maintenance on, replacing whatever window was there before. */
19
+ export async function enterMaintenance(dataDir, window = {}) {
20
+ const body = window.until === undefined ? {} : { until: window.until.toISOString() };
21
+ await writeFileAtomically(maintenanceFile(dataDir), `${JSON.stringify(body, null, 2)}\n`);
22
+ }
23
+ /** Turn maintenance off. Already off is not an error. */
24
+ export async function leaveMaintenance(dataDir) {
25
+ await rm(maintenanceFile(dataDir), { force: true });
26
+ }
27
+ /**
28
+ * The window the file describes, or `undefined` when there is no file.
29
+ *
30
+ * A file that is there but unreadable as a window still means maintenance:
31
+ * the operator put it there, and taking the site back up because of a typo in
32
+ * a date would be the surprise.
33
+ */
34
+ export function readMaintenance(dataDir) {
35
+ const text = readFileIfPresentSync(maintenanceFile(dataDir));
36
+ if (text === undefined)
37
+ return undefined;
38
+ return { until: untilIn(text) };
39
+ }
40
+ function untilIn(text) {
41
+ try {
42
+ const parsed = JSON.parse(text);
43
+ if (typeof parsed !== 'object' || parsed === null)
44
+ return undefined;
45
+ const until = parsed['until'];
46
+ if (typeof until !== 'string')
47
+ return undefined;
48
+ const date = new Date(until);
49
+ return Number.isNaN(date.getTime()) ? undefined : date;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ /**
56
+ * The running site's view of maintenance mode.
57
+ *
58
+ * `forced` is the `maintenance` setting or `GEEKITY_MAINTENANCE`, read once at
59
+ * boot: it keeps the site in maintenance for the life of the process, and the
60
+ * file can only add an expected return to it. Otherwise the file decides, and
61
+ * is looked at no more than once a second by the site's clock: a relay burst
62
+ * of a few hundred deliveries costs one read, and `geekity maintenance off`
63
+ * still takes effect within a second.
64
+ */
65
+ export function createMaintenanceSwitch(options) {
66
+ const forced = options.forced ? { until: undefined } : undefined;
67
+ let checkedAt = Number.NEGATIVE_INFINITY;
68
+ let current;
69
+ return () => {
70
+ const now = options.now().getTime();
71
+ if (now - checkedAt >= RECHECK_MS || now < checkedAt) {
72
+ current = readMaintenance(options.dataDir);
73
+ checkedAt = now;
74
+ }
75
+ return current ?? forced;
76
+ };
77
+ }
78
+ //# sourceMappingURL=maintenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maintenance.js","sourceRoot":"","sources":["../src/maintenance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAEnD,kEAAkE;AAClE,MAAM,UAAU,GAAG,IAAI,CAAC;AAOxB,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;AAC9C,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAe,EACf,MAAM,GAAiC,EAAE;IAEzC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;IACrF,MAAM,mBAAmB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC5F,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAe;IACpD,MAAM,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,IAAI,GAAG,qBAAqB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7D,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;AAClC,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QACpE,MAAM,KAAK,GAAI,MAAkC,CAAC,OAAO,CAAC,CAAC;QAC3D,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAKD;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAIvC;IACC,MAAM,MAAM,GAAkC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAChG,IAAI,SAAS,GAAG,MAAM,CAAC,iBAAiB,CAAC;IACzC,IAAI,OAAsC,CAAC;IAC3C,OAAO,GAAG,EAAE;QACV,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QACpC,IAAI,GAAG,GAAG,SAAS,IAAI,UAAU,IAAI,GAAG,GAAG,SAAS,EAAE,CAAC;YACrD,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAC3C,SAAS,GAAG,GAAG,CAAC;QAClB,CAAC;QACD,OAAO,OAAO,IAAI,MAAM,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC"}
@@ -4,7 +4,7 @@ export { COMMENT_DIGEST_TEMPLATE, createCommentDigest, DIGEST_MAX_ITEMS, DIGEST_
4
4
  export type { CommentDigest, CreateCommentDigestOptions, DigestLogger, NotificationTimers, } from './digest.ts';
5
5
  export { MODERATE_PATH, MODERATION_TOKEN_LIFETIME_SECONDS, moderationLink, NOTIFICATION_FIELDS, UNSUBSCRIBE_ACTION, UNSUBSCRIBE_PATH, UNSUBSCRIBE_TOKEN_LIFETIME_SECONDS, unsubscribeLink, } from './links.ts';
6
6
  export type { LinkContext } from './links.ts';
7
- export { addCommentOptOut, COMMENT_OPTOUTS_FILE, commentOptOutsFile, hasOptedOut, readCommentOptOuts, } from './optouts.ts';
7
+ export { addCommentOptOut, COMMENT_OPTOUTS_FILE, commentOptOutsFile, hasOptedOut, readCommentOptOuts, removeCommentOptOut, } from './optouts.ts';
8
8
  export { DEFAULT_DELIVERY_MODE, DELIVERY_MODE_LABELS, DELIVERY_WINDOW_MS, deliveryMode, isBatchedMode, NOTIFICATION_DELIVERY_MODES, NOTIFICATION_EVENTS, notificationEvent, notificationMode, notificationRecipients, notificationSwitches, notificationWanted, withNotification, withNotificationMode, } from './preferences.ts';
9
9
  export type { NotificationDeliveryMode, NotificationEvent, NotificationSwitch, } from './preferences.ts';
10
10
  export { mountNotificationLinks } from './routes.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,aAAa,EACb,0BAA0B,EAC1B,YAAY,EACZ,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,iCAAiC,EACjC,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kCAAkC,EAClC,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,2BAA2B,EAC3B,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,aAAa,EACb,0BAA0B,EAC1B,YAAY,EACZ,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,iCAAiC,EACjC,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kCAAkC,EAClC,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,2BAA2B,EAC3B,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -1,7 +1,7 @@
1
1
  export { COMMENT_PENDING_TEMPLATE, COMMENT_REPLY_TEMPLATE, COMMENTS_NOTIFICATION, createCommentNotifier, } from './comments.js';
2
2
  export { COMMENT_DIGEST_TEMPLATE, createCommentDigest, DIGEST_MAX_ITEMS, DIGEST_TICK_MS, DIGEST_TIMES_FILE, digestTimesFile, readDigestTimes, recordDigestTimes, systemNotificationTimers, } from './digest.js';
3
3
  export { MODERATE_PATH, MODERATION_TOKEN_LIFETIME_SECONDS, moderationLink, NOTIFICATION_FIELDS, UNSUBSCRIBE_ACTION, UNSUBSCRIBE_PATH, UNSUBSCRIBE_TOKEN_LIFETIME_SECONDS, unsubscribeLink, } from './links.js';
4
- export { addCommentOptOut, COMMENT_OPTOUTS_FILE, commentOptOutsFile, hasOptedOut, readCommentOptOuts, } from './optouts.js';
4
+ export { addCommentOptOut, COMMENT_OPTOUTS_FILE, commentOptOutsFile, hasOptedOut, readCommentOptOuts, removeCommentOptOut, } from './optouts.js';
5
5
  export { DEFAULT_DELIVERY_MODE, DELIVERY_MODE_LABELS, DELIVERY_WINDOW_MS, deliveryMode, isBatchedMode, NOTIFICATION_DELIVERY_MODES, NOTIFICATION_EVENTS, notificationEvent, notificationMode, notificationRecipients, notificationSwitches, notificationWanted, withNotification, withNotificationMode, } from './preferences.js';
6
6
  export { mountNotificationLinks } from './routes.js';
7
7
  export { NOTIFICATION_SECRET_FILE, notificationTokenExpiry, readNotificationToken, signNotificationToken, } from './tokens.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AAOrB,OAAO,EACL,aAAa,EACb,iCAAiC,EACjC,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kCAAkC,EAClC,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,2BAA2B,EAC3B,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AAOrB,OAAO,EACL,aAAa,EACb,iCAAiC,EACjC,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kCAAkC,EAClC,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,2BAA2B,EAC3B,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
@@ -45,4 +45,10 @@ export declare function hasOptedOut(dataDir: string, email: string | null | unde
45
45
  * another's address.
46
46
  */
47
47
  export declare function addCommentOptOut(dataDir: string, email: string): Promise<boolean>;
48
+ /**
49
+ * Forget that an address ever asked to stop, because its owner asked for
50
+ * everything this site holds about them to be erased (TASK-135). Returns
51
+ * whether it was on the list. Nothing is written when it was not.
52
+ */
53
+ export declare function removeCommentOptOut(dataDir: string, email: string): Promise<boolean>;
48
54
  //# sourceMappingURL=optouts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"optouts.d.ts","sourceRoot":"","sources":["../../src/notifications/optouts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,uCAAuC;AACvC,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,+CAA+C;AAC/C,eAAO,MAAM,yBAAyB,MAAQ,CAAC;AAE/C,uDAAuD;AACvD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAE/D;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGtF;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBvF"}
1
+ {"version":3,"file":"optouts.d.ts","sourceRoot":"","sources":["../../src/notifications/optouts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,uCAAuC;AACvC,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,+CAA+C;AAC/C,eAAO,MAAM,yBAAyB,MAAQ,CAAC;AAE/C,uDAAuD;AACvD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAE/D;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGtF;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBvF;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB1F"}
@@ -67,6 +67,23 @@ export async function addCommentOptOut(dataDir, email) {
67
67
  }, { mode: COMMENT_OPTOUTS_FILE_MODE });
68
68
  return added;
69
69
  }
70
+ /**
71
+ * Forget that an address ever asked to stop, because its owner asked for
72
+ * everything this site holds about them to be erased (TASK-135). Returns
73
+ * whether it was on the list. Nothing is written when it was not.
74
+ */
75
+ export async function removeCommentOptOut(dataDir, email) {
76
+ const unwanted = email.trim().toLowerCase();
77
+ if (unwanted === '' || !hasOptedOut(dataDir, unwanted))
78
+ return false;
79
+ let removed = false;
80
+ await updateFileAtomically(commentOptOutsFile(dataDir), (current) => {
81
+ const held = optOutsIn(current);
82
+ removed = held.delete(unwanted);
83
+ return `${JSON.stringify({ addresses: [...held].sort() }, null, 2)}\n`;
84
+ }, { mode: COMMENT_OPTOUTS_FILE_MODE });
85
+ return removed;
86
+ }
70
87
  /** The addresses a file's bytes hold. */
71
88
  function optOutsIn(source) {
72
89
  if (source === undefined)
@@ -1 +1 @@
1
- {"version":3,"file":"optouts.js","sourceRoot":"","sources":["../../src/notifications/optouts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"optouts.js","sourceRoot":"","sources":["../../src/notifications/optouts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { CommentRecord } from '../admin/store.ts';
2
+ import type { CommentRecords } from '../comments/records.ts';
3
+ /**
4
+ * Erasing one person's personal data when they ask (TASK-135).
5
+ *
6
+ * A person is found by the email they gave, because that is the one thing
7
+ * they can prove is theirs: a name is anybody's to type. Their comments stay
8
+ * where they are in the thread, with their status and their words, under the
9
+ * name {@link ERASED_NAME} and with nothing else of theirs left on them; a
10
+ * comment deleted outright would take every reply to it out of the thread,
11
+ * and the Comments screen is there for a comment whose words have to go too.
12
+ * Their contact messages are deleted, because a message is nothing but what
13
+ * they wrote and where to answer them, and the opt-out list forgets their
14
+ * address.
15
+ */
16
+ /** What an erased comment is signed with. */
17
+ export declare const ERASED_NAME = "Anonymous";
18
+ /** What was found for an address, or what was erased for it. */
19
+ export interface PersonalDataReport {
20
+ /** Comments carrying the address. */
21
+ readonly comments: number;
22
+ /** Contact messages sent from it. */
23
+ readonly messages: number;
24
+ /** Whether it is on the list of addresses not to email about replies. */
25
+ readonly optedOut: boolean;
26
+ }
27
+ /** Where a person's data can be. */
28
+ export interface PersonalDataStores {
29
+ /** The comment files and the index over them. */
30
+ readonly records: CommentRecords;
31
+ /** The data directory the contact messages and the opt-out list live in. */
32
+ readonly dataDir: string;
33
+ }
34
+ /** A comment with everything that says who wrote it taken off. */
35
+ export declare function eraseCommentAuthor(comment: CommentRecord): CommentRecord;
36
+ /** What the site holds under `email`, without changing any of it. */
37
+ export declare function findPersonalData(stores: PersonalDataStores, email: string): PersonalDataReport;
38
+ /**
39
+ * Erase everything the site holds under `email`, and say what was erased.
40
+ *
41
+ * Both this and {@link findPersonalData} read the files rather than the index,
42
+ * so a comment written by hand since the last boot is counted and erased.
43
+ */
44
+ export declare function erasePersonalData(stores: PersonalDataStores, email: string): Promise<PersonalDataReport>;
45
+ //# sourceMappingURL=erase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"erase.d.ts","sourceRoot":"","sources":["../../src/privacy/erase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D;;;;;;;;;;;;GAYG;AAEH,6CAA6C;AAC7C,eAAO,MAAM,WAAW,cAAc,CAAC;AAEvC,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,oCAAoC;AACpC,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAYD,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAYxE;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAU9F;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,CAAC,CAoB7B"}
@@ -0,0 +1,77 @@
1
+ import { commentSlugs, readComments, rewriteComments } from '../comments/records.js';
2
+ import { deleteContactMessage, listContactMessages } from '../contact/records.js';
3
+ import { hasOptedOut, removeCommentOptOut } from '../notifications/optouts.js';
4
+ import { withRedacted } from './retention.js';
5
+ /**
6
+ * Erasing one person's personal data when they ask (TASK-135).
7
+ *
8
+ * A person is found by the email they gave, because that is the one thing
9
+ * they can prove is theirs: a name is anybody's to type. Their comments stay
10
+ * where they are in the thread, with their status and their words, under the
11
+ * name {@link ERASED_NAME} and with nothing else of theirs left on them; a
12
+ * comment deleted outright would take every reply to it out of the thread,
13
+ * and the Comments screen is there for a comment whose words have to go too.
14
+ * Their contact messages are deleted, because a message is nothing but what
15
+ * they wrote and where to answer them, and the opt-out list forgets their
16
+ * address.
17
+ */
18
+ /** What an erased comment is signed with. */
19
+ export const ERASED_NAME = 'Anonymous';
20
+ /** An address as it is compared: trimmed and folded, as the opt-out list keeps it. */
21
+ function folded(email) {
22
+ return email.trim().toLowerCase();
23
+ }
24
+ /** Whether a comment was left under `email`. */
25
+ function isTheirs(comment, email) {
26
+ return comment.author.email !== null && folded(comment.author.email) === email;
27
+ }
28
+ /** A comment with everything that says who wrote it taken off. */
29
+ export function eraseCommentAuthor(comment) {
30
+ return {
31
+ ...comment,
32
+ author: { name: ERASED_NAME, url: null, email: null, avatar: null },
33
+ addressHash: null,
34
+ notify: false,
35
+ redacted: withRedacted(comment.redacted, [
36
+ 'email',
37
+ ...(comment.addressHash === null ? [] : ['addressHash']),
38
+ 'author',
39
+ ]),
40
+ };
41
+ }
42
+ /** What the site holds under `email`, without changing any of it. */
43
+ export function findPersonalData(stores, email) {
44
+ const wanted = folded(email);
45
+ const { contentDir } = stores.records;
46
+ const comments = commentSlugs(contentDir)
47
+ .flatMap((slug) => readComments(contentDir, slug))
48
+ .filter((comment) => isTheirs(comment, wanted)).length;
49
+ const messages = listContactMessages(stores.dataDir).filter((message) => folded(message.from.email) === wanted).length;
50
+ return { comments, messages, optedOut: hasOptedOut(stores.dataDir, wanted) };
51
+ }
52
+ /**
53
+ * Erase everything the site holds under `email`, and say what was erased.
54
+ *
55
+ * Both this and {@link findPersonalData} read the files rather than the index,
56
+ * so a comment written by hand since the last boot is counted and erased.
57
+ */
58
+ export async function erasePersonalData(stores, email) {
59
+ const wanted = folded(email);
60
+ if (wanted === '')
61
+ return { comments: 0, messages: 0, optedOut: false };
62
+ let comments = 0;
63
+ for (const slug of commentSlugs(stores.records.contentDir)) {
64
+ const changed = await rewriteComments(stores.records, slug, (comment) => isTheirs(comment, wanted) ? eraseCommentAuthor(comment) : undefined);
65
+ comments += changed.length;
66
+ }
67
+ let messages = 0;
68
+ for (const message of listContactMessages(stores.dataDir)) {
69
+ if (folded(message.from.email) !== wanted)
70
+ continue;
71
+ if (await deleteContactMessage(stores.dataDir, message.id))
72
+ messages += 1;
73
+ }
74
+ const optedOut = await removeCommentOptOut(stores.dataDir, wanted);
75
+ return { comments, messages, optedOut };
76
+ }
77
+ //# sourceMappingURL=erase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"erase.js","sourceRoot":"","sources":["../../src/privacy/erase.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * How long this site keeps the personal data its readers hand it (TASK-135).
3
+ *
4
+ * Three periods, each in whole days counted from the moment the data arrived,
5
+ * and `0` for keeping it for as long as the site stands. They are settings in
6
+ * `content/_data/site.json` rather than config: how long a site keeps what
7
+ * people give it is the owner's decision, made on the Discussion screen, and a
8
+ * public file is the right place for a policy a privacy notice will quote.
9
+ */
10
+ /** Days a commenter's email is kept: long enough for a reply to reach them. */
11
+ export declare const RECOMMENDED_COMMENT_EMAIL_RETENTION_DAYS = 180;
12
+ /** Days an address hash is kept: long enough to spot a run of spam. */
13
+ export declare const RECOMMENDED_ADDRESS_HASH_RETENTION_DAYS = 30;
14
+ /** Days a contact message is kept: a year of correspondence. */
15
+ export declare const RECOMMENDED_CONTACT_MESSAGE_RETENTION_DAYS = 365;
16
+ /** The three periods. Zero keeps that data forever. */
17
+ export interface RetentionPolicy {
18
+ /** Days a comment keeps its author's email, and with it their reply subscription. */
19
+ readonly commentEmailDays: number;
20
+ /** Days a comment or a contact message keeps the hash of the address it came from. */
21
+ readonly addressHashDays: number;
22
+ /** Days a contact message is kept at all. */
23
+ readonly contactMessageDays: number;
24
+ }
25
+ /**
26
+ * Whether data that arrived at `at` has outlived a period of `days`.
27
+ *
28
+ * A date that will not parse has no age, so it is never past anything: a hand
29
+ * edit that lost a timestamp should not be what deletes somebody's message.
30
+ */
31
+ export declare function outlived(at: string, days: number, now: Date): boolean;
32
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/privacy/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH,+EAA+E;AAC/E,eAAO,MAAM,wCAAwC,MAAM,CAAC;AAE5D,uEAAuE;AACvE,eAAO,MAAM,uCAAuC,KAAK,CAAC;AAE1D,gEAAgE;AAChE,eAAO,MAAM,0CAA0C,MAAM,CAAC;AAE9D,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,sFAAsF;IACtF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,6CAA6C;IAC7C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAID;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAKrE"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * How long this site keeps the personal data its readers hand it (TASK-135).
3
+ *
4
+ * Three periods, each in whole days counted from the moment the data arrived,
5
+ * and `0` for keeping it for as long as the site stands. They are settings in
6
+ * `content/_data/site.json` rather than config: how long a site keeps what
7
+ * people give it is the owner's decision, made on the Discussion screen, and a
8
+ * public file is the right place for a policy a privacy notice will quote.
9
+ */
10
+ /*
11
+ * A site that never chose a period keeps everything: a `site.json` without the
12
+ * keys reads as `0`, so upgrading the package never deletes anything. The
13
+ * periods below are what `geekity init` writes for a new site and what the
14
+ * Discussion screen recommends to an existing one.
15
+ */
16
+ /** Days a commenter's email is kept: long enough for a reply to reach them. */
17
+ export const RECOMMENDED_COMMENT_EMAIL_RETENTION_DAYS = 180;
18
+ /** Days an address hash is kept: long enough to spot a run of spam. */
19
+ export const RECOMMENDED_ADDRESS_HASH_RETENTION_DAYS = 30;
20
+ /** Days a contact message is kept: a year of correspondence. */
21
+ export const RECOMMENDED_CONTACT_MESSAGE_RETENTION_DAYS = 365;
22
+ const DAY_MS = 24 * 60 * 60 * 1000;
23
+ /**
24
+ * Whether data that arrived at `at` has outlived a period of `days`.
25
+ *
26
+ * A date that will not parse has no age, so it is never past anything: a hand
27
+ * edit that lost a timestamp should not be what deletes somebody's message.
28
+ */
29
+ export function outlived(at, days, now) {
30
+ if (days <= 0)
31
+ return false;
32
+ const arrived = Date.parse(at);
33
+ if (Number.isNaN(arrived))
34
+ return false;
35
+ return now.getTime() - arrived > days * DAY_MS;
36
+ }
37
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/privacy/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AAEH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wCAAwC,GAAG,GAAG,CAAC;AAE5D,uEAAuE;AACvE,MAAM,CAAC,MAAM,uCAAuC,GAAG,EAAE,CAAC;AAE1D,gEAAgE;AAChE,MAAM,CAAC,MAAM,0CAA0C,GAAG,GAAG,CAAC;AAY9D,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAU,EAAE,IAAY,EAAE,GAAS;IAC1D,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;AACjD,CAAC"}
@@ -0,0 +1,79 @@
1
+ import type { AdminStore, CommentRecord, RedactedField } from '../admin/store.ts';
2
+ import type { ResolvedConfig } from '../config.ts';
3
+ import type { ContactMessage } from '../contact/records.ts';
4
+ import type { NotificationTimers } from '../notifications/digest.ts';
5
+ import type { RetentionPolicy } from './policy.ts';
6
+ /**
7
+ * The retention sweep (TASK-135): what removes the personal data a site has
8
+ * kept for longer than it said it would.
9
+ *
10
+ * The files are the truth (decision-9), so the sweep reads them rather than
11
+ * the index: every comment file under `content/_data/comments/` and every
12
+ * message under `data/contact/`, each rewritten inside the lock every other
13
+ * writer of that file takes, and each comment it changes put back into the
14
+ * index in the same step. Removing a value that is already gone is not a
15
+ * change, so a sweep run twice, or run again after a crash halfway through,
16
+ * finishes in the same place and writes nothing the second time.
17
+ */
18
+ /** How often the sweep runs once the site serves. */
19
+ export declare const RETENTION_SWEEP_MS: number;
20
+ /** What one sweep changed. */
21
+ export interface RetentionReport {
22
+ /** Comments that lost an email, an address hash or both. */
23
+ readonly comments: number;
24
+ /** Contact messages deleted for being older than the site keeps them. */
25
+ readonly messagesDeleted: number;
26
+ /** Contact messages that lost their address hash. */
27
+ readonly messagesRedacted: number;
28
+ }
29
+ /**
30
+ * One comment as the policy leaves it, or `undefined` when it has nothing to
31
+ * give up. The email goes with the reply subscription it served, since there
32
+ * is nowhere left to send one; the thread, the status and the words stay.
33
+ */
34
+ export declare function expireComment(comment: CommentRecord, policy: RetentionPolicy, now: Date): CommentRecord | undefined;
35
+ /** What a contact message comes to under the policy. */
36
+ export type ContactExpiry = {
37
+ readonly kind: 'keep';
38
+ } | {
39
+ readonly kind: 'delete';
40
+ } | {
41
+ readonly kind: 'redact';
42
+ readonly message: ContactMessage;
43
+ };
44
+ /**
45
+ * One contact message under the policy: kept as it is, deleted outright, or
46
+ * kept without the hash of the address it came from. A message is its
47
+ * sender's words and address together, so it has no smaller form to shrink
48
+ * to once its own period is up.
49
+ */
50
+ export declare function expireContactMessage(message: ContactMessage, policy: RetentionPolicy, now: Date): ContactExpiry;
51
+ /** `held` with `added` joined in, in {@link REDACTED_FIELDS} order. */
52
+ export declare function withRedacted(held: readonly RedactedField[] | undefined, added: readonly RedactedField[]): RedactedField[];
53
+ /** What {@link createRetentionService} needs. */
54
+ export interface CreateRetentionServiceOptions {
55
+ /** The comment index the sweep keeps in step with the files. */
56
+ readonly admin: AdminStore;
57
+ /** Where the files are, and the clock the ages are measured by. */
58
+ readonly config: Pick<ResolvedConfig, 'contentDir' | 'dataDir' | 'now'>;
59
+ /** Defaults to the real interval timer. */
60
+ readonly timers?: NotificationTimers | undefined;
61
+ /** Defaults to `console`. */
62
+ readonly logger?: {
63
+ warn(message: string): void;
64
+ } | undefined;
65
+ }
66
+ /** The sweep, on a timer once the site serves. */
67
+ export interface RetentionService {
68
+ /** Sweep now, queued behind any sweep already running, and say what changed. */
69
+ sweep(): Promise<RetentionReport>;
70
+ /** Sweep now and then every {@link RETENTION_SWEEP_MS}. Safe twice. */
71
+ start(): void;
72
+ /** Stop the timer. Safe before starting and safe twice. */
73
+ stop(): void;
74
+ /** Resolve once the sweep in flight has finished. */
75
+ settled(): Promise<void>;
76
+ }
77
+ /** Build the retention sweep for one site. */
78
+ export declare function createRetentionService(options: CreateRetentionServiceOptions): RetentionService;
79
+ //# sourceMappingURL=retention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/privacy/retention.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIlF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;;;;;;GAWG;AAEH,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,QAAqB,CAAC;AAErD,8BAA8B;AAC9B,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,qDAAqD;IACrD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,IAAI,GACR,aAAa,GAAG,SAAS,CAiB3B;AAED,wDAAwD;AACxD,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC;AAElE;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,IAAI,GACR,aAAa,CAMf;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,EAC1C,KAAK,EAAE,SAAS,aAAa,EAAE,GAC9B,aAAa,EAAE,CAGjB;AAED,iDAAiD;AACjD,MAAM,WAAW,6BAA6B;IAC5C,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,CAAC;IACxE,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACjD,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAC;CAC/D;AAED,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IAClC,uEAAuE;IACvE,KAAK,IAAI,IAAI,CAAC;IACd,2DAA2D;IAC3D,IAAI,IAAI,IAAI,CAAC;IACb,qDAAqD;IACrD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAID,8CAA8C;AAC9C,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,6BAA6B,GAAG,gBAAgB,CAsE/F"}
@@ -0,0 +1,133 @@
1
+ import { REDACTED_FIELDS } from '../admin/store.js';
2
+ import { readSiteSettings, retentionPolicyOf } from '../admin/settings.js';
3
+ import { commentSlugs, rewriteComments } from '../comments/records.js';
4
+ import { deleteContactMessage, listContactMessages, updateContactMessage, } from '../contact/records.js';
5
+ import { systemNotificationTimers } from '../notifications/digest.js';
6
+ import { outlived } from './policy.js';
7
+ /**
8
+ * The retention sweep (TASK-135): what removes the personal data a site has
9
+ * kept for longer than it said it would.
10
+ *
11
+ * The files are the truth (decision-9), so the sweep reads them rather than
12
+ * the index: every comment file under `content/_data/comments/` and every
13
+ * message under `data/contact/`, each rewritten inside the lock every other
14
+ * writer of that file takes, and each comment it changes put back into the
15
+ * index in the same step. Removing a value that is already gone is not a
16
+ * change, so a sweep run twice, or run again after a crash halfway through,
17
+ * finishes in the same place and writes nothing the second time.
18
+ */
19
+ /** How often the sweep runs once the site serves. */
20
+ export const RETENTION_SWEEP_MS = 6 * 60 * 60 * 1000;
21
+ /**
22
+ * One comment as the policy leaves it, or `undefined` when it has nothing to
23
+ * give up. The email goes with the reply subscription it served, since there
24
+ * is nowhere left to send one; the thread, the status and the words stay.
25
+ */
26
+ export function expireComment(comment, policy, now) {
27
+ const email = comment.author.email !== null && outlived(comment.submitted, policy.commentEmailDays, now);
28
+ const address = comment.addressHash !== null && outlived(comment.submitted, policy.addressHashDays, now);
29
+ if (!email && !address)
30
+ return undefined;
31
+ return {
32
+ ...comment,
33
+ author: email ? { ...comment.author, email: null } : comment.author,
34
+ addressHash: address ? null : comment.addressHash,
35
+ notify: email ? false : comment.notify,
36
+ redacted: withRedacted(comment.redacted, [
37
+ ...(email ? ['email'] : []),
38
+ ...(address ? ['addressHash'] : []),
39
+ ]),
40
+ };
41
+ }
42
+ /**
43
+ * One contact message under the policy: kept as it is, deleted outright, or
44
+ * kept without the hash of the address it came from. A message is its
45
+ * sender's words and address together, so it has no smaller form to shrink
46
+ * to once its own period is up.
47
+ */
48
+ export function expireContactMessage(message, policy, now) {
49
+ if (outlived(message.received, policy.contactMessageDays, now))
50
+ return { kind: 'delete' };
51
+ if (message.addressHash !== null && outlived(message.received, policy.addressHashDays, now)) {
52
+ return { kind: 'redact', message: { ...message, addressHash: null } };
53
+ }
54
+ return { kind: 'keep' };
55
+ }
56
+ /** `held` with `added` joined in, in {@link REDACTED_FIELDS} order. */
57
+ export function withRedacted(held, added) {
58
+ const all = new Set([...(held ?? []), ...added]);
59
+ return REDACTED_FIELDS.filter((field) => all.has(field));
60
+ }
61
+ const NOTHING = { comments: 0, messagesDeleted: 0, messagesRedacted: 0 };
62
+ /** Build the retention sweep for one site. */
63
+ export function createRetentionService(options) {
64
+ const { admin, config } = options;
65
+ const timers = options.timers ?? systemNotificationTimers;
66
+ const logger = options.logger ?? console;
67
+ let sweeping = Promise.resolve();
68
+ let handle;
69
+ async function sweepOnce() {
70
+ // Read every time, so a period changed on the Discussion screen holds from
71
+ // the next sweep without a restart.
72
+ const policy = retentionPolicyOf(readSiteSettings(config.contentDir));
73
+ const now = config.now();
74
+ const records = { admin, contentDir: config.contentDir };
75
+ let comments = 0;
76
+ for (const slug of commentSlugs(config.contentDir)) {
77
+ const changed = await rewriteComments(records, slug, (comment) => expireComment(comment, policy, now));
78
+ comments += changed.length;
79
+ }
80
+ let messagesDeleted = 0;
81
+ let messagesRedacted = 0;
82
+ for (const listed of listContactMessages(config.dataDir)) {
83
+ const expiry = expireContactMessage(listed, policy, now);
84
+ if (expiry.kind === 'delete') {
85
+ if (await deleteContactMessage(config.dataDir, listed.id))
86
+ messagesDeleted += 1;
87
+ }
88
+ else if (expiry.kind === 'redact') {
89
+ let redacted = false;
90
+ // Decided again on the message as the lock finds it, so a Mark read
91
+ // that landed since the listing is kept.
92
+ await updateContactMessage(config.dataDir, listed.id, (current) => {
93
+ const again = expireContactMessage(current, policy, now);
94
+ if (again.kind !== 'redact')
95
+ return undefined;
96
+ redacted = true;
97
+ return again.message;
98
+ });
99
+ if (redacted)
100
+ messagesRedacted += 1;
101
+ }
102
+ }
103
+ return { comments, messagesDeleted, messagesRedacted };
104
+ }
105
+ return {
106
+ sweep() {
107
+ const next = sweeping.then(sweepOnce).catch((thrown) => {
108
+ logger.warn(`The retention sweep failed: ${messageOf(thrown)}`);
109
+ return NOTHING;
110
+ });
111
+ sweeping = next;
112
+ return next;
113
+ },
114
+ start() {
115
+ if (handle !== undefined)
116
+ return;
117
+ void this.sweep();
118
+ handle = timers.set(() => void this.sweep(), RETENTION_SWEEP_MS);
119
+ },
120
+ stop() {
121
+ if (handle !== undefined)
122
+ timers.clear(handle);
123
+ handle = undefined;
124
+ },
125
+ async settled() {
126
+ await sweeping;
127
+ },
128
+ };
129
+ }
130
+ function messageOf(error) {
131
+ return error instanceof Error ? error.message : String(error);
132
+ }
133
+ //# sourceMappingURL=retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/privacy/retention.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEvE,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC;;;;;;;;;;;GAWG;AAEH,qDAAqD;AACrD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAYrD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAsB,EACtB,MAAuB,EACvB,GAAS;IAET,MAAM,KAAK,GACT,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IAC7F,MAAM,OAAO,GACX,OAAO,CAAC,WAAW,KAAK,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;IAC3F,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAEzC,OAAO;QACL,GAAG,OAAO;QACV,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM;QACnE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW;QACjD,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM;QACtC,QAAQ,EAAE,YAAY,CAAC,OAAO,CAAC,QAAQ,EAAE;YACvC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,OAAO,CAAW,CAAC,CAAC,CAAC,EAAE,CAAC;YACtC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,aAAa,CAAW,CAAC,CAAC,CAAC,EAAE,CAAC;SAC/C,CAAC;KACH,CAAC;AACJ,CAAC;AAQD;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAuB,EACvB,MAAuB,EACvB,GAAS;IAET,IAAI,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,kBAAkB,EAAE,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC1F,IAAI,OAAO,CAAC,WAAW,KAAK,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,eAAe,EAAE,GAAG,CAAC,EAAE,CAAC;QAC5F,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,CAAC;IACxE,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,YAAY,CAC1B,IAA0C,EAC1C,KAA+B;IAE/B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;IACjD,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3D,CAAC;AA0BD,MAAM,OAAO,GAAoB,EAAE,QAAQ,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC;AAE1F,8CAA8C;AAC9C,MAAM,UAAU,sBAAsB,CAAC,OAAsC;IAC3E,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,wBAAwB,CAAC;IAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC;IACzC,IAAI,QAAQ,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IACnD,IAAI,MAAe,CAAC;IAEpB,KAAK,UAAU,SAAS;QACtB,2EAA2E;QAC3E,oCAAoC;QACpC,MAAM,MAAM,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;QACtE,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC;QAEzD,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YACnD,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAC/D,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,CACpC,CAAC;YACF,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;QAC7B,CAAC;QAED,IAAI,eAAe,GAAG,CAAC,CAAC;QACxB,IAAI,gBAAgB,GAAG,CAAC,CAAC;QACzB,KAAK,MAAM,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACzD,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;YACzD,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7B,IAAI,MAAM,oBAAoB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;oBAAE,eAAe,IAAI,CAAC,CAAC;YAClF,CAAC;iBAAM,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACpC,IAAI,QAAQ,GAAG,KAAK,CAAC;gBACrB,oEAAoE;gBACpE,yCAAyC;gBACzC,MAAM,oBAAoB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE;oBAChE,MAAM,KAAK,GAAG,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;oBACzD,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;wBAAE,OAAO,SAAS,CAAC;oBAC9C,QAAQ,GAAG,IAAI,CAAC;oBAChB,OAAO,KAAK,CAAC,OAAO,CAAC;gBACvB,CAAC,CAAC,CAAC;gBACH,IAAI,QAAQ;oBAAE,gBAAgB,IAAI,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QAED,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC;IACzD,CAAC;IAED,OAAO;QACL,KAAK;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,MAAe,EAAE,EAAE;gBAC9D,MAAM,CAAC,IAAI,CAAC,+BAA+B,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBAChE,OAAO,OAAO,CAAC;YACjB,CAAC,CAAC,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC;YAChB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,KAAK;YACH,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO;YACjC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YAClB,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE,EAAE,kBAAkB,CAAC,CAAC;QACnE,CAAC;QAED,IAAI;YACF,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC/C,MAAM,GAAG,SAAS,CAAC;QACrB,CAAC;QAED,KAAK,CAAC,OAAO;YACX,MAAM,QAAQ,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}