@jini-ai/core 0.4.7 → 0.4.9
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 +11 -0
- package/dist/agent-tools.d.ts +1 -1
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +87 -22
- package/dist/batch-tool-inputs.d.ts +147 -0
- package/dist/batch-tool-inputs.js +257 -0
- package/dist/feature-flags.d.ts +21 -0
- package/dist/feature-flags.js +17 -0
- package/dist/fetch/index.d.ts +2 -1
- package/dist/fetch/index.js +1 -0
- package/dist/fetch/loader-read.d.ts +16 -0
- package/dist/fetch/loader-read.js +22 -0
- package/dist/fetch/types.d.ts +12 -2
- package/dist/gated-mutations/gateway.js +32 -27
- package/dist/index.d.ts +9 -1
- package/dist/index.js +8 -1
- package/dist/object-store.d.ts +115 -0
- package/dist/object-store.js +70 -0
- package/dist/origin-validation.js +64 -57
- package/dist/primitives/http.d.ts +2 -2
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +17 -10
- package/dist/primitives/tasks.d.ts +10 -0
- package/dist/primitives/tasks.js +8 -0
- package/dist/provider-catalog.d.ts +76 -0
- package/dist/provider-catalog.js +6 -0
- package/dist/provider-registry.d.ts +144 -0
- package/dist/provider-registry.js +138 -0
- package/dist/records.d.ts +24 -0
- package/dist/records.js +25 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.js +57 -34
- package/dist/registration-kit.d.ts +7 -4
- package/dist/registration-kit.js +81 -62
- package/dist/settings/agent-value-write-tools.d.ts +19 -0
- package/dist/settings/agent-value-write-tools.js +131 -70
- package/dist/settings/ensure-definitions.d.ts +4 -2
- package/dist/settings/ensure-definitions.js +79 -63
- package/dist/settings/express/cms-adapter.d.ts +2 -2
- package/dist/settings/express/cms-adapter.js +34 -22
- package/dist/settings/express/http-request.d.ts +33 -0
- package/dist/settings/express/http-request.js +49 -0
- package/dist/settings/express/http.d.ts +2 -2
- package/dist/settings/express/http.js +16 -39
- package/dist/settings/express/stream.d.ts +2 -2
- package/dist/settings/express/stream.js +49 -27
- package/dist/settings/repo.memory.js +7 -4
- package/dist/settings/settings.js +54 -60
- package/dist/settings/tool-registrations.d.ts +2 -3
- package/dist/settings/tool-registrations.js +64 -39
- package/dist/settings/write-service.d.ts +19 -0
- package/dist/settings/write-service.js +34 -0
- package/dist/target-scope.d.ts +52 -0
- package/dist/target-scope.js +82 -0
- package/dist/text/escape.js +2 -2
- package/dist/text/sanitize-untrusted-text.d.ts +5 -0
- package/dist/text/sanitize-untrusted-text.js +55 -35
- package/dist/tool-exposure.d.ts +79 -0
- package/dist/tool-exposure.js +163 -0
- package/dist/tool-input-errors.d.ts +23 -0
- package/dist/tool-input-errors.js +24 -0
- package/dist/tool-input-normalization.d.ts +37 -0
- package/dist/tool-input-normalization.js +188 -0
- package/dist/tool-metadata-types.d.ts +26 -0
- package/dist/tool-metadata-types.js +2 -0
- package/dist/tool-metadata.d.ts +2 -23
- package/dist/tool-registry.d.ts +38 -22
- package/dist/tool-registry.js +35 -20
- package/package.json +20 -2
- package/plugin-directory.js +72 -33
package/dist/primitives/paths.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
function
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (!prefix)
|
|
8
|
-
throw new RangeError('pathContains requires resolved absolute paths');
|
|
1
|
+
/** Root of an already `/`-separated Windows path: a drive root (`C:/`) or a UNC share root. */
|
|
2
|
+
function windowsRoot(path) {
|
|
3
|
+
return /^[A-Za-z]:\//.exec(path)?.[0] ?? /^\/\/[^/]+\/[^/]+(?:\/|$)/.exec(path)?.[0];
|
|
4
|
+
}
|
|
5
|
+
/** Lexically resolve `.` and `..` segments; `..` never climbs above the root. */
|
|
6
|
+
function resolveSegments(rest) {
|
|
9
7
|
const segments = [];
|
|
10
|
-
for (const segment of
|
|
8
|
+
for (const segment of rest.split('/')) {
|
|
11
9
|
if (!segment || segment === '.')
|
|
12
10
|
continue;
|
|
13
11
|
if (segment === '..')
|
|
@@ -15,7 +13,16 @@ function normalizeAbsolutePath(value, separator) {
|
|
|
15
13
|
else
|
|
16
14
|
segments.push(segment);
|
|
17
15
|
}
|
|
18
|
-
return
|
|
16
|
+
return segments;
|
|
17
|
+
}
|
|
18
|
+
/** Pure lexical normalization of an already absolute path; no cwd, filesystem or symlink access. */
|
|
19
|
+
function normalizeAbsolutePath(value, separator) {
|
|
20
|
+
const windows = separator === '\\';
|
|
21
|
+
const path = windows ? value.replace(/\\/g, '/') : value;
|
|
22
|
+
const prefix = (windows ? windowsRoot(path) : undefined) ?? (path.startsWith('/') ? '/' : undefined);
|
|
23
|
+
if (!prefix)
|
|
24
|
+
throw new RangeError('pathContains requires resolved absolute paths');
|
|
25
|
+
return prefix.replace(/\/+$/, '') + '/' + resolveSegments(path.slice(prefix.length)).join('/');
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
21
28
|
* Check lexical containment using whole directory segments, including equality.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Append work to a fulfilled serial tail and report failures without rejecting the next tail.
|
|
2
|
+
* The observer owns failure policy and must not throw. Callers retain the returned tail to drain.
|
|
3
|
+
* @complexity O(1) scheduling time and space per task, excluding task work.
|
|
4
|
+
*/
|
|
5
|
+
export declare function appendSerialTask({ previous, task, onError }: {
|
|
6
|
+
previous: Promise<void>;
|
|
7
|
+
task: () => Promise<unknown>;
|
|
8
|
+
onError: (error: unknown) => void;
|
|
9
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=tasks.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Append work to a fulfilled serial tail and report failures without rejecting the next tail.
|
|
2
|
+
* The observer owns failure policy and must not throw. Callers retain the returned tail to drain.
|
|
3
|
+
* @complexity O(1) scheduling time and space per task, excluding task work.
|
|
4
|
+
*/
|
|
5
|
+
export function appendSerialTask({ previous, task, onError }, _optional = {}) {
|
|
6
|
+
return previous.then(task).then(() => undefined, onError);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=tasks.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutral catalog contracts consumed by UI and implemented by host adapters.
|
|
3
|
+
* Vendor identities, aliases and endpoint defaults belong to integrations.
|
|
4
|
+
*/
|
|
5
|
+
/** UI catalog identity policy. Operations are pure, synchronous and must agree on IDs.
|
|
6
|
+
* Normalization returns a new map, preserves values, and resolves alias collisions
|
|
7
|
+
* deterministically. The host keeps one policy for the lifetime of a mounted tab.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* MediaProvidersTab({ port, catalog, catalogPolicy: mediaProviderCatalogPolicy });
|
|
11
|
+
*/
|
|
12
|
+
export interface MediaProviderCatalogPolicy {
|
|
13
|
+
/** Canonicalize catalog entries, pinned IDs, edits and clears; preserve unknown IDs. */
|
|
14
|
+
canonicalMediaProviderId(input: {
|
|
15
|
+
id: string;
|
|
16
|
+
}, options?: Record<string, never>): string;
|
|
17
|
+
/** Normalize initial state and daemon replies before reconciliation. Canonical entries win aliases. */
|
|
18
|
+
normalizeMediaProviderMap<T>(input: {
|
|
19
|
+
providers: Readonly<Record<string, T>>;
|
|
20
|
+
}, options?: Record<string, never>): Record<string, T>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One selectable provider in the host-supplied catalog.
|
|
24
|
+
*/
|
|
25
|
+
export interface MediaProviderOption {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
/** Prefilled when the operator picks this provider and typed nothing. */
|
|
29
|
+
defaultBaseUrl?: string;
|
|
30
|
+
/** Model ids this provider advertises. A host that discovers models at
|
|
31
|
+
* runtime can leave this empty. */
|
|
32
|
+
models?: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** Whether a preset appears in the protocol or gateway chip row. */
|
|
35
|
+
export type ProviderPresetKind = 'protocol' | 'gateway';
|
|
36
|
+
/** One host-supplied endpoint preset, parameterized by the consumer's wire protocols. */
|
|
37
|
+
export interface ProviderPreset<Protocol extends string = string> {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
protocol: Protocol;
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
/** Ranked suggestions shown in the model field. Never authoritative — a
|
|
43
|
+
* host with live model discovery should pass `ExecutionPort.listModels`. */
|
|
44
|
+
preferredModels: readonly string[];
|
|
45
|
+
kind?: ProviderPresetKind;
|
|
46
|
+
/** Fixed-origin gateways resolve their base URL themselves, so the Base URL
|
|
47
|
+
* field is hidden rather than shown-and-ignored (origin: `isFixedOriginGateway`). */
|
|
48
|
+
fixedOrigin?: boolean;
|
|
49
|
+
/** Some local/self-hosted endpoints need no bearer credential. Defaults to
|
|
50
|
+
* `true` — i.e. an API key is required unless a preset opts out. */
|
|
51
|
+
requiresApiKey?: boolean;
|
|
52
|
+
/** Optional "Get key" deep link rendered beside the API-key field. */
|
|
53
|
+
apiKeyConsoleUrl?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The literal prefix that positively identifies a pasted key as belonging to THIS provider —
|
|
56
|
+
* `'AIza'` for Google, `'sk-ant-'` for Anthropic. Omit it for a vendor whose keys carry no
|
|
57
|
+
* distinguishing prefix (Azure OpenAI's are bare hex) rather than inventing one.
|
|
58
|
+
*
|
|
59
|
+
* Read in exactly one direction: to recognise a key as SOMEONE ELSE'S. It is deliberately NOT an
|
|
60
|
+
* allowlist, and no code may ask "does the selected preset's key match this?" as a validity test.
|
|
61
|
+
* That is the shape this field replaced, and it shipped a false positive within hours of landing:
|
|
62
|
+
* a real Google Gemini key in a newer shape than `/^AIza/` was reported to its owner as not
|
|
63
|
+
* looking like a Google key. Vendors add key formats without telling anyone, so a catalog can
|
|
64
|
+
* prove a string belongs to vendor X, but never that it belongs to nobody. See
|
|
65
|
+
* `apiKeyFormatWarning`.
|
|
66
|
+
*
|
|
67
|
+
* Data rather than an `if (providerId === …)` chain in the rule, so adding a provider stays a row
|
|
68
|
+
* in the preset catalog — including for a host that supplies its own `presets` and never touches
|
|
69
|
+
* this package.
|
|
70
|
+
*/
|
|
71
|
+
apiKeyPrefix?: string;
|
|
72
|
+
/** The synthetic "Custom" preset. Exactly one may carry this flag; it is
|
|
73
|
+
* never treated as configured-by-preset and never hides the Base URL. */
|
|
74
|
+
custom?: boolean;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=provider-catalog.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one keyed provider registry every provider seam shares (FROZEN CONTRACT, 2026-10-21).
|
|
3
|
+
*
|
|
4
|
+
* Four registries with four shapes grew independently (media `VendorAdapterRegistry`, visitor-auth,
|
|
5
|
+
* `OAuthProviderRegistry`, commerce `createPaymentProviderRegistry`). This is the shape new seams use
|
|
6
|
+
* and the four migrate to; `docs/provider-registry.md` in this package records the contract and the
|
|
7
|
+
* migration path for each. Other builds depend on the exported names and semantics below: change
|
|
8
|
+
* them only additively.
|
|
9
|
+
*
|
|
10
|
+
* Error policy is split by origin. A core/built-in registration that is malformed or duplicated is a
|
|
11
|
+
* composition-root programming error, so it THROWS at boot (same class as commerce's registry). A
|
|
12
|
+
* plugin contribution is third-party input, so a broken or duplicate one is recorded in
|
|
13
|
+
* `refusals()` and never crashes boot (same policy as `createPluginContributionLoader`).
|
|
14
|
+
*/
|
|
15
|
+
/** Provider ids share the plugin-tool name grammar (agent-plugins lifecycle/tools/manifest.ts). */
|
|
16
|
+
export declare const PROVIDER_ID_PATTERN: RegExp;
|
|
17
|
+
export type ProviderOrigin = 'core' | 'plugin';
|
|
18
|
+
/** One registered provider. Stored frozen; the `provider` value keeps its identity. */
|
|
19
|
+
export interface ProviderDefinition<T> {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly provider: T;
|
|
22
|
+
readonly label?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RegisteredProvider<T> extends ProviderDefinition<T> {
|
|
25
|
+
readonly origin: ProviderOrigin;
|
|
26
|
+
/** Set only for `origin: 'plugin'`. */
|
|
27
|
+
readonly pluginId?: string;
|
|
28
|
+
}
|
|
29
|
+
export type ProviderRefusalCode = 'invalid_id' | 'invalid_provider' | 'duplicate' | 'contract_mismatch' | 'displaced_by_core';
|
|
30
|
+
export interface ProviderRefusal {
|
|
31
|
+
readonly code: ProviderRefusalCode;
|
|
32
|
+
readonly pluginId: string;
|
|
33
|
+
readonly providerId: string;
|
|
34
|
+
readonly message: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ProviderRegistry<T> {
|
|
37
|
+
readonly contract: string;
|
|
38
|
+
/** Core/built-in registration. @throws {ProviderRegistryError} on a bad id, invalid provider or duplicate. */
|
|
39
|
+
register(required: {
|
|
40
|
+
readonly definition: ProviderDefinition<T>;
|
|
41
|
+
}, _optional?: Record<string, never>): void;
|
|
42
|
+
/** Plugin registration. Never throws (a throwing validator or provider getter is an `invalid_provider`
|
|
43
|
+
* refusal); returns false and records a refusal instead. */
|
|
44
|
+
contribute(required: {
|
|
45
|
+
readonly definition: ProviderDefinition<T>;
|
|
46
|
+
readonly pluginId: string;
|
|
47
|
+
readonly contract?: string;
|
|
48
|
+
}, _optional?: Record<string, never>): boolean;
|
|
49
|
+
get(required: {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
}, _optional?: Record<string, never>): RegisteredProvider<T> | undefined;
|
|
52
|
+
has(required: {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
}, _optional?: Record<string, never>): boolean;
|
|
55
|
+
/** Frozen snapshot in registration order. */
|
|
56
|
+
list(_required?: Record<string, never>, _optional?: Record<string, never>): readonly RegisteredProvider<T>[];
|
|
57
|
+
/** The configured default provider, if one was named and is registered. */
|
|
58
|
+
getDefault(_required?: Record<string, never>, _optional?: Record<string, never>): RegisteredProvider<T> | undefined;
|
|
59
|
+
/** Frozen snapshot of every refused plugin contribution, in refusal order. */
|
|
60
|
+
refusals(_required?: Record<string, never>, _optional?: Record<string, never>): readonly ProviderRefusal[];
|
|
61
|
+
}
|
|
62
|
+
export declare class ProviderRegistryError extends Error {
|
|
63
|
+
readonly code: ProviderRefusalCode;
|
|
64
|
+
constructor(required: {
|
|
65
|
+
readonly code: ProviderRefusalCode;
|
|
66
|
+
readonly message: string;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build an independent registry for one provider contract (e.g. `mail`, `dns`, `payments`).
|
|
71
|
+
* Core always wins an id: a core registration displaces an earlier plugin entry (recording a
|
|
72
|
+
* `displaced_by_core` refusal) so a plugin loaded first can never turn a later core registration
|
|
73
|
+
* into a boot crash.
|
|
74
|
+
* @param required.contract - Contract name used in messages and checked against contributions.
|
|
75
|
+
* @param optional.defaultProviderId - Validated eagerly; need not be registered yet.
|
|
76
|
+
* @param optional.validate - Contract-specific shape check; a string is the reason it is invalid.
|
|
77
|
+
* @param optional.seed - Core definitions registered in order (throwing policy).
|
|
78
|
+
* @throws {ProviderRegistryError} When the default id or a seed definition is invalid.
|
|
79
|
+
* @complexity register/contribute/get/has O(1); list/refusals O(n) snapshot.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createProviderRegistry<T>(required: {
|
|
82
|
+
readonly contract: string;
|
|
83
|
+
}, optional?: {
|
|
84
|
+
readonly defaultProviderId?: string;
|
|
85
|
+
readonly validate?: (required: {
|
|
86
|
+
readonly provider: T;
|
|
87
|
+
}) => string | undefined;
|
|
88
|
+
readonly seed?: readonly ProviderDefinition<T>[];
|
|
89
|
+
}): ProviderRegistry<T>;
|
|
90
|
+
export type ProviderMode = 'test' | 'live';
|
|
91
|
+
/** Who a provider call acts for. Credentials are pre-resolved by the host, never carried here. */
|
|
92
|
+
export interface ProviderContext {
|
|
93
|
+
readonly workspaceId: string;
|
|
94
|
+
readonly siteId?: string;
|
|
95
|
+
/** The connected account at the provider (e.g. a Stripe account or a mail domain id). */
|
|
96
|
+
readonly providerAccountId?: string;
|
|
97
|
+
readonly mode: ProviderMode;
|
|
98
|
+
}
|
|
99
|
+
/** The provider definitively refused: nothing happened, the caller may correct and retry. */
|
|
100
|
+
export type ProviderRejectedCode = 'invalid_request' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'rate_limited' | 'quota_exceeded' | 'unsupported' | 'not_configured' | 'provider_error';
|
|
101
|
+
/** The outcome is indeterminate (it may have happened): the caller must reconcile, not blindly retry. */
|
|
102
|
+
export type ProviderUnknownCode = 'timeout' | 'network' | 'provider_unavailable' | 'unexpected_response';
|
|
103
|
+
export interface ProviderError<C extends string> {
|
|
104
|
+
readonly code: C;
|
|
105
|
+
/** Caller-safe: no credentials, tokens, or raw provider payloads. */
|
|
106
|
+
readonly message: string;
|
|
107
|
+
readonly retryAfterMs?: number;
|
|
108
|
+
}
|
|
109
|
+
export type ProviderResult<T> = {
|
|
110
|
+
readonly status: 'ok';
|
|
111
|
+
readonly value: T;
|
|
112
|
+
} | {
|
|
113
|
+
readonly status: 'rejected';
|
|
114
|
+
readonly error: ProviderError<ProviderRejectedCode>;
|
|
115
|
+
} | {
|
|
116
|
+
readonly status: 'unknown';
|
|
117
|
+
readonly error: ProviderError<ProviderUnknownCode>;
|
|
118
|
+
};
|
|
119
|
+
export declare function providerOk<T>(required: {
|
|
120
|
+
readonly value: T;
|
|
121
|
+
}, _optional?: Record<string, never>): ProviderResult<T>;
|
|
122
|
+
export declare function providerRejected<T = never>(required: ProviderError<ProviderRejectedCode>, _optional?: Record<string, never>): ProviderResult<T>;
|
|
123
|
+
export declare function providerUnknown<T = never>(required: ProviderError<ProviderUnknownCode>, _optional?: Record<string, never>): ProviderResult<T>;
|
|
124
|
+
export type DomainPurpose = 'mail' | 'site' | 'tls';
|
|
125
|
+
export type DnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'TXT' | 'MX' | 'CAA' | 'NS' | 'SRV';
|
|
126
|
+
export interface DomainRecordRequirement {
|
|
127
|
+
readonly type: DnsRecordType;
|
|
128
|
+
/** Fully-qualified record name, e.g. `send.example.com`. */
|
|
129
|
+
readonly name: string;
|
|
130
|
+
readonly value: string;
|
|
131
|
+
readonly ttl?: number;
|
|
132
|
+
/** MX / SRV priority. */
|
|
133
|
+
readonly priority?: number;
|
|
134
|
+
/** Operator-facing explanation, e.g. "DKIM signing key". */
|
|
135
|
+
readonly note?: string;
|
|
136
|
+
}
|
|
137
|
+
/** What one provider needs from a domain before it can serve `purpose` for it. */
|
|
138
|
+
export interface DomainRequirement {
|
|
139
|
+
readonly purpose: DomainPurpose;
|
|
140
|
+
readonly domain: string;
|
|
141
|
+
readonly providerId: string;
|
|
142
|
+
readonly records: readonly DomainRecordRequirement[];
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=provider-registry.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one keyed provider registry every provider seam shares (FROZEN CONTRACT, 2026-10-21).
|
|
3
|
+
*
|
|
4
|
+
* Four registries with four shapes grew independently (media `VendorAdapterRegistry`, visitor-auth,
|
|
5
|
+
* `OAuthProviderRegistry`, commerce `createPaymentProviderRegistry`). This is the shape new seams use
|
|
6
|
+
* and the four migrate to; `docs/provider-registry.md` in this package records the contract and the
|
|
7
|
+
* migration path for each. Other builds depend on the exported names and semantics below: change
|
|
8
|
+
* them only additively.
|
|
9
|
+
*
|
|
10
|
+
* Error policy is split by origin. A core/built-in registration that is malformed or duplicated is a
|
|
11
|
+
* composition-root programming error, so it THROWS at boot (same class as commerce's registry). A
|
|
12
|
+
* plugin contribution is third-party input, so a broken or duplicate one is recorded in
|
|
13
|
+
* `refusals()` and never crashes boot (same policy as `createPluginContributionLoader`).
|
|
14
|
+
*/
|
|
15
|
+
/** Provider ids share the plugin-tool name grammar (agent-plugins lifecycle/tools/manifest.ts). */
|
|
16
|
+
export const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
17
|
+
export class ProviderRegistryError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(required) {
|
|
20
|
+
super(required.message);
|
|
21
|
+
this.name = 'ProviderRegistryError';
|
|
22
|
+
this.code = required.code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** @returns The first validation fault of a definition, or undefined when it may be registered. */
|
|
26
|
+
function definitionFault(required, optional = {}) {
|
|
27
|
+
const { contract, definition } = required;
|
|
28
|
+
if (typeof definition.id !== 'string' || !PROVIDER_ID_PATTERN.test(definition.id)) {
|
|
29
|
+
return { code: 'invalid_id', message: `${contract} provider id '${String(definition.id)}' must match ${PROVIDER_ID_PATTERN.source}` };
|
|
30
|
+
}
|
|
31
|
+
const reason = optional.validate?.({ provider: definition.provider });
|
|
32
|
+
if (reason !== undefined)
|
|
33
|
+
return { code: 'invalid_provider', message: `${contract} provider '${definition.id}' is invalid: ${reason}` };
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/** The id for a refusal line; a hostile definition whose id cannot even be read is recorded as ''. */
|
|
37
|
+
function safeId(required, _optional = {}) {
|
|
38
|
+
try {
|
|
39
|
+
return String(required.definition.id);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function frozenEntry(required, optional = {}) {
|
|
46
|
+
const { id, provider, label } = required.definition;
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
id, provider, origin: required.origin,
|
|
49
|
+
...(label === undefined ? {} : { label }),
|
|
50
|
+
...(optional.pluginId === undefined ? {} : { pluginId: optional.pluginId }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build an independent registry for one provider contract (e.g. `mail`, `dns`, `payments`).
|
|
55
|
+
* Core always wins an id: a core registration displaces an earlier plugin entry (recording a
|
|
56
|
+
* `displaced_by_core` refusal) so a plugin loaded first can never turn a later core registration
|
|
57
|
+
* into a boot crash.
|
|
58
|
+
* @param required.contract - Contract name used in messages and checked against contributions.
|
|
59
|
+
* @param optional.defaultProviderId - Validated eagerly; need not be registered yet.
|
|
60
|
+
* @param optional.validate - Contract-specific shape check; a string is the reason it is invalid.
|
|
61
|
+
* @param optional.seed - Core definitions registered in order (throwing policy).
|
|
62
|
+
* @throws {ProviderRegistryError} When the default id or a seed definition is invalid.
|
|
63
|
+
* @complexity register/contribute/get/has O(1); list/refusals O(n) snapshot.
|
|
64
|
+
*/
|
|
65
|
+
export function createProviderRegistry(required, optional = {}) {
|
|
66
|
+
const { contract } = required;
|
|
67
|
+
const { defaultProviderId, validate } = optional;
|
|
68
|
+
if (defaultProviderId !== undefined && !PROVIDER_ID_PATTERN.test(defaultProviderId)) {
|
|
69
|
+
throw new ProviderRegistryError({ code: 'invalid_id', message: `${contract} default provider id '${defaultProviderId}' must match ${PROVIDER_ID_PATTERN.source}` });
|
|
70
|
+
}
|
|
71
|
+
const providers = new Map();
|
|
72
|
+
const refused = [];
|
|
73
|
+
const refuse = (input) => {
|
|
74
|
+
refused.push(Object.freeze({ ...input.fault, pluginId: input.pluginId, providerId: input.providerId }));
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
const registry = {
|
|
78
|
+
contract,
|
|
79
|
+
register({ definition }) {
|
|
80
|
+
const fault = definitionFault({ contract, definition }, { ...(validate ? { validate } : {}) });
|
|
81
|
+
if (fault)
|
|
82
|
+
throw new ProviderRegistryError(fault);
|
|
83
|
+
const existing = providers.get(definition.id);
|
|
84
|
+
if (existing?.origin === 'core') {
|
|
85
|
+
throw new ProviderRegistryError({ code: 'duplicate', message: `${contract} provider '${definition.id}' is already registered` });
|
|
86
|
+
}
|
|
87
|
+
if (existing?.pluginId !== undefined) {
|
|
88
|
+
refuse({ fault: { code: 'displaced_by_core', message: `${contract} provider '${definition.id}' from plugin '${existing.pluginId}' was replaced by the built-in provider` }, pluginId: existing.pluginId, providerId: definition.id });
|
|
89
|
+
providers.delete(definition.id);
|
|
90
|
+
}
|
|
91
|
+
providers.set(definition.id, frozenEntry({ definition, origin: 'core' }));
|
|
92
|
+
},
|
|
93
|
+
contribute(input) {
|
|
94
|
+
// Third-party input: a throwing validator, id or provider getter is refused, never propagated
|
|
95
|
+
// (core registrations keep the throwing policy above).
|
|
96
|
+
try {
|
|
97
|
+
return contributeChecked(input);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return refuse({ fault: { code: 'invalid_provider', message: `${contract} provider from plugin '${input.pluginId}' was not loaded: its definition could not be validated` }, pluginId: input.pluginId, providerId: safeId({ definition: input.definition }) });
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
get({ id }) { return providers.get(id); },
|
|
104
|
+
has({ id }) { return providers.has(id); },
|
|
105
|
+
list() { return Object.freeze([...providers.values()]); },
|
|
106
|
+
getDefault() { return defaultProviderId === undefined ? undefined : providers.get(defaultProviderId); },
|
|
107
|
+
refusals() { return Object.freeze([...refused]); },
|
|
108
|
+
};
|
|
109
|
+
function contributeChecked({ definition, pluginId, contract: declared }) {
|
|
110
|
+
const providerId = String(definition.id);
|
|
111
|
+
if (declared !== undefined && declared !== contract) {
|
|
112
|
+
return refuse({ fault: { code: 'contract_mismatch', message: `plugin '${pluginId}' contributed provider '${providerId}' for contract '${declared}', not '${contract}'` }, pluginId, providerId });
|
|
113
|
+
}
|
|
114
|
+
const fault = definitionFault({ contract, definition }, { ...(validate ? { validate } : {}) });
|
|
115
|
+
if (fault)
|
|
116
|
+
return refuse({ fault, pluginId, providerId });
|
|
117
|
+
const existing = providers.get(providerId);
|
|
118
|
+
if (existing) {
|
|
119
|
+
const owner = existing.pluginId === undefined ? 'the built-in provider' : `plugin '${existing.pluginId}'`;
|
|
120
|
+
return refuse({ fault: { code: 'duplicate', message: `${contract} provider '${providerId}' from plugin '${pluginId}' was not loaded: already registered by ${owner}` }, pluginId, providerId });
|
|
121
|
+
}
|
|
122
|
+
providers.set(providerId, frozenEntry({ definition, origin: 'plugin' }, { pluginId }));
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
for (const definition of optional.seed ?? [])
|
|
126
|
+
registry.register({ definition });
|
|
127
|
+
return registry;
|
|
128
|
+
}
|
|
129
|
+
export function providerOk(required, _optional = {}) {
|
|
130
|
+
return { status: 'ok', value: required.value };
|
|
131
|
+
}
|
|
132
|
+
export function providerRejected(required, _optional = {}) {
|
|
133
|
+
return { status: 'rejected', error: { ...required } };
|
|
134
|
+
}
|
|
135
|
+
export function providerUnknown(required, _optional = {}) {
|
|
136
|
+
return { status: 'unknown', error: { ...required } };
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=provider-registry.js.map
|
package/dist/records.d.ts
CHANGED
|
@@ -21,4 +21,28 @@ export declare function isNonArrayRecord(required: {
|
|
|
21
21
|
}, _optional?: Record<string, never>): required is {
|
|
22
22
|
value: Record<string, unknown>;
|
|
23
23
|
};
|
|
24
|
+
/** Strict equality over the supplied own, enumerable string fields; omitted fields are unconstrained.
|
|
25
|
+
* Memory adapters keep their scope and identity fields in this selector, including on deletes.
|
|
26
|
+
* @complexity O(k) time and space for k selector fields.
|
|
27
|
+
*/
|
|
28
|
+
export declare function matchesRecordFields<Row extends object>({ record, fields }: {
|
|
29
|
+
record: Row;
|
|
30
|
+
fields: Partial<Row>;
|
|
31
|
+
}, _optional?: Record<string, never>): boolean;
|
|
32
|
+
/** Replace the first matching record in place, or append; preserves order and reference semantics.
|
|
33
|
+
* @complexity O(nk) time and O(k) space for n records and k identity fields.
|
|
34
|
+
*/
|
|
35
|
+
export declare function upsertRecord<Row extends object>({ records, record, identity }: {
|
|
36
|
+
records: Row[];
|
|
37
|
+
record: Row;
|
|
38
|
+
identity: Partial<Row>;
|
|
39
|
+
}, _optional?: Record<string, never>): void;
|
|
40
|
+
/** Remove a keyed record only when its additional scope fields match.
|
|
41
|
+
* @complexity O(k) time and space for k selector fields, plus the map lookup.
|
|
42
|
+
*/
|
|
43
|
+
export declare function deleteMapRecord<Key, Row extends object>({ records, key, fields }: {
|
|
44
|
+
records: Map<Key, Row>;
|
|
45
|
+
key: Key;
|
|
46
|
+
fields: Partial<Row>;
|
|
47
|
+
}, _optional?: Record<string, never>): void;
|
|
24
48
|
//# sourceMappingURL=records.d.ts.map
|
package/dist/records.js
CHANGED
|
@@ -18,4 +18,29 @@ export function isRecord(required, _optional = {}) {
|
|
|
18
18
|
export function isNonArrayRecord(required, _optional = {}) {
|
|
19
19
|
return isRecord(required) && !Array.isArray(required.value);
|
|
20
20
|
}
|
|
21
|
+
/** Strict equality over the supplied own, enumerable string fields; omitted fields are unconstrained.
|
|
22
|
+
* Memory adapters keep their scope and identity fields in this selector, including on deletes.
|
|
23
|
+
* @complexity O(k) time and space for k selector fields.
|
|
24
|
+
*/
|
|
25
|
+
export function matchesRecordFields({ record, fields }, _optional = {}) {
|
|
26
|
+
return Object.keys(fields).every(key => record[key] === fields[key]);
|
|
27
|
+
}
|
|
28
|
+
/** Replace the first matching record in place, or append; preserves order and reference semantics.
|
|
29
|
+
* @complexity O(nk) time and O(k) space for n records and k identity fields.
|
|
30
|
+
*/
|
|
31
|
+
export function upsertRecord({ records, record, identity }, _optional = {}) {
|
|
32
|
+
const index = records.findIndex(candidate => matchesRecordFields({ record: candidate, fields: identity }));
|
|
33
|
+
if (index === -1)
|
|
34
|
+
records.push(record);
|
|
35
|
+
else
|
|
36
|
+
records[index] = record;
|
|
37
|
+
}
|
|
38
|
+
/** Remove a keyed record only when its additional scope fields match.
|
|
39
|
+
* @complexity O(k) time and space for k selector fields, plus the map lookup.
|
|
40
|
+
*/
|
|
41
|
+
export function deleteMapRecord({ records, key, fields }, _optional = {}) {
|
|
42
|
+
const record = records.get(key);
|
|
43
|
+
if (record && matchesRecordFields({ record, fields }))
|
|
44
|
+
records.delete(key);
|
|
45
|
+
}
|
|
21
46
|
//# sourceMappingURL=records.js.map
|
package/dist/redact.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ export interface SecretRedactionPattern {
|
|
|
44
44
|
}
|
|
45
45
|
/** Optional caller-owned secrets and named lexical rules, without ambient credential lookup. */
|
|
46
46
|
export interface SecretRedactionOptions {
|
|
47
|
+
/** Diagnostic text can retain addresses/numbers while using the same credential rules. Default true. */
|
|
48
|
+
redactPii?: boolean;
|
|
49
|
+
/** Exact trusted registration IDs; credential patterns always take precedence. */
|
|
50
|
+
diagnosticToolIds?: readonly string[];
|
|
47
51
|
/** Conservative by default; aggressive also masks bare 20+ character opaque runs. */
|
|
48
52
|
policy?: 'conservative' | 'aggressive';
|
|
49
53
|
exactSecrets?: ReadonlyArray<string | undefined | null>;
|
package/dist/redact.js
CHANGED
|
@@ -139,21 +139,17 @@ function isLuhnValid(digits) {
|
|
|
139
139
|
}
|
|
140
140
|
return sum % 10 === 0;
|
|
141
141
|
}
|
|
142
|
-
function redactApiKeyHeaderValue(prefix, quote, name, separator, value) {
|
|
143
|
-
const redactedValue = value.startsWith('"') ? '"[REDACTED:api_key_header]"' : '[REDACTED:api_key_header]';
|
|
144
|
-
return `${prefix}${quote}${name}${quote}${separator}${redactedValue}`;
|
|
145
|
-
}
|
|
146
142
|
// A labeled credential has its value masked even when too short to have a vendor token shape.
|
|
147
143
|
// Bare long opaque runs are opt-in CLI heuristics: UUIDs, hashes, model IDs and path segments
|
|
148
144
|
// share that alphabet, so the default telemetry policy must leave them readable. Whole
|
|
149
|
-
// correlation IDs and the delegated-tool route stay readable even in aggressive mode;
|
|
145
|
+
// correlation IDs, UPPER_SNAKE diagnostic codes and the delegated-tool route stay readable even in aggressive mode;
|
|
150
146
|
// appending secret characters defeats that exception. The host's ERR-XXXX-XXXX-XXXX-XXXX
|
|
151
147
|
// correlation ID uses uppercase hex and is meant to reach the model and be quoted to an
|
|
152
148
|
// operator; delegated-tool-calls is the route segment named in delegated tool failures.
|
|
153
149
|
// Matching the ENTIRE run keeps a secret glued onto either spelling redactable.
|
|
154
150
|
export const LABELED_SECRET_RE = /\b(authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie)\b\s*[:=]\s*\S+/gi;
|
|
155
151
|
export const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
|
|
156
|
-
export const NON_SECRET_TOKEN_RE = /^(?:ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}|delegated-tool-calls)$/;
|
|
152
|
+
export const NON_SECRET_TOKEN_RE = /^(?:ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}|delegated-tool-calls|[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)$/;
|
|
157
153
|
// Runtime markers must survive a second pass; the bounded sanitizer deliberately retains
|
|
158
154
|
// its historical lowercase marker contract and uses the unmodified lexical vocabulary.
|
|
159
155
|
// Reject a masked header/query value before matching, so a following field's credential
|
|
@@ -199,39 +195,71 @@ export function redactSecretsWithCounts({ input }, options = {}) {
|
|
|
199
195
|
const counts = {};
|
|
200
196
|
if (!input)
|
|
201
197
|
return { redacted: input, counts };
|
|
202
|
-
let out = input;
|
|
203
198
|
const marker = (name) => {
|
|
204
199
|
counts[name] = (counts[name] ?? 0) + 1;
|
|
205
200
|
return `[REDACTED:${name}]`;
|
|
206
201
|
};
|
|
207
|
-
|
|
202
|
+
let out = redactExactSecrets({ text: input, marker, secrets: options.exactSecrets ?? [] });
|
|
203
|
+
out = redactUrlCredentials({ text: out, marker });
|
|
204
|
+
out = applyPatterns({ text: out, marker, patterns: credentialPatterns(options.extraPatterns ?? []) });
|
|
205
|
+
out = redactKeyedCredentials({ text: out, marker });
|
|
206
|
+
if (options.policy === 'aggressive') {
|
|
207
|
+
const trustedIds = new Set(options.diagnosticToolIds);
|
|
208
|
+
out = out.replace(/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g, token => trustedIds.has(token) ? token
|
|
209
|
+
: token.replace(OPAQUE_TOKEN_RE, match => NON_SECRET_TOKEN_RE.test(match) ? match : marker('opaque_token')));
|
|
210
|
+
}
|
|
211
|
+
if (options.redactPii !== false)
|
|
212
|
+
out = redactPii({ text: out, marker });
|
|
213
|
+
return { redacted: out, counts };
|
|
214
|
+
}
|
|
215
|
+
// PII rules run last (after credential and opt-in opaque rules) and only when PII redaction is on;
|
|
216
|
+
// splitting once keeps each list in PATTERNS order.
|
|
217
|
+
const PII_PATTERN_NAMES = ['email', 'ipv4', 'phone'];
|
|
218
|
+
const NON_PII_PATTERNS = PATTERNS.filter((pattern) => !PII_PATTERN_NAMES.includes(pattern.name));
|
|
219
|
+
const PII_PATTERNS = PATTERNS.filter((pattern) => PII_PATTERN_NAMES.includes(pattern.name));
|
|
220
|
+
/** Built-in credential rules, then the static-scan shapes (labeled `credential`), then the caller's extra rules. */
|
|
221
|
+
function credentialPatterns(extraPatterns) {
|
|
222
|
+
return [
|
|
223
|
+
...NON_PII_PATTERNS,
|
|
224
|
+
...SECRET_SHAPE_PATTERNS.map((shape) => ({ name: 'credential', regex: shape.pattern })),
|
|
225
|
+
...extraPatterns,
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
function applyPatterns({ text, marker, patterns }) {
|
|
229
|
+
let out = text;
|
|
230
|
+
for (const { name, regex } of patterns) {
|
|
208
231
|
// Clone host/global regexes so lastIndex state never leaks across requests. Named extra
|
|
209
232
|
// patterns use the same global replacement contract as the built-in vocabulary.
|
|
210
233
|
const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
|
|
211
234
|
out = out.replace(new RegExp(regex.source, flags), () => marker(name));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
/** Masks caller-supplied secrets verbatim, longest first, ignoring empty and non-string entries. */
|
|
239
|
+
function redactExactSecrets({ text, marker, secrets }) {
|
|
240
|
+
const usable = [...new Set(secrets.filter((secret) => typeof secret === 'string' && secret.length > 0))].sort((a, b) => b.length - a.length);
|
|
241
|
+
let out = text;
|
|
242
|
+
for (const secret of usable) {
|
|
215
243
|
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
216
244
|
out = out.replace(new RegExp(escaped, 'g'), () => marker('exact_secret'));
|
|
217
245
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
out = out.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => {
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
/** URL userinfo and signed query values: credentials recognized by their URL context. */
|
|
249
|
+
function redactUrlCredentials({ text, marker }) {
|
|
250
|
+
return text
|
|
251
|
+
.replace(URL_CREDENTIALS_RE, (match, scheme, userinfo) => isMaskedValue(userinfo) ? match : `${scheme}${marker('url_credentials')}@`)
|
|
252
|
+
.replace(SIGNED_QUERY_RE, (match, prefix, value) => isMaskedValue(value) ? match : `${prefix}${marker('signed_query')}`);
|
|
253
|
+
}
|
|
254
|
+
/** Values named by a header, query parameter, bearer prefix or credential label. */
|
|
255
|
+
function redactKeyedCredentials({ text, marker }) {
|
|
256
|
+
let out = text.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => {
|
|
230
257
|
// Already masked values are not new credentials and should not inflate the audit count.
|
|
231
258
|
if (isMaskedValue(value))
|
|
232
259
|
return `${prefix}${quote}${name}${quote}${separator}${value}`;
|
|
233
260
|
marker('api_key_header');
|
|
234
|
-
|
|
261
|
+
const redactedValue = value.startsWith('"') ? '"[REDACTED:api_key_header]"' : '[REDACTED:api_key_header]';
|
|
262
|
+
return `${prefix}${quote}${name}${quote}${separator}${redactedValue}`;
|
|
235
263
|
});
|
|
236
264
|
// The provider rule includes a query fragment in an echoed key; mask that whole value
|
|
237
265
|
// before the narrower core query rule can split it and leave a credential suffix behind.
|
|
@@ -239,24 +267,19 @@ export function redactSecretsWithCounts({ input }, options = {}) {
|
|
|
239
267
|
out = out.replace(API_KEY_QUERY, (match, prefix) => isMaskedValue(match.slice(prefix.length)) ? match : `${prefix}${marker('api_key_query')}`);
|
|
240
268
|
out = out.replace(SHORT_BEARER_RE, () => marker('bearer_token'));
|
|
241
269
|
out = out.replace(PROVIDER_API_KEY_HEADER, (match, name, value) => isMaskedValue(value) ? match : `${name}: ${marker('api_key_header')}`);
|
|
242
|
-
|
|
270
|
+
return out.replace(MASK_AWARE_LABELED_SECRET_RE, (match, _label, value) => {
|
|
243
271
|
// Replace only the captured value, retaining labels even when no whitespace separates
|
|
244
272
|
// them. Glued credential suffixes fail the complete-marker check and remain maskable.
|
|
245
273
|
if (isMaskedValue(value))
|
|
246
274
|
return match;
|
|
247
275
|
return `${match.slice(0, -value.length)}${marker('labeled_secret')}`;
|
|
248
276
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (piiNames.has(pattern.name))
|
|
254
|
-
applyPattern(pattern);
|
|
255
|
-
}
|
|
256
|
-
out = out.replace(CARD_CANDIDATE, (match) => {
|
|
277
|
+
}
|
|
278
|
+
/** Email/IPv4/phone rules, then Luhn-checked card candidates. */
|
|
279
|
+
function redactPii({ text, marker }) {
|
|
280
|
+
return applyPatterns({ text, marker, patterns: PII_PATTERNS }).replace(CARD_CANDIDATE, (match) => {
|
|
257
281
|
const digits = match.replace(/\D/g, '');
|
|
258
282
|
return isLuhnValid(digits) ? marker('credit_card') : match;
|
|
259
283
|
});
|
|
260
|
-
return { redacted: out, counts };
|
|
261
284
|
}
|
|
262
285
|
//# sourceMappingURL=redact.js.map
|