emdash 0.5.0 → 0.7.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-5uslYdUu.mjs} +197 -25
- package/dist/apply-5uslYdUu.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 +203 -33
- 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 +30 -4
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +11 -4
- 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 +467 -186
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +17 -9
- package/dist/astro/types.d.mts.map +1 -1
- 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 +12 -11
- 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/{content-BsBoyj8G.mjs → content-D7J5y73J.mjs} +27 -1
- package/dist/{content-BsBoyj8G.mjs.map → content-D7J5y73J.mjs.map} +1 -1
- 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 +1 -1
- 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-CCWzlriB.d.mts → index-De6_Xv3v.d.mts} +209 -19
- package/dist/index-De6_Xv3v.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- 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-g4Ug-9j9.mjs} +79 -12
- package/dist/query-g4Ug-9j9.mjs.map +1 -0
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-CN0Rt9Ob.mjs} +66 -10
- package/dist/redirect-CN0Rt9Ob.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-BR2xKwhn.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-BR2xKwhn.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-Cn1SYvYF.mjs → search-B0effn3j.mjs} +210 -226
- package/dist/search-B0effn3j.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -9
- 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-K2z0Uhnj.mjs +308 -0
- package/dist/taxonomies-K2z0Uhnj.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-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-C2v0c34j.d.mts} +10 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-C2v0c34j.d.mts.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-DeG21anB.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-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
- package/dist/types-DgrIP0tF.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/{validate-Db1yNL3i.d.mts → validate-kM8Pjuf7.d.mts} +5 -52
- package/dist/validate-kM8Pjuf7.d.mts.map +1 -0
- package/dist/version-BnTKdfam.mjs +7 -0
- package/dist/{version-CMMjTuqu.mjs.map → version-BnTKdfam.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/content.ts +2 -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/content.ts +8 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +178 -0
- package/src/astro/integration/index.ts +44 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/runtime.ts +117 -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 +33 -1
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +21 -10
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +19 -1
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +5 -4
- package/src/astro/routes/api/manifest.ts +7 -0
- 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/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- 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/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +32 -8
- package/src/astro/routes/api/setup/index.ts +5 -2
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
- package/src/astro/types.ts +9 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +39 -0
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/redirect.ts +111 -8
- package/src/database/types.ts +9 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +312 -92
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/mcp/server.ts +76 -3
- 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 +15 -3
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- 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 +41 -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/visual-editing/toolbar.ts +6 -1
- 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-CCWzlriB.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-Cn1SYvYF.mjs.map +0 -1
- package/dist/types-C3ronwXb.d.mts.map +0 -1
- package/dist/types-DeG21anB.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-Db1yNL3i.d.mts.map +0 -1
- package/dist/version-CMMjTuqu.mjs +0 -7
|
@@ -9,6 +9,7 @@ import "@emdash-cms/admin/styles.css";
|
|
|
9
9
|
// Use package-qualified import so Astro generates a proper module URL
|
|
10
10
|
// (relative imports resolve to absolute paths which break client hydration)
|
|
11
11
|
import AdminWrapper from "emdash/routes/PluginRegistry";
|
|
12
|
+
import { Font } from "astro:assets";
|
|
12
13
|
|
|
13
14
|
export const prerender = false;
|
|
14
15
|
|
|
@@ -17,6 +18,9 @@ import { resolveLocale, loadMessages, getLocaleDir } from "@emdash-cms/admin/loc
|
|
|
17
18
|
const resolvedLocale = resolveLocale(Astro.request);
|
|
18
19
|
const resolvedDir = getLocaleDir(resolvedLocale);
|
|
19
20
|
const messages = await loadMessages(resolvedLocale);
|
|
21
|
+
|
|
22
|
+
const adminConfig = Astro.locals.emdash?.config?.admin;
|
|
23
|
+
const pageTitle = adminConfig?.siteName ? `${adminConfig.siteName} Admin` : "EmDash Admin";
|
|
20
24
|
---
|
|
21
25
|
|
|
22
26
|
<!doctype html>
|
|
@@ -24,13 +28,18 @@ const messages = await loadMessages(resolvedLocale);
|
|
|
24
28
|
<head>
|
|
25
29
|
<meta charset="UTF-8" />
|
|
26
30
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
<Font cssVariable="--font-emdash" />
|
|
32
|
+
{adminConfig?.favicon ? (
|
|
33
|
+
<link rel="icon" href={adminConfig.favicon} />
|
|
34
|
+
) : (
|
|
35
|
+
<link
|
|
36
|
+
rel="icon"
|
|
37
|
+
href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'> <g clip-path='url(%23clip0_50_99)'> <rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23paint0_linear_50_99)' stroke-width='6'/> <rect x='18' y='34' width='39.3661' height='6.56101' fill='url(%23paint1_linear_50_99)'/> </g> <defs> <linearGradient id='paint0_linear_50_99' x1='-42.9996' y1='124' x2='92.4233' y2='-41.7456' gradientUnits='userSpaceOnUse'> <stop stop-color='%230F006B'/> <stop offset='0.0833333' stop-color='%23281A81'/> <stop offset='0.166667' stop-color='%235D0C83'/> <stop offset='0.25' stop-color='%23911475'/> <stop offset='0.333333' stop-color='%23CE2F55'/> <stop offset='0.416667' stop-color='%23FF6633'/> <stop offset='0.5' stop-color='%23F6821F'/> <stop offset='0.583333' stop-color='%23FBAD41'/> <stop offset='0.666667' stop-color='%23FFCD89'/> <stop offset='0.75' stop-color='%23FFE9CB'/> <stop offset='0.833333' stop-color='%23FFF7EC'/> <stop offset='0.916667' stop-color='%23FFF8EE'/> <stop offset='1' stop-color='white'/> </linearGradient> <linearGradient id='paint1_linear_50_99' x1='91.4992' y1='27.4982' x2='28.1217' y2='54.1775' gradientUnits='userSpaceOnUse'> <stop stop-color='white'/> <stop offset='0.129253' stop-color='%23FFF8EE'/> <stop offset='0.617058' stop-color='%23FBAD41'/> <stop offset='0.848019' stop-color='%23F6821F'/> <stop offset='1' stop-color='%23FF6633'/> </linearGradient> <clipPath id='clip0_50_99'> <rect width='75' height='75' fill='white'/> </clipPath> </defs> </svg>"
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
<title>{pageTitle}</title>
|
|
32
41
|
</head>
|
|
33
|
-
<body>
|
|
42
|
+
<body class="isolate">
|
|
34
43
|
<div id="admin-root" class="min-h-screen">
|
|
35
44
|
<div id="emdash-boot-loader">
|
|
36
45
|
<style>
|
|
@@ -63,10 +72,12 @@ const messages = await loadMessages(resolvedLocale);
|
|
|
63
72
|
}
|
|
64
73
|
#emdash-boot-loader p {
|
|
65
74
|
margin-top: 1rem;
|
|
66
|
-
font-family:
|
|
75
|
+
font-family: var(
|
|
76
|
+
--font-emdash,
|
|
77
|
+
ui-sans-serif,
|
|
67
78
|
system-ui,
|
|
68
|
-
-
|
|
69
|
-
|
|
79
|
+
sans-serif
|
|
80
|
+
);
|
|
70
81
|
font-size: 0.875rem;
|
|
71
82
|
color: light-dark(hsl(215.4 16.3% 46.9%), hsl(215 20.2% 65.1%));
|
|
72
83
|
}
|
|
@@ -78,7 +89,7 @@ const messages = await loadMessages(resolvedLocale);
|
|
|
78
89
|
</style>
|
|
79
90
|
<div class="loader-inner">
|
|
80
91
|
<div class="spinner"></div>
|
|
81
|
-
<p>Loading EmDash
|
|
92
|
+
<p>{adminConfig?.siteName ? `Loading ${adminConfig.siteName}...` : "Loading EmDash..."}</p>
|
|
82
93
|
</div>
|
|
83
94
|
</div>
|
|
84
95
|
<AdminWrapper client:only="react" locale={resolvedLocale} messages={messages} />
|
|
@@ -19,6 +19,7 @@ import { isParseError, parseBody } from "#api/parse.js";
|
|
|
19
19
|
import { magicLinkSendBody } from "#api/schemas.js";
|
|
20
20
|
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
21
21
|
import { checkRateLimit, getClientIp } from "#auth/rate-limit.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
22
23
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
23
24
|
|
|
24
25
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
@@ -36,7 +37,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
36
37
|
if (isParseError(body)) return body;
|
|
37
38
|
|
|
38
39
|
// Rate limit: 3 requests per 300 seconds (5 minutes) per IP
|
|
39
|
-
const ip = getClientIp(request);
|
|
40
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));
|
|
40
41
|
const rateLimit = await checkRateLimit(emdash.db, ip, "magic-link/send", 3, 300);
|
|
41
42
|
if (!rateLimit.allowed) {
|
|
42
43
|
// Return success-shaped response to avoid revealing rate limit
|
|
@@ -20,6 +20,7 @@ import { passkeyOptionsBody } from "#api/schemas.js";
|
|
|
20
20
|
import { createChallengeStore, cleanupExpiredChallenges } from "#auth/challenge-store.js";
|
|
21
21
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
22
22
|
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
23
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
23
24
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
24
25
|
|
|
25
26
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
@@ -38,7 +39,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
38
39
|
if (isParseError(body)) return body;
|
|
39
40
|
|
|
40
41
|
// Rate limit: 10 requests per 60 seconds per IP
|
|
41
|
-
const ip = getClientIp(request);
|
|
42
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));
|
|
42
43
|
const rateLimit = await checkRateLimit(emdash.db, ip, "passkey/options", 10, 60);
|
|
43
44
|
if (!rateLimit.allowed) {
|
|
44
45
|
return rateLimitResponse(60);
|
|
@@ -9,7 +9,7 @@ import type { APIRoute } from "astro";
|
|
|
9
9
|
export const prerender = false;
|
|
10
10
|
|
|
11
11
|
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
12
|
-
import { authenticateWithPasskey } from "@emdash-cms/auth/passkey";
|
|
12
|
+
import { authenticateWithPasskey, PasskeyAuthenticationError } from "@emdash-cms/auth/passkey";
|
|
13
13
|
|
|
14
14
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
@@ -63,6 +63,10 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
63
63
|
},
|
|
64
64
|
});
|
|
65
65
|
} catch (error) {
|
|
66
|
+
if (error instanceof PasskeyAuthenticationError) {
|
|
67
|
+
return apiError("UNAUTHORIZED", "Authentication failed", 401);
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
return handleError(error, "Authentication failed", "PASSKEY_VERIFY_ERROR");
|
|
67
71
|
}
|
|
68
72
|
};
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Request self-signup. Sends verification email if domain is allowed.
|
|
5
5
|
* Always returns 200 to prevent email enumeration.
|
|
6
|
+
*
|
|
7
|
+
* Rate limited: 3 requests per 5 minutes per IP. Mirrors magic-link/send.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { APIRoute } from "astro";
|
|
@@ -16,8 +18,18 @@ import { apiError, apiSuccess } from "#api/error.js";
|
|
|
16
18
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
17
19
|
import { signupRequestBody } from "#api/schemas.js";
|
|
18
20
|
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
21
|
+
import { checkRateLimit, getClientIp } from "#auth/rate-limit.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
19
23
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
20
24
|
|
|
25
|
+
// Generic response body used for both the real success path and the
|
|
26
|
+
// rate-limited / domain-disallowed paths. Keeping them identical prevents
|
|
27
|
+
// the caller from distinguishing between them.
|
|
28
|
+
const GENERIC_SUCCESS = {
|
|
29
|
+
success: true,
|
|
30
|
+
message: "If your email domain is allowed, you'll receive a verification email.",
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
22
34
|
const { emdash } = locals;
|
|
23
35
|
|
|
@@ -35,9 +47,21 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
try {
|
|
50
|
+
// Parse the body first — this avoids burning a rate-limit slot on
|
|
51
|
+
// malformed input and keeps the timing of the rate-limited and
|
|
52
|
+
// real paths aligned.
|
|
38
53
|
const body = await parseBody(request, signupRequestBody);
|
|
39
54
|
if (isParseError(body)) return body;
|
|
40
55
|
|
|
56
|
+
// Rate limit: 3 requests per 300 seconds per IP. Matches magic-link/send.
|
|
57
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));
|
|
58
|
+
const rateLimit = await checkRateLimit(emdash.db, ip, "signup/request", 3, 300);
|
|
59
|
+
if (!rateLimit.allowed) {
|
|
60
|
+
// Return success-shaped response to avoid revealing rate limiting
|
|
61
|
+
// (and by extension, the fact that the caller is probing).
|
|
62
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
const adapter = createKyselyAdapter(emdash.db);
|
|
42
66
|
|
|
43
67
|
// Get site config for signup email
|
|
@@ -60,18 +84,12 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
60
84
|
);
|
|
61
85
|
|
|
62
86
|
// Always return success to prevent email enumeration
|
|
63
|
-
return apiSuccess(
|
|
64
|
-
success: true,
|
|
65
|
-
message: "If your email domain is allowed, you'll receive a verification email.",
|
|
66
|
-
});
|
|
87
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
67
88
|
} catch (error) {
|
|
68
89
|
console.error("Signup request error:", error);
|
|
69
90
|
|
|
70
91
|
// Don't reveal internal errors - just return generic success
|
|
71
92
|
// to prevent information leakage
|
|
72
|
-
return apiSuccess(
|
|
73
|
-
success: true,
|
|
74
|
-
message: "If your email domain is allowed, you'll receive a verification email.",
|
|
75
|
-
});
|
|
93
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
76
94
|
}
|
|
77
95
|
};
|
|
@@ -139,18 +139,22 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// Anti-spam: Rate limiting
|
|
142
|
-
const meta = extractRequestMeta(request);
|
|
142
|
+
const meta = extractRequestMeta(request, emdash.config);
|
|
143
143
|
const ipSalt =
|
|
144
144
|
import.meta.env.EMDASH_AUTH_SECRET || import.meta.env.AUTH_SECRET || "emdash-ip-salt";
|
|
145
145
|
let ipHash: string;
|
|
146
146
|
if (meta.ip) {
|
|
147
147
|
ipHash = await hashIp(meta.ip, ipSalt);
|
|
148
|
-
} else if (meta.userAgent) {
|
|
149
|
-
// Fallback: hash user-agent as a rough identifier when IP is unavailable
|
|
150
|
-
ipHash = await hashIp(`ua:${meta.userAgent}`, ipSalt);
|
|
151
148
|
} else {
|
|
152
|
-
//
|
|
153
|
-
//
|
|
149
|
+
// No trusted IP — fail closed by bucketing all unidentifiable
|
|
150
|
+
// requests together. A larger limit reflects the shared bucket.
|
|
151
|
+
//
|
|
152
|
+
// Self-hosted operators behind a reverse proxy should set
|
|
153
|
+
// `trustedProxyHeaders` in the EmDash config (or the
|
|
154
|
+
// EMDASH_TRUSTED_PROXY_HEADERS env var) so this path isn't hit
|
|
155
|
+
// for legitimate traffic. UA-hashing was previously used here
|
|
156
|
+
// but was trivially rotatable — the shared bucket is stricter
|
|
157
|
+
// and forces operators toward a real fix.
|
|
154
158
|
ipHash = "unknown";
|
|
155
159
|
}
|
|
156
160
|
const unknownBucketLimit = ipHash === "unknown" ? 20 : undefined;
|
|
@@ -13,7 +13,7 @@ export const prerender = false;
|
|
|
13
13
|
|
|
14
14
|
export const GET: APIRoute = async ({ params, locals }) => {
|
|
15
15
|
const { emdash, user } = locals;
|
|
16
|
-
const denied = requirePerm(user, "content:
|
|
16
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
17
17
|
if (denied) return denied;
|
|
18
18
|
const collection = params.collection!;
|
|
19
19
|
const id = params.id!;
|
|
@@ -30,7 +30,7 @@ const DURATION_PATTERN = /^(\d+)([smhdw])$/;
|
|
|
30
30
|
|
|
31
31
|
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
32
32
|
const { emdash, user } = locals;
|
|
33
|
-
const denied = requirePerm(user, "content:
|
|
33
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
34
34
|
if (denied) return denied;
|
|
35
35
|
const collection = params.collection!;
|
|
36
36
|
const id = params.id!;
|
|
@@ -13,7 +13,7 @@ export const prerender = false;
|
|
|
13
13
|
|
|
14
14
|
export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
15
15
|
const { emdash, user } = locals;
|
|
16
|
-
const denied = requirePerm(user, "content:
|
|
16
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
17
17
|
if (denied) return denied;
|
|
18
18
|
const collection = params.collection!;
|
|
19
19
|
const id = params.id!;
|
|
@@ -12,6 +12,7 @@ import { apiError, apiSuccess, handleError, requireDb } from "#api/error.js";
|
|
|
12
12
|
import { parseBody, isParseError } from "#api/parse.js";
|
|
13
13
|
import { contentTermsBody } from "#api/schemas.js";
|
|
14
14
|
import { TaxonomyRepository } from "#db/repositories/taxonomy.js";
|
|
15
|
+
import { invalidateTermCache } from "#taxonomies/index.js";
|
|
15
16
|
|
|
16
17
|
export const prerender = false;
|
|
17
18
|
|
|
@@ -122,6 +123,10 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
122
123
|
// Set the terms (replaces existing)
|
|
123
124
|
await repo.setTermsForEntry(collection, id, taxonomy, termIds);
|
|
124
125
|
|
|
126
|
+
// Term assignments changed — invalidate the hasAnyTermAssignments cache
|
|
127
|
+
// so hydration on subsequent reads issues a fresh query.
|
|
128
|
+
invalidateTermCache();
|
|
129
|
+
|
|
125
130
|
// Get the updated terms
|
|
126
131
|
const terms = await repo.getTermsForEntry(collection, id, taxonomy);
|
|
127
132
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Returns all locale variants linked to the same translation group.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { hasPermission, type Permission } from "@emdash-cms/auth";
|
|
9
10
|
import type { APIRoute } from "astro";
|
|
10
11
|
|
|
11
12
|
import { requirePerm } from "#api/authorize.js";
|
|
@@ -13,6 +14,15 @@ import { apiError, unwrapResult } from "#api/error.js";
|
|
|
13
14
|
|
|
14
15
|
export const prerender = false;
|
|
15
16
|
|
|
17
|
+
function isPublished(t: unknown): boolean {
|
|
18
|
+
return (
|
|
19
|
+
typeof t === "object" &&
|
|
20
|
+
t !== null &&
|
|
21
|
+
"status" in t &&
|
|
22
|
+
(t as Record<string, unknown>).status === "published"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export const GET: APIRoute = async ({ params, locals }) => {
|
|
17
27
|
const { emdash, user } = locals;
|
|
18
28
|
const denied = requirePerm(user, "content:read");
|
|
@@ -26,5 +36,21 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
26
36
|
|
|
27
37
|
const result = await emdash.handleContentTranslations(collection, id);
|
|
28
38
|
|
|
39
|
+
// Filter out non-published translations for users without read_drafts so a
|
|
40
|
+
// subscriber can't enumerate locales that aren't yet live.
|
|
41
|
+
if (result.success && !hasPermission(user, "content:read_drafts")) {
|
|
42
|
+
const data =
|
|
43
|
+
result.data && typeof result.data === "object"
|
|
44
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check
|
|
45
|
+
(result.data as Record<string, unknown>)
|
|
46
|
+
: undefined;
|
|
47
|
+
const translations = Array.isArray(data?.translations) ? data.translations : [];
|
|
48
|
+
const filtered = translations.filter(isPublished);
|
|
49
|
+
return unwrapResult({
|
|
50
|
+
success: true,
|
|
51
|
+
data: { ...data, translations: filtered },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
29
55
|
return unwrapResult(result);
|
|
30
56
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* DELETE /_emdash/api/content/{collection}/{id} - Delete content
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { hasPermission
|
|
9
|
+
import { hasPermission } from "@emdash-cms/auth";
|
|
10
10
|
import type { APIRoute } from "astro";
|
|
11
11
|
|
|
12
12
|
import { requirePerm, requireOwnerPerm } from "#api/authorize.js";
|
|
@@ -30,6 +30,25 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
|
30
30
|
|
|
31
31
|
const result = await emdash.handleContentGet(collection, id, locale);
|
|
32
32
|
|
|
33
|
+
// Hide non-published items from users without content:read_drafts. Return
|
|
34
|
+
// 404 (not 403) so subscribers can't enumerate draft IDs by status code.
|
|
35
|
+
if (result.success && !hasPermission(user, "content:read_drafts")) {
|
|
36
|
+
const data =
|
|
37
|
+
result.data && typeof result.data === "object"
|
|
38
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check
|
|
39
|
+
(result.data as Record<string, unknown>)
|
|
40
|
+
: undefined;
|
|
41
|
+
const item =
|
|
42
|
+
data?.item && typeof data.item === "object"
|
|
43
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check
|
|
44
|
+
(data.item as Record<string, unknown>)
|
|
45
|
+
: undefined;
|
|
46
|
+
const status = typeof item?.status === "string" ? item.status : null;
|
|
47
|
+
if (status !== "published") {
|
|
48
|
+
return apiError("NOT_FOUND", `Content item not found: ${id}`, 404);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
return unwrapResult(result);
|
|
34
53
|
};
|
|
35
54
|
|
|
@@ -69,12 +88,21 @@ export const PUT: APIRoute = async ({ params, request, locals, cache }) => {
|
|
|
69
88
|
const editDenied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
|
|
70
89
|
if (editDenied) return editDenied;
|
|
71
90
|
|
|
91
|
+
// Only EDITOR+ can write publishedAt directly — incl. clearing to null.
|
|
92
|
+
if (body.publishedAt !== undefined && !hasPermission(user, "content:publish_any")) {
|
|
93
|
+
return apiError(
|
|
94
|
+
"FORBIDDEN",
|
|
95
|
+
"Writing publishedAt requires content:publish_any permission",
|
|
96
|
+
403,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
// Use the resolved ID (handles slug → ID resolution)
|
|
73
101
|
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
74
102
|
|
|
75
103
|
// Only allow authorId changes if user has content:edit_any permission (editor+)
|
|
76
104
|
const canChangeAuthor =
|
|
77
|
-
body.authorId !== undefined && user && hasPermission(user, "content:edit_any"
|
|
105
|
+
body.authorId !== undefined && user && hasPermission(user, "content:edit_any");
|
|
78
106
|
const updateBody = canChangeAuthor ? body : { ...body, authorId: undefined };
|
|
79
107
|
|
|
80
108
|
// Pass _rev through for optimistic concurrency validation
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* POST /_emdash/api/content/{collection} - Create content
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { hasPermission } from "@emdash-cms/auth";
|
|
8
9
|
import type { APIRoute } from "astro";
|
|
9
10
|
|
|
10
11
|
import { requirePerm, requireOwnerPerm } from "#api/authorize.js";
|
|
@@ -26,7 +27,14 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
|
26
27
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
// Subscribers must only see published content; force the status filter
|
|
31
|
+
// regardless of caller-supplied value. Any user with content:read_drafts
|
|
32
|
+
// (CONTRIBUTOR+) keeps the requested filter.
|
|
33
|
+
const params_ = hasPermission(user, "content:read_drafts")
|
|
34
|
+
? query
|
|
35
|
+
: { ...query, status: "published" };
|
|
36
|
+
|
|
37
|
+
const result = await emdash.handleContentList(collection, params_);
|
|
30
38
|
|
|
31
39
|
return unwrapResult(result);
|
|
32
40
|
};
|
|
@@ -71,6 +79,16 @@ export const POST: APIRoute = async ({ params, request, locals, cache }) => {
|
|
|
71
79
|
if (translationDenied) return translationDenied;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
// Only EDITOR+ can write publishedAt / createdAt directly — incl. clearing to null.
|
|
83
|
+
const hasDateOverride = body.publishedAt !== undefined || body.createdAt !== undefined;
|
|
84
|
+
if (hasDateOverride && !hasPermission(user, "content:publish_any")) {
|
|
85
|
+
return apiError(
|
|
86
|
+
"FORBIDDEN",
|
|
87
|
+
"Writing publishedAt or createdAt requires content:publish_any permission",
|
|
88
|
+
403,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
74
92
|
// Auto-set authorId to current user when creating content
|
|
75
93
|
const result = await emdash.handleContentCreate(collection, {
|
|
76
94
|
...body,
|
|
@@ -17,7 +17,7 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
|
17
17
|
const { emdash, user } = locals;
|
|
18
18
|
const collection = params.collection!;
|
|
19
19
|
|
|
20
|
-
const denied = requirePerm(user, "content:
|
|
20
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
21
21
|
if (denied) return denied;
|
|
22
22
|
|
|
23
23
|
if (!emdash?.handleContentListTrashed) {
|
|
@@ -271,7 +271,7 @@ async function importContent(
|
|
|
271
271
|
console.error(`Import error for "${post.title || "Untitled"}":`, error);
|
|
272
272
|
result.errors.push({
|
|
273
273
|
title: post.title || "Untitled",
|
|
274
|
-
error: "Failed to import item",
|
|
274
|
+
error: error instanceof Error && error.message ? error.message : "Failed to import item",
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
277
|
}
|
|
@@ -15,7 +15,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
16
|
import { wpPluginAnalyzeBody } from "#api/schemas.js";
|
|
17
17
|
import { getSource } from "#import/index.js";
|
|
18
|
-
import {
|
|
18
|
+
import { resolveAndValidateExternalUrl, SsrfError } from "#import/ssrf.js";
|
|
19
19
|
import type { ImportAnalysis } from "#import/types.js";
|
|
20
20
|
import type { EmDashHandlers } from "#types";
|
|
21
21
|
|
|
@@ -37,9 +37,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
37
37
|
const body = await parseBody(request, wpPluginAnalyzeBody);
|
|
38
38
|
if (isParseError(body)) return body;
|
|
39
39
|
|
|
40
|
-
// SSRF: reject internal/private network targets
|
|
40
|
+
// SSRF: reject internal/private network targets. Uses DNS resolution
|
|
41
|
+
// to catch hostnames that resolve to private addresses.
|
|
41
42
|
try {
|
|
42
|
-
|
|
43
|
+
await resolveAndValidateExternalUrl(body.url);
|
|
43
44
|
} catch (e) {
|
|
44
45
|
const msg = e instanceof SsrfError ? e.message : "Invalid URL";
|
|
45
46
|
return apiError("SSRF_BLOCKED", msg, 400);
|
|
@@ -15,7 +15,7 @@ import { isParseError, parseBody } from "#api/parse.js";
|
|
|
15
15
|
import { wpPluginExecuteBody } from "#api/schemas.js";
|
|
16
16
|
import { BylineRepository } from "#db/repositories/byline.js";
|
|
17
17
|
import { getSource } from "#import/index.js";
|
|
18
|
-
import {
|
|
18
|
+
import { resolveAndValidateExternalUrl, SsrfError } from "#import/ssrf.js";
|
|
19
19
|
import type { ImportConfig, ImportResult, NormalizedItem } from "#import/types.js";
|
|
20
20
|
import { resolveImportByline } from "#import/utils.js";
|
|
21
21
|
import type { FieldType } from "#schema/types.js";
|
|
@@ -49,9 +49,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
49
49
|
const body = await parseBody(request, wpPluginExecuteBody);
|
|
50
50
|
if (isParseError(body)) return body;
|
|
51
51
|
|
|
52
|
-
// SSRF: reject internal/private network targets
|
|
52
|
+
// SSRF: reject internal/private network targets. Uses DNS resolution
|
|
53
|
+
// to catch hostnames that resolve to private addresses.
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
+
await resolveAndValidateExternalUrl(body.url);
|
|
55
56
|
} catch (e) {
|
|
56
57
|
const msg = e instanceof SsrfError ? e.message : "Invalid URL";
|
|
57
58
|
return apiError("SSRF_BLOCKED", msg, 400);
|
|
@@ -332,7 +333,7 @@ async function importContent(
|
|
|
332
333
|
console.error(`Import error for "${item.title || "Untitled"}":`, error);
|
|
333
334
|
result.errors.push({
|
|
334
335
|
title: item.title || "Untitled",
|
|
335
|
-
error: "Failed to import item",
|
|
336
|
+
error: error instanceof Error && error.message ? error.message : "Failed to import item",
|
|
336
337
|
});
|
|
337
338
|
}
|
|
338
339
|
}
|
|
@@ -12,6 +12,7 @@ import type { APIRoute } from "astro";
|
|
|
12
12
|
import { getAuthMode } from "#auth/mode.js";
|
|
13
13
|
|
|
14
14
|
import { COMMIT, VERSION } from "../../../version.js";
|
|
15
|
+
import { getStoredConfig } from "../../integration/runtime.js";
|
|
15
16
|
import type { EmDashManifest } from "../../types.js";
|
|
16
17
|
|
|
17
18
|
export const prerender = false;
|
|
@@ -22,6 +23,10 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
22
23
|
// Determine auth mode from config
|
|
23
24
|
const authMode = getAuthMode(emdash?.config);
|
|
24
25
|
|
|
26
|
+
// Read admin branding from build-time config
|
|
27
|
+
const storedConfig = getStoredConfig();
|
|
28
|
+
const adminBranding = storedConfig?.admin;
|
|
29
|
+
|
|
25
30
|
// Check if self-signup is enabled (any allowed domain with enabled = 1)
|
|
26
31
|
// Only relevant for passkey auth — external auth providers handle their own signup
|
|
27
32
|
let signupEnabled = false;
|
|
@@ -42,6 +47,7 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
42
47
|
...emdashManifest,
|
|
43
48
|
authMode: authMode.type === "external" ? authMode.providerType : "passkey",
|
|
44
49
|
signupEnabled,
|
|
50
|
+
admin: adminBranding,
|
|
45
51
|
}
|
|
46
52
|
: {
|
|
47
53
|
version: VERSION,
|
|
@@ -52,6 +58,7 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
52
58
|
taxonomies: [],
|
|
53
59
|
authMode: "passkey",
|
|
54
60
|
signupEnabled,
|
|
61
|
+
admin: adminBranding,
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
return Response.json(
|
|
@@ -16,7 +16,7 @@ import { ulid } from "ulidx";
|
|
|
16
16
|
import { requirePerm } from "#api/authorize.js";
|
|
17
17
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
18
18
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
|
-
import { mediaUploadUrlBody } from "#api/schemas.js";
|
|
19
|
+
import { DEFAULT_MAX_UPLOAD_SIZE, mediaUploadUrlBody } from "#api/schemas.js";
|
|
20
20
|
|
|
21
21
|
export const prerender = false;
|
|
22
22
|
|
|
@@ -59,7 +59,15 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
|
-
const
|
|
62
|
+
const maxSize = emdash.config.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
63
|
+
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
64
|
+
return apiError(
|
|
65
|
+
"CONFIGURATION_ERROR",
|
|
66
|
+
"Invalid maxUploadSize configuration. Expected a positive finite number.",
|
|
67
|
+
500,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const body = await parseBody(request, mediaUploadUrlBody(maxSize));
|
|
63
71
|
if (isParseError(body)) return body;
|
|
64
72
|
|
|
65
73
|
// Validate content type
|
|
@@ -13,7 +13,7 @@ import { ulid } from "ulidx";
|
|
|
13
13
|
import { requirePerm } from "#api/authorize.js";
|
|
14
14
|
import { apiError, apiSuccess, handleError, unwrapResult } from "#api/error.js";
|
|
15
15
|
import { isParseError, parseQuery } from "#api/parse.js";
|
|
16
|
-
import { mediaListQuery } from "#api/schemas.js";
|
|
16
|
+
import { DEFAULT_MAX_UPLOAD_SIZE, formatFileSize, mediaListQuery } from "#api/schemas.js";
|
|
17
17
|
import { MediaRepository } from "#db/repositories/media.js";
|
|
18
18
|
import { generatePlaceholder } from "#media/placeholder.js";
|
|
19
19
|
import { computeContentHash } from "#utils/hash.js";
|
|
@@ -22,9 +22,6 @@ import type { MediaItem } from "../../types.js";
|
|
|
22
22
|
|
|
23
23
|
export const prerender = false;
|
|
24
24
|
|
|
25
|
-
/** Maximum allowed file upload size (50 MB). */
|
|
26
|
-
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
|
|
27
|
-
|
|
28
25
|
/**
|
|
29
26
|
* Add URL to media items
|
|
30
27
|
* Uses relative URLs to ensure portability across deployments
|
|
@@ -89,9 +86,15 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
try {
|
|
89
|
+
const rawMax = emdash.config.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
90
|
+
if (!Number.isFinite(rawMax) || rawMax <= 0) {
|
|
91
|
+
return apiError("CONFIGURATION_ERROR", "Invalid maxUploadSize configuration", 500);
|
|
92
|
+
}
|
|
93
|
+
const maxUploadSize = rawMax;
|
|
94
|
+
|
|
92
95
|
// Best-effort size check before buffering the full multipart body
|
|
93
96
|
const contentLength = request.headers.get("Content-Length");
|
|
94
|
-
if (contentLength && parseInt(contentLength, 10) >
|
|
97
|
+
if (contentLength && parseInt(contentLength, 10) > maxUploadSize) {
|
|
95
98
|
return apiError("PAYLOAD_TOO_LARGE", "Upload too large", 413);
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -110,10 +113,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
// Check file size before buffering
|
|
113
|
-
if (file.size >
|
|
116
|
+
if (file.size > maxUploadSize) {
|
|
114
117
|
return apiError(
|
|
115
118
|
"PAYLOAD_TOO_LARGE",
|
|
116
|
-
`File exceeds maximum size of ${
|
|
119
|
+
`File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,
|
|
117
120
|
413,
|
|
118
121
|
);
|
|
119
122
|
}
|
|
@@ -15,6 +15,7 @@ import { handleDeviceCodeRequest } from "#api/handlers/device-flow.js";
|
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
16
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
17
|
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
18
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
18
19
|
|
|
19
20
|
export const prerender = false;
|
|
20
21
|
|
|
@@ -35,7 +36,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|
|
35
36
|
if (isParseError(body)) return body;
|
|
36
37
|
|
|
37
38
|
// Rate limit: 10 requests per 60 seconds per IP
|
|
38
|
-
const ip = getClientIp(request);
|
|
39
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));
|
|
39
40
|
const rateLimit = await checkRateLimit(emdash.db, ip, "device/code", 10, 60);
|
|
40
41
|
if (!rateLimit.allowed) {
|
|
41
42
|
return rateLimitResponse(60);
|
|
@@ -17,6 +17,7 @@ import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
|
17
17
|
import { handleDeviceTokenExchange } from "#api/handlers/device-flow.js";
|
|
18
18
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
19
|
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
20
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
20
21
|
|
|
21
22
|
export const prerender = false;
|
|
22
23
|
|
|
@@ -37,7 +38,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
37
38
|
if (isParseError(body)) return body;
|
|
38
39
|
|
|
39
40
|
// Rate limit: 12 requests per 60 seconds per IP
|
|
40
|
-
const ip = getClientIp(request);
|
|
41
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));
|
|
41
42
|
const rateLimit = await checkRateLimit(emdash.db, ip, "device/token", 12, 60);
|
|
42
43
|
if (!rateLimit.allowed) {
|
|
43
44
|
return rateLimitResponse(60);
|