@hue-run/sdk 0.1.4 → 0.2.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/ENVIRONMENTS.md +182 -0
- package/EVALUATIONS.md +12 -0
- package/README.md +204 -21
- package/dist/ai-sdk.d.ts +9 -1
- package/dist/ai-sdk.js +37 -2
- package/dist/client.d.ts +130 -5
- package/dist/client.js +518 -110
- package/dist/config.d.ts +11 -2
- package/dist/config.js +50 -4
- package/dist/environment/client.d.ts +73 -0
- package/dist/environment/client.js +209 -0
- package/dist/environment/tools.d.ts +30 -0
- package/dist/environment/tools.js +24 -0
- package/dist/environment/types.d.ts +429 -0
- package/dist/environment/types.js +1 -0
- package/dist/environment.d.ts +5 -0
- package/dist/environment.js +2 -0
- package/dist/evals/attempt.d.ts +454 -0
- package/dist/evals/attempt.js +687 -0
- package/dist/evals/client.d.ts +99 -5
- package/dist/evals/client.js +136 -7
- package/dist/evals/environment-evidence.d.ts +6 -0
- package/dist/evals/environment-evidence.js +123 -0
- package/dist/evals/environment-json.d.ts +3 -0
- package/dist/evals/environment-json.js +76 -0
- package/dist/evals/json.d.ts +9 -1
- package/dist/evals/json.js +14 -6
- package/dist/evals/runner.d.ts +61 -2
- package/dist/evals/runner.js +71 -9
- package/dist/evals/scorer-publication.d.ts +2 -0
- package/dist/evals/scorer-publication.js +84 -0
- package/dist/evals/scorers.d.ts +11 -0
- package/dist/evals/scorers.js +56 -5
- package/dist/evals/simulation.d.ts +184 -0
- package/dist/evals/simulation.js +603 -0
- package/dist/evals/types.d.ts +304 -0
- package/dist/evals.d.ts +5 -1
- package/dist/evals.js +3 -1
- package/dist/experimental-telemetry.d.ts +8 -0
- package/dist/experimental-telemetry.js +13 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/managed.d.ts +51 -1
- package/dist/managed.js +11 -1
- package/dist/privacy.d.ts +2 -0
- package/dist/privacy.js +54 -21
- package/dist/receipt.d.ts +12 -1
- package/dist/receipt.js +10 -1
- package/dist/safety.d.ts +7 -0
- package/dist/safety.js +179 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +200 -0
- package/dist/transport.d.ts +46 -8
- package/dist/transport.js +266 -48
- package/dist/types.d.ts +167 -8
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +51 -15
package/dist/managed.d.ts
CHANGED
|
@@ -1,65 +1,115 @@
|
|
|
1
1
|
import { type Tracer } from "@opentelemetry/api";
|
|
2
2
|
import type { JsonValue } from "./types.js";
|
|
3
|
+
/** A declared input file the handler downloads and verifies before calling the target. */
|
|
3
4
|
export interface ManagedInputFile {
|
|
5
|
+
/** Hue artifact ID. */
|
|
4
6
|
artifactId: string;
|
|
7
|
+
/** File name without path separators. */
|
|
5
8
|
filename: string;
|
|
9
|
+
/** MIME type. */
|
|
6
10
|
contentType: string;
|
|
11
|
+
/** Exact size in bytes; verified after download. */
|
|
7
12
|
byteSize: number;
|
|
13
|
+
/** Lowercase hex SHA-256 of the bytes; verified after download. */
|
|
8
14
|
sha256: string;
|
|
15
|
+
/** Caller-defined role of the file in the case, up to 64 characters. */
|
|
9
16
|
role: string;
|
|
10
17
|
}
|
|
18
|
+
/** The request body Hue POSTs to a managed target (protocol version 1). */
|
|
11
19
|
export interface ManagedInvocation {
|
|
20
|
+
/** Always 1. */
|
|
12
21
|
protocolVersion: 1;
|
|
22
|
+
/** Execution to claim and complete. */
|
|
13
23
|
executionId: string;
|
|
24
|
+
/** Attempt number, starting at 1. */
|
|
14
25
|
attempt: number;
|
|
26
|
+
/** Frozen case inputs. */
|
|
15
27
|
input: JsonValue;
|
|
28
|
+
/** Experiment configuration. */
|
|
16
29
|
config: JsonValue;
|
|
30
|
+
/** Up to 16 declared input files. */
|
|
17
31
|
inputFiles: ManagedInputFile[];
|
|
32
|
+
/** RFC 3339 UTC deadline for the whole invocation. */
|
|
18
33
|
deadline: string;
|
|
34
|
+
/** Sampled W3C `traceparent` the target span must continue. */
|
|
19
35
|
traceparent: string;
|
|
20
36
|
}
|
|
37
|
+
/** What the target callback receives after the claim and file verification succeed. */
|
|
21
38
|
export interface ManagedTargetContext {
|
|
39
|
+
/** Execution being run. */
|
|
22
40
|
executionId: string;
|
|
41
|
+
/** Attempt number, starting at 1. */
|
|
23
42
|
attempt: number;
|
|
43
|
+
/** Frozen case inputs. */
|
|
24
44
|
input: JsonValue;
|
|
45
|
+
/** Experiment configuration. */
|
|
25
46
|
config: JsonValue;
|
|
47
|
+
/** Verified input files with their bytes. */
|
|
26
48
|
inputFiles: Array<ManagedInputFile & {
|
|
49
|
+
/** Verified file contents. */
|
|
27
50
|
data: Uint8Array;
|
|
28
51
|
}>;
|
|
52
|
+
/** Aborts at the execution deadline; the target must honor it. */
|
|
29
53
|
signal: AbortSignal;
|
|
54
|
+
/** Trace ID of the `ai.managed_target` span the target runs under. */
|
|
30
55
|
traceId: string;
|
|
31
56
|
}
|
|
57
|
+
/** A file the target returns for upload. */
|
|
32
58
|
export interface ManagedOutputFile {
|
|
59
|
+
/** File name without path separators. */
|
|
33
60
|
filename: string;
|
|
61
|
+
/** MIME type. */
|
|
34
62
|
contentType: string;
|
|
63
|
+
/** Exact bytes to store, at most 25 MiB. */
|
|
35
64
|
data: Uint8Array;
|
|
65
|
+
/** Marks the single primary artifact of the outcome. */
|
|
36
66
|
primary?: boolean;
|
|
37
67
|
}
|
|
68
|
+
/** The target callback's return value. */
|
|
38
69
|
export interface ManagedTargetResult {
|
|
70
|
+
/** Outcome state; `succeeded` by default. */
|
|
39
71
|
state?: "succeeded" | "error" | "cancelled";
|
|
72
|
+
/** Output JSON; omitted means unavailable, `null` is a present output. */
|
|
40
73
|
output?: JsonValue;
|
|
41
74
|
/** Public, safe error summary. Never include provider errors, credentials or stacks. */
|
|
42
75
|
error?: {
|
|
76
|
+
/** Stable lowercase type such as `target_error`. */
|
|
43
77
|
type: string;
|
|
78
|
+
/** Bounded public message. */
|
|
44
79
|
message?: string;
|
|
45
80
|
};
|
|
81
|
+
/** Up to 16 files, 64 MiB in total. */
|
|
46
82
|
files?: ManagedOutputFile[];
|
|
83
|
+
/** Provider-reported token usage, when known. */
|
|
47
84
|
usage?: {
|
|
85
|
+
/** Prompt tokens. */
|
|
48
86
|
inputTokens?: number;
|
|
87
|
+
/** Completion tokens. */
|
|
49
88
|
outputTokens?: number;
|
|
50
89
|
};
|
|
51
90
|
}
|
|
91
|
+
/** Options for {@link createManagedTargetHandler}. */
|
|
52
92
|
export interface ManagedTargetOptions {
|
|
93
|
+
/** Dedicated shared secret Hue presents as a Bearer token; validated before any network access. */
|
|
53
94
|
machineCredential: string;
|
|
95
|
+
/** Hue origin for invocation callbacks, `https://app.hue.run` by default; never taken from a request. */
|
|
54
96
|
baseUrl?: string;
|
|
97
|
+
/** The application's existing agent function; called at most once per invocation. */
|
|
55
98
|
target: (invocation: ManagedTargetContext) => Promise<ManagedTargetResult>;
|
|
56
99
|
/** Flush the application's existing trace AND log pipelines; do not shut them down. */
|
|
57
100
|
flushTelemetry: () => Promise<unknown>;
|
|
101
|
+
/** Tracer that records under the incoming trace context; defaults to the global tracer, which must be configured. */
|
|
58
102
|
tracer?: Tracer;
|
|
59
103
|
/** Reserve finalization time inside the host's request limit. Default 90 seconds. */
|
|
60
104
|
maxExecutionMillis?: number;
|
|
61
105
|
/** Upload/checkpoint/flush grace after execution deadline, at most 30 seconds. */
|
|
62
106
|
finalizationMillis?: number;
|
|
63
107
|
}
|
|
64
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Creates a machine-authenticated, framework-neutral POST handler that claims the invocation,
|
|
110
|
+
* verifies input files, runs `target` under the incoming trace context, uploads output files,
|
|
111
|
+
* saves the outcome and acknowledges telemetry. It never retries the target.
|
|
112
|
+
*
|
|
113
|
+
* @throws TypeError for an invalid credential, origin, budget or missing callbacks at construction.
|
|
114
|
+
*/
|
|
65
115
|
export declare function createManagedTargetHandler(options: ManagedTargetOptions): (request: Request) => Promise<Response>;
|
package/dist/managed.js
CHANGED
|
@@ -21,7 +21,13 @@ const STATES = new Set([
|
|
|
21
21
|
"superseded",
|
|
22
22
|
]);
|
|
23
23
|
const propagator = new W3CTraceContextPropagator();
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Creates a machine-authenticated, framework-neutral POST handler that claims the invocation,
|
|
26
|
+
* verifies input files, runs `target` under the incoming trace context, uploads output files,
|
|
27
|
+
* saves the outcome and acknowledges telemetry. It never retries the target.
|
|
28
|
+
*
|
|
29
|
+
* @throws TypeError for an invalid credential, origin, budget or missing callbacks at construction.
|
|
30
|
+
*/
|
|
25
31
|
export function createManagedTargetHandler(options) {
|
|
26
32
|
token(options.machineCredential);
|
|
27
33
|
const baseUrl = validateOptions({
|
|
@@ -343,6 +349,7 @@ function sha256(data) {
|
|
|
343
349
|
return createHash("sha256").update(data).digest("hex");
|
|
344
350
|
}
|
|
345
351
|
function token(value) {
|
|
352
|
+
// eslint-disable-next-line no-control-regex -- control characters are rejected deliberately
|
|
346
353
|
if (typeof value !== "string" || !value || value.length > 8192 || /[\s\u0000]/u.test(value))
|
|
347
354
|
throw new TypeError("Invalid credential");
|
|
348
355
|
return value;
|
|
@@ -364,6 +371,7 @@ function filename(value) {
|
|
|
364
371
|
if (typeof value !== "string" ||
|
|
365
372
|
!value.trim() ||
|
|
366
373
|
value.length > 255 ||
|
|
374
|
+
// eslint-disable-next-line no-control-regex -- control characters are rejected deliberately
|
|
367
375
|
/[\x00-\x1f\x7f/\\]/u.test(value) ||
|
|
368
376
|
value === "." ||
|
|
369
377
|
value === "..")
|
|
@@ -374,6 +382,7 @@ function shortString(value, maximum = 255) {
|
|
|
374
382
|
if (typeof value !== "string" ||
|
|
375
383
|
!value ||
|
|
376
384
|
value.length > maximum ||
|
|
385
|
+
// eslint-disable-next-line no-control-regex -- control characters are rejected deliberately
|
|
377
386
|
/[\x00-\x1f\x7f]/u.test(value))
|
|
378
387
|
throw new TypeError("Invalid string");
|
|
379
388
|
return value;
|
|
@@ -466,6 +475,7 @@ function validateResult(result) {
|
|
|
466
475
|
throw new TypeError("Invalid output files");
|
|
467
476
|
}
|
|
468
477
|
function safeUploadUrl(value) {
|
|
478
|
+
// eslint-disable-next-line no-control-regex -- control characters are rejected deliberately
|
|
469
479
|
if (typeof value !== "string" || value.length > 8192 || /[\x00-\x20\x7f]/u.test(value))
|
|
470
480
|
throw new TypeError("Invalid upload URL");
|
|
471
481
|
const url = new URL(value);
|
package/dist/privacy.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { ReadableSpan } from "@opentelemetry/sdk-trace";
|
|
|
2
2
|
import type { ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
3
3
|
import { type Resource } from "@opentelemetry/resources";
|
|
4
4
|
import type { HueOptions } from "./types.js";
|
|
5
|
+
/** Attribute keys (and their dotted children) removed in metadata-only mode. */
|
|
6
|
+
export declare const contentPrefixes: string[];
|
|
5
7
|
export type ResourceCache = WeakMap<Resource, Resource>;
|
|
6
8
|
export declare function redactSpan(span: ReadableSpan, options: HueOptions, cache: ResourceCache): ReadableSpan;
|
|
7
9
|
export declare function redactLog(log: ReadableLogRecord, options: HueOptions, cache: ResourceCache): ReadableLogRecord;
|
package/dist/privacy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
2
|
-
import { MAX_CONTENT_BYTES } from "./config.js";
|
|
3
|
-
|
|
2
|
+
import { MAX_BODY_BYTES, MAX_CONTENT_BYTES } from "./config.js";
|
|
3
|
+
/** Attribute keys (and their dotted children) removed in metadata-only mode. */
|
|
4
|
+
export const contentPrefixes = [
|
|
4
5
|
"gen_ai.input.messages",
|
|
5
6
|
"gen_ai.output.messages",
|
|
6
7
|
"gen_ai.system_instructions",
|
|
@@ -15,11 +16,25 @@ const contentPrefixes = [
|
|
|
15
16
|
"llm.prompts",
|
|
16
17
|
"llm.completions",
|
|
17
18
|
"llm.invocation_parameters",
|
|
19
|
+
"llm.prompt_template.template",
|
|
20
|
+
"llm.prompt_template.variables",
|
|
21
|
+
"llm.tools",
|
|
22
|
+
"llm.function_call",
|
|
23
|
+
"llm.choices",
|
|
18
24
|
"input.value",
|
|
19
25
|
"output.value",
|
|
26
|
+
"input.images",
|
|
27
|
+
"output.images",
|
|
28
|
+
"retrieval.documents",
|
|
29
|
+
"embedding.embeddings",
|
|
30
|
+
"reranker.query",
|
|
31
|
+
"reranker.input_documents",
|
|
32
|
+
"reranker.output_documents",
|
|
20
33
|
"ai.prompt",
|
|
21
34
|
"ai.response.text",
|
|
22
35
|
"ai.response.object",
|
|
36
|
+
"ai.response.reasoning",
|
|
37
|
+
"ai.response.files",
|
|
23
38
|
"ai.response.toolCalls",
|
|
24
39
|
"ai.response.body",
|
|
25
40
|
"ai.toolCall.args",
|
|
@@ -38,45 +53,62 @@ const contentPrefixes = [
|
|
|
38
53
|
function isContentKey(key) {
|
|
39
54
|
return contentPrefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}.`));
|
|
40
55
|
}
|
|
41
|
-
function redactValue(value, path, options, depth = 0) {
|
|
42
|
-
if (depth > 32)
|
|
56
|
+
function redactValue(value, path, options, budget, depth = 0) {
|
|
57
|
+
if (++budget.nodes > 16384 || depth > 32)
|
|
43
58
|
throw new Error("Telemetry value exceeds the supported nesting limit");
|
|
44
59
|
if (typeof value === "string") {
|
|
45
60
|
const result = options.redact ? options.redact(value, path) : value;
|
|
46
|
-
|
|
61
|
+
// JavaScript can supply an async redactor despite the synchronous contract.
|
|
62
|
+
// Observe its rejection before dropping the invalid record.
|
|
63
|
+
if (result && typeof result === "object")
|
|
64
|
+
void Promise.resolve(result).catch(() => { });
|
|
65
|
+
if (typeof result !== "string" ||
|
|
66
|
+
result.length > MAX_CONTENT_BYTES ||
|
|
67
|
+
!result.isWellFormed() ||
|
|
68
|
+
result.includes("\u0000"))
|
|
47
69
|
throw new Error("Redaction produced unsupported text");
|
|
48
70
|
if (Buffer.byteLength(result) > MAX_CONTENT_BYTES)
|
|
49
71
|
throw new Error("Telemetry text exceeds 256 KiB");
|
|
72
|
+
budget.bytes += Buffer.byteLength(result);
|
|
73
|
+
if (budget.bytes > MAX_BODY_BYTES)
|
|
74
|
+
throw new Error("Redacted record exceeds the content budget");
|
|
50
75
|
return result;
|
|
51
76
|
}
|
|
52
|
-
if (Array.isArray(value))
|
|
53
|
-
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
if (value.length > 16384)
|
|
79
|
+
throw new Error("Telemetry array exceeds the complexity limit");
|
|
80
|
+
return value.map((item, index) => redactValue(item, `${path}.${index}`, options, budget, depth + 1));
|
|
81
|
+
}
|
|
54
82
|
if (value instanceof Uint8Array) {
|
|
55
83
|
if (value.byteLength > MAX_CONTENT_BYTES)
|
|
56
84
|
throw new Error("Telemetry bytes exceed 256 KiB");
|
|
85
|
+
budget.bytes += value.byteLength;
|
|
86
|
+
if (budget.bytes > MAX_BODY_BYTES)
|
|
87
|
+
throw new Error("Redacted record exceeds the content budget");
|
|
57
88
|
return value;
|
|
58
89
|
}
|
|
59
90
|
if (value !== null && typeof value === "object")
|
|
60
91
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
61
92
|
key,
|
|
62
|
-
redactValue(item, `${path}.${key}`, options, depth + 1),
|
|
93
|
+
redactValue(item, `${path}.${key}`, options, budget, depth + 1),
|
|
63
94
|
]));
|
|
64
95
|
return value;
|
|
65
96
|
}
|
|
66
|
-
function attributes(source, options, path) {
|
|
97
|
+
function attributes(source, options, path, budget) {
|
|
67
98
|
return Object.fromEntries(Object.entries(source).flatMap(([key, value]) => !options.captureContent && isContentKey(key)
|
|
68
99
|
? []
|
|
69
|
-
: [[key, redactValue(value, `${path}.${key}`, options)]]));
|
|
100
|
+
: [[key, redactValue(value, `${path}.${key}`, options, budget)]]));
|
|
70
101
|
}
|
|
71
|
-
function redactResource(resource, options, cache) {
|
|
102
|
+
function redactResource(resource, options, cache, budget) {
|
|
72
103
|
let result = cache.get(resource);
|
|
73
104
|
if (!result) {
|
|
74
|
-
result = resourceFromAttributes(attributes(resource.attributes, options, "resource.attributes"), { schemaUrl: resource.schemaUrl });
|
|
105
|
+
result = resourceFromAttributes(attributes(resource.attributes, options, "resource.attributes", budget), { schemaUrl: resource.schemaUrl });
|
|
75
106
|
cache.set(resource, result);
|
|
76
107
|
}
|
|
77
108
|
return result;
|
|
78
109
|
}
|
|
79
110
|
export function redactSpan(span, options, cache) {
|
|
111
|
+
const budget = { bytes: 0, nodes: 0 };
|
|
80
112
|
return {
|
|
81
113
|
name: span.name,
|
|
82
114
|
kind: span.kind,
|
|
@@ -89,22 +121,22 @@ export function redactSpan(span, options, cache) {
|
|
|
89
121
|
status: {
|
|
90
122
|
code: span.status.code,
|
|
91
123
|
...(options.captureContent && span.status.message !== undefined
|
|
92
|
-
? { message: String(redactValue(span.status.message, "status.message", options)) }
|
|
124
|
+
? { message: String(redactValue(span.status.message, "status.message", options, budget)) }
|
|
93
125
|
: {}),
|
|
94
126
|
},
|
|
95
|
-
attributes: attributes(span.attributes, options, "attributes"),
|
|
127
|
+
attributes: attributes(span.attributes, options, "attributes", budget),
|
|
96
128
|
events: span.events
|
|
97
129
|
.filter((event) => options.captureContent ||
|
|
98
130
|
!/^gen_ai\.(?:system|user|assistant|tool|choice)/.test(event.name))
|
|
99
131
|
.map((event) => ({
|
|
100
132
|
...event,
|
|
101
|
-
attributes: attributes(event.attributes ?? {}, options, `events.${event.name}
|
|
133
|
+
attributes: attributes(event.attributes ?? {}, options, `events.${event.name}`, budget),
|
|
102
134
|
})),
|
|
103
135
|
links: span.links.map((link) => ({
|
|
104
136
|
...link,
|
|
105
|
-
attributes: attributes(link.attributes ?? {}, options, "links.attributes"),
|
|
137
|
+
attributes: attributes(link.attributes ?? {}, options, "links.attributes", budget),
|
|
106
138
|
})),
|
|
107
|
-
resource: redactResource(span.resource, options, cache),
|
|
139
|
+
resource: redactResource(span.resource, options, cache, budget),
|
|
108
140
|
instrumentationScope: span.instrumentationScope,
|
|
109
141
|
droppedAttributesCount: span.droppedAttributesCount,
|
|
110
142
|
droppedEventsCount: span.droppedEventsCount,
|
|
@@ -112,7 +144,8 @@ export function redactSpan(span, options, cache) {
|
|
|
112
144
|
};
|
|
113
145
|
}
|
|
114
146
|
export function redactLog(log, options, cache) {
|
|
115
|
-
const
|
|
147
|
+
const budget = { bytes: 0, nodes: 0 };
|
|
148
|
+
const body = options.captureContent ? redactValue(log.body, "body", options, budget) : undefined;
|
|
116
149
|
if (body !== undefined && Buffer.byteLength(JSON.stringify(body)) > MAX_CONTENT_BYTES)
|
|
117
150
|
throw new Error("Telemetry log body exceeds 256 KiB");
|
|
118
151
|
return {
|
|
@@ -123,13 +156,13 @@ export function redactLog(log, options, cache) {
|
|
|
123
156
|
severityNumber: log.severityNumber,
|
|
124
157
|
eventName: log.eventName,
|
|
125
158
|
body: body,
|
|
126
|
-
attributes: attributes(log.attributes, options, "attributes"),
|
|
127
|
-
resource: redactResource(log.resource, options, cache),
|
|
159
|
+
attributes: attributes(log.attributes, options, "attributes", budget),
|
|
160
|
+
resource: redactResource(log.resource, options, cache, budget),
|
|
128
161
|
instrumentationScope: {
|
|
129
162
|
...log.instrumentationScope,
|
|
130
163
|
...(log.instrumentationScope.attributes
|
|
131
164
|
? {
|
|
132
|
-
attributes: attributes(log.instrumentationScope.attributes, options, "scope.attributes"),
|
|
165
|
+
attributes: attributes(log.instrumentationScope.attributes, options, "scope.attributes", budget),
|
|
133
166
|
}
|
|
134
167
|
: {}),
|
|
135
168
|
},
|
package/dist/receipt.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { HueOptions, TraceVerification, VerifyTraceOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by {@link HueClient.verifyTrace} when verification cannot proceed: denied key, unsupported
|
|
4
|
+
* or refusing server, unreachable network or an invalid receipt. A timeout is not an error; it
|
|
5
|
+
* returns `verified: false`.
|
|
6
|
+
*/
|
|
2
7
|
export declare class HueTraceVerificationError extends Error {
|
|
8
|
+
/** Safe failure class for branching without parsing the message. */
|
|
3
9
|
readonly code: "authentication" | "http" | "invalid_response" | "transport";
|
|
10
|
+
/** HTTP status when Hue answered. */
|
|
4
11
|
readonly status?: number | undefined;
|
|
5
|
-
constructor(
|
|
12
|
+
constructor(
|
|
13
|
+
/** Safe failure class for branching without parsing the message. */
|
|
14
|
+
code: "authentication" | "http" | "invalid_response" | "transport", message: string,
|
|
15
|
+
/** HTTP status when Hue answered. */
|
|
16
|
+
status?: number | undefined);
|
|
6
17
|
}
|
|
7
18
|
/** Observe persisted evidence after the application and its exporter have finished. */
|
|
8
19
|
export declare function verifyTrace(connection: Pick<HueOptions, "apiKey"> & {
|
package/dist/receipt.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
const fields = ["input", "output", "model", "usage", "session"];
|
|
2
2
|
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by {@link HueClient.verifyTrace} when verification cannot proceed: denied key, unsupported
|
|
5
|
+
* or refusing server, unreachable network or an invalid receipt. A timeout is not an error; it
|
|
6
|
+
* returns `verified: false`.
|
|
7
|
+
*/
|
|
3
8
|
export class HueTraceVerificationError extends Error {
|
|
4
9
|
code;
|
|
5
10
|
status;
|
|
6
|
-
constructor(
|
|
11
|
+
constructor(
|
|
12
|
+
/** Safe failure class for branching without parsing the message. */
|
|
13
|
+
code, message,
|
|
14
|
+
/** HTTP status when Hue answered. */
|
|
15
|
+
status) {
|
|
7
16
|
super(message);
|
|
8
17
|
this.code = code;
|
|
9
18
|
this.status = status;
|
package/dist/safety.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Span } from "@opentelemetry/api";
|
|
2
|
+
export declare function noopSpan(): Span;
|
|
3
|
+
export declare function safeSpan(source: Span, failed: () => void): Span;
|
|
4
|
+
/** Validate a bounded data tree without invoking toJSON or property getters. */
|
|
5
|
+
export declare function encodeContent(value: unknown): string;
|
|
6
|
+
/** Bounded conservative accounting for the record data retained by our queue, not total process RSS. */
|
|
7
|
+
export declare function estimateRecordBytes(value: unknown, limit: number): number;
|
package/dist/safety.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { INVALID_SPAN_CONTEXT, trace } from "@opentelemetry/api";
|
|
2
|
+
import { types as utilTypes } from "node:util";
|
|
3
|
+
import { MAX_CONTENT_BYTES } from "./config.js";
|
|
4
|
+
export function noopSpan() {
|
|
5
|
+
return trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
|
|
6
|
+
}
|
|
7
|
+
/** Isolate the public Span interface without changing its fluent method contract. */
|
|
8
|
+
class SafeSpan {
|
|
9
|
+
source;
|
|
10
|
+
failed;
|
|
11
|
+
constructor(source, failed) {
|
|
12
|
+
this.source = source;
|
|
13
|
+
this.failed = failed;
|
|
14
|
+
}
|
|
15
|
+
write(work) {
|
|
16
|
+
try {
|
|
17
|
+
const result = work();
|
|
18
|
+
// Broken/custom providers can return rejected promises from synchronous
|
|
19
|
+
// OTel methods. Observe them without awaiting on application code paths.
|
|
20
|
+
if (result && typeof result.then === "function")
|
|
21
|
+
void Promise.resolve(result).catch(this.failed);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
this.failed();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
spanContext() {
|
|
28
|
+
try {
|
|
29
|
+
const ids = this.source.spanContext();
|
|
30
|
+
if (!ids || typeof ids.traceId !== "string" || typeof ids.spanId !== "string")
|
|
31
|
+
throw new TypeError("Invalid span context");
|
|
32
|
+
return {
|
|
33
|
+
traceId: ids.traceId,
|
|
34
|
+
spanId: ids.spanId,
|
|
35
|
+
traceFlags: ids.traceFlags,
|
|
36
|
+
isRemote: ids.isRemote,
|
|
37
|
+
traceState: ids.traceState,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
this.failed();
|
|
42
|
+
return INVALID_SPAN_CONTEXT;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
isRecording() {
|
|
46
|
+
try {
|
|
47
|
+
return this.source.isRecording() === true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
this.failed();
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
setAttribute(...args) {
|
|
55
|
+
this.write(() => this.source.setAttribute(...args));
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
setAttributes(...args) {
|
|
59
|
+
this.write(() => this.source.setAttributes(...args));
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
addEvent(...args) {
|
|
63
|
+
this.write(() => this.source.addEvent(...args));
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
addLink(...args) {
|
|
67
|
+
this.write(() => this.source.addLink(...args));
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
addLinks(...args) {
|
|
71
|
+
this.write(() => this.source.addLinks(...args));
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
setStatus(...args) {
|
|
75
|
+
this.write(() => this.source.setStatus(...args));
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
updateName(...args) {
|
|
79
|
+
this.write(() => this.source.updateName(...args));
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
end(...args) {
|
|
83
|
+
this.write(() => this.source.end(...args));
|
|
84
|
+
}
|
|
85
|
+
recordException(...args) {
|
|
86
|
+
this.write(() => this.source.recordException(...args));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function safeSpan(source, failed) {
|
|
90
|
+
return new SafeSpan(source, failed);
|
|
91
|
+
}
|
|
92
|
+
/** Validate a bounded data tree without invoking toJSON or property getters. */
|
|
93
|
+
export function encodeContent(value) {
|
|
94
|
+
let nodes = 0;
|
|
95
|
+
let bytes = 0;
|
|
96
|
+
const ancestors = new Set();
|
|
97
|
+
const charge = (amount) => {
|
|
98
|
+
bytes += amount;
|
|
99
|
+
if (bytes > MAX_CONTENT_BYTES)
|
|
100
|
+
throw new RangeError("Content limit exceeded");
|
|
101
|
+
};
|
|
102
|
+
const visit = (item, depth) => {
|
|
103
|
+
if (++nodes > 16384 || depth > 32)
|
|
104
|
+
throw new RangeError("Content complexity limit exceeded");
|
|
105
|
+
if (typeof item === "string") {
|
|
106
|
+
if (item.length > MAX_CONTENT_BYTES)
|
|
107
|
+
throw new RangeError("Content limit exceeded");
|
|
108
|
+
charge(Buffer.byteLength(JSON.stringify(item)));
|
|
109
|
+
return item;
|
|
110
|
+
}
|
|
111
|
+
if (item === null || typeof item === "boolean" || typeof item === "number") {
|
|
112
|
+
if (typeof item === "number" && !Number.isFinite(item))
|
|
113
|
+
throw new TypeError("Invalid number");
|
|
114
|
+
charge(JSON.stringify(item).length);
|
|
115
|
+
return item;
|
|
116
|
+
}
|
|
117
|
+
if (!item || typeof item !== "object" || ancestors.has(item))
|
|
118
|
+
throw new TypeError("Invalid JSON");
|
|
119
|
+
// Even descriptor/prototype reads can execute application code on a Proxy.
|
|
120
|
+
// The native check also handles revoked proxies without invoking their traps.
|
|
121
|
+
if (utilTypes.isProxy(item))
|
|
122
|
+
throw new TypeError("JSON proxies are unsupported");
|
|
123
|
+
const array = Array.isArray(item);
|
|
124
|
+
if (!array && ![Object.prototype, null].includes(Object.getPrototypeOf(item)))
|
|
125
|
+
throw new TypeError("Expected JSON data");
|
|
126
|
+
ancestors.add(item);
|
|
127
|
+
charge(2);
|
|
128
|
+
const result = array ? [] : Object.create(null);
|
|
129
|
+
// Own descriptors avoid executing application accessors during capture.
|
|
130
|
+
const keys = array
|
|
131
|
+
? Array.from({ length: Math.min(item.length, 16385) }, (_, i) => String(i))
|
|
132
|
+
: Object.keys(item);
|
|
133
|
+
if (keys.length > 16384)
|
|
134
|
+
throw new RangeError("Content complexity limit exceeded");
|
|
135
|
+
for (const key of keys) {
|
|
136
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
137
|
+
if (!descriptor || !("value" in descriptor))
|
|
138
|
+
throw new TypeError("Expected JSON data property");
|
|
139
|
+
charge(1 + (array ? 0 : Buffer.byteLength(JSON.stringify(key)) + 1));
|
|
140
|
+
const child = visit(descriptor.value, depth + 1);
|
|
141
|
+
if (array)
|
|
142
|
+
result.push(child);
|
|
143
|
+
else
|
|
144
|
+
result[key] = child;
|
|
145
|
+
}
|
|
146
|
+
ancestors.delete(item);
|
|
147
|
+
return result;
|
|
148
|
+
};
|
|
149
|
+
const encoded = JSON.stringify(visit(value, 0));
|
|
150
|
+
if (Buffer.byteLength(encoded) > MAX_CONTENT_BYTES)
|
|
151
|
+
throw new RangeError("Content limit exceeded");
|
|
152
|
+
return encoded;
|
|
153
|
+
}
|
|
154
|
+
/** Bounded conservative accounting for the record data retained by our queue, not total process RSS. */
|
|
155
|
+
export function estimateRecordBytes(value, limit) {
|
|
156
|
+
let bytes = 0;
|
|
157
|
+
let nodes = 0;
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
const visit = (item, depth) => {
|
|
160
|
+
if (++nodes > 16384 || depth > 32)
|
|
161
|
+
throw new RangeError("Telemetry complexity limit exceeded");
|
|
162
|
+
bytes += 16;
|
|
163
|
+
if (typeof item === "string")
|
|
164
|
+
bytes += item.length * 2;
|
|
165
|
+
else if (item instanceof Uint8Array)
|
|
166
|
+
bytes += item.byteLength;
|
|
167
|
+
else if (item && typeof item === "object" && !seen.has(item)) {
|
|
168
|
+
seen.add(item);
|
|
169
|
+
for (const [key, child] of Object.entries(item)) {
|
|
170
|
+
bytes += key.length * 2 + 16;
|
|
171
|
+
visit(child, depth + 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (bytes > limit)
|
|
175
|
+
throw new RangeError("Telemetry byte limit exceeded");
|
|
176
|
+
};
|
|
177
|
+
visit(value, 0);
|
|
178
|
+
return bytes;
|
|
179
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReadableSpan } from "@opentelemetry/sdk-trace";
|
|
2
|
+
import type { ReadableLogRecord, ReadWriteLogRecord } from "@opentelemetry/sdk-logs";
|
|
3
|
+
export declare function snapshotSpan(source: ReadableSpan, limit: number): {
|
|
4
|
+
record: ReadableSpan;
|
|
5
|
+
bytes: number;
|
|
6
|
+
unresolvedResource: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function snapshotLog(source: ReadableLogRecord, limit: number): {
|
|
9
|
+
record: ReadWriteLogRecord;
|
|
10
|
+
bytes: number;
|
|
11
|
+
unresolvedResource: boolean;
|
|
12
|
+
};
|