emdash 0.5.0 → 0.6.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 (205) hide show
  1. package/dist/{adapters-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
  2. package/dist/adapters-Di31kZ28.d.mts.map +1 -0
  3. package/dist/{apply-Cma_PiF6.mjs → apply-B4MsLM-w.mjs} +27 -12
  4. package/dist/apply-B4MsLM-w.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 +199 -33
  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 +30 -4
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.mjs +1 -1
  14. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  15. package/dist/astro/middleware/request-context.mjs +5 -3
  16. package/dist/astro/middleware/request-context.mjs.map +1 -1
  17. package/dist/astro/middleware/setup.mjs +1 -1
  18. package/dist/astro/middleware.d.mts.map +1 -1
  19. package/dist/astro/middleware.mjs +460 -180
  20. package/dist/astro/middleware.mjs.map +1 -1
  21. package/dist/astro/types.d.mts +8 -9
  22. package/dist/astro/types.d.mts.map +1 -1
  23. package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
  24. package/dist/byline-C4OVd8b3.mjs.map +1 -0
  25. package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
  26. package/dist/bylines-hPTW79hw.mjs.map +1 -0
  27. package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
  28. package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
  29. package/dist/chunks-HGz06Soa.mjs +19 -0
  30. package/dist/chunks-HGz06Soa.mjs.map +1 -0
  31. package/dist/cli/index.mjs +9 -8
  32. package/dist/cli/index.mjs.map +1 -1
  33. package/dist/client/cf-access.d.mts +1 -1
  34. package/dist/client/index.d.mts +1 -1
  35. package/dist/client/index.mjs +1 -1
  36. package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
  37. package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
  38. package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
  39. package/dist/connection-2igzM-AT.mjs.map +1 -0
  40. package/dist/database/instrumentation.d.mts +45 -0
  41. package/dist/database/instrumentation.d.mts.map +1 -0
  42. package/dist/database/instrumentation.mjs +61 -0
  43. package/dist/database/instrumentation.mjs.map +1 -0
  44. package/dist/db/index.d.mts +3 -3
  45. package/dist/db/index.mjs.map +1 -1
  46. package/dist/db/libsql.d.mts +1 -1
  47. package/dist/db/postgres.d.mts +1 -1
  48. package/dist/db/sqlite.d.mts +1 -1
  49. package/dist/db-errors-D0UT85nC.mjs +41 -0
  50. package/dist/db-errors-D0UT85nC.mjs.map +1 -0
  51. package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
  52. package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
  53. package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
  54. package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
  55. package/dist/{index-CCWzlriB.d.mts → index-BYv0mB9g.d.mts} +135 -19
  56. package/dist/index-BYv0mB9g.d.mts.map +1 -0
  57. package/dist/index.d.mts +11 -11
  58. package/dist/index.mjs +20 -18
  59. package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
  60. package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
  61. package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
  62. package/dist/loader-DeiBJEMe.mjs.map +1 -0
  63. package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
  64. package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
  65. package/dist/media/index.d.mts +1 -1
  66. package/dist/media/index.mjs +1 -1
  67. package/dist/media/local-runtime.d.mts +7 -7
  68. package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
  69. package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
  70. package/dist/page/index.d.mts +11 -2
  71. package/dist/page/index.d.mts.map +1 -1
  72. package/dist/page/index.mjs +23 -1
  73. package/dist/page/index.mjs.map +1 -1
  74. package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
  75. package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
  76. package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
  77. package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
  78. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  79. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  80. package/dist/{query-B6Vu0d2i.mjs → query-Bk_3vKvU.mjs} +78 -11
  81. package/dist/query-Bk_3vKvU.mjs.map +1 -0
  82. package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
  83. package/dist/registry-Ci3WxVAr.mjs.map +1 -0
  84. package/dist/request-cache-DiR961CV.mjs +79 -0
  85. package/dist/request-cache-DiR961CV.mjs.map +1 -0
  86. package/dist/request-context.d.mts +19 -16
  87. package/dist/request-context.d.mts.map +1 -1
  88. package/dist/request-context.mjs.map +1 -1
  89. package/dist/{runner-DYv3rX8P.d.mts → runner-Fl2NcUUz.d.mts} +2 -2
  90. package/dist/{runner-DYv3rX8P.d.mts.map → runner-Fl2NcUUz.d.mts.map} +1 -1
  91. package/dist/runtime.d.mts +6 -6
  92. package/dist/runtime.mjs +1 -1
  93. package/dist/{search-Cn1SYvYF.mjs → search-DI4bM2w9.mjs} +96 -206
  94. package/dist/search-DI4bM2w9.mjs.map +1 -0
  95. package/dist/seed/index.d.mts +2 -2
  96. package/dist/seed/index.mjs +8 -7
  97. package/dist/seo/index.d.mts +1 -1
  98. package/dist/storage/local.d.mts +1 -1
  99. package/dist/storage/local.mjs +1 -1
  100. package/dist/storage/s3.d.mts +1 -1
  101. package/dist/storage/s3.mjs +1 -1
  102. package/dist/taxonomies-DbrKzDju.mjs +308 -0
  103. package/dist/taxonomies-DbrKzDju.mjs.map +1 -0
  104. package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
  105. package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
  106. package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
  107. package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
  108. package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
  109. package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
  110. package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
  111. package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
  112. package/dist/{types-B6BzlZxx.d.mts → types-8xrvl_68.d.mts} +1 -1
  113. package/dist/{types-B6BzlZxx.d.mts.map → types-8xrvl_68.d.mts.map} +1 -1
  114. package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
  115. package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
  116. package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
  117. package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
  118. package/dist/{types-DeG21anB.d.mts → types-CnZYHyLW.d.mts} +55 -5
  119. package/dist/types-CnZYHyLW.d.mts.map +1 -0
  120. package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
  121. package/dist/types-DDS4MxsT.mjs.map +1 -0
  122. package/dist/{types-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
  123. package/dist/types-DgrIP0tF.d.mts.map +1 -0
  124. package/dist/{validate-Db1yNL3i.d.mts → validate-CaLH1Ia2.d.mts} +5 -52
  125. package/dist/validate-CaLH1Ia2.d.mts.map +1 -0
  126. package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
  127. package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
  128. package/dist/version-Uaf2ynPX.mjs +7 -0
  129. package/dist/{version-CMMjTuqu.mjs.map → version-Uaf2ynPX.mjs.map} +1 -1
  130. package/package.json +10 -5
  131. package/src/after.ts +62 -0
  132. package/src/api/handlers/oauth-authorization.ts +2 -32
  133. package/src/api/handlers/oauth-clients.ts +40 -4
  134. package/src/api/handlers/taxonomies.ts +13 -0
  135. package/src/api/oauth/redirect-uri.ts +34 -0
  136. package/src/api/openapi/document.ts +126 -118
  137. package/src/api/schemas/media.ts +26 -15
  138. package/src/api/schemas/schema.ts +1 -0
  139. package/src/astro/integration/font-provider.ts +176 -0
  140. package/src/astro/integration/index.ts +42 -0
  141. package/src/astro/integration/routes.ts +6 -0
  142. package/src/astro/integration/runtime.ts +63 -0
  143. package/src/astro/integration/virtual-modules.ts +41 -39
  144. package/src/astro/integration/vite-config.ts +16 -5
  145. package/src/astro/middleware/auth.ts +33 -1
  146. package/src/astro/middleware/request-context.ts +15 -3
  147. package/src/astro/middleware.ts +340 -263
  148. package/src/astro/routes/admin.astro +7 -3
  149. package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
  150. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
  151. package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
  152. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +1 -1
  153. package/src/astro/routes/api/media/upload-url.ts +10 -2
  154. package/src/astro/routes/api/media.ts +10 -7
  155. package/src/astro/routes/api/oauth/register.ts +178 -0
  156. package/src/astro/routes/api/oauth/token.ts +15 -0
  157. package/src/astro/routes/api/openapi.json.ts +15 -5
  158. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
  159. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
  160. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
  161. package/src/astro/routes/api/search/index.ts +5 -0
  162. package/src/astro/routes/api/search/suggest.ts +3 -0
  163. package/src/astro/routes/api/taxonomies/index.ts +1 -0
  164. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
  165. package/src/bylines/index.ts +22 -45
  166. package/src/components/EmDashHead.astro +23 -7
  167. package/src/database/connection.ts +23 -1
  168. package/src/database/instrumentation.ts +98 -0
  169. package/src/db/adapters.ts +15 -0
  170. package/src/emdash-runtime.ts +309 -91
  171. package/src/index.ts +6 -0
  172. package/src/loader.ts +19 -24
  173. package/src/menus/index.ts +6 -3
  174. package/src/page/index.ts +1 -1
  175. package/src/page/seo-contributions.ts +36 -0
  176. package/src/query.ts +104 -7
  177. package/src/request-cache.ts +106 -0
  178. package/src/request-context.ts +19 -0
  179. package/src/schema/query.ts +5 -2
  180. package/src/schema/registry.ts +243 -166
  181. package/src/schema/types.ts +13 -2
  182. package/src/schema/zod-generator.ts +4 -0
  183. package/src/search/fts-manager.ts +19 -5
  184. package/src/search/query.ts +4 -3
  185. package/src/seed/apply.ts +15 -1
  186. package/src/settings/index.ts +24 -5
  187. package/src/taxonomies/index.ts +324 -124
  188. package/src/utils/db-errors.ts +46 -0
  189. package/src/virtual-modules.d.ts +31 -10
  190. package/src/widgets/index.ts +54 -25
  191. package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
  192. package/dist/apply-Cma_PiF6.mjs.map +0 -1
  193. package/dist/byline-WuOq9MFJ.mjs.map +0 -1
  194. package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
  195. package/dist/connection-B4zVnQIa.mjs.map +0 -1
  196. package/dist/index-CCWzlriB.d.mts.map +0 -1
  197. package/dist/loader-BYzwzORf.mjs.map +0 -1
  198. package/dist/query-B6Vu0d2i.mjs.map +0 -1
  199. package/dist/registry-BgnP3ysR.mjs.map +0 -1
  200. package/dist/search-Cn1SYvYF.mjs.map +0 -1
  201. package/dist/types-C3ronwXb.d.mts.map +0 -1
  202. package/dist/types-DeG21anB.d.mts.map +0 -1
  203. package/dist/types-xxCWI3j0.mjs.map +0 -1
  204. package/dist/validate-Db1yNL3i.d.mts.map +0 -1
  205. package/dist/version-CMMjTuqu.mjs +0 -7
@@ -56,6 +56,9 @@ export const RESOLVED_VIRTUAL_BLOCK_COMPONENTS_ID = "\0" + VIRTUAL_BLOCK_COMPONE
56
56
  export const VIRTUAL_SEED_ID = "virtual:emdash/seed";
57
57
  export const RESOLVED_VIRTUAL_SEED_ID = "\0" + VIRTUAL_SEED_ID;
58
58
 
59
+ export const VIRTUAL_WAIT_UNTIL_ID = "virtual:emdash/wait-until";
60
+ export const RESOLVED_VIRTUAL_WAIT_UNTIL_ID = "\0" + VIRTUAL_WAIT_UNTIL_ID;
61
+
59
62
  /**
60
63
  * Generates the config virtual module.
61
64
  */
@@ -65,62 +68,42 @@ export function generateConfigModule(serializableConfig: Record<string, unknown>
65
68
 
66
69
  /**
67
70
  * Generates the dialect virtual module.
68
- * Statically imports the configured database dialect and exports the dialect type.
69
- *
70
- * For D1 adapters, also re-exports session helpers (isSessionEnabled, getD1Binding,
71
- * getDefaultConstraint, getBookmarkCookieName, createSessionDialect) used by
72
- * middleware for per-request read replica sessions.
73
71
  *
74
- * For non-D1 adapters, session exports are no-ops.
72
+ * Adapters that set `supportsRequestScope: true` on their descriptor are
73
+ * expected to export `createRequestScopedDb` from their runtime entrypoint;
74
+ * the generator re-exports it so middleware can ask for a per-request Kysely
75
+ * (used for D1 Sessions API, bookmark cookies, read-replica routing). Other
76
+ * adapters get a stub that returns null.
75
77
  */
76
- export function generateDialectModule(
77
- dbEntrypoint?: string,
78
- dbType?: string,
79
- dbConfig?: unknown,
80
- ): string {
81
- if (!dbEntrypoint) {
78
+ export function generateDialectModule(opts: {
79
+ entrypoint?: string;
80
+ type?: string;
81
+ supportsRequestScope: boolean;
82
+ }): string {
83
+ const { entrypoint, supportsRequestScope } = opts;
84
+ if (!entrypoint) {
82
85
  return [
83
86
  `export const createDialect = undefined;`,
84
87
  `export const dialectType = "sqlite";`,
85
- `export const isSessionEnabled = () => false;`,
86
- `export const getD1Binding = () => null;`,
87
- `export const getDefaultConstraint = () => "first-unconstrained";`,
88
- `export const getBookmarkCookieName = () => "";`,
89
- `export const createSessionDialect = undefined;`,
88
+ `export const createRequestScopedDb = (_opts) => null;`,
90
89
  ].join("\n");
91
90
  }
92
- const type = dbType ?? "sqlite";
91
+ const type = opts.type ?? "sqlite";
93
92
 
94
- // Check if the adapter is D1 (has session helpers)
95
- const isD1 = dbEntrypoint.includes("cloudflare") && dbEntrypoint.includes("d1");
96
-
97
- // Check if sessions are enabled in the config
98
- const sessionMode =
99
- isD1 && dbConfig && typeof dbConfig === "object" && "session" in dbConfig
100
- ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- runtime-checked above
101
- (dbConfig as { session?: string }).session
102
- : undefined;
103
- const sessionEnabled = !!sessionMode && sessionMode !== "disabled";
104
-
105
- if (isD1 && sessionEnabled) {
93
+ if (supportsRequestScope) {
106
94
  return `
107
- import { createDialect as _createDialect } from "${dbEntrypoint}";
108
- export { isSessionEnabled, getD1Binding, getDefaultConstraint, getBookmarkCookieName, createSessionDialect } from "${dbEntrypoint}";
95
+ import { createDialect as _createDialect } from "${entrypoint}";
96
+ export { createRequestScopedDb } from "${entrypoint}";
109
97
  export const createDialect = _createDialect;
110
98
  export const dialectType = ${JSON.stringify(type)};
111
99
  `;
112
100
  }
113
101
 
114
- // Non-D1 or sessions disabled: export no-ops
115
102
  return `
116
- import { createDialect as _createDialect } from "${dbEntrypoint}";
103
+ import { createDialect as _createDialect } from "${entrypoint}";
117
104
  export const createDialect = _createDialect;
118
105
  export const dialectType = ${JSON.stringify(type)};
119
- export const isSessionEnabled = () => false;
120
- export const getD1Binding = () => null;
121
- export const getDefaultConstraint = () => "first-unconstrained";
122
- export const getBookmarkCookieName = () => "";
123
- export const createSessionDialect = undefined;
106
+ export const createRequestScopedDb = (_opts) => null;
124
107
  `;
125
108
  }
126
109
 
@@ -353,6 +336,25 @@ export function generateBlockComponentsModule(descriptors: PluginDescriptor[]):
353
336
  return `${imports.join("\n")}\nexport const pluginBlockComponents = { ${spreads.join(", ")} };`;
354
337
  }
355
338
 
339
+ /**
340
+ * Generates the wait-until virtual module.
341
+ *
342
+ * Under @astrojs/cloudflare, re-exports `waitUntil` from `cloudflare:workers`
343
+ * so `after(fn)` in core can extend the worker's lifetime past the response
344
+ * for deferred bookkeeping. For any other adapter, exports `undefined` —
345
+ * Node's long-lived event loop keeps deferred promises running without a
346
+ * lifetime extender.
347
+ *
348
+ * Keeping the adapter check here — rather than in core — means core itself
349
+ * has no Cloudflare-specific imports or code paths.
350
+ */
351
+ export function generateWaitUntilModule(adapterName: string | undefined): string {
352
+ if (adapterName === "@astrojs/cloudflare") {
353
+ return `export { waitUntil } from "cloudflare:workers";`;
354
+ }
355
+ return `export const waitUntil = undefined;`;
356
+ }
357
+
356
358
  /**
357
359
  * Generates the seed virtual module.
358
360
  * Reads the user's seed file at build time (in Node context) and embeds it,
@@ -38,7 +38,10 @@ import {
38
38
  RESOLVED_VIRTUAL_BLOCK_COMPONENTS_ID,
39
39
  VIRTUAL_SEED_ID,
40
40
  RESOLVED_VIRTUAL_SEED_ID,
41
+ VIRTUAL_WAIT_UNTIL_ID,
42
+ RESOLVED_VIRTUAL_WAIT_UNTIL_ID,
41
43
  generateSeedModule,
44
+ generateWaitUntilModule,
42
45
  generateConfigModule,
43
46
  generateDialectModule,
44
47
  generateStorageModule,
@@ -176,6 +179,9 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
176
179
  if (id === VIRTUAL_SEED_ID) {
177
180
  return RESOLVED_VIRTUAL_SEED_ID;
178
181
  }
182
+ if (id === VIRTUAL_WAIT_UNTIL_ID) {
183
+ return RESOLVED_VIRTUAL_WAIT_UNTIL_ID;
184
+ }
179
185
  },
180
186
  load(id: string) {
181
187
  if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
@@ -184,11 +190,11 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
184
190
  // Generate a module that statically imports the configured dialect
185
191
  // This allows Vite to properly resolve and bundle it
186
192
  if (id === RESOLVED_VIRTUAL_DIALECT_ID) {
187
- return generateDialectModule(
188
- resolvedConfig.database?.entrypoint,
189
- resolvedConfig.database?.type,
190
- resolvedConfig.database?.config,
191
- );
193
+ return generateDialectModule({
194
+ entrypoint: resolvedConfig.database?.entrypoint,
195
+ type: resolvedConfig.database?.type,
196
+ supportsRequestScope: resolvedConfig.database?.supportsRequestScope ?? false,
197
+ });
192
198
  }
193
199
  // Generate a module that statically imports the configured storage
194
200
  if (id === RESOLVED_VIRTUAL_STORAGE_ID) {
@@ -235,6 +241,11 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
235
241
  const projectRoot = fileURLToPath(astroConfig.root);
236
242
  return generateSeedModule(projectRoot);
237
243
  }
244
+ // Generate wait-until module — re-exports cloudflare:workers'
245
+ // waitUntil under the Cloudflare adapter, undefined otherwise.
246
+ if (id === RESOLVED_VIRTUAL_WAIT_UNTIL_ID) {
247
+ return generateWaitUntilModule(astroConfig.adapter?.name);
248
+ }
238
249
  },
239
250
  };
240
251
  }
@@ -102,6 +102,7 @@ const PUBLIC_API_PREFIXES = [
102
102
  "/_emdash/api/oauth/device/token",
103
103
  "/_emdash/api/oauth/device/code",
104
104
  "/_emdash/api/oauth/token",
105
+ "/_emdash/api/oauth/register",
105
106
  "/_emdash/api/comments/",
106
107
  "/_emdash/api/media/file/",
107
108
  "/_emdash/.well-known/",
@@ -118,6 +119,30 @@ const PUBLIC_API_EXACT = new Set([
118
119
  "/_emdash/api/search",
119
120
  ]);
120
121
 
122
+ /**
123
+ * OAuth protocol endpoints that are CSRF-exempt by design.
124
+ *
125
+ * These are RFC-defined endpoints (RFC 6749 §3.2, RFC 7591 §3, RFC 8628 §3.1/§3.4)
126
+ * specified to be called cross-origin by external clients (MCP clients, CLIs,
127
+ * native apps). They authenticate each request on its own merits:
128
+ *
129
+ * - /oauth/token: requires PKCE code_verifier, device_code, or refresh_token
130
+ * - /oauth/register: RFC 7591 dynamic client registration — anonymous by design
131
+ * - /oauth/device/code: RFC 8628 device flow initiation — anonymous by design
132
+ * - /oauth/device/token: requires device_code the client already holds
133
+ *
134
+ * None of these rely on ambient cookie credentials, so browser-based CSRF
135
+ * attacks have nothing to exploit. The endpoints themselves advertise
136
+ * `Access-Control-Allow-Origin: *`. Note: /oauth/device/authorize (the user
137
+ * consent step) is NOT in this list — it is session-authenticated.
138
+ */
139
+ const CSRF_EXEMPT_PUBLIC_ROUTES = new Set([
140
+ "/_emdash/api/oauth/token",
141
+ "/_emdash/api/oauth/register",
142
+ "/_emdash/api/oauth/device/code",
143
+ "/_emdash/api/oauth/device/token",
144
+ ]);
145
+
121
146
  function isPublicEmDashRoute(pathname: string): boolean {
122
147
  if (PUBLIC_API_EXACT.has(pathname)) return true;
123
148
  if (PUBLIC_API_PREFIXES.some((p) => pathname.startsWith(p))) return true;
@@ -125,6 +150,10 @@ function isPublicEmDashRoute(pathname: string): boolean {
125
150
  return false;
126
151
  }
127
152
 
153
+ function isCsrfExemptPublicRoute(pathname: string): boolean {
154
+ return CSRF_EXEMPT_PUBLIC_ROUTES.has(pathname);
155
+ }
156
+
128
157
  export const onRequest = defineMiddleware(async (context, next) => {
129
158
  const { url } = context;
130
159
 
@@ -141,7 +170,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
141
170
  // This prevents cross-origin form submissions and fetch requests from malicious sites.
142
171
  if (isPublicApiRoute) {
143
172
  const method = context.request.method.toUpperCase();
144
- if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
173
+ if (
174
+ isUnsafeMethod(method) &&
175
+ !isCsrfExemptPublicRoute(url.pathname) // OAuth protocol endpoints — cross-origin by design
176
+ ) {
145
177
  const publicOrigin = getPublicOrigin(url, context.locals.emdash?.config);
146
178
  const csrfError = checkPublicCsrf(context.request, url, publicOrigin);
147
179
  if (csrfError) return csrfError;
@@ -13,7 +13,7 @@
13
13
  import { defineMiddleware } from "astro:middleware";
14
14
 
15
15
  import { verifyPreviewToken, parseContentId } from "../../preview/tokens.js";
16
- import { runWithContext } from "../../request-context.js";
16
+ import { getRequestContext, runWithContext } from "../../request-context.js";
17
17
  import { renderToolbar } from "../../visual-editing/toolbar.js";
18
18
 
19
19
  /**
@@ -49,11 +49,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
49
49
  // Playground mode: the playground middleware (from @emdash-cms/cloudflare) stashes
50
50
  // the per-session DO database on locals.__playgroundDb. We set it via ALS here
51
51
  // (same module instance as the loader) so getDb() picks it up correctly.
52
+ //
53
+ // `dbIsIsolated: true` tells schema-derived caches (manifest, taxonomy defs,
54
+ // byline/term existence probes) to bypass module-scope memoization — each
55
+ // playground session is its own database with its own schema, so a cached
56
+ // value from another session would be wrong.
52
57
  const playgroundDb = context.locals.__playgroundDb;
53
58
  if (playgroundDb) {
54
59
  // Check if playground user has toggled edit mode on
55
60
  const hasEditCookie = cookies.get("emdash-edit-mode")?.value === "true";
56
- return runWithContext({ editMode: hasEditCookie, db: playgroundDb }, () => next());
61
+ return runWithContext({ editMode: hasEditCookie, db: playgroundDb, dbIsIsolated: true }, () =>
62
+ next(),
63
+ );
57
64
  }
58
65
 
59
66
  // Fast path: check for CMS signals before doing any work
@@ -90,7 +97,12 @@ export const onRequest = defineMiddleware(async (context, next) => {
90
97
  const needsContext = hasEditCookie || hasPreviewToken;
91
98
 
92
99
  if (needsContext) {
93
- return runWithContext({ editMode, preview, locale }, async () => {
100
+ // Merge with any outer ALS context (e.g. the per-request D1 session db
101
+ // set by the runtime middleware). `storage.run()` replaces the store
102
+ // wholesale, so without the spread the outer `db` would be lost and
103
+ // loaders would fall back to the singleton non-session dialect.
104
+ const parent = getRequestContext();
105
+ return runWithContext({ ...parent, editMode, preview, locale }, async () => {
94
106
  let response = await next();
95
107
 
96
108
  // Preview responses must not be cached -- draft content could leak past token expiry.