@jini-ai/cms 0.4.0 → 0.4.2

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2 — 2026-10-04
4
+
5
+ - Media: optional `BlobStorePort.sizeOf` reports original blob bytes (filesystem store memoizes `stat`); `MediaRecord.createdBy` is stamped at creation and write-once.
6
+ - Navigation: entry targets carry optional `lastKnownHref` and `entryType` hints.
7
+ - Taxonomy: `unassignTerms` returns the removed ids. Trash: a no-op trash/restore does not notify again.
8
+ - Internal `@jini-ai/*` dependencies are caret ranges (`workspace:^`) instead of exact pins, so a host on a newer patch resolves a single copy without an override.
9
+
3
10
  ## Unreleased — Trash host adoption
4
11
 
5
12
  - Preserve `adapter-unavailable` for absent domains before checking entity policy; selected purge
@@ -5,6 +5,7 @@ export interface LocalFsBlobStoreDeps {
5
5
  }
6
6
  export declare class LocalFsBlobStore implements BlobStorePort {
7
7
  private readonly rootDir;
8
+ private readonly sizesByKey;
8
9
  constructor(deps: LocalFsBlobStoreDeps);
9
10
  private resolvePath;
10
11
  put(input: PutBlobInput): Promise<{
@@ -28,6 +29,9 @@ export declare class LocalFsBlobStore implements BlobStorePort {
28
29
  get(input: {
29
30
  storageKey: string;
30
31
  }): Promise<Uint8Array>;
32
+ sizeOf({ storageKey }: {
33
+ storageKey: string;
34
+ }, _optional?: Record<string, never>): Promise<number | null>;
31
35
  /**
32
36
  * `ENOENT` (the path genuinely does not exist) resolves to `false`. Any other `stat()` failure
33
37
  * (`EACCES` permission denied, `ENOTDIR`, `EIO`, ...) is rethrown raw — same "catch the one
@@ -16,6 +16,9 @@ import { dirname, join } from "node:path";
16
16
  import { computeBlobStorageKey } from "./blob-key.js";
17
17
  export class LocalFsBlobStore {
18
18
  rootDir;
19
+ // Storage keys include workspace + content hash: immutable bytes make successful sizes
20
+ // reusable for this process. Promises coalesce concurrent reads of the same hash as well.
21
+ sizesByKey = new Map();
19
22
  constructor(deps) {
20
23
  this.rootDir = deps.rootDir;
21
24
  }
@@ -27,6 +30,7 @@ export class LocalFsBlobStore {
27
30
  const path = this.resolvePath(storageKey);
28
31
  await mkdir(dirname(path), { recursive: true });
29
32
  await writeFile(path, input.bytes);
33
+ this.sizesByKey.delete(storageKey);
30
34
  return { storageKey };
31
35
  }
32
36
  /**
@@ -46,6 +50,7 @@ export class LocalFsBlobStore {
46
50
  await mkdir(dirname(path), { recursive: true });
47
51
  try {
48
52
  await writeFile(path, input.bytes, { flag: "wx" });
53
+ this.sizesByKey.delete(storageKey);
49
54
  return { storageKey, written: true };
50
55
  }
51
56
  catch (err) {
@@ -58,6 +63,27 @@ export class LocalFsBlobStore {
58
63
  async get(input) {
59
64
  return readFile(this.resolvePath(input.storageKey));
60
65
  }
66
+ sizeOf({ storageKey }, _optional = {}) {
67
+ const cached = this.sizesByKey.get(storageKey);
68
+ if (cached)
69
+ return cached;
70
+ const pending = stat(this.resolvePath(storageKey)).then((info) => info.size).catch((err) => {
71
+ if (err.code === "ENOENT")
72
+ return null;
73
+ throw err;
74
+ });
75
+ this.sizesByKey.set(storageKey, pending);
76
+ // Missing/restored blobs and transient failures must be retried. Identity guards prevent
77
+ // an older stat from clearing a newer entry after a write/remove invalidated the cache.
78
+ void pending.then((size) => {
79
+ if (size === null && this.sizesByKey.get(storageKey) === pending)
80
+ this.sizesByKey.delete(storageKey);
81
+ }, () => {
82
+ if (this.sizesByKey.get(storageKey) === pending)
83
+ this.sizesByKey.delete(storageKey);
84
+ });
85
+ return pending;
86
+ }
61
87
  /**
62
88
  * `ENOENT` (the path genuinely does not exist) resolves to `false`. Any other `stat()` failure
63
89
  * (`EACCES` permission denied, `ENOTDIR`, `EIO`, ...) is rethrown raw — same "catch the one
@@ -79,6 +105,7 @@ export class LocalFsBlobStore {
79
105
  }
80
106
  async remove(input) {
81
107
  await rm(this.resolvePath(input.storageKey), { force: true });
108
+ this.sizesByKey.delete(input.storageKey);
82
109
  }
83
110
  }
84
111
  //# sourceMappingURL=blob-store.fs.js.map
@@ -24,6 +24,9 @@ export declare class InMemoryBlobStore implements BlobStorePort {
24
24
  exists(input: {
25
25
  storageKey: string;
26
26
  }): Promise<boolean>;
27
+ sizeOf({ storageKey }: {
28
+ storageKey: string;
29
+ }, _optional?: Record<string, never>): Promise<number | null>;
27
30
  remove(input: {
28
31
  storageKey: string;
29
32
  }): Promise<void>;
@@ -29,6 +29,9 @@ export class InMemoryBlobStore {
29
29
  async exists(input) {
30
30
  return this.bytesByKey.has(input.storageKey);
31
31
  }
32
+ async sizeOf({ storageKey }, _optional = {}) {
33
+ return this.bytesByKey.get(storageKey)?.byteLength ?? null;
34
+ }
32
35
  async remove(input) {
33
36
  this.bytesByKey.delete(input.storageKey);
34
37
  }
@@ -0,0 +1,15 @@
1
+ import type { MediaRecord } from "./types.js";
2
+ import type { MediaRepoPort } from "./ports.js";
3
+ /** Bind creation writes to one authenticated caller without shared mutable actor state.
4
+ * Useful when adapting an older upload service that predates asset-level attribution. */
5
+ export declare function mediaRepoWithCreator(required: {
6
+ mediaRepo: MediaRepoPort;
7
+ principalId: string;
8
+ }, _optional?: Record<string, never>): MediaRepoPort;
9
+ /** An update retains creation attribution, even when the original creator is unknown.
10
+ * Omitting an unknown field preserves compatibility with legacy serialized records. */
11
+ export declare function preserveMediaCreator(required: {
12
+ record: MediaRecord;
13
+ original: MediaRecord;
14
+ }, _optional?: Record<string, never>): MediaRecord;
15
+ //# sourceMappingURL=created-by.d.ts.map
@@ -0,0 +1,20 @@
1
+ /** Bind creation writes to one authenticated caller without shared mutable actor state.
2
+ * Useful when adapting an older upload service that predates asset-level attribution. */
3
+ export function mediaRepoWithCreator(required, _optional = {}) {
4
+ const { mediaRepo, principalId } = required;
5
+ return {
6
+ findById: input => mediaRepo.findById(input),
7
+ findBySlug: input => mediaRepo.findBySlug(input),
8
+ list: input => mediaRepo.list(input),
9
+ remove: input => mediaRepo.remove(input),
10
+ save: record => mediaRepo.save({ ...record, createdBy: principalId }),
11
+ };
12
+ }
13
+ /** An update retains creation attribution, even when the original creator is unknown.
14
+ * Omitting an unknown field preserves compatibility with legacy serialized records. */
15
+ export function preserveMediaCreator(required, _optional = {}) {
16
+ const { createdBy: _ignored, ...metadata } = required.record;
17
+ return { ...metadata, ...(required.original.createdBy !== undefined
18
+ ? { createdBy: required.original.createdBy } : {}) };
19
+ }
20
+ //# sourceMappingURL=created-by.js.map
@@ -15,6 +15,7 @@ export type { MediaStatus, MediaSource, MediaRecord, AssetBlobStatus, AssetBlobR
15
15
  export { MediaNotFoundError, MediaValidationError, MediaConflictError, MediaSourceImmutableError, MediaStillReferencedError, } from "./types.js";
16
16
  export type { MediaRepoPort, AssetBlobRepoPort, AssetRenditionRepoPort, BlobStorePort, BlobGcJournalRepoPort, PutBlobInput, TransformDefinitionRepoPort, } from "./ports.js";
17
17
  export { computeBlobStorageKey } from "./blob-key.js";
18
+ export { preserveMediaCreator, mediaRepoWithCreator } from "./created-by.js";
18
19
  export { MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES, isAllowedMediaHtmlAttributeName, parseMediaHtmlAttributes, describeMediaHtmlAttributeError, type MediaHtmlAttributeRejectionReason, type MediaHtmlAttributeError, type ParsedMediaHtmlAttributes, } from "./html-attributes.js";
19
20
  export { InMemoryMediaRepo, InMemoryAssetBlobRepo, InMemoryAssetRenditionRepo, InMemoryBlobGcJournalRepo, InMemoryTransformDefinitionRepo, } from "./repo.memory.js";
20
21
  export { withSha256Lock } from "./blob-gc-lock.js";
@@ -1,5 +1,6 @@
1
1
  export { MediaNotFoundError, MediaValidationError, MediaConflictError, MediaSourceImmutableError, MediaStillReferencedError, } from "./types.js";
2
2
  export { computeBlobStorageKey } from "./blob-key.js";
3
+ export { preserveMediaCreator, mediaRepoWithCreator } from "./created-by.js";
3
4
  export { MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES, isAllowedMediaHtmlAttributeName, parseMediaHtmlAttributes, describeMediaHtmlAttributeError, } from "./html-attributes.js";
4
5
  export { InMemoryMediaRepo, InMemoryAssetBlobRepo, InMemoryAssetRenditionRepo, InMemoryBlobGcJournalRepo, InMemoryTransformDefinitionRepo, } from "./repo.memory.js";
5
6
  export { withSha256Lock } from "./blob-gc-lock.js";
@@ -60,7 +60,7 @@ export interface UploadMediaInput {
60
60
  alt?: string | undefined;
61
61
  caption?: string | undefined;
62
62
  credit?: string | undefined;
63
- /** Attributed on the `asset_blobs` row (required attribution). */
63
+ /** Acting principal, stamped on each media row and on a newly created asset_blobs row. */
64
64
  createdByPrincipal: string;
65
65
  }
66
66
  export interface UploadMediaDeps {
@@ -304,6 +304,7 @@ export async function uploadMedia(required, optional = {}) {
304
304
  const media = {
305
305
  id: deps.idGen.newId(),
306
306
  workspaceId: input.workspaceId,
307
+ createdBy: input.createdByPrincipal,
307
308
  title,
308
309
  slug,
309
310
  alt: input.alt?.trim() ?? "",
@@ -13,13 +13,15 @@
13
13
  *
14
14
  * `BlobStorePort`'s surface is deliberately narrower than the full
15
15
  * design: no `capabilities()`/`createPresignedUpload()` (the presign surface is
16
- * explicitly deferred by this task's scope). It has one method beyond the
17
- * task's stated minimal 3 (`put`/`get`/`exists`): `remove()`. That addition is
16
+ * explicitly deferred by this task's scope). It extends the original minimal
17
+ * 3 (`put`/`get`/`exists`) with `remove()`. That addition is
18
18
  * necessary to implement `purgeMedia`'s explicitly-requested "best-effort blob
19
19
  * unlink" (see `media-service.ts`) — without a way to delete bytes, purge could
20
20
  * only ever delete rows, never reclaim storage. This is disclosed here, not
21
21
  * silently smuggled in: it is still far short of the real journaled
22
22
  * GC protocol (no epochs, no `BEGIN IMMEDIATE`, no crash-safety guarantee).
23
+ * `putIfAbsent` adds atomic create-only writes; optional `sizeOf` reports original bytes
24
+ * without reading payloads (successful filesystem sizes are cached by content-addressed key).
23
25
  *
24
26
  * Interfaces only — no feature logic.
25
27
  */
@@ -162,6 +164,14 @@ export interface BlobStorePort {
162
164
  get(input: {
163
165
  storageKey: string;
164
166
  }): Promise<Uint8Array>;
167
+ /**
168
+ * Original byte count without reading the payload; null only when the key is absent.
169
+ * Other storage failures reject. Optional for older/custom adapters; consumers must report
170
+ * unknown size when unavailable, never download an entire blob just to count bytes.
171
+ */
172
+ sizeOf?(required: {
173
+ storageKey: string;
174
+ }, optional?: Record<string, never>): Promise<number | null>;
165
175
  exists(input: {
166
176
  storageKey: string;
167
177
  }): Promise<boolean>;
@@ -1,3 +1,4 @@
1
+ import { preserveMediaCreator } from "./created-by.js";
1
2
  export class InMemoryMediaRepo {
2
3
  rows;
3
4
  constructor(requiredArgs, optionalArgs = {}) {
@@ -19,7 +20,7 @@ export class InMemoryMediaRepo {
19
20
  this.rows.push(record);
20
21
  return;
21
22
  }
22
- this.rows[index] = record;
23
+ this.rows[index] = preserveMediaCreator({ record, original: this.rows[index] });
23
24
  }
24
25
  async remove(required) {
25
26
  this.rows = this.rows.filter((row) => !(row.workspaceId === required.workspaceId && row.id === required.id));
@@ -31,6 +31,7 @@ function isMediaShapeRejection({ error }) {
31
31
  function toMediaToolView(record) {
32
32
  return {
33
33
  id: record.id,
34
+ createdBy: record.createdBy ?? null,
34
35
  slug: record.slug,
35
36
  title: record.title,
36
37
  alt: record.alt,
@@ -41,6 +41,10 @@ export interface MediaSource {
41
41
  export interface MediaRecord {
42
42
  id: UUID;
43
43
  workspaceId: UUID;
44
+ /** Acting principal at asset creation, independent of the producer/model and editable credit.
45
+ * Optional/null for records written before attribution existed. Never inferred from a shared
46
+ * blob's owner: identical bytes may have been uploaded by different principals. */
47
+ createdBy?: string | null | undefined;
44
48
  title: string;
45
49
  /**
46
50
  * Human-memorable, unique-per-workspace lookup key (owner-directed, 2026-09-07) — an ADDITIONAL
@@ -241,6 +241,12 @@ function validateTarget(target) {
241
241
  if (!VALID_TARGET_KINDS.has(kind)) {
242
242
  throw new MenuValidationError({ message: `unknown target kind '${kind}'` });
243
243
  }
244
+ if (kind === "entryRef" && target.lastKnownHref !== undefined) {
245
+ const rawHref = target.lastKnownHref;
246
+ if (typeof rawHref !== "string" || !rawHref.trim() || !isAllowedHref({ rawHref })) {
247
+ throw new MenuValidationError({ message: `entryRef lastKnownHref must be ${ALLOWED_HREF_SHAPES_DESCRIPTION}.` });
248
+ }
249
+ }
244
250
  if (kind === "url") {
245
251
  const rawHref = target.href;
246
252
  if (typeof rawHref !== "string" || !rawHref.trim()) {
@@ -49,6 +49,11 @@ export type ReservedNavTargetKind = "dynamicQuery" | "content";
49
49
  /** A link to another content entry (page/post/media/…). Integrity-tracked. */
50
50
  export interface NavEntryTarget {
51
51
  readonly kind: "entryRef";
52
+ /** Optional content-type hint for editors; reference integrity and routing still use entryId. */
53
+ readonly entryType?: string;
54
+ /** Author's last known URL, retained for exports and routing misses. A known unavailable
55
+ * entry must remain hidden; this snapshot never overrides an availability decision. */
56
+ readonly lastKnownHref?: string;
52
57
  /** Target entry id. Extracted to `entry_refs` at the write chokepoint. */
53
58
  readonly entryId: UUID;
54
59
  }
@@ -103,7 +103,7 @@ function rethrowWriteError(error, toolId, target, permission) {
103
103
  * @complexity O(value size + rule count) plus bounded repository calls and a human wait.
104
104
  * @throws ToolInputError for invalid input, denied keys, absent/declined/stale confirmation. */
105
105
  export function buildSettingsValueWriteHandler(deps, toolId) {
106
- return async (ctx) => {
106
+ return async (ctx, options) => {
107
107
  const target = parseTarget(toolId, ctx.input);
108
108
  const permission = deriveRequiredPermission({ scope: target.scope, callerPrincipalId: ctx.principal.id });
109
109
  const isSet = toolId === "settings_set_value";
@@ -124,7 +124,7 @@ export function buildSettingsValueWriteHandler(deps, toolId) {
124
124
  if (rule) {
125
125
  if (!deps.confirmWrite)
126
126
  throw new ToolInputError({ message: `${toolId}: '${rule.namespace}.${rule.key}' requires a human confirmation card: ${rule.reason}. The owner can change it in Settings.` });
127
- const confirmed = await deps.confirmWrite({ ctx, toolId, namespace: rule.namespace, key: rule.key, scope: target.scope, previous: structuredClone(previous), ...(isSet ? { value: structuredClone(target.value) } : {}), reason: rule.reason });
127
+ const confirmed = await deps.confirmWrite({ ctx, toolId, namespace: rule.namespace, key: rule.key, scope: target.scope, previous: structuredClone(previous), ...(isSet ? { value: structuredClone(target.value) } : {}), reason: rule.reason }, options);
128
128
  if (confirmed !== true)
129
129
  throw new ToolInputError({ message: `${toolId}: the human did not confirm the change. Nothing was changed.` });
130
130
  const current = await resolveTarget(deps, target, isSet);
@@ -11,7 +11,7 @@ import type { Clock } from "@jini-ai/core/primitives";
11
11
  import type { AuthorizeFn } from "../core/commands/command.js";
12
12
  import { type SettingsWriteConfirmation } from "./agent-value-write-tools.js";
13
13
  import { type AgentSettingWriteRule } from "./agent-write-denylist.js";
14
- import { type DerivedRiskByToolId, type ToolRegistration } from "@jini-ai/core";
14
+ import { type DerivedRiskByToolId, type ToolExecutionOptions, type ToolRegistration } from "@jini-ai/core";
15
15
  import type { SettingsPrincipalLookupPort } from "./principal-lookup.js";
16
16
  import type { SettingsRepoPort } from "./ports.js";
17
17
  import { set as setSettingValue } from "./write-service.js";
@@ -33,8 +33,10 @@ export interface SettingsToolDeps {
33
33
  /** Host extensions: explicit deny or human-confirmation rules, never agent inputs. */
34
34
  extraDeniedSettings?: readonly AgentSettingWriteRule[];
35
35
  extraConfirmationSettings?: readonly AgentSettingWriteRule[];
36
- /** Must obtain an authenticated human card decision; absent means protected keys fail closed. */
37
- confirmWrite?: (request: SettingsWriteConfirmation) => Promise<boolean>;
36
+ /** Must obtain an authenticated human card decision; absent means protected keys fail closed.
37
+ * `options` is the invoking transport's execution options (its `emitSurface`), forwarded from the
38
+ * tool handler's second argument: the card can only reach a human through that seam. */
39
+ confirmWrite?: (request: SettingsWriteConfirmation, options?: ToolExecutionOptions) => Promise<boolean>;
38
40
  /** Host's validation wrapper; defaults to the package's existing set chokepoint. */
39
41
  setValue?: typeof setSettingValue;
40
42
  }
@@ -352,12 +352,14 @@ export interface UnassignTermsRequired {
352
352
  * authorize -> validateAssignmentTarget -> write -> stamp -> outbox chokepoint as `assignTerms`,
353
353
  * and — like `assignTerms` — never produces a `taxonomy_revisions` row ( disclosed
354
354
  * narrowing applies equally to removal). Idempotent: unassigning a row that isn't there is a no-op,
355
- * not an error (`remove`'s 0-vs-1 return is intentionally not surfaced to the caller). Chosen over
355
+ * not an error. Only removed ids are returned, stamped, and emitted; an entirely absent set is a no-op. Chosen over
356
356
  * a "set terms" replace call — see the taxonomy plan's "What the docs settle" #3 for why replace
357
357
  * risks silently erasing a concurrent assignment.
358
358
  * See docs/decisions/DR-005-ordered-taxonomy-validation.md.
359
359
  */
360
- export declare function unassignTerms(required: UnassignTermsRequired, _optional?: Record<string, never>): Promise<void>;
360
+ export declare function unassignTerms(required: UnassignTermsRequired, _optional?: Record<string, never>): Promise<{
361
+ removedTermIds: string[];
362
+ }>;
361
363
  export interface EntryTermsCleanupPort {
362
364
  deleteByContent(params: {
363
365
  workspaceId: string;
@@ -408,7 +408,7 @@ export async function assignTerms(required, _optional = {}) {
408
408
  * authorize -> validateAssignmentTarget -> write -> stamp -> outbox chokepoint as `assignTerms`,
409
409
  * and — like `assignTerms` — never produces a `taxonomy_revisions` row ( disclosed
410
410
  * narrowing applies equally to removal). Idempotent: unassigning a row that isn't there is a no-op,
411
- * not an error (`remove`'s 0-vs-1 return is intentionally not surfaced to the caller). Chosen over
411
+ * not an error. Only removed ids are returned, stamped, and emitted; an entirely absent set is a no-op. Chosen over
412
412
  * a "set terms" replace call — see the taxonomy plan's "What the docs settle" #3 for why replace
413
413
  * risks silently erasing a concurrent assignment.
414
414
  * See docs/decisions/DR-005-ordered-taxonomy-validation.md.
@@ -417,19 +417,24 @@ export async function unassignTerms(required, _optional = {}) {
417
417
  const { deps, principalId, contentType, contentId, termIds } = required;
418
418
  await authorizeTaxonomyManage(deps, principalId);
419
419
  await validateAssignmentTarget(deps, contentType, contentId, termIds);
420
+ const removedTermIds = [];
420
421
  for (const termId of termIds) {
421
- await deps.entryTerms.remove({ contentType, contentId, termId });
422
+ if (await deps.entryTerms.remove({ contentType, contentId, termId }) > 0)
423
+ removedTermIds.push(termId);
422
424
  }
425
+ if (removedTermIds.length === 0)
426
+ return { removedTermIds };
423
427
  const now = kernelNowIso({ clock: deps.clock });
424
428
  await deps.stampWatermark({});
425
429
  await deps.outbox.enqueue({ event: {
426
430
  name: "taxonomy.terms_unassigned",
427
431
  contentType,
428
432
  contentId,
429
- termIds,
433
+ termIds: removedTermIds,
430
434
  actorId: principalId,
431
435
  occurredAt: now,
432
436
  } });
437
+ return { removedTermIds };
433
438
  }
434
439
  /** Content-deletion event subscriber. Best-effort (a missed event leaves an orphaned
435
440
  * `entry_terms` row that is inert on read; a periodic/boot reconciliation sweep — modeled by
@@ -95,12 +95,16 @@ export function createTrashService(deps, optional = {}) {
95
95
  priorMarker: marker.priorMarker ?? trashOptional.priorMarker ?? null,
96
96
  };
97
97
  await deps.repo.insert({ row });
98
- await notifyChanged(optional, {
99
- workspaceId: required.workspaceId,
100
- entityType: required.entityType,
101
- entityId: required.entityId,
102
- change: "trash",
103
- });
98
+ // A successful noop may repair a missing index, but did not change the domain marker.
99
+ // Repeated removal must not emit another recovery watermark or outbox event (F1487).
100
+ if (!marker.noop) {
101
+ await notifyChanged(optional, {
102
+ workspaceId: required.workspaceId,
103
+ entityType: required.entityType,
104
+ entityId: required.entityId,
105
+ change: "trash",
106
+ });
107
+ }
104
108
  return marker;
105
109
  }
106
110
  });
@@ -135,12 +139,15 @@ export function createTrashService(deps, optional = {}) {
135
139
  if (!marker.ok)
136
140
  return marker.reason === "not-found" ? "not-found" : "version-changed";
137
141
  await deps.repo.deleteById({ workspaceId: required.workspaceId, id: row.id });
138
- await notifyChanged(optional, {
139
- workspaceId: required.workspaceId,
140
- entityType: required.entityType,
141
- entityId: required.entityId,
142
- change: "restore",
143
- });
142
+ // An already-clear marker still needs index cleanup, without a duplicate notification.
143
+ if (!marker.noop) {
144
+ await notifyChanged(optional, {
145
+ workspaceId: required.workspaceId,
146
+ entityType: required.entityType,
147
+ entityId: required.entityId,
148
+ change: "restore",
149
+ });
150
+ }
144
151
  return "restored";
145
152
  }
146
153
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/cms",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -132,7 +132,7 @@
132
132
  "@jini-ai/http-kit": "0.4.0"
133
133
  },
134
134
  "dependencies": {
135
- "@jini-ai/core": "0.4.0"
135
+ "@jini-ai/core": "^0.4.0"
136
136
  },
137
137
  "peerDependencies": {
138
138
  "sharp": "^0.35.3",