@jini-ai/core 0.6.21 → 0.6.23
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 +10 -0
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +20 -6
- package/dist/gated-mutations/gateway.d.ts +1 -0
- package/dist/gated-mutations/gateway.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/redact.d.ts +32 -5
- package/dist/redact.js +575 -165
- package/dist/teardown.d.ts +30 -0
- package/dist/teardown.js +43 -0
- package/dist/tool-input-envelope.d.ts +20 -0
- package/dist/tool-input-envelope.js +40 -0
- package/dist/tool-input-normalization.js +2 -0
- package/dist/user-facing-error.d.ts +34 -0
- package/dist/user-facing-error.js +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A cleanup step may be synchronous or asynchronous; failures are reported after all steps run. */
|
|
2
|
+
export type TeardownStep = () => unknown;
|
|
3
|
+
/** Injected lifecycle registration; core has no dependency on a particular test runner or host. */
|
|
4
|
+
export interface TeardownHost {
|
|
5
|
+
after(fn: () => unknown): void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Attempt every step in reverse registration order. Preserve a single failure's identity;
|
|
9
|
+
* report several as an AggregateError only after attempting the remaining cleanup.
|
|
10
|
+
* @param requiredArgs Steps owned by one lifecycle context; the input array is not mutated.
|
|
11
|
+
* @returns Resolves after cleanup, or rejects with the collected failure(s).
|
|
12
|
+
* @complexity O(n) time and space for n registered steps, excluding each step's own work.
|
|
13
|
+
* @example await runTeardownSteps({ steps: [closeDatabase, closeServer] });
|
|
14
|
+
*/
|
|
15
|
+
export declare function runTeardownSteps({ steps }: {
|
|
16
|
+
steps: readonly TeardownStep[];
|
|
17
|
+
}, _options?: {}): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Queue a step on the host's single cleanup hook. Hosts that stop at the first throwing hook
|
|
20
|
+
* still attempt every registered resource cleanup before this hook reports a failure.
|
|
21
|
+
* @param requiredArgs Lifecycle host and a step to run when that host finishes.
|
|
22
|
+
* @returns Nothing; registers one hook on first use, then appends to that host's queue.
|
|
23
|
+
* @complexity O(1) registration; cleanup is O(n) time/space for n registered steps.
|
|
24
|
+
* @example onTeardown({ t: context, step: closeServer });
|
|
25
|
+
*/
|
|
26
|
+
export declare function onTeardown({ t, step }: {
|
|
27
|
+
t: TeardownHost;
|
|
28
|
+
step: TeardownStep;
|
|
29
|
+
}, _options?: {}): void;
|
|
30
|
+
//# sourceMappingURL=teardown.d.ts.map
|
package/dist/teardown.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const queues = new WeakMap();
|
|
2
|
+
/**
|
|
3
|
+
* Attempt every step in reverse registration order. Preserve a single failure's identity;
|
|
4
|
+
* report several as an AggregateError only after attempting the remaining cleanup.
|
|
5
|
+
* @param requiredArgs Steps owned by one lifecycle context; the input array is not mutated.
|
|
6
|
+
* @returns Resolves after cleanup, or rejects with the collected failure(s).
|
|
7
|
+
* @complexity O(n) time and space for n registered steps, excluding each step's own work.
|
|
8
|
+
* @example await runTeardownSteps({ steps: [closeDatabase, closeServer] });
|
|
9
|
+
*/
|
|
10
|
+
export async function runTeardownSteps({ steps }, _options = {}) {
|
|
11
|
+
const failures = [];
|
|
12
|
+
for (const step of [...steps].reverse()) {
|
|
13
|
+
try {
|
|
14
|
+
await step();
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
failures.push(error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (failures.length === 1)
|
|
21
|
+
throw failures[0];
|
|
22
|
+
if (failures.length > 1)
|
|
23
|
+
throw new AggregateError(failures, "test teardown failed", { cause: failures[0] });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Queue a step on the host's single cleanup hook. Hosts that stop at the first throwing hook
|
|
27
|
+
* still attempt every registered resource cleanup before this hook reports a failure.
|
|
28
|
+
* @param requiredArgs Lifecycle host and a step to run when that host finishes.
|
|
29
|
+
* @returns Nothing; registers one hook on first use, then appends to that host's queue.
|
|
30
|
+
* @complexity O(1) registration; cleanup is O(n) time/space for n registered steps.
|
|
31
|
+
* @example onTeardown({ t: context, step: closeServer });
|
|
32
|
+
*/
|
|
33
|
+
export function onTeardown({ t, step }, _options = {}) {
|
|
34
|
+
const queued = queues.get(t);
|
|
35
|
+
if (queued) {
|
|
36
|
+
queued.push(step);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const steps = [step];
|
|
40
|
+
queues.set(t, steps);
|
|
41
|
+
t.after(() => runTeardownSteps({ steps }));
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=teardown.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Decode a transport's object-valued field without accepting arrays, scalars or invalid JSON.
|
|
2
|
+
* @returns A JSON object; never includes supplied values in a refusal.
|
|
3
|
+
* @throws ToolInputError when a string does not encode an object.
|
|
4
|
+
* @complexity O(n) time/space for n JSON characters; no I/O or mutation.
|
|
5
|
+
* @example decodeToolInputObject({ input: '{}' }); // {}
|
|
6
|
+
*/
|
|
7
|
+
export declare function decodeToolInputObject({ input }: {
|
|
8
|
+
input: unknown;
|
|
9
|
+
}, _options?: Record<string, never>): unknown;
|
|
10
|
+
/** Remove one delegated envelope only when the tool contract cannot own an `input` field.
|
|
11
|
+
* Mixed fields and schemas without an explicit object contract retain ordinary validation.
|
|
12
|
+
* @returns The original value or decoded inner object; never mutates caller arguments.
|
|
13
|
+
* @complexity O(k + n) time/space for k keys and n encoded input characters; no I/O.
|
|
14
|
+
* @example unwrapToolInputEnvelope({ input: { input: '{}' }, schema: { type: 'object', properties: {} } });
|
|
15
|
+
*/
|
|
16
|
+
export declare function unwrapToolInputEnvelope({ input, schema }: {
|
|
17
|
+
input: unknown;
|
|
18
|
+
schema: unknown;
|
|
19
|
+
}, _options?: Record<string, never>): unknown;
|
|
20
|
+
//# sourceMappingURL=tool-input-envelope.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ToolInputError } from './tool-input-errors.js';
|
|
2
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
/** Decode a transport's object-valued field without accepting arrays, scalars or invalid JSON.
|
|
4
|
+
* @returns A JSON object; never includes supplied values in a refusal.
|
|
5
|
+
* @throws ToolInputError when a string does not encode an object.
|
|
6
|
+
* @complexity O(n) time/space for n JSON characters; no I/O or mutation.
|
|
7
|
+
* @example decodeToolInputObject({ input: '{}' }); // {}
|
|
8
|
+
*/
|
|
9
|
+
export function decodeToolInputObject({ input }, _options = {}) {
|
|
10
|
+
if (typeof input !== 'string')
|
|
11
|
+
return input;
|
|
12
|
+
let decoded;
|
|
13
|
+
try {
|
|
14
|
+
decoded = JSON.parse(input);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new ToolInputError({ message: 'The delegated input is not valid JSON. Send one JSON object containing the tool fields.' });
|
|
18
|
+
}
|
|
19
|
+
if (!record(decoded))
|
|
20
|
+
throw new ToolInputError({ message: 'The delegated input must be a JSON object. Send the tool fields inside an object.' });
|
|
21
|
+
return decoded;
|
|
22
|
+
}
|
|
23
|
+
/** Remove one delegated envelope only when the tool contract cannot own an `input` field.
|
|
24
|
+
* Mixed fields and schemas without an explicit object contract retain ordinary validation.
|
|
25
|
+
* @returns The original value or decoded inner object; never mutates caller arguments.
|
|
26
|
+
* @complexity O(k + n) time/space for k keys and n encoded input characters; no I/O.
|
|
27
|
+
* @example unwrapToolInputEnvelope({ input: { input: '{}' }, schema: { type: 'object', properties: {} } });
|
|
28
|
+
*/
|
|
29
|
+
export function unwrapToolInputEnvelope({ input, schema }, _options = {}) {
|
|
30
|
+
if (!record(input) || !record(schema) || schema.type !== 'object')
|
|
31
|
+
return input;
|
|
32
|
+
const properties = schema.properties ?? {};
|
|
33
|
+
if (!record(properties) || Object.hasOwn(properties, 'input'))
|
|
34
|
+
return input;
|
|
35
|
+
if (Object.keys(input).length !== 1 || !Object.hasOwn(input, 'input'))
|
|
36
|
+
return input;
|
|
37
|
+
const decoded = decodeToolInputObject({ input: input.input });
|
|
38
|
+
return record(decoded) ? decoded : input;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=tool-input-envelope.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isNonArrayRecord } from './records.js';
|
|
2
2
|
import { ToolInputError, ToolInputNormalizationError } from './tool-input-errors.js';
|
|
3
|
+
import { unwrapToolInputEnvelope } from './tool-input-envelope.js';
|
|
3
4
|
export { ToolInputNormalizationError } from './tool-input-errors.js';
|
|
4
5
|
const object = (value) => isNonArrayRecord({ value });
|
|
5
6
|
const keyShape = (key) => key.replace(/[_-]/g, '').toLowerCase();
|
|
@@ -204,6 +205,7 @@ const isObjectTool = (schema) => object(schema) && schema.type === 'object';
|
|
|
204
205
|
/** First pass (open), the optional domain hook, then the final closed-schema pass. */
|
|
205
206
|
async function normalizeWithinGate({ input, registration }) {
|
|
206
207
|
const schema = registration.invocationSchema ?? registration.descriptor.inputSchema;
|
|
208
|
+
input = unwrapToolInputEnvelope({ input, schema });
|
|
207
209
|
if (isObjectTool(schema) && !object(input))
|
|
208
210
|
throw new ToolInputError({ message: `${registration.descriptor.id}: send one input object containing the tool's fields.` });
|
|
209
211
|
const notes = [];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type SecretRedactionOptions } from './redact.js';
|
|
2
|
+
/** Sanitized explanation and copyable evidence. Missing boundary metadata stays explicitly unavailable. */
|
|
3
|
+
export interface UserFacingError {
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly action: string;
|
|
6
|
+
readonly step: string;
|
|
7
|
+
readonly cause: string;
|
|
8
|
+
readonly errorId?: string;
|
|
9
|
+
readonly appVersion?: string;
|
|
10
|
+
readonly jiniVersion?: string;
|
|
11
|
+
readonly time?: string;
|
|
12
|
+
readonly copyText: string;
|
|
13
|
+
}
|
|
14
|
+
/** Boundary-owned metadata and redaction policy; formatting never reads a clock or generates IDs. */
|
|
15
|
+
export interface UserFacingErrorOptions extends SecretRedactionOptions {
|
|
16
|
+
readonly errorId?: string;
|
|
17
|
+
readonly appVersion?: string;
|
|
18
|
+
readonly jiniVersion?: string;
|
|
19
|
+
readonly time?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Format an approved explanation and an untrusted cause for disclosure and copying.
|
|
23
|
+
* Callers own disclosure policy: redaction does not make private backend facts public-safe.
|
|
24
|
+
* Redacts complete fields before bounding them; uses an existing ERR ID before a boundary fallback.
|
|
25
|
+
* No I/O, logging, clock, randomness or input mutation. Core handles hostile/circular causes.
|
|
26
|
+
* @complexity O(n) diagnostic serialization plus core redaction over all input characters; bounded output.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatUserFacingError(requiredArgs: {
|
|
29
|
+
message: string;
|
|
30
|
+
action: string;
|
|
31
|
+
step: string;
|
|
32
|
+
cause: unknown;
|
|
33
|
+
}, optionalArgs?: UserFacingErrorOptions): UserFacingError;
|
|
34
|
+
//# sourceMappingURL=user-facing-error.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { formatErrorForLog, redactSecrets } from './redact.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format an approved explanation and an untrusted cause for disclosure and copying.
|
|
4
|
+
* Callers own disclosure policy: redaction does not make private backend facts public-safe.
|
|
5
|
+
* Redacts complete fields before bounding them; uses an existing ERR ID before a boundary fallback.
|
|
6
|
+
* No I/O, logging, clock, randomness or input mutation. Core handles hostile/circular causes.
|
|
7
|
+
* @complexity O(n) diagnostic serialization plus core redaction over all input characters; bounded output.
|
|
8
|
+
*/
|
|
9
|
+
export function formatUserFacingError(requiredArgs, optionalArgs = {}) {
|
|
10
|
+
const diagnostic = formatErrorForLog({ error: requiredArgs.cause }, optionalArgs);
|
|
11
|
+
const existingId = diagnostic.match(/\bERR-[A-Fa-f0-9]{4}(?:-[A-Fa-f0-9]{4}){3}\b/)?.[0];
|
|
12
|
+
const metadata = {
|
|
13
|
+
errorId: existingId ?? optionalArgs.errorId,
|
|
14
|
+
appVersion: optionalArgs.appVersion,
|
|
15
|
+
jiniVersion: optionalArgs.jiniVersion,
|
|
16
|
+
time: optionalArgs.time,
|
|
17
|
+
};
|
|
18
|
+
const fields = {
|
|
19
|
+
message: safeField({ text: requiredArgs.message }, optionalArgs),
|
|
20
|
+
action: safeField({ text: requiredArgs.action }, optionalArgs),
|
|
21
|
+
step: safeField({ text: requiredArgs.step }, optionalArgs),
|
|
22
|
+
cause: bound({ text: diagnostic, limit: 4096 }),
|
|
23
|
+
...Object.fromEntries(Object.entries(metadata).filter((entry) => entry[1] !== undefined)
|
|
24
|
+
.map(([key, text]) => [key, safeField({ text }, optionalArgs)])),
|
|
25
|
+
};
|
|
26
|
+
const labels = [
|
|
27
|
+
['Message', fields.message], ['Next action', fields.action], ['Error ID', fields.errorId],
|
|
28
|
+
['Step', fields.step], ['Cause', fields.cause], ['App version', fields.appVersion],
|
|
29
|
+
['Jini version', fields.jiniVersion], ['Time', fields.time],
|
|
30
|
+
];
|
|
31
|
+
// Exact secrets can span a fixed label and a field; scrub the assembled block as well.
|
|
32
|
+
const copyText = bound({ text: redactSecrets({ input: labels.map(([label, value]) => `${label}: ${value ?? 'Unavailable'}`).join('\n') }, optionalArgs), limit: 8192 });
|
|
33
|
+
return { ...fields, copyText };
|
|
34
|
+
}
|
|
35
|
+
/** Redact the complete value so a credential crossing the display bound cannot leak its prefix. */
|
|
36
|
+
function safeField({ text }, options) {
|
|
37
|
+
return bound({ text: redactSecrets({ input: text }, options), limit: 512 });
|
|
38
|
+
}
|
|
39
|
+
function bound({ text, limit }) {
|
|
40
|
+
return text.length > limit ? text.slice(0, limit - 1) + '…' : text;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=user-facing-error.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
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": {
|