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.
Files changed (263) 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-B4MsLM-w.mjs → apply-x0eMK1lX.mjs} +186 -28
  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 +92 -17
  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 +7 -2
  15. package/dist/astro/middleware/request-context.mjs.map +1 -1
  16. package/dist/astro/middleware/setup.mjs +1 -1
  17. package/dist/astro/middleware.d.mts.map +1 -1
  18. package/dist/astro/middleware.mjs +263 -74
  19. package/dist/astro/middleware.mjs.map +1 -1
  20. package/dist/astro/types.d.mts +25 -8
  21. package/dist/astro/types.d.mts.map +1 -1
  22. package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
  23. package/dist/byline-Chbr2GoP.mjs.map +1 -0
  24. package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
  25. package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
  26. package/dist/cli/index.mjs +17 -13
  27. package/dist/cli/index.mjs.map +1 -1
  28. package/dist/client/cf-access.d.mts +1 -1
  29. package/dist/client/index.d.mts +1 -1
  30. package/dist/client/index.mjs +1 -1
  31. package/dist/{content-BsBoyj8G.mjs → content-BcQPYxdV.mjs} +39 -15
  32. package/dist/content-BcQPYxdV.mjs.map +1 -0
  33. package/dist/db/index.d.mts +3 -3
  34. package/dist/db/index.mjs +1 -1
  35. package/dist/db/libsql.d.mts +1 -1
  36. package/dist/db/postgres.d.mts +1 -1
  37. package/dist/db/sqlite.d.mts +1 -1
  38. package/dist/{db-errors-D0UT85nC.mjs → db-errors-l1Qh2RPR.mjs} +1 -1
  39. package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
  40. package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
  41. package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
  42. package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
  43. package/dist/error-zG5T1UGA.mjs.map +1 -0
  44. package/dist/{index-BYv0mB9g.d.mts → index-DIb-CzNx.d.mts} +232 -15
  45. package/dist/index-DIb-CzNx.d.mts.map +1 -0
  46. package/dist/index.d.mts +11 -11
  47. package/dist/index.mjs +23 -21
  48. package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
  49. package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
  50. package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
  51. package/dist/loader-CndGj8kM.mjs.map +1 -0
  52. package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
  53. package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
  54. package/dist/media/index.d.mts +1 -1
  55. package/dist/media/local-runtime.d.mts +7 -7
  56. package/dist/media/local-runtime.mjs +2 -2
  57. package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
  58. package/dist/media-D8FbNsl0.mjs.map +1 -0
  59. package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
  60. package/dist/mode-BnAOqItE.mjs.map +1 -0
  61. package/dist/page/index.d.mts +2 -2
  62. package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
  63. package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
  64. package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.d.mts.map} +1 -1
  65. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  66. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  67. package/dist/{query-Bk_3vKvU.mjs → query-fqEdLFms.mjs} +9 -9
  68. package/dist/{query-Bk_3vKvU.mjs.map → query-fqEdLFms.mjs.map} +1 -1
  69. package/dist/{redirect-7lGhLBNZ.mjs → redirect-D_pshWdf.mjs} +69 -13
  70. package/dist/redirect-D_pshWdf.mjs.map +1 -0
  71. package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
  72. package/dist/registry-C3Mr0ODu.mjs.map +1 -0
  73. package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
  74. package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
  75. package/dist/{runner-Fl2NcUUz.d.mts → runner-OURCaApa.d.mts} +2 -2
  76. package/dist/{runner-Fl2NcUUz.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
  77. package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
  78. package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
  79. package/dist/runtime.d.mts +6 -6
  80. package/dist/runtime.mjs +2 -2
  81. package/dist/{search-DI4bM2w9.mjs → search-BoZYFuUk.mjs} +339 -102
  82. package/dist/search-BoZYFuUk.mjs.map +1 -0
  83. package/dist/seed/index.d.mts +2 -2
  84. package/dist/seed/index.mjs +12 -12
  85. package/dist/seo/index.d.mts +1 -1
  86. package/dist/storage/local.d.mts +1 -1
  87. package/dist/storage/local.mjs +1 -1
  88. package/dist/storage/s3.d.mts +1 -1
  89. package/dist/storage/s3.d.mts.map +1 -1
  90. package/dist/storage/s3.mjs +4 -4
  91. package/dist/storage/s3.mjs.map +1 -1
  92. package/dist/{taxonomies-DbrKzDju.mjs → taxonomies-B4IAshV8.mjs} +5 -5
  93. package/dist/{taxonomies-DbrKzDju.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
  94. package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
  95. package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
  96. package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
  97. package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
  98. package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
  99. package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
  100. package/dist/types-BIgulNsW.mjs +68 -0
  101. package/dist/types-BIgulNsW.mjs.map +1 -0
  102. package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
  103. package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
  104. package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
  105. package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
  106. package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
  107. package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
  108. package/dist/{types-8xrvl_68.d.mts → types-CS8FIX7L.d.mts} +10 -1
  109. package/dist/{types-8xrvl_68.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
  110. package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
  111. package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
  112. package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
  113. package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
  114. package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
  115. package/dist/types-i36XcA_X.d.mts.map +1 -0
  116. package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
  117. package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
  118. package/dist/{validate-CaLH1Ia2.d.mts → validate-DHxmpFJt.d.mts} +4 -4
  119. package/dist/{validate-CaLH1Ia2.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
  120. package/dist/validation-C-ZpN2GI.mjs +144 -0
  121. package/dist/validation-C-ZpN2GI.mjs.map +1 -0
  122. package/dist/version-DJrV1K0M.mjs +7 -0
  123. package/dist/{version-Uaf2ynPX.mjs.map → version-DJrV1K0M.mjs.map} +1 -1
  124. package/dist/zod-generator-CpwccCIv.mjs +132 -0
  125. package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
  126. package/package.json +19 -6
  127. package/src/api/auth-storage.ts +37 -0
  128. package/src/api/error.ts +6 -0
  129. package/src/api/errors.ts +8 -0
  130. package/src/api/handlers/comments.ts +13 -0
  131. package/src/api/handlers/content.ts +124 -3
  132. package/src/api/handlers/index.ts +2 -0
  133. package/src/api/handlers/media.ts +8 -1
  134. package/src/api/handlers/menus.ts +160 -21
  135. package/src/api/handlers/redirects.ts +16 -3
  136. package/src/api/handlers/sections.ts +8 -1
  137. package/src/api/handlers/taxonomies.ts +128 -16
  138. package/src/api/handlers/validation.ts +212 -0
  139. package/src/api/openapi/document.ts +4 -1
  140. package/src/api/public-url.ts +6 -3
  141. package/src/api/route-utils.ts +14 -0
  142. package/src/api/schemas/common.ts +1 -1
  143. package/src/api/schemas/content.ts +8 -0
  144. package/src/api/schemas/setup.ts +8 -0
  145. package/src/api/schemas/widgets.ts +12 -10
  146. package/src/api/setup-complete.ts +40 -0
  147. package/src/astro/integration/font-provider.ts +3 -1
  148. package/src/astro/integration/index.ts +15 -2
  149. package/src/astro/integration/routes.ts +28 -0
  150. package/src/astro/integration/runtime.ts +74 -2
  151. package/src/astro/integration/virtual-modules.ts +41 -0
  152. package/src/astro/integration/vite-config.ts +43 -12
  153. package/src/astro/middleware/auth.ts +21 -0
  154. package/src/astro/middleware.ts +18 -1
  155. package/src/astro/routes/PluginRegistry.tsx +10 -1
  156. package/src/astro/routes/admin.astro +14 -7
  157. package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
  158. package/src/astro/routes/api/auth/mode.ts +57 -0
  159. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
  160. package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
  161. package/src/astro/routes/api/auth/passkey/options.ts +2 -1
  162. package/src/astro/routes/api/auth/signup/request.ts +26 -8
  163. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
  164. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
  165. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
  166. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
  167. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
  168. package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
  169. package/src/astro/routes/api/content/[collection]/index.ts +20 -10
  170. package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
  171. package/src/astro/routes/api/import/wordpress/media.ts +2 -7
  172. package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
  173. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
  174. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +4 -3
  175. package/src/astro/routes/api/manifest.ts +7 -0
  176. package/src/astro/routes/api/oauth/device/code.ts +2 -1
  177. package/src/astro/routes/api/oauth/device/token.ts +2 -1
  178. package/src/astro/routes/api/settings/email.ts +4 -9
  179. package/src/astro/routes/api/setup/admin-verify.ts +30 -5
  180. package/src/astro/routes/api/setup/admin.ts +38 -8
  181. package/src/astro/routes/api/setup/index.ts +7 -4
  182. package/src/astro/routes/api/setup/status.ts +3 -1
  183. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
  184. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
  185. package/src/astro/routes/api/widget-areas/[name].ts +4 -1
  186. package/src/astro/routes/api/widget-areas/index.ts +4 -1
  187. package/src/astro/types.ts +18 -0
  188. package/src/auth/mode.ts +15 -3
  189. package/src/auth/providers/github-admin.tsx +29 -0
  190. package/src/auth/providers/github.ts +31 -0
  191. package/src/auth/providers/google-admin.tsx +44 -0
  192. package/src/auth/providers/google.ts +31 -0
  193. package/src/auth/rate-limit.ts +50 -22
  194. package/src/auth/setup-nonce.ts +22 -0
  195. package/src/auth/trusted-proxy.ts +92 -0
  196. package/src/auth/types.ts +114 -4
  197. package/src/cli/commands/bundle.ts +3 -1
  198. package/src/components/EmDashImage.astro +7 -6
  199. package/src/components/Gallery.astro +5 -3
  200. package/src/components/Image.astro +8 -3
  201. package/src/components/InlinePortableTextEditor.tsx +2 -1
  202. package/src/components/LiveSearch.astro +5 -14
  203. package/src/database/migrations/035_bounded_404_log.ts +112 -0
  204. package/src/database/migrations/runner.ts +2 -0
  205. package/src/database/repositories/audit.ts +6 -8
  206. package/src/database/repositories/byline.ts +6 -8
  207. package/src/database/repositories/comment.ts +12 -16
  208. package/src/database/repositories/content.ts +79 -40
  209. package/src/database/repositories/index.ts +1 -1
  210. package/src/database/repositories/media.ts +10 -13
  211. package/src/database/repositories/options.ts +25 -0
  212. package/src/database/repositories/plugin-storage.ts +4 -6
  213. package/src/database/repositories/redirect.ts +123 -24
  214. package/src/database/repositories/taxonomy.ts +14 -3
  215. package/src/database/repositories/types.ts +57 -8
  216. package/src/database/repositories/user.ts +6 -8
  217. package/src/database/types.ts +9 -0
  218. package/src/emdash-runtime.ts +309 -91
  219. package/src/import/registry.ts +4 -3
  220. package/src/import/ssrf.ts +253 -12
  221. package/src/index.ts +5 -1
  222. package/src/loader.ts +6 -5
  223. package/src/mcp/server.ts +753 -107
  224. package/src/media/normalize.ts +1 -1
  225. package/src/media/url.ts +78 -0
  226. package/src/plugins/context.ts +15 -3
  227. package/src/plugins/email-console.ts +10 -3
  228. package/src/plugins/hooks.ts +11 -0
  229. package/src/plugins/manager.ts +6 -0
  230. package/src/plugins/manifest-schema.ts +12 -0
  231. package/src/plugins/request-meta.ts +66 -15
  232. package/src/plugins/routes.ts +3 -1
  233. package/src/plugins/types.ts +23 -2
  234. package/src/query.ts +1 -1
  235. package/src/request-cache.ts +3 -0
  236. package/src/schema/registry.ts +41 -5
  237. package/src/search/fts-manager.ts +0 -2
  238. package/src/search/query.ts +111 -26
  239. package/src/search/types.ts +8 -1
  240. package/src/sections/index.ts +7 -9
  241. package/src/seed/apply.ts +26 -0
  242. package/src/storage/s3.ts +12 -6
  243. package/src/virtual-modules.d.ts +21 -1
  244. package/src/visual-editing/toolbar.ts +6 -1
  245. package/src/widgets/index.ts +1 -1
  246. package/dist/apply-B4MsLM-w.mjs.map +0 -1
  247. package/dist/byline-C4OVd8b3.mjs.map +0 -1
  248. package/dist/content-BsBoyj8G.mjs.map +0 -1
  249. package/dist/error-CiYn9yDu.mjs.map +0 -1
  250. package/dist/index-BYv0mB9g.d.mts.map +0 -1
  251. package/dist/loader-DeiBJEMe.mjs.map +0 -1
  252. package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
  253. package/dist/media-DqHVh136.mjs.map +0 -1
  254. package/dist/mode-CpNnGkPz.mjs.map +0 -1
  255. package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
  256. package/dist/registry-Ci3WxVAr.mjs.map +0 -1
  257. package/dist/request-cache-DiR961CV.mjs.map +0 -1
  258. package/dist/runner-Cd-_WyDo.mjs.map +0 -1
  259. package/dist/search-DI4bM2w9.mjs.map +0 -1
  260. package/dist/types-CMMN0pNg.mjs +0 -31
  261. package/dist/types-CMMN0pNg.mjs.map +0 -1
  262. package/dist/types-DgrIP0tF.d.mts.map +0 -1
  263. package/dist/version-Uaf2ynPX.mjs +0 -7
@@ -0,0 +1,144 @@
1
+ import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
2
+ import "./dialect-helpers-DhTzaUxP.mjs";
3
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-HGz06Soa.mjs";
4
+ import { t as isMissingTableError } from "./db-errors-l1Qh2RPR.mjs";
5
+ import { t as generateZodSchema } from "./zod-generator-CpwccCIv.mjs";
6
+ import { n as SchemaRegistry } from "./registry-C3Mr0ODu.mjs";
7
+ import { sql } from "kysely";
8
+
9
+ //#region src/api/handlers/validation.ts
10
+ /**
11
+ * Field-level validation for content create / update.
12
+ *
13
+ * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)
14
+ * into the handler boundary so REST and MCP both get the same enforcement:
15
+ *
16
+ * - required fields must be present and non-empty
17
+ * - select / multiSelect values must match the configured options
18
+ * - reference fields must resolve to a real, non-trashed target
19
+ *
20
+ * Errors surface as `{ code: "VALIDATION_ERROR", message }` with all
21
+ * offending fields listed in one message so callers can fix everything in
22
+ * a single round trip.
23
+ */
24
+ /** Treat `undefined`, `null`, and `""` as "not set". */
25
+ function isMissing(value) {
26
+ return value === void 0 || value === null || value === "";
27
+ }
28
+ /**
29
+ * Resolve the target collection slug for a reference field.
30
+ *
31
+ * Schema-defined reference fields (the static `reference()` factory in
32
+ * `fields/reference.ts`) put the target in `options.collection`. The MCP
33
+ * `schema_create_field` tool also puts it there. Tests and some admin paths
34
+ * stash it inside `validation.collection` directly; we accept both.
35
+ */
36
+ function getReferenceTargetCollection(field) {
37
+ const fromOptions = field.options?.collection;
38
+ if (typeof fromOptions === "string" && fromOptions.length > 0) return fromOptions;
39
+ const validation = field.validation;
40
+ if (validation && "collection" in validation) {
41
+ const fromValidation = validation.collection;
42
+ if (typeof fromValidation === "string" && fromValidation.length > 0) return fromValidation;
43
+ }
44
+ }
45
+ /**
46
+ * Format a Zod issue path into a human-readable field reference, e.g.
47
+ * `tags`, `tags.1`, `image.alt`.
48
+ */
49
+ function formatIssuePath(path) {
50
+ if (path.length === 0) return "(root)";
51
+ return path.map((seg) => String(seg)).join(".");
52
+ }
53
+ /**
54
+ * Validate `data` against the collection's field definitions.
55
+ *
56
+ * `partial: true` switches Zod into partial mode so updates can include
57
+ * only the fields being changed without tripping required-field errors on
58
+ * fields the caller didn't touch. Required fields that ARE present in
59
+ * partial-mode data still get the empty-string check below.
60
+ */
61
+ async function validateContentData(db, collection, data, options = {}) {
62
+ const collectionWithFields = await new SchemaRegistry(db).getCollectionWithFields(collection);
63
+ if (!collectionWithFields) return {
64
+ ok: false,
65
+ error: {
66
+ code: "COLLECTION_NOT_FOUND",
67
+ message: `Collection '${collection}' not found`
68
+ }
69
+ };
70
+ const issues = [];
71
+ const knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));
72
+ for (const key of Object.keys(data)) {
73
+ if (key.startsWith("_")) continue;
74
+ if (!knownFields.has(key)) issues.push(`${key}: unknown field on collection '${collection}'`);
75
+ }
76
+ const baseSchema = generateZodSchema(collectionWithFields);
77
+ const parsed = (options.partial ? baseSchema.partial() : baseSchema).safeParse(data);
78
+ if (!parsed.success) for (const issue of parsed.error.issues) issues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);
79
+ for (const field of collectionWithFields.fields) {
80
+ if (!field.required) continue;
81
+ const present = Object.hasOwn(data, field.slug);
82
+ if (options.partial && !present) continue;
83
+ if (data[field.slug] === "") issues.push(`${field.slug}: required (empty value not allowed)`);
84
+ }
85
+ const refsByTarget = /* @__PURE__ */ new Map();
86
+ for (const field of collectionWithFields.fields) {
87
+ if (field.type !== "reference") continue;
88
+ if (options.partial && !Object.hasOwn(data, field.slug)) continue;
89
+ const value = data[field.slug];
90
+ if (isMissing(value)) continue;
91
+ if (typeof value !== "string") continue;
92
+ const target = getReferenceTargetCollection(field);
93
+ if (!target) continue;
94
+ const list = refsByTarget.get(target) ?? [];
95
+ list.push({
96
+ field: field.slug,
97
+ id: value
98
+ });
99
+ refsByTarget.set(target, list);
100
+ }
101
+ for (const [target, refs] of refsByTarget) {
102
+ try {
103
+ validateIdentifier(target, "reference target collection");
104
+ } catch {
105
+ for (const ref of refs) issues.push(`${ref.field}: invalid reference target collection '${target}'`);
106
+ continue;
107
+ }
108
+ const ids = [...new Set(refs.map((r) => r.id))];
109
+ const tableName = `ec_${target}`;
110
+ const found = /* @__PURE__ */ new Set();
111
+ let targetTableMissing = false;
112
+ for (const idChunk of chunks(ids, SQL_BATCH_SIZE)) try {
113
+ const rows = await sql`
114
+ SELECT id FROM ${sql.ref(tableName)}
115
+ WHERE id IN (${sql.join(idChunk)})
116
+ AND deleted_at IS NULL
117
+ `.execute(db);
118
+ for (const row of rows.rows) found.add(row.id);
119
+ } catch (error) {
120
+ if (isMissingTableError(error)) {
121
+ targetTableMissing = true;
122
+ break;
123
+ }
124
+ throw error;
125
+ }
126
+ if (targetTableMissing) {
127
+ for (const ref of refs) issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
128
+ continue;
129
+ }
130
+ for (const ref of refs) if (!found.has(ref.id)) issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
131
+ }
132
+ if (issues.length === 0) return { ok: true };
133
+ return {
134
+ ok: false,
135
+ error: {
136
+ code: "VALIDATION_ERROR",
137
+ message: issues.join("; ")
138
+ }
139
+ };
140
+ }
141
+
142
+ //#endregion
143
+ export { validateContentData };
144
+ //# sourceMappingURL=validation-C-ZpN2GI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation-C-ZpN2GI.mjs","names":[],"sources":["../src/api/handlers/validation.ts"],"sourcesContent":["/**\n * Field-level validation for content create / update.\n *\n * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)\n * into the handler boundary so REST and MCP both get the same enforcement:\n *\n * - required fields must be present and non-empty\n * - select / multiSelect values must match the configured options\n * - reference fields must resolve to a real, non-trashed target\n *\n * Errors surface as `{ code: \"VALIDATION_ERROR\", message }` with all\n * offending fields listed in one message so callers can fix everything in\n * a single round trip.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { SchemaRegistry } from \"../../schema/registry.js\";\nimport type { Field } from \"../../schema/types.js\";\nimport { generateZodSchema } from \"../../schema/zod-generator.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\n\ntype ValidationResult =\n\t| { ok: true }\n\t| { ok: false; error: { code: \"VALIDATION_ERROR\" | \"COLLECTION_NOT_FOUND\"; message: string } };\n\n/** Treat `undefined`, `null`, and `\"\"` as \"not set\". */\nfunction isMissing(value: unknown): boolean {\n\treturn value === undefined || value === null || value === \"\";\n}\n\n/**\n * Resolve the target collection slug for a reference field.\n *\n * Schema-defined reference fields (the static `reference()` factory in\n * `fields/reference.ts`) put the target in `options.collection`. The MCP\n * `schema_create_field` tool also puts it there. Tests and some admin paths\n * stash it inside `validation.collection` directly; we accept both.\n */\nfunction getReferenceTargetCollection(field: Field): string | undefined {\n\tconst fromOptions = field.options?.collection;\n\tif (typeof fromOptions === \"string\" && fromOptions.length > 0) return fromOptions;\n\tconst validation = field.validation;\n\tif (validation && \"collection\" in validation) {\n\t\tconst fromValidation: unknown = (validation as { collection?: unknown }).collection;\n\t\tif (typeof fromValidation === \"string\" && fromValidation.length > 0) return fromValidation;\n\t}\n\treturn undefined;\n}\n\n/**\n * Format a Zod issue path into a human-readable field reference, e.g.\n * `tags`, `tags.1`, `image.alt`.\n */\nfunction formatIssuePath(path: ReadonlyArray<PropertyKey>): string {\n\tif (path.length === 0) return \"(root)\";\n\treturn path.map((seg) => String(seg)).join(\".\");\n}\n\n/**\n * Validate `data` against the collection's field definitions.\n *\n * `partial: true` switches Zod into partial mode so updates can include\n * only the fields being changed without tripping required-field errors on\n * fields the caller didn't touch. Required fields that ARE present in\n * partial-mode data still get the empty-string check below.\n */\nexport async function validateContentData(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tdata: Record<string, unknown>,\n\toptions: { partial?: boolean } = {},\n): Promise<ValidationResult> {\n\tconst registry = new SchemaRegistry(db);\n\tconst collectionWithFields = await registry.getCollectionWithFields(collection);\n\tif (!collectionWithFields) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst issues: string[] = [];\n\n\t// Detect unknown keys explicitly so callers get a useful error rather\n\t// than silently dropped data. Leading-underscore keys (e.g. `_slug`,\n\t// `_rev`) are reserved for internal handler/runtime use and aren't real\n\t// fields; skip them.\n\tconst knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));\n\tfor (const key of Object.keys(data)) {\n\t\tif (key.startsWith(\"_\")) continue;\n\t\tif (!knownFields.has(key)) {\n\t\t\tissues.push(`${key}: unknown field on collection '${collection}'`);\n\t\t}\n\t}\n\n\t// Zod handles type, enum, length and missing-required (in non-partial\n\t// mode) checks. Empty-string handling for required string fields is\n\t// done as a separate pass below since Zod's `z.string()` accepts \"\".\n\tconst baseSchema = generateZodSchema(collectionWithFields);\n\tconst schema = options.partial ? baseSchema.partial() : baseSchema;\n\tconst parsed = schema.safeParse(data);\n\tif (!parsed.success) {\n\t\tfor (const issue of parsed.error.issues) {\n\t\t\tissues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);\n\t\t}\n\t}\n\n\t// Empty-string-on-required check. In create mode (partial=false) Zod\n\t// already catches missing/null for required fields, but `z.string()`\n\t// happily accepts \"\". In update mode (partial=true) the field is only\n\t// checked if it's present in `data`.\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (!field.required) continue;\n\t\tconst present = Object.hasOwn(data, field.slug);\n\t\tif (options.partial && !present) continue;\n\t\tif (data[field.slug] === \"\") {\n\t\t\tissues.push(`${field.slug}: required (empty value not allowed)`);\n\t\t}\n\t}\n\n\t// Reference target existence. Only check fields that:\n\t// - have a value (non-missing) in `data`\n\t// - have a resolvable target collection\n\t// - in partial mode: are present in `data`\n\t// Batch one IN-query per target collection to keep round-trips low.\n\tconst refsByTarget = new Map<string, { field: string; id: string }[]>();\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (field.type !== \"reference\") continue;\n\t\tif (options.partial && !Object.hasOwn(data, field.slug)) continue;\n\t\tconst value = data[field.slug];\n\t\tif (isMissing(value)) continue;\n\t\tif (typeof value !== \"string\") continue; // Zod will have flagged this already\n\t\tconst target = getReferenceTargetCollection(field);\n\t\tif (!target) continue;\n\t\tconst list = refsByTarget.get(target) ?? [];\n\t\tlist.push({ field: field.slug, id: value });\n\t\trefsByTarget.set(target, list);\n\t}\n\n\tfor (const [target, refs] of refsByTarget) {\n\t\t// Validate the target collection slug before interpolating into raw\n\t\t// SQL — defense-in-depth even though slugs are already validated at\n\t\t// schema-create time.\n\t\ttry {\n\t\t\tvalidateIdentifier(target, \"reference target collection\");\n\t\t} catch {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: invalid reference target collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst ids = [...new Set(refs.map((r) => r.id))];\n\t\tconst tableName = `ec_${target}`;\n\n\t\t// Chunk the IN clause to stay below D1's bind-parameter limit. One\n\t\t// reference per request is the common case today; chunking makes the\n\t\t// helper safe if a future multiSelect-of-references is added.\n\t\tconst found = new Set<string>();\n\t\tlet targetTableMissing = false;\n\t\tfor (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {\n\t\t\ttry {\n\t\t\t\tconst rows = await sql<{ id: string }>`\n\t\t\t\t\tSELECT id FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE id IN (${sql.join(idChunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(db);\n\t\t\t\tfor (const row of rows.rows) {\n\t\t\t\t\tfound.add(row.id);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Missing table = the target collection table doesn't exist\n\t\t\t\t// (orphan reference). Treat all those references as missing.\n\t\t\t\t// Any other DB error (permissions, connection, syntax) must\n\t\t\t\t// propagate — silently dropping data integrity errors as\n\t\t\t\t// \"not found\" is exactly the bug F5 fixes.\n\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\ttargetTableMissing = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\tif (targetTableMissing) {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const ref of refs) {\n\t\t\tif (!found.has(ref.id)) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length === 0) return { ok: true };\n\treturn {\n\t\tok: false,\n\t\terror: {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: issues.join(\"; \"),\n\t\t},\n\t};\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ //#region src/version.ts
2
+ const VERSION = "1.0.0";
3
+ const COMMIT = "46f3acb";
4
+
5
+ //#endregion
6
+ export { VERSION as n, COMMIT as t };
7
+ //# sourceMappingURL=version-DJrV1K0M.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-Uaf2ynPX.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Build-time version constants, replaced by tsdown/Vite `define`.\n * Falls back to \"dev\" when running uncompiled (tests, dev).\n */\n\ndeclare const __EMDASH_VERSION__: string;\ndeclare const __EMDASH_COMMIT__: string;\n\nexport const VERSION: string =\n\ttypeof __EMDASH_VERSION__ !== \"undefined\" ? __EMDASH_VERSION__ : \"dev\";\n\nexport const COMMIT: string = typeof __EMDASH_COMMIT__ !== \"undefined\" ? __EMDASH_COMMIT__ : \"dev\";\n"],"mappings":";AAQA,MAAa;AAGb,MAAa"}
1
+ {"version":3,"file":"version-DJrV1K0M.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Build-time version constants, replaced by tsdown/Vite `define`.\n * Falls back to \"dev\" when running uncompiled (tests, dev).\n */\n\ndeclare const __EMDASH_VERSION__: string;\ndeclare const __EMDASH_COMMIT__: string;\n\nexport const VERSION: string =\n\ttypeof __EMDASH_VERSION__ !== \"undefined\" ? __EMDASH_VERSION__ : \"dev\";\n\nexport const COMMIT: string = typeof __EMDASH_COMMIT__ !== \"undefined\" ? __EMDASH_COMMIT__ : \"dev\";\n"],"mappings":";AAQA,MAAa;AAGb,MAAa"}
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/utils/hash.ts
4
+ /**
5
+ * SHA-256 hash of a string, truncated to 16 hex chars (64 bits).
6
+ * For cache invalidation / ETags — not for security.
7
+ */
8
+ async function hashString(content) {
9
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
10
+ return Array.from(new Uint8Array(buf).slice(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
11
+ }
12
+ /**
13
+ * Compute content hash using Web Crypto API
14
+ *
15
+ * Uses SHA-1 which is the fastest option in SubtleCrypto.
16
+ * SHA-1 is cryptographically weak but fine for content deduplication
17
+ * where we only need to detect identical files, not resist attacks.
18
+ *
19
+ * Returns hex string prefixed with "sha1:" for future-proofing
20
+ */
21
+ async function computeContentHash(content) {
22
+ let buf;
23
+ if (content instanceof ArrayBuffer) buf = content;
24
+ else {
25
+ buf = new ArrayBuffer(content.byteLength);
26
+ new Uint8Array(buf).set(content);
27
+ }
28
+ const hashBuffer = await crypto.subtle.digest("SHA-1", buf);
29
+ const hashArray = new Uint8Array(hashBuffer);
30
+ return `sha1:${Array.from(hashArray, (b) => b.toString(16).padStart(2, "0")).join("")}`;
31
+ }
32
+
33
+ //#endregion
34
+ //#region src/schema/zod-generator.ts
35
+ /**
36
+ * Generate a Zod schema from a collection's field definitions
37
+ *
38
+ * This allows runtime validation of content based on dynamically
39
+ * defined schemas stored in D1.
40
+ */
41
+ function generateZodSchema(collection) {
42
+ const shape = {};
43
+ for (const field of collection.fields) shape[field.slug] = generateFieldSchema(field);
44
+ return z.object(shape);
45
+ }
46
+ /**
47
+ * Generate Zod schema for a single field
48
+ */
49
+ function generateFieldSchema(field) {
50
+ let schema = getBaseSchema(field.type, field);
51
+ if (field.validation) schema = applyValidation(schema, field);
52
+ if (!field.required) schema = schema.optional();
53
+ if (field.defaultValue !== void 0) schema = schema.default(field.defaultValue);
54
+ return schema;
55
+ }
56
+ /**
57
+ * Get base Zod schema for a field type
58
+ */
59
+ function getBaseSchema(type, field) {
60
+ switch (type) {
61
+ case "url": return z.string().url();
62
+ case "string":
63
+ case "text":
64
+ case "slug": return z.string();
65
+ case "number": return z.number();
66
+ case "integer": return z.number().int();
67
+ case "boolean": return z.boolean();
68
+ case "datetime": return z.string().datetime().or(z.string().date());
69
+ case "select": {
70
+ const options = field.validation?.options;
71
+ if (options && options.length > 0) {
72
+ const [first, ...rest] = options;
73
+ return z.enum([first, ...rest]);
74
+ }
75
+ return z.string();
76
+ }
77
+ case "multiSelect": {
78
+ const multiOptions = field.validation?.options;
79
+ if (multiOptions && multiOptions.length > 0) {
80
+ const [first, ...rest] = multiOptions;
81
+ return z.array(z.enum([first, ...rest]));
82
+ }
83
+ return z.array(z.string());
84
+ }
85
+ case "portableText": return z.array(z.object({
86
+ _type: z.string(),
87
+ _key: z.string()
88
+ }).passthrough());
89
+ case "image": return z.object({
90
+ id: z.string(),
91
+ src: z.string().optional(),
92
+ alt: z.string().optional(),
93
+ width: z.number().optional(),
94
+ height: z.number().optional()
95
+ });
96
+ case "file": return z.object({
97
+ id: z.string(),
98
+ src: z.string().optional(),
99
+ filename: z.string().optional(),
100
+ mimeType: z.string().optional(),
101
+ size: z.number().optional()
102
+ });
103
+ case "reference": return z.string();
104
+ case "json": return z.unknown();
105
+ default: return z.unknown();
106
+ }
107
+ }
108
+ /**
109
+ * Apply validation rules to a schema
110
+ */
111
+ function applyValidation(schema, field) {
112
+ const validation = field.validation;
113
+ if (!validation) return schema;
114
+ if (schema instanceof z.ZodString) {
115
+ let strSchema = schema;
116
+ if (validation.minLength !== void 0) strSchema = strSchema.min(validation.minLength);
117
+ if (validation.maxLength !== void 0) strSchema = strSchema.max(validation.maxLength);
118
+ if (validation.pattern) strSchema = strSchema.regex(new RegExp(validation.pattern));
119
+ return strSchema;
120
+ }
121
+ if (schema instanceof z.ZodNumber) {
122
+ let numSchema = schema;
123
+ if (validation.min !== void 0) numSchema = numSchema.min(validation.min);
124
+ if (validation.max !== void 0) numSchema = numSchema.max(validation.max);
125
+ return numSchema;
126
+ }
127
+ return schema;
128
+ }
129
+
130
+ //#endregion
131
+ export { computeContentHash as n, hashString as r, generateZodSchema as t };
132
+ //# sourceMappingURL=zod-generator-CpwccCIv.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-generator-CpwccCIv.mjs","names":[],"sources":["../src/utils/hash.ts","../src/schema/zod-generator.ts"],"sourcesContent":["/**\n * SHA-256 hash of a string, truncated to 16 hex chars (64 bits).\n * For cache invalidation / ETags — not for security.\n */\nexport async function hashString(content: string): Promise<string> {\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(content));\n\treturn Array.from(new Uint8Array(buf).slice(0, 8), (b) => b.toString(16).padStart(2, \"0\")).join(\n\t\t\"\",\n\t);\n}\n\n/**\n * Compute content hash using Web Crypto API\n *\n * Uses SHA-1 which is the fastest option in SubtleCrypto.\n * SHA-1 is cryptographically weak but fine for content deduplication\n * where we only need to detect identical files, not resist attacks.\n *\n * Returns hex string prefixed with \"sha1:\" for future-proofing\n */\nexport async function computeContentHash(content: Uint8Array | ArrayBuffer): Promise<string> {\n\t// SubtleCrypto.digest() requires BufferSource (ArrayBuffer | ArrayBufferView<ArrayBuffer>).\n\t// Uint8Array.buffer is ArrayBufferLike which may include SharedArrayBuffer in the type system,\n\t// so we ensure we have a plain ArrayBuffer.\n\tlet buf: ArrayBuffer;\n\tif (content instanceof ArrayBuffer) {\n\t\tbuf = content;\n\t} else {\n\t\tbuf = new ArrayBuffer(content.byteLength);\n\t\tnew Uint8Array(buf).set(content);\n\t}\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-1\", buf);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\tconst hashHex = Array.from(hashArray, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\treturn `sha1:${hashHex}`;\n}\n","import { z, type ZodTypeAny } from \"zod\";\n\nimport { hashString } from \"../utils/hash.js\";\nimport type { Field, FieldType, CollectionWithFields } from \"./types.js\";\n\n/** Pattern to split on underscores, hyphens, and spaces for PascalCase conversion */\nconst PASCAL_CASE_SPLIT_PATTERN = /[_\\-\\s]+/;\n\n/**\n * Generate a Zod schema from a collection's field definitions\n *\n * This allows runtime validation of content based on dynamically\n * defined schemas stored in D1.\n */\nexport function generateZodSchema(\n\tcollection: CollectionWithFields,\n): z.ZodObject<Record<string, ZodTypeAny>> {\n\tconst shape: Record<string, ZodTypeAny> = {};\n\n\tfor (const field of collection.fields) {\n\t\tshape[field.slug] = generateFieldSchema(field);\n\t}\n\n\treturn z.object(shape);\n}\n\n/**\n * Generate Zod schema for a single field\n */\nexport function generateFieldSchema(field: Field): ZodTypeAny {\n\tlet schema = getBaseSchema(field.type, field);\n\n\t// Apply validation rules\n\tif (field.validation) {\n\t\tschema = applyValidation(schema, field);\n\t}\n\n\t// Apply required/optional\n\tif (!field.required) {\n\t\tschema = schema.optional();\n\t}\n\n\t// Apply default value\n\tif (field.defaultValue !== undefined) {\n\t\tschema = schema.default(field.defaultValue);\n\t}\n\n\treturn schema;\n}\n\n/**\n * Get base Zod schema for a field type\n */\nfunction getBaseSchema(type: FieldType, field: Field): ZodTypeAny {\n\tswitch (type) {\n\t\tcase \"url\":\n\t\t\treturn z.string().url();\n\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\t\treturn z.string();\n\n\t\tcase \"number\":\n\t\t\treturn z.number();\n\n\t\tcase \"integer\":\n\t\t\treturn z.number().int();\n\n\t\tcase \"boolean\":\n\t\t\treturn z.boolean();\n\n\t\tcase \"datetime\":\n\t\t\treturn z.string().datetime().or(z.string().date());\n\n\t\tcase \"select\": {\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\tconst [first, ...rest] = options;\n\t\t\t\treturn z.enum([first, ...rest]);\n\t\t\t}\n\t\t\treturn z.string();\n\t\t}\n\n\t\tcase \"multiSelect\": {\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\tconst [first, ...rest] = multiOptions;\n\t\t\t\treturn z.array(z.enum([first, ...rest]));\n\t\t\t}\n\t\t\treturn z.array(z.string());\n\t\t}\n\n\t\tcase \"portableText\":\n\t\t\t// Portable Text is an array of blocks\n\t\t\treturn z.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\t_type: z.string(),\n\t\t\t\t\t\t_key: z.string(),\n\t\t\t\t\t})\n\t\t\t\t\t.passthrough(),\n\t\t\t);\n\n\t\tcase \"image\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\talt: z.string().optional(),\n\t\t\t\twidth: z.number().optional(),\n\t\t\t\theight: z.number().optional(),\n\t\t\t});\n\n\t\tcase \"file\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\tfilename: z.string().optional(),\n\t\t\t\tmimeType: z.string().optional(),\n\t\t\t\tsize: z.number().optional(),\n\t\t\t});\n\n\t\tcase \"reference\":\n\t\t\treturn z.string(); // Reference ID\n\n\t\tcase \"json\":\n\t\t\treturn z.unknown();\n\n\t\tdefault:\n\t\t\treturn z.unknown();\n\t}\n}\n\n/**\n * Apply validation rules to a schema\n */\nfunction applyValidation(schema: ZodTypeAny, field: Field): ZodTypeAny {\n\tconst validation = field.validation;\n\tif (!validation) return schema;\n\n\t// String validations\n\tif (schema instanceof z.ZodString) {\n\t\tlet strSchema = schema;\n\t\tif (validation.minLength !== undefined) {\n\t\t\tstrSchema = strSchema.min(validation.minLength);\n\t\t}\n\t\tif (validation.maxLength !== undefined) {\n\t\t\tstrSchema = strSchema.max(validation.maxLength);\n\t\t}\n\t\tif (validation.pattern) {\n\t\t\tstrSchema = strSchema.regex(new RegExp(validation.pattern));\n\t\t}\n\t\treturn strSchema;\n\t}\n\n\t// Number validations\n\tif (schema instanceof z.ZodNumber) {\n\t\tlet numSchema = schema;\n\t\tif (validation.min !== undefined) {\n\t\t\tnumSchema = numSchema.min(validation.min);\n\t\t}\n\t\tif (validation.max !== undefined) {\n\t\t\tnumSchema = numSchema.max(validation.max);\n\t\t}\n\t\treturn numSchema;\n\t}\n\n\treturn schema;\n}\n\n/**\n * Schema cache to avoid regenerating schemas on every request\n */\nconst schemaCache = new Map<string, { schema: z.ZodObject<any>; version: string }>();\n\n/**\n * Get or generate a cached schema for a collection\n */\nexport function getCachedSchema(\n\tcollection: CollectionWithFields,\n\tversion?: string,\n): z.ZodObject<any> {\n\tconst cacheKey = collection.slug;\n\tconst cached = schemaCache.get(cacheKey);\n\n\t// If version matches, return cached schema\n\tif (cached && (!version || cached.version === version)) {\n\t\treturn cached.schema;\n\t}\n\n\t// Generate new schema\n\tconst schema = generateZodSchema(collection);\n\n\t// Cache it\n\tschemaCache.set(cacheKey, {\n\t\tschema,\n\t\tversion: version || collection.updatedAt,\n\t});\n\n\treturn schema;\n}\n\n/**\n * Invalidate cached schema for a collection\n */\nexport function invalidateSchemaCache(slug: string): void {\n\tschemaCache.delete(slug);\n}\n\n/**\n * Clear all cached schemas\n */\nexport function clearSchemaCache(): void {\n\tschemaCache.clear();\n}\n\n/**\n * Validate data against a collection's schema\n */\nexport function validateContent(\n\tcollection: CollectionWithFields,\n\tdata: unknown,\n): { success: true; data: unknown } | { success: false; errors: z.ZodError } {\n\tconst schema = getCachedSchema(collection);\n\n\tconst result = schema.safeParse(data);\n\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Generate TypeScript interface from field definitions\n * Used by CLI `emdash types` to generate types\n */\nexport function generateTypeScript(collection: CollectionWithFields): string {\n\tconst interfaceName = getInterfaceName(collection);\n\tconst lines: string[] = [];\n\n\tlines.push(`export interface ${interfaceName} {`);\n\tlines.push(` id: string;`);\n\tlines.push(` slug: string | null;`);\n\tlines.push(` status: string;`);\n\n\tfor (const field of collection.fields) {\n\t\tconst tsType = fieldTypeToTypeScript(field);\n\t\tconst optional = field.required ? \"\" : \"?\";\n\t\tlines.push(` ${field.slug}${optional}: ${tsType};`);\n\t}\n\n\tlines.push(` createdAt: Date;`);\n\tlines.push(` updatedAt: Date;`);\n\tlines.push(` publishedAt: Date | null;`);\n\t// Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry\n\tlines.push(` bylines?: ContentBylineCredit[];`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete types file with module augmentation\n * This produces emdash-env.d.ts content that provides typed query functions\n */\nexport function generateTypesFile(collections: CollectionWithFields[]): string {\n\tconst lines: string[] = [];\n\n\t// Header\n\tlines.push(`// Generated by EmDash on dev server start`);\n\tlines.push(`// Do not edit manually`);\n\tlines.push(``);\n\tlines.push(`/// <reference types=\"emdash/locals\" />`);\n\tlines.push(``);\n\n\t// Check if we need PortableTextBlock import\n\tconst needsPortableText = collections.some((c) =>\n\t\tc.fields.some((f) => f.type === \"portableText\"),\n\t);\n\n\t// Build imports - ContentBylineCredit is always needed for bylines\n\tconst imports = [\"ContentBylineCredit\"];\n\tif (needsPortableText) {\n\t\timports.push(\"PortableTextBlock\");\n\t}\n\tlines.push(`import type { ${imports.join(\", \")} } from \"emdash\";`);\n\tlines.push(``);\n\n\t// Generate individual interfaces\n\tfor (const collection of collections) {\n\t\tlines.push(generateTypeScript(collection));\n\t\tlines.push(``);\n\t}\n\n\t// Generate the Collections interface for module augmentation\n\tlines.push(`declare module \"emdash\" {`);\n\tlines.push(` interface EmDashCollections {`);\n\tfor (const collection of collections) {\n\t\tconst interfaceName = getInterfaceName(collection);\n\t\tlines.push(` ${collection.slug}: ${interfaceName};`);\n\t}\n\tlines.push(` }`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate schema hash for cache invalidation\n */\nexport async function generateSchemaHash(collections: CollectionWithFields[]): Promise<string> {\n\tconst str = JSON.stringify(\n\t\tcollections.map((c) => ({\n\t\t\tslug: c.slug,\n\t\t\tfields: c.fields.map((f) => ({\n\t\t\t\tslug: f.slug,\n\t\t\t\ttype: f.type,\n\t\t\t\trequired: f.required,\n\t\t\t\tvalidation: f.validation,\n\t\t\t})),\n\t\t})),\n\t);\n\treturn hashString(str);\n}\n\n/**\n * Map field type to TypeScript type\n */\nfunction fieldTypeToTypeScript(field: Field): string {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\tcase \"url\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\treturn \"number\";\n\n\t\tcase \"boolean\":\n\t\t\treturn \"boolean\";\n\n\t\tcase \"select\":\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\treturn options.map((o) => `\"${o}\"`).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\n\t\tcase \"multiSelect\":\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\treturn `(${multiOptions.map((o) => `\"${o}\"`).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\n\t\tcase \"portableText\":\n\t\t\treturn \"PortableTextBlock[]\";\n\n\t\tcase \"image\":\n\t\t\treturn \"{ id: string; src?: string; alt?: string; width?: number; height?: number }\";\n\n\t\tcase \"file\":\n\t\t\treturn \"{ id: string; src?: string; filename?: string; mimeType?: string; size?: number }\";\n\n\t\tcase \"reference\":\n\t\t\t// Could be enhanced to include the referenced collection type\n\t\t\treturn \"string\";\n\n\t\tcase \"json\":\n\t\t\treturn \"unknown\";\n\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Convert string to PascalCase (handles slugs, spaces, etc.)\n */\nfunction pascalCase(str: string): string {\n\treturn str\n\t\t.split(PASCAL_CASE_SPLIT_PATTERN)\n\t\t.filter(Boolean)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n\t\t.join(\"\");\n}\n\n/**\n * Simple singularization - handles common cases\n */\nfunction singularize(str: string): string {\n\tif (str.endsWith(\"ies\")) {\n\t\treturn str.slice(0, -3) + \"y\";\n\t}\n\tif (\n\t\tstr.endsWith(\"es\") &&\n\t\t(str.endsWith(\"sses\") || str.endsWith(\"xes\") || str.endsWith(\"ches\") || str.endsWith(\"shes\"))\n\t) {\n\t\treturn str.slice(0, -2);\n\t}\n\tif (str.endsWith(\"s\") && !str.endsWith(\"ss\")) {\n\t\treturn str.slice(0, -1);\n\t}\n\treturn str;\n}\n\n/**\n * Get the interface name for a collection\n */\nfunction getInterfaceName(collection: CollectionWithFields): string {\n\treturn pascalCase(collection.labelSingular || singularize(collection.slug));\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emdash",
3
- "version": "0.6.0",
3
+ "version": "1.0.0",
4
4
  "description": "Astro-native CMS with WordPress migration support",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -45,6 +45,12 @@
45
45
  "default": "./dist/cli/index.mjs"
46
46
  },
47
47
  "./routes/*": "./src/astro/routes/*",
48
+ "./api/route-utils": "./src/api/route-utils.ts",
49
+ "./api/schemas": "./src/api/schemas/index.ts",
50
+ "./auth/providers/github": "./src/auth/providers/github.ts",
51
+ "./auth/providers/github-admin": "./src/auth/providers/github-admin.tsx",
52
+ "./auth/providers/google": "./src/auth/providers/google.ts",
53
+ "./auth/providers/google-admin": "./src/auth/providers/google-admin.tsx",
48
54
  "./db": {
49
55
  "types": "./dist/db/index.d.mts",
50
56
  "default": "./dist/db/index.mjs"
@@ -185,9 +191,9 @@
185
191
  "ulidx": "^2.4.1",
186
192
  "upng-js": "^2.1.0",
187
193
  "zod": "^4.3.5",
188
- "@emdash-cms/auth": "0.6.0",
189
- "@emdash-cms/admin": "0.6.0",
190
- "@emdash-cms/gutenberg-to-portable-text": "0.6.0"
194
+ "@emdash-cms/admin": "1.0.0",
195
+ "@emdash-cms/auth": "1.0.0",
196
+ "@emdash-cms/gutenberg-to-portable-text": "1.0.0"
191
197
  },
192
198
  "optionalDependencies": {
193
199
  "@libsql/kysely-libsql": "^0.4.0",
@@ -199,12 +205,19 @@
199
205
  "@tanstack/react-router": ">=1.100.0",
200
206
  "astro": ">=6.0.0-beta.0",
201
207
  "react": ">=18.0.0",
202
- "react-dom": ">=18.0.0"
208
+ "react-dom": ">=18.0.0",
209
+ "@emdash-cms/auth-atproto": "1.0.0"
210
+ },
211
+ "peerDependenciesMeta": {
212
+ "@emdash-cms/auth-atproto": {
213
+ "optional": true
214
+ }
203
215
  },
204
216
  "devDependencies": {
205
217
  "@apidevtools/swagger-parser": "^12.1.0",
206
218
  "@arethetypeswrong/cli": "^0.18.2",
207
219
  "@types/better-sqlite3": "^7.6.12",
220
+ "@types/react": "19.2.14",
208
221
  "@types/pg": "^8.16.0",
209
222
  "@types/sanitize-html": "^2.16.0",
210
223
  "@types/sax": "^1.2.7",
@@ -215,7 +228,7 @@
215
228
  "vite": "^6.0.0",
216
229
  "vitest": "^4.0.18",
217
230
  "zod-openapi": "^5.4.6",
218
- "@emdash-cms/blocks": "0.6.0"
231
+ "@emdash-cms/blocks": "1.0.0"
219
232
  },
220
233
  "repository": {
221
234
  "type": "git",
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Auth provider storage helper.
3
+ *
4
+ * Gives auth provider routes access to plugin-style storage collections
5
+ * namespaced under `auth:<providerId>`. Reuses the existing `_plugin_storage`
6
+ * table and `PluginStorageRepository` infrastructure.
7
+ */
8
+
9
+ import type { Kysely } from "kysely";
10
+
11
+ import type { Database } from "../database/types.js";
12
+ import { createStorageAccess } from "../plugins/context.js";
13
+ import type { StorageCollection, StorageCollectionConfig } from "../plugins/types.js";
14
+
15
+ /**
16
+ * Get storage collections for an auth provider.
17
+ *
18
+ * Returns a record of `StorageCollection` instances, one per declared
19
+ * collection in the provider's `storage` config. Data is stored in the
20
+ * shared `_plugin_storage` table under the namespace `auth:<providerId>`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const storage = getAuthProviderStorage(emdash.db, "atproto", {
25
+ * states: { indexes: [] },
26
+ * sessions: { indexes: [] },
27
+ * });
28
+ * const session = await storage.sessions.get(sessionId);
29
+ * ```
30
+ */
31
+ export function getAuthProviderStorage(
32
+ db: Kysely<Database>,
33
+ providerId: string,
34
+ storageConfig: Record<string, StorageCollectionConfig>,
35
+ ): Record<string, StorageCollection> {
36
+ return createStorageAccess(db, `auth:${providerId}`, storageConfig);
37
+ }
package/src/api/error.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * `new Response(JSON.stringify({ error: ... }), ...)` patterns.
6
6
  */
7
7
 
8
+ import { InvalidCursorError } from "../database/repositories/types.js";
8
9
  import { mapErrorStatus } from "./errors.js";
9
10
  import type { ApiResult } from "./types.js";
10
11
 
@@ -54,6 +55,11 @@ export function handleError(
54
55
  fallbackMessage: string,
55
56
  fallbackCode: string,
56
57
  ): Response {
58
+ // Bubble malformed-cursor errors as a structured 400 instead of a
59
+ // generic 500.
60
+ if (error instanceof InvalidCursorError) {
61
+ return apiError("INVALID_CURSOR", error.message, 400);
62
+ }
57
63
  console.error(`[${fallbackCode}]`, error);
58
64
  return apiError(fallbackCode, fallbackMessage, 500);
59
65
  }
package/src/api/errors.ts CHANGED
@@ -12,7 +12,9 @@ export const ErrorCode = {
12
12
  VALIDATION_ERROR: "VALIDATION_ERROR",
13
13
  INVALID_INPUT: "INVALID_INPUT",
14
14
  INVALID_JSON: "INVALID_JSON",
15
+ INVALID_CURSOR: "INVALID_CURSOR",
15
16
  CONFLICT: "CONFLICT",
17
+ SLUG_CONFLICT: "SLUG_CONFLICT",
16
18
  NOT_CONFIGURED: "NOT_CONFIGURED",
17
19
  UNAUTHORIZED: "UNAUTHORIZED",
18
20
  FORBIDDEN: "FORBIDDEN",
@@ -152,6 +154,8 @@ export const ErrorCode = {
152
154
  INVALID_CODE: "INVALID_CODE",
153
155
  EXPIRED_CODE: "EXPIRED_CODE",
154
156
  INSUFFICIENT_ROLE: "INSUFFICIENT_ROLE",
157
+ INSUFFICIENT_SCOPE: "INSUFFICIENT_SCOPE",
158
+ INSUFFICIENT_PERMISSIONS: "INSUFFICIENT_PERMISSIONS",
155
159
  TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
156
160
  TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
157
161
  TOKEN_REVOKE_ERROR: "TOKEN_REVOKE_ERROR",
@@ -335,6 +339,7 @@ export function mapErrorStatus(code: string | undefined): number {
335
339
  case ErrorCode.VALIDATION_ERROR:
336
340
  case ErrorCode.INVALID_INPUT:
337
341
  case ErrorCode.INVALID_JSON:
342
+ case ErrorCode.INVALID_CURSOR:
338
343
  case ErrorCode.MISSING_PARAM:
339
344
  case ErrorCode.INVALID_REQUEST:
340
345
  case ErrorCode.NOT_SUPPORTED:
@@ -373,6 +378,8 @@ export function mapErrorStatus(code: string | undefined): number {
373
378
  case ErrorCode.COMMENT_REJECTED:
374
379
  case ErrorCode.DOMAIN_NOT_ALLOWED:
375
380
  case ErrorCode.INSUFFICIENT_ROLE:
381
+ case ErrorCode.INSUFFICIENT_SCOPE:
382
+ case ErrorCode.INSUFFICIENT_PERMISSIONS:
376
383
  case ErrorCode.CAPABILITY_ESCALATION:
377
384
  case ErrorCode.ROUTE_VISIBILITY_ESCALATION:
378
385
  case ErrorCode.AUDIT_FAILED:
@@ -388,6 +395,7 @@ export function mapErrorStatus(code: string | undefined): number {
388
395
 
389
396
  // 409 Conflict
390
397
  case ErrorCode.CONFLICT:
398
+ case ErrorCode.SLUG_CONFLICT:
391
399
  case ErrorCode.COLLECTION_EXISTS:
392
400
  case ErrorCode.FIELD_EXISTS:
393
401
  case ErrorCode.CREDENTIAL_EXISTS:
@@ -8,6 +8,7 @@ import type { Kysely } from "kysely";
8
8
 
9
9
  import { CommentRepository } from "../../database/repositories/comment.js";
10
10
  import type { Comment, CommentStatus, PublicComment } from "../../database/repositories/comment.js";
11
+ import { InvalidCursorError } from "../../database/repositories/types.js";
11
12
  import type { Database } from "../../database/types.js";
12
13
  import type { ApiResult } from "../types.js";
13
14
 
@@ -60,6 +61,12 @@ export async function handleCommentList(
60
61
  },
61
62
  };
62
63
  } catch (error) {
64
+ if (error instanceof InvalidCursorError) {
65
+ return {
66
+ success: false,
67
+ error: { code: "INVALID_CURSOR", message: error.message },
68
+ };
69
+ }
63
70
  console.error("Comment list error:", error);
64
71
  return {
65
72
  success: false,
@@ -104,6 +111,12 @@ export async function handleCommentInbox(
104
111
  },
105
112
  };
106
113
  } catch (error) {
114
+ if (error instanceof InvalidCursorError) {
115
+ return {
116
+ success: false,
117
+ error: { code: "INVALID_CURSOR", message: error.message },
118
+ };
119
+ }
107
120
  console.error("Comment inbox error:", error);
108
121
  return {
109
122
  success: false,