@jini-ai/core 0.6.15-rc.1 → 0.6.15
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 +16 -0
- package/README.md +1 -0
- package/dist/approval.d.ts +4 -2
- package/dist/approval.js +7 -3
- package/dist/batch-tool-inputs.js +10 -3
- package/dist/contribution-registry.d.ts +23 -0
- package/dist/fetch/cache.d.ts +1 -0
- package/dist/fetch/cache.js +15 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/origin-validation.js +9 -8
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +2 -1
- package/dist/primitives/sse.js +4 -3
- package/dist/primitives/trim-chars.d.ts +25 -0
- package/dist/primitives/trim-chars.js +28 -0
- package/dist/records.d.ts +29 -6
- package/dist/records.js +105 -15
- package/dist/redact.d.ts +0 -1
- package/dist/redact.js +29 -7
- package/dist/registration-kit.js +1 -3
- package/dist/settings/agent-tools.js +7 -5
- package/dist/settings/agent-value-write-tools.js +7 -3
- package/dist/settings/dictionaries/index.d.ts +0 -22
- package/dist/settings/dictionaries/index.js +0 -1
- package/dist/settings/ensure-definitions.js +4 -2
- package/dist/settings/express/http-request.js +2 -1
- package/dist/settings/express/http.js +12 -4
- package/dist/settings/repo.memory.js +5 -2
- package/dist/settings/settings.js +5 -1
- package/dist/settings/tool-registrations.js +9 -3
- package/dist/settings/write-service.js +11 -9
- package/dist/target-scope.js +2 -1
- package/dist/text/index.d.ts +2 -0
- package/dist/text/index.js +2 -0
- package/dist/text/line-tail.d.ts +17 -0
- package/dist/text/line-tail.js +25 -0
- package/dist/text/sanitize-untrusted-text.js +33 -4
- package/dist/tool-exposure.js +20 -16
- package/dist/tool-input-normalization.d.ts +11 -3
- package/dist/tool-input-normalization.js +14 -8
- package/dist/tool-metadata-types.d.ts +5 -0
- package/dist/tool-registry.d.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.6.15
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Export async WebCrypto `canonicalArgumentDigest` from `@jini-ai/core/records`, reusing
|
|
8
|
+
`canonicalJson` for identical plugin and host SHA-256 hex digests. Canonical JSON now
|
|
9
|
+
sorts keys recursively by Unicode code point and throws `CanonicalJsonError` for
|
|
10
|
+
non-JSON values and cycles; undefined object properties remain omitted.
|
|
11
|
+
|
|
12
|
+
MCP approval fingerprints reuse the core serializer. Existing ordinary JSON
|
|
13
|
+
fingerprints retain their bytes; non-JSON schemas now fail instead of colliding
|
|
14
|
+
with valid JSON, and astral object keys follow the shared code-point ordering.
|
|
15
|
+
|
|
16
|
+
- Export generic tool-contributor, derived-contributor and paired-registry contracts
|
|
17
|
+
from the existing contribution-registry owner without coupling core to host surfaces.
|
|
18
|
+
|
|
3
19
|
## 0.6.13
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -100,6 +100,7 @@ Tool handlers receive required execution context first and optional `{ emitSurfa
|
|
|
100
100
|
| subpath | what's behind it |
|
|
101
101
|
|---|---|
|
|
102
102
|
| `./fetch` | Framework-neutral query contracts and `FetchQueryCache`; inject transport callbacks, `Clock`, scheduler and connectivity/focus ports. No React, DOM or TanStack dependency. |
|
|
103
|
+
| `./records` | `canonicalJson({ value })`, async WebCrypto `canonicalArgumentDigest({ args }, { crypto? })`, and `CanonicalJsonError`; recursively sorted Unicode code-point keys, strict JSON validation, and lowercase SHA-256 hex. |
|
|
103
104
|
| `.` | The public surface above: tokens, packs, daemon composition, tool registry (descriptors only), principal, redact, auth primitives. |
|
|
104
105
|
| `./composition` | **Not part of the public contract.** Exposes `authorizeToolInvocation` (resolves a registry's tool and runs its authorization gate — a security-sensitive value export, but one that only ever hands back a handler alongside an `'allow'` decision) plus type-only `AnyPack`/`RequiredTokenIds`/`MissingTokenIds` re-derivation helpers. Its sole intended consumers are `@jini-ai/daemon`'s `ToolExecutor` and `@jini-ai/server`'s `createLocalNodeDaemon`. Node's resolver will happily let any consumer import it — the boundary is enforced by `scripts/check-engine-boundaries.ts`, not by the exports map — so external adopters should treat it as off-limits regardless. |
|
|
105
106
|
|
package/dist/approval.d.ts
CHANGED
|
@@ -15,11 +15,13 @@ export type ApprovalAnswer = {
|
|
|
15
15
|
reason: 'declined' | 'expired' | 'abandoned';
|
|
16
16
|
};
|
|
17
17
|
/** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
|
|
18
|
-
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
18
|
+
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
19
|
+
* @param optional.onApprovalStateChange Reports only the human wait, released on every outcome.
|
|
20
|
+
* @complexity O(1) local time/space plus the injected human transport's wait. */
|
|
19
21
|
export declare function resolveApproval({ signal, askHuman }: {
|
|
20
22
|
signal: AbortSignal;
|
|
21
23
|
askHuman: () => Promise<ApprovalAnswer>;
|
|
22
|
-
},
|
|
24
|
+
}, { onApprovalStateChange }?: Pick<ToolExecutionOptions, 'onApprovalStateChange'>): Promise<ApprovalAnswer>;
|
|
23
25
|
export interface RememberedApprovalPort {
|
|
24
26
|
has(required: {
|
|
25
27
|
ctx: ToolExecutionContext;
|
package/dist/approval.js
CHANGED
|
@@ -2,19 +2,23 @@ import { humanConfirmedHandler, requireInputRecord } from './registration-kit.js
|
|
|
2
2
|
import { ToolInputError } from './tool-registry.js';
|
|
3
3
|
import { isNonArrayRecord } from './records.js';
|
|
4
4
|
/** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
|
|
5
|
-
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
6
|
-
|
|
5
|
+
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
6
|
+
* @param optional.onApprovalStateChange Reports only the human wait, released on every outcome.
|
|
7
|
+
* @complexity O(1) local time/space plus the injected human transport's wait. */
|
|
8
|
+
export async function resolveApproval({ signal, askHuman }, { onApprovalStateChange } = {}) {
|
|
7
9
|
if (signal.aborted)
|
|
8
10
|
return { confirmed: false, reason: 'abandoned' };
|
|
9
11
|
let abandon;
|
|
10
12
|
const aborted = new Promise(resolve => { abandon = () => resolve({ confirmed: false, reason: 'abandoned' }); });
|
|
11
13
|
signal.addEventListener('abort', abandon, { once: true });
|
|
12
14
|
try {
|
|
15
|
+
onApprovalStateChange?.({ pending: true });
|
|
13
16
|
const answer = await Promise.race([askHuman(), aborted]);
|
|
14
17
|
return signal.aborted ? { confirmed: false, reason: 'abandoned' } : answer;
|
|
15
18
|
}
|
|
16
19
|
finally {
|
|
17
20
|
signal.removeEventListener('abort', abandon);
|
|
21
|
+
onApprovalStateChange?.({ pending: false });
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
/** The confirmer is always the run's own delegating principal, as a user. */
|
|
@@ -38,7 +42,7 @@ export function createApprovalHandler(steps, { remembered } = {}) {
|
|
|
38
42
|
/** Ask the host transport; record the choice and any reusable grant only on an explicit yes. */
|
|
39
43
|
const askFresh = async ({ prepared, plan }, options = {}) => {
|
|
40
44
|
const { ctx } = prepared;
|
|
41
|
-
const answer = await resolveApproval({ signal: ctx.signal, askHuman: () => steps.askHuman({ ctx, description: plan.description }, options) });
|
|
45
|
+
const answer = await resolveApproval({ signal: ctx.signal, askHuman: () => steps.askHuman({ ctx, description: plan.description }, options) }, options);
|
|
42
46
|
if (ctx.signal.aborted)
|
|
43
47
|
return abandoned(prepared);
|
|
44
48
|
if (answer.confirmed !== true)
|
|
@@ -26,7 +26,13 @@ export function batchOutcomeFor({ output }, _options = {}) {
|
|
|
26
26
|
if (!isPlainObject(output) || output.batch !== true || !Array.isArray(output.results) || output.results.length === 0)
|
|
27
27
|
return undefined;
|
|
28
28
|
const failed = output.results.filter(item => isPlainObject(item) && item.ok === false).length;
|
|
29
|
-
return failed
|
|
29
|
+
return outcomeFor({ failed, total: output.results.length });
|
|
30
|
+
}
|
|
31
|
+
/** No failure completes the batch, all failing is an error, anything between is partial. */
|
|
32
|
+
function outcomeFor({ failed, total }) {
|
|
33
|
+
if (failed === 0)
|
|
34
|
+
return 'completed';
|
|
35
|
+
return failed === total ? 'error' : 'partial_failure';
|
|
30
36
|
}
|
|
31
37
|
/** A JSON object: not null, not an array. */
|
|
32
38
|
function isPlainObject(value) {
|
|
@@ -54,7 +60,8 @@ export function batchDescriptorFor({ descriptor, tool }, _optional = {}) {
|
|
|
54
60
|
}
|
|
55
61
|
const maxItems = tool.maxItems ?? DEFAULT_MAX_BATCH_ITEMS;
|
|
56
62
|
const requiredFields = schema.required ?? [];
|
|
57
|
-
const
|
|
63
|
+
const rest = { ...schema };
|
|
64
|
+
delete rest.required;
|
|
58
65
|
const needs = requiredFields.length > 0 ? ` Each item needs ${requiredFields.join(", ")} (inside each item).` : "";
|
|
59
66
|
const items = {
|
|
60
67
|
type: "array",
|
|
@@ -119,7 +126,7 @@ function batchItemResult({ index, output, notes }, _options = {}) {
|
|
|
119
126
|
*/
|
|
120
127
|
function summarizeBatchResults({ results }, _options = {}) {
|
|
121
128
|
const succeeded = results.filter((result) => result.ok).length;
|
|
122
|
-
const outcome =
|
|
129
|
+
const outcome = outcomeFor({ failed: results.length - succeeded, total: results.length });
|
|
123
130
|
return { batch: true, outcome, ...(succeeded === 0 ? { isError: true } : {}), total: results.length, succeeded, failed: results.length - succeeded, results };
|
|
124
131
|
}
|
|
125
132
|
/**
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import type { DerivedRiskByToolId } from './registration-kit.js';
|
|
2
|
+
import type { ToolRegistration } from './tool-registry.js';
|
|
3
|
+
/** A domain owns its builder and risk classification; the host binds dependencies and surfaces. */
|
|
4
|
+
export interface ToolContributor<TDeps, TSurfaces> {
|
|
5
|
+
readonly domain: string;
|
|
6
|
+
readonly build: (deps: TDeps, surfaces: TSurfaces) => ToolRegistration[];
|
|
7
|
+
readonly risk: DerivedRiskByToolId;
|
|
8
|
+
}
|
|
9
|
+
/** Post-processing sees the domain registrations; ordering and collision checks belong to the host. */
|
|
10
|
+
export interface DerivedToolContributor<TDeps, TSurfaces> {
|
|
11
|
+
readonly domain: string;
|
|
12
|
+
readonly derive: (input: {
|
|
13
|
+
readonly registrations: readonly ToolRegistration[];
|
|
14
|
+
readonly routeDeps: TDeps;
|
|
15
|
+
readonly surfaces: TSurfaces;
|
|
16
|
+
}) => ToolRegistration[];
|
|
17
|
+
readonly risk: DerivedRiskByToolId;
|
|
18
|
+
}
|
|
19
|
+
/** The two ordered registries belong to one composition root, never shared across workspaces. */
|
|
20
|
+
export interface ToolContributions<TDeps, TSurfaces> {
|
|
21
|
+
readonly contributors: ContributionRegistry<ToolContributor<TDeps, TSurfaces>>;
|
|
22
|
+
readonly derivedContributors: ContributionRegistry<DerivedToolContributor<TDeps, TSurfaces>>;
|
|
23
|
+
}
|
|
1
24
|
/** Replace-by-key registry for host-owned contributions; no singleton or execution surface. */
|
|
2
25
|
export interface ContributionRegistry<TContribution> {
|
|
3
26
|
register(required: {
|
package/dist/fetch/cache.d.ts
CHANGED
|
@@ -110,6 +110,7 @@ export declare class FetchQueryCache {
|
|
|
110
110
|
invalidate({ prefix }: {
|
|
111
111
|
prefix: QueryKey;
|
|
112
112
|
}): void;
|
|
113
|
+
private reloadInvalidated;
|
|
113
114
|
onFocus(): void;
|
|
114
115
|
/** Execute a read/write once connectivity returns; failures are not retried.
|
|
115
116
|
* @param required.run Operation to invoke online.
|
package/dist/fetch/cache.js
CHANGED
|
@@ -16,7 +16,9 @@ const browserScheduler = {
|
|
|
16
16
|
// Normalize unknown promise rejections once so consumers receive Error without repeating
|
|
17
17
|
// the same narrowing boilerplate at every read and write call site.
|
|
18
18
|
export function toQueryError(value) {
|
|
19
|
-
|
|
19
|
+
if (value instanceof Error)
|
|
20
|
+
return value;
|
|
21
|
+
return new Error(typeof value === 'string' && value.trim() ? value : 'request failed');
|
|
20
22
|
}
|
|
21
23
|
/** One provider-owned cache. Requests are shared by structural primitive keys.
|
|
22
24
|
* Failed reads retain earlier data; replacement writes supersede older requests.
|
|
@@ -162,12 +164,17 @@ export class FetchQueryCache {
|
|
|
162
164
|
const pendingRead = entry.pendingRead;
|
|
163
165
|
entry.pending = undefined;
|
|
164
166
|
entry.pendingRead = undefined;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
this.reloadInvalidated(entry, pendingRead);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
reloadInvalidated(entry, pendingRead) {
|
|
171
|
+
if (entry.observers.size) {
|
|
172
|
+
this.refresh(entry);
|
|
173
|
+
return;
|
|
170
174
|
}
|
|
175
|
+
// An awaiting loader still needs an answer even if no observer is mounted.
|
|
176
|
+
if (pendingRead)
|
|
177
|
+
void this.load({ entry, ...pendingRead });
|
|
171
178
|
}
|
|
172
179
|
onFocus() {
|
|
173
180
|
for (const entry of this.entries.values()) {
|
|
@@ -249,7 +256,8 @@ export class FetchQueryCache {
|
|
|
249
256
|
}
|
|
250
257
|
scheduleEviction(entry) {
|
|
251
258
|
this.cancelEviction(entry);
|
|
252
|
-
|
|
259
|
+
const inUse = entry.listeners.size > 0 || entry.observers.size > 0 || Boolean(entry.pending);
|
|
260
|
+
if (this.disposed || inUse || !Number.isFinite(entry.retention))
|
|
253
261
|
return;
|
|
254
262
|
entry.eviction = this.scheduler.schedule({ callback: () => this.entries.delete(JSON.stringify(entry.key)), delayMs: entry.retention });
|
|
255
263
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export * from './tool-tokens.js';
|
|
|
17
17
|
export * from './tool-exposure.js';
|
|
18
18
|
export * from './gated-mutations.js';
|
|
19
19
|
export * from './model-facing-tool-errors.js';
|
|
20
|
-
export
|
|
20
|
+
export { createContributionRegistry } from './contribution-registry.js';
|
|
21
|
+
export type { ContributionRegistry, ToolContributor, DerivedToolContributor, ToolContributions } from './contribution-registry.js';
|
|
21
22
|
export * from './provider-registry.js';
|
|
22
23
|
export * from './object-store.js';
|
|
23
24
|
export * from './naming.js';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export * from './tool-tokens.js';
|
|
|
14
14
|
export * from './tool-exposure.js';
|
|
15
15
|
export * from './gated-mutations.js';
|
|
16
16
|
export * from './model-facing-tool-errors.js';
|
|
17
|
-
export
|
|
17
|
+
export { createContributionRegistry } from './contribution-registry.js';
|
|
18
18
|
export * from './provider-registry.js';
|
|
19
19
|
export * from './object-store.js';
|
|
20
20
|
export * from './naming.js';
|
|
@@ -134,7 +134,13 @@ function parseDottedQuad(host) {
|
|
|
134
134
|
}
|
|
135
135
|
/** RFC 1918 (10/8, 172.16/12, 192.168/16) or link-local (169.254/16), from the first two octets. */
|
|
136
136
|
function isPrivateIpv4Prefix([a, b]) {
|
|
137
|
-
|
|
137
|
+
if (a === 10)
|
|
138
|
+
return true;
|
|
139
|
+
if (a === 172)
|
|
140
|
+
return b >= 16 && b <= 31;
|
|
141
|
+
if (a === 192)
|
|
142
|
+
return b === 168;
|
|
143
|
+
return a === 169 && b === 254;
|
|
138
144
|
}
|
|
139
145
|
/** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16).
|
|
140
146
|
* Implements the dev-browser LAN allow-list, not SSRF blocking. Use @jini-ai/platform/net
|
|
@@ -152,17 +158,12 @@ export function isIpLiteralHostname({ hostname }) {
|
|
|
152
158
|
return true;
|
|
153
159
|
return parseDottedQuad(host) !== undefined;
|
|
154
160
|
}
|
|
161
|
+
const LOOPBACK_OR_UNSPECIFIED_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]', '0.0.0.0', '::']);
|
|
155
162
|
/** Whether `hostname` is loopback, unspecified, or a private-LAN address.
|
|
156
163
|
* Implements the dev-browser LAN allow-list, not SSRF blocking (@jini-ai/platform/net). */
|
|
157
164
|
export function isLoopbackOrPrivateLanHost({ hostname }) {
|
|
158
165
|
const host = String(hostname || '').toLowerCase();
|
|
159
|
-
return (host
|
|
160
|
-
host === '127.0.0.1' ||
|
|
161
|
-
host === '::1' ||
|
|
162
|
-
host === '[::1]' ||
|
|
163
|
-
host === '0.0.0.0' ||
|
|
164
|
-
host === '::' ||
|
|
165
|
-
isPrivateIpv4({ hostname: host }));
|
|
166
|
+
return LOOPBACK_OR_UNSPECIFIED_HOSTS.has(host) || isPrivateIpv4({ hostname: host });
|
|
166
167
|
}
|
|
167
168
|
/** Whether a request's `Host` header names a locally-served or explicitly allow-listed host. */
|
|
168
169
|
export function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowedOrigins }) {
|
package/dist/primitives/index.js
CHANGED
package/dist/primitives/paths.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimCharsEnd } from './trim-chars.js';
|
|
1
2
|
/** Root of an already `/`-separated Windows path: a drive root (`C:/`) or a UNC share root. */
|
|
2
3
|
function windowsRoot(path) {
|
|
3
4
|
return /^[A-Za-z]:\//.exec(path)?.[0] ?? /^\/\/[^/]+\/[^/]+(?:\/|$)/.exec(path)?.[0];
|
|
@@ -22,7 +23,7 @@ function normalizeAbsolutePath(value, separator) {
|
|
|
22
23
|
const prefix = (windows ? windowsRoot(path) : undefined) ?? (path.startsWith('/') ? '/' : undefined);
|
|
23
24
|
if (!prefix)
|
|
24
25
|
throw new RangeError('pathContains requires resolved absolute paths');
|
|
25
|
-
return
|
|
26
|
+
return trimCharsEnd({ text: prefix, chars: '/' }) + '/' + resolveSegments(path.slice(prefix.length)).join('/');
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Check lexical containment using whole directory segments, including equality.
|
package/dist/primitives/sse.js
CHANGED
|
@@ -33,13 +33,14 @@ function createRecordDecoder() {
|
|
|
33
33
|
let value = colon === -1 ? '' : line.slice(colon + 1);
|
|
34
34
|
if (value.startsWith(' '))
|
|
35
35
|
value = value.slice(1);
|
|
36
|
+
// The field names are distinct, so at most one of these applies; retry/unknown fields do not
|
|
37
|
+
// belong to per-record interpretation.
|
|
36
38
|
if (field === 'event')
|
|
37
39
|
event = value;
|
|
38
|
-
|
|
40
|
+
if (field === 'data')
|
|
39
41
|
dataLines.push(value);
|
|
40
|
-
|
|
42
|
+
if (field === 'id')
|
|
41
43
|
id = value;
|
|
42
|
-
// retry/unknown fields do not belong to per-record interpretation.
|
|
43
44
|
},
|
|
44
45
|
take() {
|
|
45
46
|
const frame = { event, data: dataLines.join('\n'), dataLines, ...(id !== undefined ? { id } : {}) };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Linear-time trimming of an arbitrary character set from either end of a string. The regex
|
|
3
|
+
* spellings these replace (`s.replace(/\/+$/, '')`, `s.replace(/^-+|-+$/g, '')`) backtrack
|
|
4
|
+
* quadratically on long runs of the trimmed character that do not reach the end, because the
|
|
5
|
+
* engine retries the `+` run from every start position. A leaf module with no imports.
|
|
6
|
+
*/
|
|
7
|
+
/** Strips every trailing character contained in `chars` (a set of single UTF-16 units).
|
|
8
|
+
* Same result as `text.replace(/[chars]+$/, '')`. @complexity O(text length). */
|
|
9
|
+
export declare function trimCharsEnd({ text, chars }: {
|
|
10
|
+
text: string;
|
|
11
|
+
chars: string;
|
|
12
|
+
}): string;
|
|
13
|
+
/** Strips every leading character contained in `chars`. Same result as
|
|
14
|
+
* `text.replace(/^[chars]+/, '')`. @complexity O(text length). */
|
|
15
|
+
export declare function trimCharsStart({ text, chars }: {
|
|
16
|
+
text: string;
|
|
17
|
+
chars: string;
|
|
18
|
+
}): string;
|
|
19
|
+
/** Strips `chars` from both ends. Same result as `text.replace(/^[chars]+|[chars]+$/g, '')`.
|
|
20
|
+
* @complexity O(text length). */
|
|
21
|
+
export declare function trimChars({ text, chars }: {
|
|
22
|
+
text: string;
|
|
23
|
+
chars: string;
|
|
24
|
+
}): string;
|
|
25
|
+
//# sourceMappingURL=trim-chars.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Linear-time trimming of an arbitrary character set from either end of a string. The regex
|
|
3
|
+
* spellings these replace (`s.replace(/\/+$/, '')`, `s.replace(/^-+|-+$/g, '')`) backtrack
|
|
4
|
+
* quadratically on long runs of the trimmed character that do not reach the end, because the
|
|
5
|
+
* engine retries the `+` run from every start position. A leaf module with no imports.
|
|
6
|
+
*/
|
|
7
|
+
/** Strips every trailing character contained in `chars` (a set of single UTF-16 units).
|
|
8
|
+
* Same result as `text.replace(/[chars]+$/, '')`. @complexity O(text length). */
|
|
9
|
+
export function trimCharsEnd({ text, chars }) {
|
|
10
|
+
let end = text.length;
|
|
11
|
+
while (end > 0 && chars.includes(text.charAt(end - 1)))
|
|
12
|
+
end -= 1;
|
|
13
|
+
return text.slice(0, end);
|
|
14
|
+
}
|
|
15
|
+
/** Strips every leading character contained in `chars`. Same result as
|
|
16
|
+
* `text.replace(/^[chars]+/, '')`. @complexity O(text length). */
|
|
17
|
+
export function trimCharsStart({ text, chars }) {
|
|
18
|
+
let start = 0;
|
|
19
|
+
while (start < text.length && chars.includes(text.charAt(start)))
|
|
20
|
+
start += 1;
|
|
21
|
+
return text.slice(start);
|
|
22
|
+
}
|
|
23
|
+
/** Strips `chars` from both ends. Same result as `text.replace(/^[chars]+|[chars]+$/g, '')`.
|
|
24
|
+
* @complexity O(text length). */
|
|
25
|
+
export function trimChars({ text, chars }) {
|
|
26
|
+
return trimCharsStart({ text: trimCharsEnd({ text, chars }), chars });
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=trim-chars.js.map
|
package/dist/records.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @file
|
|
1
|
+
/** @file Object-shape predicates and canonical JSON shared by tool readers and request digests. */
|
|
2
2
|
/** Accept any non-null object, including arrays; never sanitizes or reads properties.
|
|
3
3
|
* @complexity O(1) time and space.
|
|
4
4
|
* @example isRecord({ value: [] }); // true
|
|
@@ -45,14 +45,37 @@ export declare function deleteMapRecord<Key, Row extends object>({ records, key,
|
|
|
45
45
|
key: Key;
|
|
46
46
|
fields: Partial<Row>;
|
|
47
47
|
}, _optional?: Record<string, never>): void;
|
|
48
|
+
/** Invalid canonical JSON input. Messages never include argument contents. */
|
|
49
|
+
export declare class CanonicalJsonError extends TypeError {
|
|
50
|
+
constructor(required: {
|
|
51
|
+
readonly message: string;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
48
54
|
/**
|
|
49
|
-
* Canonical JSON: object keys
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @
|
|
55
|
+
* Canonical JSON: recursively sort object keys by Unicode code point, omit undefined object
|
|
56
|
+
* properties, and preserve array order. Uses JSON scalar escaping without invoking toJSON hooks.
|
|
57
|
+
* @param required.value JSON input; shared references are allowed. Does not mutate input.
|
|
58
|
+
* @throws CanonicalJsonError for non-JSON values (including array holes) and cycles.
|
|
59
|
+
* @complexity O(s + sum(k log k * l)) time for s output bytes, k keys per object and key length l;
|
|
60
|
+
* O(s * d) worst-case space for nesting depth d, including intermediate strings and recursion.
|
|
53
61
|
* @example canonicalJson({ value: { b: 1, a: 2 } }); // '{"a":2,"b":1}'
|
|
54
62
|
*/
|
|
55
63
|
export declare function canonicalJson(required: {
|
|
56
64
|
readonly value: unknown;
|
|
57
|
-
}, _optional?:
|
|
65
|
+
}, _optional?: Record<string, never>): string;
|
|
66
|
+
/**
|
|
67
|
+
* SHA-256 lowercase hex over canonical UTF-8 tool arguments, shared by plugins and host gates.
|
|
68
|
+
* @param required.args JSON arguments; validation completes before hashing starts.
|
|
69
|
+
* @param optional.crypto Host WebCrypto port; defaults to the runtime's global WebCrypto.
|
|
70
|
+
* @returns An asynchronous digest, usable in browsers and Node without Node imports.
|
|
71
|
+
* @throws CanonicalJsonError for invalid arguments; WebCrypto failures propagate.
|
|
72
|
+
* @complexity Canonical serialization cost plus O(s) hashing time and space for s UTF-8 bytes.
|
|
73
|
+
*/
|
|
74
|
+
export declare function canonicalArgumentDigest(required: {
|
|
75
|
+
readonly args: unknown;
|
|
76
|
+
}, { crypto }?: {
|
|
77
|
+
readonly crypto?: {
|
|
78
|
+
readonly subtle: Pick<typeof globalThis.crypto.subtle, 'digest'>;
|
|
79
|
+
};
|
|
80
|
+
}): Promise<string>;
|
|
58
81
|
//# sourceMappingURL=records.d.ts.map
|
package/dist/records.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @file
|
|
1
|
+
/** @file Object-shape predicates and canonical JSON shared by tool readers and request digests. */
|
|
2
2
|
/** Accept any non-null object, including arrays; never sanitizes or reads properties.
|
|
3
3
|
* @complexity O(1) time and space.
|
|
4
4
|
* @example isRecord({ value: [] }); // true
|
|
@@ -43,23 +43,113 @@ export function deleteMapRecord({ records, key, fields }, _optional = {}) {
|
|
|
43
43
|
if (record && matchesRecordFields({ record, fields }))
|
|
44
44
|
records.delete(key);
|
|
45
45
|
}
|
|
46
|
+
/** Invalid canonical JSON input. Messages never include argument contents. */
|
|
47
|
+
export class CanonicalJsonError extends TypeError {
|
|
48
|
+
constructor(required) {
|
|
49
|
+
super(required.message);
|
|
50
|
+
this.name = 'CanonicalJsonError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Compare Unicode code points, including astral characters; UTF-16 sort differs above U+FFFF.
|
|
54
|
+
* @complexity O(l) time and space for the combined key length l.
|
|
55
|
+
*/
|
|
56
|
+
function compareJsonKeys(left, right) {
|
|
57
|
+
const a = Array.from(left);
|
|
58
|
+
const b = Array.from(right);
|
|
59
|
+
for (let index = 0; index < Math.min(a.length, b.length); index++) {
|
|
60
|
+
const difference = a[index].codePointAt(0) - b[index].codePointAt(0);
|
|
61
|
+
if (difference !== 0)
|
|
62
|
+
return difference;
|
|
63
|
+
}
|
|
64
|
+
return a.length - b.length;
|
|
65
|
+
}
|
|
66
|
+
/** Serialize JSON scalars without JSON.stringify's lossy non-JSON fallbacks.
|
|
67
|
+
* @throws CanonicalJsonError for unsupported types or non-finite numbers.
|
|
68
|
+
* @complexity O(s) time and space for s serialized characters.
|
|
69
|
+
*/
|
|
70
|
+
function canonicalScalar(value) {
|
|
71
|
+
switch (typeof value) {
|
|
72
|
+
case 'string':
|
|
73
|
+
case 'boolean':
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
case 'number':
|
|
76
|
+
if (Number.isFinite(value))
|
|
77
|
+
return JSON.stringify(value);
|
|
78
|
+
throw new CanonicalJsonError({ message: 'Canonical JSON requires finite numbers.' });
|
|
79
|
+
default:
|
|
80
|
+
throw new CanonicalJsonError({ message: 'Canonical JSON requires JSON values.' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Serialize own enumerable JSON fields; class instances must not silently collapse to {}.
|
|
84
|
+
* @throws CanonicalJsonError for non-JSON objects or enumerable symbol keys.
|
|
85
|
+
* @complexity O(k log k * l + s) time and O(s + k * l) space, plus recursive children.
|
|
86
|
+
*/
|
|
87
|
+
function canonicalObject(value, ancestors) {
|
|
88
|
+
const prototype = Object.getPrototypeOf(value);
|
|
89
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
90
|
+
throw new CanonicalJsonError({ message: 'Canonical JSON requires plain objects or arrays.' });
|
|
91
|
+
}
|
|
92
|
+
if (Object.getOwnPropertySymbols(value).some(key => Object.prototype.propertyIsEnumerable.call(value, key))) {
|
|
93
|
+
throw new CanonicalJsonError({ message: 'Canonical JSON requires string object keys.' });
|
|
94
|
+
}
|
|
95
|
+
const record = value;
|
|
96
|
+
// Object keys are unique, so comparing their code points gives a total order here.
|
|
97
|
+
const members = Object.keys(record).sort(compareJsonKeys).flatMap(key => {
|
|
98
|
+
const child = record[key];
|
|
99
|
+
if (child === undefined)
|
|
100
|
+
return [];
|
|
101
|
+
return [`${JSON.stringify(key)}:${canonicalValue(child, ancestors)}`];
|
|
102
|
+
});
|
|
103
|
+
return `{${members.join(',')}}`;
|
|
104
|
+
}
|
|
105
|
+
/** Track only active ancestors: repeated references are JSON, back-edges are cycles.
|
|
106
|
+
* @throws CanonicalJsonError for cycles or non-JSON children.
|
|
107
|
+
* @complexity O(s + sum(k log k * l)) time; O(s * d) worst-case space for depth d.
|
|
108
|
+
*/
|
|
109
|
+
function canonicalValue(value, ancestors) {
|
|
110
|
+
if (value === null)
|
|
111
|
+
return 'null';
|
|
112
|
+
if (typeof value !== 'object')
|
|
113
|
+
return canonicalScalar(value);
|
|
114
|
+
if (ancestors.has(value))
|
|
115
|
+
throw new CanonicalJsonError({ message: 'Canonical JSON does not support cycles.' });
|
|
116
|
+
ancestors.add(value);
|
|
117
|
+
try {
|
|
118
|
+
if (!Array.isArray(value))
|
|
119
|
+
return canonicalObject(value, ancestors);
|
|
120
|
+
// Read every index: holes must fail, and a custom iterator must not change the argument order.
|
|
121
|
+
const children = [];
|
|
122
|
+
for (let index = 0; index < value.length; index++)
|
|
123
|
+
children.push(canonicalValue(value[index], ancestors));
|
|
124
|
+
return `[${children.join(',')}]`;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
ancestors.delete(value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
46
130
|
/**
|
|
47
|
-
* Canonical JSON: object keys
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* @
|
|
131
|
+
* Canonical JSON: recursively sort object keys by Unicode code point, omit undefined object
|
|
132
|
+
* properties, and preserve array order. Uses JSON scalar escaping without invoking toJSON hooks.
|
|
133
|
+
* @param required.value JSON input; shared references are allowed. Does not mutate input.
|
|
134
|
+
* @throws CanonicalJsonError for non-JSON values (including array holes) and cycles.
|
|
135
|
+
* @complexity O(s + sum(k log k * l)) time for s output bytes, k keys per object and key length l;
|
|
136
|
+
* O(s * d) worst-case space for nesting depth d, including intermediate strings and recursion.
|
|
51
137
|
* @example canonicalJson({ value: { b: 1, a: 2 } }); // '{"a":2,"b":1}'
|
|
52
138
|
*/
|
|
53
139
|
export function canonicalJson(required, _optional = {}) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
140
|
+
return canonicalValue(required.value, new Set());
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* SHA-256 lowercase hex over canonical UTF-8 tool arguments, shared by plugins and host gates.
|
|
144
|
+
* @param required.args JSON arguments; validation completes before hashing starts.
|
|
145
|
+
* @param optional.crypto Host WebCrypto port; defaults to the runtime's global WebCrypto.
|
|
146
|
+
* @returns An asynchronous digest, usable in browsers and Node without Node imports.
|
|
147
|
+
* @throws CanonicalJsonError for invalid arguments; WebCrypto failures propagate.
|
|
148
|
+
* @complexity Canonical serialization cost plus O(s) hashing time and space for s UTF-8 bytes.
|
|
149
|
+
*/
|
|
150
|
+
export async function canonicalArgumentDigest(required, { crypto = globalThis.crypto } = {}) {
|
|
151
|
+
const json = canonicalJson({ value: required.args });
|
|
152
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(json));
|
|
153
|
+
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
64
154
|
}
|
|
65
155
|
//# sourceMappingURL=records.js.map
|
package/dist/redact.d.ts
CHANGED
|
@@ -38,7 +38,6 @@ export interface SecretShapePattern {
|
|
|
38
38
|
}
|
|
39
39
|
export declare const PEM_PATTERN_NAME = "PEM private key block";
|
|
40
40
|
export declare const SECRET_SHAPE_PATTERNS: readonly SecretShapePattern[];
|
|
41
|
-
/** Broad token shapes for agent-failure text; consumers retain their plain [REDACTED] pipeline. */
|
|
42
41
|
export declare const AGENT_FAILURE_SECRET_SHAPE_RE: RegExp;
|
|
43
42
|
/**
|
|
44
43
|
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|