@jini-ai/cms 0.3.5 → 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/README.md +22 -17
- package/dist/content-types/agent-tools.js +1 -1
- package/dist/content-types/errors.d.ts +19 -1
- package/dist/content-types/errors.js +22 -1
- package/dist/content-types/index-provisioning.d.ts +56 -10
- package/dist/content-types/index-provisioning.js +86 -13
- package/dist/content-types/index.d.ts +15 -3
- package/dist/content-types/index.js +14 -2
- package/dist/content-types/lifecycle.js +3 -3
- package/dist/content-types/types.d.ts +64 -3
- package/dist/content-types/types.js +67 -3
- package/dist/content-types/write-service.d.ts +18 -3
- package/dist/content-types/write-service.js +28 -2
- package/dist/core/ports.d.ts +12 -2
- package/dist/entries/field-validation.js +96 -1
- package/dist/entries/write-service.d.ts +14 -0
- package/dist/entries/write-service.js +2 -2
- package/dist/identity/admin-crud-service.d.ts +28 -0
- package/dist/identity/admin-crud-service.js +30 -0
- package/dist/identity/auth-service.d.ts +32 -1
- package/dist/identity/auth-service.js +44 -15
- package/dist/identity/index.d.ts +4 -1
- package/dist/identity/index.js +4 -1
- package/dist/identity/seed.d.ts +14 -2
- package/dist/identity/seed.js +197 -45
- package/dist/media/agent-tools.d.ts +13 -3
- package/dist/media/agent-tools.js +45 -10
- package/dist/media/content-type-sniffer.d.ts +1 -1
- package/dist/media/content-type-sniffer.js +52 -4
- package/dist/media/html-attributes.d.ts +90 -0
- package/dist/media/html-attributes.js +161 -0
- package/dist/media/image-transformer.sharp.d.ts +35 -1
- package/dist/media/image-transformer.sharp.js +44 -2
- package/dist/media/index.d.ts +2 -1
- package/dist/media/index.js +2 -1
- package/dist/media/media-service.d.ts +62 -2
- package/dist/media/media-service.js +230 -7
- package/dist/media/ports.d.ts +6 -0
- package/dist/media/rendition-service.d.ts +7 -1
- package/dist/media/rendition-service.js +10 -4
- package/dist/media/repo.memory.d.ts +4 -0
- package/dist/media/repo.memory.js +3 -0
- package/dist/media/tool-registrations.d.ts +12 -0
- package/dist/media/tool-registrations.js +20 -12
- package/dist/media/types.d.ts +26 -0
- package/dist/navigation/agent-tools.js +8 -1
- package/dist/navigation/index.d.ts +10 -1
- package/dist/navigation/index.js +10 -1
- package/dist/navigation/menu-service.d.ts +60 -4
- package/dist/navigation/menu-service.js +106 -8
- 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/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
|
@@ -46,6 +46,55 @@ export declare class MenuLocationBoundError extends MenuConflictError {
|
|
|
46
46
|
export declare const DEFAULT_MAX_TREE_DEPTH = 5;
|
|
47
47
|
/** Default max total item count across the whole tree. */
|
|
48
48
|
export declare const DEFAULT_MAX_ITEM_COUNT = 500;
|
|
49
|
+
/**
|
|
50
|
+
* The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
|
|
51
|
+
* `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
|
|
52
|
+
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth**: exported
|
|
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 — the host already depends on `@jini-ai/cms`, not
|
|
55
|
+
* the reverse — rather than each consumer hand-copying the predicate.
|
|
56
|
+
*
|
|
57
|
+
* The two host-side copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
|
|
58
|
+
* `value => passes ? value : "#"` wrapper around the identical predicate below) have **not yet been
|
|
59
|
+
* retired to import this** — that edit is intentionally out of this change's scope (a
|
|
60
|
+
* `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
|
|
61
|
+
* `@jini-ai/cms/core` today, and `features/theme` already imports `@jini-ai/cms/core` too, so both
|
|
62
|
+
* call sites may legally import `@jini-ai/cms/navigation` directly — verified by grep, not assumed).
|
|
63
|
+
* Until that follow-up lands, `apps/website/development/scripts/check-menu-href-allowlist-sync.ts`
|
|
64
|
+
* behaviorally gates the three copies against the same adversarial table so a divergence fails CI
|
|
65
|
+
* instead of drifting silently.
|
|
66
|
+
*
|
|
67
|
+
* Replaced (this change) a `URL_SCHEME_DENYLIST` (`startsWith` on a lowercased, `.trim()`ed string)
|
|
68
|
+
* that failed open: the WHATWG `URL` parser strips TAB/LF/CR from ANYWHERE in the input and folds a
|
|
69
|
+
* leading backslash to `/` for `http(s)`, so `"java\tscript:alert(1)"`, `" javascript:alert(1)"`,
|
|
70
|
+
* `"file:///etc/passwd"`, `"blob:…"`, `"about:blank"`, `"//evil.example"` (protocol-relative), and
|
|
71
|
+
* `"/\evil.example"` all resolved to a dangerous or off-origin target while never matching any
|
|
72
|
+
* `startsWith` prefix in the old list. An allowlist closes the whole bypass class at once (case,
|
|
73
|
+
* embedded control characters, and scheme are all irrelevant to an allowlist the same way) rather
|
|
74
|
+
* than needing a new prefix bolted on per newly-discovered shape.
|
|
75
|
+
*
|
|
76
|
+
* `validateTarget`'s caller REJECTS on a `false` result (`MenuValidationError`, surfaced as HTTP 400)
|
|
77
|
+
* rather than silently coercing the way `safeHref`'s render-time callers degrade to `"#"` — a
|
|
78
|
+
* write-time author-facing path should say so, not quietly rewrite the input. Verified against the
|
|
79
|
+
* workspace's stored menu data (`content.db`, `content.seed.db`) before this change: every existing
|
|
80
|
+
* `url`-kind href already starts with `#` or `/`, so this tightening rejects nothing that exists
|
|
81
|
+
* today — it governs `createMenu` too, so it also reaches the Jini MCP agent-tool path, not just
|
|
82
|
+
* hrefs written through the host's admin editor.
|
|
83
|
+
*
|
|
84
|
+
* @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
|
|
85
|
+
* otherwise a valid input — this function performs no other precondition checks.
|
|
86
|
+
* @returns `true` when `rawHref` passes the allowlist, `false` otherwise.
|
|
87
|
+
* @complexity O(1) — a handful of string checks plus one `URL` construction on the `/…` branch.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isAllowedHref(rawHref: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Human-readable description of {@link isAllowedHref}'s accepted shapes — the single source of truth
|
|
92
|
+
* for both `validateTarget`'s rejection message below and `agent-tools.ts`'s published JSON Schema
|
|
93
|
+
* `href` description, so an author-facing 400 and an LLM-facing tool description can never drift out
|
|
94
|
+
* of sync with each other the way three independent copies of the URL-checking LOGIC itself did
|
|
95
|
+
* before this change.
|
|
96
|
+
*/
|
|
97
|
+
export declare const ALLOWED_HREF_SHAPES_DESCRIPTION = "an in-page anchor ('#...'), a same-origin relative path ('/...'), an 'http://' or 'https://' URL, or a 'mailto:' address";
|
|
49
98
|
export interface TreeValidationLimits {
|
|
50
99
|
maxDepth?: number | undefined;
|
|
51
100
|
maxItemCount?: number | undefined;
|
|
@@ -54,15 +103,22 @@ export interface TreeValidationLimits {
|
|
|
54
103
|
* Validates a candidate item tree and returns a defensively-cloned copy.
|
|
55
104
|
*
|
|
56
105
|
* Checks (total/bounded validation + target integrity):
|
|
106
|
+
* - `items` (and every `children` array) is actually an array, and every node
|
|
107
|
+
* in it is a non-null object — untrusted JSON bodies can carry `null`
|
|
108
|
+
* entries or a non-array `items`/`children`, which must reject as a
|
|
109
|
+
* `MenuValidationError`, not throw an uncaught `TypeError`;
|
|
57
110
|
* - every node has a non-empty `id`, unique across the whole tree (id
|
|
58
111
|
* stability is a caller responsibility — see `updateMenuTree` doc for the
|
|
59
112
|
* simplification this build accepts);
|
|
60
113
|
* - nesting depth stays within `maxDepth`;
|
|
61
114
|
* - total item count stays within `maxItemCount`;
|
|
62
|
-
* - every
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
115
|
+
* - every node has a `target` object (a missing/`null` `target` rejects
|
|
116
|
+
* rather than crashing) whose `kind` is one of the four v1 kinds — reserved
|
|
117
|
+
* kinds (`dynamicQuery`, `content`) are rejected with a clear "not yet"
|
|
118
|
+
* error rather than silently accepted;
|
|
119
|
+
* - `url` targets require a non-empty string `href` and pass an origin/scheme
|
|
120
|
+
* ALLOWLIST (`#…`, same-origin `/…`, `http(s)://`, `mailto:` — see
|
|
121
|
+
* {@link isAllowedHref}), not a scheme denylist.
|
|
66
122
|
*
|
|
67
123
|
* @complexity O(n) over total node count for one full walk; no per-node
|
|
68
124
|
* backtracking. Space is O(n) for the cloned tree plus O(n) for the id set.
|
|
@@ -55,20 +55,100 @@ export const DEFAULT_MAX_ITEM_COUNT = 500;
|
|
|
55
55
|
const VALID_TARGET_KINDS = new Set(["entryRef", "termRef", "url", "route"]);
|
|
56
56
|
/** Named deferred seams — recognized, rejected until their resolver ships. */
|
|
57
57
|
const RESERVED_TARGET_KINDS = new Set(["dynamicQuery", "content"]);
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Fixed placeholder origin {@link isAllowedHref} resolves a claimed same-origin-relative href
|
|
60
|
+
* against, to compare the RESOLVED origin rather than the raw string shape. Mirrors
|
|
61
|
+
* `apps/website/src/server/inbound/public-http/http/site/render.ts`'s identically-named constant
|
|
62
|
+
* byte-for-byte — see that file's own header for why a fixed placeholder origin is sufficient (only
|
|
63
|
+
* the relationship between the resolved URL's origin and this one is ever inspected, never the
|
|
64
|
+
* placeholder value itself).
|
|
65
|
+
*/
|
|
66
|
+
const SAFE_HREF_RESOLUTION_BASE = "http://tovu-safehref.invalid/";
|
|
67
|
+
const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
68
|
+
/**
|
|
69
|
+
* The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
|
|
70
|
+
* `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
|
|
71
|
+
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth**: exported
|
|
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 — the host already depends on `@jini-ai/cms`, not
|
|
74
|
+
* the reverse — rather than each consumer hand-copying the predicate.
|
|
75
|
+
*
|
|
76
|
+
* The two host-side copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
|
|
77
|
+
* `value => passes ? value : "#"` wrapper around the identical predicate below) have **not yet been
|
|
78
|
+
* retired to import this** — that edit is intentionally out of this change's scope (a
|
|
79
|
+
* `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
|
|
80
|
+
* `@jini-ai/cms/core` today, and `features/theme` already imports `@jini-ai/cms/core` too, so both
|
|
81
|
+
* call sites may legally import `@jini-ai/cms/navigation` directly — verified by grep, not assumed).
|
|
82
|
+
* Until that follow-up lands, `apps/website/development/scripts/check-menu-href-allowlist-sync.ts`
|
|
83
|
+
* behaviorally gates the three copies against the same adversarial table so a divergence fails CI
|
|
84
|
+
* instead of drifting silently.
|
|
85
|
+
*
|
|
86
|
+
* Replaced (this change) a `URL_SCHEME_DENYLIST` (`startsWith` on a lowercased, `.trim()`ed string)
|
|
87
|
+
* that failed open: the WHATWG `URL` parser strips TAB/LF/CR from ANYWHERE in the input and folds a
|
|
88
|
+
* leading backslash to `/` for `http(s)`, so `"java\tscript:alert(1)"`, `" javascript:alert(1)"`,
|
|
89
|
+
* `"file:///etc/passwd"`, `"blob:…"`, `"about:blank"`, `"//evil.example"` (protocol-relative), and
|
|
90
|
+
* `"/\evil.example"` all resolved to a dangerous or off-origin target while never matching any
|
|
91
|
+
* `startsWith` prefix in the old list. An allowlist closes the whole bypass class at once (case,
|
|
92
|
+
* embedded control characters, and scheme are all irrelevant to an allowlist the same way) rather
|
|
93
|
+
* than needing a new prefix bolted on per newly-discovered shape.
|
|
94
|
+
*
|
|
95
|
+
* `validateTarget`'s caller REJECTS on a `false` result (`MenuValidationError`, surfaced as HTTP 400)
|
|
96
|
+
* rather than silently coercing the way `safeHref`'s render-time callers degrade to `"#"` — a
|
|
97
|
+
* write-time author-facing path should say so, not quietly rewrite the input. Verified against the
|
|
98
|
+
* workspace's stored menu data (`content.db`, `content.seed.db`) before this change: every existing
|
|
99
|
+
* `url`-kind href already starts with `#` or `/`, so this tightening rejects nothing that exists
|
|
100
|
+
* today — it governs `createMenu` too, so it also reaches the Jini MCP agent-tool path, not just
|
|
101
|
+
* hrefs written through the host's admin editor.
|
|
102
|
+
*
|
|
103
|
+
* @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
|
|
104
|
+
* otherwise a valid input — this function performs no other precondition checks.
|
|
105
|
+
* @returns `true` when `rawHref` passes the allowlist, `false` otherwise.
|
|
106
|
+
* @complexity O(1) — a handful of string checks plus one `URL` construction on the `/…` branch.
|
|
107
|
+
*/
|
|
108
|
+
export function isAllowedHref(rawHref) {
|
|
109
|
+
const href = rawHref.trim();
|
|
110
|
+
if (href.startsWith("#"))
|
|
111
|
+
return true;
|
|
112
|
+
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href))
|
|
113
|
+
return true;
|
|
114
|
+
if (href.startsWith("/")) {
|
|
115
|
+
try {
|
|
116
|
+
return new URL(href, SAFE_HREF_RESOLUTION_BASE).origin === SAFE_HREF_RESOLUTION_ORIGIN;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Human-readable description of {@link isAllowedHref}'s accepted shapes — the single source of truth
|
|
126
|
+
* for both `validateTarget`'s rejection message below and `agent-tools.ts`'s published JSON Schema
|
|
127
|
+
* `href` description, so an author-facing 400 and an LLM-facing tool description can never drift out
|
|
128
|
+
* of sync with each other the way three independent copies of the URL-checking LOGIC itself did
|
|
129
|
+
* before this change.
|
|
130
|
+
*/
|
|
131
|
+
export const ALLOWED_HREF_SHAPES_DESCRIPTION = "an in-page anchor ('#...'), a same-origin relative path ('/...'), an 'http://' or 'https://' URL, or a 'mailto:' address";
|
|
59
132
|
/**
|
|
60
133
|
* Validates a candidate item tree and returns a defensively-cloned copy.
|
|
61
134
|
*
|
|
62
135
|
* Checks (total/bounded validation + target integrity):
|
|
136
|
+
* - `items` (and every `children` array) is actually an array, and every node
|
|
137
|
+
* in it is a non-null object — untrusted JSON bodies can carry `null`
|
|
138
|
+
* entries or a non-array `items`/`children`, which must reject as a
|
|
139
|
+
* `MenuValidationError`, not throw an uncaught `TypeError`;
|
|
63
140
|
* - every node has a non-empty `id`, unique across the whole tree (id
|
|
64
141
|
* stability is a caller responsibility — see `updateMenuTree` doc for the
|
|
65
142
|
* simplification this build accepts);
|
|
66
143
|
* - nesting depth stays within `maxDepth`;
|
|
67
144
|
* - total item count stays within `maxItemCount`;
|
|
68
|
-
* - every
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
145
|
+
* - every node has a `target` object (a missing/`null` `target` rejects
|
|
146
|
+
* rather than crashing) whose `kind` is one of the four v1 kinds — reserved
|
|
147
|
+
* kinds (`dynamicQuery`, `content`) are rejected with a clear "not yet"
|
|
148
|
+
* error rather than silently accepted;
|
|
149
|
+
* - `url` targets require a non-empty string `href` and pass an origin/scheme
|
|
150
|
+
* ALLOWLIST (`#…`, same-origin `/…`, `http(s)://`, `mailto:` — see
|
|
151
|
+
* {@link isAllowedHref}), not a scheme denylist.
|
|
72
152
|
*
|
|
73
153
|
* @complexity O(n) over total node count for one full walk; no per-node
|
|
74
154
|
* backtracking. Space is O(n) for the cloned tree plus O(n) for the id set.
|
|
@@ -80,6 +160,9 @@ export function validateAndCloneTree(items, limits = {}) {
|
|
|
80
160
|
const seenIds = new Set();
|
|
81
161
|
let count = 0;
|
|
82
162
|
function walk(nodes, depth) {
|
|
163
|
+
if (!Array.isArray(nodes)) {
|
|
164
|
+
throw new MenuValidationError("menu items must be an array");
|
|
165
|
+
}
|
|
83
166
|
if (depth > maxDepth) {
|
|
84
167
|
throw new MenuValidationError(`menu tree exceeds max nesting depth of ${maxDepth}`);
|
|
85
168
|
}
|
|
@@ -88,6 +171,9 @@ export function validateAndCloneTree(items, limits = {}) {
|
|
|
88
171
|
if (count > maxItemCount) {
|
|
89
172
|
throw new MenuValidationError(`menu tree exceeds max item count of ${maxItemCount}`);
|
|
90
173
|
}
|
|
174
|
+
if (!node || typeof node !== "object") {
|
|
175
|
+
throw new MenuValidationError("every menu item must be an object");
|
|
176
|
+
}
|
|
91
177
|
if (!node.id || !node.id.trim()) {
|
|
92
178
|
throw new MenuValidationError("every menu item requires a non-empty id");
|
|
93
179
|
}
|
|
@@ -102,7 +188,16 @@ export function validateAndCloneTree(items, limits = {}) {
|
|
|
102
188
|
}
|
|
103
189
|
return walk(items, 1);
|
|
104
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Guards every field this function reads off an untrusted `target` before reading it — a `target`
|
|
193
|
+
* arriving `null`/`undefined` (missing from the request body) or a `url` target missing `href`
|
|
194
|
+
* previously threw an uncaught `TypeError` here, which the host's route handler had no case for and
|
|
195
|
+
* surfaced as a 500 instead of the intended 400 `MenuValidationError` path.
|
|
196
|
+
*/
|
|
105
197
|
function validateTarget(target) {
|
|
198
|
+
if (!target || typeof target !== "object") {
|
|
199
|
+
throw new MenuValidationError("every menu item requires a target");
|
|
200
|
+
}
|
|
106
201
|
const kind = target.kind;
|
|
107
202
|
if (RESERVED_TARGET_KINDS.has(kind)) {
|
|
108
203
|
throw new MenuValidationError(`target kind '${kind}' is a reserved seam and is not supported yet`);
|
|
@@ -111,9 +206,12 @@ function validateTarget(target) {
|
|
|
111
206
|
throw new MenuValidationError(`unknown target kind '${kind}'`);
|
|
112
207
|
}
|
|
113
208
|
if (kind === "url") {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
throw new MenuValidationError(
|
|
209
|
+
const rawHref = target.href;
|
|
210
|
+
if (typeof rawHref !== "string" || !rawHref.trim()) {
|
|
211
|
+
throw new MenuValidationError("url target requires a non-empty href");
|
|
212
|
+
}
|
|
213
|
+
if (!isAllowedHref(rawHref)) {
|
|
214
|
+
throw new MenuValidationError(`url target href is not allowed: '${rawHref}'. Accepted shapes: ${ALLOWED_HREF_SHAPES_DESCRIPTION}.`);
|
|
117
215
|
}
|
|
118
216
|
}
|
|
119
217
|
}
|
|
@@ -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
|
|
@@ -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
|
|
@@ -48,9 +48,10 @@
|
|
|
48
48
|
* `ToolRegistration`s.
|
|
49
49
|
*
|
|
50
50
|
* Architectural role:
|
|
51
|
-
* `features/taxonomy` domain declaration.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* `features/taxonomy` domain declaration. As of A2 (taxonomy plan) it imports nothing from its own
|
|
52
|
+
* `write-service.ts` — `contentType` is published as an open string (see `CONTENT_TYPE_SCHEMA`
|
|
53
|
+
* below), since Collections entries widened the set of legal values past the fixed `post`/`page`
|
|
54
|
+
* allow-list this file used to mirror as an `enum`.
|
|
54
55
|
*/
|
|
55
56
|
export type AgentToolSideEffect = "none" | "mutates-durable-state" | "mints-token";
|
|
56
57
|
export type AgentToolActorClassRule = "confirmer-must-equal-own-delegatedBy" | "user-only" | "none";
|
|
@@ -69,7 +70,7 @@ export interface AgentToolDefinition {
|
|
|
69
70
|
*/
|
|
70
71
|
inputSchema?: Readonly<Record<string, unknown>>;
|
|
71
72
|
}
|
|
72
|
-
/** The Taxonomy domain's fixed agent-tool catalog:
|
|
73
|
+
/** The Taxonomy domain's fixed agent-tool catalog: 7 wired (1 read, 5 ordinary writes, 1 gated-plan
|
|
73
74
|
* read) + 1 declared-but-never-wired destructive tool — see this file's header for the full
|
|
74
75
|
* `mergeTerm` safety analysis. */
|
|
75
76
|
export declare const taxonomyAgentToolCatalog: AgentToolDefinition[];
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
* `ToolRegistration`s.
|
|
49
49
|
*
|
|
50
50
|
* Architectural role:
|
|
51
|
-
* `features/taxonomy` domain declaration.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* `features/taxonomy` domain declaration. As of A2 (taxonomy plan) it imports nothing from its own
|
|
52
|
+
* `write-service.ts` — `contentType` is published as an open string (see `CONTENT_TYPE_SCHEMA`
|
|
53
|
+
* below), since Collections entries widened the set of legal values past the fixed `post`/`page`
|
|
54
|
+
* allow-list this file used to mirror as an `enum`.
|
|
54
55
|
*/
|
|
55
|
-
import { TAXONOMY_ALLOWED_CONTENT_TYPES } from "./write-service.js";
|
|
56
56
|
const TAXONOMY_ID_SCHEMA = {
|
|
57
57
|
type: "string",
|
|
58
58
|
minLength: 1,
|
|
@@ -63,12 +63,17 @@ const TERM_ID_SCHEMA = {
|
|
|
63
63
|
minLength: 1,
|
|
64
64
|
description: "A term id, as returned by taxonomy_create_term or taxonomy_list.",
|
|
65
65
|
};
|
|
66
|
+
/** A2 (taxonomy plan) — an open string, not an `enum` of `TAXONOMY_ALLOWED_CONTENT_TYPES`. `post`
|
|
67
|
+
* and `page` are always eligible; a Collection key (e.g. `'recipes'`) is eligible when that
|
|
68
|
+
* Collection's own content-type policy allows it — a call the write-service's
|
|
69
|
+
* `contentTypeTaxonomyPolicy` makes, not this schema. An `enum` here would reject a valid
|
|
70
|
+
* Collection key before the request ever reached that check. */
|
|
66
71
|
const CONTENT_TYPE_SCHEMA = {
|
|
67
72
|
type: "string",
|
|
68
|
-
|
|
69
|
-
description: "
|
|
73
|
+
minLength: 1,
|
|
74
|
+
description: "'post', 'page', or a Collection key (entries of that Collection).",
|
|
70
75
|
};
|
|
71
|
-
/** The Taxonomy domain's fixed agent-tool catalog:
|
|
76
|
+
/** The Taxonomy domain's fixed agent-tool catalog: 7 wired (1 read, 5 ordinary writes, 1 gated-plan
|
|
72
77
|
* read) + 1 declared-but-never-wired destructive tool — see this file's header for the full
|
|
73
78
|
* `mergeTerm` safety analysis. */
|
|
74
79
|
export const taxonomyAgentToolCatalog = [
|
|
@@ -128,8 +133,8 @@ export const taxonomyAgentToolCatalog = [
|
|
|
128
133
|
{
|
|
129
134
|
name: "taxonomy_assign_terms",
|
|
130
135
|
description: "Assigns one or more existing terms to a piece of content (the same <TermPicker> operation the Collections editor and the Categories & " +
|
|
131
|
-
"Tags screen both use).
|
|
132
|
-
"
|
|
136
|
+
"Tags screen both use). This call ADDS assignments; calling it twice with the same term is a no-op (idempotent). Use " +
|
|
137
|
+
"taxonomy_unassign_terms to remove an assignment. Every termId is validated (must exist; its taxonomy must be applicable to contentType) " +
|
|
133
138
|
"before ANY row is written, so a bad id in the list rejects the whole call rather than partially assigning.",
|
|
134
139
|
sideEffects: "mutates-durable-state",
|
|
135
140
|
authorization: { permission: "admin.taxonomy.manage" },
|
|
@@ -139,11 +144,29 @@ export const taxonomyAgentToolCatalog = [
|
|
|
139
144
|
required: ["contentType", "contentId", "termIds"],
|
|
140
145
|
properties: {
|
|
141
146
|
contentType: CONTENT_TYPE_SCHEMA,
|
|
142
|
-
contentId: { type: "string", minLength: 1, description: "The id of the
|
|
147
|
+
contentId: { type: "string", minLength: 1, description: "The id of the content row to assign terms to." },
|
|
143
148
|
termIds: { type: "array", items: { type: "string" }, description: "Term ids to assign. May be empty (a no-op)." },
|
|
144
149
|
},
|
|
145
150
|
},
|
|
146
151
|
},
|
|
152
|
+
{
|
|
153
|
+
name: "taxonomy_unassign_terms",
|
|
154
|
+
description: "Removes one or more previously-assigned terms from a piece of content. Idempotent — removing a term that isn't currently assigned is a " +
|
|
155
|
+
"no-op, not an error. Every termId is validated the same way taxonomy_assign_terms validates them (must exist; its taxonomy must be " +
|
|
156
|
+
"applicable to contentType) before anything is removed.",
|
|
157
|
+
sideEffects: "mutates-durable-state",
|
|
158
|
+
authorization: { permission: "admin.taxonomy.manage" },
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: "object",
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
required: ["contentType", "contentId", "termIds"],
|
|
163
|
+
properties: {
|
|
164
|
+
contentType: CONTENT_TYPE_SCHEMA,
|
|
165
|
+
contentId: { type: "string", minLength: 1, description: "The id of the content row to unassign terms from." },
|
|
166
|
+
termIds: { type: "array", items: { type: "string" }, description: "Term ids to unassign. May be empty (a no-op)." },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
147
170
|
{
|
|
148
171
|
name: "taxonomy_plan_merge_term",
|
|
149
172
|
description: "Previews merging one term into another: recomputes and returns the current overlap-loss disclosure (how many pieces of content are " +
|
|
@@ -42,4 +42,39 @@ export declare function createPostBackedContentLookup(deps: {
|
|
|
42
42
|
postRepo: ContentRecordLookupPort;
|
|
43
43
|
workspaceId: string;
|
|
44
44
|
}): ContentLookupPort;
|
|
45
|
+
/**
|
|
46
|
+
* A1 (taxonomy plan) — the single capability taxonomy needs from a host's Collections entry
|
|
47
|
+
* repository, narrowed the same way {@link ContentRecordLookupPort} narrows the post repo. The
|
|
48
|
+
* field is `type` (an entry's own column name), mapped to this port's `kind` by
|
|
49
|
+
* {@link createEntryBackedContentLookup} below — never renamed in the host's own repo just to
|
|
50
|
+
* satisfy this shape.
|
|
51
|
+
*/
|
|
52
|
+
export interface EntryRecordLookupPort {
|
|
53
|
+
findById(r: {
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
id: string;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
workspaceId: string;
|
|
58
|
+
type: string;
|
|
59
|
+
} | null>;
|
|
60
|
+
}
|
|
61
|
+
/** `ContentLookupPort` adapter backed by a host's Collections entry repository — the entries-table
|
|
62
|
+
* counterpart to `createPostBackedContentLookup` above, for every `contentType` that is a
|
|
63
|
+
* Collection key rather than `post`/`page`. */
|
|
64
|
+
export declare function createEntryBackedContentLookup(deps: {
|
|
65
|
+
entryRepo: EntryRecordLookupPort;
|
|
66
|
+
workspaceId: string;
|
|
67
|
+
}): ContentLookupPort;
|
|
68
|
+
/**
|
|
69
|
+
* Routes a `resolve()` call to the post repo for `post`/`page` (`isContentTypeOnAllowList`) and to
|
|
70
|
+
* the entry repo for every other `contentType` — a Collection key. `assignTerms`/`unassignTerms`'s
|
|
71
|
+
* `validateAssignmentTarget` (write-service.ts) already decides whether a non-post/page
|
|
72
|
+
* `contentType` is ELIGIBLE via `contentTypeTaxonomyPolicy` before calling this at all; this
|
|
73
|
+
* function only decides which table a resolvable id is looked up against.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createContentLookup(deps: {
|
|
76
|
+
postRepo: ContentRecordLookupPort;
|
|
77
|
+
entryRepo: EntryRecordLookupPort;
|
|
78
|
+
workspaceId: string;
|
|
79
|
+
}): ContentLookupPort;
|
|
45
80
|
//# sourceMappingURL=content-lookup.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isContentTypeOnAllowList } from "./write-service.js";
|
|
1
2
|
export function createPostBackedContentLookup(deps) {
|
|
2
3
|
return {
|
|
3
4
|
async resolve({ contentId }) {
|
|
@@ -6,4 +7,31 @@ export function createPostBackedContentLookup(deps) {
|
|
|
6
7
|
},
|
|
7
8
|
};
|
|
8
9
|
}
|
|
10
|
+
/** `ContentLookupPort` adapter backed by a host's Collections entry repository — the entries-table
|
|
11
|
+
* counterpart to `createPostBackedContentLookup` above, for every `contentType` that is a
|
|
12
|
+
* Collection key rather than `post`/`page`. */
|
|
13
|
+
export function createEntryBackedContentLookup(deps) {
|
|
14
|
+
return {
|
|
15
|
+
async resolve({ contentId }) {
|
|
16
|
+
const entry = await deps.entryRepo.findById({ workspaceId: deps.workspaceId, id: contentId });
|
|
17
|
+
return entry ? { workspaceId: entry.workspaceId, kind: entry.type } : null;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Routes a `resolve()` call to the post repo for `post`/`page` (`isContentTypeOnAllowList`) and to
|
|
23
|
+
* the entry repo for every other `contentType` — a Collection key. `assignTerms`/`unassignTerms`'s
|
|
24
|
+
* `validateAssignmentTarget` (write-service.ts) already decides whether a non-post/page
|
|
25
|
+
* `contentType` is ELIGIBLE via `contentTypeTaxonomyPolicy` before calling this at all; this
|
|
26
|
+
* function only decides which table a resolvable id is looked up against.
|
|
27
|
+
*/
|
|
28
|
+
export function createContentLookup(deps) {
|
|
29
|
+
const postLookup = createPostBackedContentLookup({ postRepo: deps.postRepo, workspaceId: deps.workspaceId });
|
|
30
|
+
const entryLookup = createEntryBackedContentLookup({ entryRepo: deps.entryRepo, workspaceId: deps.workspaceId });
|
|
31
|
+
return {
|
|
32
|
+
async resolve(params) {
|
|
33
|
+
return isContentTypeOnAllowList(params.contentType) ? postLookup.resolve(params) : entryLookup.resolve(params);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
9
37
|
//# sourceMappingURL=content-lookup.js.map
|
package/dist/taxonomy/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* has no imports at all: the ceremony is a parameter, not a dependency.
|
|
20
20
|
*/
|
|
21
21
|
export { TaxonomyNotApplicableError, WorkspaceMismatchError, ContentTypeMismatchError, TaxonomyNotHierarchicalError, ParentCrossTaxonomyError, TermNotFoundError, HierarchyCycleDetectedError, wouldCreateCycle, validateContentJoin, validateHierarchyAssignment, type TermTreeLookup, } from "./validation-chain.js";
|
|
22
|
-
export { TAXONOMY_ALLOWED_CONTENT_TYPES, isContentTypeOnAllowList, TaxonomyRecordNotFoundError, TermRecordNotFoundError, ContentRecordNotFoundError, TermHasAssignedContentError, TermHasChildTermsError, TaxonomyHasAssignedContentError, createTaxonomy, createTerm, renameTerm, assignTerms, deleteTerm, deleteTaxonomy, onContentDeleted, type AuthorizeFn, type Taxonomy, type Term, type TaxonomyRepoPort, type TermRepoPort, type EntryTermRepoPort, type ContentLookupPort, type TaxonomyRevisionRow, type TaxonomyRevisionRepoPort, type ClockPort, type IdGeneratorPort, type WriteServiceDeps, type CreateTaxonomyRequired, type CreateTermRequired, type RenameTermRequired, type AssignTermsRequired, type DeleteTermRequired, type DeleteTaxonomyRequired, type DeletableTaxonomyRepoPort, type DeletableTermRepoPort, type AssignmentCountEntryTermRepoPort, type TransactionalRepoPort, type EntryTermsCleanupPort, type OnContentDeletedRequired, } from "./write-service.js";
|
|
22
|
+
export { TAXONOMY_ALLOWED_CONTENT_TYPES, isContentTypeOnAllowList, TaxonomyRecordNotFoundError, TermRecordNotFoundError, ContentRecordNotFoundError, TermHasAssignedContentError, TermHasChildTermsError, TaxonomyHasAssignedContentError, createTaxonomy, createTerm, renameTerm, assignTerms, unassignTerms, deleteTerm, deleteTaxonomy, onContentDeleted, type AuthorizeFn, type Taxonomy, type Term, type TaxonomyRepoPort, type TermRepoPort, type EntryTermRepoPort, type ContentLookupPort, type ContentTypeTaxonomyPolicyPort, type UnassignableEntryTermRepoPort, type TaxonomyRevisionRow, type TaxonomyRevisionRepoPort, type ClockPort, type IdGeneratorPort, type WriteServiceDeps, type CreateTaxonomyRequired, type CreateTermRequired, type RenameTermRequired, type AssignTermsRequired, type UnassignTermsRequired, type DeleteTermRequired, type DeleteTaxonomyRequired, type DeletableTaxonomyRepoPort, type DeletableTermRepoPort, type AssignmentCountEntryTermRepoPort, type TransactionalRepoPort, type EntryTermsCleanupPort, type OnContentDeletedRequired, } from "./write-service.js";
|
|
23
23
|
export { listTaxonomiesWithTerms, type TaxonomyListPort, type TermListPort, type TaxonomyWithTerms, } from "./list.js";
|
|
24
24
|
/**
|
|
25
25
|
* The domain half of the `mergeTerm` ceremony — see this file's header for why the gateway itself
|
|
@@ -32,7 +32,7 @@ export { InMemoryTaxonomyRepo, InMemoryTermRepo, InMemoryEntryTermRepo, InMemory
|
|
|
32
32
|
* satisfy — see `content-lookup.ts` for why it is declared here rather than imported from a
|
|
33
33
|
* content feature.
|
|
34
34
|
*/
|
|
35
|
-
export { createPostBackedContentLookup, type ContentRecordLookupPort, } from "./content-lookup.js";
|
|
35
|
+
export { createPostBackedContentLookup, createEntryBackedContentLookup, createContentLookup, type ContentRecordLookupPort, type EntryRecordLookupPort, } from "./content-lookup.js";
|
|
36
36
|
/** The agent-tool surface for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
|
37
37
|
export { taxonomyAgentToolCatalog, type AgentToolDefinition as TaxonomyAgentToolDefinition, type AgentToolSideEffect as TaxonomyAgentToolSideEffect, type AgentToolActorClassRule as TaxonomyAgentToolActorClassRule, } from "./agent-tools.js";
|
|
38
38
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/taxonomy/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* has no imports at all: the ceremony is a parameter, not a dependency.
|
|
20
20
|
*/
|
|
21
21
|
export { TaxonomyNotApplicableError, WorkspaceMismatchError, ContentTypeMismatchError, TaxonomyNotHierarchicalError, ParentCrossTaxonomyError, TermNotFoundError, HierarchyCycleDetectedError, wouldCreateCycle, validateContentJoin, validateHierarchyAssignment, } from "./validation-chain.js";
|
|
22
|
-
export { TAXONOMY_ALLOWED_CONTENT_TYPES, isContentTypeOnAllowList, TaxonomyRecordNotFoundError, TermRecordNotFoundError, ContentRecordNotFoundError, TermHasAssignedContentError, TermHasChildTermsError, TaxonomyHasAssignedContentError, createTaxonomy, createTerm, renameTerm, assignTerms, deleteTerm, deleteTaxonomy, onContentDeleted, } from "./write-service.js";
|
|
22
|
+
export { TAXONOMY_ALLOWED_CONTENT_TYPES, isContentTypeOnAllowList, TaxonomyRecordNotFoundError, TermRecordNotFoundError, ContentRecordNotFoundError, TermHasAssignedContentError, TermHasChildTermsError, TaxonomyHasAssignedContentError, createTaxonomy, createTerm, renameTerm, assignTerms, unassignTerms, deleteTerm, deleteTaxonomy, onContentDeleted, } from "./write-service.js";
|
|
23
23
|
export { listTaxonomiesWithTerms, } from "./list.js";
|
|
24
24
|
/**
|
|
25
25
|
* The domain half of the `mergeTerm` ceremony — see this file's header for why the gateway itself
|
|
@@ -32,7 +32,7 @@ export { InMemoryTaxonomyRepo, InMemoryTermRepo, InMemoryEntryTermRepo, InMemory
|
|
|
32
32
|
* satisfy — see `content-lookup.ts` for why it is declared here rather than imported from a
|
|
33
33
|
* content feature.
|
|
34
34
|
*/
|
|
35
|
-
export { createPostBackedContentLookup, } from "./content-lookup.js";
|
|
35
|
+
export { createPostBackedContentLookup, createEntryBackedContentLookup, createContentLookup, } from "./content-lookup.js";
|
|
36
36
|
/** The agent-tool surface for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
|
37
37
|
export { taxonomyAgentToolCatalog, } from "./agent-tools.js";
|
|
38
38
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContentLookupPort, EntryTermRepoPort, TaxonomyRepoPort, TaxonomyRevisionRepoPort, TaxonomyRevisionRow, Term, TermRepoPort } from "./write-service.js";
|
|
1
|
+
import type { ContentLookupPort, EntryTermRepoPort, TaxonomyRepoPort, TaxonomyRevisionRepoPort, TaxonomyRevisionRow, Term, TermRepoPort, UnassignableEntryTermRepoPort } from "./write-service.js";
|
|
2
2
|
import type { Taxonomy } from "./write-service.js";
|
|
3
3
|
import type { TaxonomyListPort, TermListPort } from "./list.js";
|
|
4
4
|
/**
|
|
@@ -62,7 +62,7 @@ export declare class InMemoryTermRepo implements TermRepoPort, TermListPort {
|
|
|
62
62
|
parentId: string;
|
|
63
63
|
}): Promise<number>;
|
|
64
64
|
}
|
|
65
|
-
export declare class InMemoryEntryTermRepo implements EntryTermRepoPort {
|
|
65
|
+
export declare class InMemoryEntryTermRepo implements EntryTermRepoPort, UnassignableEntryTermRepoPort {
|
|
66
66
|
private readonly rows;
|
|
67
67
|
upsert(row: {
|
|
68
68
|
contentType: string;
|
|
@@ -94,6 +94,15 @@ export declare class InMemoryEntryTermRepo implements EntryTermRepoPort {
|
|
|
94
94
|
countByTerm(params: {
|
|
95
95
|
termId: string;
|
|
96
96
|
}): Promise<number>;
|
|
97
|
+
/** `UnassignableEntryTermRepoPort` — additive capability for `unassignTerms` (A2, taxonomy
|
|
98
|
+
* plan). Returns the number of rows actually removed (0 or 1, given `entry_terms_unique`'s
|
|
99
|
+
* `(contentType, contentId, termId)` uniqueness) rather than throwing on a no-op — mirrors
|
|
100
|
+
* `deleteByContent`'s own "return the removed count" convention. */
|
|
101
|
+
remove(row: {
|
|
102
|
+
contentType: string;
|
|
103
|
+
contentId: string;
|
|
104
|
+
termId: string;
|
|
105
|
+
}): Promise<number>;
|
|
97
106
|
}
|
|
98
107
|
export declare class InMemoryTaxonomyRevisionRepo implements TaxonomyRevisionRepoPort {
|
|
99
108
|
readonly rows: TaxonomyRevisionRow[];
|
|
@@ -123,6 +123,17 @@ export class InMemoryEntryTermRepo {
|
|
|
123
123
|
async countByTerm(params) {
|
|
124
124
|
return this.rows.filter((r) => r.termId === params.termId).length;
|
|
125
125
|
}
|
|
126
|
+
/** `UnassignableEntryTermRepoPort` — additive capability for `unassignTerms` (A2, taxonomy
|
|
127
|
+
* plan). Returns the number of rows actually removed (0 or 1, given `entry_terms_unique`'s
|
|
128
|
+
* `(contentType, contentId, termId)` uniqueness) rather than throwing on a no-op — mirrors
|
|
129
|
+
* `deleteByContent`'s own "return the removed count" convention. */
|
|
130
|
+
async remove(row) {
|
|
131
|
+
const before = this.rows.length;
|
|
132
|
+
const remaining = this.rows.filter((r) => !(r.contentType === row.contentType && r.contentId === row.contentId && r.termId === row.termId));
|
|
133
|
+
this.rows.length = 0;
|
|
134
|
+
this.rows.push(...remaining);
|
|
135
|
+
return before - this.rows.length;
|
|
136
|
+
}
|
|
126
137
|
}
|
|
127
138
|
export class InMemoryTaxonomyRevisionRepo {
|
|
128
139
|
rows = [];
|
|
@@ -102,10 +102,20 @@ export interface ContentLookupPort {
|
|
|
102
102
|
kind: string;
|
|
103
103
|
} | null>;
|
|
104
104
|
}
|
|
105
|
+
/** ADR-044: the content_types registry's per-type taxonomy allow-list for Collections entries
|
|
106
|
+
* (parallel to, never folded into, TAXONOMY_ALLOWED_CONTENT_TYPES — see the decision note).
|
|
107
|
+
* `null` = not a live registry type (not applicable). */
|
|
108
|
+
export interface ContentTypeTaxonomyPolicyPort {
|
|
109
|
+
taxonomiesFor(params: {
|
|
110
|
+
contentType: string;
|
|
111
|
+
}): Promise<"all" | ReadonlySet<string> | null>;
|
|
112
|
+
}
|
|
105
113
|
/** The hardcoded post/page taxonomy allow-list (permanent, see
|
|
106
114
|
* `docs/decisions/taxonomy-content-type-allow-list.md`) — every taxonomy is applicable to
|
|
107
|
-
* `post`/`page` content
|
|
108
|
-
*
|
|
115
|
+
* `post`/`page` content. This set is never grown to admit a Collection's content-type key; a
|
|
116
|
+
* Collection entry instead becomes eligible through {@link ContentTypeTaxonomyPolicyPort} above,
|
|
117
|
+
* an intentionally parallel (not layered) per-content-type policy carried by a host's own
|
|
118
|
+
* `content_types` registry. */
|
|
109
119
|
export declare const TAXONOMY_ALLOWED_CONTENT_TYPES: ReadonlySet<string>;
|
|
110
120
|
export declare function isContentTypeOnAllowList(contentType: string): boolean;
|
|
111
121
|
export interface TaxonomyRevisionRow {
|
|
@@ -146,6 +156,10 @@ export interface WriteServiceDeps {
|
|
|
146
156
|
/** Resolves a `(contentType, contentId)` pair's real workspace/kind for `assignTerms`'s
|
|
147
157
|
* content-join validation (Finding 1 fix). */
|
|
148
158
|
contentLookup: ContentLookupPort;
|
|
159
|
+
/** Per-content-type taxonomy allow-list for Collections entries (A1, taxonomy plan). Optional so
|
|
160
|
+
* other hosts (and every existing test double) don't break — a host that omits it simply never
|
|
161
|
+
* makes a non-post/page `contentType` resolvable, matching today's behavior exactly. */
|
|
162
|
+
contentTypeTaxonomyPolicy?: ContentTypeTaxonomyPolicyPort;
|
|
149
163
|
}
|
|
150
164
|
export declare class TaxonomyRecordNotFoundError extends Error {
|
|
151
165
|
constructor(message: string);
|
|
@@ -195,20 +209,35 @@ export interface AssignTermsRequired {
|
|
|
195
209
|
contentId: string;
|
|
196
210
|
termIds: string[];
|
|
197
211
|
}
|
|
198
|
-
/** AC-17/AC-20/INV-05/REQ-13/REQ-14 — upserts every `entry_terms` row (idempotent on-conflict per
|
|
199
|
-
* `entry_terms_unique`, EC-09), then stamps the watermark and enqueues the outbox event exactly
|
|
200
|
-
* ONCE per call regardless of `termIds.length` — never once per term. Never produces a
|
|
201
|
-
* `taxonomy_revisions` row (see this file's header for the disclosed narrowing this implements).
|
|
202
|
-
*
|
|
203
|
-
* Finding 1 fix (TM-adr041-043-044-045-audit-001): every `termId` is validated via
|
|
204
|
-
* `validateContentJoin` (allow-list -> workspace -> lens, `validation-chain.ts`'s fixed order)
|
|
205
|
-
* BEFORE any `entry_terms` row is written — every termId must resolve to a real term, and the
|
|
206
|
-
* target content must resolve to a real row whose kind matches the caller-supplied `contentType`.
|
|
207
|
-
* Validation runs for ALL termIds before ANY write, so a failure partway through never leaves a
|
|
208
|
-
* partial assignment. Content is resolved once per call (not once per term) since every term in
|
|
209
|
-
* one call shares the same `(contentType, contentId)` target.
|
|
210
|
-
*/
|
|
211
212
|
export declare function assignTerms(required: AssignTermsRequired, _optional?: Record<string, never>): Promise<void>;
|
|
213
|
+
/** A2 (taxonomy plan) — the removal half of `assignTerms`. `remove()` lives on an additive port
|
|
214
|
+
* (`UnassignableEntryTermRepoPort`) rather than widening the certified `EntryTermRepoPort`, the
|
|
215
|
+
* same precedent `AssignmentCountEntryTermRepoPort` already set for `deleteTerm`/`deleteTaxonomy`. */
|
|
216
|
+
export interface UnassignableEntryTermRepoPort {
|
|
217
|
+
remove(row: {
|
|
218
|
+
contentType: string;
|
|
219
|
+
contentId: string;
|
|
220
|
+
termId: string;
|
|
221
|
+
}): Promise<number>;
|
|
222
|
+
}
|
|
223
|
+
export interface UnassignTermsRequired {
|
|
224
|
+
deps: Omit<WriteServiceDeps, "entryTerms"> & {
|
|
225
|
+
entryTerms: EntryTermRepoPort & UnassignableEntryTermRepoPort;
|
|
226
|
+
};
|
|
227
|
+
principalId: string;
|
|
228
|
+
contentType: string;
|
|
229
|
+
contentId: string;
|
|
230
|
+
termIds: string[];
|
|
231
|
+
}
|
|
232
|
+
/** SPEC-018 REQ-07/REQ-13/REQ-14/REQ-17/REQ-22 — spec debt this codebase never built (the spec
|
|
233
|
+
* always named "assign/unassign tools"; only `assignTerms` shipped). Same
|
|
234
|
+
* authorize -> validateAssignmentTarget -> write -> stamp -> outbox chokepoint as `assignTerms`,
|
|
235
|
+
* and — like `assignTerms` — never produces a `taxonomy_revisions` row (REQ-13/INV-05's disclosed
|
|
236
|
+
* narrowing applies equally to removal). Idempotent: unassigning a row that isn't there is a no-op,
|
|
237
|
+
* not an error (`remove()`'s 0-vs-1 return is intentionally not surfaced to the caller). Chosen over
|
|
238
|
+
* a "set terms" replace call — see the taxonomy plan's "What the docs settle" #3 for why replace
|
|
239
|
+
* risks silently erasing a concurrent assignment. */
|
|
240
|
+
export declare function unassignTerms(required: UnassignTermsRequired, _optional?: Record<string, never>): Promise<void>;
|
|
212
241
|
export interface EntryTermsCleanupPort {
|
|
213
242
|
deleteByContent(params: {
|
|
214
243
|
workspaceId: string;
|