@jini-ai/cms 0.3.11 → 0.4.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 (258) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +47 -19
  3. package/dist/content-types/agent-tools.d.ts +3 -56
  4. package/dist/content-types/agent-tools.js +22 -6
  5. package/dist/content-types/cleanup.d.ts +18 -13
  6. package/dist/content-types/cleanup.js +16 -11
  7. package/dist/content-types/errors.d.ts +57 -24
  8. package/dist/content-types/errors.js +56 -39
  9. package/dist/content-types/field-defs.d.ts +8 -4
  10. package/dist/content-types/field-defs.js +19 -14
  11. package/dist/content-types/index-provisioning.d.ts +61 -47
  12. package/dist/content-types/index-provisioning.js +90 -66
  13. package/dist/content-types/index.d.ts +4 -3
  14. package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
  15. package/dist/content-types/lifecycle-dispatch.js +5 -4
  16. package/dist/content-types/lifecycle.d.ts +23 -18
  17. package/dist/content-types/lifecycle.js +71 -64
  18. package/dist/content-types/repo.memory.d.ts +5 -4
  19. package/dist/content-types/repo.memory.js +3 -2
  20. package/dist/content-types/tool-registrations.d.ts +6 -8
  21. package/dist/content-types/tool-registrations.js +24 -28
  22. package/dist/content-types/types.d.ts +17 -13
  23. package/dist/content-types/types.js +11 -5
  24. package/dist/content-types/write-service.d.ts +20 -22
  25. package/dist/content-types/write-service.js +78 -75
  26. package/dist/core/authorization.d.ts +15 -0
  27. package/dist/core/authorization.js +2 -0
  28. package/dist/core/commands/change-set.d.ts +15 -7
  29. package/dist/core/commands/command.d.ts +45 -26
  30. package/dist/core/commands/command.js +73 -63
  31. package/dist/core/entity-liveness.d.ts +11 -7
  32. package/dist/core/entity-liveness.js +5 -4
  33. package/dist/core/index.d.ts +12 -5
  34. package/dist/core/index.js +10 -4
  35. package/dist/core/ports.d.ts +25 -24
  36. package/dist/core/ports.js +1 -0
  37. package/dist/core/tools/args.d.ts +49 -0
  38. package/dist/core/tools/args.js +46 -0
  39. package/dist/core/tools/index.d.ts +3 -0
  40. package/dist/core/tools/index.js +3 -0
  41. package/dist/entries/agent-tools.d.ts +1 -73
  42. package/dist/entries/agent-tools.js +19 -13
  43. package/dist/entries/errors.d.ts +28 -14
  44. package/dist/entries/errors.js +17 -11
  45. package/dist/entries/field-validation.d.ts +5 -3
  46. package/dist/entries/field-validation.js +7 -5
  47. package/dist/entries/index.d.ts +2 -2
  48. package/dist/entries/lifecycle-dispatch.d.ts +5 -2
  49. package/dist/entries/lifecycle-dispatch.js +2 -1
  50. package/dist/entries/list.d.ts +4 -2
  51. package/dist/entries/list.js +4 -3
  52. package/dist/entries/repo.memory.d.ts +12 -10
  53. package/dist/entries/repo.memory.js +11 -10
  54. package/dist/entries/tool-registrations.d.ts +6 -8
  55. package/dist/entries/tool-registrations.js +42 -41
  56. package/dist/entries/types.d.ts +0 -7
  57. package/dist/entries/write-service.d.ts +28 -29
  58. package/dist/entries/write-service.js +114 -105
  59. package/dist/http/settings/cms-adapter.d.ts +20 -0
  60. package/dist/http/settings/cms-adapter.js +124 -0
  61. package/dist/http/settings/contracts.d.ts +135 -0
  62. package/dist/http/settings/contracts.js +2 -0
  63. package/dist/http/settings/http.d.ts +10 -0
  64. package/dist/http/settings/http.js +211 -0
  65. package/dist/http/settings/index.d.ts +5 -0
  66. package/dist/http/settings/index.js +4 -0
  67. package/dist/http/settings/shared.d.ts +68 -0
  68. package/dist/http/settings/shared.js +54 -0
  69. package/dist/http/settings/stream.d.ts +24 -0
  70. package/dist/http/settings/stream.js +147 -0
  71. package/dist/media/agent-tools.d.ts +1 -66
  72. package/dist/media/agent-tools.js +5 -1
  73. package/dist/media/blob-gc-lock.d.ts +6 -2
  74. package/dist/media/blob-gc-lock.js +4 -2
  75. package/dist/media/blob-gc.d.ts +64 -51
  76. package/dist/media/blob-gc.js +62 -52
  77. package/dist/media/blob-key.d.ts +1 -1
  78. package/dist/media/content-type-sniffer.d.ts +3 -1
  79. package/dist/media/content-type-sniffer.js +1 -1
  80. package/dist/media/html-attributes.d.ts +9 -5
  81. package/dist/media/html-attributes.js +8 -6
  82. package/dist/media/image-transformer.js +1 -1
  83. package/dist/media/image-transformer.sharp.d.ts +6 -0
  84. package/dist/media/image-transformer.sharp.js +12 -6
  85. package/dist/media/import/fetch-image.d.ts +24 -0
  86. package/dist/media/import/fetch-image.js +137 -0
  87. package/dist/media/import/index.d.ts +3 -0
  88. package/dist/media/import/index.js +3 -0
  89. package/dist/media/import/ports.d.ts +38 -0
  90. package/dist/media/index.d.ts +7 -1
  91. package/dist/media/index.js +1 -0
  92. package/dist/media/media-service.d.ts +14 -10
  93. package/dist/media/media-service.js +57 -52
  94. package/dist/media/ports.d.ts +3 -2
  95. package/dist/media/rendition-service.d.ts +3 -3
  96. package/dist/media/rendition-service.js +38 -37
  97. package/dist/media/repo.memory.d.ts +18 -7
  98. package/dist/media/repo.memory.js +12 -6
  99. package/dist/media/tool-registrations.d.ts +16 -15
  100. package/dist/media/tool-registrations.js +84 -72
  101. package/dist/media/transform-lock.d.ts +22 -12
  102. package/dist/media/transform-lock.js +14 -13
  103. package/dist/media/transform-registry.d.ts +3 -3
  104. package/dist/media/transform-registry.js +19 -18
  105. package/dist/media/transform-types.d.ts +8 -3
  106. package/dist/media/transform-types.js +10 -5
  107. package/dist/media/types.d.ts +21 -4
  108. package/dist/media/types.js +15 -2
  109. package/dist/navigation/agent-tools.d.ts +1 -80
  110. package/dist/navigation/agent-tools.js +15 -3
  111. package/dist/navigation/contracts.d.ts +1 -1
  112. package/dist/navigation/index.d.ts +2 -2
  113. package/dist/navigation/menu-service.d.ts +57 -40
  114. package/dist/navigation/menu-service.js +80 -69
  115. package/dist/navigation/ports.d.ts +1 -1
  116. package/dist/navigation/read-model.d.ts +3 -1
  117. package/dist/navigation/read-model.js +2 -2
  118. package/dist/navigation/reconcile.d.ts +2 -2
  119. package/dist/navigation/reconcile.js +2 -1
  120. package/dist/navigation/repo.memory.d.ts +7 -3
  121. package/dist/navigation/repo.memory.js +4 -2
  122. package/dist/navigation/resolver.d.ts +4 -1
  123. package/dist/navigation/resolver.js +1 -1
  124. package/dist/navigation/tool-registrations.d.ts +6 -8
  125. package/dist/navigation/tool-registrations.js +44 -42
  126. package/dist/navigation/types.d.ts +1 -1
  127. package/dist/presentation/presentation.d.ts +11 -3
  128. package/dist/presentation/presentation.js +13 -6
  129. package/dist/presentation/repo.memory.d.ts +6 -2
  130. package/dist/presentation/repo.memory.js +3 -2
  131. package/dist/settings/agent-tools.d.ts +2 -75
  132. package/dist/settings/agent-tools.js +49 -45
  133. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  134. package/dist/settings/agent-value-write-tools.js +154 -0
  135. package/dist/settings/agent-writable-preferences.js +8 -20
  136. package/dist/settings/agent-write-denylist.d.ts +19 -0
  137. package/dist/settings/agent-write-denylist.js +15 -0
  138. package/dist/settings/change-feed.d.ts +11 -2
  139. package/dist/settings/change-feed.js +4 -4
  140. package/dist/settings/definitions-dispatch.d.ts +8 -3
  141. package/dist/settings/definitions-dispatch.js +5 -5
  142. package/dist/settings/dictionaries/index.d.ts +5 -2
  143. package/dist/settings/dictionaries/index.js +2 -2
  144. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
  145. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
  146. package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
  147. package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
  148. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
  149. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
  150. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
  151. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
  152. package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
  153. package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
  154. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
  155. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
  156. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
  157. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
  158. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
  159. package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
  160. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
  161. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
  162. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
  163. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
  164. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
  165. package/dist/settings/ensure-definitions.d.ts +5 -5
  166. package/dist/settings/errors.d.ts +7 -1
  167. package/dist/settings/errors.js +7 -1
  168. package/dist/settings/index.d.ts +4 -1
  169. package/dist/settings/index.js +1 -0
  170. package/dist/settings/ports.d.ts +6 -5
  171. package/dist/settings/principal-lookup.d.ts +9 -0
  172. package/dist/settings/principal-lookup.js +2 -0
  173. package/dist/settings/purge-service.d.ts +5 -4
  174. package/dist/settings/purge-service.js +3 -2
  175. package/dist/settings/settings.d.ts +22 -7
  176. package/dist/settings/settings.js +47 -26
  177. package/dist/settings/tool-registrations.d.ts +25 -32
  178. package/dist/settings/tool-registrations.js +66 -56
  179. package/dist/settings/types.d.ts +1 -1
  180. package/dist/settings/ui-tab-definitions.d.ts +1 -1
  181. package/dist/settings/write-service.d.ts +32 -26
  182. package/dist/settings/write-service.js +26 -20
  183. package/dist/taxonomy/agent-tools.d.ts +1 -66
  184. package/dist/taxonomy/agent-tools.js +25 -15
  185. package/dist/taxonomy/content-lookup.js +1 -1
  186. package/dist/taxonomy/index.d.ts +2 -2
  187. package/dist/taxonomy/merge-term.d.ts +7 -4
  188. package/dist/taxonomy/merge-term.js +7 -5
  189. package/dist/taxonomy/repo.memory.d.ts +42 -19
  190. package/dist/taxonomy/repo.memory.js +14 -12
  191. package/dist/taxonomy/validation-chain.d.ts +39 -18
  192. package/dist/taxonomy/validation-chain.js +35 -25
  193. package/dist/taxonomy/write-service.d.ts +103 -44
  194. package/dist/taxonomy/write-service.js +221 -179
  195. package/dist/trash/adapters/record-store.d.ts +52 -0
  196. package/dist/trash/adapters/record-store.js +36 -0
  197. package/dist/trash/cursor.d.ts +20 -0
  198. package/dist/trash/cursor.js +36 -0
  199. package/dist/trash/follow-ups.d.ts +76 -0
  200. package/dist/trash/follow-ups.js +46 -0
  201. package/dist/trash/index.d.ts +8 -0
  202. package/dist/trash/index.js +8 -0
  203. package/dist/trash/ports.d.ts +308 -0
  204. package/dist/trash/ports.js +2 -0
  205. package/dist/trash/repo.memory.d.ts +50 -0
  206. package/dist/trash/repo.memory.js +114 -0
  207. package/dist/trash/sweeper.d.ts +77 -0
  208. package/dist/trash/sweeper.js +127 -0
  209. package/dist/trash/write-service.d.ts +100 -0
  210. package/dist/trash/write-service.js +242 -0
  211. package/dist/workspace/agent-tools.d.ts +1 -59
  212. package/dist/workspace/agent-tools.js +27 -12
  213. package/dist/workspace/create.d.ts +31 -6
  214. package/dist/workspace/create.js +18 -5
  215. package/dist/workspace/delete.d.ts +18 -13
  216. package/dist/workspace/delete.js +20 -12
  217. package/dist/workspace/index.d.ts +1 -1
  218. package/dist/workspace/repo.memory.d.ts +19 -4
  219. package/dist/workspace/repo.memory.js +26 -4
  220. package/dist/workspace/tool-registrations.d.ts +2 -2
  221. package/dist/workspace/tool-registrations.js +13 -13
  222. package/dist/workspace/update.d.ts +6 -5
  223. package/dist/workspace/update.js +9 -8
  224. package/package.json +44 -38
  225. package/dist/identity/admin-crud-service.d.ts +0 -280
  226. package/dist/identity/admin-crud-service.js +0 -468
  227. package/dist/identity/agent-tool-input.d.ts +0 -58
  228. package/dist/identity/agent-tool-input.js +0 -126
  229. package/dist/identity/agent-tools.d.ts +0 -101
  230. package/dist/identity/agent-tools.js +0 -290
  231. package/dist/identity/auth-service.d.ts +0 -118
  232. package/dist/identity/auth-service.js +0 -170
  233. package/dist/identity/authorize.d.ts +0 -90
  234. package/dist/identity/authorize.js +0 -94
  235. package/dist/identity/grant-service.d.ts +0 -198
  236. package/dist/identity/grant-service.js +0 -361
  237. package/dist/identity/hasher.d.ts +0 -39
  238. package/dist/identity/hasher.js +0 -39
  239. package/dist/identity/index.d.ts +0 -62
  240. package/dist/identity/index.js +0 -54
  241. package/dist/identity/password-policy.d.ts +0 -57
  242. package/dist/identity/password-policy.js +0 -66
  243. package/dist/identity/permission-migrations.d.ts +0 -86
  244. package/dist/identity/permission-migrations.js +0 -67
  245. package/dist/identity/permissions.d.ts +0 -35
  246. package/dist/identity/permissions.js +0 -603
  247. package/dist/identity/ports.d.ts +0 -201
  248. package/dist/identity/repo.memory.d.ts +0 -164
  249. package/dist/identity/repo.memory.js +0 -224
  250. package/dist/identity/seed.d.ts +0 -54
  251. package/dist/identity/seed.js +0 -429
  252. package/dist/identity/tool-registrations.d.ts +0 -50
  253. package/dist/identity/tool-registrations.js +0 -419
  254. package/dist/identity/types.d.ts +0 -185
  255. package/dist/identity/types.js +0 -64
  256. package/dist/identity/username.d.ts +0 -12
  257. package/dist/identity/username.js +0 -14
  258. /package/dist/{identity → media/import}/ports.js +0 -0
@@ -1,3 +1,5 @@
1
+ import { createSystemClock } from "@jini-ai/core/primitives";
2
+ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
1
3
  /**
2
4
  * @file `media` write/read service — walking-skeleton build.
3
5
  *
@@ -62,7 +64,7 @@ export const DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MiB
62
64
  /**
63
65
  * Renders a byte count as a human-readable MB figure for the size-cap rejection below (2026-09-21).
64
66
  * Every real caller passes a MiB-scale `maxUploadBytes` (10 MiB default, or a host override such as
65
- * Tovu's 50 MiB), so one decimal place is enough precision without ever showing a raw byte count —
67
+ * a host's 50 MiB), so one decimal place is enough precision without ever showing a raw byte count —
66
68
  * the whole point of this fix (see `uploadMedia`'s size-cap check, and `agent-tools.ts`'s file header
67
69
  * for why the catalog description can no longer state a specific number either). `toFixed(1)`'s
68
70
  * trailing `.0` is trimmed so a clean multiple of 1 MB (the common case) reads as `10 MB`, not `10.0
@@ -127,7 +129,7 @@ export const DEFAULT_ALLOWED_MIME_TYPES = new Set([
127
129
  export function resolveWriteOnceSource(required, _optional = {}) {
128
130
  const { existing, requestedSha256 } = required;
129
131
  if (existing && existing.sha256 !== requestedSha256) {
130
- throw new MediaSourceImmutableError(`source.sha256 is write-once: cannot change '${existing.sha256}' to '${requestedSha256}'`);
132
+ throw new MediaSourceImmutableError({ message: `source.sha256 is write-once: cannot change '${existing.sha256}' to '${requestedSha256}'` });
131
133
  }
132
134
  return { sha256: requestedSha256 };
133
135
  }
@@ -167,7 +169,8 @@ const MEDIA_SLUG_UUID_SHAPE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9
167
169
  * human for a shorter value — silently truncating an internally-derived value is the same
168
170
  * "coerce, don't fail" convention `deriveTitleFromFilename`/`slugifyMediaTitle` already use. */
169
171
  const MEDIA_MAX_SLUG_LENGTH = 120;
170
- export function isValidMediaSlugFormat(slug) {
172
+ export function isValidMediaSlugFormat(requiredArgs, optionalArgs = {}) {
173
+ const { slug } = requiredArgs;
171
174
  return MEDIA_SLUG_FORMAT_PATTERN.test(slug) && !MEDIA_SLUG_UUID_SHAPE_PATTERN.test(slug);
172
175
  }
173
176
  /** Truncates a slug candidate to `maxLength`, stripping a trailing dash the cut can introduce (so a
@@ -234,7 +237,7 @@ async function deriveUniqueMediaSlug(mediaRepo, workspaceId, title) {
234
237
  }
235
238
  /**
236
239
  * Validates, hashes, dedups by `(workspaceId, sha256)`, and writes a new media
237
- * asset. Ordering (INV-1a): bytes are written (or found already
240
+ * asset. Ordering : bytes are written (or found already
238
241
  * present via dedup) BEFORE the media row is saved — a media row never exists
239
242
  * without corresponding bytes.
240
243
  *
@@ -253,48 +256,49 @@ async function deriveUniqueMediaSlug(mediaRepo, workspaceId, title) {
253
256
  * overhead, not a scan) — plus {@link deriveUniqueMediaSlug}'s own O(n) in the number of prior
254
257
  * same-base-slug collisions (see that function's doc).
255
258
  * @overallScore 100
259
+ * See docs/decisions/DR-004-journaled-blob-gc.md.
256
260
  */
257
261
  export async function uploadMedia(required, optional = {}) {
258
262
  const { deps, input } = required;
259
263
  const maxUploadBytes = optional.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
260
264
  const allowedMimeTypes = optional.allowedMimeTypes ?? DEFAULT_ALLOWED_MIME_TYPES;
261
265
  if (!allowedMimeTypes.has(input.contentType)) {
262
- throw new MediaValidationError(`content type '${input.contentType}' is not allowed for upload`);
266
+ throw new MediaValidationError({ message: `content type '${input.contentType}' is not allowed for upload` });
263
267
  }
264
268
  if (input.bytes.byteLength === 0) {
265
- throw new MediaValidationError("uploaded file is empty");
269
+ throw new MediaValidationError({ message: "uploaded file is empty" });
266
270
  }
267
271
  if (input.bytes.byteLength > maxUploadBytes) {
268
- throw new MediaValidationError(`uploaded file exceeds the ${formatMaxUploadBytesAsMb(maxUploadBytes)} size cap`);
272
+ throw new MediaValidationError({ message: `uploaded file exceeds the ${formatMaxUploadBytesAsMb(maxUploadBytes)} size cap` });
269
273
  }
270
274
  const sha256 = createHash("sha256").update(input.bytes).digest("hex");
271
- const nowIso = deps.clock.nowIso();
272
- const storageKey = await withSha256Lock(sha256, async () => {
273
- const existingBlob = await deps.blobRepo.findByHash({ workspaceId: input.workspaceId, sha256 });
274
- if (existingBlob) {
275
- if (existingBlob.status === "tombstoned") {
276
- // Resurrect: cancel the pending GC in the same locked section rather
277
- // than writing a duplicate blob (INV-1 dedup rule).
278
- await deps.blobRepo.save({ ...existingBlob, status: "active", tombstonedAt: undefined });
275
+ const nowIso = kernelNowIso({ clock: deps.clock });
276
+ const storageKey = await withSha256Lock({ key: sha256, criticalSection: async () => {
277
+ const existingBlob = await deps.blobRepo.findByHash({ workspaceId: input.workspaceId, sha256 });
278
+ if (existingBlob) {
279
+ if (existingBlob.status === "tombstoned") {
280
+ // Resurrect: cancel the pending GC in the same locked section rather
281
+ // than writing a duplicate blob ( dedup rule). See docs/decisions/DR-004-journaled-blob-gc.md.
282
+ await deps.blobRepo.save({ ...existingBlob, status: "active", tombstonedAt: undefined });
283
+ }
284
+ return existingBlob.storageKey;
279
285
  }
280
- return existingBlob.storageKey;
281
- }
282
- const written = await deps.blobStore.put({
283
- workspaceId: input.workspaceId,
284
- sha256,
285
- bytes: input.bytes,
286
- });
287
- await deps.blobRepo.save({
288
- id: deps.idGen.newId(),
289
- workspaceId: input.workspaceId,
290
- sha256,
291
- storageKey: written.storageKey,
292
- status: "active",
293
- createdByPrincipal: input.createdByPrincipal,
294
- createdAt: nowIso,
295
- });
296
- return written.storageKey;
297
- });
286
+ const written = await deps.blobStore.put({
287
+ workspaceId: input.workspaceId,
288
+ sha256,
289
+ bytes: input.bytes,
290
+ });
291
+ await deps.blobRepo.save({
292
+ id: deps.idGen.newId(),
293
+ workspaceId: input.workspaceId,
294
+ sha256,
295
+ storageKey: written.storageKey,
296
+ status: "active",
297
+ createdByPrincipal: input.createdByPrincipal,
298
+ createdAt: nowIso,
299
+ });
300
+ return written.storageKey;
301
+ } });
298
302
  const title = deriveTitleFromFilename(input.filename);
299
303
  const slug = await deriveUniqueMediaSlug(deps.mediaRepo, input.workspaceId, title);
300
304
  const media = {
@@ -353,7 +357,7 @@ export async function getMediaById(required, _optional = {}) {
353
357
  const { workspaceId, id } = required.input;
354
358
  const media = await required.deps.mediaRepo.findById({ workspaceId, id });
355
359
  if (!media)
356
- throw new MediaNotFoundError(`media '${id}' was not found`);
360
+ throw new MediaNotFoundError({ message: `media '${id}' was not found` });
357
361
  return { media };
358
362
  }
359
363
  /**
@@ -373,11 +377,12 @@ export async function getMediaById(required, _optional = {}) {
373
377
  * falls through.
374
378
  *
375
379
  * Never throws (unlike {@link getMediaById}): every call site that needs this (an embed resolver, a
376
- * public rendition route) already has its own REQ-27-style non-throwing-miss contract, so this
380
+ * public rendition route) already has its own -style non-throwing-miss contract, so this
377
381
  * returns `null` on failure and lets the caller apply its own not-found handling rather than forcing
378
382
  * one shape on every caller.
379
383
  *
380
384
  * @complexity O(1) — at most two indexed repo lookups, short-circuited on the first hit.
385
+ * See docs/decisions/DR-004-journaled-blob-gc.md.
381
386
  */
382
387
  export async function findMediaByIdOrSlug(required, _optional = {}) {
383
388
  const { deps, input } = required;
@@ -402,7 +407,7 @@ export async function findMediaByIdOrSlug(required, _optional = {}) {
402
407
  * `undefined` (leave unchanged) both bypass this — only a real provided number is checked. */
403
408
  function assertPositiveIntegerOrThrow(value, field) {
404
409
  if (!Number.isInteger(value) || value <= 0) {
405
- throw new MediaValidationError(`media.${field} must be a positive integer, got ${value}`);
410
+ throw new MediaValidationError({ message: `media.${field} must be a positive integer, got ${value}` });
406
411
  }
407
412
  }
408
413
  /**
@@ -418,18 +423,18 @@ function assertPositiveIntegerOrThrow(value, field) {
418
423
  */
419
424
  async function resolveSlugForUpdate(mediaRepo, workspaceId, id, rawSlug) {
420
425
  const slug = rawSlug.trim().toLowerCase();
421
- if (!slug || !isValidMediaSlugFormat(slug)) {
422
- throw new MediaValidationError("slug must use lowercase letters, numbers, and dashes, and must not be shaped like a UUID");
426
+ if (!slug || !isValidMediaSlugFormat({ slug: slug })) {
427
+ throw new MediaValidationError({ message: "slug must use lowercase letters, numbers, and dashes, and must not be shaped like a UUID" });
423
428
  }
424
429
  // Same bound and rejection style as `post.ts`'s `resolveExplicitSlug` — a human typed this value,
425
430
  // so it is refused with a clear reason rather than silently truncated (unlike the derived-on-
426
431
  // upload path, see `MEDIA_MAX_SLUG_LENGTH`'s doc).
427
432
  if (slug.length > MEDIA_MAX_SLUG_LENGTH) {
428
- throw new MediaValidationError(`slug must be ${MEDIA_MAX_SLUG_LENGTH} characters or fewer`);
433
+ throw new MediaValidationError({ message: `slug must be ${MEDIA_MAX_SLUG_LENGTH} characters or fewer` });
429
434
  }
430
435
  const duplicate = await mediaRepo.findBySlug({ workspaceId, slug });
431
436
  if (duplicate && duplicate.id !== id) {
432
- throw new MediaConflictError(`slug '${slug}' is already used by media '${duplicate.id}'`);
437
+ throw new MediaConflictError({ message: `slug '${slug}' is already used by media '${duplicate.id}'` });
433
438
  }
434
439
  return slug;
435
440
  }
@@ -447,9 +452,9 @@ function resolveHtmlAttributesForUpdate(rawValue) {
447
452
  const trimmed = rawValue.trim();
448
453
  if (trimmed === "")
449
454
  return null;
450
- const parsed = parseMediaHtmlAttributes(trimmed);
455
+ const parsed = parseMediaHtmlAttributes({ text: trimmed });
451
456
  if (parsed.error) {
452
- throw new MediaValidationError(`media.htmlAttributes: ${describeMediaHtmlAttributeError(parsed.error)}`);
457
+ throw new MediaValidationError({ message: `media.htmlAttributes: ${describeMediaHtmlAttributeError(parsed.error)}` });
453
458
  }
454
459
  return trimmed;
455
460
  }
@@ -457,7 +462,7 @@ export async function updateMediaMetadata(required, _optional = {}) {
457
462
  const { deps, input } = required;
458
463
  const existing = await deps.mediaRepo.findById({ workspaceId: input.workspaceId, id: input.id });
459
464
  if (!existing)
460
- throw new MediaNotFoundError(`media '${input.id}' was not found`);
465
+ throw new MediaNotFoundError({ message: `media '${input.id}' was not found` });
461
466
  assertEntityLive({ entityType: "media", entityId: input.id, state: existing.status === "trashed" ? "trashed" : "live" });
462
467
  if (input.width !== undefined && input.width !== null)
463
468
  assertPositiveIntegerOrThrow(input.width, "width");
@@ -486,7 +491,7 @@ export async function updateMediaMetadata(required, _optional = {}) {
486
491
  height: input.height !== undefined ? input.height : existing.height,
487
492
  cssClass: input.cssClass !== undefined ? (input.cssClass === null ? null : input.cssClass.trim() || null) : existing.cssClass,
488
493
  htmlAttributes,
489
- updatedAt: deps.clock.nowIso(),
494
+ updatedAt: kernelNowIso({ clock: deps.clock }),
490
495
  version: existing.version + 1,
491
496
  };
492
497
  await deps.mediaRepo.save(media);
@@ -503,13 +508,13 @@ export async function trashMedia(required, _optional = {}) {
503
508
  const { deps, input } = required;
504
509
  const existing = await deps.mediaRepo.findById(input);
505
510
  if (!existing)
506
- throw new MediaNotFoundError(`media '${input.id}' was not found`);
511
+ throw new MediaNotFoundError({ message: `media '${input.id}' was not found` });
507
512
  if (existing.status === "trashed")
508
513
  return { media: existing };
509
514
  const media = {
510
515
  ...existing,
511
516
  status: "trashed",
512
- updatedAt: deps.clock.nowIso(),
517
+ updatedAt: kernelNowIso({ clock: deps.clock }),
513
518
  version: existing.version + 1,
514
519
  };
515
520
  await deps.mediaRepo.save(media);
@@ -551,12 +556,12 @@ export async function purgeMedia(required, _optional = {}) {
551
556
  const { deps, input } = required;
552
557
  const existing = await deps.mediaRepo.findById(input);
553
558
  if (!existing)
554
- throw new MediaNotFoundError(`media '${input.id}' was not found`);
559
+ throw new MediaNotFoundError({ message: `media '${input.id}' was not found` });
555
560
  if (existing.status !== "trashed") {
556
- throw new MediaStillReferencedError(`media '${input.id}' must be trashed before it can be purged`, [
557
- `media '${input.id}' is still active (stand-in for the real entry_refs ` +
558
- `where-used index, which is not implemented — see purgeMedia's doc comment)`,
559
- ]);
561
+ throw new MediaStillReferencedError({ message: `media '${input.id}' must be trashed before it can be purged`, referencing: [
562
+ `media '${input.id}' is still active (stand-in for the real entry_refs ` +
563
+ `where-used index, which is not implemented — see purgeMedia's doc comment)`,
564
+ ] });
560
565
  }
561
566
  await removeMediaRowsAndTombstoneBlob(deps, {
562
567
  workspaceId: input.workspaceId,
@@ -578,7 +583,7 @@ export async function purgeMedia(required, _optional = {}) {
578
583
  async function removeMediaRowsAndTombstoneBlob(deps, input) {
579
584
  await deps.renditionRepo.removeByAsset({ workspaceId: input.workspaceId, assetId: input.id });
580
585
  await deps.mediaRepo.remove({ workspaceId: input.workspaceId, id: input.id });
581
- const clock = deps.clock ?? { nowIso: () => new Date().toISOString() };
586
+ const clock = deps.clock ?? createSystemClock();
582
587
  await tombstoneBlobIfUnreferenced({
583
588
  deps: { mediaRepo: deps.mediaRepo, blobRepo: deps.blobRepo, clock },
584
589
  input: { workspaceId: input.workspaceId, sha256: input.sha256 },
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * Interfaces only — no feature logic.
25
25
  */
26
- import type { UUID } from "../core/ports.js";
26
+ import type { UUID } from "@jini-ai/core/primitives";
27
27
  import type { AssetBlobRecord, AssetRenditionRecord, BlobGcJournalEntry, MediaRecord } from "./types.js";
28
28
  import type { TransformDefinitionRecord } from "./transform-types.js";
29
29
  export interface MediaRepoPort {
@@ -64,8 +64,9 @@ export interface AssetBlobRepoPort {
64
64
  }): Promise<void>;
65
65
  }
66
66
  /**
67
- * `blob_gc_journal` repo (INV-1 two-phase protocol). See
67
+ * `blob_gc_journal` repo ( two-phase protocol). See
68
68
  * {@link BlobGcJournalEntry} for what a row represents.
69
+ * See docs/decisions/DR-004-journaled-blob-gc.md.
69
70
  */
70
71
  export interface BlobGcJournalRepoPort {
71
72
  save(entry: BlobGcJournalEntry): Promise<void>;
@@ -1,4 +1,4 @@
1
- import type { ClockPort, IdGeneratorPort, UUID } from "../core/ports.js";
1
+ import type { Clock, IdGenerator, UUID } from "@jini-ai/core/primitives";
2
2
  import type { AssetBlobRepoPort, AssetRenditionRepoPort, BlobStorePort, MediaRepoPort, TransformDefinitionRepoPort } from "./ports.js";
3
3
  import type { ImageTransformerPort } from "./image-transformer.js";
4
4
  export interface ResolveMediaRenditionDeps {
@@ -8,8 +8,8 @@ export interface ResolveMediaRenditionDeps {
8
8
  transformRepo: TransformDefinitionRepoPort;
9
9
  blobStore: BlobStorePort;
10
10
  imageTransformer: ImageTransformerPort;
11
- clock: ClockPort;
12
- idGen: IdGeneratorPort;
11
+ clock: Clock;
12
+ idGen: IdGenerator;
13
13
  }
14
14
  export interface ResolveMediaRenditionInput {
15
15
  workspaceId: UUID;
@@ -1,3 +1,4 @@
1
+ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
1
2
  /**
2
3
  * @file Rendition resolution service — "serve-if-exists +
3
4
  * bounded generate-if-defined", the read path behind the frozen public URL
@@ -82,7 +83,7 @@ export async function resolveMediaRendition(required, _optional = {}) {
82
83
  });
83
84
  if (existing) {
84
85
  const bytes = await deps.blobStore.get({ storageKey: existing.storageKey });
85
- return { outcome: "ok", bytes, contentType: mimeForTransformFormat(definition.params.format) };
86
+ return { outcome: "ok", bytes, contentType: mimeForTransformFormat({ format: definition.params.format }) };
86
87
  }
87
88
  const generationAllowed = isReferencedByPublishedContent({ workspaceId: input.workspaceId, name: input.transformName, version: input.version }) ||
88
89
  (await isLatestTransformVersion({
@@ -100,42 +101,42 @@ export async function resolveMediaRendition(required, _optional = {}) {
100
101
  throw new Error(`media '${media.id}': source blob for sha256 '${media.source.sha256}' was not found`);
101
102
  }
102
103
  const lockKey = `${media.source.sha256}:${input.transformName}:${input.version}`;
103
- const renditionRow = await withRenditionLock(lockKey, async () => {
104
- // Double-checked: another caller may have finished generating this exact rendition while we
105
- // were queued for the lock (single-flight) — reuse it instead of calling
106
- // `imageTransformer.transform` a second time.
107
- const racedExisting = await deps.renditionRepo.findOne({
108
- workspaceId: input.workspaceId,
109
- assetId,
110
- transformName: input.transformName,
111
- version: input.version,
112
- });
113
- if (racedExisting)
114
- return racedExisting;
115
- const sourceBytes = await deps.blobStore.get({ storageKey: sourceBlob.storageKey });
116
- const { bytes: outputBytes } = await deps.imageTransformer.transform({
117
- bytes: sourceBytes,
118
- params: definition.params,
119
- });
120
- const outputSha256 = createHash("sha256").update(outputBytes).digest("hex");
121
- const { storageKey } = await deps.blobStore.put({
122
- workspaceId: input.workspaceId,
123
- sha256: outputSha256,
124
- bytes: outputBytes,
125
- });
126
- const row = {
127
- id: deps.idGen.newId(),
128
- workspaceId: input.workspaceId,
129
- assetId,
130
- transformName: input.transformName,
131
- version: input.version,
132
- storageKey,
133
- createdAt: deps.clock.nowIso(),
134
- };
135
- await deps.renditionRepo.save(row);
136
- return row;
137
- });
104
+ const renditionRow = await withRenditionLock({ key: lockKey, criticalSection: async () => {
105
+ // Double-checked: another caller may have finished generating this exact rendition while we
106
+ // were queued for the lock (single-flight) — reuse it instead of calling
107
+ // `imageTransformer.transform` a second time.
108
+ const racedExisting = await deps.renditionRepo.findOne({
109
+ workspaceId: input.workspaceId,
110
+ assetId,
111
+ transformName: input.transformName,
112
+ version: input.version,
113
+ });
114
+ if (racedExisting)
115
+ return racedExisting;
116
+ const sourceBytes = await deps.blobStore.get({ storageKey: sourceBlob.storageKey });
117
+ const { bytes: outputBytes } = await deps.imageTransformer.transform({
118
+ bytes: sourceBytes,
119
+ params: definition.params,
120
+ });
121
+ const outputSha256 = createHash("sha256").update(outputBytes).digest("hex");
122
+ const { storageKey } = await deps.blobStore.put({
123
+ workspaceId: input.workspaceId,
124
+ sha256: outputSha256,
125
+ bytes: outputBytes,
126
+ });
127
+ const row = {
128
+ id: deps.idGen.newId(),
129
+ workspaceId: input.workspaceId,
130
+ assetId,
131
+ transformName: input.transformName,
132
+ version: input.version,
133
+ storageKey,
134
+ createdAt: kernelNowIso({ clock: deps.clock }),
135
+ };
136
+ await deps.renditionRepo.save(row);
137
+ return row;
138
+ } });
138
139
  const bytes = await deps.blobStore.get({ storageKey: renditionRow.storageKey });
139
- return { outcome: "ok", bytes, contentType: mimeForTransformFormat(definition.params.format) };
140
+ return { outcome: "ok", bytes, contentType: mimeForTransformFormat({ format: definition.params.format }) };
140
141
  }
141
142
  //# sourceMappingURL=rendition-service.js.map
@@ -9,13 +9,15 @@
9
9
  * Adapters only — dumb collections scoped by `workspaceId`. No
10
10
  * validation or business rules live here (that's `media-service.ts`'s job).
11
11
  */
12
- import type { UUID } from "../core/ports.js";
12
+ import type { UUID } from "@jini-ai/core/primitives";
13
13
  import type { AssetBlobRepoPort, AssetRenditionRepoPort, BlobGcJournalRepoPort, MediaRepoPort, TransformDefinitionRepoPort } from "./ports.js";
14
14
  import type { AssetBlobRecord, AssetRenditionRecord, BlobGcJournalEntry, MediaRecord } from "./types.js";
15
15
  import type { TransformDefinitionRecord } from "./transform-types.js";
16
16
  export declare class InMemoryMediaRepo implements MediaRepoPort {
17
17
  private rows;
18
- constructor(initialRows?: MediaRecord[]);
18
+ constructor(requiredArgs: Record<string, never>, optionalArgs?: {
19
+ initialRows?: MediaRecord[];
20
+ });
19
21
  findById(required: {
20
22
  workspaceId: UUID;
21
23
  id: UUID;
@@ -35,7 +37,9 @@ export declare class InMemoryMediaRepo implements MediaRepoPort {
35
37
  }
36
38
  export declare class InMemoryAssetBlobRepo implements AssetBlobRepoPort {
37
39
  private rows;
38
- constructor(initialRows?: AssetBlobRecord[]);
40
+ constructor(requiredArgs: Record<string, never>, optionalArgs?: {
41
+ initialRows?: AssetBlobRecord[];
42
+ });
39
43
  findByHash(required: {
40
44
  workspaceId: UUID;
41
45
  sha256: string;
@@ -51,7 +55,9 @@ export declare class InMemoryAssetBlobRepo implements AssetBlobRepoPort {
51
55
  }
52
56
  export declare class InMemoryAssetRenditionRepo implements AssetRenditionRepoPort {
53
57
  private rows;
54
- constructor(initialRows?: AssetRenditionRecord[]);
58
+ constructor(requiredArgs: Record<string, never>, optionalArgs?: {
59
+ initialRows?: AssetRenditionRecord[];
60
+ });
55
61
  listByAsset(required: {
56
62
  workspaceId: UUID;
57
63
  assetId: UUID;
@@ -69,15 +75,18 @@ export declare class InMemoryAssetRenditionRepo implements AssetRenditionRepoPor
69
75
  }): Promise<void>;
70
76
  }
71
77
  /**
72
- * In-memory `blob_gc_journal` adapter (INV-1 two-phase protocol —
78
+ * In-memory `blob_gc_journal` adapter ( two-phase protocol —
73
79
  * see `blob-gc.ts`). Same disclosed limit as every other repo in this file:
74
80
  * rows do not survive a process restart, so a real crash between the
75
81
  * delete-pass and unlink-pass loses the journal entry in this build (a real
76
82
  * database-backed table would not).
83
+ * See docs/decisions/DR-004-journaled-blob-gc.md.
77
84
  */
78
85
  export declare class InMemoryBlobGcJournalRepo implements BlobGcJournalRepoPort {
79
86
  private rows;
80
- constructor(initialRows?: BlobGcJournalEntry[]);
87
+ constructor(requiredArgs: Record<string, never>, optionalArgs?: {
88
+ initialRows?: BlobGcJournalEntry[];
89
+ });
81
90
  save(entry: BlobGcJournalEntry): Promise<void>;
82
91
  list(required: {
83
92
  workspaceId: UUID;
@@ -96,7 +105,9 @@ export declare class InMemoryBlobGcJournalRepo implements BlobGcJournalRepoPort
96
105
  */
97
106
  export declare class InMemoryTransformDefinitionRepo implements TransformDefinitionRepoPort {
98
107
  private rows;
99
- constructor(initialRows?: TransformDefinitionRecord[]);
108
+ constructor(requiredArgs: Record<string, never>, optionalArgs?: {
109
+ initialRows?: TransformDefinitionRecord[];
110
+ });
100
111
  listByName(required: {
101
112
  workspaceId: UUID;
102
113
  name: string;
@@ -1,6 +1,7 @@
1
1
  export class InMemoryMediaRepo {
2
2
  rows;
3
- constructor(initialRows = []) {
3
+ constructor(requiredArgs, optionalArgs = {}) {
4
+ const { initialRows = [] } = optionalArgs;
4
5
  this.rows = [...initialRows];
5
6
  }
6
7
  async findById(required) {
@@ -26,7 +27,8 @@ export class InMemoryMediaRepo {
26
27
  }
27
28
  export class InMemoryAssetBlobRepo {
28
29
  rows;
29
- constructor(initialRows = []) {
30
+ constructor(requiredArgs, optionalArgs = {}) {
31
+ const { initialRows = [] } = optionalArgs;
30
32
  this.rows = [...initialRows];
31
33
  }
32
34
  async findByHash(required) {
@@ -49,7 +51,8 @@ export class InMemoryAssetBlobRepo {
49
51
  }
50
52
  export class InMemoryAssetRenditionRepo {
51
53
  rows;
52
- constructor(initialRows = []) {
54
+ constructor(requiredArgs, optionalArgs = {}) {
55
+ const { initialRows = [] } = optionalArgs;
53
56
  this.rows = [...initialRows];
54
57
  }
55
58
  async listByAsset(required) {
@@ -74,15 +77,17 @@ export class InMemoryAssetRenditionRepo {
74
77
  }
75
78
  }
76
79
  /**
77
- * In-memory `blob_gc_journal` adapter (INV-1 two-phase protocol —
80
+ * In-memory `blob_gc_journal` adapter ( two-phase protocol —
78
81
  * see `blob-gc.ts`). Same disclosed limit as every other repo in this file:
79
82
  * rows do not survive a process restart, so a real crash between the
80
83
  * delete-pass and unlink-pass loses the journal entry in this build (a real
81
84
  * database-backed table would not).
85
+ * See docs/decisions/DR-004-journaled-blob-gc.md.
82
86
  */
83
87
  export class InMemoryBlobGcJournalRepo {
84
88
  rows;
85
- constructor(initialRows = []) {
89
+ constructor(requiredArgs, optionalArgs = {}) {
90
+ const { initialRows = [] } = optionalArgs;
86
91
  this.rows = [...initialRows];
87
92
  }
88
93
  async save(entry) {
@@ -109,7 +114,8 @@ export class InMemoryBlobGcJournalRepo {
109
114
  */
110
115
  export class InMemoryTransformDefinitionRepo {
111
116
  rows;
112
- constructor(initialRows = []) {
117
+ constructor(requiredArgs, optionalArgs = {}) {
118
+ const { initialRows = [] } = optionalArgs;
113
119
  this.rows = [...initialRows];
114
120
  }
115
121
  async listByName(required) {
@@ -1,3 +1,5 @@
1
+ import type { IdGenerator } from "@jini-ai/core/primitives";
2
+ import type { Clock } from "@jini-ai/core/primitives";
1
3
  /**
2
4
  * @file Media's tool-registration wiring: maps `agent-tools.ts`'s four catalog entries onto
3
5
  * `media-service.ts`'s list/upload/update/trash operations, as `ToolRegistration`s. The entire
@@ -5,7 +7,7 @@
5
7
  * present-but-unwired; see `media/agent-tools.ts`'s own header.
6
8
  *
7
9
  * Authorization shape: unlike Forms/Identity/Widgets, `media-service.ts`'s functions perform NO
8
- * internal `authorize()` call of their own — every admin HTTP route a host builds gates inline
10
+ * internal `authorize` call of their own — every admin HTTP route a host builds gates inline
9
11
  * instead. Every handler here does the same via the kit's `requireToolPermission`, which is the
10
12
  * single evaluation for these tools, located where a real route would locate it.
11
13
  *
@@ -13,7 +15,7 @@
13
15
  * own `publicUrl` addition closed there: an agent could upload/list an asset but had no tool-facing
14
16
  * way to learn a URL usable to embed it in a post/page). Unlike `post`, this package has no host to
15
17
  * resolve a public URL against — the `/m/{assetId}/{transformName}.v{version}/...` contract
16
- * (ADR-027 §4) is a HOST decision (which URL prefix, which transform pipeline, whether one even
18
+ * is a HOST decision (which URL prefix, which transform pipeline, whether one even
17
19
  * exists), not a `@jini-ai/cms` one; this package deliberately has zero `/m/`-shaped string
18
20
  * literals anywhere. So `publicUrl` is resolved through an OPTIONAL, host-injected
19
21
  * {@link MediaToolDeps.resolvePublicUrls}, batch-shaped (one call per `media_list_assets`/
@@ -26,26 +28,23 @@
26
28
  * `features/post/tool-registrations.ts`'s identical reasoning for skipping `content_post_update`/
27
29
  * `content_post_delete`: those two return the row incidentally, to confirm what was just
28
30
  * edited/trashed, not to answer "where does this live".
31
+ * See docs/decisions/DR-004-journaled-blob-gc.md.
29
32
  */
30
33
  import type { AuthorizeFn } from "../core/commands/command.js";
31
- import { type DerivedRiskByToolId, type ToolRegistration } from "../core/tools/registration-kit.js";
34
+ import { type DerivedRiskByToolId, type ToolRegistration } from "@jini-ai/core";
32
35
  import type { AssetBlobRepoPort, AssetRenditionRepoPort, BlobStorePort, MediaRepoPort } from "./ports.js";
33
36
  import type { MediaRecord } from "./types.js";
34
37
  /**
35
38
  * The exact slice of a host's route-deps bag Media's tool handlers read. Declared structurally
36
39
  * (rather than importing any host's own route-deps type) so this module carries no back-edge into
37
40
  * a host's composition root. A host satisfies this structurally by passing its existing route deps
38
- * object; nothing there needs to change shape.
41
+ * object after binding the kernel clock/ID contracts.
39
42
  */
40
43
  export interface MediaToolDeps {
41
44
  authorize: AuthorizeFn;
42
45
  workspaceId: string;
43
- clock: {
44
- nowIso(): string;
45
- };
46
- idGen: {
47
- newId(): string;
48
- };
46
+ clock: Clock;
47
+ idGen: IdGenerator;
49
48
  mediaRepo: MediaRepoPort;
50
49
  assetBlobRepo: AssetBlobRepoPort;
51
50
  assetRenditionRepo: AssetRenditionRepoPort;
@@ -58,7 +57,9 @@ export interface MediaToolDeps {
58
57
  * resolution failed soft). Omitted entirely: every response's `publicUrl` is `null`, matching this
59
58
  * field's own pre-2026-09-02 absence for every host that has not opted in yet.
60
59
  */
61
- resolvePublicUrls?: (assets: readonly MediaRecord[]) => Promise<ReadonlyMap<string, string | null>>;
60
+ resolvePublicUrls?: ((required: {
61
+ assets: readonly MediaRecord[];
62
+ }, optional?: Record<string, never>) => Promise<ReadonlyMap<string, string | null>>) | undefined;
62
63
  /**
63
64
  * Optional hook run once, right after `media_upload_asset`'s own `uploadMedia()` call succeeds —
64
65
  * the fix for a defect where a tool-driven upload recorded NO content type anywhere. `uploadMedia`
@@ -88,10 +89,10 @@ export interface MediaToolDeps {
88
89
  * matching this field's own pre-fix absence for every host that has not opted in yet — the exact
89
90
  * `resolvePublicUrls`-precedent contract.
90
91
  */
91
- recordUploadContentType?: (params: {
92
+ recordUploadContentType?: ((params: {
92
93
  media: MediaRecord;
93
94
  bytes: Uint8Array;
94
- }) => Promise<void>;
95
+ }) => Promise<void>) | undefined;
95
96
  /**
96
97
  * Optional host-supplied override of `uploadMedia`'s own `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB,
97
98
  * `media-service.ts`), forwarded verbatim as `uploadMedia`'s third (`optional`) argument in the
@@ -103,7 +104,7 @@ export interface MediaToolDeps {
103
104
  * pre-2026-09-21 absence for every host that has not opted in yet — the same degrade-soft
104
105
  * contract {@link resolvePublicUrls} and {@link recordUploadContentType} already established.
105
106
  */
106
- maxUploadBytes?: number;
107
+ maxUploadBytes?: number | undefined;
107
108
  }
108
109
  /**
109
110
  * This wiring layer's OWN risk classification, authored from what each handler below actually
@@ -111,5 +112,5 @@ export interface MediaToolDeps {
111
112
  * `sideEffects` declaration.
112
113
  */
113
114
  export declare const mediaDerivedRisk: DerivedRiskByToolId;
114
- export declare function buildMediaRegistrations(routeDeps: MediaToolDeps): ToolRegistration[];
115
+ export declare function buildMediaRegistrations(required: Omit<MediaToolDeps, "resolvePublicUrls" | "recordUploadContentType" | "maxUploadBytes">, optional?: Pick<MediaToolDeps, "resolvePublicUrls" | "recordUploadContentType" | "maxUploadBytes">): ToolRegistration[];
115
116
  //# sourceMappingURL=tool-registrations.d.ts.map