@jxburros/ai-nugget 0.3.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -271
- package/UPGRADING.md +158 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +64 -37
- package/dist/adapters/engines/base.d.ts +29 -2
- package/dist/adapters/engines/base.js +52 -8
- package/dist/adapters/engines/google.d.ts +17 -1
- package/dist/adapters/engines/google.js +93 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +51 -15
- package/dist/adapters/engines/openaiChat.d.ts +2 -1
- package/dist/adapters/engines/openaiChat.js +60 -30
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +43 -1
- package/dist/agent/loop.js +120 -13
- package/dist/cjs/adapters/engines/anthropic.js +308 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +284 -0
- package/dist/cjs/adapters/engines/ollama.js +199 -0
- package/dist/cjs/adapters/engines/openaiChat.js +261 -0
- package/dist/cjs/adapters/index.js +22 -0
- package/dist/cjs/adapters/profiles.js +125 -0
- package/dist/cjs/agent/index.js +18 -0
- package/dist/cjs/agent/loop.js +372 -0
- package/dist/cjs/agent/tools.js +51 -0
- package/dist/cjs/connect.js +27 -0
- package/dist/cjs/errors.js +110 -0
- package/dist/cjs/handler.js +561 -0
- package/dist/cjs/index.js +27 -0
- package/dist/cjs/json.js +121 -0
- package/dist/cjs/keys.js +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/policy.js +42 -0
- package/dist/cjs/redact.js +78 -0
- package/dist/cjs/tokens.js +22 -0
- package/dist/cjs/transport.js +172 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/util.js +112 -0
- package/dist/connect.d.ts +31 -0
- package/dist/connect.js +25 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +61 -1
- package/dist/handler.js +247 -61
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.js +1 -4
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +29 -1
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +21 -1
- package/dist/types.d.ts +96 -2
- package/dist/util.d.ts +16 -0
- package/dist/util.js +50 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/anthropic.js +305 -0
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
- package/nugget/dist/adapters/engines/base.d.ts +50 -0
- package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/base.js +137 -0
- package/nugget/dist/adapters/engines/base.js.map +1 -0
- package/nugget/dist/adapters/engines/google.d.ts +24 -0
- package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/google.js +281 -0
- package/nugget/dist/adapters/engines/google.js.map +1 -0
- package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
- package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/ollama.js +196 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +16 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +258 -0
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
- package/nugget/dist/adapters/index.d.ts +5 -0
- package/nugget/dist/adapters/index.d.ts.map +1 -0
- package/nugget/dist/adapters/index.js +17 -0
- package/nugget/dist/adapters/index.js.map +1 -0
- package/nugget/dist/adapters/profiles.d.ts +62 -0
- package/nugget/dist/adapters/profiles.d.ts.map +1 -0
- package/nugget/dist/adapters/profiles.js +121 -0
- package/nugget/dist/adapters/profiles.js.map +1 -0
- package/nugget/dist/agent/index.d.ts +3 -0
- package/nugget/dist/agent/index.d.ts.map +1 -0
- package/nugget/dist/agent/index.js +3 -0
- package/nugget/dist/agent/index.js.map +1 -0
- package/nugget/dist/agent/loop.d.ts +112 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +370 -0
- package/nugget/dist/agent/loop.js.map +1 -0
- package/nugget/dist/agent/tools.d.ts +29 -0
- package/nugget/dist/agent/tools.d.ts.map +1 -0
- package/nugget/dist/agent/tools.js +48 -0
- package/nugget/dist/agent/tools.js.map +1 -0
- package/nugget/dist/cjs/adapters/engines/anthropic.js +308 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +284 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +199 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +261 -0
- package/nugget/dist/cjs/adapters/index.js +22 -0
- package/nugget/dist/cjs/adapters/profiles.js +125 -0
- package/nugget/dist/cjs/agent/index.js +18 -0
- package/nugget/dist/cjs/agent/loop.js +372 -0
- package/nugget/dist/cjs/agent/tools.js +51 -0
- package/nugget/dist/cjs/connect.js +27 -0
- package/nugget/dist/cjs/errors.js +110 -0
- package/nugget/dist/cjs/handler.js +561 -0
- package/nugget/dist/cjs/index.js +27 -0
- package/nugget/dist/cjs/json.js +121 -0
- package/nugget/dist/cjs/keys.js +81 -0
- package/nugget/dist/cjs/package.json +3 -0
- package/nugget/dist/cjs/policy.js +42 -0
- package/nugget/dist/cjs/redact.js +78 -0
- package/nugget/dist/cjs/tokens.js +22 -0
- package/nugget/dist/cjs/transport.js +172 -0
- package/nugget/dist/cjs/types.js +6 -0
- package/nugget/dist/cjs/util.js +112 -0
- package/nugget/dist/connect.d.ts +31 -0
- package/nugget/dist/connect.d.ts.map +1 -0
- package/nugget/dist/connect.js +25 -0
- package/nugget/dist/connect.js.map +1 -0
- package/nugget/dist/errors.d.ts +22 -0
- package/nugget/dist/errors.d.ts.map +1 -0
- package/nugget/dist/errors.js +104 -0
- package/nugget/dist/errors.js.map +1 -0
- package/nugget/dist/handler.d.ts +108 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +557 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +12 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +12 -0
- package/nugget/dist/index.js.map +1 -0
- package/nugget/dist/json.d.ts +7 -0
- package/nugget/dist/json.d.ts.map +1 -0
- package/nugget/dist/json.js +114 -0
- package/nugget/dist/json.js.map +1 -0
- package/nugget/dist/keys.d.ts +7 -0
- package/nugget/dist/keys.d.ts.map +1 -0
- package/nugget/dist/keys.js +75 -0
- package/nugget/dist/keys.js.map +1 -0
- package/nugget/dist/policy.d.ts +6 -0
- package/nugget/dist/policy.d.ts.map +1 -0
- package/nugget/dist/policy.js +37 -0
- package/nugget/dist/policy.js.map +1 -0
- package/nugget/dist/redact.d.ts +26 -0
- package/nugget/dist/redact.d.ts.map +1 -0
- package/nugget/dist/redact.js +74 -0
- package/nugget/dist/redact.js.map +1 -0
- package/nugget/dist/tokens.d.ts +5 -0
- package/nugget/dist/tokens.d.ts.map +1 -0
- package/nugget/dist/tokens.js +18 -0
- package/nugget/dist/tokens.js.map +1 -0
- package/nugget/dist/transport.d.ts +30 -0
- package/nugget/dist/transport.d.ts.map +1 -0
- package/nugget/dist/transport.js +165 -0
- package/nugget/dist/transport.js.map +1 -0
- package/nugget/dist/types.d.ts +285 -0
- package/nugget/dist/types.d.ts.map +1 -0
- package/nugget/dist/types.js +4 -0
- package/nugget/dist/types.js.map +1 -0
- package/nugget/dist/util.d.ts +24 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +101 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +62 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +92 -29
- package/nugget/src/adapters/engines/ollama.ts +51 -17
- package/nugget/src/adapters/engines/openaiChat.ts +61 -32
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +158 -14
- package/nugget/src/connect.ts +48 -0
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +286 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/keys.ts +1 -5
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +96 -1
- package/nugget/src/util.ts +55 -0
- package/package.json +20 -6
package/dist/connect.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { globalEnv } from './util.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a server-side `Connection` plus a default `model` from app-owned
|
|
4
|
+
* env vars (`AI_PROVIDER`, `AI_MODEL`, `AI_KEY_ENV`, `AI_BASE_URL` unless
|
|
5
|
+
* overridden). This only collapses the config-reading boilerplate that was
|
|
6
|
+
* being hand-rolled the same way in every small server app; it does not add
|
|
7
|
+
* policy. `provider`/`baseUrl` still come from the server's own environment,
|
|
8
|
+
* never from client input, and the returned `keyRef` still resolves through
|
|
9
|
+
* `KeySource` at call time like any other `Connection`.
|
|
10
|
+
*/
|
|
11
|
+
export function envConnection(options = {}) {
|
|
12
|
+
const env = options.env ?? globalEnv();
|
|
13
|
+
const provider = env[options.providerVar ?? 'AI_PROVIDER']?.trim() || options.defaultProvider || 'openai';
|
|
14
|
+
const model = env[options.modelVar ?? 'AI_MODEL']?.trim() || options.defaultModel || 'gpt-4o-mini';
|
|
15
|
+
const keyEnvName = env[options.keyEnvVar ?? 'AI_KEY_ENV']?.trim() || options.defaultKeyEnv || 'OPENAI_API_KEY';
|
|
16
|
+
const baseUrl = env[options.baseUrlVar ?? 'AI_BASE_URL']?.trim();
|
|
17
|
+
const connection = {
|
|
18
|
+
id: options.id ?? 'app',
|
|
19
|
+
provider,
|
|
20
|
+
keyRef: { kind: 'env', name: keyEnvName },
|
|
21
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
22
|
+
};
|
|
23
|
+
return { connection, model };
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=connect.js.map
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { createDefaultRedactor } from './redact.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pattern-based redaction applied at the wire boundary, before an error object
|
|
4
|
+
* exists. `AIHandler` redacts again on the way out (adding session-registered
|
|
5
|
+
* exact secrets), but doing it here means a provider body that echoes a
|
|
6
|
+
* recognizable secret can never be carried by an `AIError` in the first place —
|
|
7
|
+
* including on code paths that never reach the handler's own catch blocks.
|
|
8
|
+
*/
|
|
9
|
+
const wireRedactor = createDefaultRedactor();
|
|
1
10
|
export class AIError extends Error {
|
|
2
11
|
kind;
|
|
3
12
|
status;
|
|
@@ -26,9 +35,13 @@ export function defaultRetryable(kind) {
|
|
|
26
35
|
return kind === 'rate_limit' || kind === 'timeout' || kind === 'network' || kind === 'server' || kind === 'invalid_response';
|
|
27
36
|
}
|
|
28
37
|
export function classify(status, body = '', provider, headers) {
|
|
29
|
-
const excerpt = body.slice(0, 200);
|
|
38
|
+
const excerpt = wireRedactor.redact(body.slice(0, 200));
|
|
30
39
|
const lower = body.toLowerCase();
|
|
31
40
|
let kind = 'server';
|
|
41
|
+
// 403 is classified as `auth` (non-retryable) deliberately: a forbidden
|
|
42
|
+
// response almost always means a bad/insufficient credential or a blocked
|
|
43
|
+
// region/model, none of which a retry fixes. Callers that know a specific
|
|
44
|
+
// provider returns a retryable 403 can reclassify in a `beforeCall`/adapter.
|
|
32
45
|
if (status === 401 || status === 403)
|
|
33
46
|
kind = 'auth';
|
|
34
47
|
else if (status === 408)
|
|
@@ -40,8 +53,19 @@ export function classify(status, body = '', provider, headers) {
|
|
|
40
53
|
? 'context_length'
|
|
41
54
|
: 'invalid_request';
|
|
42
55
|
}
|
|
56
|
+
else if (status === 404 || status === 410) {
|
|
57
|
+
// A missing route/deployment is not a malformed request: it is usually a
|
|
58
|
+
// wrong baseUrl or model name, and sometimes a temporarily misrouted load
|
|
59
|
+
// balancer. `not_found` keeps it distinguishable from `invalid_request` so
|
|
60
|
+
// apps don't tell a user "your request was invalid" for an infra problem.
|
|
61
|
+
// Non-retryable by default — a caller that knows its 404s are transient can
|
|
62
|
+
// retry on `kind === 'not_found'` itself.
|
|
63
|
+
kind = 'not_found';
|
|
64
|
+
}
|
|
43
65
|
else if (status >= 500)
|
|
44
66
|
kind = 'server';
|
|
67
|
+
// Everything else in 4xx (409 conflict, 405, 415, 451, …) is a genuine
|
|
68
|
+
// client-side request problem, so `invalid_request` is honest for it.
|
|
45
69
|
else
|
|
46
70
|
kind = 'invalid_request';
|
|
47
71
|
return new AIError(`HTTP ${status}: ${excerpt}`, {
|
package/dist/handler.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallInfo, CallRecord, ChatRequest, ChatResult, Connection, GovernancePolicy, KeySource, ModelInfo, Redactor, StreamEvent, TelemetrySink } from './types.js';
|
|
1
|
+
import type { CallInfo, CallRecord, ChatRequest, ChatResult, Connection, EmbedRequest, EmbedResult, GovernancePolicy, KeySource, ModelInfo, ParsedResult, Redactor, StandardSchemaV1, StreamEvent, TelemetrySink, Usage } from './types.js';
|
|
2
2
|
export interface HandlerOptions {
|
|
3
3
|
keySource: KeySource;
|
|
4
4
|
policy?: GovernancePolicy;
|
|
@@ -13,11 +13,37 @@ export interface HandlerOptions {
|
|
|
13
13
|
baseDelayMs?: number;
|
|
14
14
|
maxDelayMs?: number;
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Concurrency and pacing limits, enforced by an **in-memory queue on this
|
|
18
|
+
* handler instance only**. There is no shared/external coordination, so N
|
|
19
|
+
* instances behind a load balancer allow N × `maxConcurrent` in flight against
|
|
20
|
+
* the provider. Size these per instance, or put a shared limiter in front if a
|
|
21
|
+
* global ceiling matters.
|
|
22
|
+
*/
|
|
16
23
|
limits?: {
|
|
17
24
|
maxConcurrent?: number;
|
|
18
25
|
minIntervalMs?: number;
|
|
19
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Silence the one-time startup notice logged when no `policy` is supplied.
|
|
29
|
+
* Set this only when running unrestricted is a deliberate choice — passing an
|
|
30
|
+
* explicit `policy: allowAllPolicy()` says the same thing and is clearer.
|
|
31
|
+
*/
|
|
32
|
+
silencePolicyWarning?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Optional cost estimator. Called once per successful call/embedding with the
|
|
35
|
+
* final usage; its return value (USD) is stored on the telemetry record's
|
|
36
|
+
* `costUsd`. Return undefined to leave a call uncosted. Pricing tables live in
|
|
37
|
+
* the app, not the library — this is only the seam to attach one.
|
|
38
|
+
*/
|
|
39
|
+
pricing?(info: {
|
|
40
|
+
provider: string;
|
|
41
|
+
model: string;
|
|
42
|
+
usage: Usage;
|
|
43
|
+
}): number | undefined;
|
|
20
44
|
}
|
|
45
|
+
/** Test seam: reset the once-per-process startup notice. */
|
|
46
|
+
export declare function resetPolicyWarningForTests(): void;
|
|
21
47
|
export declare class AIHandler {
|
|
22
48
|
private opts;
|
|
23
49
|
private active;
|
|
@@ -28,11 +54,45 @@ export declare class AIHandler {
|
|
|
28
54
|
constructor(opts: HandlerOptions);
|
|
29
55
|
chat(conn: Connection, req: ChatRequest): Promise<ChatResult>;
|
|
30
56
|
stream(conn: Connection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
57
|
+
/**
|
|
58
|
+
* The shared pre-call preamble: policy check → key resolution → `beforeCall`
|
|
59
|
+
* hook. Every entry point (`stream`, `embed`, `listModels`/`testConnection`)
|
|
60
|
+
* runs exactly this sequence, so it lives once. Failures are *returned*, not
|
|
61
|
+
* recorded — each caller owns its own telemetry shape (`recordFailure` for
|
|
62
|
+
* chat/probe, `recordEmbed` for embeddings) and records the returned error.
|
|
63
|
+
*/
|
|
64
|
+
private preflight;
|
|
65
|
+
/** Record a failed call and emit the single redacted `error` event for it. */
|
|
66
|
+
private failStream;
|
|
31
67
|
listModels(conn: Connection): Promise<ModelInfo[]>;
|
|
32
68
|
testConnection(conn: Connection): Promise<{
|
|
33
69
|
ok: boolean;
|
|
34
70
|
message: string;
|
|
35
71
|
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Warm a connection so the first real call doesn't pay cold DNS/TLS and
|
|
74
|
+
* (for a local daemon) process spin-up on the hot path. Runs a lightweight
|
|
75
|
+
* health probe and never throws — call it once at boot. Combined with a CJS
|
|
76
|
+
* `require` path it removes the cold-start "offline" false negative.
|
|
77
|
+
*/
|
|
78
|
+
prewarm(conn: Connection): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Chat that returns typed, validated output. Requests JSON mode, extracts the
|
|
81
|
+
* JSON from the reply, and validates it against any Standard Schema validator
|
|
82
|
+
* (Zod / Valibot / ArkType). On a validation miss it performs exactly one
|
|
83
|
+
* corrective retry that shows the model its error, then throws
|
|
84
|
+
* `invalid_response` if still invalid. No schema library is bundled — the
|
|
85
|
+
* caller supplies the schema.
|
|
86
|
+
*/
|
|
87
|
+
chatParsed<T>(conn: Connection, req: ChatRequest, schema: StandardSchemaV1<T>): Promise<ParsedResult<T>>;
|
|
88
|
+
/**
|
|
89
|
+
* Produce embeddings through the same governed pipeline as chat: policy check,
|
|
90
|
+
* key resolution, `beforeCall` hook, concurrency, and exactly one redacted
|
|
91
|
+
* telemetry record. Throws a typed `invalid_request` error for providers whose
|
|
92
|
+
* adapter has no `embed` (Anthropic, Google) rather than failing silently.
|
|
93
|
+
*/
|
|
94
|
+
embed(conn: Connection, req: EmbedRequest): Promise<EmbedResult>;
|
|
95
|
+
private recordEmbed;
|
|
36
96
|
private runProbe;
|
|
37
97
|
private resolveConnection;
|
|
38
98
|
private acquire;
|
package/dist/handler.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { adapterFor } from './adapters/index.js';
|
|
2
2
|
import { applyAuth, profileFor } from './adapters/profiles.js';
|
|
3
3
|
import { AIError, fromUnknown } from './errors.js';
|
|
4
|
+
import { extractJson } from './json.js';
|
|
4
5
|
import { allowAllPolicy } from './policy.js';
|
|
5
6
|
import { SessionRedactor } from './redact.js';
|
|
6
7
|
import { promptChars, sleep } from './util.js';
|
|
8
|
+
/**
|
|
9
|
+
* A *startup* notice, so it fires once per process rather than once per handler
|
|
10
|
+
* — an app that builds a handler per request would otherwise flood its logs
|
|
11
|
+
* with a message that says the same thing every time.
|
|
12
|
+
*/
|
|
13
|
+
let warnedNoPolicy = false;
|
|
14
|
+
/** Test seam: reset the once-per-process startup notice. */
|
|
15
|
+
export function resetPolicyWarningForTests() {
|
|
16
|
+
warnedNoPolicy = false;
|
|
17
|
+
}
|
|
7
18
|
export class AIHandler {
|
|
8
19
|
opts;
|
|
9
20
|
active = 0;
|
|
@@ -14,6 +25,15 @@ export class AIHandler {
|
|
|
14
25
|
constructor(opts) {
|
|
15
26
|
this.opts = opts;
|
|
16
27
|
this.policy = opts.policy ?? allowAllPolicy();
|
|
28
|
+
// `allowAllPolicy()` stays the default — a library must not decide an app's
|
|
29
|
+
// governance — but "no policy configured" should be visible at runtime, not
|
|
30
|
+
// only in a README paragraph. Passing an explicit policy (including
|
|
31
|
+
// `allowAllPolicy()`) is the opt-out.
|
|
32
|
+
if (!opts.policy && !opts.silencePolicyWarning && !warnedNoPolicy) {
|
|
33
|
+
warnedNoPolicy = true;
|
|
34
|
+
globalThis.console?.warn?.('[ai-nugget] No GovernancePolicy configured — every provider/model is allowed. ' +
|
|
35
|
+
'Pass `policy` (e.g. allowlistPolicy(...)), or `policy: allowAllPolicy()` / `silencePolicyWarning: true` to accept this deliberately.');
|
|
36
|
+
}
|
|
17
37
|
}
|
|
18
38
|
async chat(conn, req) {
|
|
19
39
|
let result;
|
|
@@ -31,42 +51,13 @@ export class AIHandler {
|
|
|
31
51
|
const callId = createCallId();
|
|
32
52
|
const startedAt = Date.now();
|
|
33
53
|
const adapter = adapterFor(conn.provider, conn.baseUrl);
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let resolved;
|
|
42
|
-
try {
|
|
43
|
-
resolved = await this.resolveConnection(conn);
|
|
44
|
-
}
|
|
45
|
-
catch (errorValue) {
|
|
46
|
-
// Key resolution failures (missing/locked/denied) are a recorded outcome,
|
|
47
|
-
// not an uncaught throw — the traceability contract covers them too.
|
|
48
|
-
const error = fromUnknown(errorValue, conn.provider);
|
|
49
|
-
await this.recordFailure(callId, conn, req, startedAt, 1, error);
|
|
50
|
-
yield { type: 'error', error: this.redactedError(error) };
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const info = { callId, connection: conn, provider: conn.provider, model: req.model, metadata: req.metadata };
|
|
54
|
-
try {
|
|
55
|
-
if (await this.opts.hooks?.beforeCall?.(info) === 'deny') {
|
|
56
|
-
const error = new AIError('Call denied by beforeCall hook', { kind: 'policy_blocked', retryable: false, provider: conn.provider });
|
|
57
|
-
await this.recordFailure(callId, conn, req, startedAt, 1, error);
|
|
58
|
-
yield { type: 'error', error: this.redactedError(error) };
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch (errorValue) {
|
|
63
|
-
// A throwing beforeCall hook is a recorded, redacted outcome too — not
|
|
64
|
-
// an uncaught throw that skips telemetry and the redaction guarantee.
|
|
65
|
-
const error = fromUnknown(errorValue, conn.provider);
|
|
66
|
-
await this.recordFailure(callId, conn, req, startedAt, 1, error);
|
|
67
|
-
yield { type: 'error', error: this.redactedError(error) };
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
54
|
+
const preflight = await this.preflight(callId, conn, req.model, req.metadata);
|
|
55
|
+
if (!preflight.ok)
|
|
56
|
+
return yield* this.failStream(callId, conn, req, startedAt, 1, preflight.error);
|
|
57
|
+
// One idempotency key per logical call, reused across every retry attempt,
|
|
58
|
+
// so a provider that supports it collapses a re-sent generation instead of
|
|
59
|
+
// billing it twice (see `supportsIdempotencyKey` in the profile table).
|
|
60
|
+
const resolved = withIdempotencyKey(preflight.resolved, callId);
|
|
70
61
|
let acquired = false;
|
|
71
62
|
let recorded = false;
|
|
72
63
|
let attempt = 0;
|
|
@@ -101,11 +92,11 @@ export class AIHandler {
|
|
|
101
92
|
}
|
|
102
93
|
catch (errorValue) {
|
|
103
94
|
const error = fromUnknown(errorValue, conn.provider);
|
|
95
|
+
// `emittedOutput` stops retries once any byte has reached the caller:
|
|
96
|
+
// never silently re-run a partially delivered answer.
|
|
104
97
|
if (!error.retryable || attempt >= maxAttempts || req.signal?.aborted || emittedOutput) {
|
|
105
|
-
await this.recordFailure(callId, conn, req, startedAt, attempt, error);
|
|
106
98
|
recorded = true;
|
|
107
|
-
yield
|
|
108
|
-
return;
|
|
99
|
+
return yield* this.failStream(callId, conn, req, startedAt, attempt, error);
|
|
109
100
|
}
|
|
110
101
|
const delayMs = this.retryDelay(error, attempt);
|
|
111
102
|
yield { type: 'retry', attempt, reason: error.kind, delayMs };
|
|
@@ -113,21 +104,15 @@ export class AIHandler {
|
|
|
113
104
|
await sleep(delayMs, req.signal);
|
|
114
105
|
}
|
|
115
106
|
catch (sleepError) {
|
|
116
|
-
const canceled = fromUnknown(sleepError, conn.provider);
|
|
117
|
-
await this.recordFailure(callId, conn, req, startedAt, attempt, canceled);
|
|
118
107
|
recorded = true;
|
|
119
|
-
yield
|
|
120
|
-
return;
|
|
108
|
+
return yield* this.failStream(callId, conn, req, startedAt, attempt, fromUnknown(sleepError, conn.provider));
|
|
121
109
|
}
|
|
122
110
|
}
|
|
123
111
|
}
|
|
124
112
|
}
|
|
125
113
|
catch (errorValue) {
|
|
126
|
-
const error = fromUnknown(errorValue, conn.provider);
|
|
127
|
-
await this.recordFailure(callId, conn, req, startedAt, Math.max(1, attempt), error);
|
|
128
114
|
recorded = true;
|
|
129
|
-
yield
|
|
130
|
-
return;
|
|
115
|
+
return yield* this.failStream(callId, conn, req, startedAt, Math.max(1, attempt), fromUnknown(errorValue, conn.provider));
|
|
131
116
|
}
|
|
132
117
|
finally {
|
|
133
118
|
if (acquired)
|
|
@@ -137,6 +122,52 @@ export class AIHandler {
|
|
|
137
122
|
}
|
|
138
123
|
}
|
|
139
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The shared pre-call preamble: policy check → key resolution → `beforeCall`
|
|
127
|
+
* hook. Every entry point (`stream`, `embed`, `listModels`/`testConnection`)
|
|
128
|
+
* runs exactly this sequence, so it lives once. Failures are *returned*, not
|
|
129
|
+
* recorded — each caller owns its own telemetry shape (`recordFailure` for
|
|
130
|
+
* chat/probe, `recordEmbed` for embeddings) and records the returned error.
|
|
131
|
+
*/
|
|
132
|
+
async preflight(callId, conn, model, metadata) {
|
|
133
|
+
const policyResult = this.policy.checkModel(conn.provider, model);
|
|
134
|
+
if (!policyResult.allowed) {
|
|
135
|
+
return { ok: false, error: new AIError(policyResult.reason, { kind: 'policy_blocked', retryable: false, provider: conn.provider }) };
|
|
136
|
+
}
|
|
137
|
+
let resolved;
|
|
138
|
+
try {
|
|
139
|
+
resolved = await this.resolveConnection(conn);
|
|
140
|
+
}
|
|
141
|
+
catch (errorValue) {
|
|
142
|
+
// Key resolution failures (missing/locked/denied) are a recorded outcome,
|
|
143
|
+
// not an uncaught throw — the traceability contract covers them too.
|
|
144
|
+
return { ok: false, error: fromUnknown(errorValue, conn.provider) };
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const info = {
|
|
148
|
+
callId,
|
|
149
|
+
connection: safeConnection(conn),
|
|
150
|
+
provider: conn.provider,
|
|
151
|
+
model,
|
|
152
|
+
metadata,
|
|
153
|
+
resolved: safeResolved(resolved),
|
|
154
|
+
};
|
|
155
|
+
if (await this.opts.hooks?.beforeCall?.(info) === 'deny') {
|
|
156
|
+
return { ok: false, error: new AIError('Call denied by beforeCall hook', { kind: 'policy_blocked', retryable: false, provider: conn.provider }) };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (errorValue) {
|
|
160
|
+
// A throwing beforeCall hook is a recorded, redacted outcome too — not
|
|
161
|
+
// an uncaught throw that skips telemetry and the redaction guarantee.
|
|
162
|
+
return { ok: false, error: fromUnknown(errorValue, conn.provider) };
|
|
163
|
+
}
|
|
164
|
+
return { ok: true, resolved };
|
|
165
|
+
}
|
|
166
|
+
/** Record a failed call and emit the single redacted `error` event for it. */
|
|
167
|
+
async *failStream(callId, conn, req, startedAt, attempts, error) {
|
|
168
|
+
await this.recordFailure(callId, conn, req, startedAt, attempts, error);
|
|
169
|
+
yield { type: 'error', error: this.redactedError(error) };
|
|
170
|
+
}
|
|
140
171
|
async listModels(conn) {
|
|
141
172
|
return this.runProbe(conn, '__listModels__', async (resolved) => {
|
|
142
173
|
const adapter = adapterFor(conn.provider, conn.baseUrl);
|
|
@@ -161,23 +192,111 @@ export class AIHandler {
|
|
|
161
192
|
return { ok: false, message: error instanceof Error ? this.redact(error.message) : 'Connection failed' };
|
|
162
193
|
}
|
|
163
194
|
}
|
|
164
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Warm a connection so the first real call doesn't pay cold DNS/TLS and
|
|
197
|
+
* (for a local daemon) process spin-up on the hot path. Runs a lightweight
|
|
198
|
+
* health probe and never throws — call it once at boot. Combined with a CJS
|
|
199
|
+
* `require` path it removes the cold-start "offline" false negative.
|
|
200
|
+
*/
|
|
201
|
+
async prewarm(conn) {
|
|
202
|
+
try {
|
|
203
|
+
await this.testConnection(conn);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Warm-up only: a failure here just means the first real call pays the cost.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Chat that returns typed, validated output. Requests JSON mode, extracts the
|
|
211
|
+
* JSON from the reply, and validates it against any Standard Schema validator
|
|
212
|
+
* (Zod / Valibot / ArkType). On a validation miss it performs exactly one
|
|
213
|
+
* corrective retry that shows the model its error, then throws
|
|
214
|
+
* `invalid_response` if still invalid. No schema library is bundled — the
|
|
215
|
+
* caller supplies the schema.
|
|
216
|
+
*/
|
|
217
|
+
async chatParsed(conn, req, schema) {
|
|
218
|
+
const base = { ...req, responseFormat: req.responseFormat ?? { type: 'json' } };
|
|
219
|
+
let result = await this.chat(conn, base);
|
|
220
|
+
let validated = await validateWithSchema(schema, result.text);
|
|
221
|
+
if (validated.ok)
|
|
222
|
+
return { data: validated.value, result };
|
|
223
|
+
const retry = {
|
|
224
|
+
...base,
|
|
225
|
+
messages: [
|
|
226
|
+
...base.messages,
|
|
227
|
+
{ role: 'assistant', content: result.text },
|
|
228
|
+
{ role: 'user', content: `That response did not match the required schema (${validated.message}). Reply again with ONLY a valid JSON value that matches it.` },
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
result = await this.chat(conn, retry);
|
|
232
|
+
validated = await validateWithSchema(schema, result.text);
|
|
233
|
+
if (validated.ok)
|
|
234
|
+
return { data: validated.value, result };
|
|
235
|
+
throw new AIError(`Model output failed schema validation: ${validated.message}`, { kind: 'invalid_response', provider: conn.provider });
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Produce embeddings through the same governed pipeline as chat: policy check,
|
|
239
|
+
* key resolution, `beforeCall` hook, concurrency, and exactly one redacted
|
|
240
|
+
* telemetry record. Throws a typed `invalid_request` error for providers whose
|
|
241
|
+
* adapter has no `embed` (Anthropic, Google) rather than failing silently.
|
|
242
|
+
*/
|
|
243
|
+
async embed(conn, req) {
|
|
165
244
|
const callId = createCallId();
|
|
166
245
|
const startedAt = Date.now();
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const error = new AIError(policyResult.reason, { kind: 'policy_blocked', retryable: false, provider: conn.provider });
|
|
171
|
-
await this.recordFailure(callId, conn, req, startedAt, 1, error);
|
|
246
|
+
const inputChars = (Array.isArray(req.input) ? req.input : [req.input]).reduce((sum, text) => sum + text.length, 0);
|
|
247
|
+
const fail = async (error) => {
|
|
248
|
+
await this.recordEmbed(callId, conn, req, startedAt, { estimated: true }, inputChars, error);
|
|
172
249
|
throw this.redactedError(error);
|
|
250
|
+
};
|
|
251
|
+
const preflight = await this.preflight(callId, conn, req.model, req.metadata);
|
|
252
|
+
if (!preflight.ok)
|
|
253
|
+
return fail(preflight.error);
|
|
254
|
+
const resolved = preflight.resolved;
|
|
255
|
+
const adapter = adapterFor(conn.provider, conn.baseUrl);
|
|
256
|
+
if (!adapter.embed) {
|
|
257
|
+
return fail(new AIError(`Provider ${conn.provider} does not support embeddings`, { kind: 'invalid_request', retryable: false, provider: conn.provider }));
|
|
173
258
|
}
|
|
174
|
-
|
|
259
|
+
await this.acquire(req.signal);
|
|
175
260
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
261
|
+
const result = await adapter.embed(resolved, req);
|
|
262
|
+
await this.recordEmbed(callId, conn, req, startedAt, result.usage, inputChars);
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
catch (errorValue) {
|
|
266
|
+
return fail(fromUnknown(errorValue, conn.provider));
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
this.release();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async recordEmbed(callId, conn, req, startedAt, usage, inputChars, error) {
|
|
273
|
+
const costUsd = error ? undefined : this.opts.pricing?.({ provider: conn.provider, model: req.model, usage });
|
|
274
|
+
await this.record({
|
|
275
|
+
callId,
|
|
276
|
+
connectionId: conn.id,
|
|
277
|
+
provider: conn.provider,
|
|
278
|
+
model: req.model,
|
|
279
|
+
startedAt,
|
|
280
|
+
timing: { firstTokenMs: null, totalMs: Date.now() - startedAt },
|
|
281
|
+
usage,
|
|
282
|
+
finishReason: error ? (error.kind === 'canceled' ? 'canceled' : 'error') : 'stop',
|
|
283
|
+
...(error ? { error: { kind: error.kind, status: error.status, message: error.message } } : {}),
|
|
284
|
+
attempts: 1,
|
|
285
|
+
metadata: { ...req.metadata, operation: '__embed__' },
|
|
286
|
+
promptChars: inputChars,
|
|
287
|
+
responseChars: 0,
|
|
288
|
+
...(costUsd !== undefined ? { costUsd } : {}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async runProbe(conn, operation, action) {
|
|
292
|
+
const callId = createCallId();
|
|
293
|
+
const startedAt = Date.now();
|
|
294
|
+
const req = { model: operation, messages: [], metadata: { operation } };
|
|
295
|
+
try {
|
|
296
|
+
const preflight = await this.preflight(callId, conn, operation, req.metadata);
|
|
297
|
+
if (!preflight.ok)
|
|
298
|
+
throw preflight.error;
|
|
299
|
+
const resolved = preflight.resolved;
|
|
181
300
|
await this.acquire(undefined);
|
|
182
301
|
try {
|
|
183
302
|
const value = await action(resolved);
|
|
@@ -241,14 +360,20 @@ export class AIHandler {
|
|
|
241
360
|
}
|
|
242
361
|
this.active += 1;
|
|
243
362
|
const minInterval = this.opts.limits?.minIntervalMs ?? 0;
|
|
244
|
-
const
|
|
245
|
-
|
|
363
|
+
const previousLastStarted = this.lastStarted;
|
|
364
|
+
const wait = Math.max(0, previousLastStarted + minInterval - Date.now());
|
|
365
|
+
const reserved = Date.now() + wait;
|
|
366
|
+
this.lastStarted = reserved;
|
|
246
367
|
try {
|
|
247
368
|
if (wait)
|
|
248
369
|
await sleep(wait, signal);
|
|
249
370
|
}
|
|
250
371
|
catch (error) {
|
|
251
372
|
this.release();
|
|
373
|
+
// Only undo our own reservation — a later caller may have already
|
|
374
|
+
// paced itself off `reserved` and must keep that spacing.
|
|
375
|
+
if (this.lastStarted === reserved)
|
|
376
|
+
this.lastStarted = previousLastStarted;
|
|
252
377
|
throw error;
|
|
253
378
|
}
|
|
254
379
|
}
|
|
@@ -272,6 +397,7 @@ export class AIHandler {
|
|
|
272
397
|
return Math.round(exponential * (0.75 + Math.random() * 0.5));
|
|
273
398
|
}
|
|
274
399
|
async recordSuccess(callId, conn, req, startedAt, attempts, result) {
|
|
400
|
+
const costUsd = this.opts.pricing?.({ provider: conn.provider, model: req.model, usage: result.usage });
|
|
275
401
|
await this.record({
|
|
276
402
|
callId,
|
|
277
403
|
connectionId: conn.id,
|
|
@@ -285,6 +411,7 @@ export class AIHandler {
|
|
|
285
411
|
metadata: req.metadata,
|
|
286
412
|
promptChars: promptChars(req.messages),
|
|
287
413
|
responseChars: result.text.length,
|
|
414
|
+
...(costUsd !== undefined ? { costUsd } : {}),
|
|
288
415
|
});
|
|
289
416
|
}
|
|
290
417
|
async recordFailure(callId, conn, req, startedAt, attempts, error) {
|
|
@@ -365,6 +492,65 @@ function redactMetadata(metadata, redact) {
|
|
|
365
492
|
function trimSlash(value) {
|
|
366
493
|
return value.replace(/\/+$/, '');
|
|
367
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Drops the resolved API key *and* neutralizes the auth headers built from it,
|
|
497
|
+
* so a `beforeCall` hook can inspect the endpoint but never the secret.
|
|
498
|
+
* `keyRef` is scrubbed too — a `{ kind: 'literal' }` ref carries the plaintext
|
|
499
|
+
* key, and hooks routinely log the whole object.
|
|
500
|
+
*/
|
|
501
|
+
function safeResolved(resolved) {
|
|
502
|
+
const { apiKey: _apiKey, headers, ...rest } = resolved;
|
|
503
|
+
return { ...rest, ...safeKeyRef(resolved), headers: maskAuthHeaders(headers) };
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* The caller's own `Connection`, with any plaintext `keyRef` masked. Hooks get
|
|
507
|
+
* the connection so they can key off `id`/`provider`/`baseUrl`; they must never
|
|
508
|
+
* be handed the secret, since logging `info.connection` for audit is the
|
|
509
|
+
* obvious thing to do and would otherwise ship the key in clear text.
|
|
510
|
+
*/
|
|
511
|
+
function safeConnection(conn) {
|
|
512
|
+
return { ...conn, ...safeKeyRef(conn) };
|
|
513
|
+
}
|
|
514
|
+
function safeKeyRef(conn) {
|
|
515
|
+
if (conn.keyRef?.kind !== 'literal')
|
|
516
|
+
return conn.keyRef === undefined ? {} : { keyRef: conn.keyRef };
|
|
517
|
+
return { keyRef: { kind: 'literal', value: REDACTED } };
|
|
518
|
+
}
|
|
519
|
+
const REDACTED = '[REDACTED]';
|
|
520
|
+
/** Header names whose value is derived from the API key by {@link applyAuth}. */
|
|
521
|
+
const AUTH_HEADERS = new Set(['authorization', 'x-api-key', 'x-goog-api-key', 'api-key']);
|
|
522
|
+
function maskAuthHeaders(headers) {
|
|
523
|
+
const masked = {};
|
|
524
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
525
|
+
masked[name] = AUTH_HEADERS.has(name.toLowerCase()) ? REDACTED : value;
|
|
526
|
+
}
|
|
527
|
+
return masked;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Attach a per-call `Idempotency-Key` for providers that honor one, so a retry
|
|
531
|
+
* after a dropped connection can't be billed as a second generation. Only set
|
|
532
|
+
* where the profile declares support, and never overrides a caller-supplied
|
|
533
|
+
* header.
|
|
534
|
+
*/
|
|
535
|
+
function withIdempotencyKey(resolved, callId) {
|
|
536
|
+
const profile = profileFor(resolved.provider, resolved.baseUrl);
|
|
537
|
+
if (!profile.quirks?.supportsIdempotencyKey)
|
|
538
|
+
return resolved;
|
|
539
|
+
if (Object.keys(resolved.headers).some((name) => name.toLowerCase() === 'idempotency-key'))
|
|
540
|
+
return resolved;
|
|
541
|
+
return { ...resolved, headers: { ...resolved.headers, 'idempotency-key': callId } };
|
|
542
|
+
}
|
|
543
|
+
async function validateWithSchema(schema, text) {
|
|
544
|
+
const value = extractJson(text);
|
|
545
|
+
if (value === null)
|
|
546
|
+
return { ok: false, message: 'no JSON found in model output' };
|
|
547
|
+
const result = await schema['~standard'].validate(value);
|
|
548
|
+
if (result.issues) {
|
|
549
|
+
const message = result.issues.map((issue) => issue.message).join('; ');
|
|
550
|
+
return { ok: false, message: message || 'validation failed' };
|
|
551
|
+
}
|
|
552
|
+
return { ok: true, value: result.value };
|
|
553
|
+
}
|
|
368
554
|
function createCallId() {
|
|
369
555
|
return globalThis.crypto?.randomUUID?.() ?? `call_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
370
556
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/keys.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { globalEnv } from './util.js';
|
|
1
2
|
export function parseKeyRef(value) {
|
|
2
3
|
const trimmed = value?.trim();
|
|
3
4
|
if (!trimmed)
|
|
@@ -71,8 +72,4 @@ export function chainKeySources(...sources) {
|
|
|
71
72
|
},
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
|
-
function globalEnv() {
|
|
75
|
-
const maybeProcess = globalThis;
|
|
76
|
-
return maybeProcess.process?.env ?? {};
|
|
77
|
-
}
|
|
78
75
|
//# sourceMappingURL=keys.js.map
|
package/dist/redact.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import type { Redactor } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Prefixed secret formats. Each pattern is anchored on a vendor prefix that is
|
|
4
|
+
* distinctive enough that a match is a secret, not a coincidence.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SECRET_PATTERNS: RegExp[];
|
|
7
|
+
/**
|
|
8
|
+
* Unprefixed/opaque secrets — Azure OpenAI `api-key` values, AWS *secret*
|
|
9
|
+
* access keys, generic bearer/session tokens — carry no distinctive prefix, so
|
|
10
|
+
* matching them on shape alone would redact commit SHAs, content hashes, and
|
|
11
|
+
* base64 payloads. These patterns instead anchor on the *label* that precedes
|
|
12
|
+
* the value (`api-key: …`, `"aws_secret_access_key": "…"`, `password=…`) and
|
|
13
|
+
* redact only the value, keeping the label readable in logs.
|
|
14
|
+
*
|
|
15
|
+
* Exact-match registration (`SessionRedactor.addSecret`, which the handler does
|
|
16
|
+
* for every resolved key) remains the only guaranteed catch for a bare,
|
|
17
|
+
* unlabeled, unprefixed secret.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LABELED_SECRET_PATTERNS: RegExp[];
|
|
2
20
|
export declare function createDefaultRedactor(extraSecrets?: Iterable<string>): Redactor;
|
|
3
21
|
export declare class SessionRedactor implements Redactor {
|
|
4
22
|
private secrets;
|