emdash 0.7.0 → 0.8.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.
Files changed (225) hide show
  1. package/dist/{adapters-Di31kZ28.d.mts → adapters-BKSf3T9R.d.mts} +1 -1
  2. package/dist/{adapters-Di31kZ28.d.mts.map → adapters-BKSf3T9R.d.mts.map} +1 -1
  3. package/dist/{apply-5uslYdUu.mjs → apply-x0eMK1lX.mjs} +18 -17
  4. package/dist/apply-x0eMK1lX.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +86 -15
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +5 -5
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +22 -2
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.mjs +2 -2
  14. package/dist/astro/middleware/request-context.mjs +1 -1
  15. package/dist/astro/middleware/setup.mjs +1 -1
  16. package/dist/astro/middleware.d.mts.map +1 -1
  17. package/dist/astro/middleware.mjs +259 -71
  18. package/dist/astro/middleware.mjs.map +1 -1
  19. package/dist/astro/types.d.mts +16 -8
  20. package/dist/astro/types.d.mts.map +1 -1
  21. package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
  22. package/dist/byline-Chbr2GoP.mjs.map +1 -0
  23. package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
  24. package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
  25. package/dist/cli/index.mjs +16 -12
  26. package/dist/cli/index.mjs.map +1 -1
  27. package/dist/client/cf-access.d.mts +1 -1
  28. package/dist/client/index.d.mts +1 -1
  29. package/dist/client/index.mjs +1 -1
  30. package/dist/{content-D7J5y73J.mjs → content-BcQPYxdV.mjs} +13 -15
  31. package/dist/content-BcQPYxdV.mjs.map +1 -0
  32. package/dist/db/index.d.mts +3 -3
  33. package/dist/db/libsql.d.mts +1 -1
  34. package/dist/db/postgres.d.mts +1 -1
  35. package/dist/db/sqlite.d.mts +1 -1
  36. package/dist/{db-errors-D0UT85nC.mjs → db-errors-l1Qh2RPR.mjs} +1 -1
  37. package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
  38. package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
  39. package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
  40. package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
  41. package/dist/error-zG5T1UGA.mjs.map +1 -0
  42. package/dist/{index-De6_Xv3v.d.mts → index-DIb-CzNx.d.mts} +157 -14
  43. package/dist/index-DIb-CzNx.d.mts.map +1 -0
  44. package/dist/index.d.mts +11 -11
  45. package/dist/index.mjs +22 -20
  46. package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
  47. package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
  48. package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
  49. package/dist/loader-CndGj8kM.mjs.map +1 -0
  50. package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
  51. package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
  52. package/dist/media/index.d.mts +1 -1
  53. package/dist/media/local-runtime.d.mts +7 -7
  54. package/dist/media/local-runtime.mjs +2 -2
  55. package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
  56. package/dist/media-D8FbNsl0.mjs.map +1 -0
  57. package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
  58. package/dist/mode-BnAOqItE.mjs.map +1 -0
  59. package/dist/page/index.d.mts +2 -2
  60. package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
  61. package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
  62. package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.d.mts.map} +1 -1
  63. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  64. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  65. package/dist/{query-g4Ug-9j9.mjs → query-fqEdLFms.mjs} +9 -9
  66. package/dist/{query-g4Ug-9j9.mjs.map → query-fqEdLFms.mjs.map} +1 -1
  67. package/dist/{redirect-CN0Rt9Ob.mjs → redirect-D_pshWdf.mjs} +4 -4
  68. package/dist/redirect-D_pshWdf.mjs.map +1 -0
  69. package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
  70. package/dist/registry-C3Mr0ODu.mjs.map +1 -0
  71. package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
  72. package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
  73. package/dist/{runner-BR2xKwhn.d.mts → runner-OURCaApa.d.mts} +2 -2
  74. package/dist/{runner-BR2xKwhn.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
  75. package/dist/runtime.d.mts +6 -6
  76. package/dist/runtime.mjs +2 -2
  77. package/dist/{search-B0effn3j.mjs → search-BoZYFuUk.mjs} +227 -84
  78. package/dist/search-BoZYFuUk.mjs.map +1 -0
  79. package/dist/seed/index.d.mts +2 -2
  80. package/dist/seed/index.mjs +12 -12
  81. package/dist/seo/index.d.mts +1 -1
  82. package/dist/storage/local.d.mts +1 -1
  83. package/dist/storage/local.mjs +1 -1
  84. package/dist/storage/s3.d.mts +1 -1
  85. package/dist/storage/s3.d.mts.map +1 -1
  86. package/dist/storage/s3.mjs +4 -4
  87. package/dist/storage/s3.mjs.map +1 -1
  88. package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-B4IAshV8.mjs} +5 -5
  89. package/dist/{taxonomies-K2z0Uhnj.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
  90. package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
  91. package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
  92. package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
  93. package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
  94. package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
  95. package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
  96. package/dist/types-BIgulNsW.mjs +68 -0
  97. package/dist/types-BIgulNsW.mjs.map +1 -0
  98. package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
  99. package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
  100. package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
  101. package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
  102. package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
  103. package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
  104. package/dist/{types-C2v0c34j.d.mts → types-CS8FIX7L.d.mts} +1 -1
  105. package/dist/{types-C2v0c34j.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
  106. package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
  107. package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
  108. package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
  109. package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
  110. package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
  111. package/dist/types-i36XcA_X.d.mts.map +1 -0
  112. package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
  113. package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
  114. package/dist/{validate-kM8Pjuf7.d.mts → validate-DHxmpFJt.d.mts} +4 -4
  115. package/dist/{validate-kM8Pjuf7.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
  116. package/dist/validation-C-ZpN2GI.mjs +144 -0
  117. package/dist/validation-C-ZpN2GI.mjs.map +1 -0
  118. package/dist/version-Bbq8TCrz.mjs +7 -0
  119. package/dist/{version-BnTKdfam.mjs.map → version-Bbq8TCrz.mjs.map} +1 -1
  120. package/dist/zod-generator-CpwccCIv.mjs +132 -0
  121. package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
  122. package/package.json +18 -5
  123. package/src/api/auth-storage.ts +37 -0
  124. package/src/api/error.ts +6 -0
  125. package/src/api/errors.ts +8 -0
  126. package/src/api/handlers/comments.ts +13 -0
  127. package/src/api/handlers/content.ts +122 -3
  128. package/src/api/handlers/index.ts +2 -0
  129. package/src/api/handlers/media.ts +8 -1
  130. package/src/api/handlers/menus.ts +160 -21
  131. package/src/api/handlers/redirects.ts +16 -3
  132. package/src/api/handlers/sections.ts +8 -1
  133. package/src/api/handlers/taxonomies.ts +128 -16
  134. package/src/api/handlers/validation.ts +212 -0
  135. package/src/api/openapi/document.ts +4 -1
  136. package/src/api/public-url.ts +6 -3
  137. package/src/api/route-utils.ts +14 -0
  138. package/src/api/schemas/common.ts +1 -1
  139. package/src/api/schemas/setup.ts +8 -0
  140. package/src/api/schemas/widgets.ts +12 -10
  141. package/src/api/setup-complete.ts +40 -0
  142. package/src/astro/integration/index.ts +13 -2
  143. package/src/astro/integration/routes.ts +28 -0
  144. package/src/astro/integration/runtime.ts +19 -1
  145. package/src/astro/integration/virtual-modules.ts +41 -0
  146. package/src/astro/integration/vite-config.ts +43 -12
  147. package/src/astro/middleware/auth.ts +21 -0
  148. package/src/astro/middleware.ts +18 -1
  149. package/src/astro/routes/PluginRegistry.tsx +10 -1
  150. package/src/astro/routes/api/auth/mode.ts +57 -0
  151. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
  152. package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
  153. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
  154. package/src/astro/routes/api/content/[collection]/index.ts +1 -9
  155. package/src/astro/routes/api/import/wordpress/media.ts +2 -7
  156. package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
  157. package/src/astro/routes/api/settings/email.ts +4 -9
  158. package/src/astro/routes/api/setup/admin.ts +8 -2
  159. package/src/astro/routes/api/setup/index.ts +2 -2
  160. package/src/astro/routes/api/setup/status.ts +3 -1
  161. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
  162. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
  163. package/src/astro/routes/api/widget-areas/[name].ts +4 -1
  164. package/src/astro/routes/api/widget-areas/index.ts +4 -1
  165. package/src/astro/types.ts +9 -0
  166. package/src/auth/mode.ts +15 -3
  167. package/src/auth/providers/github-admin.tsx +29 -0
  168. package/src/auth/providers/github.ts +31 -0
  169. package/src/auth/providers/google-admin.tsx +44 -0
  170. package/src/auth/providers/google.ts +31 -0
  171. package/src/auth/types.ts +114 -4
  172. package/src/cli/commands/bundle.ts +3 -1
  173. package/src/components/EmDashImage.astro +7 -6
  174. package/src/components/Gallery.astro +5 -3
  175. package/src/components/Image.astro +8 -3
  176. package/src/components/InlinePortableTextEditor.tsx +2 -1
  177. package/src/components/LiveSearch.astro +5 -14
  178. package/src/database/repositories/audit.ts +6 -8
  179. package/src/database/repositories/byline.ts +6 -8
  180. package/src/database/repositories/comment.ts +12 -16
  181. package/src/database/repositories/content.ts +40 -40
  182. package/src/database/repositories/index.ts +1 -1
  183. package/src/database/repositories/media.ts +10 -13
  184. package/src/database/repositories/plugin-storage.ts +4 -6
  185. package/src/database/repositories/redirect.ts +12 -16
  186. package/src/database/repositories/taxonomy.ts +14 -3
  187. package/src/database/repositories/types.ts +57 -8
  188. package/src/database/repositories/user.ts +6 -8
  189. package/src/emdash-runtime.ts +306 -90
  190. package/src/index.ts +5 -1
  191. package/src/loader.ts +6 -5
  192. package/src/mcp/server.ts +678 -105
  193. package/src/media/normalize.ts +1 -1
  194. package/src/media/url.ts +78 -0
  195. package/src/plugins/email-console.ts +10 -3
  196. package/src/plugins/hooks.ts +11 -0
  197. package/src/plugins/manifest-schema.ts +12 -0
  198. package/src/plugins/types.ts +23 -2
  199. package/src/query.ts +1 -1
  200. package/src/request-cache.ts +3 -0
  201. package/src/schema/registry.ts +41 -5
  202. package/src/search/fts-manager.ts +0 -2
  203. package/src/search/query.ts +111 -26
  204. package/src/search/types.ts +8 -1
  205. package/src/sections/index.ts +7 -9
  206. package/src/storage/s3.ts +12 -6
  207. package/src/virtual-modules.d.ts +21 -1
  208. package/src/widgets/index.ts +1 -1
  209. package/dist/apply-5uslYdUu.mjs.map +0 -1
  210. package/dist/byline-C4OVd8b3.mjs.map +0 -1
  211. package/dist/content-D7J5y73J.mjs.map +0 -1
  212. package/dist/error-CiYn9yDu.mjs.map +0 -1
  213. package/dist/index-De6_Xv3v.d.mts.map +0 -1
  214. package/dist/loader-DeiBJEMe.mjs.map +0 -1
  215. package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
  216. package/dist/media-DqHVh136.mjs.map +0 -1
  217. package/dist/mode-CpNnGkPz.mjs.map +0 -1
  218. package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
  219. package/dist/registry-Ci3WxVAr.mjs.map +0 -1
  220. package/dist/request-cache-DiR961CV.mjs.map +0 -1
  221. package/dist/search-B0effn3j.mjs.map +0 -1
  222. package/dist/types-CMMN0pNg.mjs +0 -31
  223. package/dist/types-CMMN0pNg.mjs.map +0 -1
  224. package/dist/types-DgrIP0tF.d.mts.map +0 -1
  225. 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
- * Providers like Cloudflare Access implement these interfaces.
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 - returned by auth adapter functions (e.g., access())
31
+ * Auth descriptor transparent auth providers (e.g., Cloudflare Access).
26
32
  *
27
- * Similar to DatabaseDescriptor and StorageDescriptor, this allows
28
- * auth providers to be configured at build time and loaded at runtime.
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
- const storageKey = (img.meta?.storageKey as string) || img.id;
60
- if (storageKey) {
61
- return `/_emdash/api/media/file/${storageKey}`;
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
- `/_emdash/api/media/file/${image.asset._ref}`;
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 prefer stored URL (includes storage key with extension),
126
- // fall back to _ref (bare ULID, works if media file endpoint supports ID lookup)
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 = asset.url || `/_emdash/api/media/file/${asset._ref}`;
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 = /&lt;mark&gt;/g;
117
- const SNIPPET_MARK_CLOSE_RE = /&lt;\/mark&gt;/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
- // Fill in snippet (sanitize to allow only <mark> tags from FTS5 highlighting)
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, "&amp;")
406
- .replace(SNIPPET_LT_RE, "&lt;")
407
- .replace(SNIPPET_GT_RE, "&gt;")
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
- if (decoded) {
147
- q = q.where((eb) =>
148
- eb.or([
149
- eb("timestamp", "<", decoded.orderValue),
150
- eb.and([eb("timestamp", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
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
- if (decoded) {
127
- query = query.where((eb) =>
128
- eb.or([
129
- eb("created_at", "<", decoded.orderValue),
130
- eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
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
- if (decoded) {
147
- query = query.where((eb: ExpressionBuilder<Database, "_emdash_comments">) =>
148
- eb.or([
149
- eb("created_at", ">", decoded.orderValue),
150
- eb.and([eb("created_at", "=", decoded.orderValue), eb("id", ">", decoded.id)]),
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
- if (decoded) {
206
- query = query.where((eb: ExpressionBuilder<Database, "_emdash_comments">) =>
207
- eb.or([
208
- eb("created_at", "<", decoded.orderValue),
209
- eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
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 decoded = decodeCursor(options.cursor);
495
- if (decoded) {
496
- const { orderValue, id: cursorId } = decoded;
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
- );
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 decoded = decodeCursor(options.cursor);
677
- if (decoded) {
678
- const { orderValue, id: cursorId } = decoded;
679
-
680
- if (safeOrderDirection === "DESC") {
681
- query = query.where((eb) =>
682
- eb.or([
683
- eb(dbField as any, "<", orderValue),
684
- eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
685
- ]),
686
- );
687
- } else {
688
- query = query.where((eb) =>
689
- eb.or([
690
- eb(dbField as any, ">", orderValue),
691
- eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
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 decoded = decodeCursor(options.cursor);
208
- if (decoded) {
209
- const { orderValue: createdAt, id: cursorId } = decoded;
210
-
211
- // Keyset pagination: get items where (created_at, id) < cursor
212
- query = query.where((eb) =>
213
- eb.or([
214
- eb("created_at", "<", createdAt),
215
- eb.and([eb("created_at", "=", createdAt), eb("id", "<", cursorId)]),
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
- if (decoded) {
233
- query = query.where(({ eb }) =>
234
- eb(sql`(created_at, id)`, ">", sql`(${decoded.orderValue}, ${decoded.id})`),
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