@suveren/gateway 0.12.0 → 0.13.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/README.md +53 -3
- package/bin/suveren-gateway.js +128 -12
- package/dist/control-plane/index.mjs +555 -122
- package/dist/mcp-server/http.mjs +647 -273
- package/dist/ui/assets/{index-BzcgeHlq.js → index-CmLqzgqA.js} +21 -21
- package/dist/ui/assets/{index-BFOprnFZ.css → index-y1OFeYnS.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/lib/autostart-templates.d.mts +6 -0
- package/lib/autostart-templates.mjs +21 -4
- package/lib/config.d.mts +10 -0
- package/lib/config.mjs +79 -18
- package/lib/policy.d.mts +34 -0
- package/lib/policy.mjs +369 -0
- package/node_modules/@hap/core/dist/index.d.mts +49 -1
- package/node_modules/@hap/core/dist/index.d.ts +49 -1
- package/node_modules/@hap/core/dist/index.js +50 -0
- package/node_modules/@hap/core/dist/index.mjs +49 -0
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/frame.ts +63 -1
- package/node_modules/@hap/core/src/gatekeeper.ts +37 -0
- package/node_modules/@hap/core/src/types.ts +29 -1
- package/node_modules/hono/dist/cjs/hono-base.js +2 -0
- package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/mount/index.js +71 -0
- package/node_modules/hono/dist/cjs/request.js +4 -4
- package/node_modules/hono/dist/hono-base.js +2 -0
- package/node_modules/hono/dist/middleware/mount/index.js +70 -0
- package/node_modules/hono/dist/request.js +4 -4
- package/node_modules/hono/dist/types/hono-base.d.ts +2 -0
- package/node_modules/hono/dist/types/jsx/base.d.ts +1 -1
- package/node_modules/hono/dist/types/middleware/mount/index.d.ts +46 -0
- package/node_modules/hono/dist/types/request.d.ts +4 -4
- package/node_modules/hono/package.json +9 -1
- package/package.json +2 -2
- package/profiles/index.json +3 -1
- package/profiles/purchase/0.6.profile.json +234 -0
- package/profiles/sales/0.3.profile.json +290 -0
- package/server.js +51 -8
package/lib/policy.mjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed-settings policy — IT-controlled overrides that an employee cannot
|
|
3
|
+
* change locally (W3 of the Windows installer track; see
|
|
4
|
+
* suveren-as/docs/work-plan.md, "Added 2026-10-02 — Windows installer").
|
|
5
|
+
*
|
|
6
|
+
* Sources, highest precedence first:
|
|
7
|
+
* 1. Windows registry `HKLM\SOFTWARE\Policies\Suveren\Gateway`, then
|
|
8
|
+
* `HKCU\SOFTWARE\Policies\Suveren\Gateway` (HKLM wins over HKCU).
|
|
9
|
+
* Read via `reg query` (child_process) — no native modules. Skipped
|
|
10
|
+
* entirely on non-Windows.
|
|
11
|
+
* 2. A JSON file: `%ProgramData%\Suveren\gateway-policy.json` (Windows),
|
|
12
|
+
* `/Library/Application Support/Suveren/gateway-policy.json` (macOS),
|
|
13
|
+
* `/etc/suveren/gateway-policy.json` (Linux) — overridable for tests
|
|
14
|
+
* via `SUVEREN_POLICY_FILE`.
|
|
15
|
+
*
|
|
16
|
+
* A key present in EITHER source is LOCKED: every resolver in this codebase
|
|
17
|
+
* (resolveAsUrl, resolveCaFile, resolvePinTls, resolveProxyUrl,
|
|
18
|
+
* resolveSimulation / isSimulationMode) must return the policy value
|
|
19
|
+
* regardless of env var, saved config.json, or CLI flag — see those
|
|
20
|
+
* modules' own doc comments for where this is wired in. Overall precedence:
|
|
21
|
+
* policy > env > saved config > defaults.
|
|
22
|
+
*
|
|
23
|
+
* `Proxy` is the one key that OVERRIDES an environment variable rather than
|
|
24
|
+
* merely out-ranking a saved value: on a Windows company laptop the proxy is
|
|
25
|
+
* set in system settings, not `HTTP_PROXY`/`HTTPS_PROXY` — see
|
|
26
|
+
* bundle/server.js, which sets both env vars from the policy value
|
|
27
|
+
* unconditionally when `Proxy` is locked (every other locked key has no env
|
|
28
|
+
* tier to override in the first place). `NoProxy` is read the same way but
|
|
29
|
+
* has no saved-config/CLI counterpart at all — NO_PROXY stays env-only
|
|
30
|
+
* except for this one override.
|
|
31
|
+
*
|
|
32
|
+
* Registry value names = JSON file keys (PascalCase, matching what an IT
|
|
33
|
+
* admin would see in either place): AsUrl, CaFile, PinTls, Proxy, NoProxy,
|
|
34
|
+
* Simulation, InstallMethod.
|
|
35
|
+
*
|
|
36
|
+
* Mirrored (same logic, duplicated — no shared runtime package between the
|
|
37
|
+
* CLI and the two apps, see config.mjs's doc comment for the established
|
|
38
|
+
* pattern) in `apps/mcp-server/src/lib/policy.ts` and
|
|
39
|
+
* `apps/control-plane/src/lib/policy.ts`. Keep all three in step.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
import { spawnSync } from 'node:child_process';
|
|
45
|
+
|
|
46
|
+
const POLICY_KEYS = ['AsUrl', 'CaFile', 'PinTls', 'Proxy', 'NoProxy', 'Simulation', 'InstallMethod'];
|
|
47
|
+
|
|
48
|
+
/** The documented production path, relative to the hive — never changes for
|
|
49
|
+
* a real install. Overridable ONLY via `SUVEREN_POLICY_REGISTRY_KEY`, which
|
|
50
|
+
* exists purely so `gateway-policy-windows-registry.test.ts` can point at a
|
|
51
|
+
* unique per-run key instead of the real one: `pnpm -r test` runs every
|
|
52
|
+
* workspace package in parallel, so a test writing to the REAL key would
|
|
53
|
+
* race every OTHER package's tests reading the real registry on the same
|
|
54
|
+
* Windows CI runner at the same time (this raced in practice — see
|
|
55
|
+
* `SUVEREN_POLICY_REGISTRY` below for the other half of the fix). */
|
|
56
|
+
const DEFAULT_REGISTRY_BASE_KEY = 'SOFTWARE\\Policies\\Suveren\\Gateway';
|
|
57
|
+
|
|
58
|
+
/** The full `HIVE\base\key` path `reg query`/`reg add` operate on. A pure
|
|
59
|
+
* function (no I/O) so a test can assert the default without mocking
|
|
60
|
+
* anything — see gateway-policy.test.ts's "documented default paths". */
|
|
61
|
+
export function registryKeyPath(hive) {
|
|
62
|
+
const baseKey = process.env.SUVEREN_POLICY_REGISTRY_KEY || DEFAULT_REGISTRY_BASE_KEY;
|
|
63
|
+
return `${hive}\\${baseKey}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate + normalize a candidate Authority Server URL — the SAME rule
|
|
68
|
+
* `config set as-url` enforces (see config.mjs's validateAsUrl), duplicated
|
|
69
|
+
* rather than imported so this module has no dependency on config.mjs (the
|
|
70
|
+
* TS mirrors of each live in different files entirely).
|
|
71
|
+
*/
|
|
72
|
+
function validateAsUrl(candidate) {
|
|
73
|
+
const trimmed = (candidate ?? '').trim();
|
|
74
|
+
if (!trimmed) return { ok: false, error: 'The Authority Server URL is empty.' };
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = new URL(trimmed);
|
|
78
|
+
} catch {
|
|
79
|
+
return { ok: false, error: `"${trimmed}" is not a valid URL.` };
|
|
80
|
+
}
|
|
81
|
+
const isLocalHttp =
|
|
82
|
+
parsed.protocol === 'http:' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1');
|
|
83
|
+
if (parsed.protocol !== 'https:' && !isLocalHttp) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
90
|
+
if (normalized.endsWith('/') && normalized !== `${parsed.origin}/`) {
|
|
91
|
+
normalized = normalized.slice(0, -1);
|
|
92
|
+
} else if (normalized === `${parsed.origin}/`) {
|
|
93
|
+
normalized = parsed.origin;
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, url: normalized };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Same rule as config.mjs's validateCaFile: must exist on disk. Resolved
|
|
99
|
+
* against cwd like the CLI flag is — a policy-set relative path is
|
|
100
|
+
* unusual, but behaving consistently with `config set ca-file` beats a
|
|
101
|
+
* second rule nobody remembers. */
|
|
102
|
+
function validateCaFile(candidate) {
|
|
103
|
+
const trimmed = (candidate ?? '').trim();
|
|
104
|
+
if (!trimmed) return { ok: false, error: 'The CA file path is empty.' };
|
|
105
|
+
if (!existsSync(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
|
|
106
|
+
return { ok: true, path: trimmed };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Same rule as config.mjs's validateProxyUrl: http:// or https:// only. */
|
|
110
|
+
function validateProxyUrl(candidate) {
|
|
111
|
+
const trimmed = (candidate ?? '').trim();
|
|
112
|
+
if (!trimmed) return { ok: false, error: 'The proxy URL is empty.' };
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = new URL(trimmed);
|
|
116
|
+
} catch {
|
|
117
|
+
return { ok: false, error: `"${trimmed}" is not a valid URL.` };
|
|
118
|
+
}
|
|
119
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
120
|
+
return { ok: false, error: `"${trimmed}" must use http:// or https://.` };
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, url: trimmed };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Accepts a JSON boolean, or the DWORD-style 0/1 (as a number OR a string,
|
|
126
|
+
* since the registry parser below hands back numbers but a hand-edited
|
|
127
|
+
* JSON file might use either). Returns undefined — not a default — for
|
|
128
|
+
* anything else, so the caller can tell "not a boolean" from "false". */
|
|
129
|
+
function coerceBoolean(value) {
|
|
130
|
+
if (typeof value === 'boolean') return value;
|
|
131
|
+
if (typeof value === 'number') {
|
|
132
|
+
if (value === 0) return false;
|
|
133
|
+
if (value === 1) return true;
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
if (typeof value === 'string') {
|
|
137
|
+
const v = value.trim().toLowerCase();
|
|
138
|
+
if (v === 'true' || v === '1') return true;
|
|
139
|
+
if (v === 'false' || v === '0') return false;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Registry (Windows only) ────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse `reg query "HKLM\...\Gateway"` stdout. Real output looks like:
|
|
148
|
+
*
|
|
149
|
+
* HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Suveren\Gateway
|
|
150
|
+
* AsUrl REG_SZ https://as.company.internal
|
|
151
|
+
* PinTls REG_DWORD 0x1
|
|
152
|
+
*
|
|
153
|
+
* Value lines are 4-space-indented; columns are separated by runs of
|
|
154
|
+
* whitespace. REG_DWORD values print as hex ("0x1"), parsed to a number
|
|
155
|
+
* here so coerceBoolean can treat it like any other 0/1.
|
|
156
|
+
*/
|
|
157
|
+
export function parseRegQueryOutput(stdout) {
|
|
158
|
+
const out = {};
|
|
159
|
+
const lineRe = /^ {4}(\S+)\s+(REG_SZ|REG_EXPAND_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_MULTI_SZ)\s+(.*)$/;
|
|
160
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
161
|
+
const m = lineRe.exec(line);
|
|
162
|
+
if (!m) continue;
|
|
163
|
+
const [, name, type, rawValue] = m;
|
|
164
|
+
if (!POLICY_KEYS.includes(name)) continue;
|
|
165
|
+
if (type === 'REG_DWORD' || type === 'REG_QWORD') {
|
|
166
|
+
out[name] = parseInt(rawValue, 16);
|
|
167
|
+
} else {
|
|
168
|
+
out[name] = rawValue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `{ values, source }` for one registry hive, or `{ values: {}, source }`
|
|
175
|
+
* on any failure (key absent — the normal "no policy set" case — missing
|
|
176
|
+
* `reg` binary, or a platform that isn't Windows at all). Never throws:
|
|
177
|
+
* an operator without IT policy must see the gateway behave exactly as it
|
|
178
|
+
* always has.
|
|
179
|
+
*
|
|
180
|
+
* `SUVEREN_POLICY_REGISTRY=off` skips the registry entirely, returning
|
|
181
|
+
* `{}` without even spawning `reg` — unit tests set this globally (see
|
|
182
|
+
* each app's vitest setup) so a real IT policy on the machine running the
|
|
183
|
+
* suite, or another package's test writing to the registry concurrently,
|
|
184
|
+
* cannot change what these tests see. Production never sets this. */
|
|
185
|
+
function readRegistryHive(hive) {
|
|
186
|
+
const path = registryKeyPath(hive);
|
|
187
|
+
const source = `the registry policy (${path})`;
|
|
188
|
+
// Checked BEFORE the platform branch so the switch is testable on any OS,
|
|
189
|
+
// not just Windows — see gateway-policy.test.ts.
|
|
190
|
+
if (process.env.SUVEREN_POLICY_REGISTRY === 'off') return { values: {}, source };
|
|
191
|
+
if (process.platform !== 'win32') return { values: {}, source };
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = spawnSync('reg', ['query', path], { encoding: 'utf8' });
|
|
195
|
+
} catch {
|
|
196
|
+
return { values: {}, source };
|
|
197
|
+
}
|
|
198
|
+
if (!result || result.status !== 0 || !result.stdout) return { values: {}, source };
|
|
199
|
+
return { values: parseRegQueryOutput(result.stdout), source };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Policy file ────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export function policyFilePath() {
|
|
205
|
+
if (process.env.SUVEREN_POLICY_FILE) return process.env.SUVEREN_POLICY_FILE;
|
|
206
|
+
if (process.platform === 'win32') {
|
|
207
|
+
const programData = process.env.ProgramData || process.env.PROGRAMDATA || 'C:\\ProgramData';
|
|
208
|
+
return join(programData, 'Suveren', 'gateway-policy.json');
|
|
209
|
+
}
|
|
210
|
+
if (process.platform === 'darwin') {
|
|
211
|
+
return '/Library/Application Support/Suveren/gateway-policy.json';
|
|
212
|
+
}
|
|
213
|
+
return '/etc/suveren/gateway-policy.json';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Throws (fail closed, audibly — same rule as config.mjs's
|
|
217
|
+
* readConfigStrict) when the file exists but is not valid JSON. A missing
|
|
218
|
+
* file is the normal "no policy set" case and returns `{}`. */
|
|
219
|
+
function readPolicyFile() {
|
|
220
|
+
const path = policyFilePath();
|
|
221
|
+
const source = `the policy file (${path})`;
|
|
222
|
+
if (!existsSync(path)) return { values: {}, source };
|
|
223
|
+
let data;
|
|
224
|
+
try {
|
|
225
|
+
data = JSON.parse(readFileSync(path, 'utf8'));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
throw new Error(`Could not parse policy file ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
228
|
+
}
|
|
229
|
+
const values = {};
|
|
230
|
+
for (const key of POLICY_KEYS) {
|
|
231
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) values[key] = data[key];
|
|
232
|
+
}
|
|
233
|
+
return { values, source };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Merge + validate ───────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/** First entry (in precedence order) that defines each key wins. Returns
|
|
239
|
+
* the merged raw values plus, per key, which source they came from (for
|
|
240
|
+
* error messages). */
|
|
241
|
+
function mergeBySource(entries) {
|
|
242
|
+
const merged = {};
|
|
243
|
+
const sourceOf = {};
|
|
244
|
+
for (const key of POLICY_KEYS) {
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (Object.prototype.hasOwnProperty.call(entry.values, key) && entry.values[key] !== undefined) {
|
|
247
|
+
merged[key] = entry.values[key];
|
|
248
|
+
sourceOf[key] = entry.source;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { merged, sourceOf };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let cached = null;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Read + validate the effective policy. Memoized for the life of the
|
|
260
|
+
* process (registry reads spawn a child process — doing that on every
|
|
261
|
+
* `isSimulationMode()` call, which runs per tool call, would be a
|
|
262
|
+
* synchronous fork on the hot path). Policy changes take effect on the next
|
|
263
|
+
* restart anyway, same as every other setting here — see
|
|
264
|
+
* `_resetPolicyCacheForTests` for the one case that needs otherwise.
|
|
265
|
+
*
|
|
266
|
+
* Throws a named error — mentioning the key, the offending value, and which
|
|
267
|
+
* source it came from — on an invalid value, exactly like a bad `config
|
|
268
|
+
* set` input (see config.mjs's resolveAsUrl doc comment on failing closed
|
|
269
|
+
* and audibly rather than silently falling back).
|
|
270
|
+
*/
|
|
271
|
+
export function readPolicy() {
|
|
272
|
+
if (cached) return cached;
|
|
273
|
+
|
|
274
|
+
const hklm = readRegistryHive('HKLM');
|
|
275
|
+
const hkcu = readRegistryHive('HKCU');
|
|
276
|
+
const file = readPolicyFile(); // may throw — propagated, not swallowed
|
|
277
|
+
|
|
278
|
+
const { merged, sourceOf } = mergeBySource([hklm, hkcu, file]);
|
|
279
|
+
|
|
280
|
+
const policy = {};
|
|
281
|
+
const locked = new Set();
|
|
282
|
+
|
|
283
|
+
if (merged.AsUrl !== undefined) {
|
|
284
|
+
const v = validateAsUrl(String(merged.AsUrl));
|
|
285
|
+
if (!v.ok) {
|
|
286
|
+
throw new Error(`Invalid policy AsUrl ("${merged.AsUrl}") from ${sourceOf.AsUrl}: ${v.error}`);
|
|
287
|
+
}
|
|
288
|
+
policy.asUrl = v.url;
|
|
289
|
+
locked.add('asUrl');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (merged.CaFile !== undefined) {
|
|
293
|
+
const v = validateCaFile(String(merged.CaFile));
|
|
294
|
+
if (!v.ok) {
|
|
295
|
+
throw new Error(`Invalid policy CaFile ("${merged.CaFile}") from ${sourceOf.CaFile}: ${v.error}`);
|
|
296
|
+
}
|
|
297
|
+
policy.caFile = v.path;
|
|
298
|
+
locked.add('caFile');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (merged.PinTls !== undefined) {
|
|
302
|
+
const b = coerceBoolean(merged.PinTls);
|
|
303
|
+
if (b === undefined) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Invalid policy PinTls ("${merged.PinTls}") from ${sourceOf.PinTls}: must be a boolean or 0/1.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
policy.pinTls = b;
|
|
309
|
+
locked.add('pinTls');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (merged.Proxy !== undefined) {
|
|
313
|
+
const v = validateProxyUrl(String(merged.Proxy));
|
|
314
|
+
if (!v.ok) {
|
|
315
|
+
throw new Error(`Invalid policy Proxy ("${merged.Proxy}") from ${sourceOf.Proxy}: ${v.error}`);
|
|
316
|
+
}
|
|
317
|
+
policy.proxy = v.url;
|
|
318
|
+
locked.add('proxy');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// NoProxy has no saved-config/CLI counterpart — just a string, no URL
|
|
322
|
+
// shape to validate (it's a NO_PROXY-style hostname list, same grammar as
|
|
323
|
+
// the env var itself).
|
|
324
|
+
if (merged.NoProxy !== undefined) {
|
|
325
|
+
const raw = String(merged.NoProxy).trim();
|
|
326
|
+
if (!raw) {
|
|
327
|
+
throw new Error(`Invalid policy NoProxy ("${merged.NoProxy}") from ${sourceOf.NoProxy}: must not be empty.`);
|
|
328
|
+
}
|
|
329
|
+
policy.noProxy = raw;
|
|
330
|
+
locked.add('noProxy');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (merged.Simulation !== undefined) {
|
|
334
|
+
const b = coerceBoolean(merged.Simulation);
|
|
335
|
+
if (b === undefined) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Invalid policy Simulation ("${merged.Simulation}") from ${sourceOf.Simulation}: must be a boolean or 0/1.`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
policy.simulation = b;
|
|
341
|
+
locked.add('simulation');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (merged.InstallMethod !== undefined) {
|
|
345
|
+
const raw = String(merged.InstallMethod).trim().toLowerCase();
|
|
346
|
+
if (raw !== 'managed') {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Invalid policy InstallMethod ("${merged.InstallMethod}") from ${sourceOf.InstallMethod}: only "managed" is supported.`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
policy.installMethod = 'managed';
|
|
352
|
+
locked.add('installMethod');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
cached = { policy, locked };
|
|
356
|
+
return cached;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Convenience: is this one key locked by policy? */
|
|
360
|
+
export function isPolicyLocked(key) {
|
|
361
|
+
return readPolicy().locked.has(key);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Drop the memoized policy so the next `readPolicy()` call re-reads the
|
|
365
|
+
* registry/file. Test-only — a real process never needs this, since
|
|
366
|
+
* policy is only ever consulted again after a restart. */
|
|
367
|
+
export function _resetPolicyCacheForTests() {
|
|
368
|
+
cached = null;
|
|
369
|
+
}
|
|
@@ -205,10 +205,29 @@ type BoundType =
|
|
|
205
205
|
* Per-transaction cap. The execution context field named in `of` must
|
|
206
206
|
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
207
207
|
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
208
|
+
*
|
|
209
|
+
* `requiredFor` (v0.8+) closes the converse: without it, a call that does
|
|
210
|
+
* not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
|
|
211
|
+
* permits a call that declares no value whatsoever, which is indistinguishable
|
|
212
|
+
* from an unenforced bound. The same hole, and the same fix, as
|
|
213
|
+
* `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
|
|
214
|
+
* from the profile's `boundsSchema.actionTypes` registry. For a listed
|
|
215
|
+
* `action_type`, an execution whose context lacks `of` — or whose value is
|
|
216
|
+
* not a finite number — MUST be refused rather than skipped. For an
|
|
217
|
+
* unlisted (or when `requiredFor` is absent) action type, today's
|
|
218
|
+
* skip-on-absence behaviour is unchanged. Meaningful only on
|
|
219
|
+
* `per_transaction`; the other `BoundType` kinds have no per-call "value
|
|
220
|
+
* exposed or not" question — `cumulative_sum`/`cumulative_count` always read
|
|
221
|
+
* from the execution log regardless of what this call declares, and `enum`
|
|
222
|
+
* is a capability flag, not a runtime value — so the field does not exist
|
|
223
|
+
* on those variants and a profile that puts it there has it silently
|
|
224
|
+
* ignored by every enforcement point (TypeScript also refuses it when the
|
|
225
|
+
* profile is authored against this type rather than raw JSON).
|
|
208
226
|
*/
|
|
209
227
|
{
|
|
210
228
|
kind: 'per_transaction';
|
|
211
229
|
of: string;
|
|
230
|
+
requiredFor?: string[];
|
|
212
231
|
}
|
|
213
232
|
/**
|
|
214
233
|
* Cumulative sum within a time window. The SP maintains a running sum
|
|
@@ -503,6 +522,16 @@ interface AgentProfile {
|
|
|
503
522
|
boundsSchema?: {
|
|
504
523
|
keyOrder: string[];
|
|
505
524
|
fields: Record<string, ProfileBoundsField>;
|
|
525
|
+
/**
|
|
526
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
527
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
528
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
529
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
530
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
531
|
+
* Absent on profiles published before the registry existed — membership
|
|
532
|
+
* is then uncheckable, not a violation.
|
|
533
|
+
*/
|
|
534
|
+
actionTypes?: readonly string[];
|
|
506
535
|
};
|
|
507
536
|
/**
|
|
508
537
|
* v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
|
|
@@ -983,6 +1012,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
983
1012
|
valid: boolean;
|
|
984
1013
|
errors: string[];
|
|
985
1014
|
};
|
|
1015
|
+
/**
|
|
1016
|
+
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
1017
|
+
* entries must name a real `action_type` from the profile's declared
|
|
1018
|
+
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
1019
|
+
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
1020
|
+
* kinds don't carry it).
|
|
1021
|
+
*
|
|
1022
|
+
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
1023
|
+
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
1024
|
+
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
1025
|
+
* an automatic refusal here would be new behaviour beyond this change's
|
|
1026
|
+
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
1027
|
+
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
1028
|
+
* action type) or is attached to a bound kind that silently ignores it.
|
|
1029
|
+
*
|
|
1030
|
+
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
1031
|
+
* check — never throws.
|
|
1032
|
+
*/
|
|
1033
|
+
declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
|
|
986
1034
|
/**
|
|
987
1035
|
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
988
1036
|
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
@@ -1353,4 +1401,4 @@ interface ReceiptPayload {
|
|
|
1353
1401
|
*/
|
|
1354
1402
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1355
1403
|
|
|
1356
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
|
1404
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateBoundsRequiredFor, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
|
@@ -205,10 +205,29 @@ type BoundType =
|
|
|
205
205
|
* Per-transaction cap. The execution context field named in `of` must
|
|
206
206
|
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
207
207
|
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
208
|
+
*
|
|
209
|
+
* `requiredFor` (v0.8+) closes the converse: without it, a call that does
|
|
210
|
+
* not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
|
|
211
|
+
* permits a call that declares no value whatsoever, which is indistinguishable
|
|
212
|
+
* from an unenforced bound. The same hole, and the same fix, as
|
|
213
|
+
* `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
|
|
214
|
+
* from the profile's `boundsSchema.actionTypes` registry. For a listed
|
|
215
|
+
* `action_type`, an execution whose context lacks `of` — or whose value is
|
|
216
|
+
* not a finite number — MUST be refused rather than skipped. For an
|
|
217
|
+
* unlisted (or when `requiredFor` is absent) action type, today's
|
|
218
|
+
* skip-on-absence behaviour is unchanged. Meaningful only on
|
|
219
|
+
* `per_transaction`; the other `BoundType` kinds have no per-call "value
|
|
220
|
+
* exposed or not" question — `cumulative_sum`/`cumulative_count` always read
|
|
221
|
+
* from the execution log regardless of what this call declares, and `enum`
|
|
222
|
+
* is a capability flag, not a runtime value — so the field does not exist
|
|
223
|
+
* on those variants and a profile that puts it there has it silently
|
|
224
|
+
* ignored by every enforcement point (TypeScript also refuses it when the
|
|
225
|
+
* profile is authored against this type rather than raw JSON).
|
|
208
226
|
*/
|
|
209
227
|
{
|
|
210
228
|
kind: 'per_transaction';
|
|
211
229
|
of: string;
|
|
230
|
+
requiredFor?: string[];
|
|
212
231
|
}
|
|
213
232
|
/**
|
|
214
233
|
* Cumulative sum within a time window. The SP maintains a running sum
|
|
@@ -503,6 +522,16 @@ interface AgentProfile {
|
|
|
503
522
|
boundsSchema?: {
|
|
504
523
|
keyOrder: string[];
|
|
505
524
|
fields: Record<string, ProfileBoundsField>;
|
|
525
|
+
/**
|
|
526
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
527
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
528
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
529
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
530
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
531
|
+
* Absent on profiles published before the registry existed — membership
|
|
532
|
+
* is then uncheckable, not a violation.
|
|
533
|
+
*/
|
|
534
|
+
actionTypes?: readonly string[];
|
|
506
535
|
};
|
|
507
536
|
/**
|
|
508
537
|
* v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
|
|
@@ -983,6 +1012,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
983
1012
|
valid: boolean;
|
|
984
1013
|
errors: string[];
|
|
985
1014
|
};
|
|
1015
|
+
/**
|
|
1016
|
+
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
1017
|
+
* entries must name a real `action_type` from the profile's declared
|
|
1018
|
+
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
1019
|
+
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
1020
|
+
* kinds don't carry it).
|
|
1021
|
+
*
|
|
1022
|
+
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
1023
|
+
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
1024
|
+
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
1025
|
+
* an automatic refusal here would be new behaviour beyond this change's
|
|
1026
|
+
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
1027
|
+
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
1028
|
+
* action type) or is attached to a bound kind that silently ignores it.
|
|
1029
|
+
*
|
|
1030
|
+
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
1031
|
+
* check — never throws.
|
|
1032
|
+
*/
|
|
1033
|
+
declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
|
|
986
1034
|
/**
|
|
987
1035
|
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
988
1036
|
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
@@ -1353,4 +1401,4 @@ interface ReceiptPayload {
|
|
|
1353
1401
|
*/
|
|
1354
1402
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1355
1403
|
|
|
1356
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
|
1404
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateBoundsRequiredFor, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
|
@@ -72,6 +72,7 @@ __export(index_exports, {
|
|
|
72
72
|
signApproval: () => signApproval,
|
|
73
73
|
signMandateProjection: () => signMandateProjection,
|
|
74
74
|
validateBoundsParams: () => validateBoundsParams,
|
|
75
|
+
validateBoundsRequiredFor: () => validateBoundsRequiredFor,
|
|
75
76
|
validateContextParams: () => validateContextParams,
|
|
76
77
|
validateFrameParams: () => validateFrameParams,
|
|
77
78
|
validateSubject: () => validateSubject,
|
|
@@ -378,6 +379,38 @@ function validateContextParams(params, profile) {
|
|
|
378
379
|
}
|
|
379
380
|
return { valid: errors.length === 0, errors };
|
|
380
381
|
}
|
|
382
|
+
function validateBoundsRequiredFor(profile) {
|
|
383
|
+
const errors = [];
|
|
384
|
+
const boundsSchema = profile.boundsSchema;
|
|
385
|
+
if (!boundsSchema) return errors;
|
|
386
|
+
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
|
|
387
|
+
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
388
|
+
const bt = fieldDef.boundType;
|
|
389
|
+
if (!bt) continue;
|
|
390
|
+
const requiredFor = bt.requiredFor;
|
|
391
|
+
if (requiredFor === void 0) continue;
|
|
392
|
+
if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === "string")) {
|
|
393
|
+
errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (bt.kind !== "per_transaction") {
|
|
397
|
+
errors.push(
|
|
398
|
+
`Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound (found on kind "${bt.kind}") \u2014 every enforcement point ignores it there. Remove it.`
|
|
399
|
+
);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (actionTypes) {
|
|
403
|
+
for (const actionType of requiredFor) {
|
|
404
|
+
if (!actionTypes.includes(actionType)) {
|
|
405
|
+
errors.push(
|
|
406
|
+
`Bound "${fieldName}": requiredFor names action type "${actionType}", which is not in this profile's boundsSchema.actionTypes registry [${actionTypes.join(", ")}]. It can never match a real execution and the requirement is dead.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return errors;
|
|
413
|
+
}
|
|
381
414
|
var CanonicalValueError = class extends Error {
|
|
382
415
|
code;
|
|
383
416
|
field;
|
|
@@ -820,6 +853,22 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
820
853
|
switch (bt.kind) {
|
|
821
854
|
case "per_transaction": {
|
|
822
855
|
const actual = request.execution[bt.of];
|
|
856
|
+
const engaged = !!bt.requiredFor?.length && actionType !== void 0 && bt.requiredFor.includes(actionType);
|
|
857
|
+
if (engaged) {
|
|
858
|
+
const missing = actual === void 0 || actual === null;
|
|
859
|
+
const numericActual = missing ? NaN : typeof actual === "number" ? actual : Number(actual);
|
|
860
|
+
const missingOrNonNumeric = missing || Number.isNaN(numericActual);
|
|
861
|
+
if (missingOrNonNumeric) {
|
|
862
|
+
errors.push({
|
|
863
|
+
code: "BOUND_EXCEEDED",
|
|
864
|
+
field: bt.of,
|
|
865
|
+
message: missing ? `Bound "${fieldName}" requires "${bt.of}" for "${actionType}" calls, but this call exposes no ${bt.of} to check against it. Refusing: the call cannot be shown to stay within ${fieldName}.` : `Bound "${fieldName}" requires a numeric "${bt.of}" for "${actionType}" calls, but this call's value (${String(actual)}) is not a number. Refusing: the call cannot be shown to stay within ${fieldName}.`,
|
|
866
|
+
bound: boundValue,
|
|
867
|
+
actual
|
|
868
|
+
});
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
823
872
|
if (actual === void 0) continue;
|
|
824
873
|
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
825
874
|
errors.push({
|
|
@@ -1266,6 +1315,7 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1266
1315
|
signApproval,
|
|
1267
1316
|
signMandateProjection,
|
|
1268
1317
|
validateBoundsParams,
|
|
1318
|
+
validateBoundsRequiredFor,
|
|
1269
1319
|
validateContextParams,
|
|
1270
1320
|
validateFrameParams,
|
|
1271
1321
|
validateSubject,
|