emdash 0.7.0 → 1.0.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-Di31kZ28.d.mts → adapters-BKSf3T9R.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-BKSf3T9R.d.mts.map} +1 -1
- package/dist/{apply-5uslYdUu.mjs → apply-x0eMK1lX.mjs} +18 -17
- package/dist/apply-x0eMK1lX.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 +86 -15
- 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 +22 -2
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +259 -71
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +16 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
- package/dist/byline-Chbr2GoP.mjs.map +1 -0
- package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
- package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
- package/dist/cli/index.mjs +16 -12
- 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/{content-D7J5y73J.mjs → content-BcQPYxdV.mjs} +13 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- 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 → db-errors-l1Qh2RPR.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
- package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
- package/dist/error-zG5T1UGA.mjs.map +1 -0
- package/dist/{index-De6_Xv3v.d.mts → index-DIb-CzNx.d.mts} +157 -14
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +22 -20
- package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
- package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
- package/dist/loader-CndGj8kM.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
- package/dist/media-D8FbNsl0.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
- package/dist/mode-BnAOqItE.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.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-g4Ug-9j9.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-g4Ug-9j9.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-CN0Rt9Ob.mjs → redirect-D_pshWdf.mjs} +4 -4
- package/dist/redirect-D_pshWdf.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
- package/dist/registry-C3Mr0ODu.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
- package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
- package/dist/{runner-BR2xKwhn.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-BR2xKwhn.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-B0effn3j.mjs → search-BoZYFuUk.mjs} +227 -84
- package/dist/search-BoZYFuUk.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +12 -12
- 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.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-K2z0Uhnj.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
- package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
- package/dist/types-BIgulNsW.mjs +68 -0
- package/dist/types-BIgulNsW.mjs.map +1 -0
- package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
- package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
- package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
- package/dist/{types-C2v0c34j.d.mts → types-CS8FIX7L.d.mts} +1 -1
- package/dist/{types-C2v0c34j.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
- package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
- package/dist/types-i36XcA_X.d.mts.map +1 -0
- package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
- package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
- package/dist/{validate-kM8Pjuf7.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-kM8Pjuf7.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
- package/dist/validation-C-ZpN2GI.mjs +144 -0
- package/dist/validation-C-ZpN2GI.mjs.map +1 -0
- package/dist/version-DJrV1K0M.mjs +7 -0
- package/dist/{version-BnTKdfam.mjs.map → version-DJrV1K0M.mjs.map} +1 -1
- package/dist/zod-generator-CpwccCIv.mjs +132 -0
- package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
- package/package.json +19 -6
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +13 -0
- package/src/api/handlers/content.ts +122 -3
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +128 -16
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +6 -3
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/astro/integration/index.ts +13 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +19 -1
- package/src/astro/integration/virtual-modules.ts +41 -0
- package/src/astro/integration/vite-config.ts +43 -12
- package/src/astro/middleware/auth.ts +21 -0
- package/src/astro/middleware.ts +18 -1
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
- package/src/astro/routes/api/content/[collection]/index.ts +1 -9
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin.ts +8 -2
- package/src/astro/routes/api/setup/index.ts +2 -2
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +9 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/types.ts +114 -4
- package/src/cli/commands/bundle.ts +3 -1
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Gallery.astro +5 -3
- package/src/components/Image.astro +8 -3
- package/src/components/InlinePortableTextEditor.tsx +2 -1
- package/src/components/LiveSearch.astro +5 -14
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +40 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +12 -16
- package/src/database/repositories/taxonomy.ts +14 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/emdash-runtime.ts +306 -90
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +678 -105
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manifest-schema.ts +12 -0
- package/src/plugins/types.ts +23 -2
- package/src/query.ts +1 -1
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +41 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-5uslYdUu.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-D7J5y73J.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-De6_Xv3v.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/search-B0effn3j.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-BnTKdfam.mjs +0 -7
|
@@ -17,6 +17,8 @@ import { ulid } from "ulidx";
|
|
|
17
17
|
// Import auth provider via virtual module (statically bundled)
|
|
18
18
|
// This avoids dynamic import issues in Cloudflare Workers
|
|
19
19
|
import { authenticate as virtualAuthenticate } from "virtual:emdash/auth";
|
|
20
|
+
// @ts-ignore - virtual module
|
|
21
|
+
import virtualConfig from "virtual:emdash/config";
|
|
20
22
|
|
|
21
23
|
import { checkPublicCsrf } from "../../api/csrf.js";
|
|
22
24
|
import { apiError } from "../../api/error.js";
|
|
@@ -111,6 +113,7 @@ const PUBLIC_API_PREFIXES = [
|
|
|
111
113
|
const PUBLIC_API_EXACT = new Set([
|
|
112
114
|
"/_emdash/api/auth/passkey/options",
|
|
113
115
|
"/_emdash/api/auth/passkey/verify",
|
|
116
|
+
"/_emdash/api/auth/mode",
|
|
114
117
|
"/_emdash/api/oauth/token",
|
|
115
118
|
"/_emdash/api/snapshot",
|
|
116
119
|
// Public site search — read-only. The query layer hardcodes status='published'
|
|
@@ -119,6 +122,22 @@ const PUBLIC_API_EXACT = new Set([
|
|
|
119
122
|
"/_emdash/api/search",
|
|
120
123
|
]);
|
|
121
124
|
|
|
125
|
+
// Build merged public routes at module load from auth provider descriptors.
|
|
126
|
+
// Routes ending with "/" are treated as prefixes; all others are exact matches.
|
|
127
|
+
const { exact: _providerExactRoutes, prefixes: _providerPrefixRoutes } = (() => {
|
|
128
|
+
const exact = new Set<string>();
|
|
129
|
+
const prefixes: string[] = [];
|
|
130
|
+
if (!virtualConfig?.authProviders) return { exact, prefixes };
|
|
131
|
+
for (const route of virtualConfig.authProviders.flatMap((p) => p.publicRoutes ?? [])) {
|
|
132
|
+
if (route.endsWith("/")) {
|
|
133
|
+
prefixes.push(route);
|
|
134
|
+
} else {
|
|
135
|
+
exact.add(route);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { exact, prefixes };
|
|
139
|
+
})();
|
|
140
|
+
|
|
122
141
|
/**
|
|
123
142
|
* OAuth protocol endpoints that are CSRF-exempt by design.
|
|
124
143
|
*
|
|
@@ -146,6 +165,8 @@ const CSRF_EXEMPT_PUBLIC_ROUTES = new Set([
|
|
|
146
165
|
function isPublicEmDashRoute(pathname: string): boolean {
|
|
147
166
|
if (PUBLIC_API_EXACT.has(pathname)) return true;
|
|
148
167
|
if (PUBLIC_API_PREFIXES.some((p) => pathname.startsWith(p))) return true;
|
|
168
|
+
if (_providerExactRoutes.has(pathname)) return true;
|
|
169
|
+
if (_providerPrefixRoutes.some((p) => pathname.startsWith(p))) return true;
|
|
149
170
|
if (import.meta.env.DEV && pathname === "/_emdash/api/typegen") return true;
|
|
150
171
|
return false;
|
|
151
172
|
}
|
package/src/astro/middleware.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "../emdash-runtime.js";
|
|
44
44
|
import { setI18nConfig } from "../i18n/config.js";
|
|
45
45
|
import type { Database, Storage } from "../index.js";
|
|
46
|
+
import { createPublicMediaUrlResolver } from "../media/url.js";
|
|
46
47
|
import type { SandboxRunner } from "../plugins/sandbox/types.js";
|
|
47
48
|
import type { ResolvedPlugin } from "../plugins/types.js";
|
|
48
49
|
import { getRequestContext, runWithContext } from "../request-context.js";
|
|
@@ -232,6 +233,20 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
232
233
|
const { request, locals, cookies } = context;
|
|
233
234
|
const url = context.url;
|
|
234
235
|
|
|
236
|
+
// Fast path: routes outside /_emdash/ that plugins inject (e.g.,
|
|
237
|
+
// /.well-known/atproto-client-metadata.json) skip the entire runtime
|
|
238
|
+
// init + middleware chain. External servers fetch these with tight
|
|
239
|
+
// timeouts (~1-2s) so they must respond quickly even on cold starts.
|
|
240
|
+
if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
|
|
241
|
+
const isPluginFastRoute = virtualConfig.authProviders.some(
|
|
242
|
+
(p: { routes?: { pattern?: string }[] }) =>
|
|
243
|
+
p.routes?.some((r: { pattern?: string }) => r.pattern && url.pathname === r.pattern),
|
|
244
|
+
);
|
|
245
|
+
if (isPluginFastRoute) {
|
|
246
|
+
return finalizeResponse(await next());
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
const queryRecorder = isInstrumentationEnabled()
|
|
236
251
|
? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default")
|
|
237
252
|
: undefined;
|
|
@@ -301,10 +316,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
301
316
|
try {
|
|
302
317
|
const runtime = await getRuntime(config, initSubTimings);
|
|
303
318
|
setupVerified = true;
|
|
304
|
-
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for
|
|
319
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for the page-contribution methods
|
|
305
320
|
locals.emdash = {
|
|
306
321
|
collectPageMetadata: runtime.collectPageMetadata.bind(runtime),
|
|
307
322
|
collectPageFragments: runtime.collectPageFragments.bind(runtime),
|
|
323
|
+
getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
|
|
308
324
|
} as EmDashHandlers;
|
|
309
325
|
} catch {
|
|
310
326
|
// Non-fatal — EmDashHead will fall back to base SEO contributions
|
|
@@ -445,6 +461,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
445
461
|
// Direct access (for advanced use cases)
|
|
446
462
|
storage: runtime.storage,
|
|
447
463
|
db: runtime.db,
|
|
464
|
+
getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
|
|
448
465
|
hooks: runtime.hooks,
|
|
449
466
|
email: runtime.email,
|
|
450
467
|
configuredPlugins: runtime.configuredPlugins,
|
|
@@ -10,6 +10,8 @@ import { AdminApp } from "@emdash-cms/admin";
|
|
|
10
10
|
import type { Messages } from "@lingui/core";
|
|
11
11
|
// @ts-ignore - virtual module generated by integration
|
|
12
12
|
import { pluginAdmins } from "virtual:emdash/admin-registry";
|
|
13
|
+
// @ts-ignore - virtual module generated by integration
|
|
14
|
+
import { authProviders } from "virtual:emdash/auth-providers";
|
|
13
15
|
|
|
14
16
|
interface AdminWrapperProps {
|
|
15
17
|
locale: string;
|
|
@@ -17,5 +19,12 @@ interface AdminWrapperProps {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export default function AdminWrapper({ locale, messages }: AdminWrapperProps) {
|
|
20
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
<AdminApp
|
|
24
|
+
pluginAdmins={pluginAdmins}
|
|
25
|
+
authProviders={authProviders}
|
|
26
|
+
locale={locale}
|
|
27
|
+
messages={messages}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
21
30
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_emdash/api/auth/mode
|
|
3
|
+
*
|
|
4
|
+
* Public endpoint that returns the active authentication mode.
|
|
5
|
+
* Used by the login page to determine which login UI to render.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the full manifest endpoint, this is intentionally public
|
|
8
|
+
* and returns only the auth mode — no collection schemas, plugin
|
|
9
|
+
* info, or other internal details.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { APIRoute } from "astro";
|
|
13
|
+
|
|
14
|
+
import { getAuthMode } from "#auth/mode.js";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
19
|
+
const { emdash } = locals;
|
|
20
|
+
|
|
21
|
+
const authMode = getAuthMode(emdash?.config);
|
|
22
|
+
|
|
23
|
+
// Only check signup for passkey auth (external providers handle their own)
|
|
24
|
+
let signupEnabled = false;
|
|
25
|
+
if (emdash?.db && authMode.type === "passkey") {
|
|
26
|
+
try {
|
|
27
|
+
const { sql } = await import("kysely");
|
|
28
|
+
const result = await sql<{ cnt: unknown }>`
|
|
29
|
+
SELECT COUNT(*) as cnt FROM allowed_domains WHERE enabled = 1
|
|
30
|
+
`.execute(emdash.db);
|
|
31
|
+
signupEnabled = Number(result.rows[0]?.cnt ?? 0) > 0;
|
|
32
|
+
} catch {
|
|
33
|
+
// Table may not exist yet
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Collect pluggable auth providers (from authProviders config)
|
|
38
|
+
const providers = (emdash?.config?.authProviders ?? []).map((p) => ({
|
|
39
|
+
id: p.id,
|
|
40
|
+
label: p.label,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return Response.json(
|
|
44
|
+
{
|
|
45
|
+
data: {
|
|
46
|
+
authMode: authMode.type === "external" ? authMode.providerType : "passkey",
|
|
47
|
+
signupEnabled,
|
|
48
|
+
providers,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
headers: {
|
|
53
|
+
"Cache-Control": "private, no-store",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
19
19
|
|
|
20
20
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
21
|
+
import { finalizeSetup } from "#api/setup-complete.js";
|
|
21
22
|
import { createOAuthStateStore } from "#auth/oauth-state-store.js";
|
|
23
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
22
24
|
|
|
23
25
|
type ProviderName = "github" | "google";
|
|
24
26
|
|
|
@@ -126,10 +128,22 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
const adapter = createKyselyAdapter(emdash.db);
|
|
132
|
+
const stateStore = createOAuthStateStore(emdash.db);
|
|
133
|
+
|
|
129
134
|
const config: OAuthConsumerConfig = {
|
|
130
135
|
baseUrl: `${getPublicOrigin(url, emdash?.config)}/_emdash`,
|
|
131
136
|
providers,
|
|
132
137
|
canSelfSignup: async (email: string) => {
|
|
138
|
+
// During setup: first user becomes admin.
|
|
139
|
+
// Check setup_complete flag instead of countUsers() to avoid
|
|
140
|
+
// a TOCTOU race where concurrent callbacks both see 0 users.
|
|
141
|
+
const options = new OptionsRepository(emdash.db);
|
|
142
|
+
const setupComplete = await options.get("emdash:setup_complete");
|
|
143
|
+
if (setupComplete !== true && setupComplete !== "true") {
|
|
144
|
+
return { allowed: true, role: Role.ADMIN };
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
// Extract domain from email
|
|
134
148
|
const domain = email.split("@")[1]?.toLowerCase();
|
|
135
149
|
if (!domain) {
|
|
@@ -168,10 +182,16 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
|
|
|
168
182
|
},
|
|
169
183
|
};
|
|
170
184
|
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
185
|
+
const options = new OptionsRepository(emdash.db);
|
|
186
|
+
const setupCompleteBefore = await options.get("emdash:setup_complete");
|
|
174
187
|
const user = await handleOAuthCallback(config, adapter, provider, code, state, stateStore);
|
|
188
|
+
const isFirstUser = setupCompleteBefore !== true && setupCompleteBefore !== "true";
|
|
189
|
+
|
|
190
|
+
// Finalize setup outside the transaction (idempotent, safe if two callbacks race).
|
|
191
|
+
if (isFirstUser) {
|
|
192
|
+
await finalizeSetup(emdash.db);
|
|
193
|
+
console.log(`[oauth] Setup complete: created admin user via ${provider} (${user.email})`);
|
|
194
|
+
}
|
|
175
195
|
|
|
176
196
|
// Create session
|
|
177
197
|
if (session) {
|
|
@@ -71,16 +71,22 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
|
|
|
71
71
|
const { emdash } = locals;
|
|
72
72
|
const provider = params.provider;
|
|
73
73
|
|
|
74
|
+
// Determine where to redirect errors (setup wizard or login page)
|
|
75
|
+
const referer = request.headers.get("referer") ?? "";
|
|
76
|
+
const errorRedirectBase = referer.includes("/setup")
|
|
77
|
+
? "/_emdash/admin/setup"
|
|
78
|
+
: "/_emdash/admin/login";
|
|
79
|
+
|
|
74
80
|
// Validate provider
|
|
75
81
|
if (!provider || !isValidProvider(provider)) {
|
|
76
82
|
return redirect(
|
|
77
|
-
|
|
83
|
+
`${errorRedirectBase}?error=invalid_provider&message=${encodeURIComponent("Invalid OAuth provider")}`,
|
|
78
84
|
);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
if (!emdash?.db) {
|
|
82
88
|
return redirect(
|
|
83
|
-
|
|
89
|
+
`${errorRedirectBase}?error=server_error&message=${encodeURIComponent("Database not configured")}`,
|
|
84
90
|
);
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -97,7 +103,7 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
|
|
|
97
103
|
|
|
98
104
|
if (!providers[provider]) {
|
|
99
105
|
return redirect(
|
|
100
|
-
|
|
106
|
+
`${errorRedirectBase}?error=provider_not_configured&message=${encodeURIComponent(`OAuth provider ${provider} is not configured. Set either EMDASH_OAUTH_${provider.toUpperCase()}_CLIENT_ID and EMDASH_OAUTH_${provider.toUpperCase()}_CLIENT_SECRET, or ${provider.toUpperCase()}_CLIENT_ID and ${provider.toUpperCase()}_CLIENT_SECRET.`)}`,
|
|
101
107
|
);
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -114,7 +120,7 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
|
|
|
114
120
|
} catch (error) {
|
|
115
121
|
console.error("OAuth initiation error:", error);
|
|
116
122
|
return redirect(
|
|
117
|
-
|
|
123
|
+
`${errorRedirectBase}?error=oauth_error&message=${encodeURIComponent("Failed to start OAuth flow. Please try again.")}`,
|
|
118
124
|
);
|
|
119
125
|
}
|
|
120
126
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Returns all locale variants linked to the same translation group.
|
|
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 } from "#api/authorize.js";
|
|
@@ -61,15 +61,7 @@ export const POST: APIRoute = async ({ params, request, locals, cache }) => {
|
|
|
61
61
|
mapErrorStatus(source.error?.code),
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
|
-
const
|
|
65
|
-
source.data && typeof source.data === "object"
|
|
66
|
-
? (source.data as Record<string, unknown>)
|
|
67
|
-
: undefined;
|
|
68
|
-
const sourceItem =
|
|
69
|
-
sourceData?.item && typeof sourceData.item === "object"
|
|
70
|
-
? (sourceData.item as Record<string, unknown>)
|
|
71
|
-
: sourceData;
|
|
72
|
-
const sourceAuthor = typeof sourceItem?.authorId === "string" ? sourceItem.authorId : "";
|
|
64
|
+
const sourceAuthor = source.data.item.authorId ?? "";
|
|
73
65
|
const translationDenied = requireOwnerPerm(
|
|
74
66
|
user,
|
|
75
67
|
sourceAuthor,
|
|
@@ -92,7 +92,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
92
92
|
attachments,
|
|
93
93
|
emdash.db,
|
|
94
94
|
emdash.storage,
|
|
95
|
-
request.url,
|
|
96
95
|
sendProgress,
|
|
97
96
|
);
|
|
98
97
|
|
|
@@ -117,7 +116,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
117
116
|
attachments,
|
|
118
117
|
emdash.db,
|
|
119
118
|
emdash.storage,
|
|
120
|
-
request.url,
|
|
121
119
|
() => {}, // No-op progress callback
|
|
122
120
|
);
|
|
123
121
|
|
|
@@ -131,12 +129,9 @@ async function importMediaWithProgress(
|
|
|
131
129
|
attachments: AttachmentInfo[],
|
|
132
130
|
db: NonNullable<EmDashHandlers["db"]>,
|
|
133
131
|
storage: NonNullable<EmDashHandlers["storage"]>,
|
|
134
|
-
requestUrl: string,
|
|
135
132
|
onProgress: (progress: MediaImportProgress) => void,
|
|
136
133
|
): Promise<MediaImportResult> {
|
|
137
134
|
const repo = new MediaRepository(db);
|
|
138
|
-
const url = new URL(requestUrl);
|
|
139
|
-
const baseUrl = `${url.protocol}//${url.host}`;
|
|
140
135
|
const total = attachments.length;
|
|
141
136
|
|
|
142
137
|
const result: MediaImportResult = {
|
|
@@ -237,7 +232,7 @@ async function importMediaWithProgress(
|
|
|
237
232
|
const existing = await repo.findByContentHash(contentHash);
|
|
238
233
|
if (existing) {
|
|
239
234
|
// Same content already exists - reuse it
|
|
240
|
-
const existingUrl =
|
|
235
|
+
const existingUrl = `/_emdash/api/media/file/${existing.storageKey}`;
|
|
241
236
|
result.urlMap[attachment.url] = existingUrl;
|
|
242
237
|
result.imported.push({
|
|
243
238
|
wpId: attachment.id,
|
|
@@ -290,7 +285,7 @@ async function importMediaWithProgress(
|
|
|
290
285
|
});
|
|
291
286
|
|
|
292
287
|
// Build the new URL
|
|
293
|
-
const newUrl =
|
|
288
|
+
const newUrl = `/_emdash/api/media/file/${storageKey}`;
|
|
294
289
|
|
|
295
290
|
result.imported.push({
|
|
296
291
|
wpId: attachment.id,
|
|
@@ -58,6 +58,16 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
58
58
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to PrepareRequest
|
|
59
59
|
const result = await prepareImport(emdash.db, body as PrepareRequest);
|
|
60
60
|
|
|
61
|
+
// If prepare created any new collections or fields, invalidate the
|
|
62
|
+
// persisted manifest cache (`emdash:manifest_cache` in the options
|
|
63
|
+
// table) so that the execute endpoint -- a separate request -- sees
|
|
64
|
+
// the new schema. Without this the execute step reads a stale
|
|
65
|
+
// manifest and reports `Collection "<slug>" does not exist` for
|
|
66
|
+
// every item destined for a freshly-created collection. See #747.
|
|
67
|
+
if (result.collectionsCreated.length > 0 || result.fieldsCreated.length > 0) {
|
|
68
|
+
emdash.invalidateManifest();
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
return apiSuccess(result, result.success ? 200 : 400);
|
|
62
72
|
} catch (error) {
|
|
63
73
|
return handleError(error, "Failed to prepare import", "WXR_PREPARE_ERROR");
|
|
@@ -49,20 +49,15 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
49
49
|
`emdash:exclusive_hook:${EMAIL_DELIVER_HOOK}`,
|
|
50
50
|
);
|
|
51
51
|
|
|
52
|
-
// Get middleware hooks (beforeSend / afterSend)
|
|
52
|
+
// Get middleware hooks (beforeSend / afterSend). These are non-exclusive —
|
|
53
|
+
// many plugins can subscribe — so we enumerate non-exclusive providers.
|
|
53
54
|
const beforeSendPlugins = pipeline
|
|
54
|
-
.
|
|
55
|
+
.getHookProviders(EMAIL_BEFORE_SEND_HOOK)
|
|
55
56
|
.map((p) => p.pluginId);
|
|
56
57
|
const afterSendPlugins = pipeline
|
|
57
|
-
.
|
|
58
|
+
.getHookProviders(EMAIL_AFTER_SEND_HOOK)
|
|
58
59
|
.map((p) => p.pluginId);
|
|
59
60
|
|
|
60
|
-
// Note: beforeSend/afterSend are NOT exclusive hooks, but getExclusiveHookProviders
|
|
61
|
-
// only finds exclusive ones. We need all hooks for those names.
|
|
62
|
-
// For now, report what we can from the exclusive hook system.
|
|
63
|
-
// Middleware is non-exclusive so we'd need a different query.
|
|
64
|
-
// TODO: Add getHookProviders() for non-exclusive hooks to the pipeline.
|
|
65
|
-
|
|
66
61
|
return apiSuccess({
|
|
67
62
|
available: emdash.email?.isAvailable() ?? false,
|
|
68
63
|
providers: providers.map((p) => ({
|
|
@@ -49,6 +49,10 @@ export const POST: APIRoute = async ({ cookies, request, locals }) => {
|
|
|
49
49
|
const body = await parseBody(request, setupAdminBody);
|
|
50
50
|
if (isParseError(body)) return body;
|
|
51
51
|
|
|
52
|
+
// Preserve title/tagline from step 1 by reading existing setup state
|
|
53
|
+
// before we overwrite it below.
|
|
54
|
+
const existingState = await options.get<Record<string, unknown>>("emdash:setup_state");
|
|
55
|
+
|
|
52
56
|
// Mint a fresh session nonce. This binds the follow-up
|
|
53
57
|
// /setup/admin/verify call to the same browser that made this
|
|
54
58
|
// request, so an unauthenticated attacker on another host cannot
|
|
@@ -81,9 +85,11 @@ export const POST: APIRoute = async ({ cookies, request, locals }) => {
|
|
|
81
85
|
challengeStore,
|
|
82
86
|
);
|
|
83
87
|
|
|
84
|
-
// Store the nonce alongside the rest of the setup state
|
|
85
|
-
//
|
|
88
|
+
// Store the nonce alongside the rest of the setup state, preserving
|
|
89
|
+
// title/tagline from step 1. The verify endpoint will constant-time
|
|
90
|
+
// compare the nonce with the incoming cookie.
|
|
86
91
|
await options.set("emdash:setup_state", {
|
|
92
|
+
...existingState,
|
|
87
93
|
step: "admin",
|
|
88
94
|
email: body.email.toLowerCase(),
|
|
89
95
|
name: body.name || null,
|
|
@@ -81,7 +81,7 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
|
81
81
|
|
|
82
82
|
// 5. Store setup state
|
|
83
83
|
// In external auth mode, mark setup complete immediately (first user to login becomes admin)
|
|
84
|
-
//
|
|
84
|
+
// Otherwise, setup_complete is set after admin user is created (passkey or auth provider)
|
|
85
85
|
const authMode = getAuthMode(emdash.config);
|
|
86
86
|
const useExternalAuth = authMode.type === "external";
|
|
87
87
|
|
|
@@ -105,7 +105,7 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
|
105
105
|
await options.set("emdash:site_tagline", body.tagline);
|
|
106
106
|
}
|
|
107
107
|
} else {
|
|
108
|
-
// Passkey mode: store state for next step (admin creation)
|
|
108
|
+
// Passkey/provider mode: store state for next step (admin creation)
|
|
109
109
|
await options.set("emdash:setup_state", {
|
|
110
110
|
step: "site_complete",
|
|
111
111
|
title: body.title,
|
|
@@ -91,7 +91,7 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
91
91
|
const authMode = getAuthMode(emdash.config);
|
|
92
92
|
const useExternalAuth = authMode.type === "external";
|
|
93
93
|
|
|
94
|
-
// In external auth mode, setup is complete if flag is set (no users required initially)
|
|
94
|
+
// In external auth mode (not atproto), setup is complete if flag is set (no users required initially)
|
|
95
95
|
if (useExternalAuth && isComplete) {
|
|
96
96
|
return apiSuccess({
|
|
97
97
|
needsSetup: false,
|
|
@@ -106,6 +106,8 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
106
106
|
description: seed.meta?.description || "",
|
|
107
107
|
collections: seed.collections?.length || 0,
|
|
108
108
|
hasContent: !!(seed.content && Object.keys(seed.content).length > 0),
|
|
109
|
+
title: seed.settings?.title,
|
|
110
|
+
tagline: seed.settings?.tagline,
|
|
109
111
|
}
|
|
110
112
|
: null;
|
|
111
113
|
|
|
@@ -11,6 +11,8 @@ import { requirePerm } from "#api/authorize.js";
|
|
|
11
11
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
12
12
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
13
13
|
import { updateWidgetBody } from "#api/schemas.js";
|
|
14
|
+
import { rowToWidget } from "#widgets/index.js";
|
|
15
|
+
import type { WidgetRow } from "#widgets/types.js";
|
|
14
16
|
|
|
15
17
|
export const prerender = false;
|
|
16
18
|
|
|
@@ -73,10 +75,11 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
73
75
|
const widget = await db
|
|
74
76
|
.selectFrom("_emdash_widgets")
|
|
75
77
|
.selectAll()
|
|
78
|
+
.$castTo<WidgetRow>()
|
|
76
79
|
.where("id", "=", id)
|
|
77
80
|
.executeTakeFirstOrThrow();
|
|
78
81
|
|
|
79
|
-
return apiSuccess(widget);
|
|
82
|
+
return apiSuccess(rowToWidget(widget));
|
|
80
83
|
} catch (error) {
|
|
81
84
|
return handleError(error, "Failed to update widget", "WIDGET_UPDATE_ERROR");
|
|
82
85
|
}
|
|
@@ -11,6 +11,8 @@ import { requirePerm } from "#api/authorize.js";
|
|
|
11
11
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
12
12
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
13
13
|
import { createWidgetBody } from "#api/schemas.js";
|
|
14
|
+
import { rowToWidget } from "#widgets/index.js";
|
|
15
|
+
import type { WidgetRow } from "#widgets/types.js";
|
|
14
16
|
|
|
15
17
|
export const prerender = false;
|
|
16
18
|
|
|
@@ -70,10 +72,11 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
70
72
|
const widget = await db
|
|
71
73
|
.selectFrom("_emdash_widgets")
|
|
72
74
|
.selectAll()
|
|
75
|
+
.$castTo<WidgetRow>()
|
|
73
76
|
.where("id", "=", id)
|
|
74
77
|
.executeTakeFirstOrThrow();
|
|
75
78
|
|
|
76
|
-
return apiSuccess(widget, 201);
|
|
79
|
+
return apiSuccess(rowToWidget(widget), 201);
|
|
77
80
|
} catch (error) {
|
|
78
81
|
return handleError(error, "Failed to create widget", "WIDGET_CREATE_ERROR");
|
|
79
82
|
}
|
|
@@ -9,6 +9,8 @@ import type { APIRoute } from "astro";
|
|
|
9
9
|
|
|
10
10
|
import { requirePerm } from "#api/authorize.js";
|
|
11
11
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
12
|
+
import { rowToWidget } from "#widgets/index.js";
|
|
13
|
+
import type { WidgetRow } from "#widgets/types.js";
|
|
12
14
|
|
|
13
15
|
export const prerender = false;
|
|
14
16
|
|
|
@@ -40,13 +42,14 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
40
42
|
const widgets = await db
|
|
41
43
|
.selectFrom("_emdash_widgets")
|
|
42
44
|
.selectAll()
|
|
45
|
+
.$castTo<WidgetRow>()
|
|
43
46
|
.where("area_id", "=", area.id)
|
|
44
47
|
.orderBy("sort_order", "asc")
|
|
45
48
|
.execute();
|
|
46
49
|
|
|
47
50
|
return apiSuccess({
|
|
48
51
|
...area,
|
|
49
|
-
widgets,
|
|
52
|
+
widgets: widgets.map((row) => rowToWidget(row)),
|
|
50
53
|
});
|
|
51
54
|
} catch (error) {
|
|
52
55
|
return handleError(error, "Failed to fetch widget area", "WIDGET_AREA_GET_ERROR");
|
|
@@ -12,6 +12,8 @@ import { requirePerm } from "#api/authorize.js";
|
|
|
12
12
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
13
13
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
14
|
import { createWidgetAreaBody } from "#api/schemas.js";
|
|
15
|
+
import { rowToWidget } from "#widgets/index.js";
|
|
16
|
+
import type { WidgetRow } from "#widgets/types.js";
|
|
15
17
|
|
|
16
18
|
export const prerender = false;
|
|
17
19
|
|
|
@@ -35,13 +37,14 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
35
37
|
const widgets = await db
|
|
36
38
|
.selectFrom("_emdash_widgets")
|
|
37
39
|
.selectAll()
|
|
40
|
+
.$castTo<WidgetRow>()
|
|
38
41
|
.where("area_id", "=", area.id)
|
|
39
42
|
.orderBy("sort_order", "asc")
|
|
40
43
|
.execute();
|
|
41
44
|
|
|
42
45
|
return {
|
|
43
46
|
...area,
|
|
44
|
-
widgets,
|
|
47
|
+
widgets: widgets.map((row) => rowToWidget(row)),
|
|
45
48
|
widgetCount: widgets.length,
|
|
46
49
|
};
|
|
47
50
|
}),
|
package/src/astro/types.ts
CHANGED
|
@@ -348,6 +348,7 @@ export interface EmDashHandlers {
|
|
|
348
348
|
// Direct access to storage and database for advanced use cases
|
|
349
349
|
storage: import("../index.js").Storage | null;
|
|
350
350
|
db: Kysely<import("../index.js").Database>;
|
|
351
|
+
getPublicMediaUrl?: (storageKey: string) => string;
|
|
351
352
|
|
|
352
353
|
// Hook pipeline for plugin integrations
|
|
353
354
|
hooks: import("../plugins/hooks.js").HookPipeline;
|
|
@@ -380,4 +381,12 @@ export interface EmDashHandlers {
|
|
|
380
381
|
collectPageFragments: (
|
|
381
382
|
page: import("../plugins/types.js").PublicPageContext,
|
|
382
383
|
) => Promise<import("../plugins/types.js").PageFragmentContribution[]>;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Lazy search index health check. Search routes call this before
|
|
387
|
+
* querying so a crash-corrupted index gets repaired on first use
|
|
388
|
+
* rather than stalling cold start. Optional because it's only
|
|
389
|
+
* meaningful when an FTS5-capable runtime is wired in.
|
|
390
|
+
*/
|
|
391
|
+
ensureSearchHealthy?: () => Promise<void>;
|
|
383
392
|
}
|
package/src/auth/mode.ts
CHANGED
|
@@ -6,9 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { EmDashConfig } from "../astro/integration/runtime.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AuthDescriptor,
|
|
11
|
+
AuthProviderDescriptor,
|
|
12
|
+
AuthRouteDescriptor,
|
|
13
|
+
AuthResult,
|
|
14
|
+
ExternalAuthConfig,
|
|
15
|
+
} from "./types.js";
|
|
10
16
|
|
|
11
|
-
export type {
|
|
17
|
+
export type {
|
|
18
|
+
AuthDescriptor,
|
|
19
|
+
AuthProviderDescriptor,
|
|
20
|
+
AuthRouteDescriptor,
|
|
21
|
+
AuthResult,
|
|
22
|
+
ExternalAuthConfig,
|
|
23
|
+
};
|
|
12
24
|
|
|
13
25
|
/**
|
|
14
26
|
* Passkey auth mode (default)
|
|
@@ -59,7 +71,7 @@ export function getAuthMode(
|
|
|
59
71
|
): AuthMode {
|
|
60
72
|
const auth = config?.auth;
|
|
61
73
|
|
|
62
|
-
// Check for AuthDescriptor (
|
|
74
|
+
// Check for AuthDescriptor (transparent external auth like Cloudflare Access)
|
|
63
75
|
if (auth && "entrypoint" in auth && auth.entrypoint) {
|
|
64
76
|
return {
|
|
65
77
|
type: "external",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Admin Components
|
|
3
|
+
*
|
|
4
|
+
* LoginButton for the login page, rendered via the auth provider virtual module.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LinkButton } from "@cloudflare/kumo";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
|
|
10
|
+
function GitHubIcon({ className }: { className?: string }) {
|
|
11
|
+
return (
|
|
12
|
+
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
13
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
14
|
+
</svg>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function LoginButton() {
|
|
19
|
+
return (
|
|
20
|
+
<LinkButton
|
|
21
|
+
href="/_emdash/api/auth/oauth/github"
|
|
22
|
+
variant="outline"
|
|
23
|
+
className="w-full justify-center"
|
|
24
|
+
>
|
|
25
|
+
<GitHubIcon className="h-5 w-5" />
|
|
26
|
+
<span>GitHub</span>
|
|
27
|
+
</LinkButton>
|
|
28
|
+
);
|
|
29
|
+
}
|