@aphexcms/cms-core 9.8.0 → 9.9.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 (236) hide show
  1. package/dist/api/organizations.d.ts +2 -1
  2. package/dist/api/organizations.d.ts.map +1 -1
  3. package/dist/api/organizations.js +2 -1
  4. package/dist/api/schemas/jobs.d.ts +15 -0
  5. package/dist/api/schemas/jobs.d.ts.map +1 -0
  6. package/dist/api/schemas/jobs.js +17 -0
  7. package/dist/api/schemas/user.d.ts.map +1 -1
  8. package/dist/api/schemas/user.js +6 -2
  9. package/dist/auth/account-deletion.d.ts +35 -0
  10. package/dist/auth/account-deletion.d.ts.map +1 -0
  11. package/dist/auth/account-deletion.js +65 -0
  12. package/dist/auth/bootstrap.d.ts +104 -0
  13. package/dist/auth/bootstrap.d.ts.map +1 -0
  14. package/dist/auth/bootstrap.js +164 -0
  15. package/dist/auth/instance-state.d.ts +13 -0
  16. package/dist/auth/instance-state.d.ts.map +1 -0
  17. package/dist/auth/instance-state.js +30 -0
  18. package/dist/auth/invitation-status.d.ts +25 -0
  19. package/dist/auth/invitation-status.d.ts.map +1 -0
  20. package/dist/auth/invitation-status.js +41 -0
  21. package/dist/auth/sign-up-policy.d.ts +62 -0
  22. package/dist/auth/sign-up-policy.d.ts.map +1 -0
  23. package/dist/auth/sign-up-policy.js +120 -0
  24. package/dist/components/admin/ActivityView.svelte +263 -13
  25. package/dist/components/admin/ActivityView.svelte.d.ts +10 -1
  26. package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -1
  27. package/dist/db/interfaces/events.d.ts +29 -2
  28. package/dist/db/interfaces/events.d.ts.map +1 -1
  29. package/dist/db/interfaces/index.d.ts +1 -0
  30. package/dist/db/interfaces/index.d.ts.map +1 -1
  31. package/dist/db/interfaces/index.js +1 -0
  32. package/dist/db/interfaces/instance.d.ts +29 -0
  33. package/dist/db/interfaces/instance.d.ts.map +1 -1
  34. package/dist/db/interfaces/instance.js +8 -1
  35. package/dist/events/built-in-consumers.d.ts +20 -0
  36. package/dist/events/built-in-consumers.d.ts.map +1 -0
  37. package/dist/events/built-in-consumers.js +49 -0
  38. package/dist/events/catalog.d.ts +16 -0
  39. package/dist/events/catalog.d.ts.map +1 -1
  40. package/dist/events/catalog.js +17 -0
  41. package/dist/events/consumer.d.ts +14 -0
  42. package/dist/events/consumer.d.ts.map +1 -1
  43. package/dist/events/consumer.js +1 -0
  44. package/dist/events/emit.d.ts +23 -0
  45. package/dist/events/emit.d.ts.map +1 -1
  46. package/dist/events/emit.js +32 -1
  47. package/dist/events/index.d.ts +2 -0
  48. package/dist/events/index.d.ts.map +1 -1
  49. package/dist/events/index.js +2 -0
  50. package/dist/field-validation/utils.d.ts +27 -4
  51. package/dist/field-validation/utils.d.ts.map +1 -1
  52. package/dist/field-validation/utils.js +78 -10
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3 -0
  56. package/dist/jobs/run-batch.d.ts +7 -1
  57. package/dist/jobs/run-batch.d.ts.map +1 -1
  58. package/dist/jobs/run-batch.js +3 -1
  59. package/dist/lib/api/organizations.d.ts +2 -1
  60. package/dist/lib/api/organizations.d.ts.map +1 -1
  61. package/dist/lib/api/organizations.js +2 -1
  62. package/dist/lib/api/organizations.js.map +1 -1
  63. package/dist/lib/api/schemas/api-keys.d.ts +4 -4
  64. package/dist/lib/api/schemas/jobs.d.ts +15 -0
  65. package/dist/lib/api/schemas/jobs.d.ts.map +1 -0
  66. package/dist/lib/api/schemas/jobs.js +18 -0
  67. package/dist/lib/api/schemas/jobs.js.map +1 -0
  68. package/dist/lib/api/schemas/user.d.ts.map +1 -1
  69. package/dist/lib/api/schemas/user.js +6 -2
  70. package/dist/lib/api/schemas/user.js.map +1 -1
  71. package/dist/lib/auth/account-deletion.d.ts +35 -0
  72. package/dist/lib/auth/account-deletion.d.ts.map +1 -0
  73. package/dist/lib/auth/account-deletion.js +66 -0
  74. package/dist/lib/auth/account-deletion.js.map +1 -0
  75. package/dist/lib/auth/bootstrap.d.ts +104 -0
  76. package/dist/lib/auth/bootstrap.d.ts.map +1 -0
  77. package/dist/lib/auth/bootstrap.js +165 -0
  78. package/dist/lib/auth/bootstrap.js.map +1 -0
  79. package/dist/lib/auth/instance-state.d.ts +13 -0
  80. package/dist/lib/auth/instance-state.d.ts.map +1 -0
  81. package/dist/lib/auth/instance-state.js +31 -0
  82. package/dist/lib/auth/instance-state.js.map +1 -0
  83. package/dist/lib/auth/invitation-status.d.ts +25 -0
  84. package/dist/lib/auth/invitation-status.d.ts.map +1 -0
  85. package/dist/lib/auth/invitation-status.js +42 -0
  86. package/dist/lib/auth/invitation-status.js.map +1 -0
  87. package/dist/lib/auth/sign-up-policy.d.ts +62 -0
  88. package/dist/lib/auth/sign-up-policy.d.ts.map +1 -0
  89. package/dist/lib/auth/sign-up-policy.js +121 -0
  90. package/dist/lib/auth/sign-up-policy.js.map +1 -0
  91. package/dist/lib/db/interfaces/events.d.ts +29 -2
  92. package/dist/lib/db/interfaces/events.d.ts.map +1 -1
  93. package/dist/lib/db/interfaces/index.d.ts +1 -0
  94. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  95. package/dist/lib/db/interfaces/index.js +1 -0
  96. package/dist/lib/db/interfaces/index.js.map +1 -1
  97. package/dist/lib/db/interfaces/instance.d.ts +29 -0
  98. package/dist/lib/db/interfaces/instance.d.ts.map +1 -1
  99. package/dist/lib/db/interfaces/instance.js +8 -1
  100. package/dist/lib/db/interfaces/instance.js.map +1 -1
  101. package/dist/lib/events/built-in-consumers.d.ts +20 -0
  102. package/dist/lib/events/built-in-consumers.d.ts.map +1 -0
  103. package/dist/lib/events/built-in-consumers.js +50 -0
  104. package/dist/lib/events/built-in-consumers.js.map +1 -0
  105. package/dist/lib/events/catalog.d.ts +16 -0
  106. package/dist/lib/events/catalog.d.ts.map +1 -1
  107. package/dist/lib/events/catalog.js +17 -0
  108. package/dist/lib/events/catalog.js.map +1 -1
  109. package/dist/lib/events/consumer.d.ts +14 -0
  110. package/dist/lib/events/consumer.d.ts.map +1 -1
  111. package/dist/lib/events/consumer.js +1 -0
  112. package/dist/lib/events/consumer.js.map +1 -1
  113. package/dist/lib/events/emit.d.ts +23 -0
  114. package/dist/lib/events/emit.d.ts.map +1 -1
  115. package/dist/lib/events/emit.js +32 -1
  116. package/dist/lib/events/emit.js.map +1 -1
  117. package/dist/lib/events/index.d.ts +2 -0
  118. package/dist/lib/events/index.d.ts.map +1 -1
  119. package/dist/lib/events/index.js +2 -0
  120. package/dist/lib/events/index.js.map +1 -1
  121. package/dist/lib/field-validation/utils.d.ts +27 -4
  122. package/dist/lib/field-validation/utils.d.ts.map +1 -1
  123. package/dist/lib/field-validation/utils.js +78 -10
  124. package/dist/lib/field-validation/utils.js.map +1 -1
  125. package/dist/lib/index.d.ts +1 -0
  126. package/dist/lib/index.d.ts.map +1 -1
  127. package/dist/lib/index.js +3 -0
  128. package/dist/lib/index.js.map +1 -1
  129. package/dist/lib/jobs/run-batch.d.ts +7 -1
  130. package/dist/lib/jobs/run-batch.d.ts.map +1 -1
  131. package/dist/lib/jobs/run-batch.js +3 -1
  132. package/dist/lib/jobs/run-batch.js.map +1 -1
  133. package/dist/lib/local-api/collection-api.d.ts +42 -1
  134. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  135. package/dist/lib/local-api/collection-api.js +68 -8
  136. package/dist/lib/local-api/collection-api.js.map +1 -1
  137. package/dist/lib/local-api/index.d.ts +1 -1
  138. package/dist/lib/local-api/index.d.ts.map +1 -1
  139. package/dist/lib/local-api/index.js +1 -1
  140. package/dist/lib/local-api/index.js.map +1 -1
  141. package/dist/lib/plugins/resolver.d.ts.map +1 -1
  142. package/dist/lib/plugins/resolver.js +7 -1
  143. package/dist/lib/plugins/resolver.js.map +1 -1
  144. package/dist/lib/server/api/rate-limit.d.ts +38 -0
  145. package/dist/lib/server/api/rate-limit.d.ts.map +1 -0
  146. package/dist/lib/server/api/rate-limit.js +71 -0
  147. package/dist/lib/server/api/rate-limit.js.map +1 -0
  148. package/dist/lib/server/api/routes/agent-change-sets.js +1 -1
  149. package/dist/lib/server/api/routes/agent-change-sets.js.map +1 -1
  150. package/dist/lib/server/api/routes/document-versions.js +1 -1
  151. package/dist/lib/server/api/routes/document-versions.js.map +1 -1
  152. package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
  153. package/dist/lib/server/api/routes/documents-by-id.js +11 -2
  154. package/dist/lib/server/api/routes/documents-by-id.js.map +1 -1
  155. package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
  156. package/dist/lib/server/api/routes/documents-publish.js +11 -1
  157. package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
  158. package/dist/lib/server/api/routes/documents.d.ts.map +1 -1
  159. package/dist/lib/server/api/routes/documents.js +10 -0
  160. package/dist/lib/server/api/routes/documents.js.map +1 -1
  161. package/dist/lib/server/api/routes/jobs.d.ts.map +1 -1
  162. package/dist/lib/server/api/routes/jobs.js +174 -20
  163. package/dist/lib/server/api/routes/jobs.js.map +1 -1
  164. package/dist/lib/server/api/routes/organizations-by-id.d.ts.map +1 -1
  165. package/dist/lib/server/api/routes/organizations-by-id.js +61 -3
  166. package/dist/lib/server/api/routes/organizations-by-id.js.map +1 -1
  167. package/dist/lib/server/api/routes/organizations-invitations.d.ts.map +1 -1
  168. package/dist/lib/server/api/routes/organizations-invitations.js +31 -2
  169. package/dist/lib/server/api/routes/organizations-invitations.js.map +1 -1
  170. package/dist/lib/server/api/routes/resolve-organization-names.d.ts +17 -0
  171. package/dist/lib/server/api/routes/resolve-organization-names.d.ts.map +1 -0
  172. package/dist/lib/server/api/routes/resolve-organization-names.js +26 -0
  173. package/dist/lib/server/api/routes/resolve-organization-names.js.map +1 -0
  174. package/dist/lib/server/api/routes/user.d.ts +0 -8
  175. package/dist/lib/server/api/routes/user.d.ts.map +1 -1
  176. package/dist/lib/server/api/routes/user.js +144 -0
  177. package/dist/lib/server/api/routes/user.js.map +1 -1
  178. package/dist/lib/server/index.d.ts +6 -1
  179. package/dist/lib/server/index.d.ts.map +1 -1
  180. package/dist/lib/server/index.js +9 -1
  181. package/dist/lib/server/index.js.map +1 -1
  182. package/dist/lib/services/references-service.d.ts +1 -1
  183. package/dist/lib/services/references-service.d.ts.map +1 -1
  184. package/dist/lib/types/capabilities.d.ts +14 -0
  185. package/dist/lib/types/capabilities.d.ts.map +1 -1
  186. package/dist/lib/types/capabilities.js +37 -18
  187. package/dist/lib/types/capabilities.js.map +1 -1
  188. package/dist/lib/types/events.d.ts +27 -4
  189. package/dist/lib/types/events.d.ts.map +1 -1
  190. package/dist/lib/utils/fetch-remote-file.d.ts.map +1 -1
  191. package/dist/lib/utils/fetch-remote-file.js +32 -3
  192. package/dist/lib/utils/fetch-remote-file.js.map +1 -1
  193. package/dist/local-api/collection-api.d.ts +42 -1
  194. package/dist/local-api/collection-api.d.ts.map +1 -1
  195. package/dist/local-api/collection-api.js +68 -8
  196. package/dist/local-api/index.d.ts +1 -1
  197. package/dist/local-api/index.d.ts.map +1 -1
  198. package/dist/local-api/index.js +1 -1
  199. package/dist/plugins/resolver.d.ts.map +1 -1
  200. package/dist/plugins/resolver.js +7 -1
  201. package/dist/server/api/rate-limit.d.ts +38 -0
  202. package/dist/server/api/rate-limit.d.ts.map +1 -0
  203. package/dist/server/api/rate-limit.js +70 -0
  204. package/dist/server/api/routes/agent-change-sets.js +1 -1
  205. package/dist/server/api/routes/document-versions.js +1 -1
  206. package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
  207. package/dist/server/api/routes/documents-by-id.js +11 -2
  208. package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
  209. package/dist/server/api/routes/documents-publish.js +11 -1
  210. package/dist/server/api/routes/documents.d.ts.map +1 -1
  211. package/dist/server/api/routes/documents.js +10 -0
  212. package/dist/server/api/routes/jobs.d.ts.map +1 -1
  213. package/dist/server/api/routes/jobs.js +174 -20
  214. package/dist/server/api/routes/organizations-by-id.d.ts.map +1 -1
  215. package/dist/server/api/routes/organizations-by-id.js +61 -3
  216. package/dist/server/api/routes/organizations-invitations.d.ts.map +1 -1
  217. package/dist/server/api/routes/organizations-invitations.js +31 -2
  218. package/dist/server/api/routes/resolve-organization-names.d.ts +17 -0
  219. package/dist/server/api/routes/resolve-organization-names.d.ts.map +1 -0
  220. package/dist/server/api/routes/resolve-organization-names.js +25 -0
  221. package/dist/server/api/routes/user.d.ts +0 -8
  222. package/dist/server/api/routes/user.d.ts.map +1 -1
  223. package/dist/server/api/routes/user.js +144 -0
  224. package/dist/server/index.d.ts +6 -1
  225. package/dist/server/index.d.ts.map +1 -1
  226. package/dist/server/index.js +9 -1
  227. package/dist/services/references-service.d.ts +1 -1
  228. package/dist/services/references-service.d.ts.map +1 -1
  229. package/dist/types/capabilities.d.ts +14 -0
  230. package/dist/types/capabilities.d.ts.map +1 -1
  231. package/dist/types/capabilities.js +37 -18
  232. package/dist/types/events.d.ts +27 -4
  233. package/dist/types/events.d.ts.map +1 -1
  234. package/dist/utils/fetch-remote-file.d.ts.map +1 -1
  235. package/dist/utils/fetch-remote-file.js +32 -3
  236. package/package.json +6 -6
@@ -2,6 +2,47 @@ import { Hono } from 'hono';
2
2
  import { zValidator } from '@hono/zod-validator';
3
3
  import { cmsLogger } from '../../../utils/logger.js';
4
4
  import { updateUserRequest, requestPasswordResetRequest, resetPasswordRequest } from '../../../api/schemas/user.js';
5
+ import { RateLimiter, clientAddress } from '../rate-limit.js';
6
+ // Throttles for the two unauthenticated facades below. Both call the auth provider
7
+ // *server-side*, which bypasses the provider's own HTTP rate limiter entirely (it only
8
+ // engages for calls carrying a real request) — so without these they are unlimited.
9
+ //
10
+ // Two buckets per endpoint, because either one alone is porous: the address bucket stops
11
+ // one host hammering many mailboxes, and the email/token bucket still holds when the
12
+ // address is forged, which `x-forwarded-for` always permits.
13
+ const RESET_REQUEST_WINDOW_MS = 60_000;
14
+ const resetRequestByAddress = new RateLimiter({ windowMs: RESET_REQUEST_WINDOW_MS, max: 5 });
15
+ // Tighter than the address bucket: nobody legitimately asks for the same mailbox's reset
16
+ // link three times a minute, and this is the half an attacker can't sidestep.
17
+ const resetRequestByEmail = new RateLimiter({ windowMs: RESET_REQUEST_WINDOW_MS, max: 2 });
18
+ // The redemption side is a token-guessing surface rather than an email one, so it's keyed
19
+ // on the address and on the token itself — a single token being retried is the signature of
20
+ // a brute-force attempt, not of a user clicking their link again.
21
+ const RESET_SUBMIT_WINDOW_MS = 60_000;
22
+ const resetSubmitByAddress = new RateLimiter({ windowMs: RESET_SUBMIT_WINDOW_MS, max: 10 });
23
+ const resetSubmitByToken = new RateLimiter({ windowMs: RESET_SUBMIT_WINDOW_MS, max: 5 });
24
+ /**
25
+ * Apply every bucket and return a 429 response if any of them is exhausted.
26
+ *
27
+ * All buckets are consumed, not short-circuited: a request that trips the first limit must
28
+ * still count against the others, or an attacker could keep a secondary bucket permanently
29
+ * fresh by deliberately tripping the primary one.
30
+ */
31
+ function enforceRateLimits(c, checks) {
32
+ let retryAfter = 0;
33
+ for (const { limiter, key } of checks) {
34
+ const result = limiter.check(key);
35
+ if (!result.allowed)
36
+ retryAfter = Math.max(retryAfter, result.retryAfterSeconds);
37
+ }
38
+ if (retryAfter === 0)
39
+ return null;
40
+ return c.json({
41
+ success: false,
42
+ error: 'Too many requests',
43
+ message: `Too many attempts. Try again in ${retryAfter} second${retryAfter === 1 ? '' : 's'}.`
44
+ }, 429, { 'Retry-After': String(retryAfter) });
45
+ }
5
46
  /**
6
47
  * User account routes that delegate to the configured AuthProvider.
7
48
  *
@@ -10,6 +51,72 @@ import { updateUserRequest, requestPasswordResetRequest, resetPasswordRequest }
10
51
  * (e.g. the password-reset email) are owned by the AuthProvider impl —
11
52
  * cms-core's role is just to expose them over HTTP.
12
53
  */
54
+ /**
55
+ * Avatars are stored as the CDN path `/media/<assetId>/<filename>`, which is what
56
+ * makes the underlying asset recoverable from the profile field. Anything else —
57
+ * an absolute storage URL from before this format, or an external provider's
58
+ * avatar — has no asset of ours behind it and is left alone.
59
+ */
60
+ function avatarAssetId(image) {
61
+ return /^\/media\/([^/]+)\//.exec(image)?.[1] ?? null;
62
+ }
63
+ /**
64
+ * Is `assetId` an asset this user uploaded as their own avatar?
65
+ *
66
+ * The gate on both pointing the profile field at an asset and deleting the one
67
+ * it used to point at. Both checks read *server-written* facts only:
68
+ *
69
+ * - `createdBy` is stamped from the session by the upload route, so it can't be
70
+ * forged onto somebody else's asset.
71
+ * - `metadata.system` marks the asset as hidden infrastructure rather than
72
+ * content, which is what an avatar is.
73
+ *
74
+ * Without this, `image` was simply a string the client chose: an attacker could
75
+ * point their avatar at any asset in the organization and then change it again,
76
+ * and the replace-cleanup below would delete that asset for them — a delete with
77
+ * none of the `asset.delete` capability checks the assets API enforces, usable
78
+ * by any authenticated user including a viewer.
79
+ */
80
+ async function isOwnAvatarAsset(assetService, organizationId, userId, assetId) {
81
+ const asset = await assetService.findAssetById(organizationId, assetId);
82
+ if (!asset || asset.createdBy !== userId)
83
+ return false;
84
+ const metadata = asset.metadata;
85
+ return (typeof metadata === 'object' &&
86
+ metadata !== null &&
87
+ metadata.system === true);
88
+ }
89
+ /**
90
+ * Delete the asset behind a superseded avatar.
91
+ *
92
+ * Server-side rather than in the client for two reasons. The `asset.delete`
93
+ * capability gates the assets API, so a viewer replacing their own avatar could
94
+ * never clean up after themselves — their old pictures would accumulate forever.
95
+ * And doing it here means every caller of this route inherits the behaviour
96
+ * instead of each one remembering to orchestrate it.
97
+ *
98
+ * Re-checks ownership rather than trusting that the stored value passed the
99
+ * inbound check: rows written before that check existed are still out there.
100
+ *
101
+ * Best-effort by design: the profile field has already moved, so a failure here
102
+ * leaks a file but never leaves a broken avatar. Failing the request would
103
+ * report the leak by making the user's save look broken, which is worse.
104
+ */
105
+ async function discardAvatarAsset(assetService, organizationId, userId, image) {
106
+ const id = avatarAssetId(image);
107
+ if (!id)
108
+ return;
109
+ try {
110
+ if (!(await isOwnAvatarAsset(assetService, organizationId, userId, id))) {
111
+ cmsLogger.warn('[User API] Refusing to delete superseded avatar asset that is not the user’s own:', id);
112
+ return;
113
+ }
114
+ await assetService.deleteAsset(organizationId, id);
115
+ }
116
+ catch (error) {
117
+ cmsLogger.warn('[User API] Could not delete superseded avatar asset:', error);
118
+ }
119
+ }
13
120
  export const userRouter = new Hono()
14
121
  .patch('/', zValidator('json', updateUserRequest, (result, c) => {
15
122
  if (!result.success) {
@@ -48,7 +155,31 @@ export const userRouter = new Hono()
48
155
  error: 'Auth provider does not support profile image updates'
49
156
  }, 500);
50
157
  }
158
+ // An avatar may only point at an asset this user uploaded as an avatar.
159
+ // `image` is a free-form string from the client, and the replace
160
+ // cleanup below deletes whatever the *previous* value pointed at — so
161
+ // without this, setting the field to another asset's path and then
162
+ // changing it again is an unauthenticated-by-capability delete of any
163
+ // asset in the organization. Non-asset values (an external provider's
164
+ // URL, or the absolute storage URLs avatars used to use) have no asset
165
+ // of ours behind them and pass through.
166
+ const incomingAssetId = image === null ? null : avatarAssetId(image);
167
+ if (incomingAssetId &&
168
+ !(await isOwnAvatarAsset(c.var.aphexCMS.assetService, auth.organizationId, auth.user.id, incomingAssetId))) {
169
+ return c.json({
170
+ success: false,
171
+ error: 'Forbidden',
172
+ message: 'That image is not one of your uploaded avatars'
173
+ }, 403);
174
+ }
175
+ // Read the outgoing avatar before overwriting the field — once it's
176
+ // gone the asset is unreachable, and an avatar nothing points at is
177
+ // personal data with no way left to find or erase it.
178
+ const current = await provider.getUserById(auth.user.id);
51
179
  await provider.changeUserImage(auth.user.id, image);
180
+ if (current?.image && current.image !== image) {
181
+ await discardAvatarAsset(c.var.aphexCMS.assetService, auth.organizationId, auth.user.id, current.image);
182
+ }
52
183
  }
53
184
  return c.json({ success: true, message: 'User updated successfully' });
54
185
  }
@@ -77,6 +208,13 @@ export const userRouter = new Hono()
77
208
  return c.json({ success: false, error: 'Auth provider not configured' }, 500);
78
209
  }
79
210
  const { email, redirectTo } = c.req.valid('json');
211
+ // Keyed on the normalized address so casing/whitespace can't mint a fresh bucket.
212
+ const limited = enforceRateLimits(c, [
213
+ { limiter: resetRequestByAddress, key: clientAddress(c.req.raw.headers) },
214
+ { limiter: resetRequestByEmail, key: email.trim().toLowerCase() }
215
+ ]);
216
+ if (limited)
217
+ return limited;
80
218
  await provider.requestPasswordReset(email, redirectTo);
81
219
  // Constant response shape regardless of whether the email exists,
82
220
  // to avoid account enumeration via the API.
@@ -110,6 +248,12 @@ export const userRouter = new Hono()
110
248
  return c.json({ success: false, error: 'Auth provider not configured' }, 500);
111
249
  }
112
250
  const { token, newPassword } = c.req.valid('json');
251
+ const limited = enforceRateLimits(c, [
252
+ { limiter: resetSubmitByAddress, key: clientAddress(c.req.raw.headers) },
253
+ { limiter: resetSubmitByToken, key: token }
254
+ ]);
255
+ if (limited)
256
+ return limited;
113
257
  await provider.resetPassword(token, newPassword);
114
258
  return c.json({ success: true, message: 'Password reset successfully' });
115
259
  }
@@ -4,6 +4,10 @@ export * from '../cache/index.js';
4
4
  export * from '../email/index.js';
5
5
  export * from '../ai/index.js';
6
6
  export { AuthError, type AuthErrorCode } from '../auth/auth-errors.js';
7
+ export { isInstanceEmpty, canDetermineInstanceEmptiness } from '../auth/instance-state.js';
8
+ export * from '../auth/bootstrap.js';
9
+ export * from '../auth/sign-up-policy.js';
10
+ export * from '../auth/account-deletion.js';
7
11
  export { createCMSConfig } from '../config.js';
8
12
  export { cmsLogger, setLogger, type Logger } from '../utils/logger.js';
9
13
  export { CMSEngine } from '../engine.js';
@@ -17,11 +21,12 @@ export { AssetService } from '../services/asset-service.js';
17
21
  export { RolesService } from '../services/roles-service.js';
18
22
  export { GET as serveAssetCDN } from '../routes/assets-cdn.js';
19
23
  export { createAphexApi, mountAphexBuiltins, toHonoHandler, type AphexEnv } from './api/index.js';
24
+ export { RateLimiter, clientAddress, type RateLimitRule, type RateLimitResult } from './api/rate-limit.js';
20
25
  export * from '../jobs/index.js';
21
26
  export * from '../schema-utils/index.js';
22
27
  export { createHashForPublishing } from '../utils/content-hash.js';
23
28
  export { getPreviewPerspective, type PreviewPerspective } from '../preview/perspective.js';
24
29
  export { createGraphQLHandler, type GraphQLConfig, type GraphQLSettings, type GraphQLHandlerResult } from '../graphql/index.js';
25
- export { LocalAPI, createLocalAPI, getLocalAPI, CollectionAPI, SingletonOperationError, PermissionChecker, PermissionError, authToContext, requireAuth, systemContext, type Collections, type SingletonCollection, type LocalAPIContext, type CreateOptions, type UpdateOptions } from '../local-api/index.js';
30
+ export { LocalAPI, createLocalAPI, getLocalAPI, CollectionAPI, SingletonOperationError, DocumentValidationError, PermissionChecker, PermissionError, authToContext, requireAuth, systemContext, type Collections, type SingletonCollection, type LocalAPIContext, type CreateOptions, type UpdateOptions } from '../local-api/index.js';
26
31
  export { buildContentTools, type McpTool, type McpToolResult, type McpToolDeps } from '../mcp/tools.js';
27
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/server/index.ts"],"names":[],"mappings":"AAIA,cAAc,gBAAgB,CAAC;AAE/B,cAAc,kBAAkB,CAAC;AAEjC,cAAc,gBAAgB,CAAC;AAE/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,aAAa,CAAC;AAG5B,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpE,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAG5C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxF,cAAc,wBAAwB,CAAC;AAGvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAG7C,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAKzD,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI5D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG/F,cAAc,eAAe,CAAC;AAG9B,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGxF,OAAO,EACN,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACN,QAAQ,EACR,cAAc,EACd,WAAW,EACX,aAAa,EACb,uBAAuB,EACvB,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,WAAW,EACX,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EACN,iBAAiB,EACjB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/server/index.ts"],"names":[],"mappings":"AAIA,cAAc,gBAAgB,CAAC;AAE/B,cAAc,kBAAkB,CAAC;AAEjC,cAAc,gBAAgB,CAAC;AAE/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,aAAa,CAAC;AAG5B,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACxF,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AAGzC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAG5C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxF,cAAc,wBAAwB,CAAC;AAGvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAG7C,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAKzD,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI5D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAK/F,OAAO,EACN,WAAW,EACX,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,MAAM,kBAAkB,CAAC;AAG1B,cAAc,eAAe,CAAC;AAG9B,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGxF,OAAO,EACN,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACN,QAAQ,EACR,cAAc,EACd,WAAW,EACX,aAAa,EACb,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,WAAW,EACX,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EACN,iBAAiB,EACjB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,MAAM,cAAc,CAAC"}
@@ -8,6 +8,10 @@ export * from '../email/index.js';
8
8
  export * from '../ai/index.js';
9
9
  // Authentication errors
10
10
  export { AuthError } from '../auth/auth-errors.js';
11
+ export { isInstanceEmpty, canDetermineInstanceEmptiness } from '../auth/instance-state.js';
12
+ export * from '../auth/bootstrap.js';
13
+ export * from '../auth/sign-up-policy.js';
14
+ export * from '../auth/account-deletion.js';
11
15
  // Configuration system
12
16
  export { createCMSConfig } from '../config.js';
13
17
  // Logger
@@ -33,6 +37,10 @@ export { GET as serveAssetCDN } from '../routes/assets-cdn.js';
33
37
  // Hono API app — exposed so user apps (and tests) can construct or extend
34
38
  // the same router the SK catch-all forwards to.
35
39
  export { createAphexApi, mountAphexBuiltins, toHonoHandler } from './api/index.js';
40
+ // Fixed-window throttle for unauthenticated endpoints. Exposed because an app adding its own
41
+ // public route needs the same guard the password-reset facades use — and because the caveat
42
+ // travels with it: this is per-process memory, so N instances give N× the configured limit.
43
+ export { RateLimiter, clientAddress } from './api/rate-limit.js';
36
44
  // Job execution — the DB-backed job runner (claim → run handler → complete/retry/fail).
37
45
  export * from '../jobs/index.js';
38
46
  // Schema utilities
@@ -44,7 +52,7 @@ export { getPreviewPerspective } from '../preview/perspective.js';
44
52
  // GraphQL (built-in, enabled by default)
45
53
  export { createGraphQLHandler } from '../graphql/index.js';
46
54
  // Local API (unified operations layer)
47
- export { LocalAPI, createLocalAPI, getLocalAPI, CollectionAPI, SingletonOperationError, PermissionChecker, PermissionError, authToContext, requireAuth, systemContext } from '../local-api/index.js';
55
+ export { LocalAPI, createLocalAPI, getLocalAPI, CollectionAPI, SingletonOperationError, DocumentValidationError, PermissionChecker, PermissionError, authToContext, requireAuth, systemContext } from '../local-api/index.js';
48
56
  // MCP — transport-agnostic tool registry (the MCP route + a future AI panel
49
57
  // both consume these). The SvelteKit route handler is at ./routes/mcp.
50
58
  export { buildContentTools } from '../mcp/tools.js';
@@ -1,4 +1,4 @@
1
- import type { DatabaseAdapter } from '../db/interfaces.js';
1
+ import type { DatabaseAdapter } from '../db/interfaces/index.js';
2
2
  import type { SchemaType } from '../types/schemas.js';
3
3
  /**
4
4
  * Maintains the back-reference index. After every doc save the collection-API
@@ -1 +1 @@
1
- {"version":3,"file":"references-service.d.ts","sourceRoot":"","sources":["../../src/lib/services/references-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD;;;;;;;;;GASG;AACH,qBAAa,iBAAiB;IACjB,OAAO,CAAC,eAAe;gBAAf,eAAe,EAAE,eAAe;IAEpD;;;OAGG;IACG,iBAAiB,CACtB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,UAAU,GAAG,IAAI,EACzB,QAAQ,EAAE,UAAU,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;;OAOG;IACG,eAAe,CACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,UAAU,EAAE,EACrB,gBAAgB,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,GACjF,OAAO,CAAC,IAAI,CAAC;CAwBhB"}
1
+ {"version":3,"file":"references-service.d.ts","sourceRoot":"","sources":["../../src/lib/services/references-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD;;;;;;;;;GASG;AACH,qBAAa,iBAAiB;IACjB,OAAO,CAAC,eAAe;gBAAf,eAAe,EAAE,eAAe;IAEpD;;;OAGG;IACG,iBAAiB,CACtB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,UAAU,GAAG,IAAI,EACzB,QAAQ,EAAE,UAAU,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;;OAOG;IACG,eAAe,CACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,UAAU,EAAE,EACrB,gBAAgB,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,GACjF,OAAO,CAAC,IAAI,CAAC;CAwBhB"}
@@ -111,6 +111,20 @@ export declare function isInstanceRole(auth: Auth): boolean;
111
111
  * seed for the org role so behavior remains safe.
112
112
  */
113
113
  export declare function hasCapability(auth: Auth, capability: Capability | (string & {})): boolean;
114
+ /**
115
+ * Expand an API key's coarse `read`/`write` scopes into capabilities.
116
+ *
117
+ * The compatibility path for keys issued before the capability model existed,
118
+ * and the default for keys created without an explicit allowlist.
119
+ *
120
+ * **This expansion is not itself a permission check.** It says what the scopes
121
+ * mean, not what the key's owner may actually do — the caller is responsible for
122
+ * intersecting the result with the owner's grantable set before trusting it, or
123
+ * a `write` key would confer `document.delete` to an owner whose role never had
124
+ * it. `AuthService.validateApiKey` does that clamp; anything else deriving
125
+ * capabilities from scopes must too.
126
+ */
127
+ export declare function coarseApiKeyCapabilities(permissions: readonly string[]): string[];
114
128
  /**
115
129
  * Resolve the effective capability set for an Auth.
116
130
  *
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/lib/types/capabilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/lib/types/capabilities.ts"],"names":[],"mappings":"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"}
@@ -298,6 +298,37 @@ export function isInstanceRole(auth) {
298
298
  export function hasCapability(auth, capability) {
299
299
  return resolveCapabilities(auth).has(capability);
300
300
  }
301
+ /** What the coarse `read` scope buys a key. */
302
+ const API_KEY_READ_CAPABILITIES = ['document.read', 'asset.read'];
303
+ /** What the coarse `write` scope adds on top. */
304
+ const API_KEY_WRITE_CAPABILITIES = [
305
+ 'document.create',
306
+ 'document.update',
307
+ 'document.delete',
308
+ 'document.publish',
309
+ 'document.unpublish',
310
+ 'asset.upload',
311
+ 'asset.delete'
312
+ ];
313
+ /**
314
+ * Expand an API key's coarse `read`/`write` scopes into capabilities.
315
+ *
316
+ * The compatibility path for keys issued before the capability model existed,
317
+ * and the default for keys created without an explicit allowlist.
318
+ *
319
+ * **This expansion is not itself a permission check.** It says what the scopes
320
+ * mean, not what the key's owner may actually do — the caller is responsible for
321
+ * intersecting the result with the owner's grantable set before trusting it, or
322
+ * a `write` key would confer `document.delete` to an owner whose role never had
323
+ * it. `AuthService.validateApiKey` does that clamp; anything else deriving
324
+ * capabilities from scopes must too.
325
+ */
326
+ export function coarseApiKeyCapabilities(permissions) {
327
+ const caps = [...API_KEY_READ_CAPABILITIES];
328
+ if (permissions.includes('write'))
329
+ caps.push(...API_KEY_WRITE_CAPABILITIES);
330
+ return caps;
331
+ }
301
332
  /**
302
333
  * Resolve the effective capability set for an Auth.
303
334
  *
@@ -318,25 +349,13 @@ export function resolveCapabilities(auth) {
318
349
  if (auth.type === 'session' && INSTANCE_ROLE_OVERRIDES.has(auth.user.role)) {
319
350
  return new Set(ALL_CAPABILITIES);
320
351
  }
352
+ // Reached only when the key carries no capability list at all — an explicit
353
+ // one, including an empty one, is authoritative and was returned above.
354
+ // "Empty" must mean *nothing*, never "fall back to the coarse set": an
355
+ // allowlist that a clamp stripped to nothing would otherwise resolve to more
356
+ // than it asked for.
321
357
  if (auth.type === 'api_key') {
322
- // Explicit capability allowlist wins — lets operators issue keys scoped
323
- // to exactly what they need (e.g. a key that can only publish one
324
- // collection). Falls back to the coarse read/write mapping otherwise
325
- // for keys issued before the capability model existed.
326
- if (Array.isArray(auth.capabilities) && auth.capabilities.length > 0) {
327
- return new Set(auth.capabilities);
328
- }
329
- const caps = new Set(['document.read', 'asset.read']);
330
- if (auth.permissions.includes('write')) {
331
- caps.add('document.create');
332
- caps.add('document.update');
333
- caps.add('document.delete');
334
- caps.add('document.publish');
335
- caps.add('document.unpublish');
336
- caps.add('asset.upload');
337
- caps.add('asset.delete');
338
- }
339
- return caps;
358
+ return new Set(coarseApiKeyCapabilities(auth.permissions));
340
359
  }
341
360
  // Session without pre-resolved capabilities → seed fallback.
342
361
  const builtin = BUILTIN_ROLE_SEED[auth.organizationRole];
@@ -113,17 +113,23 @@ export interface Page<T> {
113
113
  limit: number;
114
114
  offset: number;
115
115
  }
116
- /** Query for the read-only event history. Newest first. */
116
+ /**
117
+ * Query for the read-only event history. Newest first.
118
+ *
119
+ * Omit `organizationId` to read across every organization — the instance-wide view.
120
+ * Only two callers may do that: the worker (which has override access by construction)
121
+ * and a super admin in the admin history view. Anything request-scoped must pass an id.
122
+ */
117
123
  export interface ListEventsOptions {
118
- organizationId: string;
124
+ organizationId?: string;
119
125
  /** Filter by exact event type (e.g. `document.published`). */
120
126
  type?: string;
121
127
  limit?: number;
122
128
  offset?: number;
123
129
  }
124
- /** Query for the read-only job history. Newest first. */
130
+ /** Query for the read-only job history. Newest first. Omitting `organizationId` reads instance-wide — see `ListEventsOptions`. */
125
131
  export interface ListJobsOptions {
126
- organizationId: string;
132
+ organizationId?: string;
127
133
  /** Filter by one status or several (e.g. only `failed`). */
128
134
  status?: JobStatus | JobStatus[];
129
135
  /** Filter by exact job type. */
@@ -131,4 +137,21 @@ export interface ListJobsOptions {
131
137
  limit?: number;
132
138
  offset?: number;
133
139
  }
140
+ /**
141
+ * The relay's backlog, as a health signal.
142
+ *
143
+ * This is the one number that distinguishes "nothing happened" from "nothing is being
144
+ * processed". Jobs and events both look quiet-but-healthy when no worker is calling
145
+ * `POST /api/internal/workers/run`; a growing outbox backlog is the only place that
146
+ * shows up, because emission still writes rows that nobody is draining.
147
+ *
148
+ * `oldestPendingAt` matters more than `pending`: a big count that is seconds old is a
149
+ * busy instance, while a single row an hour old means the relay has stopped.
150
+ */
151
+ export interface OutboxHealth {
152
+ /** Rows with `processedAt IS NULL`. */
153
+ pending: number;
154
+ /** `createdAt` of the oldest unprocessed row, or null when the backlog is empty. */
155
+ oldestPendingAt: Date | null;
156
+ }
134
157
  //# sourceMappingURL=events.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/lib/types/events.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/lib/types/events.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,sCAAsC;AACtC,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEpF,+FAA+F;AAC/F,MAAM,WAAW,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,IAAI,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED,8FAA8F;AAC9F,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,IAAI,CAAC;CACX;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,SAAS;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,0EAA0E;IAC1E,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,4BAA4B;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;CACd;AAED,kGAAkG;AAClG,MAAM,WAAW,IAAI,CAAC,CAAC;IACtB,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,kIAAkI;AAClI,MAAM,WAAW,eAAe;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;IACjC,gCAAgC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC5B,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;CAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-remote-file.d.ts","sourceRoot":"","sources":["../../src/lib/utils/fetch-remote-file.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,qBAAqB,QAAmB,CAAC;AA6EtD,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;sBAGsB;AACtB,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAuEtE"}
1
+ {"version":3,"file":"fetch-remote-file.d.ts","sourceRoot":"","sources":["../../src/lib/utils/fetch-remote-file.ts"],"names":[],"mappings":"AA8CA,eAAO,MAAM,qBAAqB,QAAmB,CAAC;AA6EtD,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;sBAGsB;AACtB,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA0EtE"}
@@ -11,7 +11,35 @@
11
11
  // that gets validated and the address that gets connected to must be the exact same one.
12
12
  import { lookup } from 'node:dns/promises';
13
13
  import net from 'node:net';
14
- import { Agent, fetch as undiciFetch } from 'undici';
14
+ // Loading `undici` is deferred, and Node's own dispatcher is forced into place first. That
15
+ // ordering is the whole point of this function, and getting it wrong breaks something far away:
16
+ //
17
+ // Node's built-in fetch and the userland `undici` package share one slot —
18
+ // `globalThis[Symbol.for('undici.globalDispatcher.1')]` — and whoever writes it first wins.
19
+ // Importing `undici` claims that slot if it's still empty, and on Node 24+ the *built-in* fetch
20
+ // then routes through this bundled userland copy (as a `Dispatcher1Wrapper`). Its stricter
21
+ // `processHeader` rejects an explicit `content-length`, which the S3 client sets on every PUT — so
22
+ // merely importing this module killed every S3/R2 upload in the process with "invalid
23
+ // content-length header".
24
+ //
25
+ // The slot is non-configurable once set, and Node caches its wrapper internally, so this can't be
26
+ // undone after the fact: writing the symbol back (even to `undefined`) leaves fetch broken. The
27
+ // only fix is to not lose the race. A `data:` fetch resolves entirely in-process — no socket, no
28
+ // DNS — and is enough to make Node install its dispatcher, after which undici's import leaves the
29
+ // slot alone and both work.
30
+ //
31
+ // This module hangs off the `/server` barrel (server/index.ts → mcp/tools → here), so with a
32
+ // static `import` every Aphex app broke its own uploads at boot. It only showed in production:
33
+ // `vite dev` loads SSR modules lazily, so a real request usually primed Node's dispatcher first,
34
+ // while a production build imports the graph eagerly at startup and undici gets there first.
35
+ let undiciPromise;
36
+ function loadUndici() {
37
+ undiciPromise ??= (async () => {
38
+ await fetch('data:text/plain,0').catch(() => undefined);
39
+ return import('undici');
40
+ })();
41
+ return undiciPromise;
42
+ }
15
43
  export const MAX_REMOTE_FILE_BYTES = 10 * 1024 * 1024; // matches the API's global JSON body cap
16
44
  const FETCH_TIMEOUT_MS = 10_000;
17
45
  const MAX_REDIRECTS = 5;
@@ -79,7 +107,7 @@ async function resolvePinnedHost(hostname) {
79
107
  * whatever hostname undici asks it to resolve, it hands back this exact IP, so the socket that
80
108
  * actually opens is guaranteed to be the one that passed `isPrivateOrReservedIp`. The hostname
81
109
  * itself still flows through normally for the Host header and TLS SNI/cert validation. */
82
- function pinnedDispatcher(host) {
110
+ function pinnedDispatcher(Agent, host) {
83
111
  return new Agent({
84
112
  connect: {
85
113
  lookup: (_hostname, options, callback) => {
@@ -94,6 +122,7 @@ function pinnedDispatcher(host) {
94
122
  * hop), a request timeout, and a streamed size cap (not just a Content-Length check, since that
95
123
  * header can lie). */
96
124
  export async function fetchRemoteFile(url) {
125
+ const { Agent, fetch: undiciFetch } = await loadUndici();
97
126
  let current = url;
98
127
  for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
99
128
  const parsed = new URL(current);
@@ -112,7 +141,7 @@ export async function fetchRemoteFile(url) {
112
141
  res = await undiciFetch(current, {
113
142
  redirect: 'manual',
114
143
  signal: controller.signal,
115
- dispatcher: pinnedDispatcher(pinnedHost),
144
+ dispatcher: pinnedDispatcher(Agent, pinnedHost),
116
145
  headers: REQUEST_HEADERS
117
146
  });
118
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aphexcms/cms-core",
3
- "version": "9.8.0",
3
+ "version": "9.9.0",
4
4
  "description": "Aphex CMS Core - A Sanity-style CMS with ports and adapters architecture",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -82,7 +82,7 @@
82
82
  "svelte": "^5.44.0",
83
83
  "tailwind-merge": "^3.4.0",
84
84
  "tailwind-variants": "^3.2.2",
85
- "@aphexcms/ui": "^0.8.3"
85
+ "@aphexcms/ui": "^0.8.5"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "graphql": {
@@ -115,10 +115,10 @@
115
115
  "dayjs": "^1.11.19",
116
116
  "dompurify": "^3.4.12",
117
117
  "esbuild": "^0.27.0",
118
- "hono": "^4.12.15",
118
+ "hono": "^4.12.34",
119
119
  "marked": "^18.0.7",
120
120
  "picocolors": "^1.1.1",
121
- "sharp": "^0.34.5",
121
+ "sharp": "^0.35.3",
122
122
  "svelte-sonner": "^1.0.7",
123
123
  "tsx": "^4.20.6",
124
124
  "undici": "^8.9.0",
@@ -129,7 +129,7 @@
129
129
  "@eslint/js": "^9.39.1",
130
130
  "@internationalized/date": "^3.10.0",
131
131
  "@sveltejs/adapter-auto": "^7.0.0",
132
- "@sveltejs/kit": "^2.59.1",
132
+ "@sveltejs/kit": "^2.70.2",
133
133
  "@sveltejs/package": "^2.5.6",
134
134
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
135
135
  "@tailwindcss/vite": "^4.3.0",
@@ -147,7 +147,7 @@
147
147
  "typescript": "^5.9.3",
148
148
  "typescript-eslint": "^8.48.0",
149
149
  "vite": "^8.1.5",
150
- "vitest": "^4.0.14"
150
+ "vitest": "^4.1.10"
151
151
  },
152
152
  "scripts": {
153
153
  "build": "svelte-package && tsc && node ../../scripts/fix-imports.js ./dist",