@glw907/cairn-cms 0.14.0 → 0.17.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/dist/auth/crypto.d.ts +8 -2
- package/dist/auth/crypto.d.ts.map +1 -1
- package/dist/auth/crypto.js +12 -2
- package/dist/auth/store.d.ts +2 -0
- package/dist/auth/store.d.ts.map +1 -1
- package/dist/auth/store.js +17 -5
- package/dist/components/EditPage.svelte +4 -6
- package/dist/components/EditPage.svelte.d.ts +1 -1
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/delivery/content-index.d.ts.map +1 -1
- package/dist/delivery/content-index.js +11 -9
- package/dist/delivery/feeds.d.ts +1 -1
- package/dist/delivery/feeds.d.ts.map +1 -1
- package/dist/delivery/feeds.js +31 -16
- package/dist/delivery/site-indexes.d.ts.map +1 -1
- package/dist/delivery/site-indexes.js +9 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +14 -0
- package/dist/github/signing.d.ts +12 -0
- package/dist/github/signing.d.ts.map +1 -1
- package/dist/github/signing.js +22 -0
- package/dist/render/pipeline.d.ts +10 -0
- package/dist/render/pipeline.d.ts.map +1 -1
- package/dist/render/pipeline.js +15 -1
- package/dist/render/sanitize-schema.d.ts +20 -0
- package/dist/render/sanitize-schema.d.ts.map +1 -0
- package/dist/render/sanitize-schema.js +48 -0
- package/dist/sveltekit/auth-routes.d.ts.map +1 -1
- package/dist/sveltekit/auth-routes.js +29 -11
- package/dist/sveltekit/content-routes.js +2 -2
- package/dist/sveltekit/guard.d.ts +1 -1
- package/dist/sveltekit/guard.d.ts.map +1 -1
- package/dist/sveltekit/guard.js +25 -10
- package/dist/sveltekit/nav-routes.js +2 -2
- package/dist/sveltekit/public-routes.js +1 -1
- package/dist/sveltekit/types.d.ts +6 -0
- package/dist/sveltekit/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/lib/auth/crypto.ts +14 -2
- package/src/lib/auth/store.ts +18 -5
- package/src/lib/components/EditPage.svelte +4 -6
- package/src/lib/delivery/content-index.ts +12 -9
- package/src/lib/delivery/feeds.ts +34 -19
- package/src/lib/delivery/site-indexes.ts +13 -1
- package/src/lib/env.ts +13 -0
- package/src/lib/github/signing.ts +32 -0
- package/src/lib/render/pipeline.ts +25 -1
- package/src/lib/render/sanitize-schema.ts +57 -0
- package/src/lib/sveltekit/auth-routes.ts +30 -11
- package/src/lib/sveltekit/content-routes.ts +2 -2
- package/src/lib/sveltekit/guard.ts +25 -10
- package/src/lib/sveltekit/nav-routes.ts +2 -2
- package/src/lib/sveltekit/public-routes.ts +1 -1
- package/src/lib/sveltekit/types.ts +5 -1
- package/dist/render/sanitize.d.ts +0 -8
- package/dist/render/sanitize.d.ts.map +0 -1
- package/dist/render/sanitize.js +0 -26
- package/src/lib/render/sanitize.ts +0 -27
package/dist/auth/crypto.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The session cookie name. On https the cookie is Secure and takes the __Host- prefix, which
|
|
3
|
+
* binds it to the origin (the browser enforces Secure, Path=/, and no Domain). On local http
|
|
4
|
+
* dev the prefix is dropped, since __Host- requires Secure and the dev cookie cannot set it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sessionCookieName(secure: boolean): string;
|
|
3
7
|
/** Magic-link tokens live 10 minutes. */
|
|
4
8
|
export declare const TOKEN_TTL_MS: number;
|
|
5
9
|
/** Sessions live 30 days. */
|
|
6
10
|
export declare const SESSION_TTL_MS: number;
|
|
11
|
+
/** A magic link is sent at most once per email per minute, to throttle inbox flooding. */
|
|
12
|
+
export declare const SEND_COOLDOWN_MS: number;
|
|
7
13
|
/** A fresh 256-bit magic-link token, url-safe. */
|
|
8
14
|
export declare function generateToken(): string;
|
|
9
15
|
/** A fresh 256-bit session id, url-safe. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/lib/auth/crypto.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/lib/auth/crypto.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED,yCAAyC;AACzC,eAAO,MAAM,YAAY,QAAiB,CAAC;AAE3C,6BAA6B;AAC7B,eAAO,MAAM,cAAc,QAA2B,CAAC;AAEvD,0FAA0F;AAC1F,eAAO,MAAM,gBAAgB,QAAY,CAAC;AAU1C,kDAAkD;AAClD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4CAA4C;AAC5C,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,oEAAoE;AACpE,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9D"}
|
package/dist/auth/crypto.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
// Token and session-id generation plus SHA-256 token hashing, on Web Crypto so the
|
|
2
2
|
// code runs unchanged in workerd. The store keeps only the hash of a token, never the
|
|
3
3
|
// token itself (spec 7.1).
|
|
4
|
-
/** The session cookie name. */
|
|
5
|
-
|
|
4
|
+
/** The base session cookie name, prefixed with __Host- when the cookie is Secure. */
|
|
5
|
+
const COOKIE_BASE = 'cairn_session';
|
|
6
|
+
/**
|
|
7
|
+
* The session cookie name. On https the cookie is Secure and takes the __Host- prefix, which
|
|
8
|
+
* binds it to the origin (the browser enforces Secure, Path=/, and no Domain). On local http
|
|
9
|
+
* dev the prefix is dropped, since __Host- requires Secure and the dev cookie cannot set it.
|
|
10
|
+
*/
|
|
11
|
+
export function sessionCookieName(secure) {
|
|
12
|
+
return secure ? `__Host-${COOKIE_BASE}` : COOKIE_BASE;
|
|
13
|
+
}
|
|
6
14
|
/** Magic-link tokens live 10 minutes. */
|
|
7
15
|
export const TOKEN_TTL_MS = 10 * 60 * 1000;
|
|
8
16
|
/** Sessions live 30 days. */
|
|
9
17
|
export const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
18
|
+
/** A magic link is sent at most once per email per minute, to throttle inbox flooding. */
|
|
19
|
+
export const SEND_COOLDOWN_MS = 60 * 1000;
|
|
10
20
|
function randomBase64Url(byteLength = 32) {
|
|
11
21
|
const bytes = new Uint8Array(byteLength);
|
|
12
22
|
crypto.getRandomValues(bytes);
|
package/dist/auth/store.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Editor, Role } from './types.js';
|
|
|
4
4
|
export declare function findEditor(db: D1Database, email: string): Promise<Editor | null>;
|
|
5
5
|
/** Replace any prior token for this email with a fresh one, atomically. */
|
|
6
6
|
export declare function issueToken(db: D1Database, email: string, tokenHash: string, expiresAt: number, now: number): Promise<void>;
|
|
7
|
+
/** True when a magic-link token for this email was issued at or after `since`, for the send cooldown. */
|
|
8
|
+
export declare function recentlyIssued(db: D1Database, email: string, since: number): Promise<boolean>;
|
|
7
9
|
/**
|
|
8
10
|
* Consume a token in one atomic statement. A returned email means the token was present and
|
|
9
11
|
* unexpired and is now gone, so the link is single-use by construction on strongly-consistent D1.
|
package/dist/auth/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/lib/auth/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAQ/C,yCAAyC;AACzC,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMtF;AAED,2EAA2E;AAC3E,wBAAsB,UAAU,CAC9B,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/lib/auth/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAQ/C,yCAAyC;AACzC,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMtF;AAED,2EAA2E;AAC3E,wBAAsB,UAAU,CAC9B,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,yGAAyG;AACzG,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMnG;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMzG;AAED,4BAA4B;AAC5B,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAUpG;AAED,iCAAiC;AACjC,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7E;AAED,2CAA2C;AAC3C,wBAAsB,WAAW,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKnE;AAED,sCAAsC;AACtC,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,0FAA0F;AAC1F,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/E;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAe1F;AAED,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5F;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1F"}
|
package/dist/auth/store.js
CHANGED
|
@@ -12,12 +12,21 @@ export async function findEditor(db, email) {
|
|
|
12
12
|
/** Replace any prior token for this email with a fresh one, atomically. */
|
|
13
13
|
export async function issueToken(db, email, tokenHash, expiresAt, now) {
|
|
14
14
|
await db.batch([
|
|
15
|
-
|
|
15
|
+
// Replace this email's prior token, and sweep any expired token while here (no cron needed).
|
|
16
|
+
db.prepare('DELETE FROM magic_token WHERE email = ? OR expires_at <= ?').bind(email, now),
|
|
16
17
|
db
|
|
17
18
|
.prepare('INSERT INTO magic_token (token_hash, email, expires_at, created_at) VALUES (?, ?, ?, ?)')
|
|
18
19
|
.bind(tokenHash, email, expiresAt, now),
|
|
19
20
|
]);
|
|
20
21
|
}
|
|
22
|
+
/** True when a magic-link token for this email was issued at or after `since`, for the send cooldown. */
|
|
23
|
+
export async function recentlyIssued(db, email, since) {
|
|
24
|
+
const row = await db
|
|
25
|
+
.prepare('SELECT 1 AS one FROM magic_token WHERE email = ? AND created_at >= ? LIMIT 1')
|
|
26
|
+
.bind(email, since)
|
|
27
|
+
.first();
|
|
28
|
+
return row != null;
|
|
29
|
+
}
|
|
21
30
|
/**
|
|
22
31
|
* Consume a token in one atomic statement. A returned email means the token was present and
|
|
23
32
|
* unexpired and is now gone, so the link is single-use by construction on strongly-consistent D1.
|
|
@@ -31,10 +40,13 @@ export async function consumeToken(db, tokenHash, now) {
|
|
|
31
40
|
}
|
|
32
41
|
/** Create a session row. */
|
|
33
42
|
export async function createSession(db, id, email, expiresAt, now) {
|
|
34
|
-
await db
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
43
|
+
await db.batch([
|
|
44
|
+
// Sweep expired sessions on login, so abandoned rows do not accumulate (no cron needed).
|
|
45
|
+
db.prepare('DELETE FROM session WHERE expires_at <= ?').bind(now),
|
|
46
|
+
db
|
|
47
|
+
.prepare('INSERT INTO session (id, email, expires_at, created_at) VALUES (?, ?, ?, ?)')
|
|
48
|
+
.bind(id, email, expiresAt, now),
|
|
49
|
+
]);
|
|
38
50
|
}
|
|
39
51
|
/**
|
|
40
52
|
* Resolve a session to its editor, joining `editor` so the role is read live. An expired
|
|
@@ -12,14 +12,13 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
12
12
|
import type { IconSet } from '../render/glyph.js';
|
|
13
13
|
import type { EditData } from '../sveltekit/content-routes.js';
|
|
14
14
|
import type { TextareaField, TagsField, FreeTagsField } from '../content/types.js';
|
|
15
|
-
import { sanitizePreviewHtml } from '../render/sanitize.js';
|
|
16
15
|
|
|
17
16
|
interface Props {
|
|
18
17
|
/** The edit load's data, plus the site name for the heading. */
|
|
19
18
|
data: EditData & { siteName: string };
|
|
20
19
|
/** The site's component registry, for the insert palette. */
|
|
21
20
|
registry?: ComponentRegistry;
|
|
22
|
-
/** The site's design-accurate render pipeline; the preview pane
|
|
21
|
+
/** The site's design-accurate render pipeline; the preview pane renders its output, which the floored pipeline already sanitized. */
|
|
23
22
|
render?: (md: string, opts?: { stagger?: boolean }) => string | Promise<string>;
|
|
24
23
|
/** The site's icon set, for the guided form's icon fields. */
|
|
25
24
|
icons?: IconSet;
|
|
@@ -46,8 +45,8 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
46
45
|
localStorage.setItem(PREVIEW_KEY, showPreview ? '1' : '0');
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
// Render the design-accurate preview as the body changes, debounced
|
|
50
|
-
//
|
|
48
|
+
// Render the design-accurate preview as the body changes, debounced. The site's render is the
|
|
49
|
+
// floored engine pipeline, so its output is already sanitized; the preview mirrors the page.
|
|
51
50
|
// previewRun is a plain counter (not reactive state) used as a latest-wins guard: if a slow earlier
|
|
52
51
|
// async render call resolves after a newer one has started, the stale result is discarded.
|
|
53
52
|
let previewRun = 0;
|
|
@@ -58,8 +57,7 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
58
57
|
const handle = setTimeout(async () => {
|
|
59
58
|
try {
|
|
60
59
|
const html = await render(md);
|
|
61
|
-
|
|
62
|
-
if (run === previewRun) previewHtml = safe;
|
|
60
|
+
if (run === previewRun) previewHtml = html;
|
|
63
61
|
} catch {
|
|
64
62
|
if (run === previewRun) previewHtml = '';
|
|
65
63
|
}
|
|
@@ -8,7 +8,7 @@ interface Props {
|
|
|
8
8
|
};
|
|
9
9
|
/** The site's component registry, for the insert palette. */
|
|
10
10
|
registry?: ComponentRegistry;
|
|
11
|
-
/** The site's design-accurate render pipeline; the preview pane
|
|
11
|
+
/** The site's design-accurate render pipeline; the preview pane renders its output, which the floored pipeline already sanitized. */
|
|
12
12
|
render?: (md: string, opts?: {
|
|
13
13
|
stagger?: boolean;
|
|
14
14
|
}) => string | Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAI7D,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,qIAAqI;IACrI,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChF,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAoJH;;;;GAIG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/content-index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,yFAAyF;AACzF,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;wEAEwE;AACxE,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,cAAc;IAC/E,WAAW,EAAE,CAAC,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,qCAAqC;AACrC,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACvD,GAAG,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IAC1D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IACzE,OAAO,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACzE,sFAAsF;IACtF,QAAQ,IAAI,cAAc,EAAE,CAAC;CAC9B;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,CAElE;AAqBD,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,KAAK,EAAE,OAAO,EAAE,EAChB,UAAU,EAAE,iBAAiB,GAC5B,YAAY,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"content-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/content-index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,yFAAyF;AACzF,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;wEAEwE;AACxE,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,cAAc;IAC/E,WAAW,EAAE,CAAC,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,qCAAqC;AACrC,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACvD,GAAG,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IAC1D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IACzE,OAAO,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACzE,sFAAsF;IACtF,QAAQ,IAAI,cAAc,EAAE,CAAC;CAC9B;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,CAElE;AAqBD,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,KAAK,EAAE,OAAO,EAAE,EAChB,UAAU,EAAE,iBAAiB,GAC5B,YAAY,CAAC,CAAC,CAAC,CAsEjB"}
|
|
@@ -30,19 +30,21 @@ function asTags(value) {
|
|
|
30
30
|
/** Build a concept's index from its raw files and normalized descriptor. */
|
|
31
31
|
export function createContentIndex(files, descriptor) {
|
|
32
32
|
const problems = [];
|
|
33
|
-
const entries =
|
|
33
|
+
const entries = [];
|
|
34
|
+
for (const file of files) {
|
|
34
35
|
const id = idFromFilename(basename(file.path));
|
|
35
36
|
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
36
37
|
const { frontmatter: raw, body } = parseMarkdown(file.raw);
|
|
37
38
|
const date = asDate(raw.date);
|
|
38
39
|
const draft = raw.draft === true;
|
|
39
|
-
// Validate once at build.
|
|
40
|
-
//
|
|
41
|
-
// failure is recorded, not thrown, so the query surface does not explode on construction.
|
|
40
|
+
// Validate once at build. A failure is recorded for the site gate and excluded from the typed
|
|
41
|
+
// read, so every readable entry's frontmatter is the validator's normalized output, never raw.
|
|
42
42
|
const result = descriptor.validate(raw, body);
|
|
43
|
-
if (!result.ok)
|
|
43
|
+
if (!result.ok) {
|
|
44
44
|
problems.push({ id, draft, errors: result.errors });
|
|
45
|
-
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
entries.push({
|
|
46
48
|
id,
|
|
47
49
|
slug,
|
|
48
50
|
permalink: permalink(descriptor, { id, slug, date }),
|
|
@@ -53,10 +55,10 @@ export function createContentIndex(files, descriptor) {
|
|
|
53
55
|
excerpt: deriveExcerpt(body, { description: asString(raw.description) }),
|
|
54
56
|
wordCount: wordCount(body),
|
|
55
57
|
draft,
|
|
56
|
-
frontmatter:
|
|
58
|
+
frontmatter: result.data,
|
|
57
59
|
body,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
60
62
|
// Dated concepts sort newest-first; undated concepts (Pages) sort by title.
|
|
61
63
|
const sorted = [...entries].sort((a, b) => descriptor.routing.dated ? (b.date ?? '').localeCompare(a.date ?? '') : a.title.localeCompare(b.title));
|
|
62
64
|
const summarize = (entry) => {
|
package/dist/delivery/feeds.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/feeds.ts"],"names":[],"mappings":"AAKA,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,uFAAuF;AACvF,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/feeds.ts"],"names":[],"mappings":"AAKA,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,uFAAuF;AACvF,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAiCD,iCAAiC;AACjC,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAqC5E;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CA4B7E"}
|
package/dist/delivery/feeds.js
CHANGED
|
@@ -13,30 +13,41 @@ function escapeXml(value) {
|
|
|
13
13
|
function cdataSafe(value) {
|
|
14
14
|
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/** Parse a YYYY-MM-DD (or ISO) string as a UTC instant. Returns undefined for an absent or
|
|
17
|
+
* unparseable date, so a feed omits the date field rather than emit Invalid Date or throw. */
|
|
18
|
+
function parseFeedDate(date) {
|
|
19
|
+
if (!date)
|
|
20
|
+
return undefined;
|
|
21
|
+
const at = new Date(`${date.slice(0, 10)}T00:00:00.000Z`);
|
|
22
|
+
return Number.isNaN(at.getTime()) ? undefined : at;
|
|
23
|
+
}
|
|
24
|
+
/** Format a date as an RFC-822 string in UTC, as RSS wants, or undefined when it cannot parse. */
|
|
17
25
|
function rfc822(date) {
|
|
18
|
-
return
|
|
26
|
+
return parseFeedDate(date)?.toUTCString();
|
|
19
27
|
}
|
|
20
|
-
/** Format a
|
|
28
|
+
/** Format a date as an ISO-8601 instant in UTC, or undefined when it cannot parse. */
|
|
21
29
|
function iso(date) {
|
|
22
|
-
return
|
|
30
|
+
return parseFeedDate(date)?.toISOString();
|
|
23
31
|
}
|
|
24
32
|
/** Build an RSS 2.0 document. */
|
|
25
33
|
export function buildRssFeed(channel, items) {
|
|
26
34
|
const entries = items
|
|
27
35
|
.map((item) => {
|
|
28
36
|
const content = item.contentHtml ?? item.summary;
|
|
37
|
+
const pubDate = rfc822(item.date);
|
|
29
38
|
return [
|
|
30
39
|
' <item>',
|
|
31
40
|
` <title>${escapeXml(item.title)}</title>`,
|
|
32
41
|
` <link>${escapeXml(item.url)}</link>`,
|
|
33
42
|
` <guid isPermaLink="true">${escapeXml(item.url)}</guid>`,
|
|
34
|
-
` <pubDate>${
|
|
43
|
+
pubDate ? ` <pubDate>${pubDate}</pubDate>` : '',
|
|
35
44
|
` <description>${escapeXml(item.summary)}</description>`,
|
|
36
45
|
// CDATA cannot contain `]]>`, so split that one sequence rather than escape the body.
|
|
37
46
|
` <content:encoded><![CDATA[${cdataSafe(content)}]]></content:encoded>`,
|
|
38
47
|
' </item>',
|
|
39
|
-
]
|
|
48
|
+
]
|
|
49
|
+
.filter((line) => line !== '')
|
|
50
|
+
.join('\n');
|
|
40
51
|
})
|
|
41
52
|
.join('\n');
|
|
42
53
|
return [
|
|
@@ -66,15 +77,19 @@ export function buildJsonFeed(channel, items) {
|
|
|
66
77
|
feed_url: channel.feedUrl,
|
|
67
78
|
...(channel.language ? { language: channel.language } : {}),
|
|
68
79
|
...(channel.author ? { authors: [channel.author] } : {}),
|
|
69
|
-
items: items.map((item) =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
items: items.map((item) => {
|
|
81
|
+
const datePublished = iso(item.date);
|
|
82
|
+
const dateModified = iso(item.updated);
|
|
83
|
+
return {
|
|
84
|
+
id: item.url,
|
|
85
|
+
url: item.url,
|
|
86
|
+
title: item.title,
|
|
87
|
+
summary: item.summary,
|
|
88
|
+
...(datePublished ? { date_published: datePublished } : {}),
|
|
89
|
+
...(dateModified ? { date_modified: dateModified } : {}),
|
|
90
|
+
...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
|
|
91
|
+
...(item.tags && item.tags.length ? { tags: item.tags } : {}),
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
79
94
|
}, null, 2);
|
|
80
95
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"site-indexes.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-indexes.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAgB,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE/D,oGAAoG;AACpG,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,YAAY,IAAI;KAC7C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CACnD,CAAC;AAEF;0EAC0E;AAC1E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,YAAY,IAAI;KAC/C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,YAAY,CACrC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACjG;CACF,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EAC5D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,WAAW,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"site-indexes.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-indexes.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAgB,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE/D,oGAAoG;AACpG,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,YAAY,IAAI;KAC7C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CACnD,CAAC;AAEF;0EAC0E;AAC1E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,YAAY,IAAI;KAC/C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,YAAY,CACrC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACjG;CACF,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EAC5D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,WAAW,CAAC,CAAC,CAAC,CAwBhB"}
|
|
@@ -9,10 +9,18 @@ import { createSiteIndex } from './site-index.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export function createSiteIndexes(adapter, config, globs, opts = {}) {
|
|
11
11
|
const descriptors = siteDescriptors(adapter, config);
|
|
12
|
+
const globRecord = globs;
|
|
12
13
|
const byConcept = {};
|
|
13
14
|
const conceptIndexes = [];
|
|
14
15
|
for (const descriptor of descriptors) {
|
|
15
|
-
|
|
16
|
+
if (descriptor.id === 'site') {
|
|
17
|
+
throw new Error('createSiteIndexes: a concept cannot be named "site", which is the reserved cross-concept resolver key');
|
|
18
|
+
}
|
|
19
|
+
if (!Object.prototype.hasOwnProperty.call(globRecord, descriptor.id)) {
|
|
20
|
+
const passed = Object.keys(globRecord);
|
|
21
|
+
throw new Error(`createSiteIndexes: no glob passed for concept "${descriptor.id}"; pass its import.meta.glob (an empty {} for an intentionally empty concept). Globs passed: ${passed.length ? passed.join(', ') : '(none)'}`);
|
|
22
|
+
}
|
|
23
|
+
const record = globRecord[descriptor.id] ?? {};
|
|
16
24
|
const index = createContentIndex(fromGlob(record), descriptor);
|
|
17
25
|
byConcept[descriptor.id] = index;
|
|
18
26
|
conceptIndexes.push({ descriptor, index });
|
package/dist/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmBrE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,UAAU,CAKnE"}
|
package/dist/env.js
CHANGED
|
@@ -11,6 +11,20 @@ export function requireOrigin(env) {
|
|
|
11
11
|
if (!origin) {
|
|
12
12
|
throw new Error('PUBLIC_ORIGIN is not configured');
|
|
13
13
|
}
|
|
14
|
+
let hostname;
|
|
15
|
+
try {
|
|
16
|
+
hostname = new URL(origin).hostname;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`PUBLIC_ORIGIN is not a valid URL, got ${origin}`);
|
|
20
|
+
}
|
|
21
|
+
// The magic-link origin must be https in production so the link and the __Host- cookie are
|
|
22
|
+
// origin-bound. http is allowed only for local dev on localhost or 127.0.0.1, matched exactly so
|
|
23
|
+
// a lookalike host like localhost.example.com cannot skip the https requirement.
|
|
24
|
+
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1';
|
|
25
|
+
if (!origin.startsWith('https://') && !isLocal) {
|
|
26
|
+
throw new Error(`PUBLIC_ORIGIN must be https in production, got ${origin}`);
|
|
27
|
+
}
|
|
14
28
|
return origin;
|
|
15
29
|
}
|
|
16
30
|
/**
|
package/dist/github/signing.d.ts
CHANGED
|
@@ -3,6 +3,18 @@ import type { AppCredentials } from './types.js';
|
|
|
3
3
|
export declare function appJwt(appId: string, privateKeyPem: string): Promise<string>;
|
|
4
4
|
/** Exchange the App JWT for a short-lived installation access token. */
|
|
5
5
|
export declare function installationToken(creds: AppCredentials): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Build an installation-token cache. A module-global instance memoizes the minted token per
|
|
8
|
+
* installation for most of its one-hour life, so a warm Worker isolate reuses it across requests
|
|
9
|
+
* instead of re-signing and re-calling GitHub on every list and commit. A cold isolate re-mints,
|
|
10
|
+
* which is always safe. This mirrors the default of @octokit/auth-app, which caches installation
|
|
11
|
+
* tokens in memory and returns them until expiry. The TTL stays under GitHub's documented one-hour
|
|
12
|
+
* lifetime, so a fixed margin avoids parsing the API expiry. `mint` and `now` are injected so the
|
|
13
|
+
* cache is testable with no network call and no real clock.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createInstallationTokenCache(mint?: (creds: AppCredentials) => Promise<string>, now?: () => number, ttlMs?: number): (creds: AppCredentials) => Promise<string>;
|
|
16
|
+
/** The shared installation-token cache, one instance per Worker isolate. */
|
|
17
|
+
export declare const cachedInstallationToken: (creds: AppCredentials) => Promise<string>;
|
|
6
18
|
/**
|
|
7
19
|
* Deploy-time self-test for the App signer: sign a dummy JWT with the configured key. It
|
|
8
20
|
* exercises the brittle PKCS#1-to-PKCS#8 conversion and the Web Crypto import and sign with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/lib/github/signing.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA0CjD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH"}
|
|
1
|
+
{"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/lib/github/signing.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA0CjD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;AAOD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAqB,EACpE,GAAG,GAAE,MAAM,MAAyB,EACpC,KAAK,SAAiB,GACrB,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAS5C;AAED,4EAA4E;AAC5E,eAAO,MAAM,uBAAuB,UAZzB,cAAc,KAAK,OAAO,CAAC,MAAM,CAYyB,CAAC;AAEtE;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH"}
|
package/dist/github/signing.js
CHANGED
|
@@ -59,6 +59,28 @@ export async function installationToken(creds) {
|
|
|
59
59
|
throw new Error(`GitHub installation token failed: ${res.status}`);
|
|
60
60
|
return (await res.json()).token;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Build an installation-token cache. A module-global instance memoizes the minted token per
|
|
64
|
+
* installation for most of its one-hour life, so a warm Worker isolate reuses it across requests
|
|
65
|
+
* instead of re-signing and re-calling GitHub on every list and commit. A cold isolate re-mints,
|
|
66
|
+
* which is always safe. This mirrors the default of @octokit/auth-app, which caches installation
|
|
67
|
+
* tokens in memory and returns them until expiry. The TTL stays under GitHub's documented one-hour
|
|
68
|
+
* lifetime, so a fixed margin avoids parsing the API expiry. `mint` and `now` are injected so the
|
|
69
|
+
* cache is testable with no network call and no real clock.
|
|
70
|
+
*/
|
|
71
|
+
export function createInstallationTokenCache(mint = installationToken, now = () => Date.now(), ttlMs = 55 * 60 * 1000) {
|
|
72
|
+
const cache = new Map();
|
|
73
|
+
return async function get(creds) {
|
|
74
|
+
const hit = cache.get(creds.installationId);
|
|
75
|
+
if (hit && hit.expiresAt > now())
|
|
76
|
+
return hit.token;
|
|
77
|
+
const token = await mint(creds);
|
|
78
|
+
cache.set(creds.installationId, { token, expiresAt: now() + ttlMs });
|
|
79
|
+
return token;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** The shared installation-token cache, one instance per Worker isolate. */
|
|
83
|
+
export const cachedInstallationToken = createInstallationTokenCache();
|
|
62
84
|
/**
|
|
63
85
|
* Deploy-time self-test for the App signer: sign a dummy JWT with the configured key. It
|
|
64
86
|
* exercises the brittle PKCS#1-to-PKCS#8 conversion and the Web Crypto import and sign with
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { type PluggableList } from 'unified';
|
|
2
|
+
import type { Schema } from 'hast-util-sanitize';
|
|
2
3
|
import type { ComponentRegistry } from './registry.js';
|
|
3
4
|
export interface RendererOptions {
|
|
4
5
|
/** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
|
|
5
6
|
* CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
|
|
6
7
|
* is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`. */
|
|
7
8
|
stagger?: boolean;
|
|
9
|
+
/** Extend the sanitize allowlist. Receives cairn's default schema (defaultSchema plus the
|
|
10
|
+
* directive markers and the common benign tags) and returns the schema to use. Add to the
|
|
11
|
+
* allowlist for the benign HTML a site's content needs; start from the argument so the
|
|
12
|
+
* dangerous strip is preserved. */
|
|
13
|
+
sanitizeSchema?: (defaults: Schema) => Schema;
|
|
14
|
+
/** Developer-only escape hatch: disable the sanitize floor entirely. This reintroduces the XSS
|
|
15
|
+
* vector the floor closes, so it is only for a site whose content is fully developer-controlled.
|
|
16
|
+
* It is a code-level adapter decision, never an editor-facing setting. */
|
|
17
|
+
unsafeDisableSanitize?: boolean;
|
|
8
18
|
}
|
|
9
19
|
/** Compose a site's render pipeline from its component registry: directive syntax to
|
|
10
20
|
* stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAStD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAIjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;0FAEsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;wCAGoC;IACpC,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9C;;+EAE2E;IAC3E,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAyBrD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAE3D"}
|
package/dist/render/pipeline.js
CHANGED
|
@@ -6,6 +6,8 @@ import remarkRehype from 'remark-rehype';
|
|
|
6
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
7
|
import rehypeSlug from 'rehype-slug';
|
|
8
8
|
import rehypeStringify from 'rehype-stringify';
|
|
9
|
+
import rehypeSanitize from 'rehype-sanitize';
|
|
10
|
+
import { buildSanitizeSchema, rehypeAnchorRel } from './sanitize-schema.js';
|
|
9
11
|
import { remarkDirectiveStamp } from './remark-directives.js';
|
|
10
12
|
import { rehypeDispatch } from './rehype-dispatch.js';
|
|
11
13
|
/** Compose a site's render pipeline from its component registry: directive syntax to
|
|
@@ -13,7 +15,19 @@ import { rehypeDispatch } from './rehype-dispatch.js';
|
|
|
13
15
|
* rehype plugin arrays (so the admin editor preview can reuse the exact same set). */
|
|
14
16
|
export function createRenderer(registry, options = {}) {
|
|
15
17
|
const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry]];
|
|
16
|
-
|
|
18
|
+
// The sanitize floor runs after rehype-raw (so author raw HTML is parsed, then cleaned) and
|
|
19
|
+
// before the dispatch (so the site's trusted build() output and its inline SVG icons are never
|
|
20
|
+
// sanitized). The anchor-rel hardening runs last so it also covers component-built anchors.
|
|
21
|
+
const floor = options.unsafeDisableSanitize
|
|
22
|
+
? []
|
|
23
|
+
: [[rehypeSanitize, buildSanitizeSchema(registry, options.sanitizeSchema)]];
|
|
24
|
+
const rehypePlugins = [
|
|
25
|
+
rehypeRaw,
|
|
26
|
+
...floor,
|
|
27
|
+
[rehypeDispatch, registry, options.stagger],
|
|
28
|
+
rehypeSlug,
|
|
29
|
+
rehypeAnchorRel,
|
|
30
|
+
];
|
|
17
31
|
const processor = unified()
|
|
18
32
|
.use(remarkParse)
|
|
19
33
|
.use(remarkGfm)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Schema } from 'hast-util-sanitize';
|
|
2
|
+
import type { Root } from 'hast';
|
|
3
|
+
import { type ComponentRegistry } from './registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Build the delivery sanitize schema. Starts from hast-util-sanitize's defaultSchema, the
|
|
6
|
+
* GitHub-lineage allowlist that strips scripts, inline event handlers, and javascript:/data: URLs,
|
|
7
|
+
* then adds exactly what cairn's render needs. The directive markers (the fixed ones plus the
|
|
8
|
+
* dataAttr<Key> markers derived from the registry) survive so the dispatch reads its stamps after
|
|
9
|
+
* the floor. The benign author tags real content uses (nav, details, summary) and class/target/rel
|
|
10
|
+
* on anchors are admitted. A site extends the result through `extend`, always starting from this
|
|
11
|
+
* safe base, so it can add to the allowlist but not weaken the core strip.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildSanitizeSchema(registry: ComponentRegistry, extend?: (defaults: Schema) => Schema): Schema;
|
|
14
|
+
/**
|
|
15
|
+
* Force rel="noopener noreferrer" on every target="_blank" anchor, to prevent reverse-tabnabbing.
|
|
16
|
+
* hast-util-sanitize runs no per-node hook, so this small transform carries the behavior the old
|
|
17
|
+
* DOMPurify preview pass enforced, now on the delivered output as well.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rehypeAnchorRel(): (tree: Root) => void;
|
|
20
|
+
//# sourceMappingURL=sanitize-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-schema.d.ts","sourceRoot":"","sources":["../../src/lib/render/sanitize-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAW,MAAM,MAAM,CAAC;AAE1C,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAMrE;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GACpC,MAAM,CAoBR;AAED;;;;GAIG;AACH,wBAAgB,eAAe,KACrB,MAAM,IAAI,UAOnB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defaultSchema } from 'hast-util-sanitize';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
import { dataAttrProp } from './registry.js';
|
|
4
|
+
// The fixed directive markers the stamp writes and the dispatch reads. They are inert data
|
|
5
|
+
// attributes, never a script vector, and must survive the floor so the dispatch still runs.
|
|
6
|
+
const FIXED_MARKERS = ['dataPrimitive', 'dataSlot', 'dataIcon', 'dataRole', 'dataRise'];
|
|
7
|
+
/**
|
|
8
|
+
* Build the delivery sanitize schema. Starts from hast-util-sanitize's defaultSchema, the
|
|
9
|
+
* GitHub-lineage allowlist that strips scripts, inline event handlers, and javascript:/data: URLs,
|
|
10
|
+
* then adds exactly what cairn's render needs. The directive markers (the fixed ones plus the
|
|
11
|
+
* dataAttr<Key> markers derived from the registry) survive so the dispatch reads its stamps after
|
|
12
|
+
* the floor. The benign author tags real content uses (nav, details, summary) and class/target/rel
|
|
13
|
+
* on anchors are admitted. A site extends the result through `extend`, always starting from this
|
|
14
|
+
* safe base, so it can add to the allowlist but not weaken the core strip.
|
|
15
|
+
*/
|
|
16
|
+
export function buildSanitizeSchema(registry, extend) {
|
|
17
|
+
const attrMarkers = registry.defs.flatMap((d) => (d.attributes ?? []).map((a) => dataAttrProp(a.key)));
|
|
18
|
+
const markers = [...FIXED_MARKERS, ...attrMarkers];
|
|
19
|
+
const attributes = defaultSchema.attributes ?? {};
|
|
20
|
+
// defaultSchema's `a` entry carries a className tuple (`['className', 'data-footnote-backref']`)
|
|
21
|
+
// that restricts a link's class to that one value. A per-tag tuple wins over a bare `*` entry, so
|
|
22
|
+
// it would strip an author's link class. Drop that tuple before admitting a free-form `className`.
|
|
23
|
+
const anchorAttrs = (attributes.a ?? []).filter((entry) => !(Array.isArray(entry) && entry[0] === 'className'));
|
|
24
|
+
const schema = {
|
|
25
|
+
...defaultSchema,
|
|
26
|
+
tagNames: [...(defaultSchema.tagNames ?? []), 'nav', 'details', 'summary'],
|
|
27
|
+
attributes: {
|
|
28
|
+
...attributes,
|
|
29
|
+
'*': [...(attributes['*'] ?? []), 'className', ...markers],
|
|
30
|
+
a: [...anchorAttrs, 'className', 'target', 'rel'],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return extend ? extend(schema) : schema;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Force rel="noopener noreferrer" on every target="_blank" anchor, to prevent reverse-tabnabbing.
|
|
37
|
+
* hast-util-sanitize runs no per-node hook, so this small transform carries the behavior the old
|
|
38
|
+
* DOMPurify preview pass enforced, now on the delivered output as well.
|
|
39
|
+
*/
|
|
40
|
+
export function rehypeAnchorRel() {
|
|
41
|
+
return (tree) => {
|
|
42
|
+
visit(tree, 'element', (node) => {
|
|
43
|
+
if (node.tagName === 'a' && node.properties?.target === '_blank') {
|
|
44
|
+
node.properties.rel = 'noopener noreferrer';
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/auth-routes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/auth-routes.ts"],"names":[],"mappings":"AAeA,OAAO,EAAyC,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB;uBA2C7B,cAAc,KAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;2BAnCjD,cAAc,KAAG,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;yBA4CnE,cAAc,KACpB;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;2BAcxB,cAAc,KAAG,OAAO,CAAC,KAAK,CAAC;0BAyBhC,cAAc,KAAG,OAAO,CAAC,KAAK,CAAC;EAUnE"}
|