@geekity/cms 0.8.0 → 0.10.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.
- package/README.md +344 -47
- package/admin/components/admin-bar.njk +26 -0
- package/admin/components/public-admin-bar.njk +41 -0
- package/admin/layouts/base.njk +1 -0
- package/admin/layouts/shell.njk +8 -19
- package/admin/pages/comments/all.njk +3 -0
- package/admin/pages/settings/discussion.njk +53 -0
- package/admin/pages/settings/email.njk +20 -0
- package/admin/pages/tools/personal-data.njk +67 -0
- package/admin/pages/users/edit.njk +1 -1
- package/admin/static/admin-bar.css +137 -0
- package/admin/static/admin.css +1 -102
- package/dist/admin/comments.d.ts +5 -0
- package/dist/admin/comments.d.ts.map +1 -1
- package/dist/admin/comments.js +15 -1
- package/dist/admin/comments.js.map +1 -1
- package/dist/admin/headers.d.ts +14 -5
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +18 -6
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/index.d.ts +2 -2
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +5 -1
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/personal-data.d.ts +35 -0
- package/dist/admin/personal-data.d.ts.map +1 -0
- package/dist/admin/personal-data.js +94 -0
- package/dist/admin/personal-data.js.map +1 -0
- package/dist/admin/preview.d.ts.map +1 -1
- package/dist/admin/preview.js +1 -1
- package/dist/admin/preview.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +20 -3
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/session.d.ts +42 -1
- package/dist/admin/session.d.ts.map +1 -1
- package/dist/admin/session.js +74 -4
- package/dist/admin/session.js.map +1 -1
- package/dist/admin/settings-discussion.d.ts.map +1 -1
- package/dist/admin/settings-discussion.js +9 -1
- package/dist/admin/settings-discussion.js.map +1 -1
- package/dist/admin/settings-email.d.ts.map +1 -1
- package/dist/admin/settings-email.js +10 -1
- package/dist/admin/settings-email.js.map +1 -1
- package/dist/admin/settings.d.ts +39 -0
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +144 -0
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/store.d.ts +65 -0
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +130 -5
- package/dist/admin/store.js.map +1 -1
- package/dist/admin/templates.d.ts +2 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +2 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/tools.d.ts +2 -0
- package/dist/admin/tools.d.ts.map +1 -1
- package/dist/admin/tools.js +3 -2
- package/dist/admin/tools.js.map +1 -1
- package/dist/avatars/avatars.d.ts +112 -0
- package/dist/avatars/avatars.d.ts.map +1 -0
- package/dist/avatars/avatars.js +259 -0
- package/dist/avatars/avatars.js.map +1 -0
- package/dist/avatars/index.d.ts +4 -0
- package/dist/avatars/index.d.ts.map +1 -0
- package/dist/avatars/index.js +3 -0
- package/dist/avatars/index.js.map +1 -0
- package/dist/avatars/routes.d.ts +11 -0
- package/dist/avatars/routes.d.ts.map +1 -0
- package/dist/avatars/routes.js +33 -0
- package/dist/avatars/routes.js.map +1 -0
- package/dist/comments/index.d.ts +2 -2
- package/dist/comments/index.d.ts.map +1 -1
- package/dist/comments/index.js +1 -1
- package/dist/comments/index.js.map +1 -1
- package/dist/comments/records.d.ts +58 -2
- package/dist/comments/records.d.ts.map +1 -1
- package/dist/comments/records.js +244 -47
- package/dist/comments/records.js.map +1 -1
- package/dist/comments/routes.d.ts.map +1 -1
- package/dist/comments/routes.js +8 -2
- package/dist/comments/routes.js.map +1 -1
- package/dist/comments/viewer.d.ts +13 -0
- package/dist/comments/viewer.d.ts.map +1 -1
- package/dist/comments/viewer.js +13 -1
- package/dist/comments/viewer.js.map +1 -1
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -1
- package/dist/contact/index.d.ts +1 -1
- package/dist/contact/index.d.ts.map +1 -1
- package/dist/contact/index.js +1 -1
- package/dist/contact/index.js.map +1 -1
- package/dist/contact/records.d.ts +8 -2
- package/dist/contact/records.d.ts.map +1 -1
- package/dist/contact/records.js +13 -6
- package/dist/contact/records.js.map +1 -1
- package/dist/env.d.ts +25 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/delivery.d.ts +3 -1
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +6 -1
- package/dist/federation/delivery.js.map +1 -1
- package/dist/federation/federation.d.ts +3 -0
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/inbox.d.ts +7 -5
- package/dist/federation/inbox.d.ts.map +1 -1
- package/dist/federation/inbox.js +15 -32
- package/dist/federation/inbox.js.map +1 -1
- package/dist/federation/index.d.ts +2 -0
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js +1 -0
- package/dist/federation/index.js.map +1 -1
- package/dist/federation/mount.js +6 -1
- package/dist/federation/mount.js.map +1 -1
- package/dist/federation/profiles.d.ts +95 -0
- package/dist/federation/profiles.d.ts.map +1 -0
- package/dist/federation/profiles.js +171 -0
- package/dist/federation/profiles.js.map +1 -0
- package/dist/federation/quotes.d.ts +2 -0
- package/dist/federation/quotes.d.ts.map +1 -1
- package/dist/federation/quotes.js +4 -0
- package/dist/federation/quotes.js.map +1 -1
- package/dist/federation/relays.d.ts +3 -1
- package/dist/federation/relays.d.ts.map +1 -1
- package/dist/federation/relays.js +6 -1
- package/dist/federation/relays.js.map +1 -1
- package/dist/federation/replies.d.ts +36 -4
- package/dist/federation/replies.d.ts.map +1 -1
- package/dist/federation/replies.js +71 -15
- package/dist/federation/replies.js.map +1 -1
- package/dist/images/icons.d.ts.map +1 -1
- package/dist/images/icons.js +3 -2
- package/dist/images/icons.js.map +1 -1
- package/dist/images/index.d.ts +2 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/markup.d.ts +15 -4
- package/dist/images/markup.d.ts.map +1 -1
- package/dist/images/markup.js +24 -15
- package/dist/images/markup.js.map +1 -1
- package/dist/images/paths.d.ts +11 -0
- package/dist/images/paths.d.ts.map +1 -1
- package/dist/images/paths.js +17 -1
- package/dist/images/paths.js.map +1 -1
- package/dist/images/variants.d.ts +13 -3
- package/dist/images/variants.d.ts.map +1 -1
- package/dist/images/variants.js +13 -11
- package/dist/images/variants.js.map +1 -1
- package/dist/index.d.ts +34 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -18
- package/dist/index.js.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +1 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/optouts.d.ts +6 -0
- package/dist/notifications/optouts.d.ts.map +1 -1
- package/dist/notifications/optouts.js +17 -0
- package/dist/notifications/optouts.js.map +1 -1
- package/dist/notifications/routes.js +1 -1
- package/dist/notifications/routes.js.map +1 -1
- package/dist/privacy/erase.d.ts +45 -0
- package/dist/privacy/erase.d.ts.map +1 -0
- package/dist/privacy/erase.js +76 -0
- package/dist/privacy/erase.js.map +1 -0
- package/dist/privacy/policy.d.ts +32 -0
- package/dist/privacy/policy.d.ts.map +1 -0
- package/dist/privacy/policy.js +37 -0
- package/dist/privacy/policy.js.map +1 -0
- package/dist/privacy/retention.d.ts +79 -0
- package/dist/privacy/retention.d.ts.map +1 -0
- package/dist/privacy/retention.js +133 -0
- package/dist/privacy/retention.js.map +1 -0
- package/dist/web/admin-bar.d.ts +21 -0
- package/dist/web/admin-bar.d.ts.map +1 -0
- package/dist/web/admin-bar.js +145 -0
- package/dist/web/admin-bar.js.map +1 -0
- package/dist/web/assets.d.ts +29 -1
- package/dist/web/assets.d.ts.map +1 -1
- package/dist/web/assets.js +48 -3
- package/dist/web/assets.js.map +1 -1
- package/dist/web/compression.d.ts +19 -0
- package/dist/web/compression.d.ts.map +1 -0
- package/dist/web/compression.js +160 -0
- package/dist/web/compression.js.map +1 -0
- package/dist/web/context.d.ts +5 -1
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +5 -2
- package/dist/web/context.js.map +1 -1
- package/dist/web/conversation.d.ts +9 -1
- package/dist/web/conversation.d.ts.map +1 -1
- package/dist/web/conversation.js +79 -17
- package/dist/web/conversation.js.map +1 -1
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/index.js.map +1 -1
- package/dist/web/maintenance.d.ts.map +1 -1
- package/dist/web/maintenance.js +7 -2
- package/dist/web/maintenance.js.map +1 -1
- package/dist/web/render.d.ts +27 -0
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +77 -38
- package/dist/web/render.js.map +1 -1
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +67 -28
- package/dist/web/routes.js.map +1 -1
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +9 -2
- package/dist/web/templates.js.map +1 -1
- package/dist/web/well-known.d.ts +21 -0
- package/dist/web/well-known.d.ts.map +1 -0
- package/dist/web/well-known.js +66 -0
- package/dist/web/well-known.js.map +1 -0
- package/dist/webmention/fetch-public.d.ts +34 -0
- package/dist/webmention/fetch-public.d.ts.map +1 -0
- package/dist/webmention/fetch-public.js +101 -0
- package/dist/webmention/fetch-public.js.map +1 -0
- package/dist/webmention/reply-context.d.ts.map +1 -1
- package/dist/webmention/reply-context.js +12 -89
- package/dist/webmention/reply-context.js.map +1 -1
- package/dist/webmention/service.d.ts.map +1 -1
- package/dist/webmention/service.js +5 -1
- package/dist/webmention/service.js.map +1 -1
- package/package.json +1 -1
- package/templates/site/content/_data/site.json +7 -1
- package/templates/site/content/pages/privacy.md +127 -0
- package/themes/default/README.md +165 -22
- package/themes/default/layouts/base.njk +51 -10
|
@@ -0,0 +1,76 @@
|
|
|
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 comments = commentSlugs(stores.records.contentDir)
|
|
46
|
+
.flatMap((slug) => readComments(stores.records, slug))
|
|
47
|
+
.filter((comment) => isTheirs(comment, wanted)).length;
|
|
48
|
+
const messages = listContactMessages(stores.dataDir).filter((message) => folded(message.from.email) === wanted).length;
|
|
49
|
+
return { comments, messages, optedOut: hasOptedOut(stores.dataDir, wanted) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Erase everything the site holds under `email`, and say what was erased.
|
|
53
|
+
*
|
|
54
|
+
* Both this and {@link findPersonalData} read the files rather than the index,
|
|
55
|
+
* so a comment written by hand since the last boot is counted and erased.
|
|
56
|
+
*/
|
|
57
|
+
export async function erasePersonalData(stores, email) {
|
|
58
|
+
const wanted = folded(email);
|
|
59
|
+
if (wanted === '')
|
|
60
|
+
return { comments: 0, messages: 0, optedOut: false };
|
|
61
|
+
let comments = 0;
|
|
62
|
+
for (const slug of commentSlugs(stores.records.contentDir)) {
|
|
63
|
+
const changed = await rewriteComments(stores.records, slug, (comment) => isTheirs(comment, wanted) ? eraseCommentAuthor(comment) : undefined);
|
|
64
|
+
comments += changed.length;
|
|
65
|
+
}
|
|
66
|
+
let messages = 0;
|
|
67
|
+
for (const message of listContactMessages(stores.dataDir)) {
|
|
68
|
+
if (folded(message.from.email) !== wanted)
|
|
69
|
+
continue;
|
|
70
|
+
if (await deleteContactMessage(stores.dataDir, message.id))
|
|
71
|
+
messages += 1;
|
|
72
|
+
}
|
|
73
|
+
const optedOut = await removeCommentOptOut(stores.dataDir, wanted);
|
|
74
|
+
return { comments, messages, optedOut };
|
|
75
|
+
}
|
|
76
|
+
//# 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, dataDir: config.dataDir };
|
|
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":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { GeekityEnv } from '../env.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The admin bar across the top of the public site, for a signed-in user
|
|
5
|
+
* (TASK-183).
|
|
6
|
+
*
|
|
7
|
+
* Where the public site's pages learn who is reading them. An anonymous request
|
|
8
|
+
* goes through untouched, so what everybody else is sent, headers and bytes, is what
|
|
9
|
+
* it was before the bar existed. For a signed-in one the account is put on the
|
|
10
|
+
* context for the handlers, and whatever HTML they answer with, a document, a
|
|
11
|
+
* listing, a search, the 404 or the 500, gets the bar straight after its
|
|
12
|
+
* `<body>` tag, the stylesheet that makes room for it in `<head>`, the
|
|
13
|
+
* `geekity-admin-bar` class on `<html>`, and the headers of a page drawn for
|
|
14
|
+
* one reader: private, no-store, and no validator. Anything that is not HTML, the JSON, the
|
|
15
|
+
* Markdown, the feeds, ActivityPub, is left exactly as it was.
|
|
16
|
+
*
|
|
17
|
+
* Not inside `/admin`, which has a bar of its own and a CSP that would refuse
|
|
18
|
+
* this one's inline stylesheet.
|
|
19
|
+
*/
|
|
20
|
+
export declare const publicAdminBar: MiddlewareHandler<GeekityEnv>;
|
|
21
|
+
//# sourceMappingURL=admin-bar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-bar.d.ts","sourceRoot":"","sources":["../../src/web/admin-bar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAYvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAU5C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc,EAAE,iBAAiB,CAAC,UAAU,CAwBxD,CAAC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { editorPath, newEditorPath, PAGE_KIND, POST_KIND } from '../admin/documents.js';
|
|
2
|
+
import { LOGOUT_PATH } from '../admin/routes.js';
|
|
3
|
+
import { ADMIN_PREFIX } from '../admin/session.js';
|
|
4
|
+
import { ADMIN_TEMPLATES, createAdminTemplateEnvironment } from '../admin/templates.js';
|
|
5
|
+
import { editUserPath } from '../admin/users.js';
|
|
6
|
+
import { signedInAccount } from '../comments/viewer.js';
|
|
7
|
+
import { PRIVATE_CACHE_CONTROL } from './negotiate.js';
|
|
8
|
+
import { requestPath } from './routes.js';
|
|
9
|
+
/** The editor screens each kind of document is written in. */
|
|
10
|
+
const KINDS = { post: POST_KIND, page: PAGE_KIND };
|
|
11
|
+
/** Built on the first signed-in page; the admin's own is closed over by its mount. */
|
|
12
|
+
let environment;
|
|
13
|
+
/**
|
|
14
|
+
* The admin bar across the top of the public site, for a signed-in user
|
|
15
|
+
* (TASK-183).
|
|
16
|
+
*
|
|
17
|
+
* Where the public site's pages learn who is reading them. An anonymous request
|
|
18
|
+
* goes through untouched, so what everybody else is sent, headers and bytes, is what
|
|
19
|
+
* it was before the bar existed. For a signed-in one the account is put on the
|
|
20
|
+
* context for the handlers, and whatever HTML they answer with, a document, a
|
|
21
|
+
* listing, a search, the 404 or the 500, gets the bar straight after its
|
|
22
|
+
* `<body>` tag, the stylesheet that makes room for it in `<head>`, the
|
|
23
|
+
* `geekity-admin-bar` class on `<html>`, and the headers of a page drawn for
|
|
24
|
+
* one reader: private, no-store, and no validator. Anything that is not HTML, the JSON, the
|
|
25
|
+
* Markdown, the feeds, ActivityPub, is left exactly as it was.
|
|
26
|
+
*
|
|
27
|
+
* Not inside `/admin`, which has a bar of its own and a CSP that would refuse
|
|
28
|
+
* this one's inline stylesheet.
|
|
29
|
+
*/
|
|
30
|
+
export const publicAdminBar = async (c, next) => {
|
|
31
|
+
const pathname = requestPath(c);
|
|
32
|
+
if (pathname === ADMIN_PREFIX || pathname.startsWith(`${ADMIN_PREFIX}/`))
|
|
33
|
+
return await next();
|
|
34
|
+
const account = signedInAccount(c);
|
|
35
|
+
if (account === undefined)
|
|
36
|
+
return await next();
|
|
37
|
+
c.set('signedIn', account);
|
|
38
|
+
await next();
|
|
39
|
+
if (!(c.res.headers.get('content-type') ?? '').startsWith('text/html'))
|
|
40
|
+
return;
|
|
41
|
+
const headers = new Headers(c.res.headers);
|
|
42
|
+
headers.set('cache-control', PRIVATE_CACHE_CONTROL);
|
|
43
|
+
headers.delete('etag');
|
|
44
|
+
headers.delete('last-modified');
|
|
45
|
+
headers.delete('content-length');
|
|
46
|
+
const { status } = c.res;
|
|
47
|
+
const body = withAdminBar(withoutSpeculationRules(await c.res.text()), renderBar(c, account));
|
|
48
|
+
// Cleared first, because assigning over a response copies its headers onto
|
|
49
|
+
// the new one, and the validators being dropped would come straight back.
|
|
50
|
+
c.res = undefined;
|
|
51
|
+
c.res = new Response(body, { status, headers });
|
|
52
|
+
};
|
|
53
|
+
/** The bar's markup for this reader and this page. */
|
|
54
|
+
function renderBar(c, account) {
|
|
55
|
+
environment ??= createAdminTemplateEnvironment({ noCache: c.var.config.watch });
|
|
56
|
+
const { user } = account;
|
|
57
|
+
const shown = c.var.shownDocument;
|
|
58
|
+
return environment
|
|
59
|
+
.render(ADMIN_TEMPLATES.publicAdminBar, {
|
|
60
|
+
site: c.var.renderer.site(),
|
|
61
|
+
adminUrl: ADMIN_PREFIX,
|
|
62
|
+
barLinks: [
|
|
63
|
+
{ label: '+ New', url: newEditorPath(POST_KIND) },
|
|
64
|
+
...(shown === undefined
|
|
65
|
+
? []
|
|
66
|
+
: [
|
|
67
|
+
{
|
|
68
|
+
label: `Edit ${capitalized(KINDS[shown.type].singular)}`,
|
|
69
|
+
url: editorPath(KINDS[shown.type], shown.slug),
|
|
70
|
+
},
|
|
71
|
+
]),
|
|
72
|
+
],
|
|
73
|
+
me: { name: user.profile?.displayName ?? user.username, url: editUserPath(user.id) },
|
|
74
|
+
logoutUrl: LOGOUT_PATH,
|
|
75
|
+
csrfToken: account.csrfToken,
|
|
76
|
+
})
|
|
77
|
+
.trim();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `html` with every `<script type="speculationrules">` taken out (TASK-140).
|
|
81
|
+
*
|
|
82
|
+
* A signed-in reader's pages are drawn for them alone and never stored, so a
|
|
83
|
+
* prefetch is a second render nobody may see, and a speculative request that
|
|
84
|
+
* carries a session is one the site would rather not be sent. Removed here
|
|
85
|
+
* rather than left to each theme, so a theme's rules stay one static block and
|
|
86
|
+
* the anonymous page keeps its bytes.
|
|
87
|
+
*/
|
|
88
|
+
function withoutSpeculationRules(html) {
|
|
89
|
+
return html.replace(/<script\b[^>]*\btype\s*=\s*(["']?)speculationrules\1[^>]*>[\s\S]*?<\/script\s*>/gi, '');
|
|
90
|
+
}
|
|
91
|
+
/** The class on `<html>` of a page that carries the bar, for a theme to key off. */
|
|
92
|
+
const ROOT_CLASS = 'geekity-admin-bar';
|
|
93
|
+
/**
|
|
94
|
+
* The room the bar takes at the top of the page, set from outside its shadow
|
|
95
|
+
* root because the bar is fixed and takes none by itself.
|
|
96
|
+
*
|
|
97
|
+
* `--geekity-admin-bar-height` is the bar's height, for a theme's own sticky
|
|
98
|
+
* or fixed header to sit under it. The script in the bar replaces it with the
|
|
99
|
+
* height it measures; until then, or without script, it is one 40px line, and
|
|
100
|
+
* two on a screen narrow enough for the bar to wrap.
|
|
101
|
+
*
|
|
102
|
+
* The page moves down by a margin on `<html>`, as it does under WordPress's
|
|
103
|
+
* bar: a theme's padding on `<html>` or `<body>` is left alone, `!important`
|
|
104
|
+
* outlasts a reset such as `html, body { margin: 0 }`, and the root's
|
|
105
|
+
* background still fills the canvas behind the bar. `scroll-padding-top` is
|
|
106
|
+
* under `:where()` so that a theme that sets its own, for a sticky header of
|
|
107
|
+
* its own, keeps it.
|
|
108
|
+
*/
|
|
109
|
+
const OFFSET_STYLE = `<style id="geekity-admin-bar-offset">
|
|
110
|
+
:root { --geekity-admin-bar-height: 40px; }
|
|
111
|
+
@media (max-width: 600px) { :root { --geekity-admin-bar-height: 80px; } }
|
|
112
|
+
html { margin-top: var(--geekity-admin-bar-height) !important; }
|
|
113
|
+
:where(html) { scroll-padding-top: var(--geekity-admin-bar-height); }
|
|
114
|
+
</style>`;
|
|
115
|
+
/**
|
|
116
|
+
* `html` with `bar` straight after its opening `<body>` tag, the offset
|
|
117
|
+
* stylesheet before `</head>`, and the root class on `<html>`; unchanged when
|
|
118
|
+
* it has no `<body>` tag to put the bar after.
|
|
119
|
+
*/
|
|
120
|
+
function withAdminBar(html, bar) {
|
|
121
|
+
const body = /<body\b[^>]*>/i.exec(html);
|
|
122
|
+
if (body === null)
|
|
123
|
+
return html;
|
|
124
|
+
const afterBody = body.index + body[0].length;
|
|
125
|
+
const head = /<\/head\s*>/i.exec(html.slice(0, body.index));
|
|
126
|
+
const withBar = head === null
|
|
127
|
+
? `${html.slice(0, afterBody)}${OFFSET_STYLE}${bar}${html.slice(afterBody)}`
|
|
128
|
+
: `${html.slice(0, head.index)}${OFFSET_STYLE}${html.slice(head.index, afterBody)}${bar}${html.slice(afterBody)}`;
|
|
129
|
+
return withRootClass(withBar);
|
|
130
|
+
}
|
|
131
|
+
/** `html` with ROOT_CLASS added to its `<html>` tag's classes. */
|
|
132
|
+
function withRootClass(html) {
|
|
133
|
+
const tag = /<html\b[^>]*>/i.exec(html);
|
|
134
|
+
if (tag === null)
|
|
135
|
+
return html;
|
|
136
|
+
const attribute = /(\sclass\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
137
|
+
const classed = attribute.test(tag[0])
|
|
138
|
+
? tag[0].replace(attribute, (_match, lead, double, single, bare) => `${lead}"${ROOT_CLASS} ${(double ?? single ?? bare ?? '').replaceAll('"', '"')}"`)
|
|
139
|
+
: tag[0].replace(/^<html\b/i, `<html class="${ROOT_CLASS}"`);
|
|
140
|
+
return `${html.slice(0, tag.index)}${classed}${html.slice(tag.index + tag[0].length)}`;
|
|
141
|
+
}
|
|
142
|
+
function capitalized(word) {
|
|
143
|
+
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=admin-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-bar.js","sourceRoot":"","sources":["../../src/web/admin-bar.ts"],"names":[],"mappings":"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"}
|
package/dist/web/assets.d.ts
CHANGED
|
@@ -17,8 +17,15 @@ export declare const UPLOAD_ASSET_MAX_AGE = 86400;
|
|
|
17
17
|
export declare function findUpload(relative: string, contentDir: string): StaticAsset | undefined;
|
|
18
18
|
/** Directory inside a theme that holds the files served over HTTP. */
|
|
19
19
|
export declare const THEME_STATIC_DIR = "static";
|
|
20
|
-
/** How long a browser may keep a theme asset. One hour. */
|
|
20
|
+
/** How long a browser may keep a theme asset at its unhashed URL. One hour. */
|
|
21
21
|
export declare const THEME_ASSET_MAX_AGE = 3600;
|
|
22
|
+
/**
|
|
23
|
+
* How long a browser may keep a file whose URL names its bytes. A year, the
|
|
24
|
+
* longest lifetime HTTP caches are asked to honour.
|
|
25
|
+
*/
|
|
26
|
+
export declare const IMMUTABLE_ASSET_MAX_AGE = 31536000;
|
|
27
|
+
/** The query parameter a fingerprinted theme asset URL carries its hash in. */
|
|
28
|
+
export declare const ASSET_VERSION_PARAM = "v";
|
|
22
29
|
/** A file on disk, located and described, ready to be served. */
|
|
23
30
|
export interface StaticAsset {
|
|
24
31
|
/** Absolute path on disk. */
|
|
@@ -52,7 +59,28 @@ export declare function findThemeAsset(relative: string, themeDirs: readonly str
|
|
|
52
59
|
export interface AssetResponseOptions {
|
|
53
60
|
/** Seconds. Defaults to {@link THEME_ASSET_MAX_AGE}. */
|
|
54
61
|
maxAge?: number | undefined;
|
|
62
|
+
/** Whether the bytes at this URL can never change, so a browser need not revalidate. */
|
|
63
|
+
immutable?: boolean | undefined;
|
|
55
64
|
}
|
|
65
|
+
/** The lifetime of a response whose URL names its bytes. */
|
|
66
|
+
export declare const IMMUTABLE_ASSET: AssetResponseOptions;
|
|
67
|
+
/**
|
|
68
|
+
* A short hash of an asset's bytes, the part of a fingerprinted URL that
|
|
69
|
+
* changes when the file does.
|
|
70
|
+
*
|
|
71
|
+
* Memoised per file on the size-and-mtime validator, so a page pays a `stat`
|
|
72
|
+
* rather than a read, and an edit in place is seen on the next render with no
|
|
73
|
+
* restart and no watcher.
|
|
74
|
+
*/
|
|
75
|
+
export declare function assetVersion(asset: StaticAsset): string;
|
|
76
|
+
/**
|
|
77
|
+
* The URL a theme should link one of its files at: under
|
|
78
|
+
* {@link THEME_ASSET_PREFIX} with a hash of the bytes the search path resolves
|
|
79
|
+
* to, so it can be cached for a year and still change the moment the file
|
|
80
|
+
* does. A file no theme has gets its plain URL, which will 404 as it would
|
|
81
|
+
* have anyway.
|
|
82
|
+
*/
|
|
83
|
+
export declare function themeAssetUrl(relative: string, themeDirs: readonly string[]): string;
|
|
56
84
|
/** The response body and headers for an asset. */
|
|
57
85
|
export declare function assetResponse(asset: StaticAsset, options?: AssetResponseOptions): Response;
|
|
58
86
|
/** The 304 an unchanged asset gets, which carries the validators and no body. */
|
package/dist/web/assets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/web/assets.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/web/assets.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAIrC,yDAAyD;AACzD,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAE/C,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,QAAQ,CAAC;AAE1C,yDAAyD;AACzD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAExF;AAED,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,WAAW,CAAC;AAEzC,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,uBAAuB,WAAW,CAAC;AAEhD,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,KAAK,EAAE,KAAK,CAAC;IACb,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC;AAErC;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,WAAW,GAAG,SAAS,CA6BpF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,UAAU,GAAG,SAAS,CAKxB;AAED,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjC;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,EAAE,oBAG7B,CAAC;AAIF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAOvD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAKpF;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,oBAAyB,GAAG,QAAQ,CAG9F;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,oBAAyB,GAAG,QAAQ,CAKjG;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,CAE9D;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,CAEjE;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOlF"}
|