@we8/cms 0.1.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 +78 -0
- package/dist/auth/auth.d.ts +87 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/auth.js +90 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/routes.admin.d.ts +22 -0
- package/dist/auth/routes.admin.d.ts.map +1 -0
- package/dist/auth/routes.admin.js +106 -0
- package/dist/auth/routes.admin.js.map +1 -0
- package/dist/auth/schema.d.ts +1395 -0
- package/dist/auth/schema.d.ts.map +1 -0
- package/dist/auth/schema.js +76 -0
- package/dist/auth/schema.js.map +1 -0
- package/dist/env.d.ts +64 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +48 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +170 -0
- package/dist/index.js.map +1 -0
- package/dist/keys/middleware.d.ts +54 -0
- package/dist/keys/middleware.d.ts.map +1 -0
- package/dist/keys/middleware.js +84 -0
- package/dist/keys/middleware.js.map +1 -0
- package/dist/keys/routes.admin.d.ts +16 -0
- package/dist/keys/routes.admin.d.ts.map +1 -0
- package/dist/keys/routes.admin.js +34 -0
- package/dist/keys/routes.admin.js.map +1 -0
- package/dist/keys/service.d.ts +36 -0
- package/dist/keys/service.d.ts.map +1 -0
- package/dist/keys/service.js +120 -0
- package/dist/keys/service.js.map +1 -0
- package/dist/lib/bots.d.ts +24 -0
- package/dist/lib/bots.d.ts.map +1 -0
- package/dist/lib/bots.js +79 -0
- package/dist/lib/bots.js.map +1 -0
- package/dist/lib/email.d.ts +62 -0
- package/dist/lib/email.d.ts.map +1 -0
- package/dist/lib/email.js +64 -0
- package/dist/lib/email.js.map +1 -0
- package/dist/lib/errors.d.ts +12 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +23 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/notify.d.ts +20 -0
- package/dist/lib/notify.d.ts.map +1 -0
- package/dist/lib/notify.js +60 -0
- package/dist/lib/notify.js.map +1 -0
- package/dist/lib/retention.d.ts +15 -0
- package/dist/lib/retention.d.ts.map +1 -0
- package/dist/lib/retention.js +40 -0
- package/dist/lib/retention.js.map +1 -0
- package/dist/lib/sanitize.d.ts +14 -0
- package/dist/lib/sanitize.d.ts.map +1 -0
- package/dist/lib/sanitize.js +145 -0
- package/dist/lib/sanitize.js.map +1 -0
- package/dist/lib/turnstile.d.ts +7 -0
- package/dist/lib/turnstile.d.ts.map +1 -0
- package/dist/lib/turnstile.js +32 -0
- package/dist/lib/turnstile.js.map +1 -0
- package/dist/lib/validate.d.ts +11 -0
- package/dist/lib/validate.d.ts.map +1 -0
- package/dist/lib/validate.js +22 -0
- package/dist/lib/validate.js.map +1 -0
- package/dist/lib/word-count.d.ts +7 -0
- package/dist/lib/word-count.d.ts.map +1 -0
- package/dist/lib/word-count.js +14 -0
- package/dist/lib/word-count.js.map +1 -0
- package/dist/middleware/auth.d.ts +46 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +60 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +33 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +55 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/module-gate.d.ts +11 -0
- package/dist/middleware/module-gate.d.ts.map +1 -0
- package/dist/middleware/module-gate.js +18 -0
- package/dist/middleware/module-gate.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +13 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +30 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-id.d.ts +8 -0
- package/dist/middleware/request-id.d.ts.map +1 -0
- package/dist/middleware/request-id.js +11 -0
- package/dist/middleware/request-id.js.map +1 -0
- package/dist/modules/aeo/checks.d.ts +41 -0
- package/dist/modules/aeo/checks.d.ts.map +1 -0
- package/dist/modules/aeo/checks.js +127 -0
- package/dist/modules/aeo/checks.js.map +1 -0
- package/dist/modules/aeo/llms.d.ts +24 -0
- package/dist/modules/aeo/llms.d.ts.map +1 -0
- package/dist/modules/aeo/llms.js +92 -0
- package/dist/modules/aeo/llms.js.map +1 -0
- package/dist/modules/aeo/routes.admin.d.ts +12 -0
- package/dist/modules/aeo/routes.admin.d.ts.map +1 -0
- package/dist/modules/aeo/routes.admin.js +37 -0
- package/dist/modules/aeo/routes.admin.js.map +1 -0
- package/dist/modules/analytics/db.d.ts +24 -0
- package/dist/modules/analytics/db.d.ts.map +1 -0
- package/dist/modules/analytics/db.js +24 -0
- package/dist/modules/analytics/db.js.map +1 -0
- package/dist/modules/analytics/routes.admin.d.ts +6 -0
- package/dist/modules/analytics/routes.admin.d.ts.map +1 -0
- package/dist/modules/analytics/routes.admin.js +21 -0
- package/dist/modules/analytics/routes.admin.js.map +1 -0
- package/dist/modules/analytics/routes.public.d.ts +6 -0
- package/dist/modules/analytics/routes.public.d.ts.map +1 -0
- package/dist/modules/analytics/routes.public.js +33 -0
- package/dist/modules/analytics/routes.public.js.map +1 -0
- package/dist/modules/analytics/service.d.ts +16 -0
- package/dist/modules/analytics/service.d.ts.map +1 -0
- package/dist/modules/analytics/service.js +148 -0
- package/dist/modules/analytics/service.js.map +1 -0
- package/dist/modules/authors/routes.admin.d.ts +6 -0
- package/dist/modules/authors/routes.admin.d.ts.map +1 -0
- package/dist/modules/authors/routes.admin.js +29 -0
- package/dist/modules/authors/routes.admin.js.map +1 -0
- package/dist/modules/authors/routes.public.d.ts +6 -0
- package/dist/modules/authors/routes.public.d.ts.map +1 -0
- package/dist/modules/authors/routes.public.js +11 -0
- package/dist/modules/authors/routes.public.js.map +1 -0
- package/dist/modules/authors/service.d.ts +26 -0
- package/dist/modules/authors/service.d.ts.map +1 -0
- package/dist/modules/authors/service.js +116 -0
- package/dist/modules/authors/service.js.map +1 -0
- package/dist/modules/content/db.d.ts +127 -0
- package/dist/modules/content/db.d.ts.map +1 -0
- package/dist/modules/content/db.js +203 -0
- package/dist/modules/content/db.js.map +1 -0
- package/dist/modules/content/publish.d.ts +38 -0
- package/dist/modules/content/publish.d.ts.map +1 -0
- package/dist/modules/content/publish.js +184 -0
- package/dist/modules/content/publish.js.map +1 -0
- package/dist/modules/content/routes.admin.d.ts +8 -0
- package/dist/modules/content/routes.admin.d.ts.map +1 -0
- package/dist/modules/content/routes.admin.js +122 -0
- package/dist/modules/content/routes.admin.js.map +1 -0
- package/dist/modules/content/routes.public.d.ts +6 -0
- package/dist/modules/content/routes.public.d.ts.map +1 -0
- package/dist/modules/content/routes.public.js +56 -0
- package/dist/modules/content/routes.public.js.map +1 -0
- package/dist/modules/content/service.d.ts +137 -0
- package/dist/modules/content/service.d.ts.map +1 -0
- package/dist/modules/content/service.js +348 -0
- package/dist/modules/content/service.js.map +1 -0
- package/dist/modules/forms/db.d.ts +42 -0
- package/dist/modules/forms/db.d.ts.map +1 -0
- package/dist/modules/forms/db.js +62 -0
- package/dist/modules/forms/db.js.map +1 -0
- package/dist/modules/forms/forms-routes.admin.d.ts +6 -0
- package/dist/modules/forms/forms-routes.admin.d.ts.map +1 -0
- package/dist/modules/forms/forms-routes.admin.js +37 -0
- package/dist/modules/forms/forms-routes.admin.js.map +1 -0
- package/dist/modules/forms/forms-service.d.ts +18 -0
- package/dist/modules/forms/forms-service.d.ts.map +1 -0
- package/dist/modules/forms/forms-service.js +121 -0
- package/dist/modules/forms/forms-service.js.map +1 -0
- package/dist/modules/forms/routes.public.d.ts +6 -0
- package/dist/modules/forms/routes.public.d.ts.map +1 -0
- package/dist/modules/forms/routes.public.js +41 -0
- package/dist/modules/forms/routes.public.js.map +1 -0
- package/dist/modules/forms/submissions-routes.admin.d.ts +6 -0
- package/dist/modules/forms/submissions-routes.admin.d.ts.map +1 -0
- package/dist/modules/forms/submissions-routes.admin.js +46 -0
- package/dist/modules/forms/submissions-routes.admin.js.map +1 -0
- package/dist/modules/forms/submissions-service.d.ts +45 -0
- package/dist/modules/forms/submissions-service.d.ts.map +1 -0
- package/dist/modules/forms/submissions-service.js +174 -0
- package/dist/modules/forms/submissions-service.js.map +1 -0
- package/dist/modules/media/db.d.ts +15 -0
- package/dist/modules/media/db.d.ts.map +1 -0
- package/dist/modules/media/db.js +24 -0
- package/dist/modules/media/db.js.map +1 -0
- package/dist/modules/media/routes.admin.d.ts +6 -0
- package/dist/modules/media/routes.admin.d.ts.map +1 -0
- package/dist/modules/media/routes.admin.js +30 -0
- package/dist/modules/media/routes.admin.js.map +1 -0
- package/dist/modules/media/routes.public.d.ts +6 -0
- package/dist/modules/media/routes.public.d.ts.map +1 -0
- package/dist/modules/media/routes.public.js +35 -0
- package/dist/modules/media/routes.public.js.map +1 -0
- package/dist/modules/media/service.d.ts +32 -0
- package/dist/modules/media/service.d.ts.map +1 -0
- package/dist/modules/media/service.js +99 -0
- package/dist/modules/media/service.js.map +1 -0
- package/dist/modules/openapi/spec.d.ts +21 -0
- package/dist/modules/openapi/spec.d.ts.map +1 -0
- package/dist/modules/openapi/spec.js +468 -0
- package/dist/modules/openapi/spec.js.map +1 -0
- package/dist/modules/seo/jsonld.d.ts +25 -0
- package/dist/modules/seo/jsonld.d.ts.map +1 -0
- package/dist/modules/seo/jsonld.js +67 -0
- package/dist/modules/seo/jsonld.js.map +1 -0
- package/dist/modules/seo/routes.public.d.ts +6 -0
- package/dist/modules/seo/routes.public.d.ts.map +1 -0
- package/dist/modules/seo/routes.public.js +39 -0
- package/dist/modules/seo/routes.public.js.map +1 -0
- package/dist/modules/seo/sitemap.d.ts +3 -0
- package/dist/modules/seo/sitemap.d.ts.map +1 -0
- package/dist/modules/seo/sitemap.js +39 -0
- package/dist/modules/seo/sitemap.js.map +1 -0
- package/dist/modules/settings/routes.admin.d.ts +12 -0
- package/dist/modules/settings/routes.admin.d.ts.map +1 -0
- package/dist/modules/settings/routes.admin.js +27 -0
- package/dist/modules/settings/routes.admin.js.map +1 -0
- package/dist/modules/site-documents/routes.admin.d.ts +10 -0
- package/dist/modules/site-documents/routes.admin.d.ts.map +1 -0
- package/dist/modules/site-documents/routes.admin.js +29 -0
- package/dist/modules/site-documents/routes.admin.js.map +1 -0
- package/dist/modules/site-documents/routes.public.d.ts +6 -0
- package/dist/modules/site-documents/routes.public.d.ts.map +1 -0
- package/dist/modules/site-documents/routes.public.js +16 -0
- package/dist/modules/site-documents/routes.public.js.map +1 -0
- package/dist/modules/site-documents/service.d.ts +11 -0
- package/dist/modules/site-documents/service.d.ts.map +1 -0
- package/dist/modules/site-documents/service.js +42 -0
- package/dist/modules/site-documents/service.js.map +1 -0
- package/dist/modules/webhooks/dispatch.d.ts +14 -0
- package/dist/modules/webhooks/dispatch.d.ts.map +1 -0
- package/dist/modules/webhooks/dispatch.js +84 -0
- package/dist/modules/webhooks/dispatch.js.map +1 -0
- package/dist/shared/constants.d.ts +79 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +89 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/envelope.d.ts +37 -0
- package/dist/shared/envelope.d.ts.map +1 -0
- package/dist/shared/envelope.js +46 -0
- package/dist/shared/envelope.js.map +1 -0
- package/dist/shared/hash.d.ts +2 -0
- package/dist/shared/hash.d.ts.map +1 -0
- package/dist/shared/hash.js +8 -0
- package/dist/shared/hash.js.map +1 -0
- package/dist/shared/index.d.ts +13 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +13 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/schemas/aeo.d.ts +182 -0
- package/dist/shared/schemas/aeo.d.ts.map +1 -0
- package/dist/shared/schemas/aeo.js +43 -0
- package/dist/shared/schemas/aeo.js.map +1 -0
- package/dist/shared/schemas/analytics.d.ts +277 -0
- package/dist/shared/schemas/analytics.d.ts.map +1 -0
- package/dist/shared/schemas/analytics.js +74 -0
- package/dist/shared/schemas/analytics.js.map +1 -0
- package/dist/shared/schemas/api-key.d.ts +77 -0
- package/dist/shared/schemas/api-key.d.ts.map +1 -0
- package/dist/shared/schemas/api-key.js +27 -0
- package/dist/shared/schemas/api-key.js.map +1 -0
- package/dist/shared/schemas/author.d.ts +147 -0
- package/dist/shared/schemas/author.d.ts.map +1 -0
- package/dist/shared/schemas/author.js +27 -0
- package/dist/shared/schemas/author.js.map +1 -0
- package/dist/shared/schemas/form.d.ts +341 -0
- package/dist/shared/schemas/form.d.ts.map +1 -0
- package/dist/shared/schemas/form.js +117 -0
- package/dist/shared/schemas/form.js.map +1 -0
- package/dist/shared/schemas/index.d.ts +12 -0
- package/dist/shared/schemas/index.d.ts.map +1 -0
- package/dist/shared/schemas/index.js +12 -0
- package/dist/shared/schemas/index.js.map +1 -0
- package/dist/shared/schemas/media.d.ts +42 -0
- package/dist/shared/schemas/media.d.ts.map +1 -0
- package/dist/shared/schemas/media.js +16 -0
- package/dist/shared/schemas/media.js.map +1 -0
- package/dist/shared/schemas/pagination.d.ts +34 -0
- package/dist/shared/schemas/pagination.d.ts.map +1 -0
- package/dist/shared/schemas/pagination.js +21 -0
- package/dist/shared/schemas/pagination.js.map +1 -0
- package/dist/shared/schemas/post.d.ts +862 -0
- package/dist/shared/schemas/post.d.ts.map +1 -0
- package/dist/shared/schemas/post.js +187 -0
- package/dist/shared/schemas/post.js.map +1 -0
- package/dist/shared/schemas/settings.d.ts +839 -0
- package/dist/shared/schemas/settings.d.ts.map +1 -0
- package/dist/shared/schemas/settings.js +202 -0
- package/dist/shared/schemas/settings.js.map +1 -0
- package/dist/shared/schemas/site-document.d.ts +38 -0
- package/dist/shared/schemas/site-document.d.ts.map +1 -0
- package/dist/shared/schemas/site-document.js +21 -0
- package/dist/shared/schemas/site-document.js.map +1 -0
- package/dist/shared/schemas/webhook.d.ts +39 -0
- package/dist/shared/schemas/webhook.d.ts.map +1 -0
- package/dist/shared/schemas/webhook.js +20 -0
- package/dist/shared/schemas/webhook.js.map +1 -0
- package/dist/spa.d.ts +9 -0
- package/dist/spa.d.ts.map +1 -0
- package/dist/spa.js +57 -0
- package/dist/spa.js.map +1 -0
- package/migrations/0001_settings.sql +39 -0
- package/migrations/0002_content.sql +135 -0
- package/migrations/0003_forms.sql +35 -0
- package/migrations/0004_analytics.sql +42 -0
- package/migrations/0005_media.sql +17 -0
- package/migrations/0006_site_documents.sql +18 -0
- package/migrations/0007_auth.sql +81 -0
- package/migrations/0008_api_keys.sql +20 -0
- package/package.json +68 -0
- package/scripts/bundle-admin.mjs +54 -0
- package/seed/local.sql +63 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitelist HTML sanitizer, applied to admin-authored post content on every
|
|
3
|
+
* write. There is no DOM in a Worker, so this uses HTMLRewriter, the streaming
|
|
4
|
+
* HTML parser built into the runtime, rather than string matching.
|
|
5
|
+
*
|
|
6
|
+
* Policy: an explicit tag+attribute whitelist. Anything not on the
|
|
7
|
+
* whitelist is either removed entirely (tags whose content is not
|
|
8
|
+
* displayable text: script/style/iframe/...) or unwrapped, keeping its text
|
|
9
|
+
* content (structurally unknown but harmless wrapper tags). All `on*` event
|
|
10
|
+
* handler attributes and `style` attributes are stripped from every element
|
|
11
|
+
* regardless of tag. `href`/`src` are further restricted to safe schemes.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sanitizeHtml(html: string): Promise<string>;
|
|
14
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/lib/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiJH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhE"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitelist HTML sanitizer, applied to admin-authored post content on every
|
|
3
|
+
* write. There is no DOM in a Worker, so this uses HTMLRewriter, the streaming
|
|
4
|
+
* HTML parser built into the runtime, rather than string matching.
|
|
5
|
+
*
|
|
6
|
+
* Policy: an explicit tag+attribute whitelist. Anything not on the
|
|
7
|
+
* whitelist is either removed entirely (tags whose content is not
|
|
8
|
+
* displayable text: script/style/iframe/...) or unwrapped, keeping its text
|
|
9
|
+
* content (structurally unknown but harmless wrapper tags). All `on*` event
|
|
10
|
+
* handler attributes and `style` attributes are stripped from every element
|
|
11
|
+
* regardless of tag. `href`/`src` are further restricted to safe schemes.
|
|
12
|
+
*/
|
|
13
|
+
const ALLOWED_TAGS = {
|
|
14
|
+
p: { attributes: [] },
|
|
15
|
+
br: { attributes: [] },
|
|
16
|
+
hr: { attributes: [] },
|
|
17
|
+
strong: { attributes: [] },
|
|
18
|
+
b: { attributes: [] },
|
|
19
|
+
em: { attributes: [] },
|
|
20
|
+
i: { attributes: [] },
|
|
21
|
+
u: { attributes: [] },
|
|
22
|
+
s: { attributes: [] },
|
|
23
|
+
span: { attributes: [] },
|
|
24
|
+
blockquote: { attributes: [] },
|
|
25
|
+
h1: { attributes: [] },
|
|
26
|
+
h2: { attributes: [] },
|
|
27
|
+
h3: { attributes: [] },
|
|
28
|
+
h4: { attributes: [] },
|
|
29
|
+
h5: { attributes: [] },
|
|
30
|
+
h6: { attributes: [] },
|
|
31
|
+
ul: { attributes: [] },
|
|
32
|
+
ol: { attributes: [] },
|
|
33
|
+
li: { attributes: [] },
|
|
34
|
+
pre: { attributes: ['class'] },
|
|
35
|
+
code: { attributes: ['class'] },
|
|
36
|
+
figure: { attributes: [] },
|
|
37
|
+
figcaption: { attributes: [] },
|
|
38
|
+
table: { attributes: [] },
|
|
39
|
+
thead: { attributes: [] },
|
|
40
|
+
tbody: { attributes: [] },
|
|
41
|
+
tr: { attributes: [] },
|
|
42
|
+
// colspan/rowspan carry merged cells; colwidth is ProseMirror's column-width
|
|
43
|
+
// attribute. Stripping any of them would make the editor's next save a
|
|
44
|
+
// silent table mutation.
|
|
45
|
+
th: { attributes: ['colspan', 'rowspan', 'colwidth'] },
|
|
46
|
+
td: { attributes: ['colspan', 'rowspan', 'colwidth'] },
|
|
47
|
+
a: { attributes: ['href', 'title', 'target'] },
|
|
48
|
+
img: { attributes: ['src', 'alt', 'width', 'height'] },
|
|
49
|
+
// iframes exist ONLY as YouTube embeds (src verified against
|
|
50
|
+
// SAFE_YOUTUBE_SRC_PATTERN below; anything else is removed whole). The
|
|
51
|
+
// attribute list is what Tiptap's Youtube extension needs to round-trip;
|
|
52
|
+
// its other serialized attributes (autoplay, modestbranding, ...) are
|
|
53
|
+
// stripped, which is stable: parseHTML only needs the wrapper + src.
|
|
54
|
+
iframe: { attributes: ['src', 'width', 'height', 'allowfullscreen', 'title', 'start'] },
|
|
55
|
+
// The div rule is reached only for Tiptap's YouTube wrapper
|
|
56
|
+
// (div[data-youtube-video]); all other divs are unwrapped in the handler
|
|
57
|
+
// before the rule lookup. Without the wrapper the editor cannot re-parse
|
|
58
|
+
// the embed and would silently drop it on the next save.
|
|
59
|
+
div: { attributes: ['data-youtube-video'] },
|
|
60
|
+
};
|
|
61
|
+
/** Tags whose content is not display text: drop the tag AND everything inside it. */
|
|
62
|
+
const REMOVE_WITH_CONTENT = new Set([
|
|
63
|
+
'script',
|
|
64
|
+
'style',
|
|
65
|
+
'object',
|
|
66
|
+
'embed',
|
|
67
|
+
'link',
|
|
68
|
+
'meta',
|
|
69
|
+
'base',
|
|
70
|
+
'form',
|
|
71
|
+
'noscript',
|
|
72
|
+
]);
|
|
73
|
+
// Absolute http(s)/mailto, site-relative ("/path", but NOT protocol-relative
|
|
74
|
+
// "//host"), and in-page anchors ("#section"). Authors legitimately write
|
|
75
|
+
// relative links in post bodies, and stripping them silently breaks pages.
|
|
76
|
+
const SAFE_HREF_PATTERN = /^(https?:|mailto:|#|\/(?!\/))/i;
|
|
77
|
+
const SAFE_IMG_SRC_PATTERN = /^(https?:|data:image\/(?:png|jpe?g|gif|webp|svg\+xml);base64,)/i;
|
|
78
|
+
const LANGUAGE_CLASS_PATTERN = /^language-[a-z0-9]+$/;
|
|
79
|
+
// The ONLY sanctioned iframe origin: YouTube's embed player (plain or
|
|
80
|
+
// privacy-enhanced host). Everything else, including youtu.be watch links,
|
|
81
|
+
// is not an embed URL and the element is removed whole.
|
|
82
|
+
const SAFE_YOUTUBE_SRC_PATTERN = /^https:\/\/(?:www\.)?youtube(?:-nocookie)?\.com\/embed\/[\w-]+/i;
|
|
83
|
+
class SanitizeHandler {
|
|
84
|
+
element(element) {
|
|
85
|
+
const tag = element.tagName.toLowerCase();
|
|
86
|
+
if (REMOVE_WITH_CONTENT.has(tag)) {
|
|
87
|
+
element.remove();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Divs stay unwrapped (pasted-content wrapper soup) EXCEPT Tiptap's
|
|
91
|
+
// YouTube wrapper, which must survive for the embed to re-parse. The
|
|
92
|
+
// attribute is serialized empty (data-youtube-video=""), so test against
|
|
93
|
+
// null, not truthiness.
|
|
94
|
+
if (tag === 'div' && element.getAttribute('data-youtube-video') === null) {
|
|
95
|
+
element.removeAndKeepContent();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const rule = ALLOWED_TAGS[tag];
|
|
99
|
+
if (!rule) {
|
|
100
|
+
element.removeAndKeepContent();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Snapshot first: mutating attributes while iterating a live handle is
|
|
104
|
+
// asking for trouble, and this is cheap either way.
|
|
105
|
+
const attrs = [...element.attributes];
|
|
106
|
+
for (const [name] of attrs) {
|
|
107
|
+
if (!name)
|
|
108
|
+
continue;
|
|
109
|
+
const lower = name.toLowerCase();
|
|
110
|
+
if (lower.startsWith('on') || lower === 'style' || !rule.attributes.includes(lower)) {
|
|
111
|
+
element.removeAttribute(name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (tag === 'a') {
|
|
115
|
+
const href = element.getAttribute('href');
|
|
116
|
+
if (href && !SAFE_HREF_PATTERN.test(href.trim())) {
|
|
117
|
+
element.removeAttribute('href');
|
|
118
|
+
}
|
|
119
|
+
element.setAttribute('rel', 'noopener noreferrer nofollow');
|
|
120
|
+
}
|
|
121
|
+
if (tag === 'img') {
|
|
122
|
+
const src = element.getAttribute('src');
|
|
123
|
+
if (!src || !SAFE_IMG_SRC_PATTERN.test(src.trim())) {
|
|
124
|
+
element.remove();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (tag === 'iframe') {
|
|
128
|
+
const src = element.getAttribute('src');
|
|
129
|
+
if (!src || !SAFE_YOUTUBE_SRC_PATTERN.test(src.trim())) {
|
|
130
|
+
element.remove();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (tag === 'pre' || tag === 'code') {
|
|
134
|
+
const cls = element.getAttribute('class');
|
|
135
|
+
if (cls && !LANGUAGE_CLASS_PATTERN.test(cls)) {
|
|
136
|
+
element.removeAttribute('class');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function sanitizeHtml(html) {
|
|
142
|
+
const rewritten = new HTMLRewriter().on('*', new SanitizeHandler()).transform(new Response(html));
|
|
143
|
+
return rewritten.text();
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/lib/sanitize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile verification. No secret configured means allow, since
|
|
3
|
+
* Turnstile is opt-in; a missing token is a reject; and a network or API
|
|
4
|
+
* failure is also a reject, because this fails closed rather than open.
|
|
5
|
+
*/
|
|
6
|
+
export declare function verifyTurnstile(secret: string | undefined, token: string | undefined, remoteIp?: string): Promise<boolean>;
|
|
7
|
+
//# sourceMappingURL=turnstile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnstile.d.ts","sourceRoot":"","sources":["../../src/lib/turnstile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAuBlB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile verification. No secret configured means allow, since
|
|
3
|
+
* Turnstile is opt-in; a missing token is a reject; and a network or API
|
|
4
|
+
* failure is also a reject, because this fails closed rather than open.
|
|
5
|
+
*/
|
|
6
|
+
export async function verifyTurnstile(secret, token, remoteIp) {
|
|
7
|
+
if (!secret)
|
|
8
|
+
return true;
|
|
9
|
+
if (!token)
|
|
10
|
+
return false;
|
|
11
|
+
const form = new FormData();
|
|
12
|
+
form.append('secret', secret);
|
|
13
|
+
form.append('response', token);
|
|
14
|
+
if (remoteIp)
|
|
15
|
+
form.append('remoteip', remoteIp);
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: form,
|
|
20
|
+
});
|
|
21
|
+
// Two-step unknown-then-assert: a single-step `(await res.json()) as T`
|
|
22
|
+
// trips no-unnecessary-type-assertion under the type-aware lint pass,
|
|
23
|
+
// which resolves Response.json() as `any` rather than `unknown`.
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
const result = data;
|
|
26
|
+
return result.success === true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=turnstile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnstile.js","sourceRoot":"","sources":["../../src/lib/turnstile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAA0B,EAC1B,KAAyB,EACzB,QAAiB;IAEjB,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAEzB,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9B,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAC/B,IAAI,QAAQ;QAAE,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAEhD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,2DAA2D,EAAE;YACnF,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;QACH,wEAAwE;QACxE,sEAAsE;QACtE,iEAAiE;QACjE,MAAM,IAAI,GAAY,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,IAA6B,CAAC;QAC7C,OAAO,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Parses with a zod schema or throws the standard validation_error envelope.
|
|
4
|
+
* Generic is inferred from the schema itself (`S extends z.ZodTypeAny`) and
|
|
5
|
+
* the return type computed via `z.infer<S>`, not `z.ZodType<T>` with T
|
|
6
|
+
* inferred directly: for schemas with `.default(...)`, the latter can
|
|
7
|
+
* resolve T to the pre-default input shape (fields optional) rather than
|
|
8
|
+
* the post-parse output shape (defaults applied, fields required).
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseOrThrow<S extends z.ZodTypeAny>(schema: S, data: unknown): z.infer<S>;
|
|
11
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/lib/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAWzF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { HttpError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses with a zod schema or throws the standard validation_error envelope.
|
|
4
|
+
* Generic is inferred from the schema itself (`S extends z.ZodTypeAny`) and
|
|
5
|
+
* the return type computed via `z.infer<S>`, not `z.ZodType<T>` with T
|
|
6
|
+
* inferred directly: for schemas with `.default(...)`, the latter can
|
|
7
|
+
* resolve T to the pre-default input shape (fields optional) rather than
|
|
8
|
+
* the post-parse output shape (defaults applied, fields required).
|
|
9
|
+
*/
|
|
10
|
+
export function parseOrThrow(schema, data) {
|
|
11
|
+
const result = schema.safeParse(data);
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
const message = result.error.issues.map((issue) => issue.message).join('; ');
|
|
14
|
+
throw new HttpError('validation_error', message || 'invalid request');
|
|
15
|
+
}
|
|
16
|
+
// z.ZodTypeAny (by zod's own design, for accepting "any schema" generically)
|
|
17
|
+
// makes result.data resolve to `any`; an explicit assertion at the return
|
|
18
|
+
// site is the correct way to leave `any` here (a plain annotated
|
|
19
|
+
// assignment just moves the same "any" flow to no-unsafe-assignment).
|
|
20
|
+
return result.data;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/lib/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAyB,MAAS,EAAE,IAAa;IAC3E,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7E,MAAM,IAAI,SAAS,CAAC,kBAAkB,EAAE,OAAO,IAAI,iBAAiB,CAAC,CAAC;IACxE,CAAC;IACD,6EAA6E;IAC7E,0EAA0E;IAC1E,iEAAiE;IACjE,sEAAsE;IACtE,OAAO,MAAM,CAAC,IAAkB,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word count, computed on every content save from already-sanitized HTML. Not
|
|
3
|
+
* a security boundary, just a display stat ("N min read"), so a plain tag strip
|
|
4
|
+
* is sufficient.
|
|
5
|
+
*/
|
|
6
|
+
export declare function countWords(html: string): number;
|
|
7
|
+
//# sourceMappingURL=word-count.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"word-count.d.ts","sourceRoot":"","sources":["../../src/lib/word-count.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word count, computed on every content save from already-sanitized HTML. Not
|
|
3
|
+
* a security boundary, just a display stat ("N min read"), so a plain tag strip
|
|
4
|
+
* is sufficient.
|
|
5
|
+
*/
|
|
6
|
+
export function countWords(html) {
|
|
7
|
+
const text = html.replace(/<[^>]*>/g, ' ');
|
|
8
|
+
const words = text
|
|
9
|
+
.trim()
|
|
10
|
+
.split(/\s+/)
|
|
11
|
+
.filter((word) => word.length > 0);
|
|
12
|
+
return words.length;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=word-count.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"word-count.js","sourceRoot":"","sources":["../../src/lib/word-count.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI;SACf,IAAI,EAAE;SACN,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACrC,OAAO,KAAK,CAAC,MAAM,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { Bindings } from '../env.js';
|
|
3
|
+
import type { UserRole } from '../shared/index.js';
|
|
4
|
+
export type AuthContext = {
|
|
5
|
+
/** The signed-in user's id, or null when no session is required on this route. */
|
|
6
|
+
userId: string | null;
|
|
7
|
+
/** The signed-in user's email; this is what publish history is attributed to. */
|
|
8
|
+
user: string | null;
|
|
9
|
+
role: UserRole;
|
|
10
|
+
};
|
|
11
|
+
export type AuthVariables = {
|
|
12
|
+
auth: AuthContext;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Session gating for /v1/admin/*.
|
|
16
|
+
*
|
|
17
|
+
* This is the seam where managed we8 and this package genuinely differ. There,
|
|
18
|
+
* a control-plane proxy signs an internal HMAC and the engine trusts the
|
|
19
|
+
* identity headers riding along with it. Here there is no proxy: the Better
|
|
20
|
+
* Auth session cookie on the request IS the identity, resolved against this
|
|
21
|
+
* worker's own database, and the user and role it yields are injected into
|
|
22
|
+
* exactly the place the engine's header trust used to fill.
|
|
23
|
+
*
|
|
24
|
+
* A public API key never reaches the admin surface. A publishable key travels
|
|
25
|
+
* in browser code, so if presenting one could reach /v1/admin/*, every visitor
|
|
26
|
+
* to the site could read the submission inbox and rewrite content. Any request
|
|
27
|
+
* carrying a key on an admin route is rejected outright rather than merely
|
|
28
|
+
* failing the session check, so the boundary is explicit rather than
|
|
29
|
+
* incidental.
|
|
30
|
+
*/
|
|
31
|
+
export declare const sessionAuth: import("hono").MiddlewareHandler<{
|
|
32
|
+
Bindings: Bindings;
|
|
33
|
+
Variables: AuthVariables;
|
|
34
|
+
}, string, {}, Response>;
|
|
35
|
+
/**
|
|
36
|
+
* Owner-only gate, mounted on the routes where the two roles part ways: the
|
|
37
|
+
* settings mutation (module flags, allowed origins, webhooks, notify
|
|
38
|
+
* addresses, retention) and the whole form-submission inbox, which is other
|
|
39
|
+
* people's personal data. Everything else in the admin API is open to a
|
|
40
|
+
* member, because a member is an editor.
|
|
41
|
+
*
|
|
42
|
+
* Only mounted behind sessionAuth, so the request is already authenticated;
|
|
43
|
+
* this narrows which authenticated user.
|
|
44
|
+
*/
|
|
45
|
+
export declare function requireOwner(c: Context, next: () => Promise<void>): Promise<void>;
|
|
46
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG;IACxB,kFAAkF;IAClF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iFAAiF;IACjF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW;cAAgC,QAAQ;eAAa,aAAa;wBA0BzF,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createMiddleware } from 'hono/factory';
|
|
2
|
+
import { HttpError } from '../lib/errors.js';
|
|
3
|
+
import { createAuth, normalizeRole } from '../auth/auth.js';
|
|
4
|
+
import { extractApiKey } from '../keys/service.js';
|
|
5
|
+
/**
|
|
6
|
+
* Session gating for /v1/admin/*.
|
|
7
|
+
*
|
|
8
|
+
* This is the seam where managed we8 and this package genuinely differ. There,
|
|
9
|
+
* a control-plane proxy signs an internal HMAC and the engine trusts the
|
|
10
|
+
* identity headers riding along with it. Here there is no proxy: the Better
|
|
11
|
+
* Auth session cookie on the request IS the identity, resolved against this
|
|
12
|
+
* worker's own database, and the user and role it yields are injected into
|
|
13
|
+
* exactly the place the engine's header trust used to fill.
|
|
14
|
+
*
|
|
15
|
+
* A public API key never reaches the admin surface. A publishable key travels
|
|
16
|
+
* in browser code, so if presenting one could reach /v1/admin/*, every visitor
|
|
17
|
+
* to the site could read the submission inbox and rewrite content. Any request
|
|
18
|
+
* carrying a key on an admin route is rejected outright rather than merely
|
|
19
|
+
* failing the session check, so the boundary is explicit rather than
|
|
20
|
+
* incidental.
|
|
21
|
+
*/
|
|
22
|
+
export const sessionAuth = createMiddleware(async (c, next) => {
|
|
23
|
+
if (extractApiKey(c) !== undefined) {
|
|
24
|
+
throw new HttpError('forbidden', 'API keys cannot access admin routes');
|
|
25
|
+
}
|
|
26
|
+
if (!c.env.BETTER_AUTH_SECRET) {
|
|
27
|
+
// Refusing beats guessing: with no secret configured, any session this
|
|
28
|
+
// worker validated would be one it could not have signed.
|
|
29
|
+
throw new HttpError('unauthorized', 'authentication is not configured');
|
|
30
|
+
}
|
|
31
|
+
const auth = createAuth(c.env);
|
|
32
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
33
|
+
if (!session) {
|
|
34
|
+
throw new HttpError('unauthorized', 'sign in required');
|
|
35
|
+
}
|
|
36
|
+
c.set('auth', {
|
|
37
|
+
userId: session.user.id,
|
|
38
|
+
user: session.user.email,
|
|
39
|
+
role: normalizeRole(session.user.role),
|
|
40
|
+
});
|
|
41
|
+
await next();
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Owner-only gate, mounted on the routes where the two roles part ways: the
|
|
45
|
+
* settings mutation (module flags, allowed origins, webhooks, notify
|
|
46
|
+
* addresses, retention) and the whole form-submission inbox, which is other
|
|
47
|
+
* people's personal data. Everything else in the admin API is open to a
|
|
48
|
+
* member, because a member is an editor.
|
|
49
|
+
*
|
|
50
|
+
* Only mounted behind sessionAuth, so the request is already authenticated;
|
|
51
|
+
* this narrows which authenticated user.
|
|
52
|
+
*/
|
|
53
|
+
export function requireOwner(c, next) {
|
|
54
|
+
const auth = c.get('auth');
|
|
55
|
+
if (auth?.role !== 'owner') {
|
|
56
|
+
throw new HttpError('forbidden', 'owner role required');
|
|
57
|
+
}
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAenD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CACzC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;IAChB,IAAI,aAAa,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,qCAAqC,CAAC,CAAC;IAC1E,CAAC;IAED,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;QAC9B,uEAAuE;QACvE,0DAA0D;QAC1D,MAAM,IAAI,SAAS,CAAC,cAAc,EAAE,kCAAkC,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IAC1E,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,SAAS,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IAC1D,CAAC;IAED,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE;QACZ,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE;QACvB,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK;QACxB,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;KACvC,CAAC,CAAC;IAEH,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CACF,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,CAAU,EAAE,IAAyB;IAChE,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAA4B,CAAC;IACtD,IAAI,IAAI,EAAE,IAAI,KAAK,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,qBAAqB,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import { type Bindings } from '../env.js';
|
|
3
|
+
export type CorsKind = 'publicRead' | 'publicWrite' | 'admin';
|
|
4
|
+
/**
|
|
5
|
+
* Per-route CORS. Public reads allow any origin (they are public by
|
|
6
|
+
* definition), public writes are restricted to the site's configured allowed
|
|
7
|
+
* origins, and admin routes allow those same origins WITH credentials.
|
|
8
|
+
*
|
|
9
|
+
* That last part differs from managed we8, where admin traffic arrives only
|
|
10
|
+
* from a control-plane proxy and no browser origin is ever allowed. Here the
|
|
11
|
+
* admin portal is a browser application talking to this worker directly, and
|
|
12
|
+
* it authenticates with a session cookie, so the admin surface has to answer
|
|
13
|
+
* preflights and echo credentials. It stays bounded by the same allowlist the
|
|
14
|
+
* site owner already maintains: an origin that is not on it gets nothing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function corsMiddleware(kind: CorsKind): MiddlewareHandler;
|
|
17
|
+
/**
|
|
18
|
+
* Server-side origin REJECTION for public write routes. corsMiddleware only
|
|
19
|
+
* controls response headers: it never blocks the request, and browsers do not
|
|
20
|
+
* preflight simple requests (a plain form post, for instance), so a cross-site
|
|
21
|
+
* write would still execute server-side. This middleware 403s when an Origin
|
|
22
|
+
* header is present and not on the allowlist.
|
|
23
|
+
*
|
|
24
|
+
* Two deliberate pass-throughs: requests with NO Origin header (non-browser
|
|
25
|
+
* clients, for which API keys, Turnstile, and rate limits are the controls,
|
|
26
|
+
* and where Origin is trivially spoofable anyway), and sites with an EMPTY
|
|
27
|
+
* allowlist, since rejecting everything by default would break a fresh install
|
|
28
|
+
* before its owner has configured anything.
|
|
29
|
+
*/
|
|
30
|
+
export declare const enforceOrigin: MiddlewareHandler<{
|
|
31
|
+
Bindings: Bindings;
|
|
32
|
+
}, string, {}, Response>;
|
|
33
|
+
//# sourceMappingURL=cors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../src/middleware/cors.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGvD,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,aAAa,GAAG,OAAO,CAAC;AAE9D;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,CAchE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa;cAAgC,QAAQ;wBAShE,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { cors } from 'hono/cors';
|
|
2
|
+
import { createMiddleware } from 'hono/factory';
|
|
3
|
+
import { getSettings } from '../env.js';
|
|
4
|
+
import { HttpError } from '../lib/errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Per-route CORS. Public reads allow any origin (they are public by
|
|
7
|
+
* definition), public writes are restricted to the site's configured allowed
|
|
8
|
+
* origins, and admin routes allow those same origins WITH credentials.
|
|
9
|
+
*
|
|
10
|
+
* That last part differs from managed we8, where admin traffic arrives only
|
|
11
|
+
* from a control-plane proxy and no browser origin is ever allowed. Here the
|
|
12
|
+
* admin portal is a browser application talking to this worker directly, and
|
|
13
|
+
* it authenticates with a session cookie, so the admin surface has to answer
|
|
14
|
+
* preflights and echo credentials. It stays bounded by the same allowlist the
|
|
15
|
+
* site owner already maintains: an origin that is not on it gets nothing.
|
|
16
|
+
*/
|
|
17
|
+
export function corsMiddleware(kind) {
|
|
18
|
+
return cors({
|
|
19
|
+
origin: async (origin, c) => {
|
|
20
|
+
if (kind === 'publicRead')
|
|
21
|
+
return '*';
|
|
22
|
+
// Hono's cors() origin callback is not parameterized per-app, so c.env
|
|
23
|
+
// is not narrowed to our Bindings here; this is the one contained cast.
|
|
24
|
+
const settings = await getSettings(c.env.DB);
|
|
25
|
+
return settings.allowedOrigins.includes(origin) ? origin : null;
|
|
26
|
+
},
|
|
27
|
+
credentials: kind === 'admin',
|
|
28
|
+
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
29
|
+
allowHeaders: ['Content-Type', 'X-We8-Key', 'Authorization'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Server-side origin REJECTION for public write routes. corsMiddleware only
|
|
34
|
+
* controls response headers: it never blocks the request, and browsers do not
|
|
35
|
+
* preflight simple requests (a plain form post, for instance), so a cross-site
|
|
36
|
+
* write would still execute server-side. This middleware 403s when an Origin
|
|
37
|
+
* header is present and not on the allowlist.
|
|
38
|
+
*
|
|
39
|
+
* Two deliberate pass-throughs: requests with NO Origin header (non-browser
|
|
40
|
+
* clients, for which API keys, Turnstile, and rate limits are the controls,
|
|
41
|
+
* and where Origin is trivially spoofable anyway), and sites with an EMPTY
|
|
42
|
+
* allowlist, since rejecting everything by default would break a fresh install
|
|
43
|
+
* before its owner has configured anything.
|
|
44
|
+
*/
|
|
45
|
+
export const enforceOrigin = createMiddleware(async (c, next) => {
|
|
46
|
+
const origin = c.req.header('Origin');
|
|
47
|
+
if (origin) {
|
|
48
|
+
const settings = await getSettings(c.env.DB);
|
|
49
|
+
if (settings.allowedOrigins.length > 0 && !settings.allowedOrigins.includes(origin)) {
|
|
50
|
+
throw new HttpError('forbidden', 'origin not allowed');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
await next();
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=cors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.js","sourceRoot":"","sources":["../../src/middleware/cors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,WAAW,EAAiB,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAI7C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,OAAO,IAAI,CAAC;QACV,MAAM,EAAE,KAAK,EAAE,MAAc,EAAE,CAAU,EAAE,EAAE;YAC3C,IAAI,IAAI,KAAK,YAAY;gBAAE,OAAO,GAAG,CAAC;YAEtC,uEAAuE;YACvE,wEAAwE;YACxE,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAE,CAAC,CAAC,GAAgB,CAAC,EAAE,CAAC,CAAC;YAC3D,OAAO,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QAClE,CAAC;QACD,WAAW,EAAE,IAAI,KAAK,OAAO;QAC7B,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC;QAClE,YAAY,EAAE,CAAC,cAAc,EAAE,WAAW,EAAE,eAAe,CAAC;KAC7D,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,gBAAgB,CAAyB,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;IACtF,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,QAAQ,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACpF,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,oBAAoB,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IACD,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ModuleFlags } from '../shared/index.js';
|
|
2
|
+
import { type Bindings } from '../env.js';
|
|
3
|
+
/**
|
|
4
|
+
* 404s a disabled module's routes, indistinguishable from a route that never
|
|
5
|
+
* existed (don't leak "this module exists but is off"). Mounted by each
|
|
6
|
+
* module's router at its mount point.
|
|
7
|
+
*/
|
|
8
|
+
export declare function moduleGate(moduleName: keyof ModuleFlags): import("hono").MiddlewareHandler<{
|
|
9
|
+
Bindings: Bindings;
|
|
10
|
+
}, string, {}, Response>;
|
|
11
|
+
//# sourceMappingURL=module-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-gate.d.ts","sourceRoot":"","sources":["../../src/middleware/module-gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGvD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,WAAW;cAClB,QAAQ;yBAO7C"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createMiddleware } from 'hono/factory';
|
|
2
|
+
import { getSettings } from '../env.js';
|
|
3
|
+
import { HttpError } from '../lib/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* 404s a disabled module's routes, indistinguishable from a route that never
|
|
6
|
+
* existed (don't leak "this module exists but is off"). Mounted by each
|
|
7
|
+
* module's router at its mount point.
|
|
8
|
+
*/
|
|
9
|
+
export function moduleGate(moduleName) {
|
|
10
|
+
return createMiddleware(async (c, next) => {
|
|
11
|
+
const settings = await getSettings(c.env.DB);
|
|
12
|
+
if (!settings.modules[moduleName]) {
|
|
13
|
+
throw new HttpError('not_found', 'not found');
|
|
14
|
+
}
|
|
15
|
+
await next();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=module-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-gate.js","sourceRoot":"","sources":["../../src/middleware/module-gate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,WAAW,EAAiB,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,UAA6B;IACtD,OAAO,gBAAgB,CAAyB,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAChE,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type RateLimitBucket } from '../shared/index.js';
|
|
2
|
+
import type { Bindings } from '../env.js';
|
|
3
|
+
/**
|
|
4
|
+
* D1 sliding-window rate limit, keyed on a hash of the caller's IP.
|
|
5
|
+
* A factory, not a single global middleware: mounted per bucket on whichever
|
|
6
|
+
* routes need it (forms mount formsBurst and formsHourly
|
|
7
|
+
* together; engagement endpoints mount just `engagement`). Admin routes are
|
|
8
|
+
* exempt simply by never mounting this on them.
|
|
9
|
+
*/
|
|
10
|
+
export declare function rateLimit(bucket: RateLimitBucket): import("hono").MiddlewareHandler<{
|
|
11
|
+
Bindings: Bindings;
|
|
12
|
+
}, string, {}, Response>;
|
|
13
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAEzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAO1C;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe;cAGX,QAAQ;yBAsB7C"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RATE_LIMIT_BUCKETS, sha256Hex } from '../shared/index.js';
|
|
2
|
+
import { createMiddleware } from 'hono/factory';
|
|
3
|
+
import { HttpError } from '../lib/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* D1 sliding-window rate limit, keyed on a hash of the caller's IP.
|
|
6
|
+
* A factory, not a single global middleware: mounted per bucket on whichever
|
|
7
|
+
* routes need it (forms mount formsBurst and formsHourly
|
|
8
|
+
* together; engagement endpoints mount just `engagement`). Admin routes are
|
|
9
|
+
* exempt simply by never mounting this on them.
|
|
10
|
+
*/
|
|
11
|
+
export function rateLimit(bucket) {
|
|
12
|
+
const config = RATE_LIMIT_BUCKETS[bucket];
|
|
13
|
+
return createMiddleware(async (c, next) => {
|
|
14
|
+
const ip = c.req.header('CF-Connecting-IP') ?? 'unknown';
|
|
15
|
+
const ipHash = await sha256Hex(ip);
|
|
16
|
+
const { results } = await c.env.DB.prepare(`SELECT COUNT(*) as count FROM rate_events
|
|
17
|
+
WHERE bucket = ?1 AND ip_hash = ?2 AND created_at > datetime('now', ?3)`)
|
|
18
|
+
.bind(bucket, ipHash, `-${config.windowSeconds} seconds`)
|
|
19
|
+
.all();
|
|
20
|
+
const count = results[0]?.count ?? 0;
|
|
21
|
+
if (count >= config.max) {
|
|
22
|
+
throw new HttpError('rate_limited', 'too many requests');
|
|
23
|
+
}
|
|
24
|
+
await c.env.DB.prepare(`INSERT INTO rate_events (bucket, ip_hash) VALUES (?1, ?2)`)
|
|
25
|
+
.bind(bucket, ipHash)
|
|
26
|
+
.run();
|
|
27
|
+
await next();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAwB,MAAM,oBAAoB,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAM7C;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,MAAuB;IAC/C,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAE1C,OAAO,gBAAgB,CAAyB,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAChE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,kBAAkB,CAAC,IAAI,SAAS,CAAC;QACzD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;QAEnC,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CACxC;+EACyE,CAC1E;aACE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,MAAM,CAAC,aAAa,UAAU,CAAC;aACxD,GAAG,EAAY,CAAC;QAEnB,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC;QACrC,IAAI,KAAK,IAAI,MAAM,CAAC,GAAG,EAAE,CAAC;YACxB,MAAM,IAAI,SAAS,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC;QAC3D,CAAC;QAED,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,2DAA2D,CAAC;aAChF,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;aACpB,GAAG,EAAE,CAAC;QAET,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type RequestIdVariables = {
|
|
2
|
+
requestId: string;
|
|
3
|
+
};
|
|
4
|
+
/** Assigns a request id and adds X-Request-Id / X-Response-Time to every response. */
|
|
5
|
+
export declare const requestId: import("hono").MiddlewareHandler<{
|
|
6
|
+
Variables: RequestIdVariables;
|
|
7
|
+
}, string, {}, Response>;
|
|
8
|
+
//# sourceMappingURL=request-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-id.d.ts","sourceRoot":"","sources":["../../src/middleware/request-id.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,sFAAsF;AACtF,eAAO,MAAM,SAAS;eAAiC,kBAAkB;wBASvE,CAAC"}
|