@jini-ai/cms 0.3.11 → 0.4.1
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.
- package/CHANGELOG.md +59 -0
- package/README.md +47 -19
- package/dist/content-types/agent-tools.d.ts +3 -56
- package/dist/content-types/agent-tools.js +22 -6
- package/dist/content-types/cleanup.d.ts +18 -13
- package/dist/content-types/cleanup.js +16 -11
- package/dist/content-types/errors.d.ts +57 -24
- package/dist/content-types/errors.js +56 -39
- package/dist/content-types/field-defs.d.ts +8 -4
- package/dist/content-types/field-defs.js +19 -14
- package/dist/content-types/index-provisioning.d.ts +61 -47
- package/dist/content-types/index-provisioning.js +90 -66
- package/dist/content-types/index.d.ts +4 -3
- package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
- package/dist/content-types/lifecycle-dispatch.js +5 -4
- package/dist/content-types/lifecycle.d.ts +23 -18
- package/dist/content-types/lifecycle.js +71 -64
- package/dist/content-types/repo.memory.d.ts +5 -4
- package/dist/content-types/repo.memory.js +3 -2
- package/dist/content-types/tool-registrations.d.ts +6 -8
- package/dist/content-types/tool-registrations.js +24 -28
- package/dist/content-types/types.d.ts +17 -13
- package/dist/content-types/types.js +11 -5
- package/dist/content-types/write-service.d.ts +20 -22
- package/dist/content-types/write-service.js +78 -75
- package/dist/core/authorization.d.ts +15 -0
- package/dist/core/authorization.js +2 -0
- package/dist/core/commands/change-set.d.ts +15 -7
- package/dist/core/commands/command.d.ts +45 -26
- package/dist/core/commands/command.js +73 -63
- package/dist/core/entity-liveness.d.ts +11 -7
- package/dist/core/entity-liveness.js +5 -4
- package/dist/core/index.d.ts +12 -5
- package/dist/core/index.js +10 -4
- package/dist/core/ports.d.ts +25 -24
- package/dist/core/ports.js +1 -0
- package/dist/core/tools/args.d.ts +49 -0
- package/dist/core/tools/args.js +46 -0
- package/dist/core/tools/index.d.ts +3 -0
- package/dist/core/tools/index.js +3 -0
- package/dist/entries/agent-tools.d.ts +1 -73
- package/dist/entries/agent-tools.js +19 -13
- package/dist/entries/errors.d.ts +28 -14
- package/dist/entries/errors.js +17 -11
- package/dist/entries/field-validation.d.ts +5 -3
- package/dist/entries/field-validation.js +7 -5
- package/dist/entries/index.d.ts +2 -2
- package/dist/entries/lifecycle-dispatch.d.ts +5 -2
- package/dist/entries/lifecycle-dispatch.js +2 -1
- package/dist/entries/list.d.ts +4 -2
- package/dist/entries/list.js +4 -3
- package/dist/entries/repo.memory.d.ts +12 -10
- package/dist/entries/repo.memory.js +11 -10
- package/dist/entries/tool-registrations.d.ts +6 -8
- package/dist/entries/tool-registrations.js +42 -41
- package/dist/entries/types.d.ts +0 -7
- package/dist/entries/write-service.d.ts +28 -29
- package/dist/entries/write-service.js +114 -105
- package/dist/http/settings/cms-adapter.d.ts +20 -0
- package/dist/http/settings/cms-adapter.js +124 -0
- package/dist/http/settings/contracts.d.ts +135 -0
- package/dist/http/settings/contracts.js +2 -0
- package/dist/http/settings/http.d.ts +10 -0
- package/dist/http/settings/http.js +211 -0
- package/dist/http/settings/index.d.ts +5 -0
- package/dist/http/settings/index.js +4 -0
- package/dist/http/settings/shared.d.ts +68 -0
- package/dist/http/settings/shared.js +54 -0
- package/dist/http/settings/stream.d.ts +24 -0
- package/dist/http/settings/stream.js +147 -0
- package/dist/media/agent-tools.d.ts +1 -66
- package/dist/media/agent-tools.js +5 -1
- package/dist/media/blob-gc-lock.d.ts +6 -2
- package/dist/media/blob-gc-lock.js +4 -2
- package/dist/media/blob-gc.d.ts +64 -51
- package/dist/media/blob-gc.js +62 -52
- package/dist/media/blob-key.d.ts +1 -1
- package/dist/media/content-type-sniffer.d.ts +3 -1
- package/dist/media/content-type-sniffer.js +1 -1
- package/dist/media/html-attributes.d.ts +9 -5
- package/dist/media/html-attributes.js +8 -6
- package/dist/media/image-transformer.js +1 -1
- package/dist/media/image-transformer.sharp.d.ts +6 -0
- package/dist/media/image-transformer.sharp.js +12 -6
- package/dist/media/import/fetch-image.d.ts +24 -0
- package/dist/media/import/fetch-image.js +137 -0
- package/dist/media/import/index.d.ts +3 -0
- package/dist/media/import/index.js +3 -0
- package/dist/media/import/ports.d.ts +38 -0
- package/dist/media/index.d.ts +7 -1
- package/dist/media/index.js +1 -0
- package/dist/media/media-service.d.ts +14 -10
- package/dist/media/media-service.js +57 -52
- package/dist/media/ports.d.ts +3 -2
- package/dist/media/rendition-service.d.ts +3 -3
- package/dist/media/rendition-service.js +38 -37
- package/dist/media/repo.memory.d.ts +18 -7
- package/dist/media/repo.memory.js +12 -6
- package/dist/media/tool-registrations.d.ts +16 -15
- package/dist/media/tool-registrations.js +84 -72
- package/dist/media/transform-lock.d.ts +22 -12
- package/dist/media/transform-lock.js +14 -13
- package/dist/media/transform-registry.d.ts +3 -3
- package/dist/media/transform-registry.js +19 -18
- package/dist/media/transform-types.d.ts +8 -3
- package/dist/media/transform-types.js +10 -5
- package/dist/media/types.d.ts +21 -4
- package/dist/media/types.js +15 -2
- package/dist/navigation/agent-tools.d.ts +1 -80
- package/dist/navigation/agent-tools.js +15 -3
- package/dist/navigation/contracts.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/menu-service.d.ts +57 -40
- package/dist/navigation/menu-service.js +80 -69
- package/dist/navigation/ports.d.ts +1 -1
- package/dist/navigation/read-model.d.ts +3 -1
- package/dist/navigation/read-model.js +2 -2
- package/dist/navigation/reconcile.d.ts +2 -2
- package/dist/navigation/reconcile.js +2 -1
- package/dist/navigation/repo.memory.d.ts +7 -3
- package/dist/navigation/repo.memory.js +4 -2
- package/dist/navigation/resolver.d.ts +4 -1
- package/dist/navigation/resolver.js +1 -1
- package/dist/navigation/tool-registrations.d.ts +6 -8
- package/dist/navigation/tool-registrations.js +44 -42
- package/dist/navigation/types.d.ts +1 -1
- package/dist/presentation/presentation.d.ts +11 -3
- package/dist/presentation/presentation.js +13 -6
- package/dist/presentation/repo.memory.d.ts +6 -2
- package/dist/presentation/repo.memory.js +3 -2
- package/dist/settings/agent-tools.d.ts +2 -75
- package/dist/settings/agent-tools.js +49 -45
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +154 -0
- package/dist/settings/agent-writable-preferences.js +8 -20
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +11 -2
- package/dist/settings/change-feed.js +4 -4
- package/dist/settings/definitions-dispatch.d.ts +8 -3
- package/dist/settings/definitions-dispatch.js +5 -5
- package/dist/settings/dictionaries/index.d.ts +5 -2
- package/dist/settings/dictionaries/index.js +2 -2
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
- package/dist/settings/ensure-definitions.d.ts +5 -5
- package/dist/settings/errors.d.ts +7 -1
- package/dist/settings/errors.js +7 -1
- package/dist/settings/index.d.ts +4 -1
- package/dist/settings/index.js +1 -0
- package/dist/settings/ports.d.ts +6 -5
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +5 -4
- package/dist/settings/purge-service.js +3 -2
- package/dist/settings/settings.d.ts +22 -7
- package/dist/settings/settings.js +47 -26
- package/dist/settings/tool-registrations.d.ts +27 -32
- package/dist/settings/tool-registrations.js +66 -56
- package/dist/settings/types.d.ts +1 -1
- package/dist/settings/ui-tab-definitions.d.ts +1 -1
- package/dist/settings/write-service.d.ts +32 -26
- package/dist/settings/write-service.js +26 -20
- package/dist/taxonomy/agent-tools.d.ts +1 -66
- package/dist/taxonomy/agent-tools.js +25 -15
- package/dist/taxonomy/content-lookup.js +1 -1
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/merge-term.d.ts +7 -4
- package/dist/taxonomy/merge-term.js +7 -5
- package/dist/taxonomy/repo.memory.d.ts +42 -19
- package/dist/taxonomy/repo.memory.js +14 -12
- package/dist/taxonomy/validation-chain.d.ts +39 -18
- package/dist/taxonomy/validation-chain.js +35 -25
- package/dist/taxonomy/write-service.d.ts +103 -44
- package/dist/taxonomy/write-service.js +221 -179
- package/dist/trash/adapters/record-store.d.ts +52 -0
- package/dist/trash/adapters/record-store.js +36 -0
- package/dist/trash/cursor.d.ts +20 -0
- package/dist/trash/cursor.js +36 -0
- package/dist/trash/follow-ups.d.ts +76 -0
- package/dist/trash/follow-ups.js +46 -0
- package/dist/trash/index.d.ts +8 -0
- package/dist/trash/index.js +8 -0
- package/dist/trash/ports.d.ts +308 -0
- package/dist/trash/ports.js +2 -0
- package/dist/trash/repo.memory.d.ts +50 -0
- package/dist/trash/repo.memory.js +114 -0
- package/dist/trash/sweeper.d.ts +77 -0
- package/dist/trash/sweeper.js +127 -0
- package/dist/trash/write-service.d.ts +100 -0
- package/dist/trash/write-service.js +242 -0
- package/dist/workspace/agent-tools.d.ts +1 -59
- package/dist/workspace/agent-tools.js +27 -12
- package/dist/workspace/create.d.ts +31 -6
- package/dist/workspace/create.js +18 -5
- package/dist/workspace/delete.d.ts +18 -13
- package/dist/workspace/delete.js +20 -12
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/repo.memory.d.ts +19 -4
- package/dist/workspace/repo.memory.js +26 -4
- package/dist/workspace/tool-registrations.d.ts +2 -2
- package/dist/workspace/tool-registrations.js +13 -13
- package/dist/workspace/update.d.ts +6 -5
- package/dist/workspace/update.js +9 -8
- package/package.json +44 -38
- package/dist/core/tools/registration-kit.d.ts +0 -346
- package/dist/core/tools/registration-kit.js +0 -426
- package/dist/identity/admin-crud-service.d.ts +0 -280
- package/dist/identity/admin-crud-service.js +0 -468
- package/dist/identity/agent-tool-input.d.ts +0 -58
- package/dist/identity/agent-tool-input.js +0 -126
- package/dist/identity/agent-tools.d.ts +0 -101
- package/dist/identity/agent-tools.js +0 -290
- package/dist/identity/auth-service.d.ts +0 -118
- package/dist/identity/auth-service.js +0 -170
- package/dist/identity/authorize.d.ts +0 -90
- package/dist/identity/authorize.js +0 -94
- package/dist/identity/grant-service.d.ts +0 -198
- package/dist/identity/grant-service.js +0 -361
- package/dist/identity/hasher.d.ts +0 -39
- package/dist/identity/hasher.js +0 -39
- package/dist/identity/index.d.ts +0 -62
- package/dist/identity/index.js +0 -54
- package/dist/identity/password-policy.d.ts +0 -57
- package/dist/identity/password-policy.js +0 -66
- package/dist/identity/permission-migrations.d.ts +0 -86
- package/dist/identity/permission-migrations.js +0 -67
- package/dist/identity/permissions.d.ts +0 -35
- package/dist/identity/permissions.js +0 -603
- package/dist/identity/ports.d.ts +0 -201
- package/dist/identity/repo.memory.d.ts +0 -164
- package/dist/identity/repo.memory.js +0 -224
- package/dist/identity/seed.d.ts +0 -54
- package/dist/identity/seed.js +0 -429
- package/dist/identity/tool-registrations.d.ts +0 -50
- package/dist/identity/tool-registrations.js +0 -419
- package/dist/identity/types.d.ts +0 -185
- package/dist/identity/types.js +0 -64
- package/dist/identity/username.d.ts +0 -12
- package/dist/identity/username.js +0 -14
- package/dist/server/index.d.ts +0 -28
- package/dist/server/index.js +0 -28
- /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
|
-
*
|
|
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(
|
|
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
|
|
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
|
|
272
|
-
const storageKey = await withSha256Lock(sha256, async () => {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
558
|
-
|
|
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 ??
|
|
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 },
|
package/dist/media/ports.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Interfaces only — no feature logic.
|
|
25
25
|
*/
|
|
26
|
-
import type { UUID } from "
|
|
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 (
|
|
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 {
|
|
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:
|
|
12
|
-
idGen:
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 "
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
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 "
|
|
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
|
|
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
|
-
|
|
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?: (
|
|
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(
|
|
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
|