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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Auth Provider
|
|
3
|
+
*
|
|
4
|
+
* Returns an AuthProviderDescriptor for GitHub OAuth login.
|
|
5
|
+
* Credentials are read from environment variables at runtime.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { github } from "emdash/auth/providers/github";
|
|
10
|
+
*
|
|
11
|
+
* emdash({
|
|
12
|
+
* authProviders: [github()],
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AuthProviderDescriptor } from "../types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configure GitHub OAuth as an auth provider.
|
|
21
|
+
*
|
|
22
|
+
* Requires `EMDASH_OAUTH_GITHUB_CLIENT_ID` and `EMDASH_OAUTH_GITHUB_CLIENT_SECRET`
|
|
23
|
+
* (or `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`) environment variables.
|
|
24
|
+
*/
|
|
25
|
+
export function github(): AuthProviderDescriptor {
|
|
26
|
+
return {
|
|
27
|
+
id: "github",
|
|
28
|
+
label: "GitHub",
|
|
29
|
+
adminEntry: "emdash/auth/providers/github-admin",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google 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 GoogleIcon({ className }: { className?: string }) {
|
|
11
|
+
return (
|
|
12
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
13
|
+
<path
|
|
14
|
+
fill="#4285F4"
|
|
15
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
16
|
+
/>
|
|
17
|
+
<path
|
|
18
|
+
fill="#34A853"
|
|
19
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
fill="#FBBC05"
|
|
23
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
24
|
+
/>
|
|
25
|
+
<path
|
|
26
|
+
fill="#EA4335"
|
|
27
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function LoginButton() {
|
|
34
|
+
return (
|
|
35
|
+
<LinkButton
|
|
36
|
+
href="/_emdash/api/auth/oauth/google"
|
|
37
|
+
variant="outline"
|
|
38
|
+
className="w-full justify-center"
|
|
39
|
+
>
|
|
40
|
+
<GoogleIcon className="h-5 w-5" />
|
|
41
|
+
<span>Google</span>
|
|
42
|
+
</LinkButton>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth Auth Provider
|
|
3
|
+
*
|
|
4
|
+
* Returns an AuthProviderDescriptor for Google OAuth login.
|
|
5
|
+
* Credentials are read from environment variables at runtime.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { google } from "emdash/auth/providers/google";
|
|
10
|
+
*
|
|
11
|
+
* emdash({
|
|
12
|
+
* authProviders: [google()],
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AuthProviderDescriptor } from "../types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configure Google OAuth as an auth provider.
|
|
21
|
+
*
|
|
22
|
+
* Requires `EMDASH_OAUTH_GOOGLE_CLIENT_ID` and `EMDASH_OAUTH_GOOGLE_CLIENT_SECRET`
|
|
23
|
+
* (or `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`) environment variables.
|
|
24
|
+
*/
|
|
25
|
+
export function google(): AuthProviderDescriptor {
|
|
26
|
+
return {
|
|
27
|
+
id: "google",
|
|
28
|
+
label: "Google",
|
|
29
|
+
adminEntry: "emdash/auth/providers/google-admin",
|
|
30
|
+
};
|
|
31
|
+
}
|
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
|
}
|
|
@@ -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
|
|
@@ -1198,7 +1195,10 @@ export class ContentRepository {
|
|
|
1198
1195
|
scheduledAt: "scheduled_at",
|
|
1199
1196
|
deletedAt: "deleted_at",
|
|
1200
1197
|
title: "title",
|
|
1198
|
+
name: "name",
|
|
1201
1199
|
slug: "slug",
|
|
1200
|
+
status: "status",
|
|
1201
|
+
locale: "locale",
|
|
1202
1202
|
};
|
|
1203
1203
|
|
|
1204
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";
|
|
@@ -202,20 +202,17 @@ export class MediaRepository {
|
|
|
202
202
|
.orderBy("id", "desc")
|
|
203
203
|
.limit(limit + 1);
|
|
204
204
|
|
|
205
|
-
// Handle cursor-based pagination
|
|
205
|
+
// Handle cursor-based pagination — throws on invalid cursor.
|
|
206
206
|
if (options.cursor) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
eb.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
]),
|
|
217
|
-
);
|
|
218
|
-
}
|
|
207
|
+
const { orderValue: createdAt, id: cursorId } = decodeCursor(options.cursor);
|
|
208
|
+
|
|
209
|
+
// Keyset pagination: get items where (created_at, id) < cursor
|
|
210
|
+
query = query.where((eb) =>
|
|
211
|
+
eb.or([
|
|
212
|
+
eb("created_at", "<", createdAt),
|
|
213
|
+
eb.and([eb("created_at", "=", createdAt), eb("id", "<", cursorId)]),
|
|
214
|
+
]),
|
|
215
|
+
);
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
if (options.mimeType) {
|
|
@@ -226,14 +226,12 @@ export class PluginStorageRepository<T = unknown> implements StorageCollection<T
|
|
|
226
226
|
query = query.where(({ eb }) => eb(sql.join(whereSqlParts, sql.raw("")), "=", sql.raw("1")));
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// Handle cursor-based pagination
|
|
229
|
+
// Handle cursor-based pagination — throws on invalid cursor.
|
|
230
230
|
if (cursor) {
|
|
231
231
|
const decoded = decodeCursor(cursor);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
}
|
|
232
|
+
query = query.where(({ eb }) =>
|
|
233
|
+
eb(sql`(created_at, id)`, ">", sql`(${decoded.orderValue}, ${decoded.id})`),
|
|
234
|
+
);
|
|
237
235
|
}
|
|
238
236
|
|
|
239
237
|
// Build ORDER BY using sql template
|