@zhin.js/gateway 0.0.0-stage → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -2
- package/lib/config/ai-config-loader.d.ts +22 -0
- package/lib/config/ai-config-loader.js +96 -0
- package/lib/config/configuration-chat-policy.d.ts +57 -0
- package/lib/config/configuration-chat-policy.js +423 -0
- package/lib/config/configuration-patch.d.ts +16 -0
- package/lib/config/configuration-patch.js +105 -0
- package/lib/config/configuration-path.d.ts +6 -0
- package/lib/config/configuration-path.js +40 -0
- package/lib/config/configuration-projection.d.ts +6 -0
- package/lib/config/configuration-projection.js +28 -0
- package/lib/config/configuration-redaction.d.ts +24 -0
- package/lib/config/configuration-redaction.js +128 -0
- package/lib/config/configuration.d.ts +81 -0
- package/lib/config/configuration.js +686 -0
- package/lib/config/console-schema.d.ts +11 -0
- package/lib/config/console-schema.js +222 -0
- package/lib/config/environment-files.d.ts +2 -0
- package/lib/config/environment-files.js +21 -0
- package/lib/config/plugin-package-map.d.ts +2 -0
- package/lib/config/plugin-package-map.js +14 -0
- package/lib/config/plugin-schema-catalog.d.ts +22 -0
- package/lib/config/plugin-schema-catalog.js +262 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.js +12 -0
- package/lib/database/config.d.ts +22 -0
- package/lib/database/config.js +159 -0
- package/lib/database/index.d.ts +2 -0
- package/lib/database/index.js +2 -0
- package/lib/database/project-database.d.ts +31 -0
- package/lib/database/project-database.js +130 -0
- package/lib/database.d.ts +2 -0
- package/lib/database.js +2 -0
- package/lib/http/console-rpc.d.ts +19 -0
- package/lib/http/console-rpc.js +51 -0
- package/lib/http/gateway-console.d.ts +63 -0
- package/lib/http/gateway-console.js +309 -0
- package/lib/http/gateway-events.d.ts +35 -0
- package/lib/http/gateway-events.js +172 -0
- package/lib/http/gateway-http-server.d.ts +95 -0
- package/lib/http/gateway-http-server.js +518 -0
- package/lib/http/local-principal.d.ts +2 -0
- package/lib/http/local-principal.js +16 -0
- package/lib/http/run-gateway.d.ts +43 -0
- package/lib/http/run-gateway.js +268 -0
- package/lib/http/start-gateway.d.ts +114 -0
- package/lib/http/start-gateway.js +220 -0
- package/lib/http-response.d.ts +6 -0
- package/lib/http-response.js +25 -0
- package/lib/http.d.ts +6 -0
- package/lib/http.js +6 -0
- package/lib/iam/crypto.d.ts +12 -0
- package/lib/iam/crypto.js +58 -0
- package/lib/iam/index.d.ts +8 -0
- package/lib/iam/index.js +8 -0
- package/lib/iam/install.d.ts +36 -0
- package/lib/iam/install.js +74 -0
- package/lib/iam/local-admin.d.ts +27 -0
- package/lib/iam/local-admin.js +102 -0
- package/lib/iam/rate-limit.d.ts +15 -0
- package/lib/iam/rate-limit.js +52 -0
- package/lib/iam/routes.d.ts +23 -0
- package/lib/iam/routes.js +157 -0
- package/lib/iam/rpc.d.ts +14 -0
- package/lib/iam/rpc.js +100 -0
- package/lib/iam/service.d.ts +203 -0
- package/lib/iam/service.js +784 -0
- package/lib/iam/setup-service.d.ts +83 -0
- package/lib/iam/setup-service.js +206 -0
- package/lib/iam/tables.d.ts +20 -0
- package/lib/iam/tables.js +82 -0
- package/lib/iam.d.ts +2 -0
- package/lib/iam.js +2 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +22 -0
- package/lib/local/client.d.ts +71 -0
- package/lib/local/client.js +296 -0
- package/lib/local/handler.d.ts +30 -0
- package/lib/local/handler.js +86 -0
- package/lib/local/local-socket.d.ts +55 -0
- package/lib/local/local-socket.js +232 -0
- package/lib/local/paths.d.ts +34 -0
- package/lib/local/paths.js +58 -0
- package/lib/local/protocol.d.ts +140 -0
- package/lib/local/protocol.js +90 -0
- package/lib/local/state-file.d.ts +8 -0
- package/lib/local/state-file.js +45 -0
- package/lib/local.d.ts +10 -0
- package/lib/local.js +10 -0
- package/lib/onboarding/allowlist.d.ts +54 -0
- package/lib/onboarding/allowlist.js +106 -0
- package/lib/onboarding/errors.d.ts +7 -0
- package/lib/onboarding/errors.js +17 -0
- package/lib/onboarding/index.d.ts +6 -0
- package/lib/onboarding/index.js +6 -0
- package/lib/onboarding/install.d.ts +99 -0
- package/lib/onboarding/install.js +132 -0
- package/lib/onboarding/package-manager.d.ts +47 -0
- package/lib/onboarding/package-manager.js +179 -0
- package/lib/onboarding/service.d.ts +53 -0
- package/lib/onboarding/service.js +144 -0
- package/lib/onboarding/state-store.d.ts +26 -0
- package/lib/onboarding/state-store.js +97 -0
- package/lib/onboarding.d.ts +2 -0
- package/lib/onboarding.js +2 -0
- package/lib/packages/authorization.d.ts +23 -0
- package/lib/packages/authorization.js +109 -0
- package/lib/packages/errors.d.ts +11 -0
- package/lib/packages/errors.js +26 -0
- package/lib/packages/index.d.ts +5 -0
- package/lib/packages/index.js +5 -0
- package/lib/packages/rpc.d.ts +30 -0
- package/lib/packages/rpc.js +155 -0
- package/lib/packages/service.d.ts +121 -0
- package/lib/packages/service.js +441 -0
- package/lib/packages/snapshot.d.ts +14 -0
- package/lib/packages/snapshot.js +73 -0
- package/lib/packages.d.ts +2 -0
- package/lib/packages.js +2 -0
- package/lib/plugin-management/activity.d.ts +6 -0
- package/lib/plugin-management/activity.js +14 -0
- package/lib/plugin-management/config-explanation.d.ts +6 -0
- package/lib/plugin-management/config-explanation.js +113 -0
- package/lib/plugin-management/use-cases.d.ts +15 -0
- package/lib/plugin-management/use-cases.js +120 -0
- package/lib/plugin-management/workflow.d.ts +92 -0
- package/lib/plugin-management/workflow.js +772 -0
- package/lib/plugin-management.d.ts +5 -0
- package/lib/plugin-management.js +5 -0
- package/lib/project/config-file.d.ts +6 -0
- package/lib/project/config-file.js +58 -0
- package/lib/project/environment.d.ts +13 -0
- package/lib/project/environment.js +52 -0
- package/lib/project/instance-declarations.d.ts +55 -0
- package/lib/project/instance-declarations.js +542 -0
- package/lib/project/legacy-config.d.ts +25 -0
- package/lib/project/legacy-config.js +70 -0
- package/lib/project/node-requirements.d.ts +7 -0
- package/lib/project/node-requirements.js +20 -0
- package/lib/project/tsx-module-loader.d.ts +7 -0
- package/lib/project/tsx-module-loader.js +29 -0
- package/lib/project.d.ts +7 -0
- package/lib/project.js +7 -0
- package/lib/server.d.ts +11 -0
- package/lib/server.js +11 -0
- package/lib/supervisor/instance-logs.d.ts +39 -0
- package/lib/supervisor/instance-logs.js +152 -0
- package/lib/supervisor/instance-supervisor.d.ts +140 -0
- package/lib/supervisor/instance-supervisor.js +408 -0
- package/lib/supervisor/run-instance.d.ts +35 -0
- package/lib/supervisor/run-instance.js +161 -0
- package/lib/supervisor/terminal-state.d.ts +30 -0
- package/lib/supervisor/terminal-state.js +57 -0
- package/lib/supervisor.d.ts +4 -0
- package/lib/supervisor.js +4 -0
- package/lib/terminal/backends.d.ts +38 -0
- package/lib/terminal/backends.js +193 -0
- package/lib/terminal/config.d.ts +63 -0
- package/lib/terminal/config.js +237 -0
- package/lib/terminal/host.d.ts +26 -0
- package/lib/terminal/host.js +76 -0
- package/lib/terminal/index.d.ts +60 -0
- package/lib/terminal/index.js +135 -0
- package/lib/terminal/modules.d.ts +87 -0
- package/lib/terminal/modules.js +44 -0
- package/lib/terminal/service.d.ts +101 -0
- package/lib/terminal/service.js +455 -0
- package/lib/terminal/tickets.d.ts +46 -0
- package/lib/terminal/tickets.js +76 -0
- package/lib/terminal.d.ts +3 -0
- package/lib/terminal.js +3 -0
- package/lib/usage/aggregate.d.ts +87 -0
- package/lib/usage/aggregate.js +212 -0
- package/lib/usage.d.ts +2 -0
- package/lib/usage.js +2 -0
- package/package.json +116 -3
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { expandEnvironmentValue } from '@zhin.js/runtime';
|
|
2
|
+
import { isUnknownConfigurationSchemaBoundary, sensitiveConfigKey as sensitiveKey } from './configuration-redaction.js';
|
|
3
|
+
const accessMessages = Object.freeze({
|
|
4
|
+
sensitive: '敏感配置不能通过聊天读取或修改,请使用 CLI 或 Console。',
|
|
5
|
+
missing: '配置项不存在。',
|
|
6
|
+
unknown: '无法确认配置范围安全,请使用 CLI 或 Console。',
|
|
7
|
+
permission: '权限相关配置不能通过聊天读取或修改,请使用 CLI 或 Console。',
|
|
8
|
+
scope: '该配置不在聊天可管理范围内,请使用 CLI 或 Console。',
|
|
9
|
+
leaf: '聊天只能读写单个叶子字段(标量或标量数组)。',
|
|
10
|
+
required: '必填字段不能通过聊天删除。',
|
|
11
|
+
});
|
|
12
|
+
export class ConfigChatAccessError extends Error {
|
|
13
|
+
kind;
|
|
14
|
+
constructor(kind) {
|
|
15
|
+
super(accessMessages[kind]);
|
|
16
|
+
this.kind = kind;
|
|
17
|
+
this.name = 'ConfigChatAccessError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Role, approval and allow/deny-list vocabulary (C4): `master`, `trusted`,
|
|
22
|
+
* `endpoints[*].master/trusted`, `ai.trigger.masters/trusted`, `toolHardGate.*`, and
|
|
23
|
+
* compound names such as `adminIds`, `ownerId`, `masterId`, `trustedUsers`,
|
|
24
|
+
* `superUsers`, `whitelist`, `allowlist`, `blacklist` or `bypassMasters`.
|
|
25
|
+
* Matched by contains/prefix/suffix after normalization (like `sensitiveConfigKey`) so
|
|
26
|
+
* casing, separators or compounds cannot bypass it. Deliberately fail-closed: an
|
|
27
|
+
* ordinary key that merely contains a role word (e.g. `masterVolume`) is CLI-only.
|
|
28
|
+
*/
|
|
29
|
+
export function isChatPermissionSegment(segment) {
|
|
30
|
+
const normalized = segment.replace(/[^a-z0-9]/giu, '').toLowerCase();
|
|
31
|
+
return /(?:master|owner|admin|trust|approv|permission|permit|privilege|superuser|sudo|operator|moderator|bypass|exempt|toolhardgate)/u.test(normalized)
|
|
32
|
+
|| /(?:allow|deny|denied|white|black|block|ban|bann|safe|ignore)(?:ed)?(?:lists?|users?|userids?|ids?|groups?|senders?|members?)$/u.test(normalized)
|
|
33
|
+
|| /^(?:allow|deny|white|black|block|ban)(?:ed)?(?:lists?)?$/u.test(normalized)
|
|
34
|
+
|| /roles?(?:ids?)?$|^roles?|acls?$|grants?$/u.test(normalized);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Indirect role binding (M2): `endpoints[*].id|name` is the key that binds leaf
|
|
38
|
+
* `master`/`trusted` to an account, so remapping it from chat is a permission change.
|
|
39
|
+
*/
|
|
40
|
+
export function isChatIdentityRemapPath(path) {
|
|
41
|
+
return path.some((segment, index) => segment === 'endpoints' && /^(?:0|[1-9][0-9]*)$/u.test(path[index + 1] ?? '')
|
|
42
|
+
&& /^(?:id|name|instanceid|endpointid)$/u.test((path[index + 2] ?? '').replace(/[^a-z0-9]/giu, '').toLowerCase()));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* M1/B1: a chat `set` never plants a new `${…}` or `env://` reference (even with a
|
|
46
|
+
* non-credential-looking name), since a later `get` or runtime echo would expand it.
|
|
47
|
+
* Existing references are untouched by unrelated operations.
|
|
48
|
+
*/
|
|
49
|
+
export function assertChatValueHasNoEnvironmentReference(value, depth = 0) {
|
|
50
|
+
if (depth > 32)
|
|
51
|
+
throw new ConfigChatAccessError('unknown');
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
if (value.includes('${') || /env:\/\//iu.test(value))
|
|
54
|
+
throw new ConfigChatAccessError('sensitive');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
for (const item of value)
|
|
59
|
+
assertChatValueHasNoEnvironmentReference(item, depth + 1);
|
|
60
|
+
else if (record(value))
|
|
61
|
+
for (const [key, item] of Object.entries(value)) {
|
|
62
|
+
assertChatValueHasNoEnvironmentReference(key, depth + 1);
|
|
63
|
+
assertChatValueHasNoEnvironmentReference(item, depth + 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function startsWith(path, prefix) {
|
|
67
|
+
return prefix.length <= path.length && prefix.every((segment, index) => path[index] === segment);
|
|
68
|
+
}
|
|
69
|
+
/** C3/C4 scope gate, checked before any schema proof or value read. */
|
|
70
|
+
export function assertChatConfigurationScope(path, scope) {
|
|
71
|
+
if (!path.length)
|
|
72
|
+
throw new ConfigChatAccessError('leaf');
|
|
73
|
+
if (scope.declarationKeys.has(path[0]))
|
|
74
|
+
throw new ConfigChatAccessError('scope');
|
|
75
|
+
if (path.some(isChatPermissionSegment) || isChatIdentityRemapPath(path))
|
|
76
|
+
throw new ConfigChatAccessError('permission');
|
|
77
|
+
if (scope.hostKeys.includes(path[0]) && !scope.hostPaths.some(prefix => startsWith(path, prefix))) {
|
|
78
|
+
throw new ConfigChatAccessError('scope');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Whitelisted Host roots reachable from a (possibly shorter) `fields` scope. */
|
|
82
|
+
export function chatHostRoots(prefix, scope) {
|
|
83
|
+
return scope.hostPaths.filter(entry => startsWith(entry, prefix) || startsWith(prefix, entry))
|
|
84
|
+
.map(entry => entry.length >= prefix.length ? entry : prefix);
|
|
85
|
+
}
|
|
86
|
+
function record(value) {
|
|
87
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
function own(value, key) { return Object.hasOwn(value, key); }
|
|
90
|
+
function sensitive(schema) {
|
|
91
|
+
return schema.writeOnly === true || schema.secret === true || schema.format === 'password' || schema.component === 'password'
|
|
92
|
+
|| record(schema['x-zhin-config']) && (schema['x-zhin-config'].secret === true || schema['x-zhin-config'].component === 'password');
|
|
93
|
+
}
|
|
94
|
+
function unknown() { throw new ConfigChatAccessError('unknown'); }
|
|
95
|
+
function assertSafe(nodes) {
|
|
96
|
+
if (nodes.some(sensitive))
|
|
97
|
+
throw new ConfigChatAccessError('sensitive');
|
|
98
|
+
}
|
|
99
|
+
function intersect(left, right) {
|
|
100
|
+
// A combinatorial declaration is uncertain for this deliberately conservative
|
|
101
|
+
// reader, rather than permission to inspect arbitrary configuration values.
|
|
102
|
+
if (left.length * right.length > 64)
|
|
103
|
+
unknown();
|
|
104
|
+
return left.flatMap(a => right.map(b => [...a, ...b]));
|
|
105
|
+
}
|
|
106
|
+
function variants(value, root, seen = new Set()) {
|
|
107
|
+
if (!record(value) || seen.has(value))
|
|
108
|
+
unknown();
|
|
109
|
+
const nextSeen = new Set([...seen, value]);
|
|
110
|
+
if (isUnknownConfigurationSchemaBoundary(value, root)
|
|
111
|
+
|| value.hidden === true || record(value['x-zhin-config']) && value['x-zhin-config'].hidden === true)
|
|
112
|
+
unknown();
|
|
113
|
+
let result = [[value]];
|
|
114
|
+
if (value.$ref !== undefined) {
|
|
115
|
+
if (typeof value.$ref !== 'string' || !value.$ref.startsWith('#/'))
|
|
116
|
+
unknown();
|
|
117
|
+
let referenced = root;
|
|
118
|
+
for (const raw of value.$ref.slice(2).split('/')) {
|
|
119
|
+
if (/~(?![01])/u.test(raw))
|
|
120
|
+
unknown();
|
|
121
|
+
const key = raw.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
122
|
+
if (!record(referenced) || !own(referenced, key))
|
|
123
|
+
unknown();
|
|
124
|
+
referenced = referenced[key];
|
|
125
|
+
}
|
|
126
|
+
result = intersect(result, variants(referenced, root, nextSeen));
|
|
127
|
+
}
|
|
128
|
+
if (value.allOf !== undefined) {
|
|
129
|
+
if (!Array.isArray(value.allOf) || !value.allOf.length)
|
|
130
|
+
unknown();
|
|
131
|
+
for (const branch of value.allOf)
|
|
132
|
+
result = intersect(result, variants(branch, root, nextSeen));
|
|
133
|
+
}
|
|
134
|
+
for (const keyword of ['anyOf', 'oneOf'])
|
|
135
|
+
if (value[keyword] !== undefined) {
|
|
136
|
+
if (!Array.isArray(value[keyword]) || !value[keyword].length)
|
|
137
|
+
unknown();
|
|
138
|
+
result = intersect(result, value[keyword].flatMap(branch => variants(branch, root, nextSeen)));
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
function typeNames(nodes) {
|
|
143
|
+
const types = new Set();
|
|
144
|
+
for (const node of nodes) {
|
|
145
|
+
for (const type of Array.isArray(node.type) ? node.type : [node.type])
|
|
146
|
+
if (typeof type === 'string')
|
|
147
|
+
types.add(type);
|
|
148
|
+
if (record(node.properties) || record(node.patternProperties) || record(node.additionalProperties))
|
|
149
|
+
types.add('object');
|
|
150
|
+
if (node.items !== undefined || node.prefixItems !== undefined)
|
|
151
|
+
types.add('array');
|
|
152
|
+
const examples = own(node, 'const') ? [node.const] : Array.isArray(node.enum) ? node.enum : [];
|
|
153
|
+
for (const value of examples)
|
|
154
|
+
types.add(value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value);
|
|
155
|
+
}
|
|
156
|
+
if (!types.size || [...types].some(type => !['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'].includes(type)))
|
|
157
|
+
unknown();
|
|
158
|
+
return [...types];
|
|
159
|
+
}
|
|
160
|
+
function children(nodes, key) {
|
|
161
|
+
if (sensitiveKey(key))
|
|
162
|
+
throw new ConfigChatAccessError('sensitive');
|
|
163
|
+
const found = [];
|
|
164
|
+
for (const node of nodes) {
|
|
165
|
+
if (node.items !== undefined || node.prefixItems !== undefined || node.type === 'array') {
|
|
166
|
+
if (!/^(?:0|[1-9][0-9]*)$/u.test(key))
|
|
167
|
+
unknown();
|
|
168
|
+
const index = Number(key);
|
|
169
|
+
const modernPrefix = Array.isArray(node.prefixItems) ? node.prefixItems : undefined;
|
|
170
|
+
const legacyPrefix = !modernPrefix && Array.isArray(node.items) ? node.items : undefined;
|
|
171
|
+
const prefix = modernPrefix ?? legacyPrefix;
|
|
172
|
+
const item = prefix && index < prefix.length ? prefix[index] : legacyPrefix ? node.additionalItems : node.items;
|
|
173
|
+
if (item !== undefined)
|
|
174
|
+
found.push(item);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let matched = false;
|
|
178
|
+
if (record(node.properties) && own(node.properties, key)) {
|
|
179
|
+
found.push(node.properties[key]);
|
|
180
|
+
matched = true;
|
|
181
|
+
}
|
|
182
|
+
if (record(node.patternProperties))
|
|
183
|
+
for (const [pattern, schema] of Object.entries(node.patternProperties)) {
|
|
184
|
+
let matches;
|
|
185
|
+
try {
|
|
186
|
+
matches = new RegExp(pattern, 'u').test(key);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
unknown();
|
|
190
|
+
}
|
|
191
|
+
if (matches) {
|
|
192
|
+
found.push(schema);
|
|
193
|
+
matched = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!matched && node.additionalProperties !== undefined)
|
|
197
|
+
found.push(node.additionalProperties);
|
|
198
|
+
}
|
|
199
|
+
if (!found.length)
|
|
200
|
+
unknown();
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
function descend(input, key, root) {
|
|
204
|
+
return input.flatMap(nodes => {
|
|
205
|
+
assertSafe(nodes);
|
|
206
|
+
let result = [[]];
|
|
207
|
+
for (const child of children(nodes, key))
|
|
208
|
+
result = intersect(result, variants(child, root));
|
|
209
|
+
return result;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function locate(root, path) {
|
|
213
|
+
let current = variants(root, root);
|
|
214
|
+
for (const key of path)
|
|
215
|
+
current = descend(current, key, root);
|
|
216
|
+
for (const nodes of current) {
|
|
217
|
+
assertSafe(nodes);
|
|
218
|
+
typeNames(nodes);
|
|
219
|
+
}
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
function inspectTree(current, root, values, seen = new Set()) {
|
|
223
|
+
const allowed = new Set(current.flatMap(typeNames));
|
|
224
|
+
for (const value of values) {
|
|
225
|
+
if (value === undefined)
|
|
226
|
+
continue;
|
|
227
|
+
const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
228
|
+
if (!allowed.has(type) && !(type === 'number' && allowed.has('integer')))
|
|
229
|
+
unknown();
|
|
230
|
+
if (typeof value === 'string' && /^env:\/\//u.test(value) && sensitiveKey(value.slice(6)))
|
|
231
|
+
throw new ConfigChatAccessError('sensitive');
|
|
232
|
+
}
|
|
233
|
+
for (const nodes of current) {
|
|
234
|
+
assertSafe(nodes);
|
|
235
|
+
const types = typeNames(nodes);
|
|
236
|
+
if (!types.includes('object') && !types.includes('array'))
|
|
237
|
+
continue;
|
|
238
|
+
if (nodes.some(node => seen.has(node)))
|
|
239
|
+
unknown();
|
|
240
|
+
const nextSeen = new Set([...seen, ...nodes]);
|
|
241
|
+
const keys = new Set();
|
|
242
|
+
for (const node of nodes) {
|
|
243
|
+
if (record(node.properties))
|
|
244
|
+
for (const key of Object.keys(node.properties))
|
|
245
|
+
keys.add(key);
|
|
246
|
+
if (record(node.patternProperties))
|
|
247
|
+
for (const schema of Object.values(node.patternProperties))
|
|
248
|
+
inspectTree(variants(schema, root), root, [], nextSeen);
|
|
249
|
+
if (record(node.additionalProperties))
|
|
250
|
+
inspectTree(variants(node.additionalProperties, root), root, [], nextSeen);
|
|
251
|
+
if (types.includes('array')) {
|
|
252
|
+
const prefix = Array.isArray(node.prefixItems) ? node.prefixItems : Array.isArray(node.items) ? node.items : [];
|
|
253
|
+
for (const schema of prefix)
|
|
254
|
+
inspectTree(variants(schema, root), root, [], nextSeen);
|
|
255
|
+
if (record(node.items))
|
|
256
|
+
inspectTree(variants(node.items, root), root, [], nextSeen);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const value of values)
|
|
260
|
+
if (record(value) || Array.isArray(value))
|
|
261
|
+
for (const key of Object.keys(value))
|
|
262
|
+
keys.add(key);
|
|
263
|
+
if (!keys.size && types.includes('object') && !nodes.some(node => record(node.properties) || record(node.additionalProperties) || node.additionalProperties === false))
|
|
264
|
+
unknown();
|
|
265
|
+
if (types.includes('array') && !nodes.some(node => record(node.items) || Array.isArray(node.prefixItems) || Array.isArray(node.items) || node.items === false))
|
|
266
|
+
unknown();
|
|
267
|
+
for (const key of keys) {
|
|
268
|
+
const childValues = values.map(value => value !== null && typeof value === 'object' && Object.hasOwn(value, key) ? value[key] : undefined);
|
|
269
|
+
inspectTree(descend([nodes], key, root), root, childValues, nextSeen);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** Raw JSON Schema only. Unknown branches never confer access. No values are expanded. */
|
|
274
|
+
export function assertChatConfigurationAccess(schema, path, values, deep = true, absolutePath = path) {
|
|
275
|
+
if (absolutePath.some(sensitiveKey))
|
|
276
|
+
throw new ConfigChatAccessError('sensitive');
|
|
277
|
+
if (!record(schema))
|
|
278
|
+
unknown();
|
|
279
|
+
const current = locate(schema, path);
|
|
280
|
+
const allowed = new Set(current.flatMap(typeNames));
|
|
281
|
+
for (const value of values)
|
|
282
|
+
if (value !== undefined) {
|
|
283
|
+
const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
284
|
+
if (!allowed.has(type) && !(type === 'number' && allowed.has('integer')))
|
|
285
|
+
unknown();
|
|
286
|
+
}
|
|
287
|
+
if (deep) {
|
|
288
|
+
expandEnvironmentValue(values, name => { if (sensitiveKey(name))
|
|
289
|
+
throw new ConfigChatAccessError('sensitive'); return undefined; }, () => '');
|
|
290
|
+
for (const value of values)
|
|
291
|
+
if (typeof value === 'string' && /^env:\/\//u.test(value)
|
|
292
|
+
&& sensitiveKey(value.slice(6)))
|
|
293
|
+
throw new ConfigChatAccessError('sensitive');
|
|
294
|
+
inspectTree(current, schema, values);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const scalarTypes = new Set(['string', 'number', 'integer', 'boolean', 'null']);
|
|
298
|
+
/**
|
|
299
|
+
* Leaf-only chat writes: the target must resolve to scalars or arrays of scalars in
|
|
300
|
+
* every schema branch. Object targets would let a parent overwrite/removal bypass
|
|
301
|
+
* per-child checks, so they are rejected as `leaf`.
|
|
302
|
+
*/
|
|
303
|
+
export function chatLeafTypes(schema, path, values = []) {
|
|
304
|
+
if (!record(schema))
|
|
305
|
+
unknown();
|
|
306
|
+
const current = locate(schema, path);
|
|
307
|
+
// Desired files can be edited outside the Host. Schema bounds do not prove
|
|
308
|
+
// that the currently stored array actually contains only scalar values.
|
|
309
|
+
for (const value of values)
|
|
310
|
+
if (Array.isArray(value))
|
|
311
|
+
for (const item of value) {
|
|
312
|
+
if (!scalarTypes.has(item === null ? 'null' : typeof item))
|
|
313
|
+
throw new ConfigChatAccessError('leaf');
|
|
314
|
+
}
|
|
315
|
+
const types = new Set();
|
|
316
|
+
for (const nodes of current) {
|
|
317
|
+
const names = typeNames(nodes);
|
|
318
|
+
if (names.includes('object'))
|
|
319
|
+
throw new ConfigChatAccessError('leaf');
|
|
320
|
+
if (names.includes('array')) {
|
|
321
|
+
if (!nodes.some(node => node.items !== undefined || node.prefixItems !== undefined))
|
|
322
|
+
unknown();
|
|
323
|
+
let prefixLength = 0;
|
|
324
|
+
let maxItems = Infinity;
|
|
325
|
+
for (const node of nodes) {
|
|
326
|
+
const prefix = Array.isArray(node.prefixItems) ? node.prefixItems : Array.isArray(node.items) ? node.items : [];
|
|
327
|
+
prefixLength = Math.max(prefixLength, prefix.length);
|
|
328
|
+
if (node.maxItems !== undefined) {
|
|
329
|
+
if (typeof node.maxItems !== 'number' || !Number.isInteger(node.maxItems) || node.maxItems < 0)
|
|
330
|
+
unknown();
|
|
331
|
+
maxItems = Math.min(maxItems, node.maxItems);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Every prefix position can have a different type. One position past the
|
|
335
|
+
// longest prefix represents the homogeneous tail; never iterate to maxItems.
|
|
336
|
+
for (let index = 0; index <= prefixLength && index < maxItems; index++) {
|
|
337
|
+
const key = String(index);
|
|
338
|
+
// A forbidden position also makes all later positions unreachable.
|
|
339
|
+
if (children(nodes, key).includes(false))
|
|
340
|
+
break;
|
|
341
|
+
for (const items of descend([nodes], key, schema)) {
|
|
342
|
+
if (typeNames(items).some(type => !scalarTypes.has(type)))
|
|
343
|
+
throw new ConfigChatAccessError('leaf');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const name of names)
|
|
348
|
+
types.add(name);
|
|
349
|
+
}
|
|
350
|
+
return Object.freeze([...types]);
|
|
351
|
+
}
|
|
352
|
+
/** `unset` removes only optional leaves; any branch listing the key as required refuses. */
|
|
353
|
+
export function assertChatOptionalLeaf(schema, path) {
|
|
354
|
+
if (!record(schema))
|
|
355
|
+
unknown();
|
|
356
|
+
chatLeafTypes(schema, path);
|
|
357
|
+
if (!path.length)
|
|
358
|
+
throw new ConfigChatAccessError('required');
|
|
359
|
+
const key = path.at(-1);
|
|
360
|
+
for (const nodes of locate(schema, path.slice(0, -1))) {
|
|
361
|
+
if (nodes.some(node => Array.isArray(node.required) && node.required.includes(key)))
|
|
362
|
+
throw new ConfigChatAccessError('required');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** Prove the same properties-only ancestry used by root discovery, without enumerating siblings. */
|
|
366
|
+
function declaredPropertyPath(schema, path) {
|
|
367
|
+
try {
|
|
368
|
+
let current = variants(schema, schema);
|
|
369
|
+
for (const key of path) {
|
|
370
|
+
// Listing never descends into array items or dynamic dictionary keys.
|
|
371
|
+
if (current.some(nodes => typeNames(nodes).includes('array'))
|
|
372
|
+
|| !current.some(nodes => nodes.some(node => record(node.properties) && own(node.properties, key))))
|
|
373
|
+
return false;
|
|
374
|
+
current = descend(current, key, schema);
|
|
375
|
+
}
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (error instanceof ConfigChatAccessError)
|
|
380
|
+
return false;
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Declared leaf paths below `path` (properties only), each still re-checked by the store. */
|
|
385
|
+
export function chatConfigurationLeaves(schema, path, limit = 256) {
|
|
386
|
+
if (!record(schema) || !declaredPropertyPath(schema, path))
|
|
387
|
+
return [];
|
|
388
|
+
const found = [];
|
|
389
|
+
const walk = (relative) => {
|
|
390
|
+
if (found.length >= limit || relative.length > 12)
|
|
391
|
+
return;
|
|
392
|
+
if (relative.some(isChatPermissionSegment) || isChatIdentityRemapPath(relative) || relative.some(sensitiveKey))
|
|
393
|
+
return;
|
|
394
|
+
let current;
|
|
395
|
+
try {
|
|
396
|
+
current = locate(schema, relative);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (error instanceof ConfigChatAccessError)
|
|
400
|
+
return;
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
chatLeafTypes(schema, relative);
|
|
405
|
+
found.push(Object.freeze([...relative]));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (!(error instanceof ConfigChatAccessError))
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
const keys = new Set();
|
|
413
|
+
for (const nodes of current)
|
|
414
|
+
for (const node of nodes)
|
|
415
|
+
if (record(node.properties))
|
|
416
|
+
for (const key of Object.keys(node.properties))
|
|
417
|
+
keys.add(key);
|
|
418
|
+
for (const key of [...keys].sort())
|
|
419
|
+
walk([...relative, key]);
|
|
420
|
+
};
|
|
421
|
+
walk(path);
|
|
422
|
+
return Object.freeze(found);
|
|
423
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ConfigPatch, RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
2
|
+
/** Raised for a malformed request or a path outside the caller's declared scope; nothing is written. */
|
|
3
|
+
export declare class ConfigPatchScopeError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/** Structural request checks shared by the RPC boundary and the store. */
|
|
7
|
+
export declare function normalizeConfigPatchOperations(value: unknown): readonly ConfigPatch[];
|
|
8
|
+
/**
|
|
9
|
+
* Reads `x-zhin-config.patchPaths` from a plugin's own JSON Schema root: dotted Host sub-paths
|
|
10
|
+
* (at least two segments, e.g. `ai.knowledge`) the plugin owns besides its instance block.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readDeclaredPatchPaths(schema: Readonly<Record<string, unknown>> | null, instanceKey: string): readonly (readonly string[])[];
|
|
13
|
+
/** Resolves the allowlist for one caller scope. Plugin scopes always include their own block. */
|
|
14
|
+
export declare function resolvePatchScope(document: RuntimeConfigDocument, instanceKey: string | undefined, declared: readonly (readonly string[])[]): readonly (readonly string[])[];
|
|
15
|
+
export declare function assertPatchesInScope(patches: readonly ConfigPatch[], allowed: readonly (readonly string[])[], scopeLabel: string): void;
|
|
16
|
+
export declare function readPath(document: RuntimeConfigDocument, path: readonly string[]): unknown;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CONFIG_PATCH_LIMITS, HOST_CONFIG_PATCH_PATHS } from '@zhin.js/console-protocol';
|
|
2
|
+
import { HOST_CONFIG_KEYS } from '@zhin.js/runtime';
|
|
3
|
+
import { assertUnambiguousConfigKey } from './configuration-projection.js';
|
|
4
|
+
import { isForbiddenConfigurationPathSegment } from './configuration-path.js';
|
|
5
|
+
/** Host keys a plugin may never claim: declarations, the root plugin section and transport. */
|
|
6
|
+
const UNCLAIMABLE_HOST_KEYS = new Set(['plugin', 'http']);
|
|
7
|
+
const HOST_KEY_SET = new Set(HOST_CONFIG_KEYS);
|
|
8
|
+
const SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/u;
|
|
9
|
+
/** Raised for a malformed request or a path outside the caller's declared scope; nothing is written. */
|
|
10
|
+
export class ConfigPatchScopeError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'ConfigPatchScopeError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Structural request checks shared by the RPC boundary and the store. */
|
|
17
|
+
export function normalizeConfigPatchOperations(value) {
|
|
18
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
19
|
+
throw new ConfigPatchScopeError('patches 必须是非空数组');
|
|
20
|
+
if (value.length > CONFIG_PATCH_LIMITS.maxOperations) {
|
|
21
|
+
throw new ConfigPatchScopeError(`一次最多 ${CONFIG_PATCH_LIMITS.maxOperations} 个 patch`);
|
|
22
|
+
}
|
|
23
|
+
return Object.freeze(value.map((item, index) => {
|
|
24
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
25
|
+
throw new ConfigPatchScopeError(`patches[${index}] 必须是对象`);
|
|
26
|
+
const operation = item;
|
|
27
|
+
const path = normalizePath(operation.path, `patches[${index}].path`);
|
|
28
|
+
if (operation.op === 'set') {
|
|
29
|
+
if (!Object.hasOwn(operation, 'value') || operation.value === undefined) {
|
|
30
|
+
throw new ConfigPatchScopeError(`patches[${index}] 的 set 需要 value;删除请用 op: "remove"`);
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze({ op: 'set', path, value: structuredClone(operation.value) });
|
|
33
|
+
}
|
|
34
|
+
if (operation.op === 'remove')
|
|
35
|
+
return Object.freeze({ op: 'remove', path });
|
|
36
|
+
throw new ConfigPatchScopeError(`patches[${index}].op 只能是 set 或 remove`);
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function normalizePath(value, label) {
|
|
40
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > CONFIG_PATCH_LIMITS.maxPathDepth) {
|
|
41
|
+
throw new ConfigPatchScopeError(`${label} 必须是 1–${CONFIG_PATCH_LIMITS.maxPathDepth} 段的字符串数组`);
|
|
42
|
+
}
|
|
43
|
+
for (const segment of value) {
|
|
44
|
+
if (typeof segment !== 'string' || !segment || segment.length > CONFIG_PATCH_LIMITS.maxSegmentLength
|
|
45
|
+
|| isForbiddenConfigurationPathSegment(segment)) {
|
|
46
|
+
throw new ConfigPatchScopeError(`${label} 含有无效路径段`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze([...value]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reads `x-zhin-config.patchPaths` from a plugin's own JSON Schema root: dotted Host sub-paths
|
|
53
|
+
* (at least two segments, e.g. `ai.knowledge`) the plugin owns besides its instance block.
|
|
54
|
+
*/
|
|
55
|
+
export function readDeclaredPatchPaths(schema, instanceKey) {
|
|
56
|
+
const metadata = schema?.['x-zhin-config'];
|
|
57
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
58
|
+
return [];
|
|
59
|
+
const declared = metadata.patchPaths;
|
|
60
|
+
if (declared === undefined)
|
|
61
|
+
return [];
|
|
62
|
+
if (!Array.isArray(declared))
|
|
63
|
+
throw new ConfigPatchScopeError(`${instanceKey} 的 x-zhin-config.patchPaths 必须是字符串数组`);
|
|
64
|
+
return Object.freeze(declared.map((entry) => {
|
|
65
|
+
const path = typeof entry === 'string' ? entry.split('.') : [];
|
|
66
|
+
if (path.length < 2 || path.some(segment => !SEGMENT.test(segment) || isForbiddenConfigurationPathSegment(segment))
|
|
67
|
+
|| !HOST_KEY_SET.has(path[0]) || UNCLAIMABLE_HOST_KEYS.has(path[0])) {
|
|
68
|
+
throw new ConfigPatchScopeError(`${instanceKey} 的 x-zhin-config.patchPaths 项 ${String(entry)} 无效:只能声明 Host 配置下至少两级的路径(如 ai.knowledge)`);
|
|
69
|
+
}
|
|
70
|
+
return Object.freeze(path);
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
/** Resolves the allowlist for one caller scope. Plugin scopes always include their own block. */
|
|
74
|
+
export function resolvePatchScope(document, instanceKey, declared) {
|
|
75
|
+
if (instanceKey === undefined)
|
|
76
|
+
return HOST_CONFIG_PATCH_PATHS;
|
|
77
|
+
if (HOST_KEY_SET.has(instanceKey)) {
|
|
78
|
+
throw new ConfigPatchScopeError(`${instanceKey} 是 Host 配置键,不是插件实例;请省略 instanceKey 使用 Host 白名单`);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
assertUnambiguousConfigKey(document, instanceKey);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new ConfigPatchScopeError(`未声明的插件实例:${instanceKey}`);
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze([Object.freeze([instanceKey]), ...declared]);
|
|
87
|
+
}
|
|
88
|
+
export function assertPatchesInScope(patches, allowed, scopeLabel) {
|
|
89
|
+
for (const patch of patches) {
|
|
90
|
+
const inside = allowed.some(prefix => prefix.length <= patch.path.length
|
|
91
|
+
&& prefix.every((segment, index) => patch.path[index] === segment));
|
|
92
|
+
if (!inside) {
|
|
93
|
+
throw new ConfigPatchScopeError(`路径 ${patch.path.join('.')} 不在 ${scopeLabel} 可写范围内(允许:${allowed.map(path => path.join('.')).join('、') || '无'})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function readPath(document, path) {
|
|
98
|
+
let value = document;
|
|
99
|
+
for (const segment of path) {
|
|
100
|
+
if (!value || typeof value !== 'object' || !Object.hasOwn(value, segment))
|
|
101
|
+
return undefined;
|
|
102
|
+
value = value[segment];
|
|
103
|
+
}
|
|
104
|
+
return structuredClone(value);
|
|
105
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** One immutable policy for parsed, structured-write and chat-read paths. */
|
|
2
|
+
export declare function isForbiddenConfigurationPathSegment(segment: string): boolean;
|
|
3
|
+
/** JSON Pointer preserves literal dots and slashes; dotted paths cover simple fields. */
|
|
4
|
+
export declare function parseConfigurationPath(input: string): readonly string[];
|
|
5
|
+
/** Lossless text, or undefined when the parser cannot address it. Callers quote token delimiters. */
|
|
6
|
+
export declare function formatConfigurationPath(path: readonly string[]): string | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CONFIG_PATCH_LIMITS } from '@zhin.js/console-protocol';
|
|
2
|
+
const forbidden = Object.freeze(['__proto__', 'constructor', 'prototype']);
|
|
3
|
+
const simpleSegment = /^[A-Za-z0-9_-]+$/u;
|
|
4
|
+
/** One immutable policy for parsed, structured-write and chat-read paths. */
|
|
5
|
+
export function isForbiddenConfigurationPathSegment(segment) {
|
|
6
|
+
return forbidden.includes(segment);
|
|
7
|
+
}
|
|
8
|
+
/** JSON Pointer preserves literal dots and slashes; dotted paths cover simple fields. */
|
|
9
|
+
export function parseConfigurationPath(input) {
|
|
10
|
+
if (typeof input !== 'string')
|
|
11
|
+
throw new TypeError('Invalid configuration path');
|
|
12
|
+
if (input === '')
|
|
13
|
+
return Object.freeze([]);
|
|
14
|
+
const pointer = input.startsWith('/');
|
|
15
|
+
const segments = pointer ? input.slice(1).split('/').map(segment => {
|
|
16
|
+
if (/~(?![01])/u.test(segment))
|
|
17
|
+
throw new TypeError('Invalid configuration path');
|
|
18
|
+
return segment.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
19
|
+
}) : input.split('.');
|
|
20
|
+
if (segments.length > CONFIG_PATCH_LIMITS.maxPathDepth || segments.some(segment => !segment
|
|
21
|
+
|| segment.length > CONFIG_PATCH_LIMITS.maxSegmentLength || isForbiddenConfigurationPathSegment(segment)
|
|
22
|
+
|| /[\u0000-\u001f\u007f]/u.test(segment) || (!pointer && !simpleSegment.test(segment)))) {
|
|
23
|
+
throw new TypeError('Invalid configuration path');
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze(segments);
|
|
26
|
+
}
|
|
27
|
+
/** Lossless text, or undefined when the parser cannot address it. Callers quote token delimiters. */
|
|
28
|
+
export function formatConfigurationPath(path) {
|
|
29
|
+
const text = path.every(segment => simpleSegment.test(segment)) ? path.join('.')
|
|
30
|
+
: `/${path.map(segment => segment.replaceAll('~', '~0').replaceAll('/', '~1')).join('/')}`;
|
|
31
|
+
try {
|
|
32
|
+
parseConfigurationPath(text);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof TypeError)
|
|
36
|
+
return undefined;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ConfigPatch, type RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
2
|
+
/** The Console presents business sections, never the package-name declaration map. */
|
|
3
|
+
export declare function flattenConfigDocument(document: RuntimeConfigDocument): Record<string, unknown>;
|
|
4
|
+
export declare function assertUnambiguousConfigKey(document: RuntimeConfigDocument, key: string): void;
|
|
5
|
+
export declare function configKeyPatch(document: RuntimeConfigDocument, key: string, data: unknown): ConfigPatch;
|
|
6
|
+
export declare function configKeyRemovePatch(document: RuntimeConfigDocument, key: string): ConfigPatch;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readPluginConfigurationMap } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { HOST_CONFIG_KEYS, readApplicationPluginDeclaration } from '@zhin.js/runtime';
|
|
3
|
+
const HOST_KEYS = new Set([...HOST_CONFIG_KEYS, 'plugin']);
|
|
4
|
+
const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype']);
|
|
5
|
+
/** The Console presents business sections, never the package-name declaration map. */
|
|
6
|
+
export function flattenConfigDocument(document) {
|
|
7
|
+
readApplicationPluginDeclaration(document);
|
|
8
|
+
const flat = {};
|
|
9
|
+
for (const key of HOST_KEYS)
|
|
10
|
+
if (Object.hasOwn(document, key))
|
|
11
|
+
Object.defineProperty(flat, key, { value: document[key], enumerable: true, configurable: true, writable: true });
|
|
12
|
+
for (const [key, value] of Object.entries(readPluginConfigurationMap(document)))
|
|
13
|
+
Object.defineProperty(flat, key, { value, enumerable: true, configurable: true, writable: true });
|
|
14
|
+
return flat;
|
|
15
|
+
}
|
|
16
|
+
export function assertUnambiguousConfigKey(document, key) {
|
|
17
|
+
const declaration = readApplicationPluginDeclaration(document);
|
|
18
|
+
if (FORBIDDEN.has(key) || !HOST_KEYS.has(key) && !declaration.instances.some(item => item.instanceKey === key))
|
|
19
|
+
throw new Error(`Unknown configuration key: ${key}; edit the configuration source to change declarations`);
|
|
20
|
+
}
|
|
21
|
+
export function configKeyPatch(document, key, data) {
|
|
22
|
+
assertUnambiguousConfigKey(document, key);
|
|
23
|
+
return Object.freeze({ op: 'set', path: Object.freeze([key]), value: data });
|
|
24
|
+
}
|
|
25
|
+
export function configKeyRemovePatch(document, key) {
|
|
26
|
+
assertUnambiguousConfigKey(document, key);
|
|
27
|
+
return Object.freeze({ op: 'remove', path: Object.freeze([key]) });
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Masks stored configuration values before they leave the store in a response that the
|
|
3
|
+
* caller did not ask to read (e.g. a trusted project patch conflict). Conservative: a
|
|
4
|
+
* schema secret/writeOnly/password/hidden annotation, a secret-like key, or an unexpanded
|
|
5
|
+
* environment reference (`${…}`, `env://…`) is replaced as a whole and never echoed.
|
|
6
|
+
*/
|
|
7
|
+
type Schema = Readonly<Record<string, unknown>>;
|
|
8
|
+
/** Shared proof limit: these branches/resources need a resolver we do not provide. */
|
|
9
|
+
export declare function isUnknownConfigurationSchemaBoundary(schema: Schema, root: Schema): boolean;
|
|
10
|
+
/** Credential names, excluding ordinary budgets such as maxTokens and tokenLimit. */
|
|
11
|
+
export declare function sensitiveConfigKey(key: string): boolean;
|
|
12
|
+
export declare const REDACTED_CONFIG_VALUE = "[redacted]";
|
|
13
|
+
/**
|
|
14
|
+
* Masks `value`, stored at `path` (first segment is the section) under `sectionSchema`.
|
|
15
|
+
* A missing schema still applies key and environment-reference masking.
|
|
16
|
+
* `unknownSchema: 'redact'` (project reads and conflict evidence) also replaces any value under a schema
|
|
17
|
+
* boundary this masker cannot prove (`if`/`not`/`contains`/…, nested `$id`, non-local
|
|
18
|
+
* `$ref`; the chat policy's {@link isUnknownConfigurationSchemaBoundary}), since a secret
|
|
19
|
+
* annotation could hide behind it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function maskConfigValue(sectionSchema: Schema | null | undefined, path: readonly string[], value: unknown, options?: Readonly<{
|
|
22
|
+
unknownSchema?: 'redact';
|
|
23
|
+
}>): unknown;
|
|
24
|
+
export {};
|