@jini-ai/cms 0.3.6 → 0.3.7
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/dist/content-types/index-provisioning.d.ts +1 -1
- package/dist/content-types/index-provisioning.js +1 -1
- package/dist/core/ports.d.ts +1 -1
- package/dist/identity/auth-service.d.ts +1 -1
- package/dist/identity/auth-service.js +1 -1
- package/dist/identity/seed.d.ts +1 -1
- package/dist/identity/seed.js +3 -3
- package/dist/media/agent-tools.d.ts +13 -3
- package/dist/media/agent-tools.js +45 -10
- package/dist/media/html-attributes.d.ts +6 -6
- package/dist/media/html-attributes.js +6 -6
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/media-service.d.ts +2 -1
- package/dist/media/media-service.js +23 -7
- package/dist/media/tool-registrations.d.ts +12 -0
- package/dist/media/tool-registrations.js +20 -12
- package/dist/media/types.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/index.js +2 -2
- package/dist/navigation/menu-service.d.ts +4 -4
- package/dist/navigation/menu-service.js +4 -4
- package/dist/settings/dictionaries/index.d.ts +3 -3
- package/dist/settings/dictionaries/index.js +3 -3
- package/dist/taxonomy/agent-tools.d.ts +5 -4
- package/dist/taxonomy/agent-tools.js +33 -10
- package/dist/taxonomy/content-lookup.d.ts +35 -0
- package/dist/taxonomy/content-lookup.js +28 -0
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/index.js +2 -2
- package/dist/taxonomy/repo.memory.d.ts +11 -2
- package/dist/taxonomy/repo.memory.js +11 -0
- package/dist/taxonomy/write-service.d.ts +44 -15
- package/dist/taxonomy/write-service.js +60 -8
- package/package.json +3 -2
- package/dist/content-types/agent-tools.d.ts.map +0 -1
- package/dist/content-types/agent-tools.js.map +0 -1
- package/dist/content-types/cleanup.d.ts.map +0 -1
- package/dist/content-types/cleanup.js.map +0 -1
- package/dist/content-types/errors.d.ts.map +0 -1
- package/dist/content-types/errors.js.map +0 -1
- package/dist/content-types/field-defs.d.ts.map +0 -1
- package/dist/content-types/field-defs.js.map +0 -1
- package/dist/content-types/index-provisioning.d.ts.map +0 -1
- package/dist/content-types/index-provisioning.js.map +0 -1
- package/dist/content-types/index.d.ts.map +0 -1
- package/dist/content-types/index.js.map +0 -1
- package/dist/content-types/lifecycle-dispatch.d.ts.map +0 -1
- package/dist/content-types/lifecycle-dispatch.js.map +0 -1
- package/dist/content-types/lifecycle.d.ts.map +0 -1
- package/dist/content-types/lifecycle.js.map +0 -1
- package/dist/content-types/list.d.ts.map +0 -1
- package/dist/content-types/list.js.map +0 -1
- package/dist/content-types/repo.memory.d.ts.map +0 -1
- package/dist/content-types/repo.memory.js.map +0 -1
- package/dist/content-types/tool-registrations.d.ts.map +0 -1
- package/dist/content-types/tool-registrations.js.map +0 -1
- package/dist/content-types/types.d.ts.map +0 -1
- package/dist/content-types/types.js.map +0 -1
- package/dist/content-types/write-service.d.ts.map +0 -1
- package/dist/content-types/write-service.js.map +0 -1
- package/dist/core/commands/change-set.d.ts.map +0 -1
- package/dist/core/commands/change-set.js.map +0 -1
- package/dist/core/commands/command.d.ts.map +0 -1
- package/dist/core/commands/command.js.map +0 -1
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/core/ports.d.ts.map +0 -1
- package/dist/core/ports.js.map +0 -1
- package/dist/core/tools/registration-kit.d.ts.map +0 -1
- package/dist/core/tools/registration-kit.js.map +0 -1
- package/dist/entries/agent-tools.d.ts.map +0 -1
- package/dist/entries/agent-tools.js.map +0 -1
- package/dist/entries/errors.d.ts.map +0 -1
- package/dist/entries/errors.js.map +0 -1
- package/dist/entries/field-validation.d.ts.map +0 -1
- package/dist/entries/field-validation.js.map +0 -1
- package/dist/entries/index.d.ts.map +0 -1
- package/dist/entries/index.js.map +0 -1
- package/dist/entries/lifecycle-dispatch.d.ts.map +0 -1
- package/dist/entries/lifecycle-dispatch.js.map +0 -1
- package/dist/entries/list.d.ts.map +0 -1
- package/dist/entries/list.js.map +0 -1
- package/dist/entries/repo.memory.d.ts.map +0 -1
- package/dist/entries/repo.memory.js.map +0 -1
- package/dist/entries/tool-registrations.d.ts.map +0 -1
- package/dist/entries/tool-registrations.js.map +0 -1
- package/dist/entries/types.d.ts.map +0 -1
- package/dist/entries/types.js.map +0 -1
- package/dist/entries/write-service.d.ts.map +0 -1
- package/dist/entries/write-service.js.map +0 -1
- package/dist/identity/admin-crud-service.d.ts.map +0 -1
- package/dist/identity/admin-crud-service.js.map +0 -1
- package/dist/identity/agent-tool-input.d.ts.map +0 -1
- package/dist/identity/agent-tool-input.js.map +0 -1
- package/dist/identity/agent-tools.d.ts.map +0 -1
- package/dist/identity/agent-tools.js.map +0 -1
- package/dist/identity/auth-service.d.ts.map +0 -1
- package/dist/identity/auth-service.js.map +0 -1
- package/dist/identity/authorize.d.ts.map +0 -1
- package/dist/identity/authorize.js.map +0 -1
- package/dist/identity/grant-service.d.ts.map +0 -1
- package/dist/identity/grant-service.js.map +0 -1
- package/dist/identity/hasher.d.ts.map +0 -1
- package/dist/identity/hasher.js.map +0 -1
- package/dist/identity/index.d.ts.map +0 -1
- package/dist/identity/index.js.map +0 -1
- package/dist/identity/password-policy.d.ts.map +0 -1
- package/dist/identity/password-policy.js.map +0 -1
- package/dist/identity/permission-migrations.d.ts.map +0 -1
- package/dist/identity/permission-migrations.js.map +0 -1
- package/dist/identity/permissions.d.ts.map +0 -1
- package/dist/identity/permissions.js.map +0 -1
- package/dist/identity/ports.d.ts.map +0 -1
- package/dist/identity/ports.js.map +0 -1
- package/dist/identity/repo.memory.d.ts.map +0 -1
- package/dist/identity/repo.memory.js.map +0 -1
- package/dist/identity/seed.d.ts.map +0 -1
- package/dist/identity/seed.js.map +0 -1
- package/dist/identity/tool-registrations.d.ts.map +0 -1
- package/dist/identity/tool-registrations.js.map +0 -1
- package/dist/identity/types.d.ts.map +0 -1
- package/dist/identity/types.js.map +0 -1
- package/dist/identity/username.d.ts.map +0 -1
- package/dist/identity/username.js.map +0 -1
- package/dist/media/agent-tools.d.ts.map +0 -1
- package/dist/media/agent-tools.js.map +0 -1
- package/dist/media/blob-gc-lock.d.ts.map +0 -1
- package/dist/media/blob-gc-lock.js.map +0 -1
- package/dist/media/blob-gc.d.ts.map +0 -1
- package/dist/media/blob-gc.js.map +0 -1
- package/dist/media/blob-key.d.ts.map +0 -1
- package/dist/media/blob-key.js.map +0 -1
- package/dist/media/blob-store.fs.d.ts.map +0 -1
- package/dist/media/blob-store.fs.js.map +0 -1
- package/dist/media/blob-store.memory.d.ts.map +0 -1
- package/dist/media/blob-store.memory.js.map +0 -1
- package/dist/media/content-type-sniffer.d.ts.map +0 -1
- package/dist/media/content-type-sniffer.js.map +0 -1
- package/dist/media/html-attributes.d.ts.map +0 -1
- package/dist/media/html-attributes.js.map +0 -1
- package/dist/media/image-transformer.d.ts.map +0 -1
- package/dist/media/image-transformer.js.map +0 -1
- package/dist/media/image-transformer.sharp.d.ts.map +0 -1
- package/dist/media/image-transformer.sharp.js.map +0 -1
- package/dist/media/index.d.ts.map +0 -1
- package/dist/media/index.js.map +0 -1
- package/dist/media/media-service.d.ts.map +0 -1
- package/dist/media/media-service.js.map +0 -1
- package/dist/media/ports.d.ts.map +0 -1
- package/dist/media/ports.js.map +0 -1
- package/dist/media/rendition-service.d.ts.map +0 -1
- package/dist/media/rendition-service.js.map +0 -1
- package/dist/media/repo.memory.d.ts.map +0 -1
- package/dist/media/repo.memory.js.map +0 -1
- package/dist/media/tool-registrations.d.ts.map +0 -1
- package/dist/media/tool-registrations.js.map +0 -1
- package/dist/media/transform-lock.d.ts.map +0 -1
- package/dist/media/transform-lock.js.map +0 -1
- package/dist/media/transform-registry.d.ts.map +0 -1
- package/dist/media/transform-registry.js.map +0 -1
- package/dist/media/transform-types.d.ts.map +0 -1
- package/dist/media/transform-types.js.map +0 -1
- package/dist/media/types.d.ts.map +0 -1
- package/dist/media/types.js.map +0 -1
- package/dist/navigation/agent-tools.d.ts.map +0 -1
- package/dist/navigation/agent-tools.js.map +0 -1
- package/dist/navigation/contracts.d.ts.map +0 -1
- package/dist/navigation/contracts.js.map +0 -1
- package/dist/navigation/index.d.ts.map +0 -1
- package/dist/navigation/index.js.map +0 -1
- package/dist/navigation/menu-service.d.ts.map +0 -1
- package/dist/navigation/menu-service.js.map +0 -1
- package/dist/navigation/ports.d.ts.map +0 -1
- package/dist/navigation/ports.js.map +0 -1
- package/dist/navigation/read-model.d.ts.map +0 -1
- package/dist/navigation/read-model.js.map +0 -1
- package/dist/navigation/reconcile.d.ts.map +0 -1
- package/dist/navigation/reconcile.js.map +0 -1
- package/dist/navigation/repo.memory.d.ts.map +0 -1
- package/dist/navigation/repo.memory.js.map +0 -1
- package/dist/navigation/resolver.d.ts.map +0 -1
- package/dist/navigation/resolver.js.map +0 -1
- package/dist/navigation/tool-registrations.d.ts.map +0 -1
- package/dist/navigation/tool-registrations.js.map +0 -1
- package/dist/navigation/types.d.ts.map +0 -1
- package/dist/navigation/types.js.map +0 -1
- package/dist/presentation/index.d.ts.map +0 -1
- package/dist/presentation/index.js.map +0 -1
- package/dist/presentation/presentation.d.ts.map +0 -1
- package/dist/presentation/presentation.js.map +0 -1
- package/dist/presentation/repo.memory.d.ts.map +0 -1
- package/dist/presentation/repo.memory.js.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/settings/agent-tools.d.ts.map +0 -1
- package/dist/settings/agent-tools.js.map +0 -1
- package/dist/settings/agent-writable-preferences.d.ts.map +0 -1
- package/dist/settings/agent-writable-preferences.js.map +0 -1
- package/dist/settings/change-feed.d.ts.map +0 -1
- package/dist/settings/change-feed.js.map +0 -1
- package/dist/settings/definitions-dispatch.d.ts.map +0 -1
- package/dist/settings/definitions-dispatch.js.map +0 -1
- package/dist/settings/dictionaries/index.d.ts.map +0 -1
- package/dist/settings/dictionaries/index.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ar.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js.map +0 -1
- package/dist/settings/ensure-definitions.d.ts.map +0 -1
- package/dist/settings/ensure-definitions.js.map +0 -1
- package/dist/settings/errors.d.ts.map +0 -1
- package/dist/settings/errors.js.map +0 -1
- package/dist/settings/index.d.ts.map +0 -1
- package/dist/settings/index.js.map +0 -1
- package/dist/settings/ports.d.ts.map +0 -1
- package/dist/settings/ports.js.map +0 -1
- package/dist/settings/purge-service.d.ts.map +0 -1
- package/dist/settings/purge-service.js.map +0 -1
- package/dist/settings/repo.memory.d.ts.map +0 -1
- package/dist/settings/repo.memory.js.map +0 -1
- package/dist/settings/settings.d.ts.map +0 -1
- package/dist/settings/settings.js.map +0 -1
- package/dist/settings/tool-registrations.d.ts.map +0 -1
- package/dist/settings/tool-registrations.js.map +0 -1
- package/dist/settings/types.d.ts.map +0 -1
- package/dist/settings/types.js.map +0 -1
- package/dist/settings/ui-tab-definitions.d.ts.map +0 -1
- package/dist/settings/ui-tab-definitions.js.map +0 -1
- package/dist/settings/write-service.d.ts.map +0 -1
- package/dist/settings/write-service.js.map +0 -1
- package/dist/taxonomy/agent-tools.d.ts.map +0 -1
- package/dist/taxonomy/agent-tools.js.map +0 -1
- package/dist/taxonomy/content-lookup.d.ts.map +0 -1
- package/dist/taxonomy/content-lookup.js.map +0 -1
- package/dist/taxonomy/index.d.ts.map +0 -1
- package/dist/taxonomy/index.js.map +0 -1
- package/dist/taxonomy/list.d.ts.map +0 -1
- package/dist/taxonomy/list.js.map +0 -1
- package/dist/taxonomy/merge-term.d.ts.map +0 -1
- package/dist/taxonomy/merge-term.js.map +0 -1
- package/dist/taxonomy/repo.memory.d.ts.map +0 -1
- package/dist/taxonomy/repo.memory.js.map +0 -1
- package/dist/taxonomy/validation-chain.d.ts.map +0 -1
- package/dist/taxonomy/validation-chain.js.map +0 -1
- package/dist/taxonomy/write-service.d.ts.map +0 -1
- package/dist/taxonomy/write-service.js.map +0 -1
- package/dist/workspace/agent-tools.d.ts.map +0 -1
- package/dist/workspace/agent-tools.js.map +0 -1
- package/dist/workspace/create.d.ts.map +0 -1
- package/dist/workspace/create.js.map +0 -1
- package/dist/workspace/delete.d.ts.map +0 -1
- package/dist/workspace/delete.js.map +0 -1
- package/dist/workspace/index.d.ts.map +0 -1
- package/dist/workspace/index.js.map +0 -1
- package/dist/workspace/repo.memory.d.ts.map +0 -1
- package/dist/workspace/repo.memory.js.map +0 -1
- package/dist/workspace/tool-registrations.d.ts.map +0 -1
- package/dist/workspace/tool-registrations.js.map +0 -1
- package/dist/workspace/update.d.ts.map +0 -1
- package/dist/workspace/update.js.map +0 -1
|
@@ -83,7 +83,7 @@ export declare function validateIdentifierGrammar(value: string): boolean;
|
|
|
83
83
|
*
|
|
84
84
|
* The PARAMETER type is deliberately the wider `ContentTypeFieldKind`. Narrowing it would be a
|
|
85
85
|
* breaking change for `@jini-ai/cms`'s public consumers (`content-types/index.ts` documents this
|
|
86
|
-
* function as what a host's own DDL provisioner calls, and
|
|
86
|
+
* function as what a host's own DDL provisioner calls, and the host re-exports it), and it would also
|
|
87
87
|
* make the runtime guard below unreachable-looking to a reader while remaining fully reachable to
|
|
88
88
|
* an untyped JavaScript caller. The runtime gate — not the signature — is what excludes
|
|
89
89
|
* storage-only kinds.
|
|
@@ -113,7 +113,7 @@ const KIND_TO_CAST_LITERAL = {
|
|
|
113
113
|
*
|
|
114
114
|
* The PARAMETER type is deliberately the wider `ContentTypeFieldKind`. Narrowing it would be a
|
|
115
115
|
* breaking change for `@jini-ai/cms`'s public consumers (`content-types/index.ts` documents this
|
|
116
|
-
* function as what a host's own DDL provisioner calls, and
|
|
116
|
+
* function as what a host's own DDL provisioner calls, and the host re-exports it), and it would also
|
|
117
117
|
* make the runtime guard below unreachable-looking to a reader while remaining fully reachable to
|
|
118
118
|
* an untyped JavaScript caller. The runtime gate — not the signature — is what excludes
|
|
119
119
|
* storage-only kinds.
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -130,7 +130,7 @@ export interface OutboxPort {
|
|
|
130
130
|
* attempt cap) — an implementation must persist exactly what it is told, not re-derive the
|
|
131
131
|
* decision from the row's own stored `attempts` (2026-09-06: this parameter replaced that
|
|
132
132
|
* adapter-side re-derivation, mirroring `WebhookDeliveryRepoPort.markFailed`'s `nextStatus`
|
|
133
|
-
* shape in the
|
|
133
|
+
* shape in the host). `"pending"` re-enters the retry queue at `nextAttemptAt`; `"failed"`
|
|
134
134
|
* is terminal — `claimPending` only ever selects `"pending"` rows, so a `"failed"` row is
|
|
135
135
|
* permanently excluded from retry regardless of `nextAttemptAt`.
|
|
136
136
|
*/
|
|
@@ -15,7 +15,7 @@ import { type PrincipalRecord, type SessionRecord } from "./types.js";
|
|
|
15
15
|
* verifying credentials, and a host that identifies a principal by some other means (e.g. a
|
|
16
16
|
* loopback-only single-use boot token, which has nothing to verify a password against) calls it
|
|
17
17
|
* directly instead of re-implementing session construction. Before this export existed, a
|
|
18
|
-
* downstream host
|
|
18
|
+
* downstream host had duplicated this file's private `hashToken` byte-for-byte because
|
|
19
19
|
* `login()` was the only minter and hard-requires a password — this closes that fork.
|
|
20
20
|
*
|
|
21
21
|
* Architectural role:
|
|
@@ -16,7 +16,7 @@ import { normalizeUsername } from "./username.js";
|
|
|
16
16
|
* verifying credentials, and a host that identifies a principal by some other means (e.g. a
|
|
17
17
|
* loopback-only single-use boot token, which has nothing to verify a password against) calls it
|
|
18
18
|
* directly instead of re-implementing session construction. Before this export existed, a
|
|
19
|
-
* downstream host
|
|
19
|
+
* downstream host had duplicated this file's private `hashToken` byte-for-byte because
|
|
20
20
|
* `login()` was the only minter and hard-requires a password — this closes that fork.
|
|
21
21
|
*
|
|
22
22
|
* Architectural role:
|
package/dist/identity/seed.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface SeedIdentityResult {
|
|
|
34
34
|
* - A seed that RAN TO COMPLETION is a no-op — the owner-username lookup below early-returns after
|
|
35
35
|
* a single confirming read (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
36
36
|
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
37
|
-
*
|
|
37
|
+
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
38
38
|
* `identityReady`.)
|
|
39
39
|
* - "Ran to completion" is judged on the owner user AND its role link, not the user alone. The link
|
|
40
40
|
* is the seed's last write, so the user is not on its own evidence the seed finished — see
|
package/dist/identity/seed.js
CHANGED
|
@@ -149,7 +149,7 @@ const BUILTIN_VIEWER_PERMISSIONS = ["content.read", "changeset.read", "plugin.re
|
|
|
149
149
|
/**
|
|
150
150
|
* Why every step below is "find, then save only what is missing" rather than a straight insert.
|
|
151
151
|
*
|
|
152
|
-
* This function's writes are NOT transactional and its caller is not awaited by its hosts (
|
|
152
|
+
* This function's writes are NOT transactional and its caller is not awaited by its hosts (one host
|
|
153
153
|
* hands `seedIdentity`'s promise back as `RouteDeps.identityReady` and returns synchronously), so
|
|
154
154
|
* a process that exits before the seed finishes — a fast-failing CLI command, a Ctrl-C, a crash —
|
|
155
155
|
* leaves the workspace half-seeded. `seedIdentity`'s own early-return guard keys on the OWNER USER,
|
|
@@ -249,7 +249,7 @@ async function seedBuiltinRoleWithPolicy(required) {
|
|
|
249
249
|
* still log in, and holds no role — and `resolveEffectivePermissions` (`authorize.ts`) starts from
|
|
250
250
|
* `principal_roles` + `principal_policies`, so with neither present EVERY permission evaluates to
|
|
251
251
|
* `no_grant`. Without this step the guard early-returns on the user it finds forever and the site
|
|
252
|
-
* stays bricked for its owner, silently. Neither host-side reconciler closes it either:
|
|
252
|
+
* stays bricked for its owner, silently. Neither host-side reconciler closes it either: the host's
|
|
253
253
|
* `migrateDeprecatedPermissionGrants` and `applyBuiltinRoleGrants` are handed policy and role repos
|
|
254
254
|
* only, never `principalRoles`, so neither can write a principal->role link at all.
|
|
255
255
|
*
|
|
@@ -296,7 +296,7 @@ async function ensureOwnerRoleBinding(required) {
|
|
|
296
296
|
* - A seed that RAN TO COMPLETION is a no-op — the owner-username lookup below early-returns after
|
|
297
297
|
* a single confirming read (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
298
298
|
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
299
|
-
*
|
|
299
|
+
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
300
300
|
* `identityReady`.)
|
|
301
301
|
* - "Ran to completion" is judged on the owner user AND its role link, not the user alone. The link
|
|
302
302
|
* is the seed's last write, so the user is not on its own evidence the seed finished — see
|
|
@@ -34,9 +34,19 @@
|
|
|
34
34
|
* content-types pass-through-`ToolPolicy` reasoning, not an oversight.
|
|
35
35
|
*
|
|
36
36
|
* Architectural role:
|
|
37
|
-
* `media` domain declaration. Imports only the
|
|
38
|
-
* (`
|
|
39
|
-
*
|
|
37
|
+
* `media` domain declaration. Imports only the constant its own domain already enforces
|
|
38
|
+
* (`DEFAULT_ALLOWED_MIME_TYPES` from `media-service.ts`), so the published JSON Schema's allowed
|
|
39
|
+
* content types cannot drift from the validator.
|
|
40
|
+
*
|
|
41
|
+
* `media_upload_asset`'s size-cap wording (2026-09-21): this catalog is a module-load-time constant
|
|
42
|
+
* shared by every host (see this file's own module doc above), but a host can now override
|
|
43
|
+
* `uploadMedia`'s cap per-call via `MediaToolDeps.maxUploadBytes` (`tool-registrations.ts`). Stating
|
|
44
|
+
* `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB) here would be an outright lie for a host that raised its own
|
|
45
|
+
* cap (e.g. Tovu's 50 MiB) — the model would be told a wrong, lower number than what the tool
|
|
46
|
+
* actually accepts. The description and schema below therefore describe the cap generically instead
|
|
47
|
+
* of stating a figure; the size-rejection error itself (`uploadMedia`'s `MediaValidationError`,
|
|
48
|
+
* `media-service.ts`) states the ACTUAL configured limit in MB, which is the one place that can be
|
|
49
|
+
* host-accurate.
|
|
40
50
|
*/
|
|
41
51
|
export type AgentToolSideEffect = "none" | "mutates-durable-state" | "mints-token";
|
|
42
52
|
export type AgentToolActorClassRule = "confirmer-must-equal-own-delegatedBy" | "user-only" | "none";
|
|
@@ -34,11 +34,22 @@
|
|
|
34
34
|
* content-types pass-through-`ToolPolicy` reasoning, not an oversight.
|
|
35
35
|
*
|
|
36
36
|
* Architectural role:
|
|
37
|
-
* `media` domain declaration. Imports only the
|
|
38
|
-
* (`
|
|
39
|
-
*
|
|
37
|
+
* `media` domain declaration. Imports only the constant its own domain already enforces
|
|
38
|
+
* (`DEFAULT_ALLOWED_MIME_TYPES` from `media-service.ts`), so the published JSON Schema's allowed
|
|
39
|
+
* content types cannot drift from the validator.
|
|
40
|
+
*
|
|
41
|
+
* `media_upload_asset`'s size-cap wording (2026-09-21): this catalog is a module-load-time constant
|
|
42
|
+
* shared by every host (see this file's own module doc above), but a host can now override
|
|
43
|
+
* `uploadMedia`'s cap per-call via `MediaToolDeps.maxUploadBytes` (`tool-registrations.ts`). Stating
|
|
44
|
+
* `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB) here would be an outright lie for a host that raised its own
|
|
45
|
+
* cap (e.g. Tovu's 50 MiB) — the model would be told a wrong, lower number than what the tool
|
|
46
|
+
* actually accepts. The description and schema below therefore describe the cap generically instead
|
|
47
|
+
* of stating a figure; the size-rejection error itself (`uploadMedia`'s `MediaValidationError`,
|
|
48
|
+
* `media-service.ts`) states the ACTUAL configured limit in MB, which is the one place that can be
|
|
49
|
+
* host-accurate.
|
|
40
50
|
*/
|
|
41
|
-
import {
|
|
51
|
+
import { MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES } from "./html-attributes.js";
|
|
52
|
+
import { DEFAULT_ALLOWED_MIME_TYPES } from "./media-service.js";
|
|
42
53
|
const MEDIA_ID_SCHEMA = {
|
|
43
54
|
type: "string",
|
|
44
55
|
description: "The media asset's id, as returned by media_upload_asset or media_list_assets.",
|
|
@@ -54,7 +65,7 @@ const MEDIA_ID_SCHEMA = {
|
|
|
54
65
|
export const mediaAgentToolCatalog = [
|
|
55
66
|
{
|
|
56
67
|
name: "media_list_assets",
|
|
57
|
-
description: "Lists every media asset in the workspace (all statuses — active and trashed), with id, title, alt, caption, credit, sha256, and status. Read-only. Call this to find a mediaId before updating or trashing an asset.",
|
|
68
|
+
description: "Lists every media asset in the workspace (all statuses — active and trashed), with id, slug, title, alt, caption, credit, sha256, and status. Read-only. Call this to find a mediaId before updating or trashing an asset.",
|
|
58
69
|
sideEffects: "none",
|
|
59
70
|
authorization: { permission: "media.read" },
|
|
60
71
|
inputSchema: {
|
|
@@ -68,8 +79,9 @@ export const mediaAgentToolCatalog = [
|
|
|
68
79
|
name: "media_upload_asset",
|
|
69
80
|
description: `Uploads a new media asset from base64-encoded bytes. Rejects a content type outside the allowed set ` +
|
|
70
81
|
`(${[...DEFAULT_ALLOWED_MIME_TYPES].join(", ")} — SVG is never accepted, even here: it requires a sanitizer ` +
|
|
71
|
-
`this build does not have) or a file over
|
|
72
|
-
`
|
|
82
|
+
`this build does not have) or a file over this host's configured size cap (a rejected upload's error ` +
|
|
83
|
+
`states the exact limit, in MB). Uploading the same bytes twice always creates two separate library ` +
|
|
84
|
+
`entries (the underlying blob is deduplicated, but each upload is its own asset).`,
|
|
73
85
|
sideEffects: "mutates-durable-state",
|
|
74
86
|
authorization: { permission: "media.upload" },
|
|
75
87
|
inputSchema: {
|
|
@@ -86,7 +98,7 @@ export const mediaAgentToolCatalog = [
|
|
|
86
98
|
dataBase64: {
|
|
87
99
|
type: "string",
|
|
88
100
|
minLength: 1,
|
|
89
|
-
description:
|
|
101
|
+
description: "The file's bytes, base64-encoded. Decoded size must be over 0 bytes and within this host's configured upload size cap (a too-large upload's error states the exact limit, in MB).",
|
|
90
102
|
},
|
|
91
103
|
alt: { type: "string", description: "Optional accessibility alt text." },
|
|
92
104
|
caption: { type: "string", description: "Optional display caption." },
|
|
@@ -96,8 +108,11 @@ export const mediaAgentToolCatalog = [
|
|
|
96
108
|
},
|
|
97
109
|
{
|
|
98
110
|
name: "media_update_metadata",
|
|
99
|
-
description: "Updates a media asset's
|
|
100
|
-
"
|
|
111
|
+
description: "Updates a media asset's metadata: title, alt, caption, credit, and its asset-wide presentation (cssClass, " +
|
|
112
|
+
"htmlAttributes). Omit a field to leave it unchanged. htmlAttributes/cssClass apply EVERYWHERE this asset " +
|
|
113
|
+
"renders; to change one placement on one page only, put the attribute on that page's data-embed-config " +
|
|
114
|
+
"marker element instead (pages_write_region). The bytes (sha256) are write-once and cannot be changed by " +
|
|
115
|
+
"this or any tool — to replace the file itself, upload a new asset.",
|
|
101
116
|
sideEffects: "mutates-durable-state",
|
|
102
117
|
authorization: { permission: "media.update" },
|
|
103
118
|
inputSchema: {
|
|
@@ -110,6 +125,26 @@ export const mediaAgentToolCatalog = [
|
|
|
110
125
|
alt: { type: "string", description: "New alt text. Omit to leave unchanged." },
|
|
111
126
|
caption: { type: "string", description: "New caption. Omit to leave unchanged." },
|
|
112
127
|
credit: { type: "string", description: "New credit line. Omit to leave unchanged." },
|
|
128
|
+
slug: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "New slug: lowercase letters, numbers and dashes, not UUID-shaped, unique in the workspace (a slug " +
|
|
131
|
+
"another asset uses is refused and nothing is written). Changing it breaks any page marker that " +
|
|
132
|
+
"references the OLD slug.",
|
|
133
|
+
},
|
|
134
|
+
cssClass: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Space-separated class names added to the rendered <img>/<video> tag everywhere this asset renders. " +
|
|
137
|
+
"Replaces the stored value; empty string clears it.",
|
|
138
|
+
},
|
|
139
|
+
htmlAttributes: {
|
|
140
|
+
type: "string",
|
|
141
|
+
description: "Extra attributes for the rendered <img>/<video> tag, written as HTML attribute text, e.g. " +
|
|
142
|
+
'`autoplay muted loop playsinline` or `loading="eager" data-motion="fade"`. REPLACES the whole ' +
|
|
143
|
+
"stored value: read the current value first and keep what should stay. Empty string clears it. " +
|
|
144
|
+
`Allowed names only: ${MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES.join(", ")}, plus any data-* or aria-*. ` +
|
|
145
|
+
"Anything else (on* handlers, javascript: values, style, class) is rejected and NOTHING is written. " +
|
|
146
|
+
"Browsers autoplay only video that is also muted (and playsinline on iPhone).",
|
|
147
|
+
},
|
|
113
148
|
},
|
|
114
149
|
},
|
|
115
150
|
},
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Server-side allowlist for `MediaRecord.htmlAttributes` (2026-09-07) — the enforcement half
|
|
3
|
-
* of a validator that ALSO has a browser-side copy in
|
|
3
|
+
* of a validator that ALSO has a browser-side copy in the consuming product's admin
|
|
4
4
|
* (`apps/admin/src/features/media/rules.ts`, `parseMediaHtmlAttributes` and friends).
|
|
5
5
|
*
|
|
6
6
|
* Deliberately duplicated, not shared via one imported module, for the same reason this codebase
|
|
7
|
-
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into
|
|
7
|
+
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into that admin's `FILE_HANDLER_ALLOWED_MIME_TYPES`/
|
|
8
8
|
* `IMPORTABLE_CONTENT_TYPES`: `@jini-ai/cms/media`'s barrel (`index.ts`) re-exports files that
|
|
9
9
|
* import real Node built-ins (`node:crypto` in `media-service.ts`, `node:fs` in
|
|
10
10
|
* `blob-store.fs.ts`, native `sharp` bindings in `image-transformer.sharp.ts`) — importing this
|
|
11
|
-
* subpath from a browser-bundled Vite app (
|
|
11
|
+
* subpath from a browser-bundled Vite app (that product's `apps/admin`) risks pulling those into the admin
|
|
12
12
|
* SPA's bundle even if only this one pure function is actually used, and this admin's own copy
|
|
13
13
|
* already exists, tested, with no such risk. This file is the copy every Node-side consumer
|
|
14
|
-
* (`media-service.ts`'s own `updateMediaMetadata`, and
|
|
14
|
+
* (`media-service.ts`'s own `updateMediaMetadata`, and that product's `apps/website` render path, both pure
|
|
15
15
|
* backend code) can safely import from `@jini-ai/cms/media` instead of re-deriving the rules a
|
|
16
16
|
* third time.
|
|
17
17
|
*
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* needed — that is the one piece of drift risk this duplication accepts, the same risk every other
|
|
22
22
|
* hand-copied list in this codebase already accepts.
|
|
23
23
|
*
|
|
24
|
-
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in
|
|
24
|
+
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in the consuming product's admin
|
|
25
25
|
* but rendered on the public site, so a free-text HTML-attribute passthrough is a stored-XSS vector
|
|
26
26
|
* (`onerror`, `onclick`, `style`, `href="javascript:"`, any `on*` handler) the moment it reaches a
|
|
27
27
|
* public page. A client-side-only check is not a control, since the API accepts whatever a caller
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* families (checked separately in {@link isAllowedMediaHtmlAttributeName}). Picked for the owner's
|
|
34
34
|
* stated near-term uses (animations, custom WebMCP hooks) plus the standard `<img>`/`<video>`
|
|
35
35
|
* attributes those uses actually need; extend this list, not the parser, when a new one is needed —
|
|
36
|
-
* and keep it in sync with the identical constant in
|
|
36
|
+
* and keep it in sync with the identical constant in that admin's copy (see this file's header).
|
|
37
37
|
*/
|
|
38
38
|
export declare const MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES: readonly ["loading", "decoding", "playsinline", "muted", "loop", "autoplay", "poster"];
|
|
39
39
|
/** Whether `name` is on the allowlist — a well-formed attribute name
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Server-side allowlist for `MediaRecord.htmlAttributes` (2026-09-07) — the enforcement half
|
|
3
|
-
* of a validator that ALSO has a browser-side copy in
|
|
3
|
+
* of a validator that ALSO has a browser-side copy in the consuming product's admin
|
|
4
4
|
* (`apps/admin/src/features/media/rules.ts`, `parseMediaHtmlAttributes` and friends).
|
|
5
5
|
*
|
|
6
6
|
* Deliberately duplicated, not shared via one imported module, for the same reason this codebase
|
|
7
|
-
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into
|
|
7
|
+
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into that admin's `FILE_HANDLER_ALLOWED_MIME_TYPES`/
|
|
8
8
|
* `IMPORTABLE_CONTENT_TYPES`: `@jini-ai/cms/media`'s barrel (`index.ts`) re-exports files that
|
|
9
9
|
* import real Node built-ins (`node:crypto` in `media-service.ts`, `node:fs` in
|
|
10
10
|
* `blob-store.fs.ts`, native `sharp` bindings in `image-transformer.sharp.ts`) — importing this
|
|
11
|
-
* subpath from a browser-bundled Vite app (
|
|
11
|
+
* subpath from a browser-bundled Vite app (that product's `apps/admin`) risks pulling those into the admin
|
|
12
12
|
* SPA's bundle even if only this one pure function is actually used, and this admin's own copy
|
|
13
13
|
* already exists, tested, with no such risk. This file is the copy every Node-side consumer
|
|
14
|
-
* (`media-service.ts`'s own `updateMediaMetadata`, and
|
|
14
|
+
* (`media-service.ts`'s own `updateMediaMetadata`, and that product's `apps/website` render path, both pure
|
|
15
15
|
* backend code) can safely import from `@jini-ai/cms/media` instead of re-deriving the rules a
|
|
16
16
|
* third time.
|
|
17
17
|
*
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* needed — that is the one piece of drift risk this duplication accepts, the same risk every other
|
|
22
22
|
* hand-copied list in this codebase already accepts.
|
|
23
23
|
*
|
|
24
|
-
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in
|
|
24
|
+
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in the consuming product's admin
|
|
25
25
|
* but rendered on the public site, so a free-text HTML-attribute passthrough is a stored-XSS vector
|
|
26
26
|
* (`onerror`, `onclick`, `style`, `href="javascript:"`, any `on*` handler) the moment it reaches a
|
|
27
27
|
* public page. A client-side-only check is not a control, since the API accepts whatever a caller
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* families (checked separately in {@link isAllowedMediaHtmlAttributeName}). Picked for the owner's
|
|
34
34
|
* stated near-term uses (animations, custom WebMCP hooks) plus the standard `<img>`/`<video>`
|
|
35
35
|
* attributes those uses actually need; extend this list, not the parser, when a new one is needed —
|
|
36
|
-
* and keep it in sync with the identical constant in
|
|
36
|
+
* and keep it in sync with the identical constant in that admin's copy (see this file's header).
|
|
37
37
|
*/
|
|
38
38
|
export const MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES = [
|
|
39
39
|
"loading",
|
package/dist/media/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { withSha256Lock } from "./blob-gc-lock.js";
|
|
|
21
21
|
export { DEFAULT_GC_GRACE_MS, resolveGcGraceMs, isBlobUnreferenced, tombstoneBlobIfUnreferenced, runBlobGcDeletePass, runBlobGcUnlinkPass, runBlobGcCycle, runMonthlyOrphanSweepStub, } from "./blob-gc.js";
|
|
22
22
|
export { InMemoryBlobStore } from "./blob-store.memory.js";
|
|
23
23
|
export { LocalFsBlobStore, type LocalFsBlobStoreDeps } from "./blob-store.fs.js";
|
|
24
|
-
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, type UploadMediaInput, type UploadMediaDeps, type UpdateMediaMetadataInput, type FindMediaByIdOrSlugRequired, } from "./media-service.js";
|
|
24
|
+
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, isValidMediaSlugFormat, type UploadMediaInput, type UploadMediaDeps, type UpdateMediaMetadataInput, type FindMediaByIdOrSlugRequired, } from "./media-service.js";
|
|
25
25
|
export type { TransformFit, TransformFormat, TransformParams, TransformDefinitionRecord, } from "./transform-types.js";
|
|
26
26
|
export { TransformValidationError, mimeForTransformFormat, MAX_TRANSFORM_DIMENSION_PX } from "./transform-types.js";
|
|
27
27
|
export { withRenditionLock, withTransformRegistryLock } from "./transform-lock.js";
|
package/dist/media/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { withSha256Lock } from "./blob-gc-lock.js";
|
|
|
6
6
|
export { DEFAULT_GC_GRACE_MS, resolveGcGraceMs, isBlobUnreferenced, tombstoneBlobIfUnreferenced, runBlobGcDeletePass, runBlobGcUnlinkPass, runBlobGcCycle, runMonthlyOrphanSweepStub, } from "./blob-gc.js";
|
|
7
7
|
export { InMemoryBlobStore } from "./blob-store.memory.js";
|
|
8
8
|
export { LocalFsBlobStore } from "./blob-store.fs.js";
|
|
9
|
-
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, } from "./media-service.js";
|
|
9
|
+
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, isValidMediaSlugFormat, } from "./media-service.js";
|
|
10
10
|
export { TransformValidationError, mimeForTransformFormat, MAX_TRANSFORM_DIMENSION_PX } from "./transform-types.js";
|
|
11
11
|
export { withRenditionLock, withTransformRegistryLock } from "./transform-lock.js";
|
|
12
12
|
export { registerTransform, getLatestTransformDefinition, isLatestTransformVersion, isReferencedByPublishedContent, } from "./transform-registry.js";
|
|
@@ -23,7 +23,7 @@ export declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
|
23
23
|
* image types above, an accepted video is never re-encoded — there is no
|
|
24
24
|
* `TransformFormat` for video (`transform-types.ts`'s union is image-only), so
|
|
25
25
|
* a video asset's public URL bypasses the transform/rendition pipeline
|
|
26
|
-
* entirely and serves the original bytes as-is (host concern; see
|
|
26
|
+
* entirely and serves the original bytes as-is (host concern; see the host's
|
|
27
27
|
* `routes/site/media-rendition.ts`). This widens the SAME advisory,
|
|
28
28
|
* client-declared-string check item 4 above already discloses as untrusted —
|
|
29
29
|
* no new ingress hardening was added for video.
|
|
@@ -49,6 +49,7 @@ export declare function resolveWriteOnceSource(required: {
|
|
|
49
49
|
existing: MediaSource | undefined;
|
|
50
50
|
requestedSha256: string;
|
|
51
51
|
}, _optional?: Record<string, never>): MediaSource;
|
|
52
|
+
export declare function isValidMediaSlugFormat(slug: string): boolean;
|
|
52
53
|
export interface UploadMediaInput {
|
|
53
54
|
workspaceId: UUID;
|
|
54
55
|
bytes: Uint8Array;
|
|
@@ -58,6 +58,22 @@ import { describeMediaHtmlAttributeError, parseMediaHtmlAttributes } from "./htm
|
|
|
58
58
|
export { MediaConflictError, MediaNotFoundError, MediaSourceImmutableError, MediaStillReferencedError, MediaValidationError, };
|
|
59
59
|
/** Default byte-size cap (this pass's stand-in for the real ingress policy's streaming cap). */
|
|
60
60
|
export const DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
61
|
+
/**
|
|
62
|
+
* Renders a byte count as a human-readable MB figure for the size-cap rejection below (2026-09-21).
|
|
63
|
+
* Every real caller passes a MiB-scale `maxUploadBytes` (10 MiB default, or a host override such as
|
|
64
|
+
* Tovu's 50 MiB), so one decimal place is enough precision without ever showing a raw byte count —
|
|
65
|
+
* the whole point of this fix (see `uploadMedia`'s size-cap check, and `agent-tools.ts`'s file header
|
|
66
|
+
* for why the catalog description can no longer state a specific number either). `toFixed(1)`'s
|
|
67
|
+
* trailing `.0` is trimmed so a clean multiple of 1 MB (the common case) reads as `10 MB`, not `10.0
|
|
68
|
+
* MB`.
|
|
69
|
+
*
|
|
70
|
+
* @complexity O(1).
|
|
71
|
+
*/
|
|
72
|
+
function formatMaxUploadBytesAsMb(bytes) {
|
|
73
|
+
const mb = bytes / (1024 * 1024);
|
|
74
|
+
const rounded = mb.toFixed(1);
|
|
75
|
+
return `${rounded.endsWith(".0") ? rounded.slice(0, -2) : rounded} MB`;
|
|
76
|
+
}
|
|
61
77
|
/**
|
|
62
78
|
* Advisory MIME allowlist. SVG is deliberately EXCLUDED (not "TODO, forgot") —
|
|
63
79
|
* SVG must be sanitized at ingest before it's safe to store;
|
|
@@ -77,7 +93,7 @@ export const DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
|
77
93
|
* image types above, an accepted video is never re-encoded — there is no
|
|
78
94
|
* `TransformFormat` for video (`transform-types.ts`'s union is image-only), so
|
|
79
95
|
* a video asset's public URL bypasses the transform/rendition pipeline
|
|
80
|
-
* entirely and serves the original bytes as-is (host concern; see
|
|
96
|
+
* entirely and serves the original bytes as-is (host concern; see the host's
|
|
81
97
|
* `routes/site/media-rendition.ts`). This widens the SAME advisory,
|
|
82
98
|
* client-declared-string check item 4 above already discloses as untrusted —
|
|
83
99
|
* no new ingress hardening was added for video.
|
|
@@ -150,7 +166,7 @@ const MEDIA_SLUG_UUID_SHAPE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9
|
|
|
150
166
|
* human for a shorter value — silently truncating an internally-derived value is the same
|
|
151
167
|
* "coerce, don't fail" convention `deriveTitleFromFilename`/`slugifyMediaTitle` already use. */
|
|
152
168
|
const MEDIA_MAX_SLUG_LENGTH = 120;
|
|
153
|
-
function isValidMediaSlugFormat(slug) {
|
|
169
|
+
export function isValidMediaSlugFormat(slug) {
|
|
154
170
|
return MEDIA_SLUG_FORMAT_PATTERN.test(slug) && !MEDIA_SLUG_UUID_SHAPE_PATTERN.test(slug);
|
|
155
171
|
}
|
|
156
172
|
/** Truncates a slug candidate to `maxLength`, stripping a trailing dash the cut can introduce (so a
|
|
@@ -165,9 +181,9 @@ function capSlugCandidate(candidate, maxLength) {
|
|
|
165
181
|
}
|
|
166
182
|
/** Turns free text into a slug candidate: lowercase, non-alphanumeric runs collapsed to one dash,
|
|
167
183
|
* leading/trailing dashes trimmed. Same algorithm as `post.ts`'s private `slugify` (duplicated, not
|
|
168
|
-
* imported — cross-repo: `post.ts` lives in
|
|
184
|
+
* imported — cross-repo: `post.ts` lives in the host, this file in `@jini-ai/cms`; this codebase's own
|
|
169
185
|
* precedent for a small hand-copied helper mirrored across a repo boundary is `DEFAULT_ALLOWED_MIME_TYPES`
|
|
170
|
-
* vs.
|
|
186
|
+
* vs. the host's `FILE_HANDLER_ALLOWED_MIME_TYPES`/`IMPORTABLE_CONTENT_TYPES`). Never throws and never
|
|
171
187
|
* returns `undefined` — an all-punctuation input collapses to `""`, which every caller here treats
|
|
172
188
|
* as "derive nothing, fall back to a fixed default" rather than a malformed-input error. */
|
|
173
189
|
function slugifyMediaTitle(title) {
|
|
@@ -181,12 +197,12 @@ function slugifyMediaTitle(title) {
|
|
|
181
197
|
* Derives a unique-per-workspace slug from `title`, suffixing `-2`, `-3`, … on collision — the
|
|
182
198
|
* identical loop shape `post.ts`'s `createPost` uses for its own derived-slug path. Shared by
|
|
183
199
|
* {@link uploadMedia} (derive-on-create) and the backfill a host runs once for pre-existing rows
|
|
184
|
-
* with no slug yet (see
|
|
200
|
+
* with no slug yet (see the host's `development/scripts/backfill-media-slugs.ts`).
|
|
185
201
|
*
|
|
186
202
|
* `base` falls back to `"untitled"` when `title` slugifies to the empty string (all-punctuation or
|
|
187
203
|
* non-Latin titles that `slugifyMediaTitle` strips to nothing) — `uploadMedia`'s own title is never
|
|
188
204
|
* empty (`deriveTitleFromFilename` guarantees a non-empty string), but the backfill script
|
|
189
|
-
* (`development/scripts/backfill-media-slugs.ts`,
|
|
205
|
+
* (`development/scripts/backfill-media-slugs.ts`, host-side) calls this against arbitrary pre-existing
|
|
190
206
|
* titles, so this fallback is load-bearing there, not just a defensive floor. Note this is the SAME
|
|
191
207
|
* fallback base every empty-slugifying title collapses onto, so two such titles in one workspace
|
|
192
208
|
* collide on `"untitled"` and are disambiguated by the ordinary suffix loop below exactly like any
|
|
@@ -248,7 +264,7 @@ export async function uploadMedia(required, optional = {}) {
|
|
|
248
264
|
throw new MediaValidationError("uploaded file is empty");
|
|
249
265
|
}
|
|
250
266
|
if (input.bytes.byteLength > maxUploadBytes) {
|
|
251
|
-
throw new MediaValidationError(`uploaded file exceeds the ${maxUploadBytes}
|
|
267
|
+
throw new MediaValidationError(`uploaded file exceeds the ${formatMaxUploadBytesAsMb(maxUploadBytes)} size cap`);
|
|
252
268
|
}
|
|
253
269
|
const sha256 = createHash("sha256").update(input.bytes).digest("hex");
|
|
254
270
|
const nowIso = deps.clock.nowIso();
|
|
@@ -92,6 +92,18 @@ export interface MediaToolDeps {
|
|
|
92
92
|
media: MediaRecord;
|
|
93
93
|
bytes: Uint8Array;
|
|
94
94
|
}) => Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Optional host-supplied override of `uploadMedia`'s own `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB,
|
|
97
|
+
* `media-service.ts`), forwarded verbatim as `uploadMedia`'s third (`optional`) argument in the
|
|
98
|
+
* `media_upload_asset` handler below (2026-09-21). Before this field existed, the handler called
|
|
99
|
+
* `uploadMedia({ deps, input })` with no third argument at all, so EVERY host's assistant upload
|
|
100
|
+
* tool was hard-capped at 10 MiB regardless of what limit that host's own HTTP upload route
|
|
101
|
+
* enforced — a host raising its own cap (e.g. to 50 MiB) had no way to raise this tool's cap to
|
|
102
|
+
* match. Omitted: `uploadMedia`'s own default applies unchanged, matching this field's own
|
|
103
|
+
* pre-2026-09-21 absence for every host that has not opted in yet — the same degrade-soft
|
|
104
|
+
* contract {@link resolvePublicUrls} and {@link recordUploadContentType} already established.
|
|
105
|
+
*/
|
|
106
|
+
maxUploadBytes?: number;
|
|
95
107
|
}
|
|
96
108
|
/**
|
|
97
109
|
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
@@ -30,6 +30,7 @@ function isMediaShapeRejection(error) {
|
|
|
30
30
|
function toMediaToolView(record) {
|
|
31
31
|
return {
|
|
32
32
|
id: record.id,
|
|
33
|
+
slug: record.slug,
|
|
33
34
|
title: record.title,
|
|
34
35
|
alt: record.alt,
|
|
35
36
|
caption: record.caption,
|
|
@@ -37,6 +38,8 @@ function toMediaToolView(record) {
|
|
|
37
38
|
sha256: record.source.sha256,
|
|
38
39
|
status: record.status,
|
|
39
40
|
version: record.version,
|
|
41
|
+
cssClass: record.cssClass,
|
|
42
|
+
htmlAttributes: record.htmlAttributes,
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
@@ -92,7 +95,7 @@ export function buildMediaRegistrations(routeDeps) {
|
|
|
92
95
|
credit: typeof input.credit === "string" ? input.credit : undefined,
|
|
93
96
|
createdByPrincipal: ctx.principal.id,
|
|
94
97
|
},
|
|
95
|
-
});
|
|
98
|
+
}, { maxUploadBytes: routeDeps.maxUploadBytes });
|
|
96
99
|
if (routeDeps.recordUploadContentType) {
|
|
97
100
|
try {
|
|
98
101
|
await routeDeps.recordUploadContentType({ media, bytes });
|
|
@@ -121,18 +124,23 @@ export function buildMediaRegistrations(routeDeps) {
|
|
|
121
124
|
const input = requireInputRecord(ctx.input);
|
|
122
125
|
const mediaId = requireString(input, "mediaId");
|
|
123
126
|
await requireToolPermission(routeDeps, { principalId: ctx.principal.id, permission: "media.update", entityType: "media", entityId: mediaId });
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
return withSchemaOnRejection({ toolId: "media_update_metadata", catalog: CATALOG_BY_ID, isShapeRejection: isMediaShapeRejection }, async () => {
|
|
128
|
+
const { media } = await updateMediaMetadata({
|
|
129
|
+
deps: { clock: routeDeps.clock, mediaRepo: routeDeps.mediaRepo },
|
|
130
|
+
input: {
|
|
131
|
+
workspaceId: routeDeps.workspaceId,
|
|
132
|
+
id: mediaId,
|
|
133
|
+
title: typeof input.title === "string" ? input.title : undefined,
|
|
134
|
+
alt: typeof input.alt === "string" ? input.alt : undefined,
|
|
135
|
+
caption: typeof input.caption === "string" ? input.caption : undefined,
|
|
136
|
+
credit: typeof input.credit === "string" ? input.credit : undefined,
|
|
137
|
+
slug: typeof input.slug === "string" ? input.slug : undefined,
|
|
138
|
+
cssClass: typeof input.cssClass === "string" ? input.cssClass : undefined,
|
|
139
|
+
htmlAttributes: typeof input.htmlAttributes === "string" ? input.htmlAttributes : undefined,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return { media: toMediaToolView(media) };
|
|
134
143
|
});
|
|
135
|
-
return { media: toMediaToolView(media) };
|
|
136
144
|
},
|
|
137
145
|
media_trash_asset: async (ctx) => {
|
|
138
146
|
const mediaId = requireString(requireInputRecord(ctx.input), "mediaId");
|
package/dist/media/types.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface MediaRecord {
|
|
|
50
50
|
*
|
|
51
51
|
* Derived from `title` at upload time (`deriveMediaSlug`), then independently editable afterward
|
|
52
52
|
* — renaming `title` does NOT recompute this field; it changes only on an explicit `slug` write.
|
|
53
|
-
* Unique per `(workspaceId, slug)`, enforced by the host's own DB index (see
|
|
53
|
+
* Unique per `(workspaceId, slug)`, enforced by the host's own DB index (see the host's
|
|
54
54
|
* `idx_media_workspace_slug`) — `updateMediaMetadata`'s own `findBySlug` check is a friendly-error
|
|
55
55
|
* courtesy on top of that, not the enforcement itself, the same split `posts_workspace_slug_unique`
|
|
56
56
|
* already establishes for `post`'s identical `slug` field.
|
|
@@ -20,10 +20,10 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, type MenuRepoPort, }
|
|
|
20
20
|
* `isAllowedHref`/`ALLOWED_HREF_SHAPES_DESCRIPTION` (2026-09-03): the canonical author-link
|
|
21
21
|
* allowlist, promoted here specifically so a HOST can import it rather than hand-copy it — the
|
|
22
22
|
* dependency direction a host -> `@jini-ai/cms` is the one that is actually legal (the reverse is
|
|
23
|
-
* not).
|
|
23
|
+
* not). The host's own render-time `safeHref` (`server/inbound/public-http/http/site/render.ts` and its
|
|
24
24
|
* `features/theme/static-render.ts` duplicate) have not yet been migrated to import this — see
|
|
25
25
|
* `menu-service.ts`'s own doc comment on `isAllowedHref` for the retirement plan and why nothing in
|
|
26
|
-
* `check:boundaries` blocks either
|
|
26
|
+
* `check:boundaries` blocks either host-side call site from doing so.
|
|
27
27
|
*/
|
|
28
28
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, type TreeValidationLimits, type CreateMenuDeps, type CreateMenuServiceInput, type CreateMenuRequired, type CreateMenuOptional, type UpdateMenuTreeDeps, type UpdateMenuTreeServiceInput, type UpdateMenuTreeRequired, type UpdateMenuTreeOptional, type AssignLocationDeps, type AssignLocationServiceInput, type AssignLocationRequired, type AssignLocationOptional, type DeleteMenuDeps, type DeleteMenuServiceInput, type DeleteMenuRequired, type DeleteMenuOptional, } from "./menu-service.js";
|
|
29
29
|
export { resolveForLocation, resolveMenuDoc, type ResolveTargetHrefFn, type ResolvedTargetHref, type ResolveForLocationDeps, type ResolveForLocationServiceInput, type ResolveForLocationRequired, type ResolveForLocationOptional, } from "./resolver.js";
|
package/dist/navigation/index.js
CHANGED
|
@@ -11,10 +11,10 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, } from "./repo.memory
|
|
|
11
11
|
* `isAllowedHref`/`ALLOWED_HREF_SHAPES_DESCRIPTION` (2026-09-03): the canonical author-link
|
|
12
12
|
* allowlist, promoted here specifically so a HOST can import it rather than hand-copy it — the
|
|
13
13
|
* dependency direction a host -> `@jini-ai/cms` is the one that is actually legal (the reverse is
|
|
14
|
-
* not).
|
|
14
|
+
* not). The host's own render-time `safeHref` (`server/inbound/public-http/http/site/render.ts` and its
|
|
15
15
|
* `features/theme/static-render.ts` duplicate) have not yet been migrated to import this — see
|
|
16
16
|
* `menu-service.ts`'s own doc comment on `isAllowedHref` for the retirement plan and why nothing in
|
|
17
|
-
* `check:boundaries` blocks either
|
|
17
|
+
* `check:boundaries` blocks either host-side call site from doing so.
|
|
18
18
|
*/
|
|
19
19
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, } from "./menu-service.js";
|
|
20
20
|
export { resolveForLocation, resolveMenuDoc, } from "./resolver.js";
|
|
@@ -48,13 +48,13 @@ export declare const DEFAULT_MAX_TREE_DEPTH = 5;
|
|
|
48
48
|
export declare const DEFAULT_MAX_ITEM_COUNT = 500;
|
|
49
49
|
/**
|
|
50
50
|
* The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
|
|
51
|
-
* `/…`, `http(s)://`, `mailto:`) as
|
|
51
|
+
* `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
|
|
52
52
|
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth**: exported
|
|
53
53
|
* from this package's public surface (`navigation/index.ts` -> `@jini-ai/cms/navigation`) so the
|
|
54
|
-
* dependency runs the direction that is actually legal —
|
|
54
|
+
* dependency runs the direction that is actually legal — the host already depends on `@jini-ai/cms`, not
|
|
55
55
|
* the reverse — rather than each consumer hand-copying the predicate.
|
|
56
56
|
*
|
|
57
|
-
* The two
|
|
57
|
+
* The two host-side copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
|
|
58
58
|
* `value => passes ? value : "#"` wrapper around the identical predicate below) have **not yet been
|
|
59
59
|
* retired to import this** — that edit is intentionally out of this change's scope (a
|
|
60
60
|
* `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
|
|
@@ -79,7 +79,7 @@ export declare const DEFAULT_MAX_ITEM_COUNT = 500;
|
|
|
79
79
|
* workspace's stored menu data (`content.db`, `content.seed.db`) before this change: every existing
|
|
80
80
|
* `url`-kind href already starts with `#` or `/`, so this tightening rejects nothing that exists
|
|
81
81
|
* today — it governs `createMenu` too, so it also reaches the Jini MCP agent-tool path, not just
|
|
82
|
-
* hrefs written through
|
|
82
|
+
* hrefs written through the host's admin editor.
|
|
83
83
|
*
|
|
84
84
|
* @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
|
|
85
85
|
* otherwise a valid input — this function performs no other precondition checks.
|
|
@@ -67,13 +67,13 @@ const SAFE_HREF_RESOLUTION_BASE = "http://tovu-safehref.invalid/";
|
|
|
67
67
|
const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
68
68
|
/**
|
|
69
69
|
* The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
|
|
70
|
-
* `/…`, `http(s)://`, `mailto:`) as
|
|
70
|
+
* `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
|
|
71
71
|
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth**: exported
|
|
72
72
|
* from this package's public surface (`navigation/index.ts` -> `@jini-ai/cms/navigation`) so the
|
|
73
|
-
* dependency runs the direction that is actually legal —
|
|
73
|
+
* dependency runs the direction that is actually legal — the host already depends on `@jini-ai/cms`, not
|
|
74
74
|
* the reverse — rather than each consumer hand-copying the predicate.
|
|
75
75
|
*
|
|
76
|
-
* The two
|
|
76
|
+
* The two host-side copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
|
|
77
77
|
* `value => passes ? value : "#"` wrapper around the identical predicate below) have **not yet been
|
|
78
78
|
* retired to import this** — that edit is intentionally out of this change's scope (a
|
|
79
79
|
* `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
|
|
@@ -98,7 +98,7 @@ const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
|
98
98
|
* workspace's stored menu data (`content.db`, `content.seed.db`) before this change: every existing
|
|
99
99
|
* `url`-kind href already starts with `#` or `/`, so this tightening rejects nothing that exists
|
|
100
100
|
* today — it governs `createMenu` too, so it also reaches the Jini MCP agent-tool path, not just
|
|
101
|
-
* hrefs written through
|
|
101
|
+
* hrefs written through the host's admin editor.
|
|
102
102
|
*
|
|
103
103
|
* @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
|
|
104
104
|
* otherwise a valid input — this function performs no other precondition checks.
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Relocated here from Jini's `@jini-ai/ui` (2026-08-08): these keys aren't UI-rendering logic,
|
|
6
6
|
* they're settings-domain presentation copy, and this package's `settings` module is the actual
|
|
7
|
-
* domain owner shared across every host
|
|
7
|
+
* domain owner shared across every host. `@jini-ai/ui` keeps the *component*
|
|
8
8
|
* that renders a settings dialog; a host supplies the copy, and this is the shared copy any
|
|
9
9
|
* OD-parity host can reuse instead of re-authoring its own. See project memory "Settings-dialog
|
|
10
|
-
* i18n relocation" for the full decision record and the sibling move of the 8
|
|
11
|
-
* capability-fact keys (those stayed host-side — see
|
|
10
|
+
* i18n relocation" for the full decision record and the sibling move of the 8 host-specific
|
|
11
|
+
* capability-fact keys (those stayed host-side — see the host's own `settings-capabilities-i18n.ts`).
|
|
12
12
|
*
|
|
13
13
|
* No `en` dictionary: this package's convention (matching `@jini-ai/ui`) is "the English string
|
|
14
14
|
* IS the key" — `translateSettingsDialog` falls back to the raw key for English and any
|