@jini-ai/core 0.6.12 → 0.6.15-rc.2
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 +15 -0
- package/README.md +1 -0
- package/dist/approval.d.ts +1 -0
- package/dist/approval.js +9 -1
- package/dist/fetch/cache.d.ts +2 -0
- package/dist/fetch/cache.js +11 -3
- package/dist/filesystem.d.ts +46 -0
- package/dist/filesystem.js +8 -0
- package/dist/gated-mutations/actor-identity.js +6 -0
- package/dist/records.d.ts +34 -1
- package/dist/records.js +110 -1
- package/dist/redact.d.ts +8 -5
- package/dist/redact.js +64 -8
- package/dist/settings/dictionaries/index.d.ts +0 -22
- package/dist/settings/dictionaries/index.js +0 -1
- package/dist/text/sanitize-untrusted-text.d.ts +8 -1
- package/dist/text/sanitize-untrusted-text.js +21 -6
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.6.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7479159: Tighten diagnostic identifier exemptions and ignore ordinary header values shorter than eight characters during exact-secret masking (credential headers are masked at any length; exact secrets still match inside other text but never inside a credential label, URL scheme, secret-named query parameter or existing redaction marker, so they cannot split another credential's label). Keep null tool schemas informational. Preserve safe reader refusals for explicit plugin skill/file requests and use host-formatted plugin tool pointers for relevance, deferred instructions, and supporting files (optional for automatic use; omitted when the host supplies none).
|
|
8
|
+
- c31264d: Move `canonicalJson` to `@jini-ai/core/records` so packages below devops can reuse it (the infra outbox request digest). `@jini-ai/devops/dns` re-exports it unchanged.
|
|
9
|
+
- 0728914: Engine guard clean-up (`pnpm guard` 106 -> 0): layer, subpath-isolation and module-state findings fixed by moving
|
|
10
|
+
shared contracts to their owners instead of reaching across domains. Additive public API: `@jini-ai/core/filesystem`
|
|
11
|
+
(guarded-reader contracts), `useFetchQueryScope` from `@jini-ai/ui` fetch-query (per-provider query scope),
|
|
12
|
+
`@jini-ai/chat/react` re-exports of the accepted-input, run-state and agent-plugin chat-state helpers. The Claude
|
|
13
|
+
filesystem probe cache in `@jini-ai/agent-runtime` is instance-owned. No behaviour change intended; host-facing
|
|
14
|
+
breaks are listed in `owner-store-to-server` and `guard-host-ports`.
|
|
15
|
+
- 6ecbdb2: Add opt-in exact-only secret redaction with literal replacement markers, preserving unmatched text and existing policies.
|
|
16
|
+
- 63bacc5: OpenTelemetry and Sentry observability adapters moved out of `@jini-ai/diagnostics` into the `observability-opentelemetry` and `observability-sentry` provider plugins of `@jini-ai/provider-plugins`. Removed: the `@jini-ai/diagnostics/observability/opentelemetry` and `/observability/sentry` subpaths and every `@opentelemetry/*` / `@sentry/node` dev and optional peer dependency. The OTel plugin accepts any OTLP/HTTP endpoint plus generic request headers. Packages bundled into plugin artifacts now ship their LICENSE file.
|
|
17
|
+
|
|
3
18
|
## 0.6.12
|
|
4
19
|
|
|
5
20
|
## 0.6.11
|
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
|
@@ -127,6 +127,7 @@ export declare function createApprovalProposalStore<TDescription>({ responseTool
|
|
|
127
127
|
}, _optional?: {}): Promise<unknown>;
|
|
128
128
|
};
|
|
129
129
|
/** Apply a host classification through the same handler owner used by domain plans.
|
|
130
|
+
* A host description's string `rememberKey` opts into reusable grants; descriptions remain opaque.
|
|
130
131
|
* @returns The registration with one approval handler; authorization policy is preserved.
|
|
131
132
|
*/
|
|
132
133
|
export declare function applyApprovalPolicy<TDescription>({ registration, classify, asks, describe, askHuman, notConfirmed }: {
|
package/dist/approval.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { humanConfirmedHandler, requireInputRecord } from './registration-kit.js';
|
|
2
2
|
import { ToolInputError } from './tool-registry.js';
|
|
3
|
+
import { isNonArrayRecord } from './records.js';
|
|
3
4
|
/** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
|
|
4
5
|
* An answer never revives an aborted operation. Transport failures propagate without an effect. */
|
|
5
6
|
export async function resolveApproval({ signal, askHuman }, _optional = {}) {
|
|
@@ -193,12 +194,19 @@ export function createApprovalProposalStore({ responseTool, messages }, { create
|
|
|
193
194
|
};
|
|
194
195
|
}
|
|
195
196
|
/** Apply a host classification through the same handler owner used by domain plans.
|
|
197
|
+
* A host description's string `rememberKey` opts into reusable grants; descriptions remain opaque.
|
|
196
198
|
* @returns The registration with one approval handler; authorization policy is preserved.
|
|
197
199
|
*/
|
|
198
200
|
export function applyApprovalPolicy({ registration, classify, asks, describe, askHuman, notConfirmed }, optional = {}) {
|
|
199
201
|
return { ...registration, handler: createApprovalHandler({
|
|
200
202
|
prepare: async ({ ctx }) => classify({ input: ctx.input }),
|
|
201
|
-
describe: async ({ ctx, prepared }) =>
|
|
203
|
+
describe: async ({ ctx, prepared }) => {
|
|
204
|
+
const ask = asks({ class: prepared });
|
|
205
|
+
const description = await describe({ ctx, class: prepared });
|
|
206
|
+
const candidate = { value: description };
|
|
207
|
+
return { ask, description, ...(isNonArrayRecord(candidate) && typeof candidate.value.rememberKey === 'string'
|
|
208
|
+
? { rememberKey: candidate.value.rememberKey } : {}) };
|
|
209
|
+
},
|
|
202
210
|
askHuman, notConfirmed,
|
|
203
211
|
run: ({ ctx }, options) => registration.handler(ctx, options),
|
|
204
212
|
}, optional) };
|
package/dist/fetch/cache.d.ts
CHANGED
|
@@ -121,6 +121,8 @@ export declare class FetchQueryCache {
|
|
|
121
121
|
activate(): void;
|
|
122
122
|
dispose(): void;
|
|
123
123
|
private onOnline;
|
|
124
|
+
/** Follow newer work, retaining cached data or propagating a failed cold replacement. */
|
|
125
|
+
private supersedingResult;
|
|
124
126
|
private refresh;
|
|
125
127
|
private publish;
|
|
126
128
|
private cancelEviction;
|
package/dist/fetch/cache.js
CHANGED
|
@@ -81,7 +81,7 @@ export class FetchQueryCache {
|
|
|
81
81
|
const run = () => {
|
|
82
82
|
// A queued offline refresh may have been superseded before reconnection.
|
|
83
83
|
if (entry.pending !== pending)
|
|
84
|
-
return (entry
|
|
84
|
+
return this.supersedingResult({ entry });
|
|
85
85
|
this.publish(entry, { ...entry.state, isFetching: true });
|
|
86
86
|
return fetch();
|
|
87
87
|
};
|
|
@@ -100,7 +100,7 @@ export class FetchQueryCache {
|
|
|
100
100
|
// Only the current request may publish or clear invalidation. An older transport
|
|
101
101
|
// need not support abort: its callers follow the replacement request instead.
|
|
102
102
|
if (entry.pending !== pending)
|
|
103
|
-
return (entry
|
|
103
|
+
return this.supersedingResult({ entry });
|
|
104
104
|
// Arbitration and publication share this continuation: a microtask between them would
|
|
105
105
|
// let an older result overwrite a replacement accepted after arbitration.
|
|
106
106
|
if (replacement !== entry.replacement) {
|
|
@@ -113,7 +113,7 @@ export class FetchQueryCache {
|
|
|
113
113
|
return value;
|
|
114
114
|
}, error => {
|
|
115
115
|
if (entry.pending !== pending)
|
|
116
|
-
return (entry
|
|
116
|
+
return this.supersedingResult({ entry });
|
|
117
117
|
// A replacement write is newer than this read's failure as well as its data.
|
|
118
118
|
// Keep the write's timestamp and freshness; only settle the obsolete fetch flag.
|
|
119
119
|
if (replacement !== entry.replacement) {
|
|
@@ -224,6 +224,14 @@ export class FetchQueryCache {
|
|
|
224
224
|
void this.load({ entry, fetch: observer.fetch, staleTime: observer.staleTime });
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
|
+
/** Follow newer work, retaining cached data or propagating a failed cold replacement. */
|
|
228
|
+
async supersedingResult({ entry }, _options = {}) {
|
|
229
|
+
if (entry.pending)
|
|
230
|
+
return entry.pending;
|
|
231
|
+
if (entry.state.data === undefined && entry.state.error !== null)
|
|
232
|
+
throw entry.state.error;
|
|
233
|
+
return entry.state.data;
|
|
234
|
+
}
|
|
227
235
|
refresh(entry) {
|
|
228
236
|
const observer = entry.observers.values().next().value;
|
|
229
237
|
if (observer)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Shared guarded-reader contracts and refusal identity; native filesystem effects remain in platform. */
|
|
2
|
+
/** Refusal of a malformed, denied, escaped, or over-limit file operation. */
|
|
3
|
+
export declare class FsFilePathError extends Error {
|
|
4
|
+
constructor({ message }: {
|
|
5
|
+
message: string;
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export interface DenyRules {
|
|
9
|
+
segments: ReadonlySet<string>;
|
|
10
|
+
filenamePatterns: readonly RegExp[];
|
|
11
|
+
}
|
|
12
|
+
export interface GuardedReaderLimits {
|
|
13
|
+
maxFileBytes: number;
|
|
14
|
+
binarySniffBytes: number;
|
|
15
|
+
maxListedFiles: number;
|
|
16
|
+
maxWalkDepth: number;
|
|
17
|
+
maxWalkEntries: number;
|
|
18
|
+
}
|
|
19
|
+
export interface GuardedFileReader {
|
|
20
|
+
resolve(required: {
|
|
21
|
+
relativePath: string;
|
|
22
|
+
}): string;
|
|
23
|
+
read(required: {
|
|
24
|
+
relativePath: string;
|
|
25
|
+
}): {
|
|
26
|
+
content: string;
|
|
27
|
+
bytes: number;
|
|
28
|
+
};
|
|
29
|
+
readBytes(required: {
|
|
30
|
+
relativePath: string;
|
|
31
|
+
maxBytes: number;
|
|
32
|
+
}): Promise<Uint8Array>;
|
|
33
|
+
list(required: Record<string, never>, optional?: {
|
|
34
|
+
relativePath?: string;
|
|
35
|
+
}): {
|
|
36
|
+
files: string[];
|
|
37
|
+
truncated: boolean;
|
|
38
|
+
};
|
|
39
|
+
isDeniedFileName(required: {
|
|
40
|
+
fileName: string;
|
|
41
|
+
}): boolean;
|
|
42
|
+
isDeniedPathSegment(required: {
|
|
43
|
+
segmentName: string;
|
|
44
|
+
}): boolean;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=filesystem.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Shared guarded-reader contracts and refusal identity; native filesystem effects remain in platform. */
|
|
2
|
+
// A distinct refusal class lets tool adapters treat rejected paths/content as actionable input
|
|
3
|
+
// problems instead of redacting them as an internal crash.
|
|
4
|
+
/** Refusal of a malformed, denied, escaped, or over-limit file operation. */
|
|
5
|
+
export class FsFilePathError extends Error {
|
|
6
|
+
constructor({ message }) { super(message); this.name = "FsFilePathError"; }
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=filesystem.js.map
|
|
@@ -8,6 +8,7 @@ export class WorkspaceMismatchError extends Error {
|
|
|
8
8
|
// API key acts for a delegating user. Reject mismatches before a caller persists the row.
|
|
9
9
|
// This helper performs no I/O or foreign-key checks: dependent domains own read-time orphan
|
|
10
10
|
// tolerance and reconciliation, rather than making identity population depend on storage.
|
|
11
|
+
// A delegator is optional, but its composite reference must be complete when supplied.
|
|
11
12
|
export function appendActorReference(required, optional = {}) {
|
|
12
13
|
const { referencingRowWorkspaceId, actorWorkspaceId, actorId } = required;
|
|
13
14
|
const { delegatedByWorkspaceId, delegatedById } = optional;
|
|
@@ -16,6 +17,11 @@ export function appendActorReference(required, optional = {}) {
|
|
|
16
17
|
message: `actor workspace '${actorWorkspaceId}' does not match referencing row workspace '${referencingRowWorkspaceId}'`
|
|
17
18
|
});
|
|
18
19
|
}
|
|
20
|
+
if ((delegatedByWorkspaceId == null) !== (delegatedById == null)) {
|
|
21
|
+
throw new WorkspaceMismatchError({
|
|
22
|
+
message: 'delegator workspace and id must be provided together'
|
|
23
|
+
});
|
|
24
|
+
}
|
|
19
25
|
if (delegatedByWorkspaceId != null && delegatedByWorkspaceId !== referencingRowWorkspaceId) {
|
|
20
26
|
throw new WorkspaceMismatchError({
|
|
21
27
|
message: `delegator workspace '${delegatedByWorkspaceId}' does not match referencing row workspace '${referencingRowWorkspaceId}'`
|
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,4 +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
|
+
}
|
|
54
|
+
/**
|
|
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.
|
|
61
|
+
* @example canonicalJson({ value: { b: 1, a: 2 } }); // '{"a":2,"b":1}'
|
|
62
|
+
*/
|
|
63
|
+
export declare function canonicalJson(required: {
|
|
64
|
+
readonly value: unknown;
|
|
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>;
|
|
48
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,4 +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
|
+
}
|
|
130
|
+
/**
|
|
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.
|
|
137
|
+
* @example canonicalJson({ value: { b: 1, a: 2 } }); // '{"a":2,"b":1}'
|
|
138
|
+
*/
|
|
139
|
+
export function canonicalJson(required, _optional = {}) {
|
|
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('');
|
|
154
|
+
}
|
|
46
155
|
//# sourceMappingURL=records.js.map
|
package/dist/redact.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare const AGENT_FAILURE_SECRET_SHAPE_RE: RegExp;
|
|
|
44
44
|
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|
|
45
45
|
* of the caller-supplied path (never a substring — a directory named `not-secrets-actually` is not
|
|
46
46
|
* a match). `secrets` is the owner's explicit example: whatever a site or app tree names its
|
|
47
|
-
* credentials folder (`sites/**\/secrets/`, `
|
|
47
|
+
* credentials folder (`sites/**\/secrets/`, `app/**\/secrets/`, or anywhere else), it never
|
|
48
48
|
* becomes reachable through this domain. A denied segment is refused by the guarded reader
|
|
49
49
|
* AND skipped entirely by the `fs_list_files` walk — its contents are never even enumerated,
|
|
50
50
|
* not merely refused once named. Besides `secrets`, the set names the
|
|
@@ -76,8 +76,10 @@ export interface SecretRedactionOptions {
|
|
|
76
76
|
redactPii?: boolean;
|
|
77
77
|
/** Exact trusted registration IDs; credential patterns always take precedence. */
|
|
78
78
|
diagnosticToolIds?: readonly string[];
|
|
79
|
-
/** Conservative by default; aggressive
|
|
80
|
-
policy?: 'conservative' | 'aggressive';
|
|
79
|
+
/** Conservative by default; aggressive masks opaque runs; exact-only replaces only supplied secrets. */
|
|
80
|
+
policy?: 'conservative' | 'aggressive' | 'exact-only';
|
|
81
|
+
/** Literal replacement under exact-only. Defaults to '[REDACTED:exact_secret]'; ignored otherwise. */
|
|
82
|
+
marker?: string;
|
|
81
83
|
exactSecrets?: ReadonlyArray<string | undefined | null>;
|
|
82
84
|
extraPatterns?: readonly SecretRedactionPattern[];
|
|
83
85
|
}
|
|
@@ -86,10 +88,11 @@ export declare const OPAQUE_TOKEN_RE: RegExp;
|
|
|
86
88
|
export declare const NON_SECRET_TOKEN_RE: RegExp;
|
|
87
89
|
/**
|
|
88
90
|
* Returns `input` with every recognised secret / PII pattern replaced by a
|
|
89
|
-
* `[REDACTED:<kind>]` marker.
|
|
91
|
+
* `[REDACTED:<kind>]` marker. Under the default policies, idempotent — re-running on already redacted
|
|
90
92
|
* text only matches new tokens. Defaults to conservative; `{ policy: 'aggressive' }`
|
|
91
93
|
* additionally masks bare 20+ character opaque runs. Exact secrets and extra patterns
|
|
92
|
-
* are honored under either policy.
|
|
94
|
+
* are honored under either heuristic policy. Exact-only replaces only supplied secrets in one pass,
|
|
95
|
+
* preserves unmatched characters, and inserts the optional literal marker without reprocessing it.
|
|
93
96
|
*
|
|
94
97
|
* Empty / non-string input passes through unchanged so the caller can use
|
|
95
98
|
* this as a no-op on optional fields.
|
package/dist/redact.js
CHANGED
|
@@ -58,7 +58,7 @@ export const AGENT_FAILURE_SECRET_SHAPE_RE = /\b(?:AIza[\w-]+|AQ\.[\w.\/-]+|sk-[
|
|
|
58
58
|
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|
|
59
59
|
* of the caller-supplied path (never a substring — a directory named `not-secrets-actually` is not
|
|
60
60
|
* a match). `secrets` is the owner's explicit example: whatever a site or app tree names its
|
|
61
|
-
* credentials folder (`sites/**\/secrets/`, `
|
|
61
|
+
* credentials folder (`sites/**\/secrets/`, `app/**\/secrets/`, or anywhere else), it never
|
|
62
62
|
* becomes reachable through this domain. A denied segment is refused by the guarded reader
|
|
63
63
|
* AND skipped entirely by the `fs_list_files` walk — its contents are never even enumerated,
|
|
64
64
|
* not merely refused once named. Besides `secrets`, the set names the
|
|
@@ -125,8 +125,8 @@ const PATTERNS = [
|
|
|
125
125
|
// the value, not the literal 'Bearer ', so the marker stays readable in
|
|
126
126
|
// the redacted output ("Authorization: Bearer [REDACTED:bearer_token]").
|
|
127
127
|
{ name: 'bearer_token', regex: /(?<=\bBearer\s+)[A-Za-z0-9._~+/-]{16,}={0,2}/g },
|
|
128
|
-
//
|
|
129
|
-
{ name: 'email', regex: /\b[\w.+-]+@[\w-]+\.[\
|
|
128
|
+
// Alphabetic TLDs preserve versioned package paths in error stacks (e.g. pkg@1.2.3).
|
|
129
|
+
{ name: 'email', regex: /\b[\w.+-]+@(?:[\w-]+\.)+[A-Za-z]{2,}\b/g },
|
|
130
130
|
// IPv4. Reject all-zero / 255.255.255.255-ish junk shapes by gating on
|
|
131
131
|
// each octet being 0-255.
|
|
132
132
|
{
|
|
@@ -215,10 +215,11 @@ function isMaskedValue(value) {
|
|
|
215
215
|
}
|
|
216
216
|
/**
|
|
217
217
|
* Returns `input` with every recognised secret / PII pattern replaced by a
|
|
218
|
-
* `[REDACTED:<kind>]` marker.
|
|
218
|
+
* `[REDACTED:<kind>]` marker. Under the default policies, idempotent — re-running on already redacted
|
|
219
219
|
* text only matches new tokens. Defaults to conservative; `{ policy: 'aggressive' }`
|
|
220
220
|
* additionally masks bare 20+ character opaque runs. Exact secrets and extra patterns
|
|
221
|
-
* are honored under either policy.
|
|
221
|
+
* are honored under either heuristic policy. Exact-only replaces only supplied secrets in one pass,
|
|
222
|
+
* preserves unmatched characters, and inserts the optional literal marker without reprocessing it.
|
|
222
223
|
*
|
|
223
224
|
* Empty / non-string input passes through unchanged so the caller can use
|
|
224
225
|
* this as a no-op on optional fields.
|
|
@@ -241,8 +242,16 @@ export function redactSecretsWithCounts({ input }, options = {}) {
|
|
|
241
242
|
return { redacted: input, counts };
|
|
242
243
|
const marker = (name) => {
|
|
243
244
|
counts[name] = (counts[name] ?? 0) + 1;
|
|
245
|
+
if (options.policy === 'exact-only')
|
|
246
|
+
return options.marker ?? '[REDACTED:exact_secret]';
|
|
244
247
|
return `[REDACTED:${name}]`;
|
|
245
248
|
};
|
|
249
|
+
if (options.policy === 'exact-only') {
|
|
250
|
+
return {
|
|
251
|
+
redacted: redactExactSecrets({ text: input, marker, secrets: options.exactSecrets ?? [] }, { singlePass: true }),
|
|
252
|
+
counts,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
246
255
|
let out = redactExactSecrets({ text: input, marker, secrets: options.exactSecrets ?? [] });
|
|
247
256
|
out = redactUrlCredentials({ text: out, marker });
|
|
248
257
|
out = applyPatterns({ text: out, marker, patterns: credentialPatterns(options.extraPatterns ?? []) });
|
|
@@ -279,16 +288,63 @@ function applyPatterns({ text, marker, patterns }) {
|
|
|
279
288
|
}
|
|
280
289
|
return out;
|
|
281
290
|
}
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
// Exact secrets match as substrings at any length (a secret glued into other text stays masked),
|
|
292
|
+
// but never where they overlap a span the later rules depend on: a credential label in label
|
|
293
|
+
// position, a secret-named URL parameter, a URL scheme, or a complete marker. As plain substrings
|
|
294
|
+
// they split labels before the label rules ran (`a` inside `password=hunter2` broke the label
|
|
295
|
+
// and left `hunter2` visible; `api_key` swallowed the whole `api_key=` label), and they split
|
|
296
|
+
// earlier markers (`a` inside `exact_secret`, `email` inside `[REDACTED:email]`).
|
|
297
|
+
const PROTECTED_SPAN_RE = new RegExp([
|
|
298
|
+
`\\b(?:${SECRET_LABELS})\\b\\s*[:=]`,
|
|
299
|
+
'\\bbearer\\s+',
|
|
300
|
+
URL_SECRET_PARAM_RE.source.slice(0, URL_SECRET_PARAM_RE.source.lastIndexOf('=') + 1),
|
|
301
|
+
URL_CREDENTIALS_RE.source.slice(0, URL_CREDENTIALS_RE.source.indexOf(')') + 1),
|
|
302
|
+
'\\[REDACTED:[^\\]]*\\]',
|
|
303
|
+
'\\[redacted\\]',
|
|
304
|
+
].join('|'), 'gi');
|
|
305
|
+
/** Masks caller-supplied secrets verbatim, longest first, ignoring empty and non-string entries;
|
|
306
|
+
* an occurrence overlapping a protected span is left for the label rules. */
|
|
307
|
+
function redactExactSecrets({ text, marker, secrets }, options = {}) {
|
|
284
308
|
const usable = [...new Set(secrets.filter((secret) => typeof secret === 'string' && secret.length > 0))].sort((a, b) => b.length - a.length);
|
|
309
|
+
if (options.singlePass) {
|
|
310
|
+
// Match the original text once: a marker can contain a supplied secret or '$' literally.
|
|
311
|
+
let out = '';
|
|
312
|
+
let cursor = 0;
|
|
313
|
+
for (const [start, end] of mergedSecretRanges({ text, secrets: usable })) {
|
|
314
|
+
out += text.slice(cursor, start) + marker('exact_secret');
|
|
315
|
+
cursor = end;
|
|
316
|
+
}
|
|
317
|
+
return out + text.slice(cursor);
|
|
318
|
+
}
|
|
285
319
|
let out = text;
|
|
286
320
|
for (const secret of usable) {
|
|
287
321
|
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
288
|
-
|
|
322
|
+
// Recomputed per secret: a longer secret's marker is protected from the shorter ones.
|
|
323
|
+
const spans = [...out.matchAll(PROTECTED_SPAN_RE)].map(({ index, 0: span }) => ({ start: index, end: index + span.length }));
|
|
324
|
+
out = out.replace(new RegExp(escaped, 'g'), (match, offset) => spans.some(({ start, end }) => offset < end && offset + match.length > start) ? match : marker('exact_secret'));
|
|
289
325
|
}
|
|
290
326
|
return out;
|
|
291
327
|
}
|
|
328
|
+
/** Every occurrence of every secret, overlapping ranges merged: with 'abc' and 'bcdef', a left-to-right
|
|
329
|
+
* alternation would replace 'abc' and leave 'def' — part of the other secret — in the output.
|
|
330
|
+
* @complexity O(s * n) indexOf scans for s secrets over n characters, plus O(r log r) for r matches. */
|
|
331
|
+
function mergedSecretRanges({ text, secrets }) {
|
|
332
|
+
const ranges = [];
|
|
333
|
+
for (const secret of secrets) {
|
|
334
|
+
for (let at = text.indexOf(secret); at !== -1; at = text.indexOf(secret, at + 1))
|
|
335
|
+
ranges.push([at, at + secret.length]);
|
|
336
|
+
}
|
|
337
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
338
|
+
const merged = [];
|
|
339
|
+
for (const [start, end] of ranges) {
|
|
340
|
+
const last = merged.at(-1);
|
|
341
|
+
if (last && start < last[1])
|
|
342
|
+
last[1] = Math.max(last[1], end);
|
|
343
|
+
else
|
|
344
|
+
merged.push([start, end]);
|
|
345
|
+
}
|
|
346
|
+
return merged;
|
|
347
|
+
}
|
|
292
348
|
/** URL userinfo and signed query values: credentials recognized by their URL context. */
|
|
293
349
|
function redactUrlCredentials({ text, marker }) {
|
|
294
350
|
return text
|
|
@@ -14,28 +14,6 @@
|
|
|
14
14
|
* IS the key" — `translateSettingsDialog` falls back to the raw key for English and any
|
|
15
15
|
* unrecognized locale.
|
|
16
16
|
*/
|
|
17
|
-
import { SETTINGS_DIALOG_ES } from './settings-dialog.es.js';
|
|
18
|
-
import { SETTINGS_DIALOG_ID } from './settings-dialog.id.js';
|
|
19
|
-
import { SETTINGS_DIALOG_DE } from './settings-dialog.de.js';
|
|
20
|
-
import { SETTINGS_DIALOG_ZH_CN } from './settings-dialog.zh-CN.js';
|
|
21
|
-
import { SETTINGS_DIALOG_ZH_TW } from './settings-dialog.zh-TW.js';
|
|
22
|
-
import { SETTINGS_DIALOG_PT_BR } from './settings-dialog.pt-BR.js';
|
|
23
|
-
import { SETTINGS_DIALOG_RU } from './settings-dialog.ru.js';
|
|
24
|
-
import { SETTINGS_DIALOG_FA } from './settings-dialog.fa.js';
|
|
25
|
-
import { SETTINGS_DIALOG_AR } from './settings-dialog.ar.js';
|
|
26
|
-
import { SETTINGS_DIALOG_JA } from './settings-dialog.ja.js';
|
|
27
|
-
import { SETTINGS_DIALOG_KO } from './settings-dialog.ko.js';
|
|
28
|
-
import { SETTINGS_DIALOG_PL } from './settings-dialog.pl.js';
|
|
29
|
-
import { SETTINGS_DIALOG_HU } from './settings-dialog.hu.js';
|
|
30
|
-
import { SETTINGS_DIALOG_FR } from './settings-dialog.fr.js';
|
|
31
|
-
import { SETTINGS_DIALOG_UK } from './settings-dialog.uk.js';
|
|
32
|
-
import { SETTINGS_DIALOG_TR } from './settings-dialog.tr.js';
|
|
33
|
-
import { SETTINGS_DIALOG_TH } from './settings-dialog.th.js';
|
|
34
|
-
import { SETTINGS_DIALOG_IT } from './settings-dialog.it.js';
|
|
35
|
-
import { SETTINGS_DIALOG_HI } from './settings-dialog.hi.js';
|
|
36
|
-
import { SETTINGS_DIALOG_UR } from './settings-dialog.ur.js';
|
|
37
|
-
import { SETTINGS_DIALOG_BN } from './settings-dialog.bn.js';
|
|
38
|
-
export { SETTINGS_DIALOG_ES, SETTINGS_DIALOG_ID, SETTINGS_DIALOG_DE, SETTINGS_DIALOG_ZH_CN, SETTINGS_DIALOG_ZH_TW, SETTINGS_DIALOG_PT_BR, SETTINGS_DIALOG_RU, SETTINGS_DIALOG_FA, SETTINGS_DIALOG_AR, SETTINGS_DIALOG_JA, SETTINGS_DIALOG_KO, SETTINGS_DIALOG_PL, SETTINGS_DIALOG_HU, SETTINGS_DIALOG_FR, SETTINGS_DIALOG_UK, SETTINGS_DIALOG_TR, SETTINGS_DIALOG_TH, SETTINGS_DIALOG_IT, SETTINGS_DIALOG_HI, SETTINGS_DIALOG_UR, SETTINGS_DIALOG_BN, };
|
|
39
17
|
/** Every non-English locale this dictionary covers, keyed the same way `@jini-ai/ui`'s
|
|
40
18
|
* `SETTINGS_DIALOG_DICTIONARIES` is. */
|
|
41
19
|
export declare const SETTINGS_DIALOG_DICTIONARIES: Readonly<Record<string, Record<string, string>>>;
|
|
@@ -35,7 +35,6 @@ import { SETTINGS_DIALOG_IT } from './settings-dialog.it.js';
|
|
|
35
35
|
import { SETTINGS_DIALOG_HI } from './settings-dialog.hi.js';
|
|
36
36
|
import { SETTINGS_DIALOG_UR } from './settings-dialog.ur.js';
|
|
37
37
|
import { SETTINGS_DIALOG_BN } from './settings-dialog.bn.js';
|
|
38
|
-
export { SETTINGS_DIALOG_ES, SETTINGS_DIALOG_ID, SETTINGS_DIALOG_DE, SETTINGS_DIALOG_ZH_CN, SETTINGS_DIALOG_ZH_TW, SETTINGS_DIALOG_PT_BR, SETTINGS_DIALOG_RU, SETTINGS_DIALOG_FA, SETTINGS_DIALOG_AR, SETTINGS_DIALOG_JA, SETTINGS_DIALOG_KO, SETTINGS_DIALOG_PL, SETTINGS_DIALOG_HU, SETTINGS_DIALOG_FR, SETTINGS_DIALOG_UK, SETTINGS_DIALOG_TR, SETTINGS_DIALOG_TH, SETTINGS_DIALOG_IT, SETTINGS_DIALOG_HI, SETTINGS_DIALOG_UR, SETTINGS_DIALOG_BN, };
|
|
39
38
|
/** Every non-English locale this dictionary covers, keyed the same way `@jini-ai/ui`'s
|
|
40
39
|
* `SETTINGS_DIALOG_DICTIONARIES` is. */
|
|
41
40
|
export const SETTINGS_DIALOG_DICTIONARIES = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SecretRedactionOptions } from "../redact.js";
|
|
1
2
|
/**
|
|
2
3
|
* Strip ANSI/terminal escape sequences (CSI `ESC [ ... final-byte`, OSC `ESC ] ... BEL-or-ST`,
|
|
3
4
|
* and bare ESC-prefixed bytes) plus other C0/C1 control characters from `text`. Written as an
|
|
@@ -8,6 +9,10 @@ export declare function stripControlSequences({ text }: {
|
|
|
8
9
|
text: string;
|
|
9
10
|
}): string;
|
|
10
11
|
export interface SanitizeTextOptions {
|
|
12
|
+
/** Model-facing error paths keep slugs/tool IDs that would otherwise read as opaque tokens. Default false. */
|
|
13
|
+
keepIdentifiers?: boolean;
|
|
14
|
+
/** Known credentials forwarded to core's exact-secret redactor. */
|
|
15
|
+
exactSecrets?: SecretRedactionOptions['exactSecrets'];
|
|
11
16
|
/** Exact IDs from trusted registration metadata, never inferred from error text or arguments.
|
|
12
17
|
* Credential rules still run first; prefixes and glued suffixes are never exempted. */
|
|
13
18
|
diagnosticToolIds?: readonly string[];
|
|
@@ -19,7 +24,9 @@ export interface SanitizeTextOptions {
|
|
|
19
24
|
* length of `text` — the one function every boundary that surfaces daemon/network-derived text
|
|
20
25
|
* to stderr/stdout should route through first.
|
|
21
26
|
* Trusted diagnosticToolIds exempt complete IDs (including dots) only, after credential masking.
|
|
22
|
-
*
|
|
27
|
+
* keepIdentifiers exempts only identifier-shaped opaque runs; every other opaque run stays masked.
|
|
28
|
+
* @complexity O(n + t) local time/space for n text bytes and t trusted ID bytes,
|
|
29
|
+
* plus core's exact-secret replacements when supplied.
|
|
23
30
|
*/
|
|
24
31
|
export declare function sanitizeUntrustedText({ text }: {
|
|
25
32
|
text: string;
|
|
@@ -97,26 +97,41 @@ const DIAGNOSTIC_TOKENS = new Set([
|
|
|
97
97
|
'minContains', 'maxContains', 'patternProperties', 'propertyNames', 'prefixItems',
|
|
98
98
|
'contentEncoding', 'contentMediaType', 'contentSchema',
|
|
99
99
|
]);
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
/** Short lowercase slugs with at least one letter-only segment and two separators.
|
|
101
|
+
* Bound both segments and total length so long passphrases do not become diagnostic exemptions.
|
|
102
|
+
* UUIDs, numeric-only runs, mixed case and letter+digit segments never qualify.
|
|
103
|
+
* @complexity O(n) in token length; pure. */
|
|
104
|
+
function isIdentifierShaped({ token }) {
|
|
105
|
+
return token.length <= 31 && /[a-z]/.test(token)
|
|
106
|
+
&& /^(?:[a-z]{1,15}|[0-9]{1,15})(?:[-_](?:[a-z]{1,15}|[0-9]{1,15})){2,}$/.test(token);
|
|
107
|
+
}
|
|
108
|
+
/** Mask credentials before preserving trusted IDs and UPPER_SNAKE diagnostics; redact opaque spans elsewhere.
|
|
109
|
+
* `keepIdentifiers` additionally exempts identifier-shaped runs (see {@link isIdentifierShaped}).
|
|
110
|
+
* @complexity Local scans of text plus core's exact-secret replacements; no I/O. */
|
|
111
|
+
function redactSecretLike({ text }, options = {}) {
|
|
112
|
+
const diagnosticToolIds = new Set(options.diagnosticToolIds);
|
|
113
|
+
const keepOpaque = (match) => NON_SECRET_TOKEN_RE.test(match) || DIAGNOSTIC_TOKENS.has(match)
|
|
114
|
+
|| (options.keepIdentifiers === true && isIdentifierShaped({ token: match }));
|
|
115
|
+
return redactSecrets({ input: text }, { redactPii: false, exactSecrets: options.exactSecrets ?? [] })
|
|
103
116
|
.replace(LABELED_SECRET_RE, match => match.replace(/([:=]\s*)\S+$/, '$1[redacted]'))
|
|
104
117
|
.replace(/\[REDACTED:[^\]\r\n]+\]/g, '[redacted]')
|
|
105
118
|
// Trust whole dotted IDs so their components cannot become a general redaction bypass.
|
|
106
119
|
.replace(/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g, token => diagnosticToolIds.has(token) ? token
|
|
107
|
-
: token.replace(OPAQUE_TOKEN_RE, match =>
|
|
120
|
+
: token.replace(OPAQUE_TOKEN_RE, match => keepOpaque(match) ? match : '[redacted]'));
|
|
108
121
|
}
|
|
109
122
|
/**
|
|
110
123
|
* Strip terminal control sequences, redact anything that looks like a secret, and cap the
|
|
111
124
|
* length of `text` — the one function every boundary that surfaces daemon/network-derived text
|
|
112
125
|
* to stderr/stdout should route through first.
|
|
113
126
|
* Trusted diagnosticToolIds exempt complete IDs (including dots) only, after credential masking.
|
|
114
|
-
*
|
|
127
|
+
* keepIdentifiers exempts only identifier-shaped opaque runs; every other opaque run stays masked.
|
|
128
|
+
* @complexity O(n + t) local time/space for n text bytes and t trusted ID bytes,
|
|
129
|
+
* plus core's exact-secret replacements when supplied.
|
|
115
130
|
*/
|
|
116
131
|
export function sanitizeUntrustedText({ text }, options = {}) {
|
|
117
132
|
const requestedLength = options.maxLength ?? DEFAULT_MAX_EXCERPT_LENGTH;
|
|
118
133
|
const maxLength = Number.isFinite(requestedLength) ? Math.max(0, Math.floor(requestedLength)) : DEFAULT_MAX_EXCERPT_LENGTH;
|
|
119
|
-
const sanitized = redactSecretLike({ text: stripControlSequences({ text }),
|
|
134
|
+
const sanitized = redactSecretLike({ text: stripControlSequences({ text }) }, options);
|
|
120
135
|
if (sanitized.length <= maxLength)
|
|
121
136
|
return sanitized;
|
|
122
137
|
// The marker is part of the output budget, including when the budget cannot fit the full marker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15-rc.2",
|
|
4
4
|
"description": "The Jini kernel: typed DI tokens, pack composition and lifecycle, the tool registry, redaction, and origin/API-token auth primitives. Zero dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"./text": "universal",
|
|
28
28
|
"./settings": "universal",
|
|
29
29
|
"./settings/express": "node",
|
|
30
|
-
"./plugin-directory": "universal"
|
|
30
|
+
"./plugin-directory": "universal",
|
|
31
|
+
"./filesystem": "universal"
|
|
31
32
|
},
|
|
32
33
|
"isolation": {
|
|
33
34
|
"entries": {
|
|
@@ -155,6 +156,11 @@
|
|
|
155
156
|
"types": "./plugin-directory.d.ts",
|
|
156
157
|
"import": "./plugin-directory.js",
|
|
157
158
|
"default": "./plugin-directory.js"
|
|
159
|
+
},
|
|
160
|
+
"./filesystem": {
|
|
161
|
+
"types": "./dist/filesystem.d.ts",
|
|
162
|
+
"import": "./dist/filesystem.js",
|
|
163
|
+
"default": "./dist/filesystem.js"
|
|
158
164
|
}
|
|
159
165
|
},
|
|
160
166
|
"files": [
|