@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { respondToSettingsError } from "./shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* @file Request-level steps of the settings HTTP registrar (`http.ts`): the
|
|
4
|
+
* authentication gate, the failure envelope and value-write body parsing.
|
|
5
|
+
* Pure or response-only; no service, authorization or storage effects.
|
|
6
|
+
*/
|
|
7
|
+
export const SETTING_SCOPES = ["global", "workspace", "user"];
|
|
8
|
+
export function unauthenticated(res) {
|
|
9
|
+
res.status(401).json({ error: "unauthenticated", code: "UNAUTHENTICATED" });
|
|
10
|
+
}
|
|
11
|
+
/** Readiness/principal failures preserve the authentication boundary's existing 401 envelope, so both collapse to null. */
|
|
12
|
+
export async function resolvePrincipal(required, context) {
|
|
13
|
+
try {
|
|
14
|
+
await required.ready;
|
|
15
|
+
return await required.principalResolver(context);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Map a handler failure to its envelope, or end a response whose headers have already been sent. */
|
|
22
|
+
export function failRequest(required, optional) {
|
|
23
|
+
const { response, error, mappings } = required;
|
|
24
|
+
if (!response.headersSent) {
|
|
25
|
+
respondToSettingsError({ response, error, mappings });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Reporting cannot prevent ending a response whose headers have already been sent.
|
|
29
|
+
try {
|
|
30
|
+
optional.onError?.({ operation: "settings-request", error });
|
|
31
|
+
}
|
|
32
|
+
catch { /* Host diagnostics are best-effort. */ }
|
|
33
|
+
finally {
|
|
34
|
+
response.end();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export const VALUE_MUTATION_REQUIRED = {
|
|
38
|
+
set: "namespace, key, scope (global|workspace|user), and valueJson are required",
|
|
39
|
+
clear: "namespace, key, and scope (global|workspace|user) are required",
|
|
40
|
+
};
|
|
41
|
+
/** The fields a value write needs, or null when one is missing or the scope is unknown. Pure, O(1). */
|
|
42
|
+
export function readValueMutation(body, operation) {
|
|
43
|
+
const namespace = String(body.namespace ?? "");
|
|
44
|
+
const key = String(body.key ?? "");
|
|
45
|
+
const scope = body.scope;
|
|
46
|
+
const complete = namespace && key && SETTING_SCOPES.includes(scope) && (operation === "clear" || "valueJson" in body);
|
|
47
|
+
return complete ? { namespace, key, scope } : null;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=http-request.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SettingsHttpRequired, SettingsHttpOptions } from "./contracts.js";
|
|
2
|
-
/** Mount all eight endpoints. The returned disposer closes active feeds and cancels their schedules.
|
|
3
|
-
|
|
2
|
+
/** Mount all eight endpoints. The returned disposer closes active feeds and cancels their schedules.
|
|
3
|
+
* Injected policy and services preserve the settings HTTP response envelopes.
|
|
4
4
|
* O(1) mounting; read/mutation work is delegated. Feed resources are bounded per connection.
|
|
5
5
|
* @example registerSettingsRoutes({ app, workspaceId, ready, service, changeFeed, principalResolver, authorize, scheduler, routes, permissions });
|
|
6
6
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AliasDepthExceededError, DefinitionInvalidError, DefinitionNotFoundError, DefinitionTombstonedError, ForbiddenError, PrincipalNotFoundError, RenameRetypeConflictError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, } from "../index.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveTargetWorkspaceId, resolveUserLayerReadTarget, settingsWritePermission } from "./shared.js";
|
|
3
3
|
import { openSettingsChangeStream } from "./stream.js";
|
|
4
|
+
import { failRequest, readValueMutation, resolvePrincipal, SETTING_SCOPES, unauthenticated, VALUE_MUTATION_REQUIRED } from "./http-request.js";
|
|
4
5
|
const VALUE_ERRORS = [
|
|
5
6
|
{ matches: (e) => e instanceof PrincipalNotFoundError, status: 404, code: "PRINCIPAL_NOT_FOUND" },
|
|
6
7
|
{ matches: (e) => e instanceof DefinitionNotFoundError, status: 404, code: "DEFINITION_NOT_FOUND" },
|
|
@@ -18,7 +19,6 @@ const DEFINITION_ERRORS = [
|
|
|
18
19
|
{ matches: (e) => e instanceof ForbiddenError, status: 403, code: "FORBIDDEN" },
|
|
19
20
|
];
|
|
20
21
|
const RESET_ERRORS = VALUE_ERRORS.filter((m) => m.code === "FORBIDDEN");
|
|
21
|
-
const scopes = ["global", "workspace", "user"];
|
|
22
22
|
function validation(res, error) {
|
|
23
23
|
res.status(400).json({ error, code: "VALIDATION_ERROR" });
|
|
24
24
|
}
|
|
@@ -29,8 +29,8 @@ function denied(res, principalId, permission, reason) {
|
|
|
29
29
|
function bodyOf(req) {
|
|
30
30
|
return req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
|
|
31
31
|
}
|
|
32
|
-
/** Mount all eight endpoints. The returned disposer closes active feeds and cancels their schedules.
|
|
33
|
-
|
|
32
|
+
/** Mount all eight endpoints. The returned disposer closes active feeds and cancels their schedules.
|
|
33
|
+
* Injected policy and services preserve the settings HTTP response envelopes.
|
|
34
34
|
* O(1) mounting; read/mutation work is delegated. Feed resources are bounded per connection.
|
|
35
35
|
* @example registerSettingsRoutes({ app, workspaceId, ready, service, changeFeed, principalResolver, authorize, scheduler, routes, permissions });
|
|
36
36
|
*/
|
|
@@ -47,36 +47,16 @@ export function registerSettingsRoutes(required, optional = {}) {
|
|
|
47
47
|
res.status(404).json({ error: "workspace was not found" });
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
await required.ready;
|
|
53
|
-
principal = await required.principalResolver({ request: req, response: res });
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Readiness/principal failures preserve the authentication boundary's existing 401 envelope.
|
|
57
|
-
res.status(401).json({ error: "unauthenticated", code: "UNAUTHENTICATED" });
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
50
|
+
const principal = await resolvePrincipal(required, { request: req, response: res });
|
|
60
51
|
if (!principal) {
|
|
61
|
-
res
|
|
52
|
+
unauthenticated(res);
|
|
62
53
|
return;
|
|
63
54
|
}
|
|
64
55
|
try {
|
|
65
56
|
await handler(req, res, principal.id);
|
|
66
57
|
}
|
|
67
58
|
catch (error) {
|
|
68
|
-
|
|
69
|
-
// Reporting cannot prevent ending a response whose headers have already been sent.
|
|
70
|
-
try {
|
|
71
|
-
optional.onError?.({ operation: "settings-request", error });
|
|
72
|
-
}
|
|
73
|
-
catch { /* Host diagnostics are best-effort. */ }
|
|
74
|
-
finally {
|
|
75
|
-
res.end();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else
|
|
79
|
-
respondToSettingsError({ response: res, error, mappings });
|
|
59
|
+
failRequest({ response: res, error, mappings }, optional);
|
|
80
60
|
}
|
|
81
61
|
};
|
|
82
62
|
const check = async (res, principalId, permission, entityType) => {
|
|
@@ -152,13 +132,12 @@ export function registerSettingsRoutes(required, optional = {}) {
|
|
|
152
132
|
}, DEFINITION_ERRORS));
|
|
153
133
|
const mutate = (operation) => async (req, res, callerPrincipalId) => {
|
|
154
134
|
const body = bodyOf(req);
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!namespace || !key || !scopes.includes(scope) || (operation === "set" && !("valueJson" in body))) {
|
|
159
|
-
validation(res, operation === "set" ? "namespace, key, scope (global|workspace|user), and valueJson are required" : "namespace, key, and scope (global|workspace|user) are required");
|
|
135
|
+
const fields = readValueMutation(body, operation);
|
|
136
|
+
if (!fields) {
|
|
137
|
+
validation(res, VALUE_MUTATION_REQUIRED[operation]);
|
|
160
138
|
return;
|
|
161
139
|
}
|
|
140
|
+
const { namespace, key, scope } = fields;
|
|
162
141
|
const target = resolveTargetWorkspaceId({ workspaceId, input: { bodyWorkspaceId: body.workspaceId, scope } });
|
|
163
142
|
if (!target.ok) {
|
|
164
143
|
validation(res, target.error);
|
|
@@ -178,7 +157,7 @@ export function registerSettingsRoutes(required, optional = {}) {
|
|
|
178
157
|
const body = bodyOf(req);
|
|
179
158
|
const namespace = String(body.namespace ?? "");
|
|
180
159
|
const scope = body.scope;
|
|
181
|
-
if (!namespace || !
|
|
160
|
+
if (!namespace || !SETTING_SCOPES.includes(scope)) {
|
|
182
161
|
validation(res, "namespace and scope (global|workspace|user) are required");
|
|
183
162
|
return;
|
|
184
163
|
}
|
|
@@ -198,12 +177,10 @@ export function registerSettingsRoutes(required, optional = {}) {
|
|
|
198
177
|
const close = await openSettingsChangeStream({ request: req, response: res, workspaceId, principalId,
|
|
199
178
|
changeFeed: required.changeFeed, authorize: required.authorize, permission: permissions.read, scheduler: required.scheduler,
|
|
200
179
|
onClose: ({ close }) => { streams.delete(close); }, isDisposed: () => disposed }, optional);
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
streams.add(close);
|
|
206
|
-
}
|
|
180
|
+
if (disposed)
|
|
181
|
+
close();
|
|
182
|
+
else if (!res.destroyed && !res.writableEnded)
|
|
183
|
+
streams.add(close);
|
|
207
184
|
}));
|
|
208
185
|
return { dispose: () => { disposed = true; for (const close of streams)
|
|
209
186
|
close(); streams.clear(); } };
|
|
@@ -14,8 +14,8 @@ interface StreamRequired {
|
|
|
14
14
|
}) => void;
|
|
15
15
|
isDisposed: () => boolean;
|
|
16
16
|
}
|
|
17
|
-
/** Starts after HTTP authorization. All scheduling and storage are injected.
|
|
18
|
-
|
|
17
|
+
/** Starts after HTTP authorization. All scheduling and storage are injected.
|
|
18
|
+
* Poll with bounded pages, tenant-scoped ids and retryable reads; close cancels all schedules.
|
|
19
19
|
* O(page size) per tick and O(1) connection state; repository effects live behind the feed port.
|
|
20
20
|
* Invalid scheduler intervals/page sizes throw RangeError before opening; initial read errors propagate.
|
|
21
21
|
*/
|
|
@@ -1,29 +1,16 @@
|
|
|
1
|
-
/** Starts after HTTP authorization. All scheduling and storage are injected.
|
|
2
|
-
|
|
1
|
+
/** Starts after HTTP authorization. All scheduling and storage are injected.
|
|
2
|
+
* Poll with bounded pages, tenant-scoped ids and retryable reads; close cancels all schedules.
|
|
3
3
|
* O(page size) per tick and O(1) connection state; repository effects live behind the feed port.
|
|
4
4
|
* Invalid scheduler intervals/page sizes throw RangeError before opening; initial read errors propagate.
|
|
5
5
|
*/
|
|
6
6
|
export async function openSettingsChangeStream(required, optional = {}) {
|
|
7
7
|
const { request, response, workspaceId, principalId, changeFeed, scheduler } = required;
|
|
8
|
-
const pageSize = optional
|
|
9
|
-
const intervals = [optional.pollIntervalMs ?? 1_000, optional.keepaliveIntervalMs ?? 25_000, optional.reauthorizeIntervalMs ?? 30_000];
|
|
10
|
-
if (!Number.isSafeInteger(pageSize) || pageSize <= 0 || intervals.some((n) => !Number.isFinite(n) || n <= 0)) {
|
|
11
|
-
throw new RangeError("settings feed page size and intervals must be positive");
|
|
12
|
-
}
|
|
8
|
+
const { pageSize, pollIntervalMs, keepaliveIntervalMs, reauthorizeIntervalMs } = readStreamTimings(optional);
|
|
13
9
|
let closed = false;
|
|
14
10
|
let ticking = false;
|
|
15
11
|
let checkingAuthorization = false;
|
|
16
12
|
const cancellations = [];
|
|
17
|
-
const reportError = (
|
|
18
|
-
// Reporting is best-effort: a host hook must never turn recovery into an unhandled rejection.
|
|
19
|
-
try {
|
|
20
|
-
if (optional.onError)
|
|
21
|
-
optional.onError({ operation, error });
|
|
22
|
-
else
|
|
23
|
-
console.error(`[settings] change feed ${operation} failed`, error);
|
|
24
|
-
}
|
|
25
|
-
catch { /* Diagnostic failures cannot interrupt stream cleanup or retries. */ }
|
|
26
|
-
};
|
|
13
|
+
const reportError = createErrorReporter(optional);
|
|
27
14
|
const recover = (operation, effect) => {
|
|
28
15
|
try {
|
|
29
16
|
effect();
|
|
@@ -52,18 +39,15 @@ export async function openSettingsChangeStream(required, optional = {}) {
|
|
|
52
39
|
response.on("close", close);
|
|
53
40
|
try {
|
|
54
41
|
const head = await changeFeed.head({});
|
|
55
|
-
|
|
42
|
+
const gone = () => closed || required.isDisposed() || response.destroyed || response.writableEnded;
|
|
43
|
+
if (gone()) {
|
|
56
44
|
close();
|
|
57
45
|
return close;
|
|
58
46
|
}
|
|
59
|
-
|
|
60
|
-
let cursor = Number.isFinite(resumeFrom) && resumeFrom >= 0 ? Math.min(resumeFrom, head) : head;
|
|
47
|
+
let cursor = resumeCursor({ lastEventId: request.headers["last-event-id"], head });
|
|
61
48
|
let lastEmittedId = cursor;
|
|
62
49
|
const viewer = { workspaceId, principalId };
|
|
63
|
-
response.writeHead(200, {
|
|
64
|
-
"Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform",
|
|
65
|
-
...(request.httpVersionMajor < 2 ? { Connection: "keep-alive" } : {}), "X-Accel-Buffering": "no",
|
|
66
|
-
});
|
|
50
|
+
response.writeHead(200, sseHeaders({ httpVersionMajor: request.httpVersionMajor }));
|
|
67
51
|
const tick = async () => {
|
|
68
52
|
if (closed || ticking)
|
|
69
53
|
return;
|
|
@@ -116,8 +100,8 @@ export async function openSettingsChangeStream(required, optional = {}) {
|
|
|
116
100
|
else
|
|
117
101
|
cancellations.push(cancel);
|
|
118
102
|
};
|
|
119
|
-
schedule(
|
|
120
|
-
schedule(
|
|
103
|
+
schedule(pollIntervalMs, () => { void tick(); });
|
|
104
|
+
schedule(keepaliveIntervalMs, () => {
|
|
121
105
|
if (closed)
|
|
122
106
|
return;
|
|
123
107
|
try {
|
|
@@ -128,7 +112,7 @@ export async function openSettingsChangeStream(required, optional = {}) {
|
|
|
128
112
|
close();
|
|
129
113
|
}
|
|
130
114
|
});
|
|
131
|
-
schedule(
|
|
115
|
+
schedule(reauthorizeIntervalMs, () => { void reauthorize(); });
|
|
132
116
|
if (!closed)
|
|
133
117
|
response.write(": connected\n\n");
|
|
134
118
|
return close;
|
|
@@ -144,4 +128,42 @@ export async function openSettingsChangeStream(required, optional = {}) {
|
|
|
144
128
|
throw error;
|
|
145
129
|
}
|
|
146
130
|
}
|
|
131
|
+
/** Fill the documented defaults, then refuse any non-positive page size or interval before an effect. Pure. */
|
|
132
|
+
function readStreamTimings(optional) {
|
|
133
|
+
const timings = {
|
|
134
|
+
pageSize: optional.revisionPageSize ?? 200,
|
|
135
|
+
pollIntervalMs: optional.pollIntervalMs ?? 1_000,
|
|
136
|
+
keepaliveIntervalMs: optional.keepaliveIntervalMs ?? 25_000,
|
|
137
|
+
reauthorizeIntervalMs: optional.reauthorizeIntervalMs ?? 30_000,
|
|
138
|
+
};
|
|
139
|
+
const intervals = [timings.pollIntervalMs, timings.keepaliveIntervalMs, timings.reauthorizeIntervalMs];
|
|
140
|
+
if (!Number.isSafeInteger(timings.pageSize) || timings.pageSize <= 0 || intervals.some((n) => !Number.isFinite(n) || n <= 0)) {
|
|
141
|
+
throw new RangeError("settings feed page size and intervals must be positive");
|
|
142
|
+
}
|
|
143
|
+
return timings;
|
|
144
|
+
}
|
|
145
|
+
function createErrorReporter(optional) {
|
|
146
|
+
return (operation, error) => {
|
|
147
|
+
// Reporting is best-effort: a host hook must never turn recovery into an unhandled rejection.
|
|
148
|
+
try {
|
|
149
|
+
if (optional.onError)
|
|
150
|
+
optional.onError({ operation, error });
|
|
151
|
+
else
|
|
152
|
+
console.error(`[settings] change feed ${operation} failed`, error);
|
|
153
|
+
}
|
|
154
|
+
catch { /* Diagnostic failures cannot interrupt stream cleanup or retries. */ }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/** A client's Last-Event-ID resumes at or before the ledger head; a missing or unusable id starts at head. Pure. */
|
|
158
|
+
function resumeCursor(required) {
|
|
159
|
+
const resumeFrom = Number(required.lastEventId);
|
|
160
|
+
return Number.isFinite(resumeFrom) && resumeFrom >= 0 ? Math.min(resumeFrom, required.head) : required.head;
|
|
161
|
+
}
|
|
162
|
+
/** HTTP/2 forbids the Connection header; HTTP/1 needs it to keep the stream open. Pure. */
|
|
163
|
+
function sseHeaders(required) {
|
|
164
|
+
return {
|
|
165
|
+
"Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform",
|
|
166
|
+
...(required.httpVersionMajor < 2 ? { Connection: "keep-alive" } : {}), "X-Accel-Buffering": "no",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
147
169
|
//# sourceMappingURL=stream.js.map
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* `SettingsRepoPort` this barrel exports.
|
|
12
12
|
*/
|
|
13
13
|
export { SCOPE_BIT, type SettingScope, type SettingOwnerKind, type DefinitionStatus, type ValueState, type RevisionEntityKind, type RevisionOp, type SettingValueSchema, type SettingDefinitionRecord, type SettingValueRecord, type SettingRevisionRecord, type SettingScopeContext, } from "./types.js";
|
|
14
|
-
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
14
|
+
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, SettingsRevisionConflictError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
15
15
|
export type { SettingsRepoPort } from "./ports.js";
|
|
16
16
|
export { InMemorySettingsRepo } from "./repo.memory.js";
|
|
17
17
|
export { type DefinitionInput, validateDefinitionInput, validateValueAgainstSchema, registerCoercer, invalidateDefinitionNamespaceCache, invalidateWorkspaceSettingsCache, resolveDefinitionRaw, resolveDefinition, type ResolvedSetting, getEffective, } from "./settings.js";
|
|
18
|
-
export { type AuthorizeFn, type SettingsWriteServiceDeps, deriveRequiredPermission, type RegisterDefinitionsRequired, registerDefinitions, type SetValueRequired, set, type ClearValueRequired, clear, type ResetNamespaceRequired, resetNamespace, type RenameDefinitionRequired, renameDefinition, type RetypeDefinitionRequired, retypeDefinition, type ReconcileDefinitionDefaultRequired, reconcileDefinitionDefault, type DeprecateDefinitionRequired, deprecateDefinition, type TombstoneDefinitionRequired, tombstoneDefinition, } from "./write-service.js";
|
|
18
|
+
export { type AuthorizeFn, type SettingsWriteServiceDeps, deriveRequiredPermission, type RegisterDefinitionsRequired, registerDefinitions, type SetValueRequired, type SettingsValueWriteOptions, set, type ClearValueRequired, clear, type ResetNamespaceRequired, resetNamespace, type RenameDefinitionRequired, renameDefinition, type RetypeDefinitionRequired, retypeDefinition, type ReconcileDefinitionDefaultRequired, reconcileDefinitionDefault, type DeprecateDefinitionRequired, deprecateDefinition, type TombstoneDefinitionRequired, tombstoneDefinition, } from "./write-service.js";
|
|
19
19
|
export { type PurgeServiceDeps, type PurgeTenantSettingsRequired, purgeTenantSettings, } from "./purge-service.js";
|
|
20
20
|
export { type ChangeFeedViewer, isRevisionVisibleTo, type ChangeFeedBatch, collectChangedNamespaces, } from "./change-feed.js";
|
|
21
21
|
export { type DefinitionOpRequestItem, type DefinitionOpContext, type DefinitionOpHandler, NON_REGISTER_DEFINITION_OP_NAMES, type NonRegisterDefinitionOp, parseNonRegisterDefinitionOp, NON_REGISTER_DEFINITION_OPS, } from "./definitions-dispatch.js";
|
package/dist/settings/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `SettingsRepoPort` this barrel exports.
|
|
12
12
|
*/
|
|
13
13
|
export { SCOPE_BIT, } from "./types.js";
|
|
14
|
-
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
14
|
+
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, SettingsRevisionConflictError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
15
15
|
export { InMemorySettingsRepo } from "./repo.memory.js";
|
|
16
16
|
export { validateDefinitionInput, validateValueAgainstSchema, registerCoercer, invalidateDefinitionNamespaceCache, invalidateWorkspaceSettingsCache, resolveDefinitionRaw, resolveDefinition, getEffective, } from "./settings.js";
|
|
17
17
|
export { deriveRequiredPermission, registerDefinitions, set, clear, resetNamespace, renameDefinition, retypeDefinition, reconcileDefinitionDefault, deprecateDefinition, tombstoneDefinition, } from "./write-service.js";
|
package/dist/settings/ports.d.ts
CHANGED
|
@@ -68,7 +68,13 @@ export interface SettingsRepoPort {
|
|
|
68
68
|
principalId: UUID;
|
|
69
69
|
settingId: UUID;
|
|
70
70
|
}): Promise<void>;
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Appends a revision and returns its assigned `seq` (used to stamp the paired value row).
|
|
73
|
+
* Inside a transaction, serialize revision writers until that enclosing transaction ends:
|
|
74
|
+
* `set`/`clear` compare the stored value's seq AFTER this call, so the lock must cover the
|
|
75
|
+
* comparison and value save too. SQLite's write transaction and Postgres's transaction-scoped
|
|
76
|
+
* ledger lock provide this; memory refuses overlapping transactions.
|
|
77
|
+
*/
|
|
72
78
|
appendRevision(record: Omit<SettingRevisionRecord, "seq">): Promise<number>;
|
|
73
79
|
listRevisions(required: {
|
|
74
80
|
settingId: UUID;
|
|
@@ -179,8 +179,11 @@ export class InMemorySettingsRepo {
|
|
|
179
179
|
nextSeq: this.nextSeq,
|
|
180
180
|
};
|
|
181
181
|
this.inTransaction = true;
|
|
182
|
+
// No `finally`: the flag is released on both exits explicitly (V8 coverage counts the
|
|
183
|
+
// unreachable continuation after a catch-block `throw` inside try/finally as a missed branch).
|
|
184
|
+
let result;
|
|
182
185
|
try {
|
|
183
|
-
|
|
186
|
+
result = await fn();
|
|
184
187
|
}
|
|
185
188
|
catch (error) {
|
|
186
189
|
this.definitions = snapshot.definitions;
|
|
@@ -189,11 +192,11 @@ export class InMemorySettingsRepo {
|
|
|
189
192
|
this.userValues = snapshot.userValues;
|
|
190
193
|
this.revisions = snapshot.revisions;
|
|
191
194
|
this.nextSeq = snapshot.nextSeq;
|
|
192
|
-
throw error;
|
|
193
|
-
}
|
|
194
|
-
finally {
|
|
195
195
|
this.inTransaction = false;
|
|
196
|
+
throw error;
|
|
196
197
|
}
|
|
198
|
+
this.inTransaction = false;
|
|
199
|
+
return result;
|
|
197
200
|
}
|
|
198
201
|
}
|
|
199
202
|
//# sourceMappingURL=repo.memory.js.map
|
|
@@ -142,35 +142,36 @@ export function invalidateWorkspaceSettingsCache(_required, _optional = {}) {
|
|
|
142
142
|
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
143
143
|
*/
|
|
144
144
|
export async function resolveDefinitionRaw(deps, input) {
|
|
145
|
+
const found = await findInCallerOrPlatformPartition(deps.repo, input);
|
|
146
|
+
if (found?.status !== "alias")
|
|
147
|
+
return found ?? null;
|
|
148
|
+
const cursor = aliasTarget(found, input);
|
|
149
|
+
if (!cursor)
|
|
150
|
+
return null;
|
|
151
|
+
const target = await findInCallerOrPlatformPartition(deps.repo, cursor);
|
|
152
|
+
// An alias whose target is itself an alias is a chain deeper than one: absent.
|
|
153
|
+
return target?.status === "alias" ? null : target ?? null;
|
|
154
|
+
}
|
|
155
|
+
/** One alias hop only. A malformed target or a self-alias (the only cycle reachable in one hop)
|
|
156
|
+
* fails closed as absent (`null`) without a second lookup. The hop stays in the caller's workspace. O(1). */
|
|
157
|
+
function aliasTarget(alias, input) {
|
|
158
|
+
if (alias.aliasOfNamespace == null || alias.aliasOfKey == null)
|
|
159
|
+
return null;
|
|
160
|
+
if (alias.aliasOfNamespace === input.namespace && alias.aliasOfKey === input.key)
|
|
161
|
+
return null;
|
|
162
|
+
return { namespace: alias.aliasOfNamespace, key: alias.aliasOfKey, workspaceId: input.workspaceId };
|
|
163
|
+
}
|
|
164
|
+
/** One coordinate lookup: the caller's partition, then the platform (null) partition. O(1) repo calls (at most two). */
|
|
165
|
+
async function findInCallerOrPlatformPartition(repo, cursor) {
|
|
145
166
|
// Namespace fencing makes `site.*` (non-null workspace_id) and. See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
146
167
|
// `core.*`/`theme.*` (null workspace_id) disjoint by construction, so a
|
|
147
168
|
// caller resolving inside a workspace context may still be asking for a
|
|
148
169
|
// platform definition. Try the caller's own partition first, then fall
|
|
149
170
|
// back to the platform (null) partition.
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (visited.has(address))
|
|
155
|
-
return null;
|
|
156
|
-
visited.add(address);
|
|
157
|
-
const found = (await deps.repo.findActiveDefinition(cursor)) ??
|
|
158
|
-
(cursor.workspaceId !== null
|
|
159
|
-
? await deps.repo.findActiveDefinition({ ...cursor, workspaceId: null })
|
|
160
|
-
: null);
|
|
161
|
-
if (!found)
|
|
162
|
-
return null;
|
|
163
|
-
if (found.status !== "alias")
|
|
164
|
-
return found;
|
|
165
|
-
if (depth === 1 || found.aliasOfNamespace == null || found.aliasOfKey == null)
|
|
166
|
-
return null;
|
|
167
|
-
cursor = {
|
|
168
|
-
namespace: found.aliasOfNamespace,
|
|
169
|
-
key: found.aliasOfKey,
|
|
170
|
-
workspaceId: input.workspaceId,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
171
|
+
const own = await repo.findActiveDefinition(cursor);
|
|
172
|
+
if (own || cursor.workspaceId === null)
|
|
173
|
+
return own;
|
|
174
|
+
return repo.findActiveDefinition({ ...cursor, workspaceId: null });
|
|
174
175
|
}
|
|
175
176
|
/**
|
|
176
177
|
* The read-path resolver: typed-absent (`null`) for a tombstoned or
|
|
@@ -215,45 +216,38 @@ export async function getEffective(deps, input) {
|
|
|
215
216
|
});
|
|
216
217
|
if (!definition)
|
|
217
218
|
return null;
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
const hit = await firstSetLayer(deps.repo, definition.settingId, input.scopeContext);
|
|
220
|
+
if (hit)
|
|
221
|
+
return { value: coerceToVersion(definition, hit.record), sourceLayer: hit.sourceLayer, defVersion: definition.version };
|
|
222
|
+
return { value: definition.defaultValue, sourceLayer: "default", defVersion: definition.version };
|
|
223
|
+
}
|
|
224
|
+
/** A value stored under an older definition version is coerced in memory (no write-back). O(1). */
|
|
225
|
+
function coerceToVersion(definition, record) {
|
|
226
|
+
if (record.defVersion === definition.version)
|
|
227
|
+
return record.valueJson;
|
|
228
|
+
const coercer = coercers.get(definition.coercionTag ?? "identity") ?? coercers.get("identity");
|
|
229
|
+
return coercer({ value: record.valueJson });
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Precedence `user ?? workspace ?? global`; a `cleared` row is absent at its layer. The user layer
|
|
233
|
+
* needs both a workspace and a principal; the workspace layer needs a workspace.
|
|
234
|
+
* O(1): up to three repo reads, in precedence order, stopping at the first `set` row.
|
|
235
|
+
*/
|
|
236
|
+
async function firstSetLayer(repo, settingId, scope) {
|
|
224
237
|
// Every layer read below goes straight to the repo. See this module's cache header for why there
|
|
225
238
|
// is no per-layer cache wrapping these three calls.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
value: coerce(userValue.valueJson, userValue.defVersion),
|
|
233
|
-
sourceLayer: "user",
|
|
234
|
-
defVersion: definition.version,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
239
|
+
const { workspaceId, principalId } = scope;
|
|
240
|
+
if (workspaceId && principalId) {
|
|
241
|
+
const user = await repo.getUserValue({ workspaceId, principalId, settingId });
|
|
242
|
+
if (user?.state === "set")
|
|
243
|
+
return { record: user, sourceLayer: "user" };
|
|
237
244
|
}
|
|
238
|
-
if (
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
value: coerce(workspaceValue.valueJson, workspaceValue.defVersion),
|
|
244
|
-
sourceLayer: "workspace",
|
|
245
|
-
defVersion: definition.version,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
245
|
+
if (workspaceId) {
|
|
246
|
+
const workspace = await repo.getWorkspaceValue({ workspaceId, settingId });
|
|
247
|
+
if (workspace?.state === "set")
|
|
248
|
+
return { record: workspace, sourceLayer: "workspace" };
|
|
248
249
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
value: coerce(globalValue.valueJson, globalValue.defVersion),
|
|
253
|
-
sourceLayer: "global",
|
|
254
|
-
defVersion: definition.version,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
return { value: definition.defaultValue, sourceLayer: "default", defVersion: definition.version };
|
|
250
|
+
const global = await repo.getGlobalValue(settingId);
|
|
251
|
+
return global?.state === "set" ? { record: global, sourceLayer: "global" } : null;
|
|
258
252
|
}
|
|
259
253
|
//# sourceMappingURL=settings.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { IdGenerator } from "../primitives/index.js";
|
|
2
|
-
import type { Clock } from "../primitives/index.js";
|
|
1
|
+
import type { Clock, IdGenerator } from "../primitives/index.js";
|
|
3
2
|
/**
|
|
4
3
|
* @file Settings tools: three reads and three bounded value writes. Bulk reset and definition
|
|
5
4
|
* registration stay unwired. The generic tools reuse write-service authorization/validation and
|
|
@@ -12,7 +11,7 @@ import type { LegacyToolAuthorizeFn as AuthorizeFn } from "../tool-permissions.j
|
|
|
12
11
|
import { type SettingsWriteConfirmation } from "./agent-value-write-tools.js";
|
|
13
12
|
import { type AgentSettingWriteRule } from "./agent-write-denylist.js";
|
|
14
13
|
import { type DerivedRiskByToolId } from "../registration-kit.js";
|
|
15
|
-
import type
|
|
14
|
+
import { type ToolExecutionOptions, type ToolRegistration } from "../tool-registry.js";
|
|
16
15
|
import type { SettingsPrincipalLookupPort } from "./principal-lookup.js";
|
|
17
16
|
import type { SettingsRepoPort } from "./ports.js";
|
|
18
17
|
import { set as setSettingValue } from "./write-service.js";
|