@jini-ai/cms 0.3.11 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +47 -19
- package/dist/content-types/agent-tools.d.ts +3 -56
- package/dist/content-types/agent-tools.js +22 -6
- package/dist/content-types/cleanup.d.ts +18 -13
- package/dist/content-types/cleanup.js +16 -11
- package/dist/content-types/errors.d.ts +57 -24
- package/dist/content-types/errors.js +56 -39
- package/dist/content-types/field-defs.d.ts +8 -4
- package/dist/content-types/field-defs.js +19 -14
- package/dist/content-types/index-provisioning.d.ts +61 -47
- package/dist/content-types/index-provisioning.js +90 -66
- package/dist/content-types/index.d.ts +4 -3
- package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
- package/dist/content-types/lifecycle-dispatch.js +5 -4
- package/dist/content-types/lifecycle.d.ts +23 -18
- package/dist/content-types/lifecycle.js +71 -64
- package/dist/content-types/repo.memory.d.ts +5 -4
- package/dist/content-types/repo.memory.js +3 -2
- package/dist/content-types/tool-registrations.d.ts +6 -8
- package/dist/content-types/tool-registrations.js +24 -28
- package/dist/content-types/types.d.ts +17 -13
- package/dist/content-types/types.js +11 -5
- package/dist/content-types/write-service.d.ts +20 -22
- package/dist/content-types/write-service.js +78 -75
- package/dist/core/authorization.d.ts +15 -0
- package/dist/core/authorization.js +2 -0
- package/dist/core/commands/change-set.d.ts +15 -7
- package/dist/core/commands/command.d.ts +45 -26
- package/dist/core/commands/command.js +73 -63
- package/dist/core/entity-liveness.d.ts +11 -7
- package/dist/core/entity-liveness.js +5 -4
- package/dist/core/index.d.ts +12 -5
- package/dist/core/index.js +10 -4
- package/dist/core/ports.d.ts +25 -24
- package/dist/core/ports.js +1 -0
- package/dist/core/tools/args.d.ts +49 -0
- package/dist/core/tools/args.js +46 -0
- package/dist/core/tools/index.d.ts +3 -0
- package/dist/core/tools/index.js +3 -0
- package/dist/entries/agent-tools.d.ts +1 -73
- package/dist/entries/agent-tools.js +19 -13
- package/dist/entries/errors.d.ts +28 -14
- package/dist/entries/errors.js +17 -11
- package/dist/entries/field-validation.d.ts +5 -3
- package/dist/entries/field-validation.js +7 -5
- package/dist/entries/index.d.ts +2 -2
- package/dist/entries/lifecycle-dispatch.d.ts +5 -2
- package/dist/entries/lifecycle-dispatch.js +2 -1
- package/dist/entries/list.d.ts +4 -2
- package/dist/entries/list.js +4 -3
- package/dist/entries/repo.memory.d.ts +12 -10
- package/dist/entries/repo.memory.js +11 -10
- package/dist/entries/tool-registrations.d.ts +6 -8
- package/dist/entries/tool-registrations.js +42 -41
- package/dist/entries/types.d.ts +0 -7
- package/dist/entries/write-service.d.ts +28 -29
- package/dist/entries/write-service.js +114 -105
- package/dist/http/settings/cms-adapter.d.ts +20 -0
- package/dist/http/settings/cms-adapter.js +124 -0
- package/dist/http/settings/contracts.d.ts +135 -0
- package/dist/http/settings/contracts.js +2 -0
- package/dist/http/settings/http.d.ts +10 -0
- package/dist/http/settings/http.js +211 -0
- package/dist/http/settings/index.d.ts +5 -0
- package/dist/http/settings/index.js +4 -0
- package/dist/http/settings/shared.d.ts +68 -0
- package/dist/http/settings/shared.js +54 -0
- package/dist/http/settings/stream.d.ts +24 -0
- package/dist/http/settings/stream.js +147 -0
- package/dist/media/agent-tools.d.ts +1 -66
- package/dist/media/agent-tools.js +5 -1
- package/dist/media/blob-gc-lock.d.ts +6 -2
- package/dist/media/blob-gc-lock.js +4 -2
- package/dist/media/blob-gc.d.ts +64 -51
- package/dist/media/blob-gc.js +62 -52
- package/dist/media/blob-key.d.ts +1 -1
- package/dist/media/content-type-sniffer.d.ts +3 -1
- package/dist/media/content-type-sniffer.js +1 -1
- package/dist/media/html-attributes.d.ts +9 -5
- package/dist/media/html-attributes.js +8 -6
- package/dist/media/image-transformer.js +1 -1
- package/dist/media/image-transformer.sharp.d.ts +6 -0
- package/dist/media/image-transformer.sharp.js +12 -6
- package/dist/media/import/fetch-image.d.ts +24 -0
- package/dist/media/import/fetch-image.js +137 -0
- package/dist/media/import/index.d.ts +3 -0
- package/dist/media/import/index.js +3 -0
- package/dist/media/import/ports.d.ts +38 -0
- package/dist/media/index.d.ts +7 -1
- package/dist/media/index.js +1 -0
- package/dist/media/media-service.d.ts +14 -10
- package/dist/media/media-service.js +57 -52
- package/dist/media/ports.d.ts +3 -2
- package/dist/media/rendition-service.d.ts +3 -3
- package/dist/media/rendition-service.js +38 -37
- package/dist/media/repo.memory.d.ts +18 -7
- package/dist/media/repo.memory.js +12 -6
- package/dist/media/tool-registrations.d.ts +16 -15
- package/dist/media/tool-registrations.js +84 -72
- package/dist/media/transform-lock.d.ts +22 -12
- package/dist/media/transform-lock.js +14 -13
- package/dist/media/transform-registry.d.ts +3 -3
- package/dist/media/transform-registry.js +19 -18
- package/dist/media/transform-types.d.ts +8 -3
- package/dist/media/transform-types.js +10 -5
- package/dist/media/types.d.ts +21 -4
- package/dist/media/types.js +15 -2
- package/dist/navigation/agent-tools.d.ts +1 -80
- package/dist/navigation/agent-tools.js +15 -3
- package/dist/navigation/contracts.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/menu-service.d.ts +57 -40
- package/dist/navigation/menu-service.js +80 -69
- package/dist/navigation/ports.d.ts +1 -1
- package/dist/navigation/read-model.d.ts +3 -1
- package/dist/navigation/read-model.js +2 -2
- package/dist/navigation/reconcile.d.ts +2 -2
- package/dist/navigation/reconcile.js +2 -1
- package/dist/navigation/repo.memory.d.ts +7 -3
- package/dist/navigation/repo.memory.js +4 -2
- package/dist/navigation/resolver.d.ts +4 -1
- package/dist/navigation/resolver.js +1 -1
- package/dist/navigation/tool-registrations.d.ts +6 -8
- package/dist/navigation/tool-registrations.js +44 -42
- package/dist/navigation/types.d.ts +1 -1
- package/dist/presentation/presentation.d.ts +11 -3
- package/dist/presentation/presentation.js +13 -6
- package/dist/presentation/repo.memory.d.ts +6 -2
- package/dist/presentation/repo.memory.js +3 -2
- package/dist/settings/agent-tools.d.ts +2 -75
- package/dist/settings/agent-tools.js +49 -45
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +154 -0
- package/dist/settings/agent-writable-preferences.js +8 -20
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +11 -2
- package/dist/settings/change-feed.js +4 -4
- package/dist/settings/definitions-dispatch.d.ts +8 -3
- package/dist/settings/definitions-dispatch.js +5 -5
- package/dist/settings/dictionaries/index.d.ts +5 -2
- package/dist/settings/dictionaries/index.js +2 -2
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
- package/dist/settings/ensure-definitions.d.ts +5 -5
- package/dist/settings/errors.d.ts +7 -1
- package/dist/settings/errors.js +7 -1
- package/dist/settings/index.d.ts +4 -1
- package/dist/settings/index.js +1 -0
- package/dist/settings/ports.d.ts +6 -5
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +5 -4
- package/dist/settings/purge-service.js +3 -2
- package/dist/settings/settings.d.ts +22 -7
- package/dist/settings/settings.js +47 -26
- package/dist/settings/tool-registrations.d.ts +27 -32
- package/dist/settings/tool-registrations.js +66 -56
- package/dist/settings/types.d.ts +1 -1
- package/dist/settings/ui-tab-definitions.d.ts +1 -1
- package/dist/settings/write-service.d.ts +32 -26
- package/dist/settings/write-service.js +26 -20
- package/dist/taxonomy/agent-tools.d.ts +1 -66
- package/dist/taxonomy/agent-tools.js +25 -15
- package/dist/taxonomy/content-lookup.js +1 -1
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/merge-term.d.ts +7 -4
- package/dist/taxonomy/merge-term.js +7 -5
- package/dist/taxonomy/repo.memory.d.ts +42 -19
- package/dist/taxonomy/repo.memory.js +14 -12
- package/dist/taxonomy/validation-chain.d.ts +39 -18
- package/dist/taxonomy/validation-chain.js +35 -25
- package/dist/taxonomy/write-service.d.ts +103 -44
- package/dist/taxonomy/write-service.js +221 -179
- package/dist/trash/adapters/record-store.d.ts +52 -0
- package/dist/trash/adapters/record-store.js +36 -0
- package/dist/trash/cursor.d.ts +20 -0
- package/dist/trash/cursor.js +36 -0
- package/dist/trash/follow-ups.d.ts +76 -0
- package/dist/trash/follow-ups.js +46 -0
- package/dist/trash/index.d.ts +8 -0
- package/dist/trash/index.js +8 -0
- package/dist/trash/ports.d.ts +308 -0
- package/dist/trash/ports.js +2 -0
- package/dist/trash/repo.memory.d.ts +50 -0
- package/dist/trash/repo.memory.js +114 -0
- package/dist/trash/sweeper.d.ts +77 -0
- package/dist/trash/sweeper.js +127 -0
- package/dist/trash/write-service.d.ts +100 -0
- package/dist/trash/write-service.js +242 -0
- package/dist/workspace/agent-tools.d.ts +1 -59
- package/dist/workspace/agent-tools.js +27 -12
- package/dist/workspace/create.d.ts +31 -6
- package/dist/workspace/create.js +18 -5
- package/dist/workspace/delete.d.ts +18 -13
- package/dist/workspace/delete.js +20 -12
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/repo.memory.d.ts +19 -4
- package/dist/workspace/repo.memory.js +26 -4
- package/dist/workspace/tool-registrations.d.ts +2 -2
- package/dist/workspace/tool-registrations.js +13 -13
- package/dist/workspace/update.d.ts +6 -5
- package/dist/workspace/update.js +9 -8
- package/package.json +44 -38
- package/dist/core/tools/registration-kit.d.ts +0 -346
- package/dist/core/tools/registration-kit.js +0 -426
- package/dist/identity/admin-crud-service.d.ts +0 -280
- package/dist/identity/admin-crud-service.js +0 -468
- package/dist/identity/agent-tool-input.d.ts +0 -58
- package/dist/identity/agent-tool-input.js +0 -126
- package/dist/identity/agent-tools.d.ts +0 -101
- package/dist/identity/agent-tools.js +0 -290
- package/dist/identity/auth-service.d.ts +0 -118
- package/dist/identity/auth-service.js +0 -170
- package/dist/identity/authorize.d.ts +0 -90
- package/dist/identity/authorize.js +0 -94
- package/dist/identity/grant-service.d.ts +0 -198
- package/dist/identity/grant-service.js +0 -361
- package/dist/identity/hasher.d.ts +0 -39
- package/dist/identity/hasher.js +0 -39
- package/dist/identity/index.d.ts +0 -62
- package/dist/identity/index.js +0 -54
- package/dist/identity/password-policy.d.ts +0 -57
- package/dist/identity/password-policy.js +0 -66
- package/dist/identity/permission-migrations.d.ts +0 -86
- package/dist/identity/permission-migrations.js +0 -67
- package/dist/identity/permissions.d.ts +0 -35
- package/dist/identity/permissions.js +0 -603
- package/dist/identity/ports.d.ts +0 -201
- package/dist/identity/repo.memory.d.ts +0 -164
- package/dist/identity/repo.memory.js +0 -224
- package/dist/identity/seed.d.ts +0 -54
- package/dist/identity/seed.js +0 -429
- package/dist/identity/tool-registrations.d.ts +0 -50
- package/dist/identity/tool-registrations.js +0 -419
- package/dist/identity/types.d.ts +0 -185
- package/dist/identity/types.js +0 -64
- package/dist/identity/username.d.ts +0 -12
- package/dist/identity/username.js +0 -14
- package/dist/server/index.d.ts +0 -28
- package/dist/server/index.js +0 -28
- /package/dist/{identity → media/import}/ports.js +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
1
2
|
/**
|
|
2
3
|
* @file Write-service for the `navigation` library.
|
|
3
4
|
*
|
|
@@ -9,14 +10,14 @@
|
|
|
9
10
|
*
|
|
10
11
|
* How it relates to the project:
|
|
11
12
|
* - Storage is `MenuRepoPort` (`repo.memory.ts`) — a self-contained repo, not
|
|
12
|
-
*
|
|
13
|
+
* the generic entries repo (see `repo.memory.ts`'s file header for why).
|
|
13
14
|
* - Location assignment additionally writes `NavLocationBindingRepoPort`
|
|
14
|
-
*
|
|
15
|
+
* (`ports.ts`), the one real port this library declares.
|
|
15
16
|
* - In production this whole module runs inside the command
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* gateway/write chokepoint (same-transaction revision, attribution,
|
|
18
|
+
* `entry_refs` extraction). That gateway is not implemented as running code
|
|
19
|
+
* yet, so these functions are the chokepoint's future *contents* called
|
|
20
|
+
* directly for now.
|
|
20
21
|
*
|
|
21
22
|
* Architectural role:
|
|
22
23
|
* Feature logic only. No Express/route code, no direct SQL — everything goes
|
|
@@ -28,7 +29,7 @@ import { NAV_DOC_TYPE, } from "./types.js";
|
|
|
28
29
|
// Outbox event publication — each mutating function below
|
|
29
30
|
// enqueues its matching NAVIGATION_EVENTS entry after its repo write(s)
|
|
30
31
|
// succeed, never on a rejection path. Mirrors the host's proven
|
|
31
|
-
// `outbox.enqueue
|
|
32
|
+
// `outbox.enqueue` call shape.
|
|
32
33
|
// ---------------------------------------------------------------------------
|
|
33
34
|
/**
|
|
34
35
|
* `payload` is typed as a plain `Record<string, unknown>` (not the specific
|
|
@@ -42,7 +43,7 @@ function buildEvent(required) {
|
|
|
42
43
|
return {
|
|
43
44
|
id: required.idGen.newId(),
|
|
44
45
|
name: required.name,
|
|
45
|
-
occurredAt: required.clock
|
|
46
|
+
occurredAt: kernelNowIso({ clock: required.clock }),
|
|
46
47
|
aggregateId: required.aggregateId,
|
|
47
48
|
workspaceId: required.workspaceId,
|
|
48
49
|
payload: required.payload,
|
|
@@ -52,10 +53,19 @@ function buildEvent(required) {
|
|
|
52
53
|
// Typed errors
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
export class MenuNotFoundError extends Error {
|
|
56
|
+
constructor({ message }, optionalArgs = {}) {
|
|
57
|
+
super(message, optionalArgs);
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
export class MenuValidationError extends Error {
|
|
61
|
+
constructor({ message }, optionalArgs = {}) {
|
|
62
|
+
super(message, optionalArgs);
|
|
63
|
+
}
|
|
57
64
|
}
|
|
58
65
|
export class MenuConflictError extends Error {
|
|
66
|
+
constructor({ message }, optionalArgs = {}) {
|
|
67
|
+
super(message, optionalArgs);
|
|
68
|
+
}
|
|
59
69
|
}
|
|
60
70
|
/**
|
|
61
71
|
* The 409-style purge rejection: hard delete is blocked while the menu is
|
|
@@ -65,8 +75,9 @@ export class MenuConflictError extends Error {
|
|
|
65
75
|
*/
|
|
66
76
|
export class MenuLocationBoundError extends MenuConflictError {
|
|
67
77
|
boundLocations;
|
|
68
|
-
constructor(
|
|
69
|
-
|
|
78
|
+
constructor(requiredArgs, optionalArgs = {}) {
|
|
79
|
+
const { message, boundLocations } = requiredArgs;
|
|
80
|
+
super({ message: message });
|
|
70
81
|
this.boundLocations = boundLocations;
|
|
71
82
|
}
|
|
72
83
|
}
|
|
@@ -83,17 +94,17 @@ const RESERVED_TARGET_KINDS = new Set(["dynamicQuery", "content"]);
|
|
|
83
94
|
/**
|
|
84
95
|
* Fixed placeholder origin {@link isAllowedHref} resolves a claimed same-origin-relative href
|
|
85
96
|
* against, to compare the RESOLVED origin rather than the raw string shape. Mirrors
|
|
86
|
-
*
|
|
97
|
+
* the host's public renderer's identically-named constant
|
|
87
98
|
* byte-for-byte — see that file's own header for why a fixed placeholder origin is sufficient (only
|
|
88
99
|
* the relationship between the resolved URL's origin and this one is ever inspected, never the
|
|
89
100
|
* placeholder value itself).
|
|
90
101
|
*/
|
|
91
|
-
const SAFE_HREF_RESOLUTION_BASE = "http://
|
|
102
|
+
const SAFE_HREF_RESOLUTION_BASE = "http://safehref.invalid/";
|
|
92
103
|
const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
93
104
|
/**
|
|
94
105
|
* The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
|
|
95
106
|
* `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
|
|
96
|
-
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth
|
|
107
|
+
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth** exported
|
|
97
108
|
* from this package's public surface (`navigation/index.ts` -> `@jini-ai/cms/navigation`) so the
|
|
98
109
|
* dependency runs the direction that is actually legal — the host already depends on `@jini-ai/cms`, not
|
|
99
110
|
* the reverse — rather than each consumer hand-copying the predicate.
|
|
@@ -104,14 +115,14 @@ const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
|
104
115
|
* `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
|
|
105
116
|
* `@jini-ai/cms/core` today, and `features/theme` already imports `@jini-ai/cms/core` too, so both
|
|
106
117
|
* call sites may legally import `@jini-ai/cms/navigation` directly — verified by grep, not assumed).
|
|
107
|
-
* Until that follow-up lands,
|
|
118
|
+
* Until that follow-up lands, the host's menu href allowlist sync check
|
|
108
119
|
* behaviorally gates the three copies against the same adversarial table so a divergence fails CI
|
|
109
120
|
* instead of drifting silently.
|
|
110
121
|
*
|
|
111
|
-
* Replaced (this change) a `URL_SCHEME_DENYLIST` (`startsWith` on a lowercased, `.trim
|
|
122
|
+
* Replaced (this change) a `URL_SCHEME_DENYLIST` (`startsWith` on a lowercased, `.trim`ed string)
|
|
112
123
|
* that failed open: the WHATWG `URL` parser strips TAB/LF/CR from ANYWHERE in the input and folds a
|
|
113
|
-
* leading backslash to `/` for `http(s)`, so `"java\tscript:alert
|
|
114
|
-
* `"file
|
|
124
|
+
* leading backslash to `/` for `http(s)`, so `"java\tscript:alert"`, `" javascript:alert"`,
|
|
125
|
+
* `"file:// etc/passwd"`, `"blob:…"`, `"about:blank"`, `"//evil.example"` (protocol-relative), and
|
|
115
126
|
* `"/\evil.example"` all resolved to a dangerous or off-origin target while never matching any
|
|
116
127
|
* `startsWith` prefix in the old list. An allowlist closes the whole bypass class at once (case,
|
|
117
128
|
* embedded control characters, and scheme are all irrelevant to an allowlist the same way) rather
|
|
@@ -126,11 +137,11 @@ const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
|
126
137
|
* hrefs written through the host's admin editor.
|
|
127
138
|
*
|
|
128
139
|
* @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
|
|
129
|
-
*
|
|
140
|
+
* otherwise a valid input — this function performs no other precondition checks.
|
|
130
141
|
* @returns `true` when `rawHref` passes the allowlist, `false` otherwise.
|
|
131
|
-
* @complexity O
|
|
142
|
+
* @complexity O — a handful of string checks plus one `URL` construction on the `/…` branch.
|
|
132
143
|
*/
|
|
133
|
-
export function isAllowedHref(rawHref) {
|
|
144
|
+
export function isAllowedHref({ rawHref }, _optional = {}) {
|
|
134
145
|
const href = rawHref.trim();
|
|
135
146
|
if (href.startsWith("#"))
|
|
136
147
|
return true;
|
|
@@ -159,51 +170,51 @@ export const ALLOWED_HREF_SHAPES_DESCRIPTION = "an in-page anchor ('#...'), a sa
|
|
|
159
170
|
*
|
|
160
171
|
* Checks (total/bounded validation + target integrity):
|
|
161
172
|
* - `items` (and every `children` array) is actually an array, and every node
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
173
|
+
* in it is a non-null object — untrusted JSON bodies can carry `null`
|
|
174
|
+
* entries or a non-array `items`/`children`, which must reject as a
|
|
175
|
+
* `MenuValidationError`, not throw an uncaught `TypeError`;
|
|
165
176
|
* - every node has a non-empty `id`, unique across the whole tree (id
|
|
166
|
-
*
|
|
167
|
-
*
|
|
177
|
+
* stability is a caller responsibility — see `updateMenuTree` doc for the
|
|
178
|
+
* simplification this build accepts);
|
|
168
179
|
* - nesting depth stays within `maxDepth`;
|
|
169
180
|
* - total item count stays within `maxItemCount`;
|
|
170
181
|
* - every node has a `target` object (a missing/`null` `target` rejects
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
182
|
+
* rather than crashing) whose `kind` is one of the four v1 kinds — reserved
|
|
183
|
+
* kinds (`dynamicQuery`, `content`) are rejected with a clear "not yet"
|
|
184
|
+
* error rather than silently accepted;
|
|
174
185
|
* - `url` targets require a non-empty string `href` and pass an origin/scheme
|
|
175
|
-
*
|
|
176
|
-
*
|
|
186
|
+
* ALLOWLIST (`#…`, same-origin `/…`, `http(s)://`, `mailto:` — see
|
|
187
|
+
* {@link isAllowedHref}), not a scheme denylist.
|
|
177
188
|
*
|
|
178
189
|
* @complexity O(n) over total node count for one full walk; no per-node
|
|
179
190
|
* backtracking. Space is O(n) for the cloned tree plus O(n) for the id set.
|
|
180
191
|
* @overallScore 100
|
|
181
192
|
*/
|
|
182
|
-
export function validateAndCloneTree(items, limits = {}) {
|
|
193
|
+
export function validateAndCloneTree({ items }, limits = {}) {
|
|
183
194
|
const maxDepth = limits.maxDepth ?? DEFAULT_MAX_TREE_DEPTH;
|
|
184
195
|
const maxItemCount = limits.maxItemCount ?? DEFAULT_MAX_ITEM_COUNT;
|
|
185
196
|
const seenIds = new Set();
|
|
186
197
|
let count = 0;
|
|
187
198
|
function walk(nodes, depth) {
|
|
188
199
|
if (!Array.isArray(nodes)) {
|
|
189
|
-
throw new MenuValidationError("menu items must be an array");
|
|
200
|
+
throw new MenuValidationError({ message: "menu items must be an array" });
|
|
190
201
|
}
|
|
191
202
|
if (depth > maxDepth) {
|
|
192
|
-
throw new MenuValidationError(`menu tree exceeds max nesting depth of ${maxDepth}`);
|
|
203
|
+
throw new MenuValidationError({ message: `menu tree exceeds max nesting depth of ${maxDepth}` });
|
|
193
204
|
}
|
|
194
205
|
return nodes.map((node) => {
|
|
195
206
|
count += 1;
|
|
196
207
|
if (count > maxItemCount) {
|
|
197
|
-
throw new MenuValidationError(`menu tree exceeds max item count of ${maxItemCount}`);
|
|
208
|
+
throw new MenuValidationError({ message: `menu tree exceeds max item count of ${maxItemCount}` });
|
|
198
209
|
}
|
|
199
210
|
if (!node || typeof node !== "object") {
|
|
200
|
-
throw new MenuValidationError("every menu item must be an object");
|
|
211
|
+
throw new MenuValidationError({ message: "every menu item must be an object" });
|
|
201
212
|
}
|
|
202
213
|
if (!node.id || !node.id.trim()) {
|
|
203
|
-
throw new MenuValidationError("every menu item requires a non-empty id");
|
|
214
|
+
throw new MenuValidationError({ message: "every menu item requires a non-empty id" });
|
|
204
215
|
}
|
|
205
216
|
if (seenIds.has(node.id)) {
|
|
206
|
-
throw new MenuValidationError(`duplicate item id '${node.id}' in menu tree`);
|
|
217
|
+
throw new MenuValidationError({ message: `duplicate item id '${node.id}' in menu tree` });
|
|
207
218
|
}
|
|
208
219
|
seenIds.add(node.id);
|
|
209
220
|
validateTarget(node.target);
|
|
@@ -221,30 +232,30 @@ export function validateAndCloneTree(items, limits = {}) {
|
|
|
221
232
|
*/
|
|
222
233
|
function validateTarget(target) {
|
|
223
234
|
if (!target || typeof target !== "object") {
|
|
224
|
-
throw new MenuValidationError("every menu item requires a target");
|
|
235
|
+
throw new MenuValidationError({ message: "every menu item requires a target" });
|
|
225
236
|
}
|
|
226
237
|
const kind = target.kind;
|
|
227
238
|
if (RESERVED_TARGET_KINDS.has(kind)) {
|
|
228
|
-
throw new MenuValidationError(`target kind '${kind}' is a reserved seam and is not supported yet`);
|
|
239
|
+
throw new MenuValidationError({ message: `target kind '${kind}' is a reserved seam and is not supported yet` });
|
|
229
240
|
}
|
|
230
241
|
if (!VALID_TARGET_KINDS.has(kind)) {
|
|
231
|
-
throw new MenuValidationError(`unknown target kind '${kind}'`);
|
|
242
|
+
throw new MenuValidationError({ message: `unknown target kind '${kind}'` });
|
|
232
243
|
}
|
|
233
244
|
if (kind === "url") {
|
|
234
245
|
const rawHref = target.href;
|
|
235
246
|
if (typeof rawHref !== "string" || !rawHref.trim()) {
|
|
236
|
-
throw new MenuValidationError("url target requires a non-empty href");
|
|
247
|
+
throw new MenuValidationError({ message: "url target requires a non-empty href" });
|
|
237
248
|
}
|
|
238
|
-
if (!isAllowedHref(rawHref)) {
|
|
239
|
-
throw new MenuValidationError(`url target href is not allowed: '${rawHref}'. Accepted shapes: ${ALLOWED_HREF_SHAPES_DESCRIPTION}.`);
|
|
249
|
+
if (!isAllowedHref({ rawHref: rawHref })) {
|
|
250
|
+
throw new MenuValidationError({ message: `url target href is not allowed: '${rawHref}'. Accepted shapes: ${ALLOWED_HREF_SHAPES_DESCRIPTION}.` });
|
|
240
251
|
}
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
254
|
function assertValidTitleAndSlug(title, slug) {
|
|
244
255
|
if (!title)
|
|
245
|
-
throw new MenuValidationError("title is required");
|
|
256
|
+
throw new MenuValidationError({ message: "title is required" });
|
|
246
257
|
if (!slug.match(/^[a-z0-9-]+$/)) {
|
|
247
|
-
throw new MenuValidationError("slug must use lowercase letters, numbers, and dashes");
|
|
258
|
+
throw new MenuValidationError({ message: "slug must use lowercase letters, numbers, and dashes" });
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
/**
|
|
@@ -269,8 +280,8 @@ export async function createMenu(required, optional = {}) {
|
|
|
269
280
|
assertValidTitleAndSlug(title, slug);
|
|
270
281
|
const duplicate = await deps.repo.findBySlug({ workspaceId: input.workspaceId, slug });
|
|
271
282
|
if (duplicate)
|
|
272
|
-
throw new MenuConflictError(`slug '${slug}' already exists`);
|
|
273
|
-
const items = validateAndCloneTree(input.items ?? [], optional.limits);
|
|
283
|
+
throw new MenuConflictError({ message: `slug '${slug}' already exists` });
|
|
284
|
+
const items = validateAndCloneTree({ items: input.items ?? [] }, optional.limits);
|
|
274
285
|
const menu = {
|
|
275
286
|
id: deps.idGen.newId(),
|
|
276
287
|
workspaceId: input.workspaceId,
|
|
@@ -279,7 +290,7 @@ export async function createMenu(required, optional = {}) {
|
|
|
279
290
|
status: "published",
|
|
280
291
|
doc: { type: NAV_DOC_TYPE, version: 1, items },
|
|
281
292
|
locations: [],
|
|
282
|
-
updatedAt: deps.clock
|
|
293
|
+
updatedAt: kernelNowIso({ clock: deps.clock }),
|
|
283
294
|
version: 1,
|
|
284
295
|
};
|
|
285
296
|
await deps.repo.save(menu);
|
|
@@ -304,7 +315,7 @@ export async function createMenu(required, optional = {}) {
|
|
|
304
315
|
* previous tree and is deferred — a caller-discipline requirement for now,
|
|
305
316
|
* not a runtime-enforced invariant.
|
|
306
317
|
*
|
|
307
|
-
* @complexity O(n) in the new tree size for validation; O
|
|
318
|
+
* @complexity O(n) in the new tree size for validation; O additional repo
|
|
308
319
|
* calls (one read, at most one slug-uniqueness read, one write).
|
|
309
320
|
* @overallScore 90
|
|
310
321
|
* @findings Medium: id-stability across edits (ids not renumbered on survive)
|
|
@@ -317,10 +328,10 @@ export async function updateMenuTree(required, optional = {}) {
|
|
|
317
328
|
const { deps, input } = required;
|
|
318
329
|
const existing = await deps.repo.findById({ workspaceId: input.workspaceId, id: input.id });
|
|
319
330
|
if (!existing)
|
|
320
|
-
throw new MenuNotFoundError(`menu '${input.id}' was not found`);
|
|
331
|
+
throw new MenuNotFoundError({ message: `menu '${input.id}' was not found` });
|
|
321
332
|
assertEntityLive({ entityType: "menu", entityId: input.id, state: existing.status === "trash" ? "trashed" : "live" });
|
|
322
333
|
if (existing.version !== input.expectedVersion) {
|
|
323
|
-
throw new MenuConflictError(`menu '${input.id}' was modified concurrently (expected version ${input.expectedVersion}, found ${existing.version})`);
|
|
334
|
+
throw new MenuConflictError({ message: `menu '${input.id}' was modified concurrently (expected version ${input.expectedVersion}, found ${existing.version})` });
|
|
324
335
|
}
|
|
325
336
|
const title = (input.title ?? existing.title).trim();
|
|
326
337
|
const slug = (input.slug ?? existing.slug).trim().toLowerCase();
|
|
@@ -328,16 +339,16 @@ export async function updateMenuTree(required, optional = {}) {
|
|
|
328
339
|
if (slug !== existing.slug) {
|
|
329
340
|
const duplicate = await deps.repo.findBySlug({ workspaceId: input.workspaceId, slug });
|
|
330
341
|
if (duplicate && duplicate.id !== existing.id) {
|
|
331
|
-
throw new MenuConflictError(`slug '${slug}' already exists`);
|
|
342
|
+
throw new MenuConflictError({ message: `slug '${slug}' already exists` });
|
|
332
343
|
}
|
|
333
344
|
}
|
|
334
|
-
const items = validateAndCloneTree(input.items, optional.limits);
|
|
345
|
+
const items = validateAndCloneTree({ items: input.items }, optional.limits);
|
|
335
346
|
const menu = {
|
|
336
347
|
...existing,
|
|
337
348
|
title,
|
|
338
349
|
slug,
|
|
339
350
|
doc: { type: NAV_DOC_TYPE, version: existing.doc.version, items },
|
|
340
|
-
updatedAt: deps.clock
|
|
351
|
+
updatedAt: kernelNowIso({ clock: deps.clock }),
|
|
341
352
|
version: existing.version + 1,
|
|
342
353
|
};
|
|
343
354
|
await deps.repo.save(menu);
|
|
@@ -372,7 +383,7 @@ export async function updateMenuTree(required, optional = {}) {
|
|
|
372
383
|
* transaction machinery — do not copy this sequencing pattern into code that
|
|
373
384
|
* has real transactions available.
|
|
374
385
|
*
|
|
375
|
-
* @complexity O
|
|
386
|
+
* @complexity O repo calls (bounded: at most two menu reads/writes plus one
|
|
376
387
|
* binding upsert), independent of workspace size.
|
|
377
388
|
* @overallScore 90
|
|
378
389
|
* @findings Medium: no rollback across the two writes (menu save, then
|
|
@@ -385,9 +396,9 @@ export async function assignLocation(required, _optional = {}) {
|
|
|
385
396
|
const { deps, input } = required;
|
|
386
397
|
const menu = await deps.repo.findById({ workspaceId: input.workspaceId, id: input.menuId });
|
|
387
398
|
if (!menu)
|
|
388
|
-
throw new MenuNotFoundError(`menu '${input.menuId}' was not found`);
|
|
399
|
+
throw new MenuNotFoundError({ message: `menu '${input.menuId}' was not found` });
|
|
389
400
|
assertEntityLive({ entityType: "menu", entityId: input.menuId, state: menu.status === "trash" ? "trashed" : "live" });
|
|
390
|
-
const now = deps.clock
|
|
401
|
+
const now = kernelNowIso({ clock: deps.clock });
|
|
391
402
|
const existingBinding = await deps.bindingRepo.findByLocation({
|
|
392
403
|
workspaceId: input.workspaceId,
|
|
393
404
|
locationKey: input.locationKey,
|
|
@@ -447,17 +458,17 @@ export async function assignLocation(required, _optional = {}) {
|
|
|
447
458
|
/**
|
|
448
459
|
* Deletion ladder (mirrors the media library's):
|
|
449
460
|
* 1. First call on a non-trashed menu **soft-deletes** it (`status: 'trash'`,
|
|
450
|
-
*
|
|
451
|
-
* 2. A second call on an already-trashed menu attempts the **hard purge
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
461
|
+
* revisioned) and returns — no purge yet.
|
|
462
|
+
* 2. A second call on an already-trashed menu attempts the **hard purge** If
|
|
463
|
+
* the menu is still bound to any location, purge is rejected with
|
|
464
|
+
* `MenuLocationBoundError` (409-style) listing the bound keys, unless
|
|
465
|
+
* `force` is set — permission-gating that error behind
|
|
466
|
+
* `navigation.delete.force` is the caller's (gateway's) job, not this
|
|
467
|
+
* function's; this function only enforces the *shape* of the guard.
|
|
457
468
|
* 3. On a successful purge, the menu row and all of its location bindings are
|
|
458
|
-
*
|
|
469
|
+
* removed.
|
|
459
470
|
*
|
|
460
|
-
* @complexity O
|
|
471
|
+
* @complexity O repo calls; O(k) bindings scanned where k = locations this
|
|
461
472
|
* menu holds (small, bounded by registered locations).
|
|
462
473
|
* @overallScore 100
|
|
463
474
|
*/
|
|
@@ -465,12 +476,12 @@ export async function deleteMenu(required, _optional = {}) {
|
|
|
465
476
|
const { deps, input } = required;
|
|
466
477
|
const existing = await deps.repo.findById({ workspaceId: input.workspaceId, id: input.id });
|
|
467
478
|
if (!existing)
|
|
468
|
-
throw new MenuNotFoundError(`menu '${input.id}' was not found`);
|
|
479
|
+
throw new MenuNotFoundError({ message: `menu '${input.id}' was not found` });
|
|
469
480
|
if (existing.status !== "trash") {
|
|
470
481
|
const trashed = {
|
|
471
482
|
...existing,
|
|
472
483
|
status: "trash",
|
|
473
|
-
updatedAt: deps.clock
|
|
484
|
+
updatedAt: kernelNowIso({ clock: deps.clock }),
|
|
474
485
|
version: existing.version + 1,
|
|
475
486
|
};
|
|
476
487
|
await deps.repo.save(trashed);
|
|
@@ -490,7 +501,7 @@ export async function deleteMenu(required, _optional = {}) {
|
|
|
490
501
|
});
|
|
491
502
|
if (bindings.length > 0 && !input.force) {
|
|
492
503
|
const boundLocations = bindings.map((row) => row.locationKey);
|
|
493
|
-
throw new MenuLocationBoundError(`menu '${input.id}' is still bound to location(s): ${boundLocations.join(", ")} — unassign before purging, or use force`, boundLocations);
|
|
504
|
+
throw new MenuLocationBoundError({ message: `menu '${input.id}' is still bound to location(s): ${boundLocations.join(", ")} — unassign before purging, or use force`, boundLocations: boundLocations });
|
|
494
505
|
}
|
|
495
506
|
await deps.repo.remove({ workspaceId: input.workspaceId, id: input.id });
|
|
496
507
|
await deps.bindingRepo.removeByMenu({ workspaceId: input.workspaceId, menuId: input.id });
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* INTERFACES ONLY. No feature logic lives here.
|
|
24
24
|
*/
|
|
25
|
-
import type { ISODateTime, UUID } from "
|
|
25
|
+
import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
|
|
26
26
|
import type { NavLocationBindingRow, NavLocationDescriptor, NavLocationKey, NavMenuDoc, NavMenuEntry, ResolvedNav } from "./types.js";
|
|
27
27
|
/**
|
|
28
28
|
* Persistence for the derived `nav_location_bindings` index. Writes happen only inside the same transaction as the menu-entry
|
|
@@ -4,8 +4,10 @@ import { type ResolveTargetHrefFn } from "./resolver.js";
|
|
|
4
4
|
export interface NavMenuReadModelDeps {
|
|
5
5
|
menuRepo: MenuRepoPort;
|
|
6
6
|
bindingRepo: NavLocationBindingRepoPort;
|
|
7
|
+
}
|
|
8
|
+
export interface NavMenuReadModelOptions {
|
|
7
9
|
/** Overridable for tests / once a host's routing library lands; defaults to the honest placeholder documented above. */
|
|
8
10
|
resolveTargetHref?: ResolveTargetHrefFn | undefined;
|
|
9
11
|
}
|
|
10
|
-
export declare function createNavMenuReadModel(deps: NavMenuReadModelDeps): NavMenuReadModel;
|
|
12
|
+
export declare function createNavMenuReadModel(deps: NavMenuReadModelDeps, optional?: NavMenuReadModelOptions): NavMenuReadModel;
|
|
11
13
|
//# sourceMappingURL=read-model.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveForLocation } from "./resolver.js";
|
|
2
2
|
const DEFAULT_RESOLVE_TARGET_HREF = async () => null;
|
|
3
|
-
export function createNavMenuReadModel(deps) {
|
|
4
|
-
const resolveTargetHref =
|
|
3
|
+
export function createNavMenuReadModel(deps, optional = {}) {
|
|
4
|
+
const resolveTargetHref = optional.resolveTargetHref ?? DEFAULT_RESOLVE_TARGET_HREF;
|
|
5
5
|
return {
|
|
6
6
|
async getMenu(required) {
|
|
7
7
|
return deps.menuRepo.findById({ workspaceId: required.workspaceId, id: required.menuId });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Clock, UUID } from "@jini-ai/core/primitives";
|
|
2
2
|
import type { MenuRepoPort } from "./repo.memory.js";
|
|
3
3
|
import type { NavLocationBindingRepoPort } from "./ports.js";
|
|
4
4
|
/**
|
|
@@ -32,7 +32,7 @@ import type { NavLocationBindingRepoPort } from "./ports.js";
|
|
|
32
32
|
export interface RebuildNavLocationBindingsDeps {
|
|
33
33
|
menuRepo: MenuRepoPort;
|
|
34
34
|
bindingRepo: NavLocationBindingRepoPort;
|
|
35
|
-
clock:
|
|
35
|
+
clock: Clock;
|
|
36
36
|
workspaceId: UUID;
|
|
37
37
|
}
|
|
38
38
|
export interface RebuildNavLocationBindingsResult {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
1
2
|
/**
|
|
2
3
|
* Recompute the workspace's `nav_location_bindings` index from scratch from
|
|
3
4
|
* every menu's `.locations` field, and replace the stored index with it.
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
*/
|
|
15
16
|
export async function rebuildNavLocationBindings(deps) {
|
|
16
17
|
const menus = await deps.menuRepo.list({ workspaceId: deps.workspaceId });
|
|
17
|
-
const boundAt = deps.clock
|
|
18
|
+
const boundAt = kernelNowIso({ clock: deps.clock });
|
|
18
19
|
const byLocation = new Map();
|
|
19
20
|
for (const menu of menus) {
|
|
20
21
|
for (const locationKey of menu.locations) {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* `menu-service.ts`'s job) — these classes are dumb, uniqueness-enforcing
|
|
32
32
|
* collections.
|
|
33
33
|
*/
|
|
34
|
-
import type { UUID } from "
|
|
34
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
35
35
|
import type { NavLocationBindingRepoPort } from "./ports.js";
|
|
36
36
|
import type { NavLocationBindingRow, NavLocationKey, NavMenuEntry } from "./types.js";
|
|
37
37
|
/**
|
|
@@ -72,7 +72,9 @@ export interface MenuRepoPort {
|
|
|
72
72
|
*/
|
|
73
73
|
export declare class InMemoryMenuRepo implements MenuRepoPort {
|
|
74
74
|
private rows;
|
|
75
|
-
constructor(
|
|
75
|
+
constructor(requiredArgs: Record<string, never>, optionalArgs?: {
|
|
76
|
+
initialRows?: NavMenuEntry[];
|
|
77
|
+
});
|
|
76
78
|
findById(required: {
|
|
77
79
|
workspaceId: UUID;
|
|
78
80
|
id: UUID;
|
|
@@ -103,7 +105,9 @@ export declare class InMemoryMenuRepo implements MenuRepoPort {
|
|
|
103
105
|
*/
|
|
104
106
|
export declare class InMemoryNavLocationBindingRepo implements NavLocationBindingRepoPort {
|
|
105
107
|
private rows;
|
|
106
|
-
constructor(
|
|
108
|
+
constructor(requiredArgs: Record<string, never>, optionalArgs?: {
|
|
109
|
+
initialRows?: NavLocationBindingRow[];
|
|
110
|
+
});
|
|
107
111
|
findByLocation(required: {
|
|
108
112
|
workspaceId: UUID;
|
|
109
113
|
locationKey: NavLocationKey;
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export class InMemoryMenuRepo {
|
|
12
12
|
rows;
|
|
13
|
-
constructor(
|
|
13
|
+
constructor(requiredArgs, optionalArgs = {}) {
|
|
14
|
+
const { initialRows = [] } = optionalArgs;
|
|
14
15
|
this.rows = [...initialRows];
|
|
15
16
|
}
|
|
16
17
|
async findById(required) {
|
|
@@ -50,7 +51,8 @@ export class InMemoryMenuRepo {
|
|
|
50
51
|
*/
|
|
51
52
|
export class InMemoryNavLocationBindingRepo {
|
|
52
53
|
rows;
|
|
53
|
-
constructor(
|
|
54
|
+
constructor(requiredArgs, optionalArgs = {}) {
|
|
55
|
+
const { initialRows = [] } = optionalArgs;
|
|
54
56
|
this.rows = [...initialRows];
|
|
55
57
|
}
|
|
56
58
|
async findByLocation(required) {
|
|
@@ -54,7 +54,10 @@ export interface ResolvedTargetHref {
|
|
|
54
54
|
* `null` if it cannot be resolved at all (deleted target, or — for `termRef`
|
|
55
55
|
* today — no schema yet to resolve against).
|
|
56
56
|
*/
|
|
57
|
-
export type ResolveTargetHrefFn = (
|
|
57
|
+
export type ResolveTargetHrefFn = (required: {
|
|
58
|
+
target: NavTarget;
|
|
59
|
+
context: NavResolveContext;
|
|
60
|
+
}, optional?: Record<string, never>) => Promise<ResolvedTargetHref | null>;
|
|
58
61
|
export interface ResolveForLocationDeps {
|
|
59
62
|
menuRepo: MenuRepoPort;
|
|
60
63
|
bindingRepo: NavLocationBindingRepoPort;
|
|
@@ -105,7 +105,7 @@ async function resolveItem(node, context, resolveTargetHref) {
|
|
|
105
105
|
// code. termRef in particular has no compatible entry_refs schema today
|
|
106
106
|
// — a resolver with no term schema to
|
|
107
107
|
// consult returns null here, same as any other "cannot resolve" outcome.
|
|
108
|
-
const result = await resolveTargetHref(node.target, context);
|
|
108
|
+
const result = await resolveTargetHref({ target: node.target, context });
|
|
109
109
|
if (result === null) {
|
|
110
110
|
href = null;
|
|
111
111
|
available = false;
|
|
@@ -1,23 +1,21 @@
|
|
|
1
|
+
import type { IdGenerator } from "@jini-ai/core/primitives";
|
|
2
|
+
import type { Clock } from "@jini-ai/core/primitives";
|
|
1
3
|
import type { AuthorizeFn } from "../core/commands/command.js";
|
|
2
4
|
import type { OutboxPort } from "../core/ports.js";
|
|
3
|
-
import { type DerivedRiskByToolId, type ToolRegistration } from "
|
|
5
|
+
import { type DerivedRiskByToolId, type ToolRegistration } from "@jini-ai/core";
|
|
4
6
|
import type { NavLocationBindingRepoPort } from "./ports.js";
|
|
5
7
|
import type { MenuRepoPort } from "./repo.memory.js";
|
|
6
8
|
/**
|
|
7
9
|
* The exact slice of the route-deps bag Menus' tool handlers read. Declared structurally (rather
|
|
8
10
|
* than importing a host's own `RouteDeps`) so this module carries no back-edge into any host's
|
|
9
11
|
* composition root. A host's `server/routes/*` satisfies this structurally by passing its existing
|
|
10
|
-
* route-deps object
|
|
12
|
+
* route-deps object after binding the kernel clock/ID contracts.
|
|
11
13
|
*/
|
|
12
14
|
export interface MenusToolDeps {
|
|
13
15
|
authorize: AuthorizeFn;
|
|
14
16
|
workspaceId: string;
|
|
15
|
-
clock:
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
idGen: {
|
|
19
|
-
newId(): string;
|
|
20
|
-
};
|
|
17
|
+
clock: Clock;
|
|
18
|
+
idGen: IdGenerator;
|
|
21
19
|
outbox: OutboxPort;
|
|
22
20
|
menuRepo: MenuRepoPort;
|
|
23
21
|
navLocationBindingRepo: NavLocationBindingRepoPort;
|