emdash 0.4.0 → 0.6.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/{adapters-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-B4MsLM-w.mjs} +27 -12
- package/dist/apply-B4MsLM-w.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +208 -34
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +34 -9
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +5 -3
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +460 -180
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -8
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +9 -8
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db-errors-D0UT85nC.mjs +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CRg3PWfZ.d.mts → index-BYv0mB9g.d.mts} +135 -19
- package/dist/index-BYv0mB9g.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +20 -18
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-B6Vu0d2i.mjs → query-Bk_3vKvU.mjs} +78 -11
- package/dist/query-Bk_3vKvU.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-Fl2NcUUz.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-Fl2NcUUz.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-B5p9D36n.mjs → search-DI4bM2w9.mjs} +110 -209
- package/dist/search-DI4bM2w9.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +8 -7
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-DbrKzDju.mjs +308 -0
- package/dist/taxonomies-DbrKzDju.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-8xrvl_68.d.mts} +1 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-8xrvl_68.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-gLYVCXCQ.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-BYWYxLcp.d.mts → types-DgrIP0tF.d.mts} +9 -2
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-CcNRWH6I.d.mts → validate-CaLH1Ia2.d.mts} +5 -52
- package/dist/validate-CaLH1Ia2.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/version-Uaf2ynPX.mjs +7 -0
- package/dist/{version-DlTDRdpv.mjs.map → version-Uaf2ynPX.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/auth.ts +7 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +176 -0
- package/src/astro/integration/index.ts +42 -0
- package/src/astro/integration/routes.ts +17 -1
- package/src/astro/integration/runtime.ts +63 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +39 -6
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +10 -5
- package/src/astro/routes/api/auth/invite/register-options.ts +78 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +1 -1
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- package/src/astro/routes/api/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +6 -4
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/components/Table.astro +73 -41
- package/src/components/index.ts +2 -12
- package/src/components/marks.ts +20 -0
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +1 -0
- package/src/plugins/email-console.ts +9 -2
- package/src/plugins/types.ts +8 -0
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +15 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CRg3PWfZ.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/search-B5p9D36n.mjs.map +0 -1
- package/dist/types-BYWYxLcp.d.mts.map +0 -1
- package/dist/types-gLYVCXCQ.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-CcNRWH6I.d.mts.map +0 -1
- package/dist/version-DlTDRdpv.mjs +0 -7
package/src/api/schemas/media.ts
CHANGED
|
@@ -21,21 +21,32 @@ export const mediaUpdateBody = z
|
|
|
21
21
|
})
|
|
22
22
|
.meta({ id: "MediaUpdateBody" });
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
/** Default maximum allowed file upload size (50 MB). */
|
|
25
|
+
export const DEFAULT_MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
export function formatFileSize(bytes: number): string {
|
|
28
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
29
|
+
if (bytes < 1024 * 1024) return `${Math.floor(bytes / 1024)}KB`;
|
|
30
|
+
return `${Math.floor(bytes / 1024 / 1024)}MB`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function mediaUploadUrlBody(maxSize: number) {
|
|
34
|
+
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
35
|
+
throw new Error(`EmDash: maxUploadSize must be a positive finite number, got ${maxSize}`);
|
|
36
|
+
}
|
|
37
|
+
return z
|
|
38
|
+
.object({
|
|
39
|
+
filename: z.string().min(1, "filename is required"),
|
|
40
|
+
contentType: z.string().min(1, "contentType is required"),
|
|
41
|
+
size: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.positive()
|
|
45
|
+
.max(maxSize, `File size must not exceed ${formatFileSize(maxSize)}`),
|
|
46
|
+
contentHash: z.string().optional(),
|
|
47
|
+
})
|
|
48
|
+
.meta({ id: "MediaUploadUrlBody" });
|
|
49
|
+
}
|
|
39
50
|
|
|
40
51
|
export const mediaConfirmBody = z
|
|
41
52
|
.object({
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmDash Noto Sans font provider
|
|
3
|
+
*
|
|
4
|
+
* A custom Astro font provider that wraps Google Fonts to resolve
|
|
5
|
+
* multiple Noto Sans families (Latin, Arabic, JP, etc.) under a
|
|
6
|
+
* single logical font entry. This lets all @font-face blocks share
|
|
7
|
+
* the same font-family name, so the browser picks the right file
|
|
8
|
+
* per character via unicode-range.
|
|
9
|
+
*
|
|
10
|
+
* Without this, registering "Noto Sans" and "Noto Sans Arabic" as
|
|
11
|
+
* separate font entries on the same cssVariable triggers an Astro
|
|
12
|
+
* warning and the last entry overwrites the first.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { fontProviders } from "astro/config";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* All subset names used by Google Fonts CSS responses.
|
|
19
|
+
* Passed when resolving extra script families so the unifont
|
|
20
|
+
* provider doesn't filter out any faces.
|
|
21
|
+
*/
|
|
22
|
+
const ALL_GOOGLE_SUBSETS = [
|
|
23
|
+
"arabic",
|
|
24
|
+
"armenian",
|
|
25
|
+
"bengali",
|
|
26
|
+
"chinese-simplified",
|
|
27
|
+
"chinese-traditional",
|
|
28
|
+
"chinese-hongkong",
|
|
29
|
+
"cyrillic",
|
|
30
|
+
"cyrillic-ext",
|
|
31
|
+
"devanagari",
|
|
32
|
+
"ethiopic",
|
|
33
|
+
"georgian",
|
|
34
|
+
"greek",
|
|
35
|
+
"greek-ext",
|
|
36
|
+
"gujarati",
|
|
37
|
+
"gurmukhi",
|
|
38
|
+
"hebrew",
|
|
39
|
+
"japanese",
|
|
40
|
+
"kannada",
|
|
41
|
+
"khmer",
|
|
42
|
+
"korean",
|
|
43
|
+
"lao",
|
|
44
|
+
"latin",
|
|
45
|
+
"latin-ext",
|
|
46
|
+
"malayalam",
|
|
47
|
+
"math",
|
|
48
|
+
"myanmar",
|
|
49
|
+
"oriya",
|
|
50
|
+
"sinhala",
|
|
51
|
+
"symbols",
|
|
52
|
+
"tamil",
|
|
53
|
+
"telugu",
|
|
54
|
+
"thai",
|
|
55
|
+
"tibetan",
|
|
56
|
+
"vietnamese",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Known Noto Sans script families on Google Fonts.
|
|
61
|
+
* Maps user-friendly script names to Google Fonts family names.
|
|
62
|
+
*/
|
|
63
|
+
const NOTO_SCRIPT_FAMILIES: Record<string, string> = {
|
|
64
|
+
arabic: "Noto Sans Arabic",
|
|
65
|
+
armenian: "Noto Sans Armenian",
|
|
66
|
+
bengali: "Noto Sans Bengali",
|
|
67
|
+
"chinese-simplified": "Noto Sans SC",
|
|
68
|
+
"chinese-traditional": "Noto Sans TC",
|
|
69
|
+
"chinese-hongkong": "Noto Sans HK",
|
|
70
|
+
devanagari: "Noto Sans Devanagari",
|
|
71
|
+
ethiopic: "Noto Sans Ethiopic",
|
|
72
|
+
georgian: "Noto Sans Georgian",
|
|
73
|
+
gujarati: "Noto Sans Gujarati",
|
|
74
|
+
gurmukhi: "Noto Sans Gurmukhi",
|
|
75
|
+
hebrew: "Noto Sans Hebrew",
|
|
76
|
+
japanese: "Noto Sans JP",
|
|
77
|
+
kannada: "Noto Sans Kannada",
|
|
78
|
+
khmer: "Noto Sans Khmer",
|
|
79
|
+
korean: "Noto Sans KR",
|
|
80
|
+
lao: "Noto Sans Lao",
|
|
81
|
+
malayalam: "Noto Sans Malayalam",
|
|
82
|
+
myanmar: "Noto Sans Myanmar",
|
|
83
|
+
oriya: "Noto Sans Oriya",
|
|
84
|
+
sinhala: "Noto Sans Sinhala",
|
|
85
|
+
tamil: "Noto Sans Tamil",
|
|
86
|
+
telugu: "Noto Sans Telugu",
|
|
87
|
+
thai: "Noto Sans Thai",
|
|
88
|
+
tibetan: "Noto Sans Tibetan",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export interface NotoSansProviderOptions {
|
|
92
|
+
/**
|
|
93
|
+
* Additional Noto Sans script families to include.
|
|
94
|
+
* Use script names like "arabic", "japanese", "chinese-simplified".
|
|
95
|
+
*
|
|
96
|
+
* @see {@link NOTO_SCRIPT_FAMILIES} for the full list of supported scripts.
|
|
97
|
+
*/
|
|
98
|
+
scripts?: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Use ReturnType to get the provider type without importing it directly.
|
|
102
|
+
// The Astro FontProvider type is not part of the public API surface.
|
|
103
|
+
type GoogleProvider = ReturnType<typeof fontProviders.google>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a font provider that resolves Noto Sans plus additional
|
|
107
|
+
* script-specific Noto families from Google Fonts, all under one
|
|
108
|
+
* font-family name.
|
|
109
|
+
*/
|
|
110
|
+
export function notoSans(options?: NotoSansProviderOptions): GoogleProvider {
|
|
111
|
+
// Create a single Google provider instance to share initialization
|
|
112
|
+
const googleProvider = fontProviders.google();
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
name: "emdash-noto",
|
|
116
|
+
async init(context) {
|
|
117
|
+
await googleProvider.init?.(context);
|
|
118
|
+
},
|
|
119
|
+
async resolveFont(resolveFontOptions) {
|
|
120
|
+
// Resolve the base Noto Sans (Latin, Cyrillic, Greek, etc.)
|
|
121
|
+
const base = await googleProvider.resolveFont(resolveFontOptions);
|
|
122
|
+
const baseFonts = base?.fonts ?? [];
|
|
123
|
+
|
|
124
|
+
if (!options?.scripts?.length) {
|
|
125
|
+
return base;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Collect subset names already covered by the base font so we
|
|
129
|
+
// can filter out duplicate faces from extra script families.
|
|
130
|
+
// e.g. Noto Sans Arabic includes latin/latin-ext faces that
|
|
131
|
+
// would otherwise override the base Noto Sans latin faces.
|
|
132
|
+
const baseSubsets = new Set(baseFonts.map((f) => f.meta?.subset).filter(Boolean));
|
|
133
|
+
|
|
134
|
+
// Resolve additional script families
|
|
135
|
+
const extraFonts = await Promise.all(
|
|
136
|
+
options.scripts.map(async (script) => {
|
|
137
|
+
const family = NOTO_SCRIPT_FAMILIES[script];
|
|
138
|
+
if (!family) {
|
|
139
|
+
// Silently skip subset names that are already covered
|
|
140
|
+
// by the base Noto Sans font (latin, cyrillic, etc.)
|
|
141
|
+
if (ALL_GOOGLE_SUBSETS.includes(script)) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
console.warn(
|
|
145
|
+
`[emdash] Unknown Noto Sans script "${script}". ` +
|
|
146
|
+
`Available: ${Object.keys(NOTO_SCRIPT_FAMILIES).join(", ")}`,
|
|
147
|
+
);
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return googleProvider.resolveFont({
|
|
151
|
+
...resolveFontOptions,
|
|
152
|
+
familyName: family,
|
|
153
|
+
// Pass all known subset names so the unifont provider
|
|
154
|
+
// doesn't filter out any faces. Each script family
|
|
155
|
+
// only returns faces for its own subsets anyway.
|
|
156
|
+
subsets: ALL_GOOGLE_SUBSETS,
|
|
157
|
+
});
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Merge, dropping faces from extra fonts that duplicate base subsets
|
|
162
|
+
const extraFaces = extraFonts.flatMap((r) =>
|
|
163
|
+
(r?.fonts ?? []).filter((f) => !f.meta?.subset || !baseSubsets.has(f.meta.subset)),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
fonts: [...baseFonts, ...extraFaces],
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Get the list of available Noto Sans script names */
|
|
174
|
+
export function getAvailableNotoScripts(): string[] {
|
|
175
|
+
return Object.keys(NOTO_SCRIPT_FAMILIES);
|
|
176
|
+
}
|
|
@@ -14,6 +14,7 @@ import type { AstroIntegration, AstroIntegrationLogger } from "astro";
|
|
|
14
14
|
|
|
15
15
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
16
16
|
import { local } from "../storage/adapters.js";
|
|
17
|
+
import { notoSans } from "./font-provider.js";
|
|
17
18
|
import { injectCoreRoutes, injectBuiltinAuthRoutes, injectMcpRoute } from "./routes.js";
|
|
18
19
|
import type { EmDashConfig, PluginDescriptor } from "./runtime.js";
|
|
19
20
|
import { createViteConfig } from "./vite-config.js";
|
|
@@ -158,6 +159,7 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
158
159
|
auth: resolvedConfig.auth,
|
|
159
160
|
marketplace: resolvedConfig.marketplace,
|
|
160
161
|
siteUrl: resolvedConfig.siteUrl,
|
|
162
|
+
maxUploadSize: resolvedConfig.maxUploadSize,
|
|
161
163
|
};
|
|
162
164
|
|
|
163
165
|
// Determine auth mode for route injection
|
|
@@ -207,8 +209,48 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
207
209
|
? { allowedDomains: [{ hostname: new URL(resolvedConfig.siteUrl).hostname }] }
|
|
208
210
|
: {}),
|
|
209
211
|
};
|
|
212
|
+
|
|
213
|
+
// Inject default Noto Sans font for the admin UI.
|
|
214
|
+
// Uses the Astro Font API so fonts are downloaded at build time
|
|
215
|
+
// and self-hosted (no runtime CDN requests).
|
|
216
|
+
//
|
|
217
|
+
// The admin CSS references var(--font-emdash) with a system font
|
|
218
|
+
// fallback. Users can add extra script coverage (Arabic, CJK, etc.)
|
|
219
|
+
// by passing fonts.scripts in the emdash() config. The custom
|
|
220
|
+
// notoSans provider resolves all script families from Google Fonts
|
|
221
|
+
// under a single font-family name, so they stack via unicode-range.
|
|
222
|
+
const fontsConfig = resolvedConfig.fonts;
|
|
223
|
+
const emdashFonts =
|
|
224
|
+
fontsConfig === false
|
|
225
|
+
? []
|
|
226
|
+
: [
|
|
227
|
+
{
|
|
228
|
+
provider: notoSans({
|
|
229
|
+
scripts: fontsConfig?.scripts,
|
|
230
|
+
}),
|
|
231
|
+
name: "Noto Sans",
|
|
232
|
+
cssVariable: "--font-emdash",
|
|
233
|
+
weights: ["100 900" as const],
|
|
234
|
+
styles: ["normal" as const, "italic" as const],
|
|
235
|
+
subsets: [
|
|
236
|
+
"latin" as const,
|
|
237
|
+
"latin-ext" as const,
|
|
238
|
+
"cyrillic" as const,
|
|
239
|
+
"cyrillic-ext" as const,
|
|
240
|
+
"devanagari" as const,
|
|
241
|
+
"greek" as const,
|
|
242
|
+
"greek-ext" as const,
|
|
243
|
+
"vietnamese" as const,
|
|
244
|
+
],
|
|
245
|
+
fallbacks: ["ui-sans-serif", "system-ui", "sans-serif"],
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
|
|
210
249
|
updateConfig({
|
|
211
250
|
security: securityConfig,
|
|
251
|
+
// fonts is a valid AstroConfig key but may not be in the
|
|
252
|
+
// type definition for the minimum supported Astro version
|
|
253
|
+
...({ fonts: emdashFonts } as Record<string, unknown>),
|
|
212
254
|
vite: createViteConfig(
|
|
213
255
|
{
|
|
214
256
|
serializableConfig,
|
|
@@ -511,10 +511,16 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
511
511
|
});
|
|
512
512
|
|
|
513
513
|
injectRoute({
|
|
514
|
-
pattern: "
|
|
514
|
+
pattern: "/.well-known/oauth-authorization-server/_emdash",
|
|
515
515
|
entrypoint: resolveRoute("api/well-known/oauth-authorization-server.ts"),
|
|
516
516
|
});
|
|
517
517
|
|
|
518
|
+
// RFC 7591 Dynamic Client Registration
|
|
519
|
+
injectRoute({
|
|
520
|
+
pattern: "/_emdash/api/oauth/register",
|
|
521
|
+
entrypoint: resolveRoute("api/oauth/register.ts"),
|
|
522
|
+
});
|
|
523
|
+
|
|
518
524
|
// Plugin-defined API routes
|
|
519
525
|
// All plugin routes are handled by a single catch-all handler
|
|
520
526
|
injectRoute({
|
|
@@ -712,6 +718,11 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
712
718
|
entrypoint: resolveRoute("api/setup/dev-reset.ts"),
|
|
713
719
|
});
|
|
714
720
|
|
|
721
|
+
injectRoute({
|
|
722
|
+
pattern: "/_emdash/api/dev/emails",
|
|
723
|
+
entrypoint: resolveRoute("api/dev/emails.ts"),
|
|
724
|
+
});
|
|
725
|
+
|
|
715
726
|
// Current user endpoint (always available)
|
|
716
727
|
injectRoute({
|
|
717
728
|
pattern: "/_emdash/api/auth/me",
|
|
@@ -794,6 +805,11 @@ export function injectBuiltinAuthRoutes(injectRoute: InjectRoute): void {
|
|
|
794
805
|
entrypoint: resolveRoute("api/auth/invite/complete.ts"),
|
|
795
806
|
});
|
|
796
807
|
|
|
808
|
+
injectRoute({
|
|
809
|
+
pattern: "/_emdash/api/auth/invite/register-options",
|
|
810
|
+
entrypoint: resolveRoute("api/auth/invite/register-options.ts"),
|
|
811
|
+
});
|
|
812
|
+
|
|
797
813
|
// Magic link routes
|
|
798
814
|
injectRoute({
|
|
799
815
|
pattern: "/_emdash/api/auth/magic-link/send",
|
|
@@ -256,6 +256,19 @@ export interface EmDashConfig {
|
|
|
256
256
|
*/
|
|
257
257
|
marketplace?: string;
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Maximum allowed media file upload size in bytes.
|
|
261
|
+
*
|
|
262
|
+
* Applies to both direct multipart uploads and signed-URL uploads.
|
|
263
|
+
* When unset, defaults to 52_428_800 (50 MB).
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* emdash({ maxUploadSize: 100 * 1024 * 1024 }) // 100 MB
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
maxUploadSize?: number;
|
|
271
|
+
|
|
259
272
|
/**
|
|
260
273
|
* Public browser-facing origin for the site.
|
|
261
274
|
*
|
|
@@ -322,6 +335,56 @@ export interface EmDashConfig {
|
|
|
322
335
|
* ```
|
|
323
336
|
*/
|
|
324
337
|
mediaProviders?: MediaProviderDescriptor[];
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Admin UI font configuration.
|
|
341
|
+
*
|
|
342
|
+
* By default, EmDash loads Noto Sans via the Astro Font API, covering
|
|
343
|
+
* Latin, Latin Extended, Cyrillic, Cyrillic Extended, Greek, Greek
|
|
344
|
+
* Extended, Devanagari, and Vietnamese. Fonts are downloaded from
|
|
345
|
+
* Google at build time and self-hosted, so there are no runtime CDN
|
|
346
|
+
* requests.
|
|
347
|
+
*
|
|
348
|
+
* To add support for additional writing systems (Arabic, CJK, etc.),
|
|
349
|
+
* pass script names. EmDash resolves the matching Noto Sans variant
|
|
350
|
+
* from Google Fonts and merges all script faces under a single
|
|
351
|
+
* font-family, so the browser downloads only the glyphs it needs
|
|
352
|
+
* via unicode-range.
|
|
353
|
+
*
|
|
354
|
+
* Set to `false` to disable font injection entirely and use system fonts.
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* ```ts
|
|
358
|
+
* // Add Arabic and Japanese support
|
|
359
|
+
* emdash({
|
|
360
|
+
* fonts: {
|
|
361
|
+
* scripts: ["arabic", "japanese"],
|
|
362
|
+
* },
|
|
363
|
+
* })
|
|
364
|
+
* ```
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* // Disable web fonts entirely (use system fonts)
|
|
369
|
+
* emdash({
|
|
370
|
+
* fonts: false,
|
|
371
|
+
* })
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
fonts?:
|
|
375
|
+
| false
|
|
376
|
+
| {
|
|
377
|
+
/**
|
|
378
|
+
* Additional Noto Sans script families to include.
|
|
379
|
+
*
|
|
380
|
+
* Available scripts: arabic, armenian, bengali, chinese-simplified,
|
|
381
|
+
* chinese-traditional, chinese-hongkong, devanagari, ethiopic,
|
|
382
|
+
* georgian, gujarati, gurmukhi, hebrew, japanese, kannada, khmer,
|
|
383
|
+
* korean, lao, malayalam, myanmar, oriya, sinhala, tamil, telugu,
|
|
384
|
+
* thai, tibetan.
|
|
385
|
+
*/
|
|
386
|
+
scripts?: string[];
|
|
387
|
+
};
|
|
325
388
|
}
|
|
326
389
|
|
|
327
390
|
/**
|
|
@@ -56,6 +56,9 @@ export const RESOLVED_VIRTUAL_BLOCK_COMPONENTS_ID = "\0" + VIRTUAL_BLOCK_COMPONE
|
|
|
56
56
|
export const VIRTUAL_SEED_ID = "virtual:emdash/seed";
|
|
57
57
|
export const RESOLVED_VIRTUAL_SEED_ID = "\0" + VIRTUAL_SEED_ID;
|
|
58
58
|
|
|
59
|
+
export const VIRTUAL_WAIT_UNTIL_ID = "virtual:emdash/wait-until";
|
|
60
|
+
export const RESOLVED_VIRTUAL_WAIT_UNTIL_ID = "\0" + VIRTUAL_WAIT_UNTIL_ID;
|
|
61
|
+
|
|
59
62
|
/**
|
|
60
63
|
* Generates the config virtual module.
|
|
61
64
|
*/
|
|
@@ -65,62 +68,42 @@ export function generateConfigModule(serializableConfig: Record<string, unknown>
|
|
|
65
68
|
|
|
66
69
|
/**
|
|
67
70
|
* Generates the dialect virtual module.
|
|
68
|
-
* Statically imports the configured database dialect and exports the dialect type.
|
|
69
|
-
*
|
|
70
|
-
* For D1 adapters, also re-exports session helpers (isSessionEnabled, getD1Binding,
|
|
71
|
-
* getDefaultConstraint, getBookmarkCookieName, createSessionDialect) used by
|
|
72
|
-
* middleware for per-request read replica sessions.
|
|
73
71
|
*
|
|
74
|
-
*
|
|
72
|
+
* Adapters that set `supportsRequestScope: true` on their descriptor are
|
|
73
|
+
* expected to export `createRequestScopedDb` from their runtime entrypoint;
|
|
74
|
+
* the generator re-exports it so middleware can ask for a per-request Kysely
|
|
75
|
+
* (used for D1 Sessions API, bookmark cookies, read-replica routing). Other
|
|
76
|
+
* adapters get a stub that returns null.
|
|
75
77
|
*/
|
|
76
|
-
export function generateDialectModule(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
): string {
|
|
81
|
-
|
|
78
|
+
export function generateDialectModule(opts: {
|
|
79
|
+
entrypoint?: string;
|
|
80
|
+
type?: string;
|
|
81
|
+
supportsRequestScope: boolean;
|
|
82
|
+
}): string {
|
|
83
|
+
const { entrypoint, supportsRequestScope } = opts;
|
|
84
|
+
if (!entrypoint) {
|
|
82
85
|
return [
|
|
83
86
|
`export const createDialect = undefined;`,
|
|
84
87
|
`export const dialectType = "sqlite";`,
|
|
85
|
-
`export const
|
|
86
|
-
`export const getD1Binding = () => null;`,
|
|
87
|
-
`export const getDefaultConstraint = () => "first-unconstrained";`,
|
|
88
|
-
`export const getBookmarkCookieName = () => "";`,
|
|
89
|
-
`export const createSessionDialect = undefined;`,
|
|
88
|
+
`export const createRequestScopedDb = (_opts) => null;`,
|
|
90
89
|
].join("\n");
|
|
91
90
|
}
|
|
92
|
-
const type =
|
|
91
|
+
const type = opts.type ?? "sqlite";
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
const isD1 = dbEntrypoint.includes("cloudflare") && dbEntrypoint.includes("d1");
|
|
96
|
-
|
|
97
|
-
// Check if sessions are enabled in the config
|
|
98
|
-
const sessionMode =
|
|
99
|
-
isD1 && dbConfig && typeof dbConfig === "object" && "session" in dbConfig
|
|
100
|
-
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- runtime-checked above
|
|
101
|
-
(dbConfig as { session?: string }).session
|
|
102
|
-
: undefined;
|
|
103
|
-
const sessionEnabled = !!sessionMode && sessionMode !== "disabled";
|
|
104
|
-
|
|
105
|
-
if (isD1 && sessionEnabled) {
|
|
93
|
+
if (supportsRequestScope) {
|
|
106
94
|
return `
|
|
107
|
-
import { createDialect as _createDialect } from "${
|
|
108
|
-
export {
|
|
95
|
+
import { createDialect as _createDialect } from "${entrypoint}";
|
|
96
|
+
export { createRequestScopedDb } from "${entrypoint}";
|
|
109
97
|
export const createDialect = _createDialect;
|
|
110
98
|
export const dialectType = ${JSON.stringify(type)};
|
|
111
99
|
`;
|
|
112
100
|
}
|
|
113
101
|
|
|
114
|
-
// Non-D1 or sessions disabled: export no-ops
|
|
115
102
|
return `
|
|
116
|
-
import { createDialect as _createDialect } from "${
|
|
103
|
+
import { createDialect as _createDialect } from "${entrypoint}";
|
|
117
104
|
export const createDialect = _createDialect;
|
|
118
105
|
export const dialectType = ${JSON.stringify(type)};
|
|
119
|
-
export const
|
|
120
|
-
export const getD1Binding = () => null;
|
|
121
|
-
export const getDefaultConstraint = () => "first-unconstrained";
|
|
122
|
-
export const getBookmarkCookieName = () => "";
|
|
123
|
-
export const createSessionDialect = undefined;
|
|
106
|
+
export const createRequestScopedDb = (_opts) => null;
|
|
124
107
|
`;
|
|
125
108
|
}
|
|
126
109
|
|
|
@@ -353,6 +336,25 @@ export function generateBlockComponentsModule(descriptors: PluginDescriptor[]):
|
|
|
353
336
|
return `${imports.join("\n")}\nexport const pluginBlockComponents = { ${spreads.join(", ")} };`;
|
|
354
337
|
}
|
|
355
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Generates the wait-until virtual module.
|
|
341
|
+
*
|
|
342
|
+
* Under @astrojs/cloudflare, re-exports `waitUntil` from `cloudflare:workers`
|
|
343
|
+
* so `after(fn)` in core can extend the worker's lifetime past the response
|
|
344
|
+
* for deferred bookkeeping. For any other adapter, exports `undefined` —
|
|
345
|
+
* Node's long-lived event loop keeps deferred promises running without a
|
|
346
|
+
* lifetime extender.
|
|
347
|
+
*
|
|
348
|
+
* Keeping the adapter check here — rather than in core — means core itself
|
|
349
|
+
* has no Cloudflare-specific imports or code paths.
|
|
350
|
+
*/
|
|
351
|
+
export function generateWaitUntilModule(adapterName: string | undefined): string {
|
|
352
|
+
if (adapterName === "@astrojs/cloudflare") {
|
|
353
|
+
return `export { waitUntil } from "cloudflare:workers";`;
|
|
354
|
+
}
|
|
355
|
+
return `export const waitUntil = undefined;`;
|
|
356
|
+
}
|
|
357
|
+
|
|
356
358
|
/**
|
|
357
359
|
* Generates the seed virtual module.
|
|
358
360
|
* Reads the user's seed file at build time (in Node context) and embeds it,
|
|
@@ -38,7 +38,10 @@ import {
|
|
|
38
38
|
RESOLVED_VIRTUAL_BLOCK_COMPONENTS_ID,
|
|
39
39
|
VIRTUAL_SEED_ID,
|
|
40
40
|
RESOLVED_VIRTUAL_SEED_ID,
|
|
41
|
+
VIRTUAL_WAIT_UNTIL_ID,
|
|
42
|
+
RESOLVED_VIRTUAL_WAIT_UNTIL_ID,
|
|
41
43
|
generateSeedModule,
|
|
44
|
+
generateWaitUntilModule,
|
|
42
45
|
generateConfigModule,
|
|
43
46
|
generateDialectModule,
|
|
44
47
|
generateStorageModule,
|
|
@@ -176,6 +179,9 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
176
179
|
if (id === VIRTUAL_SEED_ID) {
|
|
177
180
|
return RESOLVED_VIRTUAL_SEED_ID;
|
|
178
181
|
}
|
|
182
|
+
if (id === VIRTUAL_WAIT_UNTIL_ID) {
|
|
183
|
+
return RESOLVED_VIRTUAL_WAIT_UNTIL_ID;
|
|
184
|
+
}
|
|
179
185
|
},
|
|
180
186
|
load(id: string) {
|
|
181
187
|
if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
|
|
@@ -184,11 +190,11 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
184
190
|
// Generate a module that statically imports the configured dialect
|
|
185
191
|
// This allows Vite to properly resolve and bundle it
|
|
186
192
|
if (id === RESOLVED_VIRTUAL_DIALECT_ID) {
|
|
187
|
-
return generateDialectModule(
|
|
188
|
-
resolvedConfig.database?.entrypoint,
|
|
189
|
-
resolvedConfig.database?.type,
|
|
190
|
-
resolvedConfig.database?.
|
|
191
|
-
);
|
|
193
|
+
return generateDialectModule({
|
|
194
|
+
entrypoint: resolvedConfig.database?.entrypoint,
|
|
195
|
+
type: resolvedConfig.database?.type,
|
|
196
|
+
supportsRequestScope: resolvedConfig.database?.supportsRequestScope ?? false,
|
|
197
|
+
});
|
|
192
198
|
}
|
|
193
199
|
// Generate a module that statically imports the configured storage
|
|
194
200
|
if (id === RESOLVED_VIRTUAL_STORAGE_ID) {
|
|
@@ -235,6 +241,11 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
235
241
|
const projectRoot = fileURLToPath(astroConfig.root);
|
|
236
242
|
return generateSeedModule(projectRoot);
|
|
237
243
|
}
|
|
244
|
+
// Generate wait-until module — re-exports cloudflare:workers'
|
|
245
|
+
// waitUntil under the Cloudflare adapter, undefined otherwise.
|
|
246
|
+
if (id === RESOLVED_VIRTUAL_WAIT_UNTIL_ID) {
|
|
247
|
+
return generateWaitUntilModule(astroConfig.adapter?.name);
|
|
248
|
+
}
|
|
238
249
|
},
|
|
239
250
|
};
|
|
240
251
|
}
|
|
@@ -97,12 +97,12 @@ const PUBLIC_API_PREFIXES = [
|
|
|
97
97
|
"/_emdash/api/auth/dev-bypass",
|
|
98
98
|
"/_emdash/api/auth/signup/",
|
|
99
99
|
"/_emdash/api/auth/magic-link/",
|
|
100
|
-
"/_emdash/api/auth/invite/
|
|
101
|
-
"/_emdash/api/auth/invite/complete",
|
|
100
|
+
"/_emdash/api/auth/invite/",
|
|
102
101
|
"/_emdash/api/auth/oauth/",
|
|
103
102
|
"/_emdash/api/oauth/device/token",
|
|
104
103
|
"/_emdash/api/oauth/device/code",
|
|
105
104
|
"/_emdash/api/oauth/token",
|
|
105
|
+
"/_emdash/api/oauth/register",
|
|
106
106
|
"/_emdash/api/comments/",
|
|
107
107
|
"/_emdash/api/media/file/",
|
|
108
108
|
"/_emdash/.well-known/",
|
|
@@ -119,6 +119,30 @@ const PUBLIC_API_EXACT = new Set([
|
|
|
119
119
|
"/_emdash/api/search",
|
|
120
120
|
]);
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* OAuth protocol endpoints that are CSRF-exempt by design.
|
|
124
|
+
*
|
|
125
|
+
* These are RFC-defined endpoints (RFC 6749 §3.2, RFC 7591 §3, RFC 8628 §3.1/§3.4)
|
|
126
|
+
* specified to be called cross-origin by external clients (MCP clients, CLIs,
|
|
127
|
+
* native apps). They authenticate each request on its own merits:
|
|
128
|
+
*
|
|
129
|
+
* - /oauth/token: requires PKCE code_verifier, device_code, or refresh_token
|
|
130
|
+
* - /oauth/register: RFC 7591 dynamic client registration — anonymous by design
|
|
131
|
+
* - /oauth/device/code: RFC 8628 device flow initiation — anonymous by design
|
|
132
|
+
* - /oauth/device/token: requires device_code the client already holds
|
|
133
|
+
*
|
|
134
|
+
* None of these rely on ambient cookie credentials, so browser-based CSRF
|
|
135
|
+
* attacks have nothing to exploit. The endpoints themselves advertise
|
|
136
|
+
* `Access-Control-Allow-Origin: *`. Note: /oauth/device/authorize (the user
|
|
137
|
+
* consent step) is NOT in this list — it is session-authenticated.
|
|
138
|
+
*/
|
|
139
|
+
const CSRF_EXEMPT_PUBLIC_ROUTES = new Set([
|
|
140
|
+
"/_emdash/api/oauth/token",
|
|
141
|
+
"/_emdash/api/oauth/register",
|
|
142
|
+
"/_emdash/api/oauth/device/code",
|
|
143
|
+
"/_emdash/api/oauth/device/token",
|
|
144
|
+
]);
|
|
145
|
+
|
|
122
146
|
function isPublicEmDashRoute(pathname: string): boolean {
|
|
123
147
|
if (PUBLIC_API_EXACT.has(pathname)) return true;
|
|
124
148
|
if (PUBLIC_API_PREFIXES.some((p) => pathname.startsWith(p))) return true;
|
|
@@ -126,6 +150,10 @@ function isPublicEmDashRoute(pathname: string): boolean {
|
|
|
126
150
|
return false;
|
|
127
151
|
}
|
|
128
152
|
|
|
153
|
+
function isCsrfExemptPublicRoute(pathname: string): boolean {
|
|
154
|
+
return CSRF_EXEMPT_PUBLIC_ROUTES.has(pathname);
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
130
158
|
const { url } = context;
|
|
131
159
|
|
|
@@ -142,7 +170,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
142
170
|
// This prevents cross-origin form submissions and fetch requests from malicious sites.
|
|
143
171
|
if (isPublicApiRoute) {
|
|
144
172
|
const method = context.request.method.toUpperCase();
|
|
145
|
-
if (
|
|
173
|
+
if (
|
|
174
|
+
isUnsafeMethod(method) &&
|
|
175
|
+
!isCsrfExemptPublicRoute(url.pathname) // OAuth protocol endpoints — cross-origin by design
|
|
176
|
+
) {
|
|
146
177
|
const publicOrigin = getPublicOrigin(url, context.locals.emdash?.config);
|
|
147
178
|
const csrfError = checkPublicCsrf(context.request, url, publicOrigin);
|
|
148
179
|
if (csrfError) return csrfError;
|
|
@@ -277,7 +308,9 @@ async function handleEmDashAuth(
|
|
|
277
308
|
const { url, locals } = context;
|
|
278
309
|
const { emdash } = locals;
|
|
279
310
|
|
|
280
|
-
const
|
|
311
|
+
const isPublicAdminRoute =
|
|
312
|
+
url.pathname.startsWith("/_emdash/admin/login") ||
|
|
313
|
+
url.pathname.startsWith("/_emdash/admin/invite/accept");
|
|
281
314
|
const isApiRoute = url.pathname.startsWith("/_emdash/api");
|
|
282
315
|
|
|
283
316
|
if (!emdash?.db) {
|
|
@@ -291,7 +324,7 @@ async function handleEmDashAuth(
|
|
|
291
324
|
if (authMode.type === "external") {
|
|
292
325
|
// In dev mode, fall back to passkey auth since external JWT won't be present
|
|
293
326
|
if (import.meta.env.DEV) {
|
|
294
|
-
if (
|
|
327
|
+
if (isPublicAdminRoute) {
|
|
295
328
|
return next();
|
|
296
329
|
}
|
|
297
330
|
|
|
@@ -303,7 +336,7 @@ async function handleEmDashAuth(
|
|
|
303
336
|
}
|
|
304
337
|
|
|
305
338
|
// Passkey authentication (default)
|
|
306
|
-
if (
|
|
339
|
+
if (isPublicAdminRoute) {
|
|
307
340
|
return next();
|
|
308
341
|
}
|
|
309
342
|
|