@jini-ai/core 0.4.8 → 0.6.10
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 +23 -2
- package/README.md +5 -0
- package/dist/agent-tools.d.ts +1 -1
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +87 -22
- package/dist/batch-tool-inputs.d.ts +147 -0
- package/dist/batch-tool-inputs.js +257 -0
- package/dist/feature-flags.d.ts +21 -0
- package/dist/feature-flags.js +17 -0
- package/dist/fetch/index.d.ts +2 -1
- package/dist/fetch/index.js +1 -0
- package/dist/fetch/loader-read.d.ts +16 -0
- package/dist/fetch/loader-read.js +22 -0
- package/dist/fetch/types.d.ts +12 -2
- package/dist/gated-mutations/gateway.js +32 -27
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -1
- package/dist/object-store.d.ts +115 -0
- package/dist/object-store.js +70 -0
- package/dist/origin-validation.js +64 -57
- package/dist/primitives/http.d.ts +2 -2
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +17 -10
- package/dist/primitives/tasks.d.ts +10 -0
- package/dist/primitives/tasks.js +8 -0
- package/dist/provider-catalog.d.ts +76 -0
- package/dist/provider-catalog.js +6 -0
- package/dist/provider-registry.d.ts +144 -0
- package/dist/provider-registry.js +138 -0
- package/dist/records.d.ts +24 -0
- package/dist/records.js +25 -0
- package/dist/redact.d.ts +32 -0
- package/dist/redact.js +91 -34
- package/dist/registration-kit.d.ts +7 -4
- package/dist/registration-kit.js +81 -62
- package/dist/settings/agent-value-write-tools.d.ts +19 -0
- package/dist/settings/agent-value-write-tools.js +131 -70
- package/dist/settings/ensure-definitions.d.ts +4 -2
- package/dist/settings/ensure-definitions.js +79 -63
- package/dist/settings/errors.d.ts +13 -0
- package/dist/settings/errors.js +15 -0
- package/dist/settings/express/cms-adapter.d.ts +2 -2
- package/dist/settings/express/cms-adapter.js +34 -22
- package/dist/settings/express/http-request.d.ts +33 -0
- package/dist/settings/express/http-request.js +49 -0
- package/dist/settings/express/http.d.ts +2 -2
- package/dist/settings/express/http.js +16 -39
- package/dist/settings/express/stream.d.ts +2 -2
- package/dist/settings/express/stream.js +49 -27
- package/dist/settings/index.d.ts +2 -2
- package/dist/settings/index.js +1 -1
- package/dist/settings/ports.d.ts +7 -1
- package/dist/settings/repo.memory.js +7 -4
- package/dist/settings/settings.js +54 -60
- package/dist/settings/tool-registrations.d.ts +2 -3
- package/dist/settings/tool-registrations.js +64 -39
- package/dist/settings/write-service.d.ts +42 -4
- package/dist/settings/write-service.js +72 -5
- package/dist/target-scope.d.ts +2 -0
- package/dist/target-scope.js +18 -2
- package/dist/text/escape.js +2 -2
- package/dist/text/sanitize-untrusted-text.d.ts +5 -0
- package/dist/text/sanitize-untrusted-text.js +55 -35
- package/dist/tool-exposure.d.ts +79 -0
- package/dist/tool-exposure.js +163 -0
- package/dist/tool-input-errors.d.ts +23 -0
- package/dist/tool-input-errors.js +24 -0
- package/dist/tool-input-normalization.d.ts +37 -0
- package/dist/tool-input-normalization.js +188 -0
- package/dist/tool-metadata-types.d.ts +26 -0
- package/dist/tool-metadata-types.js +2 -0
- package/dist/tool-metadata.d.ts +2 -23
- package/dist/tool-registry.d.ts +38 -22
- package/dist/tool-registry.js +35 -20
- package/package.json +20 -2
- package/plugin-directory.js +72 -33
|
@@ -1,51 +1,65 @@
|
|
|
1
|
-
import { adaptLegacyAuthorize } from "../tool-permissions.js";
|
|
1
|
+
import { adaptLegacyAuthorize, ForbiddenError as PermissionDeniedError, requireToolPermission } from "../tool-permissions.js";
|
|
2
2
|
import { ToolInputError } from "../tool-registry.js";
|
|
3
|
-
import { ForbiddenError as PermissionDeniedError } from "../tool-permissions.js";
|
|
4
|
-
import { requireToolPermission } from "../tool-permissions.js";
|
|
5
3
|
import { AGENT_WRITE_CONFIRMATION_SETTINGS, AGENT_WRITE_DENIED_SETTINGS, findAgentWriteRule } from "./agent-write-denylist.js";
|
|
6
4
|
import { DefinitionNotFoundError, DefinitionTombstonedError, ForbiddenError, ScopeNotAllowedError, ValueValidationFailedError } from "./errors.js";
|
|
7
5
|
import { getEffective, resolveDefinitionRaw, validateValueAgainstSchema } from "./settings.js";
|
|
8
6
|
import { SCOPE_BIT } from "./types.js";
|
|
9
7
|
import { clear, deriveRequiredPermission, set } from "./write-service.js";
|
|
8
|
+
const WRITE_FIELDS = {
|
|
9
|
+
settings_set_value: ["namespace", "key", "value", "scope"],
|
|
10
|
+
settings_clear_value: ["namespace", "key", "scope"],
|
|
11
|
+
};
|
|
10
12
|
/** Strict boundary, including direct handler calls. JSON is cloned before the first await
|
|
11
|
-
* so a held-open card cannot consent to a value later changed by the caller. O(input size).
|
|
13
|
+
* so a held-open card cannot consent to a value later changed by the caller. O(input size).
|
|
14
|
+
* Refusal order: non-object, unexpected field, namespace, key, scope, missing value, non-JSON value. */
|
|
12
15
|
function parseTarget(toolId, raw) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const input = raw;
|
|
16
|
-
const fields = toolId === "settings_set_value" ? ["namespace", "key", "value", "scope"] : ["namespace", "key", "scope"];
|
|
16
|
+
const input = requireInputObject(toolId, raw);
|
|
17
|
+
const fields = WRITE_FIELDS[toolId];
|
|
17
18
|
const extra = Object.keys(input).find((key) => !fields.includes(key));
|
|
18
19
|
if (extra)
|
|
19
20
|
throw new ToolInputError({ message: `${toolId}: unexpected input '${extra}'. Only ${fields.join(", ")} are accepted; the caller's own user layer is the only user target.` });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
const namespace = input.namespace;
|
|
25
|
-
const key = input.key;
|
|
26
|
-
const scope = input.scope === undefined ? "workspace" : input.scope;
|
|
27
|
-
if (scope !== "workspace" && scope !== "user")
|
|
28
|
-
throw new ToolInputError({ message: `${toolId}: scope must be 'workspace' or 'user'; global settings are not writable by this tool.` });
|
|
21
|
+
const namespace = requireCoordinate(toolId, input, "namespace");
|
|
22
|
+
const key = requireCoordinate(toolId, input, "key");
|
|
23
|
+
const scope = parseScope(toolId, input.scope);
|
|
29
24
|
if (toolId === "settings_clear_value")
|
|
30
25
|
return { namespace, key, scope };
|
|
26
|
+
return { namespace, key, scope, value: snapshotJsonValue(toolId, input) };
|
|
27
|
+
}
|
|
28
|
+
function requireInputObject(toolId, raw) {
|
|
29
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
30
|
+
throw new ToolInputError({ message: `${toolId}: input must be an object containing namespace and key.` });
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
function requireCoordinate(toolId, input, field) {
|
|
34
|
+
const value = input[field];
|
|
35
|
+
if (typeof value !== "string" || !value.trim())
|
|
36
|
+
throw new ToolInputError({ message: `${toolId}: '${field}' must be a non-empty string. Call settings_list_definitions to see valid keys.` });
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/** Omitted means workspace; anything other than workspace/user (including null and global) is refused. */
|
|
40
|
+
function parseScope(toolId, raw) {
|
|
41
|
+
const scope = raw === undefined ? "workspace" : raw;
|
|
42
|
+
if (scope !== "workspace" && scope !== "user")
|
|
43
|
+
throw new ToolInputError({ message: `${toolId}: scope must be 'workspace' or 'user'; global settings are not writable by this tool.` });
|
|
44
|
+
return scope;
|
|
45
|
+
}
|
|
46
|
+
function snapshotJsonValue(toolId, input) {
|
|
31
47
|
if (!("value" in input))
|
|
32
48
|
throw new ToolInputError({ message: `${toolId}: a JSON 'value' is required.` });
|
|
33
49
|
// The executor validates JSON input too. The round trip protects direct callers and isolates
|
|
34
50
|
// the snapshot; values JSON would silently change (NaN, undefined, functions) are rejected.
|
|
35
|
-
|
|
51
|
+
// A serialization that yields nothing makes JSON.parse throw, so it is refused the same way.
|
|
36
52
|
try {
|
|
37
|
-
|
|
38
|
-
if (value === undefined || typeof value === "function" || typeof value === "symbol" || (typeof value === "number" && !Number.isFinite(value)))
|
|
39
|
-
throw new Error("not JSON");
|
|
40
|
-
return value;
|
|
41
|
-
});
|
|
53
|
+
return JSON.parse(JSON.stringify(input.value, rejectNonJson));
|
|
42
54
|
}
|
|
43
|
-
catch {
|
|
44
|
-
throw new ToolInputError({ message: `${toolId}: value must be valid JSON.` });
|
|
55
|
+
catch (cause) {
|
|
56
|
+
throw new ToolInputError({ message: `${toolId}: value must be valid JSON.` }, { cause });
|
|
45
57
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
}
|
|
59
|
+
function rejectNonJson(_key, value) {
|
|
60
|
+
if (value === undefined || typeof value === "function" || typeof value === "symbol" || (typeof value === "number" && !Number.isFinite(value)))
|
|
61
|
+
throw new Error("not JSON");
|
|
62
|
+
return value;
|
|
49
63
|
}
|
|
50
64
|
/** Definition/scope/value preflight for readable refusal before any card. O(1) repository
|
|
51
65
|
* lookups plus alias resolution and schema validation, with no writes. */
|
|
@@ -68,17 +82,24 @@ function readLayer(deps, target, settingId, principalId) {
|
|
|
68
82
|
const base = { workspaceId: deps.workspaceId, settingId };
|
|
69
83
|
return target.scope === "user" ? deps.settingsRepo.getUserValue({ ...base, principalId }) : deps.settingsRepo.getWorkspaceValue(base);
|
|
70
84
|
}
|
|
85
|
+
/** The effective agent-write key policy: package denials plus host extensions (never agent input),
|
|
86
|
+
* and the host's confirmation policy (replacing the package defaults) plus host extensions. O(r). */
|
|
87
|
+
export function agentWriteRules(deps) {
|
|
88
|
+
return {
|
|
89
|
+
denied: [...AGENT_WRITE_DENIED_SETTINGS, ...(deps.extraDeniedSettings ?? [])],
|
|
90
|
+
confirmation: [...(deps.confirmationSettings ?? AGENT_WRITE_CONFIRMATION_SETTINGS), ...(deps.extraConfirmationSettings ?? [])],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
71
93
|
/** Fail closed on host-denied keys, including a protected target reached through an alias.
|
|
72
94
|
* Returns a confirmation rule if either coordinate needs one; O(r) in fixed policy rules. */
|
|
73
95
|
function writePolicy(deps, toolId, target, definition) {
|
|
74
96
|
const targets = definition ? [target, definition] : [target];
|
|
75
|
-
const denied =
|
|
97
|
+
const { denied, confirmation } = agentWriteRules(deps);
|
|
76
98
|
for (const coordinate of targets) {
|
|
77
99
|
const rule = findAgentWriteRule(denied, coordinate);
|
|
78
100
|
if (rule)
|
|
79
101
|
throw new ToolInputError({ message: `${toolId}: '${coordinate.namespace}.${coordinate.key}' can't be changed by the assistant: ${rule.reason}. The owner can change it in Settings.` });
|
|
80
102
|
}
|
|
81
|
-
const confirmation = [...(deps.confirmationSettings ?? AGENT_WRITE_CONFIRMATION_SETTINGS), ...(deps.extraConfirmationSettings ?? [])];
|
|
82
103
|
for (const coordinate of targets) {
|
|
83
104
|
const rule = findAgentWriteRule(confirmation, coordinate);
|
|
84
105
|
if (rule)
|
|
@@ -89,15 +110,84 @@ function writePolicy(deps, toolId, target, definition) {
|
|
|
89
110
|
/** Maps expected caller-fixable errors; infrastructure failures remain untouched. O(1). */
|
|
90
111
|
function rethrowWriteError(error, toolId, target, permission) {
|
|
91
112
|
if (error instanceof DefinitionNotFoundError || error instanceof DefinitionTombstonedError)
|
|
92
|
-
throw new ToolInputError({ message: `${toolId}: ${error.message}. Call settings_list_definitions to see valid keys.` });
|
|
113
|
+
throw new ToolInputError({ message: `${toolId}: ${error.message}. Call settings_list_definitions to see valid keys.` }, { cause: error });
|
|
93
114
|
if (error instanceof ScopeNotAllowedError)
|
|
94
|
-
throw new ToolInputError({ message: `${toolId}: '${target.namespace}.${target.key}' can't be set ${target.scope === "user" ? "per-user" : "at workspace scope"}. Call settings_list_definitions to see allowed scopes.` });
|
|
115
|
+
throw new ToolInputError({ message: `${toolId}: '${target.namespace}.${target.key}' can't be set ${target.scope === "user" ? "per-user" : "at workspace scope"}. Call settings_list_definitions to see allowed scopes.` }, { cause: error });
|
|
95
116
|
if (error instanceof ValueValidationFailedError)
|
|
96
|
-
throw new ToolInputError({ message: `${toolId}: ${error.message}. Call settings_list_definitions and use a value matching the setting's schema.` });
|
|
117
|
+
throw new ToolInputError({ message: `${toolId}: ${error.message}. Call settings_list_definitions and use a value matching the setting's schema.` }, { cause: error });
|
|
97
118
|
if (error instanceof ForbiddenError || error instanceof PermissionDeniedError)
|
|
98
|
-
throw new ToolInputError({ message: `${toolId}: permission '${permission}' is required. Ask the owner to grant it before retrying.` });
|
|
119
|
+
throw new ToolInputError({ message: `${toolId}: permission '${permission}' is required. Ask the owner to grant it before retrying.` }, { cause: error });
|
|
99
120
|
throw error;
|
|
100
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Builds `write-service.ts`'s own five-field dep shape from `RouteDeps`.
|
|
124
|
+
*
|
|
125
|
+
* Deliberately duplicates a host's own HTTP admin layer's equivalent field-mapping helper rather
|
|
126
|
+
* than importing it, for the reason `tool-registrations.ts`'s header already gives about
|
|
127
|
+
* `resolveOwnOrOtherPrincipalRead`: this is domain-owned wiring, and importing from a host's HTTP
|
|
128
|
+
* admin layer would invert the ports/adapters direction. It is a field mapping, not logic, so the
|
|
129
|
+
* two copies carry no behavioral drift risk — and `write-service.ts` is the single chokepoint
|
|
130
|
+
* either way, so a divergence here could only produce a missing dependency, which fails loudly.
|
|
131
|
+
*/
|
|
132
|
+
export function toWriteDeps(routeDeps) {
|
|
133
|
+
return {
|
|
134
|
+
repo: routeDeps.settingsRepo,
|
|
135
|
+
clock: routeDeps.clock,
|
|
136
|
+
ids: routeDeps.idGen,
|
|
137
|
+
authorize: routeDeps.authorize,
|
|
138
|
+
principals: routeDeps.principalRepo,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Cancellation is checked at each step that precedes a card or a write. O(1). */
|
|
142
|
+
function assertRunActive(ctx, toolId) {
|
|
143
|
+
if (ctx.signal.aborted)
|
|
144
|
+
throw new ToolInputError({ message: `${toolId}: the run ended before the change could be applied. Nothing was changed.` });
|
|
145
|
+
}
|
|
146
|
+
/** Effects: policy -> readiness -> authorize -> definition preflight -> raw read. No writes. */
|
|
147
|
+
async function prepareWrite(deps, request) {
|
|
148
|
+
const { ctx, toolId, target, permission } = request;
|
|
149
|
+
assertRunActive(ctx, toolId);
|
|
150
|
+
writePolicy(deps, toolId, target);
|
|
151
|
+
await Promise.all([deps.settingsReady, deps.settingsUiTabsReady]);
|
|
152
|
+
await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: deps.authorize }), workspaceId: deps.workspaceId, principalId: ctx.principal.id, permission }, { entityType: "setting-value" });
|
|
153
|
+
const definition = await resolveTarget(deps, target, toolId === "settings_set_value");
|
|
154
|
+
const rule = writePolicy(deps, toolId, target, definition);
|
|
155
|
+
const layer = await readLayer(deps, target, definition.settingId, ctx.principal.id);
|
|
156
|
+
const previous = layer?.state === "set" ? structuredClone(layer.valueJson) : null;
|
|
157
|
+
return { definition, rule, layer, previous };
|
|
158
|
+
}
|
|
159
|
+
/** Opens the host's human card for a protected key, then proves nothing changed while it was open.
|
|
160
|
+
* Fails closed without a confirmation port, on any answer but `true`, and on a stale definition or layer. */
|
|
161
|
+
async function confirmProtectedWrite(deps, request, prepared, options) {
|
|
162
|
+
const { ctx, toolId, target } = request;
|
|
163
|
+
const { definition, layer, previous, rule } = prepared;
|
|
164
|
+
const isSet = toolId === "settings_set_value";
|
|
165
|
+
if (!deps.confirmWrite)
|
|
166
|
+
throw new ToolInputError({ message: `${toolId}: '${rule.namespace}.${rule.key}' requires a human confirmation card: ${rule.reason}. The owner can change it in Settings.` });
|
|
167
|
+
const confirmed = await deps.confirmWrite({ ctx, toolId, namespace: rule.namespace, key: rule.key, scope: target.scope, previous: structuredClone(previous), ...(isSet ? { value: structuredClone(target.value) } : {}), reason: rule.reason }, options);
|
|
168
|
+
if (confirmed !== true)
|
|
169
|
+
throw new ToolInputError({ message: `${toolId}: the human did not confirm the change. Nothing was changed.` });
|
|
170
|
+
const current = await resolveTarget(deps, target, isSet);
|
|
171
|
+
const currentLayer = await readLayer(deps, target, current.settingId, ctx.principal.id);
|
|
172
|
+
if (current.settingId !== definition.settingId || current.version !== definition.version || currentLayer?.seq !== layer?.seq)
|
|
173
|
+
throw new ToolInputError({ message: `${toolId}: this setting changed while the confirmation card was open. Read it again and request a new confirmation. Nothing was changed by this call.` });
|
|
174
|
+
}
|
|
175
|
+
/** The service write itself; `write-service.ts` rechecks authorization inside its transaction. */
|
|
176
|
+
async function applyWrite(deps, request, prepared) {
|
|
177
|
+
const { ctx, toolId, target } = request;
|
|
178
|
+
const { definition, previous } = prepared;
|
|
179
|
+
const writeDeps = toWriteDeps(deps);
|
|
180
|
+
// Resolved coordinates preserve host validators even when the request named an alias.
|
|
181
|
+
const input = { namespace: definition.namespace, key: definition.key, scope: target.scope, workspaceId: deps.workspaceId, callerPrincipalId: ctx.principal.id, authWorkspaceId: deps.workspaceId };
|
|
182
|
+
const key = `${target.namespace}.${target.key}`;
|
|
183
|
+
if (toolId === "settings_set_value") {
|
|
184
|
+
const result = await (deps.setValue ?? set)({ deps: writeDeps, input: { ...input, value: target.value } });
|
|
185
|
+
return { key, scope: target.scope, previous, value: result.value, revisionSeq: result.revisionSeq };
|
|
186
|
+
}
|
|
187
|
+
await clear({ deps: writeDeps, input });
|
|
188
|
+
const effective = await getEffective({ repo: deps.settingsRepo }, { namespace: definition.namespace, key: definition.key, scopeContext: { workspaceId: deps.workspaceId, principalId: ctx.principal.id } });
|
|
189
|
+
return { key, scope: target.scope, previous, effective: effective?.value ?? null };
|
|
190
|
+
}
|
|
101
191
|
/** Builds one value-write handler. Effects: authorize -> raw read -> optional human card ->
|
|
102
192
|
* service write -> result. The service rechecks authorization and performs its normal transaction.
|
|
103
193
|
* @complexity O(value size + rule count) plus bounded repository calls and a human wait.
|
|
@@ -109,45 +199,16 @@ export function buildSettingsValueWriteHandler(deps, toolId) {
|
|
|
109
199
|
ctx = { ...ctx, principal: { ...ctx.principal }, run: { ...ctx.run } };
|
|
110
200
|
const target = parseTarget(toolId, ctx.input);
|
|
111
201
|
const permission = deriveRequiredPermission({ scope: target.scope, callerPrincipalId: ctx.principal.id });
|
|
112
|
-
const
|
|
202
|
+
const request = { ctx, toolId, target, permission };
|
|
113
203
|
try {
|
|
114
|
-
|
|
115
|
-
throw new ToolInputError({ message: `${toolId}: the run ended before the change could be applied. Nothing was changed.` });
|
|
116
|
-
writePolicy(deps, toolId, target);
|
|
117
|
-
await Promise.all([deps.settingsReady, deps.settingsUiTabsReady]);
|
|
118
|
-
await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: deps.authorize }), workspaceId: deps.workspaceId, principalId: ctx.principal.id, permission }, { entityType: "setting-value" });
|
|
119
|
-
const definition = await resolveTarget(deps, target, isSet);
|
|
120
|
-
const rule = writePolicy(deps, toolId, target, definition);
|
|
121
|
-
const layer = await readLayer(deps, target, definition.settingId, ctx.principal.id);
|
|
122
|
-
const previous = layer?.state === "set" ? structuredClone(layer.valueJson) : null;
|
|
204
|
+
const prepared = await prepareWrite(deps, request);
|
|
123
205
|
// Cancellation may have arrived during readiness, authorization or the raw read.
|
|
124
206
|
// Check before opening a card so a finished run cannot leave a new exchange waiting.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const confirmed = await deps.confirmWrite({ ctx, toolId, namespace: rule.namespace, key: rule.key, scope: target.scope, previous: structuredClone(previous), ...(isSet ? { value: structuredClone(target.value) } : {}), reason: rule.reason }, options);
|
|
131
|
-
if (confirmed !== true)
|
|
132
|
-
throw new ToolInputError({ message: `${toolId}: the human did not confirm the change. Nothing was changed.` });
|
|
133
|
-
const current = await resolveTarget(deps, target, isSet);
|
|
134
|
-
const currentLayer = await readLayer(deps, target, current.settingId, ctx.principal.id);
|
|
135
|
-
if (current.settingId !== definition.settingId || current.version !== definition.version || currentLayer?.seq !== layer?.seq)
|
|
136
|
-
throw new ToolInputError({ message: `${toolId}: this setting changed while the confirmation card was open. Read it again and request a new confirmation. Nothing was changed by this call.` });
|
|
137
|
-
}
|
|
138
|
-
if (ctx.signal.aborted)
|
|
139
|
-
throw new ToolInputError({ message: `${toolId}: the run ended before the change could be applied. Nothing was changed.` });
|
|
140
|
-
const writeDeps = { repo: deps.settingsRepo, clock: deps.clock, ids: deps.idGen, authorize: deps.authorize, principals: deps.principalRepo };
|
|
141
|
-
// Resolved coordinates preserve host validators even when the request named an alias.
|
|
142
|
-
const input = { namespace: definition.namespace, key: definition.key, scope: target.scope, workspaceId: deps.workspaceId, callerPrincipalId: ctx.principal.id, authWorkspaceId: deps.workspaceId };
|
|
143
|
-
const key = `${target.namespace}.${target.key}`;
|
|
144
|
-
if (isSet) {
|
|
145
|
-
const result = await (deps.setValue ?? set)({ deps: writeDeps, input: { ...input, value: target.value } });
|
|
146
|
-
return { key, scope: target.scope, previous, value: result.value, revisionSeq: result.revisionSeq };
|
|
147
|
-
}
|
|
148
|
-
await clear({ deps: writeDeps, input });
|
|
149
|
-
const effective = await getEffective({ repo: deps.settingsRepo }, { namespace: definition.namespace, key: definition.key, scopeContext: { workspaceId: deps.workspaceId, principalId: ctx.principal.id } });
|
|
150
|
-
return { key, scope: target.scope, previous, effective: effective?.value ?? null };
|
|
207
|
+
assertRunActive(ctx, toolId);
|
|
208
|
+
if (prepared.rule)
|
|
209
|
+
await confirmProtectedWrite(deps, request, { ...prepared, rule: prepared.rule }, options);
|
|
210
|
+
assertRunActive(ctx, toolId);
|
|
211
|
+
return await applyWrite(deps, request, prepared);
|
|
151
212
|
}
|
|
152
213
|
catch (error) {
|
|
153
214
|
rethrowWriteError(error, toolId, target, permission);
|
|
@@ -44,7 +44,8 @@ export interface SettingDefinitionSpec<Key extends string = string> {
|
|
|
44
44
|
* Bitmask over `SCOPE_BIT`. Defaults to `workspace`, which is what most
|
|
45
45
|
* settings-dialog tabs want: one operator's choice shouldn't silently
|
|
46
46
|
* become every workspace's. Pass `SCOPE_BIT.user` (or a mask including it)
|
|
47
|
-
* for a genuinely per-operator preference.
|
|
47
|
+
* for a genuinely per-operator preference. On existing core definitions, explicitly
|
|
48
|
+
* declared bits are added at boot; existing scopes and value rows are preserved.
|
|
48
49
|
*/
|
|
49
50
|
scopes?: number;
|
|
50
51
|
}
|
|
@@ -73,7 +74,8 @@ export type EnsureSettingDefinitionsInput = SettingDefinitionsInput & ({
|
|
|
73
74
|
/**
|
|
74
75
|
* Idempotently registers every definition in `input.definitions` under
|
|
75
76
|
* `input.namespace`, and reconciles the stored default of any that already
|
|
76
|
-
* exist to the default source declares.
|
|
77
|
+
* exist to the default source declares. Explicit core scope bits are added without removing
|
|
78
|
+
* existing scopes or moving stored values. Safe to call on every boot.
|
|
77
79
|
*
|
|
78
80
|
* The reconcile half matters as much as the register half: registration is
|
|
79
81
|
* once-only, so without it a `defaultValue` edited in source never reaches an
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { resolveDefinitionRaw } from "./settings.js";
|
|
2
2
|
import { SCOPE_BIT } from "./types.js";
|
|
3
|
-
import { reconcileDefinitionDefault, registerDefinitions } from "./write-service.js";
|
|
3
|
+
import { extendDefinitionScopes, reconcileDefinitionDefault, registerDefinitions } from "./write-service.js";
|
|
4
4
|
/** Boot-time infra work is trusted by construction — mirrors every other
|
|
5
5
|
* `ensure*SettingDefinitions`'s identical shim. */
|
|
6
6
|
const alwaysAllowBoot = async () => ({ allowed: true, reason: "system_boot" });
|
|
7
7
|
/**
|
|
8
8
|
* Idempotently registers every definition in `input.definitions` under
|
|
9
9
|
* `input.namespace`, and reconciles the stored default of any that already
|
|
10
|
-
* exist to the default source declares.
|
|
10
|
+
* exist to the default source declares. Explicit core scope bits are added without removing
|
|
11
|
+
* existing scopes or moving stored values. Safe to call on every boot.
|
|
11
12
|
*
|
|
12
13
|
* The reconcile half matters as much as the register half: registration is
|
|
13
14
|
* once-only, so without it a `defaultValue` edited in source never reaches an
|
|
@@ -31,71 +32,86 @@ const alwaysAllowBoot = async () => ({ allowed: true, reason: "system_boot" });
|
|
|
31
32
|
* lookup plus at most one registration or default reconciliation.
|
|
32
33
|
*/
|
|
33
34
|
export async function ensureSettingDefinitions(deps, input) {
|
|
34
|
-
const ownerKind = input.ownerKind ?? "core";
|
|
35
35
|
const workspaceId = input.ownerKind === "site" ? input.workspaceId : null;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
const target = {
|
|
37
|
+
namespace: input.namespace,
|
|
38
|
+
ownerKind: input.ownerKind ?? "core",
|
|
39
|
+
workspaceId,
|
|
40
|
+
authWorkspaceId: workspaceId ?? input.systemPrincipalId,
|
|
41
|
+
systemPrincipalId: input.systemPrincipalId,
|
|
42
|
+
writeDeps: {
|
|
43
|
+
repo: deps.settingsRepo,
|
|
44
|
+
clock: deps.clock,
|
|
45
|
+
ids: deps.ids,
|
|
46
|
+
authorize: alwaysAllowBoot,
|
|
47
|
+
principals: deps.principals,
|
|
48
|
+
},
|
|
43
49
|
};
|
|
44
50
|
for (const def of input.definitions) {
|
|
45
|
-
const existing = await resolveDefinitionRaw({ repo: deps.settingsRepo }, { namespace:
|
|
46
|
-
if (existing)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// non-`core` row is operator-owned; and a row whose identity differs
|
|
56
|
-
// from the slot we asked for means `resolveDefinitionRaw` followed an
|
|
57
|
-
// alias, so rewriting it would silently change a DIFFERENT definition's
|
|
58
|
-
// default. `reconcileDefinitionDefault` would throw on the first two —
|
|
59
|
-
// this skips them quietly because boot must not fail on them.
|
|
60
|
-
const isSameSlot = existing.namespace === input.namespace && existing.key === def.key;
|
|
61
|
-
if (ownerKind === "core" && existing.status === "active" && existing.ownerKind === "core" && isSameSlot) {
|
|
62
|
-
await reconcileDefinitionDefault({
|
|
63
|
-
deps: writeDeps,
|
|
64
|
-
input: {
|
|
65
|
-
namespace: input.namespace,
|
|
66
|
-
key: def.key,
|
|
67
|
-
workspaceId: null,
|
|
68
|
-
defaultValue: def.defaultValue,
|
|
69
|
-
callerPrincipalId: input.systemPrincipalId,
|
|
70
|
-
authWorkspaceId,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
continue;
|
|
51
|
+
const existing = await resolveDefinitionRaw({ repo: deps.settingsRepo }, { namespace: target.namespace, key: def.key, workspaceId });
|
|
52
|
+
if (!existing)
|
|
53
|
+
await registerMissingDefinition(target, def);
|
|
54
|
+
// Core registration is once-only, but the DEFAULT is not. Without this, a
|
|
55
|
+
// `defaultValue` changed in source is unreachable in any install that
|
|
56
|
+
// has already booted — the stored `default_json` from first boot keeps
|
|
57
|
+
// winning, for every settings-dialog tab. See `reconcileDefinitionDefault`
|
|
58
|
+
// for the bug this closes.
|
|
59
|
+
else if (isReconcilableCoreSlot(existing, { ownerKind: target.ownerKind, namespace: target.namespace, key: def.key })) {
|
|
60
|
+
await reconcileExistingCoreDefinition(target, def);
|
|
75
61
|
}
|
|
76
|
-
await registerDefinitions({
|
|
77
|
-
deps: writeDeps,
|
|
78
|
-
input: {
|
|
79
|
-
callerPrincipalId: input.systemPrincipalId,
|
|
80
|
-
// Platform (core) definitions aren't scoped to any one workspace, but
|
|
81
|
-
// `registerDefinitions`'s authorization check still needs a workspace
|
|
82
|
-
// context to authorize against — the seeded workspace mirrors every
|
|
83
|
-
// other boot-time registrar's identical `authWorkspaceId` shape.
|
|
84
|
-
authWorkspaceId,
|
|
85
|
-
definitions: [
|
|
86
|
-
{
|
|
87
|
-
namespace: input.namespace,
|
|
88
|
-
key: def.key,
|
|
89
|
-
ownerKind,
|
|
90
|
-
workspaceId,
|
|
91
|
-
schema: def.schema,
|
|
92
|
-
defaultValue: def.defaultValue,
|
|
93
|
-
scopes: def.scopes ?? SCOPE_BIT.workspace,
|
|
94
|
-
secret: false,
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
62
|
}
|
|
100
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Three slots are deliberately left alone rather than reconciled:
|
|
66
|
+
* a non-`active` row (deprecated/tombstoned) is not ours to rewrite; a
|
|
67
|
+
* non-`core` row is operator-owned; and a row whose identity differs
|
|
68
|
+
* from the slot we asked for means `resolveDefinitionRaw` followed an
|
|
69
|
+
* alias, so rewriting it would silently change a DIFFERENT definition's
|
|
70
|
+
* default. `reconcileDefinitionDefault` would throw on the first two —
|
|
71
|
+
* this skips them quietly because boot must not fail on them. A site
|
|
72
|
+
* target never reconciles: its stored default belongs to the site.
|
|
73
|
+
*/
|
|
74
|
+
function isReconcilableCoreSlot(existing, slot) {
|
|
75
|
+
const isSameSlot = existing.namespace === slot.namespace && existing.key === slot.key;
|
|
76
|
+
return slot.ownerKind === "core" && existing.status === "active" && existing.ownerKind === "core" && isSameSlot;
|
|
77
|
+
}
|
|
78
|
+
async function reconcileExistingCoreDefinition(target, def) {
|
|
79
|
+
const slot = {
|
|
80
|
+
namespace: target.namespace,
|
|
81
|
+
key: def.key,
|
|
82
|
+
workspaceId: null,
|
|
83
|
+
callerPrincipalId: target.systemPrincipalId,
|
|
84
|
+
authWorkspaceId: target.authWorkspaceId,
|
|
85
|
+
};
|
|
86
|
+
// Existing installations need newly declared personal layers too, not just fresh boots.
|
|
87
|
+
if (def.scopes !== undefined) {
|
|
88
|
+
await extendDefinitionScopes({ deps: target.writeDeps, input: { ...slot, scopes: def.scopes } });
|
|
89
|
+
}
|
|
90
|
+
await reconcileDefinitionDefault({ deps: target.writeDeps, input: { ...slot, defaultValue: def.defaultValue } });
|
|
91
|
+
}
|
|
92
|
+
async function registerMissingDefinition(target, def) {
|
|
93
|
+
await registerDefinitions({
|
|
94
|
+
deps: target.writeDeps,
|
|
95
|
+
input: {
|
|
96
|
+
callerPrincipalId: target.systemPrincipalId,
|
|
97
|
+
// Platform (core) definitions aren't scoped to any one workspace, but
|
|
98
|
+
// `registerDefinitions`'s authorization check still needs a workspace
|
|
99
|
+
// context to authorize against — the seeded workspace mirrors every
|
|
100
|
+
// other boot-time registrar's identical `authWorkspaceId` shape.
|
|
101
|
+
authWorkspaceId: target.authWorkspaceId,
|
|
102
|
+
definitions: [
|
|
103
|
+
{
|
|
104
|
+
namespace: target.namespace,
|
|
105
|
+
key: def.key,
|
|
106
|
+
ownerKind: target.ownerKind,
|
|
107
|
+
workspaceId: target.workspaceId,
|
|
108
|
+
schema: def.schema,
|
|
109
|
+
defaultValue: def.defaultValue,
|
|
110
|
+
scopes: def.scopes ?? SCOPE_BIT.workspace,
|
|
111
|
+
secret: false,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
101
117
|
//# sourceMappingURL=ensure-definitions.js.map
|
|
@@ -18,6 +18,19 @@ export declare class SecretNotSupportedError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
export declare class ValueValidationFailedError extends Error {
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A scoped value changed since the caller's snapshot; read again before retrying.
|
|
23
|
+
* Both sequences refer to the addressed value row, never the ledger's global watermark.
|
|
24
|
+
* @example if (error instanceof SettingsRevisionConflictError) await refreshSnapshot();
|
|
25
|
+
*/
|
|
26
|
+
export declare class SettingsRevisionConflictError extends Error {
|
|
27
|
+
readonly expectedRevisionSeq: number;
|
|
28
|
+
readonly actualRevisionSeq: number;
|
|
29
|
+
constructor(required: {
|
|
30
|
+
expectedRevisionSeq: number;
|
|
31
|
+
actualRevisionSeq: number;
|
|
32
|
+
}, optional?: ErrorOptions);
|
|
33
|
+
}
|
|
21
34
|
export declare class RenameRetypeConflictError extends Error {
|
|
22
35
|
}
|
|
23
36
|
export declare class AliasDepthExceededError extends Error {
|
package/dist/settings/errors.js
CHANGED
|
@@ -18,6 +18,21 @@ export class SecretNotSupportedError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
export class ValueValidationFailedError extends Error {
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A scoped value changed since the caller's snapshot; read again before retrying.
|
|
23
|
+
* Both sequences refer to the addressed value row, never the ledger's global watermark.
|
|
24
|
+
* @example if (error instanceof SettingsRevisionConflictError) await refreshSnapshot();
|
|
25
|
+
*/
|
|
26
|
+
export class SettingsRevisionConflictError extends Error {
|
|
27
|
+
expectedRevisionSeq;
|
|
28
|
+
actualRevisionSeq;
|
|
29
|
+
constructor(required, optional = {}) {
|
|
30
|
+
super(`Setting revision changed: expected ${required.expectedRevisionSeq}, found ${required.actualRevisionSeq}.`, optional);
|
|
31
|
+
this.name = "SettingsRevisionConflictError";
|
|
32
|
+
this.expectedRevisionSeq = required.expectedRevisionSeq;
|
|
33
|
+
this.actualRevisionSeq = required.actualRevisionSeq;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
export class RenameRetypeConflictError extends Error {
|
|
22
37
|
}
|
|
23
38
|
export class AliasDepthExceededError extends Error {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SettingsWriteServiceDeps, type SettingsRepoPort } from "../index.js";
|
|
2
2
|
import type { SettingsService, SettingsPermissions, SettingsChangeFeed } from "./contracts.js";
|
|
3
|
-
/** Keep the CMS write chokepoint while translating its permission vocabulary to host policy.
|
|
4
|
-
|
|
3
|
+
/** Keep the CMS write chokepoint while translating its permission vocabulary to host policy.
|
|
4
|
+
* Bind the CMS ledger and write chokepoint without copying their domain logic.
|
|
5
5
|
* Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
|
|
6
6
|
* @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
|
|
7
7
|
* @example createCmsSettingsService({ deps: cmsPorts, permissions });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getEffective, resolveDefinition, set, clear, resetNamespace, registerDefinitions, NON_REGISTER_DEFINITION_OPS, parseNonRegisterDefinitionOp, collectChangedNamespaces, } from "../index.js";
|
|
2
|
-
/** Keep the CMS write chokepoint while translating its permission vocabulary to host policy.
|
|
3
|
-
|
|
2
|
+
/** Keep the CMS write chokepoint while translating its permission vocabulary to host policy.
|
|
3
|
+
* Bind the CMS ledger and write chokepoint without copying their domain logic.
|
|
4
4
|
* Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
|
|
5
5
|
* @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
|
|
6
6
|
* @example createCmsSettingsService({ deps: cmsPorts, permissions });
|
|
@@ -76,35 +76,47 @@ async function applyDefinitions(required) {
|
|
|
76
76
|
const toRegister = [];
|
|
77
77
|
const applied = [];
|
|
78
78
|
for (const raw of items) {
|
|
79
|
-
const item = raw;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const key = String(item.key ?? "");
|
|
83
|
-
const ownerKind = item.ownerKind;
|
|
84
|
-
const workspaceId = ownerKind === "site" ? authWorkspaceId : null;
|
|
85
|
-
const defaultValue = (item.defaultJson ?? null);
|
|
86
|
-
if (op === "register") {
|
|
87
|
-
toRegister.push({ namespace, key, ownerKind, workspaceId, schema: item.schemaJson,
|
|
88
|
-
defaultValue, scopes: Number(item.scopes), secret: Boolean(item.secret ?? false) });
|
|
79
|
+
const item = parseDefinitionItem(raw, { authWorkspaceId });
|
|
80
|
+
if (item.op === "register") {
|
|
81
|
+
toRegister.push(toRegisterInput(item));
|
|
89
82
|
}
|
|
90
83
|
else {
|
|
91
|
-
const parsedOp = parseNonRegisterDefinitionOp({ op });
|
|
84
|
+
const parsedOp = parseNonRegisterDefinitionOp({ op: item.op });
|
|
92
85
|
if (!parsedOp)
|
|
93
|
-
return { unknownOp: op };
|
|
94
|
-
|
|
95
|
-
...(item.newNamespace === undefined ? {} : { newNamespace: item.newNamespace }),
|
|
96
|
-
...(item.newKey === undefined ? {} : { newKey: item.newKey }),
|
|
97
|
-
...(item.schemaJson === undefined ? {} : { schemaJson: item.schemaJson }),
|
|
98
|
-
...(item.coercionJson === undefined ? {} : { coercionJson: item.coercionJson }),
|
|
99
|
-
};
|
|
100
|
-
await NON_REGISTER_DEFINITION_OPS[parsedOp]({ ctx: { deps, callerPrincipalId, authWorkspaceId }, item: opItem });
|
|
86
|
+
return { unknownOp: item.op };
|
|
87
|
+
await NON_REGISTER_DEFINITION_OPS[parsedOp]({ ctx: { deps, callerPrincipalId, authWorkspaceId }, item: toOpItem(item) });
|
|
101
88
|
}
|
|
102
|
-
applied.push({ key: `${namespace}.${key}`, op, status: "applied" });
|
|
89
|
+
applied.push({ key: `${item.namespace}.${item.key}`, op: item.op, status: "applied" });
|
|
103
90
|
}
|
|
104
91
|
if (toRegister.length)
|
|
105
92
|
await registerDefinitions({ deps, input: { definitions: toRegister, callerPrincipalId, authWorkspaceId } });
|
|
106
93
|
return { applied };
|
|
107
94
|
}
|
|
95
|
+
/** Site definitions live in the caller's workspace; every other owner is platform-wide. Pure, O(1). */
|
|
96
|
+
function parseDefinitionItem(raw, context) {
|
|
97
|
+
const ownerKind = raw.ownerKind;
|
|
98
|
+
return {
|
|
99
|
+
op: String(raw.op ?? "register"),
|
|
100
|
+
namespace: String(raw.namespace ?? ""),
|
|
101
|
+
key: String(raw.key ?? ""),
|
|
102
|
+
ownerKind,
|
|
103
|
+
workspaceId: ownerKind === "site" ? context.authWorkspaceId : null,
|
|
104
|
+
defaultValue: (raw.defaultJson ?? null),
|
|
105
|
+
raw,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function toRegisterInput(item) {
|
|
109
|
+
const { namespace, key, ownerKind, workspaceId, defaultValue, raw } = item;
|
|
110
|
+
return { namespace, key, ownerKind, workspaceId, schema: raw.schemaJson,
|
|
111
|
+
defaultValue, scopes: Number(raw.scopes), secret: Boolean(raw.secret ?? false) };
|
|
112
|
+
}
|
|
113
|
+
/** Op-specific fields are forwarded only when present, so CMS defaults still apply to omitted ones. */
|
|
114
|
+
const OPTIONAL_OP_FIELDS = ["newNamespace", "newKey", "schemaJson", "coercionJson"];
|
|
115
|
+
function toOpItem(item) {
|
|
116
|
+
const { namespace, key, ownerKind, workspaceId, defaultValue, raw } = item;
|
|
117
|
+
const present = OPTIONAL_OP_FIELDS.filter((field) => raw[field] !== undefined).map((field) => [field, raw[field]]);
|
|
118
|
+
return { namespace, key, ownerKind, workspaceId, defaultJson: defaultValue, ...Object.fromEntries(present) };
|
|
119
|
+
}
|
|
108
120
|
/** Adapt the ledger through CMS tenant/principal disclosure rules, returning names and cursors only.
|
|
109
121
|
* @complexity O(1) setup; collect is O(page size), with namespace lookup per visible revision.
|
|
110
122
|
* @example createCmsSettingsChangeFeed({ repo });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import type { SettingScope } from "../index.js";
|
|
3
|
+
import type { SettingsHttpOptions, SettingsHttpRequired } from "./contracts.js";
|
|
4
|
+
import { type SettingsErrorMapping } from "./shared.js";
|
|
5
|
+
/**
|
|
6
|
+
* @file Request-level steps of the settings HTTP registrar (`http.ts`): the
|
|
7
|
+
* authentication gate, the failure envelope and value-write body parsing.
|
|
8
|
+
* Pure or response-only; no service, authorization or storage effects.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SETTING_SCOPES: readonly string[];
|
|
11
|
+
export declare function unauthenticated(res: Response): void;
|
|
12
|
+
/** Readiness/principal failures preserve the authentication boundary's existing 401 envelope, so both collapse to null. */
|
|
13
|
+
export declare function resolvePrincipal(required: Pick<SettingsHttpRequired, "ready" | "principalResolver">, context: {
|
|
14
|
+
request: Request;
|
|
15
|
+
response: Response;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
} | null>;
|
|
19
|
+
/** Map a handler failure to its envelope, or end a response whose headers have already been sent. */
|
|
20
|
+
export declare function failRequest(required: {
|
|
21
|
+
response: Response;
|
|
22
|
+
error: unknown;
|
|
23
|
+
mappings: readonly SettingsErrorMapping[];
|
|
24
|
+
}, optional: SettingsHttpOptions): void;
|
|
25
|
+
export type ValueMutationOperation = "set" | "clear";
|
|
26
|
+
export declare const VALUE_MUTATION_REQUIRED: Record<ValueMutationOperation, string>;
|
|
27
|
+
/** The fields a value write needs, or null when one is missing or the scope is unknown. Pure, O(1). */
|
|
28
|
+
export declare function readValueMutation(body: Record<string, unknown>, operation: ValueMutationOperation): {
|
|
29
|
+
namespace: string;
|
|
30
|
+
key: string;
|
|
31
|
+
scope: SettingScope;
|
|
32
|
+
} | null;
|
|
33
|
+
//# sourceMappingURL=http-request.d.ts.map
|