emdash 0.6.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-B4MsLM-w.mjs → apply-x0eMK1lX.mjs} +186 -28
- 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 +92 -17
- 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 +7 -2
- 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 +263 -74
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -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 +17 -13
- 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-BsBoyj8G.mjs → content-BcQPYxdV.mjs} +39 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +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 → 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-BYv0mB9g.d.mts → index-DIb-CzNx.d.mts} +232 -15
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- 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-Bk_3vKvU.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-Bk_3vKvU.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-D_pshWdf.mjs} +69 -13
- 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-Fl2NcUUz.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-Fl2NcUUz.d.mts.map → runner-OURCaApa.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 +2 -2
- package/dist/{search-DI4bM2w9.mjs → search-BoZYFuUk.mjs} +339 -102
- 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-DbrKzDju.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-DbrKzDju.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-8xrvl_68.d.mts → types-CS8FIX7L.d.mts} +10 -1
- package/dist/{types-8xrvl_68.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-CaLH1Ia2.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-CaLH1Ia2.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-Uaf2ynPX.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 +124 -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/content.ts +8 -0
- 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/font-provider.ts +3 -1
- package/src/astro/integration/index.ts +15 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +74 -2
- 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/admin.astro +14 -7
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -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/auth/passkey/options.ts +2 -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]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +20 -10
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- 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/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +4 -3
- package/src/astro/routes/api/manifest.ts +7 -0
- 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/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +38 -8
- package/src/astro/routes/api/setup/index.ts +7 -4
- 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 +18 -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/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -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/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- 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 +79 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +123 -24
- 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/database/types.ts +9 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +753 -107
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/manifest-schema.ts +12 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- 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/seed/apply.ts +26 -0
- package/src/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-B4MsLM-w.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-BsBoyj8G.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-BYv0mB9g.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-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-DI4bM2w9.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-Uaf2ynPX.mjs +0 -7
package/src/auth/types.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
* Auth Provider Types
|
|
3
3
|
*
|
|
4
4
|
* Defines the interfaces for pluggable authentication providers.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Two systems coexist:
|
|
7
|
+
* - `AuthDescriptor` — transparent auth (Cloudflare Access) that authenticates
|
|
8
|
+
* every request via headers/cookies. No login UI needed.
|
|
9
|
+
* - `AuthProviderDescriptor` — pluggable login methods (GitHub, Google,
|
|
10
|
+
* AT Protocol, etc.) that appear as options on the login page and setup
|
|
11
|
+
* wizard. Passkey is built-in; providers are additive.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
/**
|
|
@@ -22,10 +28,10 @@ export interface AuthResult {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
|
-
* Auth descriptor
|
|
31
|
+
* Auth descriptor — transparent auth providers (e.g., Cloudflare Access).
|
|
26
32
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
33
|
+
* These authenticate every request via headers/cookies. No login UI needed.
|
|
34
|
+
* The module's `authenticate()` function is called by middleware on each request.
|
|
29
35
|
*/
|
|
30
36
|
export interface AuthDescriptor {
|
|
31
37
|
/**
|
|
@@ -64,6 +70,110 @@ export interface AuthProviderModule {
|
|
|
64
70
|
authenticate(request: Request, config: unknown): Promise<AuthResult>;
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Pluggable Auth Providers (additive login methods)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Descriptor for a pluggable auth provider.
|
|
79
|
+
*
|
|
80
|
+
* Auth providers appear as login options on the login page and setup wizard.
|
|
81
|
+
* They coexist with passkey (which is built-in) and with each other.
|
|
82
|
+
* Any provider can be used to create the initial admin account.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* // astro.config.ts
|
|
87
|
+
* import { atproto } from "@emdash-cms/auth-atproto";
|
|
88
|
+
*
|
|
89
|
+
* emdash({
|
|
90
|
+
* authProviders: [atproto(), github(), google()],
|
|
91
|
+
* })
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export interface AuthProviderDescriptor {
|
|
95
|
+
/** Unique provider ID (e.g., "github", "atproto") */
|
|
96
|
+
id: string;
|
|
97
|
+
|
|
98
|
+
/** Human-readable label for UI (e.g., "GitHub", "AT Protocol") */
|
|
99
|
+
label: string;
|
|
100
|
+
|
|
101
|
+
/** Provider-specific config (JSON-serializable) */
|
|
102
|
+
config?: unknown;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Module exporting React components for the admin UI.
|
|
106
|
+
* Statically imported at build time via virtual module.
|
|
107
|
+
*
|
|
108
|
+
* The module should export components matching `AuthProviderAdminExports`.
|
|
109
|
+
*/
|
|
110
|
+
adminEntry?: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Astro route handlers this provider needs injected at build time.
|
|
114
|
+
* Used for login initiation, OAuth callbacks, well-known endpoints, etc.
|
|
115
|
+
*/
|
|
116
|
+
routes?: AuthRouteDescriptor[];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* URL prefixes/paths that should bypass auth middleware.
|
|
120
|
+
* Added to the public routes set so login/callback endpoints work
|
|
121
|
+
* for unauthenticated users.
|
|
122
|
+
*/
|
|
123
|
+
publicRoutes?: string[];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Storage collections for persistent auth state (e.g., OAuth sessions).
|
|
127
|
+
* Same format as plugin storage — collections are stored in the shared
|
|
128
|
+
* `_plugin_storage` table namespaced under `auth:<providerId>`.
|
|
129
|
+
*
|
|
130
|
+
* Access via `getAuthProviderStorage()` from `emdash/api/route-utils`.
|
|
131
|
+
*/
|
|
132
|
+
storage?: Record<
|
|
133
|
+
string,
|
|
134
|
+
{ indexes?: Array<string | string[]>; uniqueIndexes?: Array<string | string[]> }
|
|
135
|
+
>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A route that an auth provider needs injected into the Astro app.
|
|
140
|
+
*/
|
|
141
|
+
export interface AuthRouteDescriptor {
|
|
142
|
+
/** URL pattern (e.g., "/_emdash/api/auth/atproto/login") */
|
|
143
|
+
pattern: string;
|
|
144
|
+
/** Module specifier for the Astro route handler */
|
|
145
|
+
entrypoint: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Expected exports from an auth provider's `adminEntry` module.
|
|
150
|
+
*
|
|
151
|
+
* All exports are optional. Providers export whichever components
|
|
152
|
+
* make sense for their auth flow.
|
|
153
|
+
*/
|
|
154
|
+
export interface AuthProviderAdminExports {
|
|
155
|
+
/**
|
|
156
|
+
* Compact button for the login page (icon + label).
|
|
157
|
+
* Used for providers with a simple redirect flow (GitHub, Google).
|
|
158
|
+
* Rendered in the "Or continue with" section.
|
|
159
|
+
*/
|
|
160
|
+
LoginButton?: import("react").ComponentType;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Full login form for providers that need custom input.
|
|
164
|
+
* Used for providers like AT Protocol that need a handle field.
|
|
165
|
+
* Rendered as an expandable section on the login page.
|
|
166
|
+
*/
|
|
167
|
+
LoginForm?: import("react").ComponentType;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Setup wizard step for creating the admin account via this provider.
|
|
171
|
+
* When present, this provider appears as an option in the setup wizard's
|
|
172
|
+
* "Create admin account" step.
|
|
173
|
+
*/
|
|
174
|
+
SetupStep?: import("react").ComponentType<{ onComplete: () => void }>;
|
|
175
|
+
}
|
|
176
|
+
|
|
67
177
|
/**
|
|
68
178
|
* Configuration options common to external auth providers
|
|
69
179
|
*/
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
ICON_SIZE,
|
|
39
39
|
} from "./bundle-utils.js";
|
|
40
40
|
|
|
41
|
-
const TS_EXT_RE = /\.tsx
|
|
41
|
+
const TS_EXT_RE = /\.(tsx?|[mc]?js)$/;
|
|
42
42
|
const SLASH_RE = /\//g;
|
|
43
43
|
const LEADING_AT_RE = /^@/;
|
|
44
44
|
const emdash_SCOPE_RE = /^@emdash-cms\//;
|
|
@@ -163,6 +163,8 @@ export const bundleCommand = defineCommand({
|
|
|
163
163
|
const tmpDir = join(pluginDir, ".emdash-bundle-tmp");
|
|
164
164
|
|
|
165
165
|
try {
|
|
166
|
+
// Clean up any stale temp directory from a previous failed run
|
|
167
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
166
168
|
await mkdir(tmpDir, { recursive: true });
|
|
167
169
|
|
|
168
170
|
// Build main entry to extract manifest.
|
|
@@ -20,6 +20,7 @@ import type { MediaValue } from "../fields/types.js";
|
|
|
20
20
|
import type { HTMLAttributes } from "astro/types";
|
|
21
21
|
import type { ImageEmbed } from "../media/types.js";
|
|
22
22
|
import { getMediaProvider } from "../media/provider-loader.js";
|
|
23
|
+
import { buildRenderMediaUrl } from "../media/url.js";
|
|
23
24
|
// Standard responsive breakpoints
|
|
24
25
|
const BREAKPOINTS = [640, 750, 828, 960, 1080, 1280, 1600, 1920];
|
|
25
26
|
|
|
@@ -53,14 +54,14 @@ function normalizeImage(
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* Build the URL for a local image
|
|
57
|
+
* Build the URL for a local image. Prefers `meta.storageKey`; falls back to
|
|
58
|
+
* the internal proxy with `img.id` when no storage key is available.
|
|
57
59
|
*/
|
|
58
60
|
function buildLocalImageUrl(img: MediaValue): string {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
return "";
|
|
61
|
+
return buildRenderMediaUrl(Astro.locals.emdash?.getPublicMediaUrl, {
|
|
62
|
+
storageKey: img.meta?.storageKey as string | undefined,
|
|
63
|
+
id: img.id,
|
|
64
|
+
});
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Uses Astro's Image component for optimization when dimensions are available.
|
|
7
7
|
*/
|
|
8
8
|
import { Image as AstroImage } from "astro:assets";
|
|
9
|
+
import { buildRenderMediaUrl } from "../media/url.js";
|
|
9
10
|
|
|
10
11
|
export interface Props {
|
|
11
12
|
node: {
|
|
@@ -39,9 +40,10 @@ if (!images.length) {
|
|
|
39
40
|
<div class="emdash-gallery" style={`--columns: ${columns}`}>
|
|
40
41
|
{
|
|
41
42
|
images.map((image) => {
|
|
42
|
-
const src =
|
|
43
|
-
image.asset.url
|
|
44
|
-
|
|
43
|
+
const src = buildRenderMediaUrl(Astro.locals.emdash?.getPublicMediaUrl, {
|
|
44
|
+
url: image.asset.url,
|
|
45
|
+
id: image.asset._ref,
|
|
46
|
+
});
|
|
45
47
|
const hasSize = image.width && image.height;
|
|
46
48
|
return (
|
|
47
49
|
<figure class="emdash-gallery-item">
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ImageEmbed } from "../media/types.js";
|
|
9
9
|
import { getMediaProvider } from "../media/provider-loader.js";
|
|
10
|
+
import { buildRenderMediaUrl } from "../media/url.js";
|
|
10
11
|
// Standard responsive breakpoints
|
|
11
12
|
const BREAKPOINTS = [640, 750, 828, 960, 1080, 1280, 1600, 1920];
|
|
12
13
|
|
|
@@ -122,10 +123,14 @@ if (providerId && providerId !== "local") {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
// Fallback for local provider
|
|
126
|
-
//
|
|
126
|
+
// Fallback for local provider. `asset.url` carries the storage key with
|
|
127
|
+
// extension when present; `asset._ref` is a bare ULID that only the internal
|
|
128
|
+
// `/file/{id}` route can resolve. `buildRenderMediaUrl` picks the right shape.
|
|
127
129
|
if (!src) {
|
|
128
|
-
src =
|
|
130
|
+
src = buildRenderMediaUrl(Astro.locals.emdash?.getPublicMediaUrl, {
|
|
131
|
+
url: asset.url,
|
|
132
|
+
id: asset._ref,
|
|
133
|
+
});
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
// Build placeholder background style
|
|
@@ -1741,6 +1741,7 @@ export function InlinePortableTextEditor({
|
|
|
1741
1741
|
editorProps: {
|
|
1742
1742
|
attributes: {
|
|
1743
1743
|
class: "prose prose-sm sm:prose-base dark:prose-invert max-w-none emdash-inline-editor",
|
|
1744
|
+
dir: "auto",
|
|
1744
1745
|
},
|
|
1745
1746
|
},
|
|
1746
1747
|
onUpdate: () => {
|
|
@@ -1795,7 +1796,7 @@ export function InlinePortableTextEditor({
|
|
|
1795
1796
|
// Don't save if focus moved to the slash menu (portalled to body)
|
|
1796
1797
|
if (related?.closest(".emdash-slash-menu")) return;
|
|
1797
1798
|
if (related?.closest(".emdash-media-picker")) return;
|
|
1798
|
-
save();
|
|
1799
|
+
void save();
|
|
1799
1800
|
},
|
|
1800
1801
|
[save, mediaPickerOpen],
|
|
1801
1802
|
);
|
|
@@ -109,13 +109,6 @@ const config = {
|
|
|
109
109
|
</emdash-live-search>
|
|
110
110
|
|
|
111
111
|
<script>
|
|
112
|
-
// Sanitization patterns for search snippets (allow only <mark> tags from FTS5)
|
|
113
|
-
const SNIPPET_AMP_RE = /&/g;
|
|
114
|
-
const SNIPPET_LT_RE = /</g;
|
|
115
|
-
const SNIPPET_GT_RE = />/g;
|
|
116
|
-
const SNIPPET_MARK_OPEN_RE = /<mark>/g;
|
|
117
|
-
const SNIPPET_MARK_CLOSE_RE = /<\/mark>/g;
|
|
118
|
-
|
|
119
112
|
interface SearchResult {
|
|
120
113
|
collection: string;
|
|
121
114
|
id: string;
|
|
@@ -396,17 +389,15 @@ const config = {
|
|
|
396
389
|
collectionEl.textContent = result.collection;
|
|
397
390
|
}
|
|
398
391
|
|
|
399
|
-
//
|
|
392
|
+
// Snippets returned by /api/search are already sanitised
|
|
393
|
+
// server-side by sanitizeSnippet() — they contain only
|
|
394
|
+
// HTML-escaped text plus literal <mark>...</mark> tags
|
|
395
|
+
// around matched terms.
|
|
400
396
|
const snippetEl = link.querySelector(
|
|
401
397
|
".emdash-live-search-result-snippet"
|
|
402
398
|
);
|
|
403
399
|
if (snippetEl && this.config.showSnippets && result.snippet) {
|
|
404
|
-
snippetEl.innerHTML = result.snippet
|
|
405
|
-
.replace(SNIPPET_AMP_RE, "&")
|
|
406
|
-
.replace(SNIPPET_LT_RE, "<")
|
|
407
|
-
.replace(SNIPPET_GT_RE, ">")
|
|
408
|
-
.replace(SNIPPET_MARK_OPEN_RE, "<mark>")
|
|
409
|
-
.replace(SNIPPET_MARK_CLOSE_RE, "</mark>");
|
|
400
|
+
snippetEl.innerHTML = result.snippet;
|
|
410
401
|
} else if (snippetEl) {
|
|
411
402
|
snippetEl.remove();
|
|
412
403
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Migration: Bounded 404 logging
|
|
6
|
+
*
|
|
7
|
+
* Hardens `_emdash_404_log` against unauthenticated DoS. Previously every 404
|
|
8
|
+
* inserted a new row, so an attacker could grow the table without bound.
|
|
9
|
+
*
|
|
10
|
+
* Changes:
|
|
11
|
+
* - Adds `hits` (default 1, NOT NULL)
|
|
12
|
+
* - Adds `last_seen_at` (nullable; SQLite can't add NOT NULL with a
|
|
13
|
+
* non-constant default to a populated table, so the column is nullable
|
|
14
|
+
* at the schema level and backfilled from `created_at` for existing rows;
|
|
15
|
+
* new inserts via `log404` always set it)
|
|
16
|
+
* - Deduplicates existing rows by path, keeping the most recent row per
|
|
17
|
+
* path and summing hits
|
|
18
|
+
* - Adds a UNIQUE index on `path` so upsert semantics work
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
22
|
+
// 1. Add columns.
|
|
23
|
+
await db.schema
|
|
24
|
+
.alterTable("_emdash_404_log")
|
|
25
|
+
.addColumn("hits", "integer", (col) => col.notNull().defaultTo(1))
|
|
26
|
+
.execute();
|
|
27
|
+
|
|
28
|
+
// SQLite won't accept a non-constant default when adding a NOT NULL column
|
|
29
|
+
// to a table with existing rows, so backfill in two steps: add nullable,
|
|
30
|
+
// populate, then rely on the application layer / future inserts to set it.
|
|
31
|
+
await db.schema.alterTable("_emdash_404_log").addColumn("last_seen_at", "text").execute();
|
|
32
|
+
|
|
33
|
+
// Backfill last_seen_at from created_at for existing rows.
|
|
34
|
+
await sql`
|
|
35
|
+
UPDATE _emdash_404_log
|
|
36
|
+
SET last_seen_at = created_at
|
|
37
|
+
WHERE last_seen_at IS NULL
|
|
38
|
+
`.execute(db);
|
|
39
|
+
|
|
40
|
+
// 2. Deduplicate existing rows by path.
|
|
41
|
+
// For each path, roll up hits and pick the freshest last_seen_at onto
|
|
42
|
+
// a single keeper row, then delete the non-keepers. Uses window
|
|
43
|
+
// functions (ROW_NUMBER) so the dedup SQL is valid on both SQLite
|
|
44
|
+
// (3.25+, 2018) and Postgres. The previous GROUP BY approach was
|
|
45
|
+
// accepted by SQLite but invalid on Postgres because `id` wasn't in
|
|
46
|
+
// the GROUP BY or wrapped in an aggregate.
|
|
47
|
+
await sql`
|
|
48
|
+
WITH ranked AS (
|
|
49
|
+
SELECT
|
|
50
|
+
id,
|
|
51
|
+
path,
|
|
52
|
+
ROW_NUMBER() OVER (
|
|
53
|
+
PARTITION BY path
|
|
54
|
+
ORDER BY created_at DESC, id DESC
|
|
55
|
+
) AS rn,
|
|
56
|
+
COUNT(*) OVER (PARTITION BY path) AS path_count,
|
|
57
|
+
MAX(created_at) OVER (PARTITION BY path) AS latest_created_at
|
|
58
|
+
FROM _emdash_404_log
|
|
59
|
+
)
|
|
60
|
+
UPDATE _emdash_404_log
|
|
61
|
+
SET
|
|
62
|
+
hits = (SELECT path_count FROM ranked WHERE ranked.id = _emdash_404_log.id),
|
|
63
|
+
last_seen_at = (SELECT latest_created_at FROM ranked WHERE ranked.id = _emdash_404_log.id)
|
|
64
|
+
WHERE id IN (SELECT id FROM ranked WHERE rn = 1)
|
|
65
|
+
`.execute(db);
|
|
66
|
+
|
|
67
|
+
// Delete the non-keepers (every row except the freshest per path).
|
|
68
|
+
await sql`
|
|
69
|
+
DELETE FROM _emdash_404_log
|
|
70
|
+
WHERE id IN (
|
|
71
|
+
SELECT id FROM (
|
|
72
|
+
SELECT
|
|
73
|
+
id,
|
|
74
|
+
ROW_NUMBER() OVER (
|
|
75
|
+
PARTITION BY path
|
|
76
|
+
ORDER BY created_at DESC, id DESC
|
|
77
|
+
) AS rn
|
|
78
|
+
FROM _emdash_404_log
|
|
79
|
+
) AS ranked
|
|
80
|
+
WHERE rn > 1
|
|
81
|
+
)
|
|
82
|
+
`.execute(db);
|
|
83
|
+
|
|
84
|
+
// 3. Add unique index on path for upsert semantics.
|
|
85
|
+
await db.schema
|
|
86
|
+
.createIndex("idx_404_log_path_unique")
|
|
87
|
+
.on("_emdash_404_log")
|
|
88
|
+
.column("path")
|
|
89
|
+
.unique()
|
|
90
|
+
.execute();
|
|
91
|
+
|
|
92
|
+
// Drop the old non-unique index; the unique one covers the same lookups.
|
|
93
|
+
await db.schema.dropIndex("idx_404_log_path").execute();
|
|
94
|
+
|
|
95
|
+
// 4. Index on last_seen_at for eviction ordering.
|
|
96
|
+
await db.schema
|
|
97
|
+
.createIndex("idx_404_log_last_seen")
|
|
98
|
+
.on("_emdash_404_log")
|
|
99
|
+
.column("last_seen_at")
|
|
100
|
+
.execute();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
104
|
+
await db.schema.dropIndex("idx_404_log_last_seen").execute();
|
|
105
|
+
await db.schema.dropIndex("idx_404_log_path_unique").execute();
|
|
106
|
+
|
|
107
|
+
// Restore the original non-unique path index.
|
|
108
|
+
await db.schema.createIndex("idx_404_log_path").on("_emdash_404_log").column("path").execute();
|
|
109
|
+
|
|
110
|
+
await db.schema.alterTable("_emdash_404_log").dropColumn("last_seen_at").execute();
|
|
111
|
+
await db.schema.alterTable("_emdash_404_log").dropColumn("hits").execute();
|
|
112
|
+
}
|
|
@@ -35,6 +35,7 @@ import * as m031 from "./031_bylines.js";
|
|
|
35
35
|
import * as m032 from "./032_rate_limits.js";
|
|
36
36
|
import * as m033 from "./033_optimize_content_indexes.js";
|
|
37
37
|
import * as m034 from "./034_published_at_index.js";
|
|
38
|
+
import * as m035 from "./035_bounded_404_log.js";
|
|
38
39
|
|
|
39
40
|
const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
|
|
40
41
|
"001_initial": m001,
|
|
@@ -70,6 +71,7 @@ const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
|
|
|
70
71
|
"032_rate_limits": m032,
|
|
71
72
|
"033_optimize_content_indexes": m033,
|
|
72
73
|
"034_published_at_index": m034,
|
|
74
|
+
"035_bounded_404_log": m035,
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
/** Total number of registered migrations. Exported for use in tests. */
|
|
@@ -143,14 +143,12 @@ export class AuditRepository {
|
|
|
143
143
|
|
|
144
144
|
if (query.cursor) {
|
|
145
145
|
const decoded = decodeCursor(query.cursor);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
eb.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
}
|
|
146
|
+
q = q.where((eb) =>
|
|
147
|
+
eb.or([
|
|
148
|
+
eb("timestamp", "<", decoded.orderValue),
|
|
149
|
+
eb.and([eb("timestamp", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
|
|
150
|
+
]),
|
|
151
|
+
);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
const rows = await q.execute();
|
|
@@ -123,14 +123,12 @@ export class BylineRepository {
|
|
|
123
123
|
|
|
124
124
|
if (options?.cursor) {
|
|
125
125
|
const decoded = decodeCursor(options.cursor);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
eb.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
133
|
-
}
|
|
126
|
+
query = query.where((eb) =>
|
|
127
|
+
eb.or([
|
|
128
|
+
eb("created_at", "<", decoded.orderValue),
|
|
129
|
+
eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
const rows = await query.execute();
|
|
@@ -143,14 +143,12 @@ export class CommentRepository {
|
|
|
143
143
|
// Cursor pagination (ascending by created_at)
|
|
144
144
|
if (options.cursor) {
|
|
145
145
|
const decoded = decodeCursor(options.cursor);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
eb.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
}
|
|
146
|
+
query = query.where((eb: ExpressionBuilder<Database, "_emdash_comments">) =>
|
|
147
|
+
eb.or([
|
|
148
|
+
eb("created_at", ">", decoded.orderValue),
|
|
149
|
+
eb.and([eb("created_at", "=", decoded.orderValue), eb("id", ">", decoded.id)]),
|
|
150
|
+
]),
|
|
151
|
+
);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
query = query
|
|
@@ -202,14 +200,12 @@ export class CommentRepository {
|
|
|
202
200
|
// Cursor pagination (descending by created_at)
|
|
203
201
|
if (options.cursor) {
|
|
204
202
|
const decoded = decodeCursor(options.cursor);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
eb.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
212
|
-
}
|
|
203
|
+
query = query.where((eb: ExpressionBuilder<Database, "_emdash_comments">) =>
|
|
204
|
+
eb.or([
|
|
205
|
+
eb("created_at", "<", decoded.orderValue),
|
|
206
|
+
eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
|
|
207
|
+
]),
|
|
208
|
+
);
|
|
213
209
|
}
|
|
214
210
|
|
|
215
211
|
query = query
|
|
@@ -489,27 +489,26 @@ export class ContentRepository {
|
|
|
489
489
|
query = query.where("locale" as any, "=", options.where.locale);
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
// Handle cursor pagination
|
|
492
|
+
// Handle cursor pagination — decodeCursor throws InvalidCursorError
|
|
493
|
+
// on malformed input; let it propagate so handlers surface a
|
|
494
|
+
// structured INVALID_CURSOR rather than silently returning page 1.
|
|
493
495
|
if (options.cursor) {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
eb.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
eb.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
]),
|
|
511
|
-
);
|
|
512
|
-
}
|
|
496
|
+
const { orderValue, id: cursorId } = decodeCursor(options.cursor);
|
|
497
|
+
|
|
498
|
+
if (safeOrderDirection === "DESC") {
|
|
499
|
+
query = query.where((eb) =>
|
|
500
|
+
eb.or([
|
|
501
|
+
eb(dbField as any, "<", orderValue),
|
|
502
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
|
|
503
|
+
]),
|
|
504
|
+
);
|
|
505
|
+
} else {
|
|
506
|
+
query = query.where((eb) =>
|
|
507
|
+
eb.or([
|
|
508
|
+
eb(dbField as any, ">", orderValue),
|
|
509
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
|
|
510
|
+
]),
|
|
511
|
+
);
|
|
513
512
|
}
|
|
514
513
|
}
|
|
515
514
|
|
|
@@ -671,27 +670,24 @@ export class ContentRepository {
|
|
|
671
670
|
.selectAll()
|
|
672
671
|
.where("deleted_at" as never, "is not", null);
|
|
673
672
|
|
|
674
|
-
// Handle cursor pagination
|
|
673
|
+
// Handle cursor pagination — decodeCursor throws on invalid input.
|
|
675
674
|
if (options.cursor) {
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
eb.
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
eb.
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
]),
|
|
693
|
-
);
|
|
694
|
-
}
|
|
675
|
+
const { orderValue, id: cursorId } = decodeCursor(options.cursor);
|
|
676
|
+
|
|
677
|
+
if (safeOrderDirection === "DESC") {
|
|
678
|
+
query = query.where((eb) =>
|
|
679
|
+
eb.or([
|
|
680
|
+
eb(dbField as any, "<", orderValue),
|
|
681
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
|
|
682
|
+
]),
|
|
683
|
+
);
|
|
684
|
+
} else {
|
|
685
|
+
query = query.where((eb) =>
|
|
686
|
+
eb.or([
|
|
687
|
+
eb(dbField as any, ">", orderValue),
|
|
688
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
|
|
689
|
+
]),
|
|
690
|
+
);
|
|
695
691
|
}
|
|
696
692
|
}
|
|
697
693
|
|
|
@@ -1018,6 +1014,7 @@ export class ContentRepository {
|
|
|
1018
1014
|
UPDATE ${sql.ref(tableName)}
|
|
1019
1015
|
SET live_revision_id = NULL,
|
|
1020
1016
|
status = 'draft',
|
|
1017
|
+
published_at = NULL,
|
|
1021
1018
|
updated_at = ${now}
|
|
1022
1019
|
WHERE id = ${id}
|
|
1023
1020
|
AND deleted_at IS NULL
|
|
@@ -1031,6 +1028,45 @@ export class ContentRepository {
|
|
|
1031
1028
|
return updated;
|
|
1032
1029
|
}
|
|
1033
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* Set the draft revision pointer for a content item.
|
|
1033
|
+
*
|
|
1034
|
+
* Used by seed/import paths that stage a new revision's data before
|
|
1035
|
+
* promoting it to live via `publish()`.
|
|
1036
|
+
*
|
|
1037
|
+
* Validates that the content item exists and is not soft-deleted, that
|
|
1038
|
+
* the revision exists, and that the revision belongs to the same
|
|
1039
|
+
* collection and entry. Without these checks, a caller could leave the
|
|
1040
|
+
* content row pointing at a missing or unrelated revision.
|
|
1041
|
+
*/
|
|
1042
|
+
async setDraftRevision(type: string, id: string, revisionId: string): Promise<void> {
|
|
1043
|
+
const tableName = getTableName(type);
|
|
1044
|
+
const now = new Date().toISOString();
|
|
1045
|
+
|
|
1046
|
+
const existing = await this.findById(type, id);
|
|
1047
|
+
if (!existing) {
|
|
1048
|
+
throw new EmDashValidationError("Content item not found");
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
1052
|
+
const revision = await revisionRepo.findById(revisionId);
|
|
1053
|
+
if (!revision) {
|
|
1054
|
+
throw new EmDashValidationError("Revision not found");
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (revision.collection !== type || revision.entryId !== id) {
|
|
1058
|
+
throw new EmDashValidationError("Revision does not belong to the specified content item");
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
await sql`
|
|
1062
|
+
UPDATE ${sql.ref(tableName)}
|
|
1063
|
+
SET draft_revision_id = ${revisionId},
|
|
1064
|
+
updated_at = ${now}
|
|
1065
|
+
WHERE id = ${id}
|
|
1066
|
+
AND deleted_at IS NULL
|
|
1067
|
+
`.execute(this.db);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1034
1070
|
/**
|
|
1035
1071
|
* Discard pending draft changes
|
|
1036
1072
|
*
|
|
@@ -1159,7 +1195,10 @@ export class ContentRepository {
|
|
|
1159
1195
|
scheduledAt: "scheduled_at",
|
|
1160
1196
|
deletedAt: "deleted_at",
|
|
1161
1197
|
title: "title",
|
|
1198
|
+
name: "name",
|
|
1162
1199
|
slug: "slug",
|
|
1200
|
+
status: "status",
|
|
1201
|
+
locale: "locale",
|
|
1163
1202
|
};
|
|
1164
1203
|
|
|
1165
1204
|
const mapped = mapping[field];
|
|
@@ -27,4 +27,4 @@ export { RedirectRepository } from "./redirect.js";
|
|
|
27
27
|
export { BylineRepository } from "./byline.js";
|
|
28
28
|
export type { CreateBylineInput, UpdateBylineInput, ContentBylineInput } from "./byline.js";
|
|
29
29
|
export type * from "./types.js";
|
|
30
|
-
export { EmDashValidationError, encodeCursor, decodeCursor } from "./types.js";
|
|
30
|
+
export { EmDashValidationError, InvalidCursorError, encodeCursor, decodeCursor } from "./types.js";
|