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
package/src/media/normalize.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { MediaProvider, MediaProviderItem, MediaValue } from "./types.js";
|
|
13
13
|
|
|
14
|
-
const INTERNAL_MEDIA_PREFIX = "/_emdash/api/media/file/";
|
|
14
|
+
export const INTERNAL_MEDIA_PREFIX = "/_emdash/api/media/file/";
|
|
15
15
|
const URL_PATTERN = /^https?:\/\//;
|
|
16
16
|
|
|
17
17
|
/**
|
package/src/media/url.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public media URL resolution.
|
|
3
|
+
*
|
|
4
|
+
* Used at render time by the Image components to decide whether a storage
|
|
5
|
+
* key should be served from the configured `publicUrl` (R2 custom domain,
|
|
6
|
+
* S3 CDN) or through the internal `/_emdash/api/media/file/{key}` route.
|
|
7
|
+
*/
|
|
8
|
+
import type { Storage } from "../storage/types.js";
|
|
9
|
+
import { INTERNAL_MEDIA_PREFIX } from "./normalize.js";
|
|
10
|
+
|
|
11
|
+
// Keys accepted by the public-URL rewrite: the `{ulid}{ext}` shape produced by
|
|
12
|
+
// the upload pipeline, with letters, digits, dots, dashes, and underscores.
|
|
13
|
+
// Slashes, `?`, `#`, and `%` are rejected so attacker-controlled content in a
|
|
14
|
+
// portable-text `asset.url` cannot traverse or reroute on the CDN origin.
|
|
15
|
+
const SAFE_STORAGE_KEY = /^[A-Za-z0-9._-]+$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the public URL for a locally stored media key. Returns an empty
|
|
19
|
+
* string when no key is given. When a storage adapter is supplied, defers to
|
|
20
|
+
* `storage.getPublicUrl()`; otherwise returns the internal proxy route.
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePublicMediaUrl(
|
|
23
|
+
storage: Storage | null | undefined,
|
|
24
|
+
storageKey: string,
|
|
25
|
+
): string {
|
|
26
|
+
if (!storageKey) return "";
|
|
27
|
+
if (storage) return storage.getPublicUrl(storageKey);
|
|
28
|
+
return `/_emdash/api/media/file/${storageKey}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the `getPublicMediaUrl` closure attached to `Astro.locals.emdash`.
|
|
33
|
+
* Shared by the anonymous fast path and the full-runtime path in middleware.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export function createPublicMediaUrlResolver(
|
|
38
|
+
storage: Storage | null | undefined,
|
|
39
|
+
): (key: string) => string {
|
|
40
|
+
return (key) => resolvePublicMediaUrl(storage, key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Input shape for {@link buildRenderMediaUrl}. */
|
|
44
|
+
export interface RenderMediaRef {
|
|
45
|
+
/** Storage key with extension (the canonical shape from the upload pipeline). */
|
|
46
|
+
storageKey?: string;
|
|
47
|
+
/** Pre-baked URL (either an internal proxy URL or an external URL). */
|
|
48
|
+
url?: string;
|
|
49
|
+
/** Bare media id (ULID without extension); only the internal proxy can look this up. */
|
|
50
|
+
id?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a render-time media URL. Prefers `storageKey`, then rewrites an
|
|
55
|
+
* internal `url` via `resolve`, then falls back to the internal proxy for a
|
|
56
|
+
* bare `id`. External URLs and non-matching internal-looking URLs pass
|
|
57
|
+
* through untouched. Returns `""` when nothing usable is present.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export function buildRenderMediaUrl(
|
|
62
|
+
resolve: ((key: string) => string) | undefined,
|
|
63
|
+
ref: RenderMediaRef,
|
|
64
|
+
): string {
|
|
65
|
+
const { storageKey, url, id } = ref;
|
|
66
|
+
if (storageKey) {
|
|
67
|
+
return resolve ? resolve(storageKey) : `${INTERNAL_MEDIA_PREFIX}${storageKey}`;
|
|
68
|
+
}
|
|
69
|
+
if (url) {
|
|
70
|
+
if (resolve && url.startsWith(INTERNAL_MEDIA_PREFIX)) {
|
|
71
|
+
const key = url.slice(INTERNAL_MEDIA_PREFIX.length);
|
|
72
|
+
if (SAFE_STORAGE_KEY.test(key)) return resolve(key);
|
|
73
|
+
}
|
|
74
|
+
return url;
|
|
75
|
+
}
|
|
76
|
+
if (id) return `${INTERNAL_MEDIA_PREFIX}${id}`;
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
@@ -30,9 +30,16 @@ export interface StoredEmail {
|
|
|
30
30
|
* instances (the runtime and the route handler may load separate copies
|
|
31
31
|
* of this module, but globalThis is always the same object).
|
|
32
32
|
*/
|
|
33
|
-
const GLOBAL_KEY = "
|
|
34
|
-
const
|
|
35
|
-
|
|
33
|
+
const GLOBAL_KEY = Symbol.for("emdash:dev-emails");
|
|
34
|
+
const g = globalThis as Record<symbol, unknown>;
|
|
35
|
+
const storedEmails: StoredEmail[] = (() => {
|
|
36
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts)
|
|
37
|
+
const existing = g[GLOBAL_KEY] as StoredEmail[] | undefined;
|
|
38
|
+
if (existing) return existing;
|
|
39
|
+
const fresh: StoredEmail[] = [];
|
|
40
|
+
g[GLOBAL_KEY] = fresh;
|
|
41
|
+
return fresh;
|
|
42
|
+
})();
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
45
|
* Get all stored dev emails (most recent first).
|
package/src/plugins/hooks.ts
CHANGED
|
@@ -1218,6 +1218,17 @@ export class HookPipeline {
|
|
|
1218
1218
|
return hooks.filter((h) => h.exclusive).map((h) => ({ pluginId: h.pluginId }));
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* Get all plugins that registered a non-exclusive handler for a given
|
|
1223
|
+
* hook (e.g. `email:beforeSend`, `email:afterSend`), preserving priority
|
|
1224
|
+
* order. Partitions with `getExclusiveHookProviders()`, which returns
|
|
1225
|
+
* plugins whose registration is marked `exclusive: true`.
|
|
1226
|
+
*/
|
|
1227
|
+
getHookProviders(hookName: string): Array<{ pluginId: string }> {
|
|
1228
|
+
const hooks = this.hooks.get(hookName as HookNameV2) ?? [];
|
|
1229
|
+
return hooks.filter((h) => !h.exclusive).map((h) => ({ pluginId: h.pluginId }));
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1221
1232
|
/**
|
|
1222
1233
|
* Invoke an exclusive hook — dispatch only to the selected provider.
|
|
1223
1234
|
* Returns null if no provider is selected or if the selected hook
|
|
@@ -131,6 +131,18 @@ const settingFieldSchema = z.discriminatedUnion("type", [
|
|
|
131
131
|
default: z.string().optional(),
|
|
132
132
|
}),
|
|
133
133
|
z.object({ ...baseSettingFields, type: z.literal("secret") }),
|
|
134
|
+
z.object({
|
|
135
|
+
...baseSettingFields,
|
|
136
|
+
type: z.literal("url"),
|
|
137
|
+
default: z.string().optional(),
|
|
138
|
+
placeholder: z.string().optional(),
|
|
139
|
+
}),
|
|
140
|
+
z.object({
|
|
141
|
+
...baseSettingFields,
|
|
142
|
+
type: z.literal("email"),
|
|
143
|
+
default: z.string().optional(),
|
|
144
|
+
placeholder: z.string().optional(),
|
|
145
|
+
}),
|
|
134
146
|
]);
|
|
135
147
|
|
|
136
148
|
const adminPageSchema = z.object({
|
package/src/plugins/types.ts
CHANGED
|
@@ -1114,7 +1114,14 @@ export interface PluginDashboardWidget {
|
|
|
1114
1114
|
/**
|
|
1115
1115
|
* Settings field types (for admin UI generation)
|
|
1116
1116
|
*/
|
|
1117
|
-
export type SettingFieldType =
|
|
1117
|
+
export type SettingFieldType =
|
|
1118
|
+
| "string"
|
|
1119
|
+
| "number"
|
|
1120
|
+
| "boolean"
|
|
1121
|
+
| "select"
|
|
1122
|
+
| "secret"
|
|
1123
|
+
| "url"
|
|
1124
|
+
| "email";
|
|
1118
1125
|
|
|
1119
1126
|
export interface BaseSettingField {
|
|
1120
1127
|
type: SettingFieldType;
|
|
@@ -1150,12 +1157,26 @@ export interface SecretSettingField extends BaseSettingField {
|
|
|
1150
1157
|
type: "secret";
|
|
1151
1158
|
}
|
|
1152
1159
|
|
|
1160
|
+
export interface UrlSettingField extends BaseSettingField {
|
|
1161
|
+
type: "url";
|
|
1162
|
+
default?: string;
|
|
1163
|
+
placeholder?: string;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
export interface EmailSettingField extends BaseSettingField {
|
|
1167
|
+
type: "email";
|
|
1168
|
+
default?: string;
|
|
1169
|
+
placeholder?: string;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1153
1172
|
export type SettingField =
|
|
1154
1173
|
| StringSettingField
|
|
1155
1174
|
| NumberSettingField
|
|
1156
1175
|
| BooleanSettingField
|
|
1157
1176
|
| SelectSettingField
|
|
1158
|
-
| SecretSettingField
|
|
1177
|
+
| SecretSettingField
|
|
1178
|
+
| UrlSettingField
|
|
1179
|
+
| EmailSettingField;
|
|
1159
1180
|
|
|
1160
1181
|
/**
|
|
1161
1182
|
* Block Kit element for block editing fields.
|
package/src/query.ts
CHANGED
|
@@ -478,7 +478,7 @@ export async function getEmDashEntry<T extends string, D = InferCollectionData<T
|
|
|
478
478
|
// Edit mode (authenticated editors) has collection-wide draft access.
|
|
479
479
|
if (isPreviewMode && !isEditMode) {
|
|
480
480
|
const dbId = entryDatabaseId(baseEntry);
|
|
481
|
-
if (preview
|
|
481
|
+
if (preview.id !== dbId && preview.id !== id) {
|
|
482
482
|
// Token doesn't match — serve only if publicly visible, without draft access
|
|
483
483
|
if (isVisible(baseEntry)) {
|
|
484
484
|
return successResult(wrapEntry(baseEntry), {
|
package/src/request-cache.ts
CHANGED
|
@@ -22,6 +22,7 @@ type CacheStore = WeakMap<EmDashRequestContext, Map<string, Promise<unknown>>>;
|
|
|
22
22
|
const STORE_KEY = Symbol.for("emdash:request-cache");
|
|
23
23
|
const g = globalThis as Record<symbol, unknown>;
|
|
24
24
|
const store: CacheStore =
|
|
25
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts)
|
|
25
26
|
(g[STORE_KEY] as CacheStore | undefined) ??
|
|
26
27
|
(() => {
|
|
27
28
|
const wm: CacheStore = new WeakMap();
|
|
@@ -47,6 +48,7 @@ export function requestCached<T>(key: string, fn: () => Promise<T>): Promise<T>
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const existing = cache.get(key);
|
|
51
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; key namespacing guarantees the stored promise resolves to T
|
|
50
52
|
if (existing) return existing as Promise<T>;
|
|
51
53
|
|
|
52
54
|
const promise = Promise.resolve()
|
|
@@ -74,6 +76,7 @@ export function peekRequestCache<T>(key: string): Promise<T> | undefined {
|
|
|
74
76
|
const ctx = getRequestContext();
|
|
75
77
|
if (!ctx) return undefined;
|
|
76
78
|
const cache = store.get(ctx);
|
|
79
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; caller is responsible for using a T-compatible key
|
|
77
80
|
return cache?.get(key) as Promise<T> | undefined;
|
|
78
81
|
}
|
|
79
82
|
|
package/src/schema/registry.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { FTSManager } from "../search/fts-manager.js";
|
|
|
11
11
|
import {
|
|
12
12
|
type Collection,
|
|
13
13
|
type CollectionSource,
|
|
14
|
+
type CollectionSupport,
|
|
14
15
|
type ColumnType,
|
|
15
16
|
type Field,
|
|
16
17
|
type CreateCollectionInput,
|
|
@@ -49,6 +50,34 @@ function isColumnType(value: string): value is ColumnType {
|
|
|
49
50
|
return COLUMN_TYPES.has(value);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
const VALID_COLLECTION_SUPPORTS: ReadonlySet<string> = new Set<CollectionSupport>([
|
|
54
|
+
"drafts",
|
|
55
|
+
"revisions",
|
|
56
|
+
"preview",
|
|
57
|
+
"scheduling",
|
|
58
|
+
"search",
|
|
59
|
+
"seo",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
function isCollectionSupport(value: unknown): value is CollectionSupport {
|
|
63
|
+
return typeof value === "string" && VALID_COLLECTION_SUPPORTS.has(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a collection's `supports` column (stored as a JSON array of
|
|
68
|
+
* CollectionSupport keys). Unknown/invalid entries are filtered out so the
|
|
69
|
+
* runtime value matches the declared `CollectionSupport[]` type.
|
|
70
|
+
*
|
|
71
|
+
* Throws on malformed JSON so corruption surfaces loudly; returns an empty
|
|
72
|
+
* array only for explicitly null/empty values or non-array JSON.
|
|
73
|
+
*/
|
|
74
|
+
function parseSupports(raw: string | null | undefined): CollectionSupport[] {
|
|
75
|
+
if (!raw) return [];
|
|
76
|
+
const parsed: unknown = JSON.parse(raw);
|
|
77
|
+
if (!Array.isArray(parsed)) return [];
|
|
78
|
+
return parsed.filter(isCollectionSupport);
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
/**
|
|
53
82
|
* Error thrown when a schema operation fails
|
|
54
83
|
*/
|
|
@@ -132,11 +161,18 @@ export class SchemaRegistry {
|
|
|
132
161
|
|
|
133
162
|
const id = ulid();
|
|
134
163
|
|
|
164
|
+
// Default `supports` to drafts + revisions when the caller didn't
|
|
165
|
+
// specify it. Explicit empty array (`[]`) is preserved as an opt-out
|
|
166
|
+
// — only `undefined` triggers the default. This is the canonical
|
|
167
|
+
// default for new collections; the MCP and admin UI layers used to
|
|
168
|
+
// duplicate this default but now defer to the registry.
|
|
169
|
+
const supports = input.supports ?? ["drafts", "revisions"];
|
|
170
|
+
|
|
135
171
|
// Insert collection record and create content table in a transaction
|
|
136
172
|
// so a failure in table creation doesn't leave an orphaned row.
|
|
137
173
|
// Uses withTransaction for D1 compatibility (no transaction support).
|
|
138
174
|
// Derive hasSeo from supports array if not explicitly set
|
|
139
|
-
const hasSeo = input.hasSeo ??
|
|
175
|
+
const hasSeo = input.hasSeo ?? supports.includes("seo") ?? false;
|
|
140
176
|
|
|
141
177
|
await withTransaction(this.db, async (trx) => {
|
|
142
178
|
await trx
|
|
@@ -148,7 +184,7 @@ export class SchemaRegistry {
|
|
|
148
184
|
label_singular: input.labelSingular ?? null,
|
|
149
185
|
description: input.description ?? null,
|
|
150
186
|
icon: input.icon ?? null,
|
|
151
|
-
supports:
|
|
187
|
+
supports: JSON.stringify(supports),
|
|
152
188
|
source: input.source ?? "manual",
|
|
153
189
|
has_seo: hasSeo ? 1 : 0,
|
|
154
190
|
comments_enabled: input.commentsEnabled ? 1 : 0,
|
|
@@ -243,7 +279,7 @@ export class SchemaRegistry {
|
|
|
243
279
|
// Sync FTS state when the supports array changes (e.g. search toggled on/off)
|
|
244
280
|
if (input.supports !== undefined) {
|
|
245
281
|
const hadSearch = existing.supports.includes("search");
|
|
246
|
-
const hasSearch = (
|
|
282
|
+
const hasSearch = parseSupports(row.supports).includes("search");
|
|
247
283
|
if (hadSearch !== hasSearch) {
|
|
248
284
|
await this.syncSearchState(slug, trx);
|
|
249
285
|
}
|
|
@@ -525,7 +561,7 @@ export class SchemaRegistry {
|
|
|
525
561
|
.executeTakeFirst();
|
|
526
562
|
if (!row) return;
|
|
527
563
|
|
|
528
|
-
const wantsSearch = (
|
|
564
|
+
const wantsSearch = parseSupports(row.supports).includes("search");
|
|
529
565
|
const searchableFields = await ftsManager.getSearchableFields(collectionSlug);
|
|
530
566
|
const config = await ftsManager.getSearchConfig(collectionSlug);
|
|
531
567
|
const ftsActive = config?.enabled === true;
|
|
@@ -881,7 +917,7 @@ export class SchemaRegistry {
|
|
|
881
917
|
labelSingular: row.label_singular ?? undefined,
|
|
882
918
|
description: row.description ?? undefined,
|
|
883
919
|
icon: row.icon ?? undefined,
|
|
884
|
-
supports:
|
|
920
|
+
supports: parseSupports(row.supports),
|
|
885
921
|
source: row.source && isCollectionSource(row.source) ? row.source : undefined,
|
|
886
922
|
hasSeo: row.has_seo === 1,
|
|
887
923
|
urlPattern: row.url_pattern ?? undefined,
|
|
@@ -418,8 +418,6 @@ export class FTSManager {
|
|
|
418
418
|
console.warn(
|
|
419
419
|
`FTS index for "${collectionSlug}" has ${ftsRows} rows but content table has ${contentRows}. Rebuilding.`,
|
|
420
420
|
);
|
|
421
|
-
const fields = await this.getSearchableFields(collectionSlug);
|
|
422
|
-
const config = await this.getSearchConfig(collectionSlug);
|
|
423
421
|
if (fields.length > 0) {
|
|
424
422
|
await this.rebuildIndex(collectionSlug, fields, config?.weights);
|
|
425
423
|
}
|
package/src/search/query.ts
CHANGED
|
@@ -26,6 +26,23 @@ const WHITESPACE_SPLIT_PATTERN = /\s+/;
|
|
|
26
26
|
const FTS_OPERATORS_PATTERN = /\b(AND|OR|NOT|NEAR)\b/i;
|
|
27
27
|
const DOUBLE_QUOTE_PATTERN = /"/g;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Detect FTS5 query syntax errors. Match specifically on the SQLite FTS5
|
|
31
|
+
* error fingerprints rather than a broad "fts5" / "syntax error" filter
|
|
32
|
+
* (which would also swallow internal table-corruption errors). The two
|
|
33
|
+
* fingerprints we care about are:
|
|
34
|
+
*
|
|
35
|
+
* - "fts5: syntax error near …" — unbalanced quotes, stray operators,
|
|
36
|
+
* other malformed user input
|
|
37
|
+
* - "unknown special query: …" — bare special tokens like `^*` that
|
|
38
|
+
* parse but don't resolve to a real FTS5 directive
|
|
39
|
+
*/
|
|
40
|
+
function isFts5SyntaxError(error: unknown): boolean {
|
|
41
|
+
if (!(error instanceof Error)) return false;
|
|
42
|
+
const message = error.message.toLowerCase();
|
|
43
|
+
return message.includes("fts5: syntax error") || message.includes("unknown special query");
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
/**
|
|
30
47
|
* Search across multiple collections
|
|
31
48
|
*
|
|
@@ -198,14 +215,16 @@ async function searchSingleCollection(
|
|
|
198
215
|
const bm25Expr = bm25Args ? `bm25("${ftsTable}", ${bm25Args})` : `bm25("${ftsTable}")`;
|
|
199
216
|
|
|
200
217
|
// Snippet column index is 2 (after id=0, locale=1, first searchable field=2)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
let results;
|
|
219
|
+
try {
|
|
220
|
+
results = await sql<{
|
|
221
|
+
id: string;
|
|
222
|
+
slug: string | null;
|
|
223
|
+
locale: string;
|
|
224
|
+
title: string | null;
|
|
225
|
+
snippet: string | null;
|
|
226
|
+
score: number;
|
|
227
|
+
}>`
|
|
209
228
|
SELECT
|
|
210
229
|
c.id,
|
|
211
230
|
c.slug,
|
|
@@ -222,6 +241,20 @@ async function searchSingleCollection(
|
|
|
222
241
|
ORDER BY score
|
|
223
242
|
LIMIT ${limit}
|
|
224
243
|
`.execute(db);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
// FTS5 returns syntax errors for queries with unbalanced quotes,
|
|
246
|
+
// stray operators, or other malformed input. Treat these as
|
|
247
|
+
// "no matches" so the user gets an empty result rather than an
|
|
248
|
+
// internals-leaking error. Other errors (table missing, IO) still
|
|
249
|
+
// propagate. Intentionally not logged: any anonymous client can
|
|
250
|
+
// trigger this path, and the underlying error message embeds the
|
|
251
|
+
// raw query, so logging would be both noisy and a log-injection
|
|
252
|
+
// vector.
|
|
253
|
+
if (isFts5SyntaxError(error)) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
225
258
|
|
|
226
259
|
return results.rows.map((row) => ({
|
|
227
260
|
collection,
|
|
@@ -229,11 +262,51 @@ async function searchSingleCollection(
|
|
|
229
262
|
slug: row.slug,
|
|
230
263
|
locale: row.locale,
|
|
231
264
|
title: row.title ?? undefined,
|
|
232
|
-
snippet
|
|
265
|
+
// SQLite's snippet() returns NULL when the targeted column is
|
|
266
|
+
// NULL for that row — even if the row matched via a different
|
|
267
|
+
// searchable column. Skip sanitization in that case so we don't
|
|
268
|
+
// throw on `null.replace`. The SearchResult.snippet field is
|
|
269
|
+
// already optional, so omitting it is the documented contract.
|
|
270
|
+
snippet: row.snippet === null ? undefined : sanitizeSnippet(row.snippet),
|
|
233
271
|
score: Math.abs(row.score), // bm25 returns negative scores
|
|
234
272
|
}));
|
|
235
273
|
}
|
|
236
274
|
|
|
275
|
+
// Module-scope regexes so the engine doesn't recompile per call —
|
|
276
|
+
// snippet sanitization runs on every search result.
|
|
277
|
+
const SNIPPET_AMP_RE = /&/g;
|
|
278
|
+
const SNIPPET_LT_RE = /</g;
|
|
279
|
+
const SNIPPET_GT_RE = />/g;
|
|
280
|
+
const SNIPPET_QUOT_RE = /"/g;
|
|
281
|
+
const SNIPPET_APOS_RE = /'/g;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Make an FTS5 snippet safe to render with `set:html` / `innerHTML`.
|
|
285
|
+
*
|
|
286
|
+
* SQLite's `snippet()` function splices literal `<mark>` and `</mark>`
|
|
287
|
+
* markers around matched terms but does not escape the surrounding
|
|
288
|
+
* source text. Posts that legitimately contain `<`, `>`, `&`, `"` or
|
|
289
|
+
* `'` would render as broken markup, and a `<script>` literal in a
|
|
290
|
+
* title (or any other indexed field) would execute when displayed.
|
|
291
|
+
*
|
|
292
|
+
* The fix: HTML-escape the whole string, which turns the markers into
|
|
293
|
+
* `<mark>` / `</mark>`. Then restore those two patterns to
|
|
294
|
+
* their original tag form. The result is "the indexed text with all
|
|
295
|
+
* HTML metacharacters escaped, plus a small set of literal `<mark>`
|
|
296
|
+
* highlight tags around matched terms" — which matches the API's
|
|
297
|
+
* documented contract.
|
|
298
|
+
*/
|
|
299
|
+
function sanitizeSnippet(snippet: string): string {
|
|
300
|
+
return snippet
|
|
301
|
+
.replace(SNIPPET_AMP_RE, "&")
|
|
302
|
+
.replace(SNIPPET_LT_RE, "<")
|
|
303
|
+
.replace(SNIPPET_GT_RE, ">")
|
|
304
|
+
.replace(SNIPPET_QUOT_RE, """)
|
|
305
|
+
.replace(SNIPPET_APOS_RE, "'")
|
|
306
|
+
.replaceAll("<mark>", "<mark>")
|
|
307
|
+
.replaceAll("</mark>", "</mark>");
|
|
308
|
+
}
|
|
309
|
+
|
|
237
310
|
/**
|
|
238
311
|
* Get search suggestions for autocomplete
|
|
239
312
|
*
|
|
@@ -282,23 +355,35 @@ export async function getSuggestions(
|
|
|
282
355
|
continue;
|
|
283
356
|
}
|
|
284
357
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
358
|
+
let results;
|
|
359
|
+
try {
|
|
360
|
+
results = await sql<{
|
|
361
|
+
id: string;
|
|
362
|
+
title: string;
|
|
363
|
+
}>`
|
|
364
|
+
SELECT
|
|
365
|
+
c.id,
|
|
366
|
+
c.title
|
|
367
|
+
FROM "${sql.raw(ftsTable)}" f
|
|
368
|
+
JOIN "${sql.raw(contentTable)}" c ON f.id = c.id
|
|
369
|
+
WHERE "${sql.raw(ftsTable)}" MATCH ${prefixQuery}
|
|
370
|
+
AND c.status = 'published'
|
|
371
|
+
AND c.deleted_at IS NULL
|
|
372
|
+
AND c.title IS NOT NULL
|
|
373
|
+
${locale ? sql`AND c.locale = ${locale}` : sql``}
|
|
374
|
+
ORDER BY bm25("${sql.raw(ftsTable)}")
|
|
375
|
+
LIMIT ${limit}
|
|
376
|
+
`.execute(db);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
// Same swallow as searchSingleCollection: malformed prefix
|
|
379
|
+
// queries should yield no suggestions, not surface DB errors.
|
|
380
|
+
// Intentionally not logged (anonymous-triggerable, echoes
|
|
381
|
+
// user input -- see searchSingleCollection for rationale).
|
|
382
|
+
if (isFts5SyntaxError(error)) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
302
387
|
|
|
303
388
|
for (const row of results.rows) {
|
|
304
389
|
suggestions.push({
|
package/src/search/types.ts
CHANGED
|
@@ -58,7 +58,14 @@ export interface SearchResult {
|
|
|
58
58
|
locale: string;
|
|
59
59
|
/** Entry title (if available) */
|
|
60
60
|
title?: string;
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Highlighted snippet showing match context.
|
|
63
|
+
*
|
|
64
|
+
* Sanitized server-side to be safe for `set:html` / `innerHTML`:
|
|
65
|
+
* all HTML metacharacters in the source text are escaped, and
|
|
66
|
+
* matched terms are wrapped in literal `<mark>...</mark>` tags
|
|
67
|
+
* (the only HTML the snippet is allowed to contain).
|
|
68
|
+
*/
|
|
62
69
|
snippet?: string;
|
|
63
70
|
/** Relevance score (higher = more relevant) */
|
|
64
71
|
score: number;
|
package/src/sections/index.ts
CHANGED
|
@@ -137,17 +137,15 @@ export async function getSectionsWithDb(
|
|
|
137
137
|
// Order by title ASC, id ASC for stable cursor pagination
|
|
138
138
|
query = query.orderBy("title", "asc").orderBy("id", "asc");
|
|
139
139
|
|
|
140
|
-
// Cursor-based pagination
|
|
140
|
+
// Cursor-based pagination — throws on invalid cursor.
|
|
141
141
|
if (options.cursor) {
|
|
142
142
|
const decoded = decodeCursor(options.cursor);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
eb.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
);
|
|
150
|
-
}
|
|
143
|
+
query = query.where((eb) =>
|
|
144
|
+
eb.or([
|
|
145
|
+
eb("title", ">", decoded.orderValue),
|
|
146
|
+
eb.and([eb("title", "=", decoded.orderValue), eb("id", ">", decoded.id)]),
|
|
147
|
+
]),
|
|
148
|
+
);
|
|
151
149
|
}
|
|
152
150
|
|
|
153
151
|
query = query.limit(limit + 1);
|
package/src/storage/s3.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
S3Client,
|
|
10
|
+
type S3ClientConfig,
|
|
10
11
|
PutObjectCommand,
|
|
11
12
|
GetObjectCommand,
|
|
12
13
|
DeleteObjectCommand,
|
|
@@ -131,9 +132,14 @@ export class S3Storage implements Storage {
|
|
|
131
132
|
this.publicUrl = config.publicUrl;
|
|
132
133
|
this.endpoint = config.endpoint;
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
// S3ClientConfig types `credentials` as required, but the SDK accepts
|
|
136
|
+
// omitted credentials at runtime (falls back to the provider chain).
|
|
137
|
+
/* eslint-disable typescript-eslint(no-unsafe-type-assertion) -- upstream @aws-sdk/client-s3 overstates required fields */
|
|
138
|
+
const clientConfig = {
|
|
135
139
|
endpoint: config.endpoint,
|
|
136
140
|
region: config.region || "auto",
|
|
141
|
+
// Required for R2 and some S3-compatible services
|
|
142
|
+
forcePathStyle: true,
|
|
137
143
|
...(config.accessKeyId && config.secretAccessKey
|
|
138
144
|
? {
|
|
139
145
|
credentials: {
|
|
@@ -142,9 +148,9 @@ export class S3Storage implements Storage {
|
|
|
142
148
|
},
|
|
143
149
|
}
|
|
144
150
|
: {}),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
} as S3ClientConfig;
|
|
152
|
+
/* eslint-enable typescript-eslint(no-unsafe-type-assertion) */
|
|
153
|
+
this.client = new S3Client(clientConfig);
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
async upload(options: {
|
|
@@ -317,8 +323,8 @@ export class S3Storage implements Storage {
|
|
|
317
323
|
if (this.publicUrl) {
|
|
318
324
|
return `${this.publicUrl.replace(TRAILING_SLASH_PATTERN, "")}/${key}`;
|
|
319
325
|
}
|
|
320
|
-
//
|
|
321
|
-
return
|
|
326
|
+
// No public URL configured; defer to the /_emdash/api/media/file route.
|
|
327
|
+
return `/_emdash/api/media/file/${key}`;
|
|
322
328
|
}
|
|
323
329
|
}
|
|
324
330
|
|
package/src/virtual-modules.d.ts
CHANGED
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
|
|
8
8
|
declare module "virtual:emdash/config" {
|
|
9
9
|
import type { I18nConfig } from "./i18n/config.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
AuthDescriptor,
|
|
12
|
+
AuthProviderDescriptor,
|
|
13
|
+
DatabaseDescriptor,
|
|
14
|
+
StorageDescriptor,
|
|
15
|
+
} from "./index.js";
|
|
11
16
|
|
|
12
17
|
interface VirtualConfig {
|
|
13
18
|
database?: DatabaseDescriptor;
|
|
14
19
|
storage?: StorageDescriptor;
|
|
15
20
|
auth?: AuthDescriptor;
|
|
21
|
+
authProviders?: AuthProviderDescriptor[];
|
|
16
22
|
i18n?: I18nConfig | null;
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -103,6 +109,20 @@ declare module "virtual:emdash/block-components" {
|
|
|
103
109
|
export const pluginBlockComponents: Record<string, unknown>;
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
declare module "virtual:emdash/auth-providers" {
|
|
113
|
+
import type { ComponentType } from "react";
|
|
114
|
+
|
|
115
|
+
interface AuthProviderEntry {
|
|
116
|
+
id: string;
|
|
117
|
+
label: string;
|
|
118
|
+
LoginButton?: ComponentType;
|
|
119
|
+
LoginForm?: ComponentType;
|
|
120
|
+
SetupStep?: ComponentType<{ onComplete: () => void }>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const authProviders: Record<string, AuthProviderEntry>;
|
|
124
|
+
}
|
|
125
|
+
|
|
106
126
|
declare module "virtual:emdash/wait-until" {
|
|
107
127
|
/**
|
|
108
128
|
* Optional host-provided lifetime extender for work deferred past the
|
package/src/widgets/index.ts
CHANGED
|
@@ -125,7 +125,7 @@ export function getWidgetComponents(): WidgetComponentDef[] {
|
|
|
125
125
|
/**
|
|
126
126
|
* Convert a widget row to the API type
|
|
127
127
|
*/
|
|
128
|
-
function rowToWidget(row: WidgetRow): Widget {
|
|
128
|
+
export function rowToWidget(row: WidgetRow): Widget {
|
|
129
129
|
const widget: Widget = {
|
|
130
130
|
id: row.id,
|
|
131
131
|
type: row.type,
|