@jini-ai/cms 0.3.10 → 0.4.0
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 +25 -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 +106 -45
- 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/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/{identity → media/import}/ports.js +0 -0
|
@@ -1,24 +1,27 @@
|
|
|
1
|
+
import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
1
2
|
/**
|
|
2
3
|
* Thrown when a command's idempotency key was already used in the workspace.
|
|
3
4
|
* Callers should re-read current state instead of retrying the mutation.
|
|
4
5
|
*/
|
|
5
6
|
export class DuplicateCommandError extends Error {
|
|
6
7
|
changeSetId;
|
|
7
|
-
constructor(
|
|
8
|
+
constructor(requiredArgs, optionalArgs = {}) {
|
|
9
|
+
const { message, changeSetId } = requiredArgs;
|
|
8
10
|
super(message);
|
|
9
11
|
this.changeSetId = changeSetId;
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
|
-
* Thrown when `authorize
|
|
14
|
-
* this to 403 `FORBIDDEN`. Raised before the idempotency check
|
|
15
|
+
* Thrown when `authorize` denies the caller. Routes map
|
|
16
|
+
* this to 403 `FORBIDDEN`. Raised before the idempotency check — a
|
|
15
17
|
* replayed command id from a denied caller therefore never produces
|
|
16
|
-
* `DuplicateCommandError` either
|
|
18
|
+
* `DuplicateCommandError` either.
|
|
19
|
+
* See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
17
20
|
*/
|
|
18
21
|
export class ForbiddenError extends Error {
|
|
19
22
|
permission;
|
|
20
23
|
reason;
|
|
21
|
-
constructor(message, permission, reason) {
|
|
24
|
+
constructor({ message, permission, reason }, _optional = {}) {
|
|
22
25
|
super(message);
|
|
23
26
|
this.permission = permission;
|
|
24
27
|
this.reason = reason;
|
|
@@ -47,26 +50,29 @@ function isIdempotencyKeyConflict(error) {
|
|
|
47
50
|
* If execute throws, nothing is recorded; if inverse capture throws, the
|
|
48
51
|
* mutation never runs.
|
|
49
52
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* With a working `mutation.rollback`, the feature mutation and its change-set
|
|
54
|
+
* record form one compensated unit of work : failures in version capture,
|
|
55
|
+
* record preparation or `changeSets.insert` after `execute` trigger rollback and
|
|
56
|
+
* rejection. When rollback is omitted, the gateway still
|
|
57
|
+
* rejects but cannot undo the applied feature mutation; a caller-owned outer
|
|
58
|
+
* transaction must supply atomicity. Record/event atomicity depends on the repo
|
|
59
|
+
* honoring its port contract.
|
|
55
60
|
*
|
|
56
|
-
* The `change-set.applied` event is passed
|
|
57
|
-
* separate `deps.outbox.enqueue
|
|
61
|
+
* The `change-set.applied` event is passed in `changeSets.insert`'s optional object, not a
|
|
62
|
+
* separate `deps.outbox.enqueue` call afterward — a durable adapter co-persists it inside the
|
|
58
63
|
* same transaction as the change-set record, so it can never land without a durable delivery
|
|
59
64
|
* record (or vice versa). This still does NOT cover the domain mutation itself
|
|
60
|
-
* (`mutation.execute
|
|
65
|
+
* (`mutation.execute` stays outside any shared transaction, covered only by the compensating
|
|
61
66
|
* rollback above), and it covers only this producer. See
|
|
62
67
|
* `docs/decisions/change-set-outbox-transaction-boundary.md` for the full rationale, including the
|
|
63
|
-
* other direct `OutboxPort.enqueue
|
|
68
|
+
* other direct `OutboxPort.enqueue` producers this decision deliberately leaves untouched.
|
|
69
|
+
* See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
64
70
|
*/
|
|
65
71
|
export async function executeCommand(required, _optional = {}) {
|
|
66
72
|
const { deps, command, mutation } = required;
|
|
67
73
|
// authorize() runs BEFORE the idempotency check, so
|
|
68
74
|
// a denied caller never learns whether a replayed command id previously
|
|
69
|
-
// succeeded (no DUPLICATE_COMMAND / changeSetId leak
|
|
75
|
+
// succeeded (no DUPLICATE_COMMAND / changeSetId leak). `permission`. See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
70
76
|
// and `authorize` are wired together or not at all (see ExecuteCommandDeps
|
|
71
77
|
// doc) — partial wiring is a programming error, not a silent allow/skip.
|
|
72
78
|
if (Boolean(deps.authorize) !== Boolean(command.permission)) {
|
|
@@ -82,7 +88,7 @@ export async function executeCommand(required, _optional = {}) {
|
|
|
82
88
|
entityId: mutation.entityId,
|
|
83
89
|
});
|
|
84
90
|
if (!authResult.allowed) {
|
|
85
|
-
throw new ForbiddenError(`principal '${command.actor.id}' is not authorized for '${command.permission}' (${authResult.reason})`, command.permission, authResult.reason);
|
|
91
|
+
throw new ForbiddenError({ message: `principal '${command.actor.id}' is not authorized for '${command.permission}' (${authResult.reason})`, permission: command.permission, reason: authResult.reason });
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
if (command.idempotencyKey) {
|
|
@@ -91,63 +97,68 @@ export async function executeCommand(required, _optional = {}) {
|
|
|
91
97
|
idempotencyKey: command.idempotencyKey,
|
|
92
98
|
});
|
|
93
99
|
if (existing) {
|
|
94
|
-
throw new DuplicateCommandError(`command with idempotency key '${command.idempotencyKey}' was already executed`, existing.id);
|
|
100
|
+
throw new DuplicateCommandError({ message: `command with idempotency key '${command.idempotencyKey}' was already executed`, changeSetId: existing.id });
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
const inversePayload = await mutation.captureInverse();
|
|
98
104
|
const result = await mutation.execute();
|
|
99
|
-
|
|
100
|
-
const now = deps.clock.nowIso();
|
|
101
|
-
const changeSetId = deps.idGen.newId();
|
|
102
|
-
// BR-04: the event rides into insert()'s third argument (see this function's doc comment) so a
|
|
103
|
-
// durable adapter co-persists it atomically with the record it belongs to. Built unconditionally
|
|
104
|
-
// (cheap, pure data) but only passed through when an outbox is actually wired.
|
|
105
|
-
const event = {
|
|
106
|
-
id: deps.idGen.newId(),
|
|
107
|
-
name: "change-set.applied",
|
|
108
|
-
occurredAt: now,
|
|
109
|
-
aggregateId: changeSetId,
|
|
110
|
-
workspaceId: command.workspaceId,
|
|
111
|
-
actorId: command.actor.id,
|
|
112
|
-
changeSetId,
|
|
113
|
-
payload: {
|
|
114
|
-
changeSetId,
|
|
115
|
-
entityType: mutation.entityType,
|
|
116
|
-
entityId: mutation.entityId,
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
// Unit of work: the change-set record (and, when an outbox is wired, its delivery event) must
|
|
120
|
-
// land together, or the feature mutation is rolled back (REQ-01 / EC-08). On the in-memory
|
|
121
|
-
// adapter this is a compensating restore via `mutation.rollback`; the SQLite adapter does this
|
|
122
|
-
// as one real transaction and the rollback becomes a no-op for this step.
|
|
105
|
+
let insertingRecord = false;
|
|
123
106
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
107
|
+
const entityVersionAtApply = mutation.captureEntityVersion?.({ result }) ?? undefined;
|
|
108
|
+
const now = kernelNowIso({ clock: deps.clock });
|
|
109
|
+
const changeSetId = deps.idGen.newId();
|
|
110
|
+
// the event rides into insert's optional object (see this function's doc comment) so a. See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
111
|
+
// durable adapter co-persists it atomically with the record it belongs to. Built unconditionally
|
|
112
|
+
// (cheap, pure data) but only passed through when an outbox is actually wired.
|
|
113
|
+
const event = {
|
|
114
|
+
id: deps.idGen.newId(),
|
|
115
|
+
name: "change-set.applied",
|
|
116
|
+
occurredAt: now,
|
|
117
|
+
aggregateId: changeSetId,
|
|
126
118
|
workspaceId: command.workspaceId,
|
|
127
119
|
actorId: command.actor.id,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
idempotencyKey: command.idempotencyKey,
|
|
131
|
-
intentRef: command.intentRef,
|
|
132
|
-
createdAt: now,
|
|
133
|
-
appliedAt: now,
|
|
134
|
-
}, [
|
|
135
|
-
{
|
|
136
|
-
id: deps.idGen.newId(),
|
|
120
|
+
changeSetId,
|
|
121
|
+
payload: {
|
|
137
122
|
changeSetId,
|
|
138
123
|
entityType: mutation.entityType,
|
|
139
124
|
entityId: mutation.entityId,
|
|
140
|
-
operation: mutation.operation,
|
|
141
|
-
inversePayload: inversePayload ?? undefined,
|
|
142
|
-
entityVersionAtApply,
|
|
143
|
-
position: 0,
|
|
144
125
|
},
|
|
145
|
-
|
|
126
|
+
};
|
|
127
|
+
// Unit of work: the change-set record (and, when an outbox is wired, its delivery event) must
|
|
128
|
+
// land together, or the feature mutation is rolled back. On the in-memory. See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
129
|
+
// adapter this is a compensating restore via `mutation.rollback`; a caller-owned transaction
|
|
130
|
+
// can instead cover the feature mutation and record together (see the rollback contract).
|
|
131
|
+
const itemId = deps.idGen.newId();
|
|
132
|
+
insertingRecord = true;
|
|
133
|
+
await deps.changeSets.insert({ record: {
|
|
134
|
+
id: changeSetId,
|
|
135
|
+
workspaceId: command.workspaceId,
|
|
136
|
+
actorId: command.actor.id,
|
|
137
|
+
status: "applied",
|
|
138
|
+
summary: command.summary,
|
|
139
|
+
idempotencyKey: command.idempotencyKey,
|
|
140
|
+
intentRef: command.intentRef,
|
|
141
|
+
createdAt: now,
|
|
142
|
+
appliedAt: now,
|
|
143
|
+
},
|
|
144
|
+
items: [
|
|
145
|
+
{
|
|
146
|
+
id: itemId,
|
|
147
|
+
changeSetId,
|
|
148
|
+
entityType: mutation.entityType,
|
|
149
|
+
entityId: mutation.entityId,
|
|
150
|
+
operation: mutation.operation,
|
|
151
|
+
inversePayload: inversePayload ?? undefined,
|
|
152
|
+
entityVersionAtApply,
|
|
153
|
+
position: 0,
|
|
154
|
+
},
|
|
155
|
+
] }, { event: deps.outbox ? event : undefined });
|
|
156
|
+
return { result, changeSetId };
|
|
146
157
|
}
|
|
147
158
|
catch (recordError) {
|
|
148
|
-
//
|
|
149
|
-
// mutation so
|
|
150
|
-
// original persist error; a rollback that itself throws is a harder failure
|
|
159
|
+
// the mutation applied but its record did not. Undo the. See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
160
|
+
// mutation so holds when rollback is supplied (no mutation without a record). See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
161
|
+
// We surface the original preparation/persist error; a rollback that itself throws is a harder failure
|
|
151
162
|
// that the SQLite transaction path is designed to remove.
|
|
152
163
|
await mutation.rollback?.();
|
|
153
164
|
// TOCTOU: the idempotency check above and this insert are separated by
|
|
@@ -157,17 +168,16 @@ export async function executeCommand(required, _optional = {}) {
|
|
|
157
168
|
// change set) rather than letting the raw driver error escape, so callers' existing
|
|
158
169
|
// `instanceof DuplicateCommandError` handling (409) catches this race the same as the
|
|
159
170
|
// already-covered non-racing replay.
|
|
160
|
-
if (command.idempotencyKey && isIdempotencyKeyConflict(recordError)) {
|
|
171
|
+
if (insertingRecord && command.idempotencyKey && isIdempotencyKeyConflict(recordError)) {
|
|
161
172
|
const winner = await deps.changeSets.findByIdempotencyKey({
|
|
162
173
|
workspaceId: command.workspaceId,
|
|
163
174
|
idempotencyKey: command.idempotencyKey,
|
|
164
175
|
});
|
|
165
176
|
if (winner) {
|
|
166
|
-
throw new DuplicateCommandError(`command with idempotency key '${command.idempotencyKey}' was already executed`, winner.id);
|
|
177
|
+
throw new DuplicateCommandError({ message: `command with idempotency key '${command.idempotencyKey}' was already executed`, changeSetId: winner.id });
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
180
|
throw recordError;
|
|
170
181
|
}
|
|
171
|
-
return { result, changeSetId };
|
|
172
182
|
}
|
|
173
183
|
//# sourceMappingURL=command.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ToolInputError } from "@jini-ai/core";
|
|
2
2
|
/**
|
|
3
|
-
* @file The write-side counterpart to `not-trashed.ts`'s read-side `notTrashed()` filter (
|
|
3
|
+
* @file The write-side counterpart to `not-trashed.ts`'s read-side `notTrashed()` filter (host
|
|
4
4
|
* `features/trash/not-trashed.ts`).
|
|
5
5
|
*
|
|
6
6
|
* Purpose:
|
|
@@ -11,13 +11,13 @@ import { ToolInputError } from "@jini-ai/core";
|
|
|
11
11
|
* This module is the one shared check every writer calls right after its own load, so the check
|
|
12
12
|
* exists exactly once.
|
|
13
13
|
*
|
|
14
|
-
* Why it lives in `@jini-ai/cms/core` rather than in a
|
|
15
|
-
*
|
|
16
|
-
* taxonomy) both need it, Jini cannot import anything
|
|
14
|
+
* Why it lives in `@jini-ai/cms/core` rather than in a host module or a Jini domain package:
|
|
15
|
+
* a host's writers (pages, SEO, redirects) and Jini's own domain writers (media, content-types,
|
|
16
|
+
* taxonomy) both need it, Jini cannot import anything a host owns, and duplicating the check per side
|
|
17
17
|
* reintroduces the drift this module exists to remove. `core` is the one place both reach.
|
|
18
18
|
*
|
|
19
19
|
* Why `EntityNotLiveError` extends `ToolInputError` rather than a plain `Error`: the daemon's tool
|
|
20
|
-
* executor already treats `ToolInputError` as caller-facing (see
|
|
20
|
+
* executor already treats `ToolInputError` as caller-facing (see the kernel registration kit's doc
|
|
21
21
|
* comment), so every domain's agent tool surfaces this error's message to the caller with no
|
|
22
22
|
* per-domain allowlist entry, and every HTTP route that already maps `ToolInputError`-family errors
|
|
23
23
|
* picks it up for free. A per-domain rule added to `contracts/core/model-facing-tool-errors.ts`
|
|
@@ -36,11 +36,15 @@ export type EntityLiveness = "live" | "trashed" | "tombstoned";
|
|
|
36
36
|
* the instance so a caller that wants to branch on them (rather than parse the message) can.
|
|
37
37
|
*/
|
|
38
38
|
export declare class EntityNotLiveError extends ToolInputError {
|
|
39
|
+
readonly code: "ENTITY_IN_TRASH" | "ENTITY_TOMBSTONED";
|
|
39
40
|
readonly entityType: string;
|
|
40
41
|
readonly entityId: string;
|
|
41
42
|
readonly state: "trashed" | "tombstoned";
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
constructor(requiredArgs: {
|
|
44
|
+
entityType: string;
|
|
45
|
+
entityId: string;
|
|
46
|
+
state: "trashed" | "tombstoned";
|
|
47
|
+
}, _optionalArgs?: Record<string, never>);
|
|
44
48
|
}
|
|
45
49
|
/**
|
|
46
50
|
* Throws {@link EntityNotLiveError} unless `required.state` is `"live"`. Call this immediately
|
|
@@ -5,12 +5,13 @@ import { ToolInputError } from "@jini-ai/core";
|
|
|
5
5
|
* the instance so a caller that wants to branch on them (rather than parse the message) can.
|
|
6
6
|
*/
|
|
7
7
|
export class EntityNotLiveError extends ToolInputError {
|
|
8
|
+
code;
|
|
8
9
|
entityType;
|
|
9
10
|
entityId;
|
|
10
11
|
state;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
super(buildEntityNotLiveMessage(entityType, entityId, state));
|
|
12
|
+
constructor(requiredArgs, _optionalArgs = {}) {
|
|
13
|
+
const { entityType, entityId, state } = requiredArgs;
|
|
14
|
+
super({ message: buildEntityNotLiveMessage(entityType, entityId, state) });
|
|
14
15
|
this.entityType = entityType;
|
|
15
16
|
this.entityId = entityId;
|
|
16
17
|
this.state = state;
|
|
@@ -33,6 +34,6 @@ function buildEntityNotLiveMessage(entityType, entityId, state) {
|
|
|
33
34
|
export function assertEntityLive(required) {
|
|
34
35
|
if (required.state === "live")
|
|
35
36
|
return;
|
|
36
|
-
throw new EntityNotLiveError(required.entityType, required.entityId, required.state);
|
|
37
|
+
throw new EntityNotLiveError({ entityType: required.entityType, entityId: required.entityId, state: required.state });
|
|
37
38
|
}
|
|
38
39
|
//# sourceMappingURL=entity-liveness.js.map
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
* kit every domain's agent-tool layer builds on. No Express, no `node:*`, no DOM, no concrete
|
|
9
9
|
* adapters.
|
|
10
10
|
*
|
|
11
|
-
* ## What belongs in
|
|
11
|
+
* ## What belongs in Node-bound domain entries
|
|
12
12
|
*
|
|
13
13
|
* Only the Node-bound half: concrete repository implementations (SQLite/Postgres), filesystem blob
|
|
14
|
-
* stores, and anything importing `node
|
|
14
|
+
* stores, image transformers, and anything importing `node:*` or a native dependency. These
|
|
15
|
+
* adapters depend on core; the reverse is a boundary violation. User-management tool registration
|
|
16
|
+
* composition belongs to its own server entry point.
|
|
17
|
+
*
|
|
18
|
+
* HTTP routing and request/response handling belong in domain HTTP entries, composed through
|
|
19
|
+
* `@jini-ai/http-kit` and `@jini-ai/server`. The source runtime's Express composition root stays
|
|
20
|
+
* out of this kernel: putting its routes here would reproduce the coupling described below.
|
|
15
21
|
*
|
|
16
22
|
* ## Why the split is drawn here, and why it is enforced by `exports` rather than convention
|
|
17
23
|
*
|
|
@@ -26,7 +32,7 @@
|
|
|
26
32
|
* A package's `exports` map is exactly the enforcement a single `src/` tree cannot provide: a
|
|
27
33
|
* consumer of `@jini-ai/cms/core` physically cannot reach a Node adapter, and `/core` physically
|
|
28
34
|
* cannot import a transport type, because the module resolver refuses it. Keep it that way — if a
|
|
29
|
-
* core module wants something from
|
|
35
|
+
* core module wants something from a Node-bound adapter, the dependency points the wrong way and the fix
|
|
30
36
|
* is a port (interface) in core, not a widened export.
|
|
31
37
|
*
|
|
32
38
|
* ## Why the barrel does not re-export `./commands/appliers`
|
|
@@ -38,9 +44,10 @@
|
|
|
38
44
|
* measured reason no domain could be extracted on its own. A per-entity applier registry belongs
|
|
39
45
|
* behind a registration call the host makes, not behind a kernel barrel.
|
|
40
46
|
*/
|
|
41
|
-
export type {
|
|
47
|
+
export type { DomainEvent, EventBusPort, OutboxRecord, OutboxPort, } from './ports.js';
|
|
42
48
|
export type { ChangeSetStatus, ChangeSetOperation, ChangeSetRecord, ChangeSetItemRecord, ChangeSetWithItems, ChangeSetRepoPort, } from './commands/change-set.js';
|
|
43
49
|
export * from './commands/command.js';
|
|
44
50
|
export * from './entity-liveness.js';
|
|
45
|
-
export * from './tools/
|
|
51
|
+
export * from './tools/args.js';
|
|
52
|
+
export type { AuthorizationPort, AuthorizationRequired, AuthorizationOptional } from './authorization.js';
|
|
46
53
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
* kit every domain's agent-tool layer builds on. No Express, no `node:*`, no DOM, no concrete
|
|
9
9
|
* adapters.
|
|
10
10
|
*
|
|
11
|
-
* ## What belongs in
|
|
11
|
+
* ## What belongs in Node-bound domain entries
|
|
12
12
|
*
|
|
13
13
|
* Only the Node-bound half: concrete repository implementations (SQLite/Postgres), filesystem blob
|
|
14
|
-
* stores, and anything importing `node
|
|
14
|
+
* stores, image transformers, and anything importing `node:*` or a native dependency. These
|
|
15
|
+
* adapters depend on core; the reverse is a boundary violation. User-management tool registration
|
|
16
|
+
* composition belongs to its own server entry point.
|
|
17
|
+
*
|
|
18
|
+
* HTTP routing and request/response handling belong in domain HTTP entries, composed through
|
|
19
|
+
* `@jini-ai/http-kit` and `@jini-ai/server`. The source runtime's Express composition root stays
|
|
20
|
+
* out of this kernel: putting its routes here would reproduce the coupling described below.
|
|
15
21
|
*
|
|
16
22
|
* ## Why the split is drawn here, and why it is enforced by `exports` rather than convention
|
|
17
23
|
*
|
|
@@ -26,7 +32,7 @@
|
|
|
26
32
|
* A package's `exports` map is exactly the enforcement a single `src/` tree cannot provide: a
|
|
27
33
|
* consumer of `@jini-ai/cms/core` physically cannot reach a Node adapter, and `/core` physically
|
|
28
34
|
* cannot import a transport type, because the module resolver refuses it. Keep it that way — if a
|
|
29
|
-
* core module wants something from
|
|
35
|
+
* core module wants something from a Node-bound adapter, the dependency points the wrong way and the fix
|
|
30
36
|
* is a port (interface) in core, not a widened export.
|
|
31
37
|
*
|
|
32
38
|
* ## Why the barrel does not re-export `./commands/appliers`
|
|
@@ -40,5 +46,5 @@
|
|
|
40
46
|
*/
|
|
41
47
|
export * from './commands/command.js';
|
|
42
48
|
export * from './entity-liveness.js';
|
|
43
|
-
export * from './tools/
|
|
49
|
+
export * from './tools/args.js';
|
|
44
50
|
//# sourceMappingURL=index.js.map
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -21,14 +21,7 @@
|
|
|
21
21
|
* Core/domain code depends on interfaces from this file only, never on concrete
|
|
22
22
|
* infrastructure SDKs or framework-specific implementations.
|
|
23
23
|
*/
|
|
24
|
-
|
|
25
|
-
export type ISODateTime = string;
|
|
26
|
-
export type JsonPrimitive = string | number | boolean | null;
|
|
27
|
-
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
|
28
|
-
export interface JsonObject {
|
|
29
|
-
[key: string]: JsonValue;
|
|
30
|
-
}
|
|
31
|
-
export type JsonArray = JsonValue[];
|
|
24
|
+
import type { UUID, ISODateTime } from "@jini-ai/core/primitives";
|
|
32
25
|
/**
|
|
33
26
|
* Immutable domain event emitted by synchronous command handlers and delivered
|
|
34
27
|
* asynchronously via the outbox pipeline.
|
|
@@ -73,19 +66,26 @@ export interface EventBusPort {
|
|
|
73
66
|
/** Publish a single event to subscribers. */
|
|
74
67
|
publish<TPayload>(event: DomainEvent<TPayload>): Promise<void>;
|
|
75
68
|
/** Publish multiple events in order. */
|
|
76
|
-
publishBatch<TPayload>(
|
|
69
|
+
publishBatch<TPayload>(required: {
|
|
70
|
+
events: Array<DomainEvent<TPayload>>;
|
|
71
|
+
}, optional?: Record<string, never>): Promise<void>;
|
|
77
72
|
/**
|
|
78
73
|
* Subscribe a handler to an event name.
|
|
79
74
|
* Returns an async unsubscriber function.
|
|
80
75
|
*/
|
|
81
|
-
subscribe<TPayload>(
|
|
76
|
+
subscribe<TPayload>(required: {
|
|
77
|
+
eventName: string;
|
|
78
|
+
handler: (event: DomainEvent<TPayload>) => Promise<void>;
|
|
79
|
+
}, optional?: Record<string, never>): Promise<() => Promise<void>>;
|
|
82
80
|
/**
|
|
83
81
|
* Subscribe a handler to every event published on this bus, regardless of name. Additive to
|
|
84
82
|
* `subscribe` — existing per-name callers are unaffected.
|
|
85
83
|
* The Integrations fan-out (`registerWebhookFanout`) is the first real consumer: it narrows
|
|
86
84
|
* by topic itself rather than requiring a fixed list of event names up front.
|
|
87
85
|
*/
|
|
88
|
-
subscribeAll(
|
|
86
|
+
subscribeAll(required: {
|
|
87
|
+
handler: (event: DomainEvent) => Promise<void>;
|
|
88
|
+
}, optional?: Record<string, never>): Promise<() => Promise<void>>;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Persisted outbox row used for reliable async event delivery.
|
|
@@ -120,9 +120,14 @@ export interface OutboxPort {
|
|
|
120
120
|
/** Append an event for later delivery. */
|
|
121
121
|
enqueue(event: DomainEvent): Promise<void>;
|
|
122
122
|
/** Claim pending rows for processing. */
|
|
123
|
-
claimPending(
|
|
123
|
+
claimPending(required: {
|
|
124
|
+
batchSize: number;
|
|
125
|
+
nowIso: ISODateTime;
|
|
126
|
+
}, optional?: Record<string, never>): Promise<OutboxRecord[]>;
|
|
124
127
|
/** Mark a row as delivered. */
|
|
125
|
-
markDelivered(
|
|
128
|
+
markDelivered(required: {
|
|
129
|
+
id: UUID;
|
|
130
|
+
}, optional?: Record<string, never>): Promise<void>;
|
|
126
131
|
/**
|
|
127
132
|
* Mark a row as failed and set its next retry time.
|
|
128
133
|
*
|
|
@@ -134,16 +139,12 @@ export interface OutboxPort {
|
|
|
134
139
|
* is terminal — `claimPending` only ever selects `"pending"` rows, so a `"failed"` row is
|
|
135
140
|
* permanently excluded from retry regardless of `nextAttemptAt`.
|
|
136
141
|
*/
|
|
137
|
-
markFailed(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
/** ID generator abstraction to keep ID strategy swappable and testable. */
|
|
145
|
-
export interface IdGeneratorPort {
|
|
146
|
-
/** Create a new globally unique ID. */
|
|
147
|
-
newId(): UUID;
|
|
142
|
+
markFailed(required: {
|
|
143
|
+
id: UUID;
|
|
144
|
+
error: string;
|
|
145
|
+
nextAttemptAt: ISODateTime;
|
|
146
|
+
nextStatus: Extract<OutboxRecord["status"], "pending" | "failed">;
|
|
147
|
+
}, optional?: Record<string, never>): Promise<void>;
|
|
148
148
|
}
|
|
149
|
+
/** Clock and ID generator contracts are owned by the shared kernel primitives. */
|
|
149
150
|
//# sourceMappingURL=ports.d.ts.map
|
package/dist/core/ports.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** CMS permission checking preserves the CMS command denial contract. Generic registration wiring lives in the kernel. */
|
|
2
|
+
import { type AuthorizeFn } from "../commands/command.js";
|
|
3
|
+
import type { AuthorizationPort } from "../authorization.js";
|
|
4
|
+
export type { AuthorizationPort, AuthorizationRequired, AuthorizationOptional } from "../authorization.js";
|
|
5
|
+
export interface RequireToolPermissionRequired {
|
|
6
|
+
authorize: AuthorizationPort;
|
|
7
|
+
workspaceId: string;
|
|
8
|
+
principalId: string;
|
|
9
|
+
permission: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RequireToolPermissionOptional {
|
|
12
|
+
entityType?: string | undefined;
|
|
13
|
+
entityId?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
|
|
17
|
+
* tool whose own domain function carries no `authorize()` call to inherit.
|
|
18
|
+
*
|
|
19
|
+
* This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
|
|
20
|
+
* rule's concern. For each tool that
|
|
21
|
+
* calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
|
|
22
|
+
* to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
|
|
23
|
+
* they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
|
|
24
|
+
* inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
|
|
25
|
+
* Database/Recovery gate in the route instead, so their handlers call this in the route's place.
|
|
26
|
+
* Which kind a given tool is stays documented next to that tool's handler, not here.
|
|
27
|
+
*
|
|
28
|
+
* Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
|
|
29
|
+
* tool caller and a route caller see the identical error shape for an identical denial. Throw
|
|
30
|
+
* rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
|
|
31
|
+
* failed execution and has no response to write to.
|
|
32
|
+
*
|
|
33
|
+
* Import the defining command module directly: its barrel also exports mutation appliers that
|
|
34
|
+
* name content features. Pulling that barrel into shared permission wiring would couple every
|
|
35
|
+
* tool domain to those features; the direct import preserves the identical denial class without
|
|
36
|
+
* that dependency closure.
|
|
37
|
+
*
|
|
38
|
+
* @param required - The `authorize()` evaluator and the workspace the run is scoped to.
|
|
39
|
+
* @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
|
|
40
|
+
* @throws {ForbiddenError} If `authorize()` denies.
|
|
41
|
+
* @complexity O(1) beyond the injected `authorize()` call.
|
|
42
|
+
* @overallScore 100
|
|
43
|
+
*/
|
|
44
|
+
export declare function requireToolPermission(required: RequireToolPermissionRequired, optional?: RequireToolPermissionOptional): Promise<void>;
|
|
45
|
+
/** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
|
|
46
|
+
export declare function adaptLegacyAuthorize(required: {
|
|
47
|
+
authorize: AuthorizeFn;
|
|
48
|
+
}, _optional?: Record<string, never>): AuthorizationPort;
|
|
49
|
+
//# sourceMappingURL=args.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** CMS permission checking preserves the CMS command denial contract. Generic registration wiring lives in the kernel. */
|
|
2
|
+
import { ForbiddenError } from "../commands/command.js";
|
|
3
|
+
/**
|
|
4
|
+
* Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
|
|
5
|
+
* tool whose own domain function carries no `authorize()` call to inherit.
|
|
6
|
+
*
|
|
7
|
+
* This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
|
|
8
|
+
* rule's concern. For each tool that
|
|
9
|
+
* calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
|
|
10
|
+
* to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
|
|
11
|
+
* they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
|
|
12
|
+
* inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
|
|
13
|
+
* Database/Recovery gate in the route instead, so their handlers call this in the route's place.
|
|
14
|
+
* Which kind a given tool is stays documented next to that tool's handler, not here.
|
|
15
|
+
*
|
|
16
|
+
* Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
|
|
17
|
+
* tool caller and a route caller see the identical error shape for an identical denial. Throw
|
|
18
|
+
* rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
|
|
19
|
+
* failed execution and has no response to write to.
|
|
20
|
+
*
|
|
21
|
+
* Import the defining command module directly: its barrel also exports mutation appliers that
|
|
22
|
+
* name content features. Pulling that barrel into shared permission wiring would couple every
|
|
23
|
+
* tool domain to those features; the direct import preserves the identical denial class without
|
|
24
|
+
* that dependency closure.
|
|
25
|
+
*
|
|
26
|
+
* @param required - The `authorize()` evaluator and the workspace the run is scoped to.
|
|
27
|
+
* @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
|
|
28
|
+
* @throws {ForbiddenError} If `authorize()` denies.
|
|
29
|
+
* @complexity O(1) beyond the injected `authorize()` call.
|
|
30
|
+
* @overallScore 100
|
|
31
|
+
*/
|
|
32
|
+
export async function requireToolPermission(required, optional = {}) {
|
|
33
|
+
const result = await required.authorize({
|
|
34
|
+
principalId: required.principalId, permission: required.permission, workspaceId: required.workspaceId,
|
|
35
|
+
}, optional);
|
|
36
|
+
if (!result.allowed)
|
|
37
|
+
throw new ForbiddenError({
|
|
38
|
+
message: `principal '${required.principalId}' is not authorized for '${required.permission}' (${result.reason})`,
|
|
39
|
+
permission: required.permission, reason: result.reason,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
|
|
43
|
+
export function adaptLegacyAuthorize(required, _optional = {}) {
|
|
44
|
+
return (request, optional = {}) => required.authorize({ ...request, ...optional });
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=args.js.map
|
|
@@ -1,76 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @file The Entries agent-tool catalog — this domain's instance of the per-domain `agent-tools.ts`
|
|
3
|
-
* convention `features/content-types/agent-tools.ts`, `features/settings/agent-tools.ts`, and every
|
|
4
|
-
* other domain catalog already use.
|
|
5
|
-
*
|
|
6
|
-
* Purpose:
|
|
7
|
-
* A static, in-process catalog describing every agent-callable tool this domain exposes. Every
|
|
8
|
-
* entry maps 1:1 onto a real exported function of `features/entries/write-service.ts` (`createEntry`,
|
|
9
|
-
* `updateEntry`, `publishEntry`, `unpublishEntry`) or `features/entries/list.ts` (`listEntries`) —
|
|
10
|
-
* this catalog never names an operation the domain cannot perform.
|
|
11
|
-
*
|
|
12
|
-
* Naming and scope — a real discrepancy from this dispatch's own brief, disclosed rather than
|
|
13
|
-
* silently reconciled: the brief framed this domain as "backing the Pages, Posts, and Collections
|
|
14
|
-
* admin sections", but reading the actual admin routes shows that is only true for Collections.
|
|
15
|
-
* `server/routes/admin/entries/{create,update,lifecycle,list}.ts` (`/api/admin/v1/entries`, gated by
|
|
16
|
-
* `admin.collections.manage`/`admin.collections.read`) are the ONLY routes backed by THIS package's
|
|
17
|
-
* `write-service.ts`/`list.ts` — exactly what this catalog wraps. Pages and Posts
|
|
18
|
-
* (`server/routes/admin/pages/*`, `server/routes/admin/posts/*`) are, in this codebase's CURRENT
|
|
19
|
-
* state, backed by a wholly separate legacy `features/post` module (`createPost`/`updatePost`/
|
|
20
|
-
* `getAdminPostById`/`listAdminPages`, its own `post` table, routed through `core/commands`'s
|
|
21
|
-
* `executeCommand` gateway, gated by `content.write`/`content.read` — a different permission
|
|
22
|
-
* namespace entirely). `features/entries` has no relationship to that module at all: no shared
|
|
23
|
-
* table, no shared write chokepoint, no shared permission. So this catalog's tools name their
|
|
24
|
-
* resource `collections_entry_*` (pairing with `features/content-types/agent-tools.ts`'s own
|
|
25
|
-
* `collections_content_type_*` prefix — the two packages are "one cohesive domain" per
|
|
26
|
-
* `server/routes/admin/content-types/deps.ts`'s own file header), not `entry_*`/`page_*`/`post_*` —
|
|
27
|
-
* a name that could be misread as covering Pages/Posts. Wiring Pages/Posts agent tools would mean
|
|
28
|
-
* wrapping `features/post`, a different backend this dispatch was not asked to touch and has not
|
|
29
|
-
* inspected for the same safety discipline this catalog applies here.
|
|
30
|
-
*
|
|
31
|
-
* Deliberate absences (the point of a catalog, not an oversight):
|
|
32
|
-
* - There is no delete/purge tool: `write-service.ts` exposes no delete function at all (confirmed
|
|
33
|
-
* by reading the whole file) — there is nothing to wrap or exclude on that front.
|
|
34
|
-
* - `collections_entry_update`'s schema omits `bodyJson` even though `updateEntry` itself accepts an
|
|
35
|
-
* optional `bodyJson` (REQ-44/45, used internally by `widgets/embed-service.ts`'s own guardrailed
|
|
36
|
-
* mutation path). The generic Collections admin route (`entries/update.ts`) never forwards
|
|
37
|
-
* `body.bodyJson` at all — only `title`/`fieldsJson`/`expectedVersion` — so exposing it here would
|
|
38
|
-
* invent capability beyond what the human admin UI's own Collections editor exposes. Omitting it
|
|
39
|
-
* from the input is enough: `updateEntry` leaves `bodyJson` unchanged whenever the field is
|
|
40
|
-
* `undefined`, so this exclusion changes nothing about what a call can still do.
|
|
41
|
-
* - `collections_entry_list`'s schema omits `status`/`orderBy`/`limit` even though
|
|
42
|
-
* `EntryListPort.listByWorkspace` supports all three (added for `widgets/resolvers/recent-entries.ts`'s
|
|
43
|
-
* own bounded-query need) — `server/routes/admin/entries/list.ts` only ever forwards `type` from
|
|
44
|
-
* its query string, so the other three are not part of what the Collections admin list screen
|
|
45
|
-
* itself can do; adding them here would again exceed the human surface this catalog mirrors.
|
|
46
|
-
*
|
|
47
|
-
* What IS included, and why it is safe: all four write-service functions are `admin.collections.*`
|
|
48
|
-
* self-enforcing (each opens with its own `deps.authorize()` call before any other side effect —
|
|
49
|
-
* confirmed by reading `write-service.ts` directly), and `publishEntry`/`unpublishEntry` are ordinary,
|
|
50
|
-
* fully reversible status flips with a full revision trail (`entryRepo.appendRevision`) — publishing
|
|
51
|
-
* and unpublishing are each other's own undo.
|
|
52
|
-
*
|
|
53
|
-
* How it relates to the project:
|
|
54
|
-
* `features/entries/tool-registrations.ts` maps these entries into `@jini-ai/core` `ToolRegistration`s.
|
|
55
|
-
*
|
|
56
|
-
* Architectural role:
|
|
57
|
-
* `features/entries` domain declaration. No dependencies.
|
|
58
|
-
*/
|
|
59
|
-
export type AgentToolSideEffect = "none" | "mutates-durable-state" | "mints-token";
|
|
60
|
-
export interface AgentToolDefinition {
|
|
61
|
-
name: string;
|
|
62
|
-
description: string;
|
|
63
|
-
sideEffects: AgentToolSideEffect;
|
|
64
|
-
authorization: {
|
|
65
|
-
permission: string;
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* JSON Schema for this tool's `input`, published to the model via `ToolDescriptor.inputSchema`
|
|
69
|
-
* (`assistant/tool-registration-kit.ts`'s `buildDomainRegistrations`, which refuses to wire any
|
|
70
|
-
* tool lacking one).
|
|
71
|
-
*/
|
|
72
|
-
inputSchema?: Readonly<Record<string, unknown>>;
|
|
73
|
-
}
|
|
1
|
+
import type { AgentToolDefinition } from "@jini-ai/core";
|
|
74
2
|
/** The Entries domain's fixed agent-tool catalog: the Collections authoring surface's 4 writes
|
|
75
3
|
* (create/update/publish/unpublish) plus its 1 read (list) — see this file's header for the
|
|
76
4
|
* Pages/Posts naming disclosure and the 3 deliberate scope exclusions. */
|