@hue-run/sdk 0.1.4 → 0.1.5

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 CHANGED
@@ -93,7 +93,7 @@ Reuse the client across server requests. Await stream completion before flushing
93
93
  returning a streaming `Response` does not mean its stream has finished. The
94
94
  [Next.js streaming recipe](https://docs.hue.run/integrations/opentelemetry#flush-streamed-responses-in-next-js)
95
95
  shows how to keep completion and flushing within the request's background lifetime.
96
- For a standalone script, put the operation in `try` and call `await hue.shutdown()`
96
+ For a standalone script, put the operation in `try` and call `await hue.shutdownSafe()`
97
97
  in `finally`. Shut down a shared server client only when the application stops.
98
98
 
99
99
  The integration creates real Vercel provider, streaming and tool spans and passes
@@ -117,8 +117,7 @@ names cannot be classified automatically; use them deliberately.
117
117
  there is no SDK retention timer or automatic content expiry. To redact strings
118
118
  before export, supply `redact(value, path)`; it applies to supported strings in
119
119
  attributes, resources, event/link attributes and log bodies. Return a string.
120
- A throwing callback or invalid/oversized content fails closed: that record is
121
- counted as failed and the flush reports it. Shared resources are redacted once per
120
+ Invalid/oversized helper content is omitted with an instrumentation failure; the span can still be delivered. Export-time redactor failures reject the affected record and are reported by flush. Shared resources are redacted once per
122
121
  export batch. Do not put user content or secrets in span names or scope names.
123
122
 
124
123
  Manual helpers encode JSON values without converting null into absence. Unknown
@@ -165,7 +164,7 @@ flush. This is an in-memory queue, not durable storage.
165
164
 
166
165
  `flush()` waits for the current trace and log export work. A partial rejection,
167
166
  invalid acknowledgement, queue drop or failure throws `HueExportError`; its
168
- `report` contains cumulative accepted/rejected/failed/pending counts. Accepted
167
+ `report` contains cumulative accepted/rejected/failed counts and current pending gauges. Accepted
169
168
  means the collector acknowledged receipt, not that a complete trace has arrived.
170
169
  A malformed response reports uncertain acceptance as failure. Partial successes
171
170
  are not retried. Warning-only acknowledgements with zero rejected records remain
@@ -255,3 +254,11 @@ Use a 120-second host request limit for the default 90-second execution and
255
254
  See the [managed-run guide](https://docs.hue.run/evaluations/managed-runs) and the
256
255
  [full adapter contract](MANAGED_TARGETS.md) for registration, file handling,
257
256
  existing-provider flush callbacks and recovery. Local/CI runners remain available.
257
+
258
+ ## Serving safely
259
+
260
+ Use `createHueSafe(options)` for best-effort startup. Invalid initialization returns a disabled client with an instrumentation failure recorded. Pass `enabled: false` to disable Hue without a key; disabled helpers still execute the application callback. `flushSafe({ timeoutMillis: 1000 })` and `shutdownSafe({ timeoutMillis: 1000 })` return `{ ok, timedOut, report }` without rejecting. Strict initialization, connection checks and `flush()` remain available for diagnostics; do not gate application readiness or responses on them.
261
+
262
+ Capture/serialization/redaction/provider failures omit unsafe telemetry, record failures, and preserve the original business result/error. Async diagnostic rejections are contained; diagnostics are rate-limited. The default `maxQueueBytes` is 8 MiB across traces/logs including in-flight work, alongside the existing record cap. `pendingBytes` is a current queue gauge; `droppedSpans`, `droppedLogs` and `instrumentationFailures` are cumulative failure counters. This is a telemetry budget, not a total process memory ceiling. A timeout bounds the caller and does not cancel a borrowed provider. Never retry the business operation to recover telemetry. See [production safety](https://docs.hue.run/guides/production-safety).
263
+
264
+ Queued records snapshot supported telemetry values when a span ends or a log is emitted; later caller mutations cannot change queued data. Resource attributes still awaiting detection are omitted with a sanitized warning. Later records include them after detection finishes; await resource detection before instrumentation when those attributes are required.
package/dist/ai-sdk.js CHANGED
@@ -1,10 +1,19 @@
1
1
  import { OpenTelemetry } from "@ai-sdk/otel";
2
2
  /** Use as the call's telemetry option; it does not change global AI SDK integrations. */
3
3
  export function hueTelemetry(hue) {
4
+ // Core tracing also installs beside AI SDK 6. This adapter requires the v7
5
+ // per-call integration API; fail explicitly during configuration on v6.
6
+ if (hue.enabled) {
7
+ const { version } = createRequire(import.meta.url)("ai/package.json");
8
+ const [major, minor, patch] = version.split(".").map(Number);
9
+ if (major !== 7 || (minor === 0 && patch < 99))
10
+ throw new TypeError("hueTelemetry requires ai@^7.0.99; AI SDK 6 applications can use Hue core tracing with their existing OpenTelemetry integration");
11
+ }
4
12
  return {
5
- isEnabled: true,
13
+ isEnabled: hue.enabled,
6
14
  recordInputs: hue.captureContent,
7
15
  recordOutputs: hue.captureContent,
8
16
  integrations: [new OpenTelemetry({ tracer: hue.tracer, usage: true })],
9
17
  };
10
18
  }
19
+ import { createRequire } from "node:module";
package/dist/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Context, type Span, type Tracer } from "@opentelemetry/api";
2
2
  import { HueTransport } from "./transport.js";
3
- import type { ExportReport, FlushableLoggerProvider, FlushableTracerProvider, HueOptions, HueSpan, JsonValue, ProjectConnection, SpanOptions, VerifyTraceOptions, TraceVerification } from "./types.js";
3
+ import type { ExportReport, FlushableLoggerProvider, FlushableTracerProvider, HueOptions, HueSpan, JsonValue, ProjectConnection, SpanOptions, VerifyTraceOptions, TraceVerification, SafeLifecycleOptions, SafeLifecycleResult } from "./types.js";
4
4
  export interface ExistingHueProviders {
5
5
  transport: HueTransport;
6
6
  tracerProvider: FlushableTracerProvider;
@@ -14,6 +14,7 @@ export declare class HueClient {
14
14
  readonly transport: HueTransport;
15
15
  readonly tracer: Tracer;
16
16
  readonly captureContent: boolean;
17
+ readonly enabled: boolean;
17
18
  private logger;
18
19
  private storage;
19
20
  private tracerProvider;
@@ -22,6 +23,8 @@ export declare class HueClient {
22
23
  private closed;
23
24
  private shutdownPromise?;
24
25
  private flushPromise?;
26
+ private safeFlushPromise?;
27
+ private safeShutdownPromise?;
25
28
  constructor(options: HueOptions | ExistingHueProviders);
26
29
  verifyTrace(traceId: string, options?: VerifyTraceOptions): Promise<TraceVerification>;
27
30
  getContext(): Context;
@@ -34,8 +37,15 @@ export declare class HueClient {
34
37
  }, explicitContext?: Context): void;
35
38
  private setContent;
36
39
  checkConnection(): Promise<ProjectConnection>;
40
+ /** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
41
+ flushSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
42
+ /** Safe for finally blocks; preserves the application's result or original exception. */
43
+ shutdownSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
44
+ private safeLifecycle;
37
45
  flush(): Promise<ExportReport>;
38
46
  private flushOnce;
39
47
  shutdown(): Promise<ExportReport>;
40
48
  }
41
49
  export declare function createHue(options: HueOptions | ExistingHueProviders): HueClient;
50
+ /** Fail-open initialization for production; strict createHue remains available for setup/CI. */
51
+ export declare function createHueSafe(options: HueOptions | ExistingHueProviders): HueClient;
package/dist/client.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { context, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
2
+ import { context, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
3
3
  import { SeverityNumber } from "@opentelemetry/api-logs";
4
4
  import { LoggerProvider } from "@opentelemetry/sdk-logs";
5
5
  import { TracerProvider } from "@opentelemetry/sdk-trace";
6
6
  import { resourceFromAttributes } from "@opentelemetry/resources";
7
- import { MAX_CONTENT_BYTES } from "./config.js";
7
+ import { encodeContent, noopSpan, safeSpan } from "./safety.js";
8
8
  import { createHueTransport, HueExportError } from "./transport.js";
9
9
  import { verifyTrace } from "./receipt.js";
10
10
  export class HueConnectionError extends Error {
@@ -30,20 +30,32 @@ function identifier(value) {
30
30
  class ContextualTracer {
31
31
  source;
32
32
  storage;
33
- constructor(source, storage) {
33
+ enabled;
34
+ failed;
35
+ constructor(source, storage, enabled, failed) {
34
36
  this.source = source;
35
37
  this.storage = storage;
38
+ this.enabled = enabled;
39
+ this.failed = failed;
36
40
  }
37
41
  startSpan(name, options = {}, parent) {
38
- const active = this.storage.getStore();
39
- return this.source.startSpan(name, {
40
- ...options,
41
- attributes: {
42
- ...options.attributes,
43
- ...(active?.sessionId ? { "gen_ai.conversation.id": active.sessionId } : {}),
44
- ...(active?.userId ? { "user.id": active.userId } : {}),
45
- },
46
- }, parent ?? active?.context ?? context.active());
42
+ if (!this.enabled())
43
+ return noopSpan();
44
+ try {
45
+ const active = this.storage.getStore();
46
+ return safeSpan(this.source.startSpan(name, {
47
+ ...options,
48
+ attributes: {
49
+ ...options.attributes,
50
+ ...(active?.sessionId ? { "gen_ai.conversation.id": active.sessionId } : {}),
51
+ ...(active?.userId ? { "user.id": active.userId } : {}),
52
+ },
53
+ }, parent ?? active?.context ?? context.active()), this.failed);
54
+ }
55
+ catch {
56
+ this.failed();
57
+ return noopSpan();
58
+ }
47
59
  }
48
60
  startActiveSpan(name, optionsOrFn, contextOrFn, fn) {
49
61
  const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
@@ -54,17 +66,33 @@ class ContextualTracer {
54
66
  : fn;
55
67
  if (!callback)
56
68
  throw new TypeError("A span callback is required");
57
- const parent = typeof contextOrFn === "object"
58
- ? contextOrFn
59
- : (this.storage.getStore()?.context ?? context.active());
69
+ let parent = ROOT_CONTEXT;
70
+ try {
71
+ parent =
72
+ typeof contextOrFn === "object"
73
+ ? contextOrFn
74
+ : (this.storage.getStore()?.context ?? context.active());
75
+ }
76
+ catch {
77
+ this.failed();
78
+ }
60
79
  const span = this.startSpan(name, options, parent);
61
- return this.storage.run({ ...this.storage.getStore(), context: trace.setSpan(parent, span) }, () => callback(span));
80
+ let active = ROOT_CONTEXT;
81
+ try {
82
+ active = trace.setSpan(parent, span);
83
+ }
84
+ catch {
85
+ this.failed();
86
+ active = trace.setSpan(ROOT_CONTEXT, span);
87
+ }
88
+ return this.storage.run({ ...this.storage.getStore(), context: active }, () => callback(span));
62
89
  }
63
90
  }
64
91
  export class HueClient {
65
92
  transport;
66
93
  tracer;
67
94
  captureContent;
95
+ enabled;
68
96
  logger;
69
97
  storage = new AsyncLocalStorage();
70
98
  tracerProvider;
@@ -73,6 +101,8 @@ export class HueClient {
73
101
  closed = false;
74
102
  shutdownPromise;
75
103
  flushPromise;
104
+ safeFlushPromise;
105
+ safeShutdownPromise;
76
106
  constructor(options) {
77
107
  if ("transport" in options) {
78
108
  this.transport = options.transport;
@@ -87,59 +117,87 @@ export class HueClient {
87
117
  });
88
118
  const tracer = new TracerProvider({
89
119
  resource,
90
- spanProcessors: [this.transport.spanProcessor],
120
+ spanProcessors: this.transport.options.enabled === false ? [] : [this.transport.spanProcessor],
91
121
  });
92
122
  const logger = new LoggerProvider({
93
123
  resource,
94
- processors: [this.transport.logRecordProcessor],
124
+ processors: this.transport.options.enabled === false ? [] : [this.transport.logRecordProcessor],
95
125
  });
96
126
  this.ownedProviders = { tracer, logger };
97
127
  this.tracerProvider = tracer;
98
128
  this.loggerProvider = logger;
99
129
  }
100
130
  this.captureContent = this.transport.options.captureContent;
101
- this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", "0.1.4"), this.storage);
102
- this.logger = this.loggerProvider.getLogger("@hue-run/sdk", "0.1.4");
131
+ this.enabled = this.transport.options.enabled !== false;
132
+ this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", "0.1.5"), this.storage, () => this.enabled && !this.closed, () => this.transport.instrumentationFailure());
133
+ this.logger = this.loggerProvider.getLogger("@hue-run/sdk", "0.1.5");
103
134
  }
104
135
  verifyTrace(traceId, options = {}) {
136
+ if (!this.enabled)
137
+ return Promise.reject(new HueConnectionError("Hue telemetry is disabled"));
105
138
  return verifyTrace(this.transport.options, traceId, options);
106
139
  }
107
140
  getContext() {
108
- return this.storage.getStore()?.context ?? context.active();
141
+ try {
142
+ return this.storage.getStore()?.context ?? context.active();
143
+ }
144
+ catch {
145
+ this.transport.instrumentationFailure();
146
+ return ROOT_CONTEXT;
147
+ }
109
148
  }
110
149
  async withSpan(name, callback, options = {}) {
111
- if (this.closed)
112
- throw new Error("Hue client is shut down");
113
- const inherited = this.storage.getStore();
114
- const sessionId = identifier(options.sessionId ?? inherited?.sessionId);
115
- const userId = identifier(options.userId ?? inherited?.userId);
116
- const parent = options.parentContext ?? inherited?.context ?? context.active();
117
- const active = { context: parent, sessionId, userId };
118
- return this.storage.run(active, async () => {
119
- const span = this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, parent);
120
- const spanContext = trace.setSpan(parent, span);
121
- const handle = {
122
- span,
123
- context: spanContext,
124
- traceId: span.spanContext().traceId,
125
- spanId: span.spanContext().spanId,
126
- setInput: (value) => this.setContent(span, "input.value", value),
127
- setOutput: (value) => this.setContent(span, "output.value", value),
128
- };
129
- return this.storage.run({ ...active, context: spanContext }, async () => {
130
- try {
131
- if (options.input !== undefined)
132
- handle.setInput(options.input);
133
- return await callback(handle);
134
- }
135
- catch (error) {
136
- this.recordError(span, error);
137
- throw error;
138
- }
139
- finally {
140
- span.end();
141
- }
142
- });
150
+ let span = noopSpan();
151
+ let active = { context: this.getContext() };
152
+ if (this.enabled && !this.closed) {
153
+ try {
154
+ const inherited = this.storage.getStore();
155
+ active = {
156
+ context: options.parentContext ?? inherited?.context ?? context.active(),
157
+ sessionId: identifier(options.sessionId ?? inherited?.sessionId),
158
+ userId: identifier(options.userId ?? inherited?.userId),
159
+ };
160
+ span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
161
+ }
162
+ catch {
163
+ this.transport.instrumentationFailure();
164
+ }
165
+ }
166
+ let spanContext;
167
+ try {
168
+ spanContext = trace.setSpan(active.context, span);
169
+ }
170
+ catch {
171
+ this.transport.instrumentationFailure();
172
+ spanContext = trace.setSpan(ROOT_CONTEXT, span);
173
+ }
174
+ const handle = {
175
+ span,
176
+ context: spanContext,
177
+ traceId: span.spanContext().traceId,
178
+ spanId: span.spanContext().spanId,
179
+ setInput: (value) => this.setContent(span, "input.value", value),
180
+ setOutput: (value) => this.setContent(span, "output.value", value),
181
+ };
182
+ return this.storage.run({ ...active, context: spanContext }, async () => {
183
+ // Setup, capture and cleanup have separate failure boundaries from customer code.
184
+ try {
185
+ if (this.enabled && !this.closed && options.input !== undefined)
186
+ handle.setInput(options.input);
187
+ }
188
+ catch {
189
+ this.transport.instrumentationFailure();
190
+ }
191
+ try {
192
+ return await callback(handle);
193
+ }
194
+ catch (error) {
195
+ this.recordError(span, error);
196
+ throw error;
197
+ }
198
+ finally {
199
+ span.end();
200
+ }
143
201
  });
144
202
  }
145
203
  async tool(name, input, execute) {
@@ -152,56 +210,66 @@ export class HueClient {
152
210
  }, { attributes: { "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": name } });
153
211
  }
154
212
  recordError(span, error) {
155
- const type = error instanceof Error ? error.name : "Error";
156
- span.setStatus({
157
- code: SpanStatusCode.ERROR,
158
- ...(this.captureContent
159
- ? { message: error instanceof Error ? error.message : "Operation failed" }
160
- : {}),
161
- });
162
- span.addEvent("exception", {
163
- "exception.type": type,
164
- ...(this.captureContent && error instanceof Error
165
- ? {
166
- "exception.message": error.message,
167
- ...(error.stack ? { "exception.stacktrace": error.stack } : {}),
168
- }
169
- : {}),
170
- });
213
+ if (!this.enabled || this.closed)
214
+ return;
215
+ try {
216
+ const type = error instanceof Error ? error.name : "Error";
217
+ span.setStatus({
218
+ code: SpanStatusCode.ERROR,
219
+ ...(this.captureContent
220
+ ? { message: error instanceof Error ? error.message : "Operation failed" }
221
+ : {}),
222
+ });
223
+ span.addEvent("exception", {
224
+ "exception.type": type,
225
+ ...(this.captureContent && error instanceof Error
226
+ ? {
227
+ "exception.message": error.message,
228
+ ...(error.stack ? { "exception.stacktrace": error.stack } : {}),
229
+ }
230
+ : {}),
231
+ });
232
+ }
233
+ catch {
234
+ this.transport.instrumentationFailure();
235
+ }
171
236
  }
172
237
  recordMessages(messages, explicitContext) {
173
- if (this.closed)
174
- throw new Error("Hue client is shut down");
175
- if (!this.captureContent)
238
+ if (!this.enabled || this.closed || !this.captureContent)
176
239
  return;
177
- const active = explicitContext ?? this.getContext();
178
- if (!trace.getSpanContext(active))
179
- throw new Error("Message records require an active span or explicit span context");
180
- const body = {};
181
- if (messages.input !== undefined)
182
- body["gen_ai.input.messages"] = messages.input;
183
- if (messages.output !== undefined)
184
- body["gen_ai.output.messages"] = messages.output;
185
- this.logger.emit({
186
- context: active,
187
- severityNumber: SeverityNumber.INFO,
188
- eventName: "gen_ai.client.inference.operation.details",
189
- body,
190
- });
240
+ try {
241
+ const active = explicitContext ?? this.getContext();
242
+ if (!trace.getSpanContext(active))
243
+ throw new Error("Message records require an active span or explicit span context");
244
+ const body = {};
245
+ if (messages.input !== undefined)
246
+ body["gen_ai.input.messages"] = messages.input;
247
+ if (messages.output !== undefined)
248
+ body["gen_ai.output.messages"] = messages.output;
249
+ this.logger.emit({
250
+ context: active,
251
+ severityNumber: SeverityNumber.INFO,
252
+ eventName: "gen_ai.client.inference.operation.details",
253
+ body: JSON.parse(encodeContent(body)),
254
+ });
255
+ }
256
+ catch {
257
+ this.transport.instrumentationFailure("logs");
258
+ }
191
259
  }
192
260
  setContent(span, key, value) {
193
- if (!this.captureContent)
261
+ if (!this.enabled || this.closed || !this.captureContent)
194
262
  return;
195
- const encoded = JSON.stringify(value, (_key, item) => {
196
- if (typeof item === "number" && !Number.isFinite(item))
197
- throw new TypeError("Captured JSON numbers must be finite");
198
- return item;
199
- });
200
- if (encoded === undefined || Buffer.byteLength(encoded) > MAX_CONTENT_BYTES)
201
- throw new RangeError("Captured content must be JSON and no more than 256 KiB");
202
- span.setAttribute(key, encoded);
263
+ try {
264
+ span.setAttribute(key, encodeContent(value));
265
+ }
266
+ catch {
267
+ this.transport.instrumentationFailure();
268
+ }
203
269
  }
204
270
  async checkConnection() {
271
+ if (!this.enabled)
272
+ throw new HueConnectionError("Hue telemetry is disabled");
205
273
  const options = this.transport.options;
206
274
  let response;
207
275
  try {
@@ -258,6 +326,62 @@ export class HueClient {
258
326
  throw new HueConnectionError("Hue returned an invalid project response");
259
327
  }
260
328
  }
329
+ /** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
330
+ flushSafe(options = {}) {
331
+ if (this.safeFlushPromise)
332
+ return this.safeFlushPromise;
333
+ const work = this.flushPromise ?? this.flush();
334
+ const result = this.safeLifecycle(() => work, options);
335
+ this.safeFlushPromise = result;
336
+ const clear = () => {
337
+ this.safeFlushPromise = undefined;
338
+ };
339
+ // Retain the timed-out result until its underlying drain settles. Repeated
340
+ // timeouts must not accumulate promises against a hung borrowed provider.
341
+ void work.then(clear, clear);
342
+ return result;
343
+ }
344
+ /** Safe for finally blocks; preserves the application's result or original exception. */
345
+ shutdownSafe(options = {}) {
346
+ if (this.safeShutdownPromise)
347
+ return this.safeShutdownPromise;
348
+ const work = this.shutdown();
349
+ const result = this.safeLifecycle(() => work, options);
350
+ this.safeShutdownPromise = result;
351
+ const clear = () => {
352
+ this.safeShutdownPromise = undefined;
353
+ };
354
+ void work.then(clear, clear);
355
+ return result;
356
+ }
357
+ async safeLifecycle(work, options) {
358
+ let timer;
359
+ try {
360
+ const timeout = options.timeoutMillis ?? 1000;
361
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 60000)
362
+ throw new TypeError("Invalid lifecycle budget");
363
+ const outcome = await Promise.race([
364
+ work().then(() => "complete"),
365
+ new Promise((resolve) => {
366
+ timer = setTimeout(() => resolve("timeout"), timeout);
367
+ }),
368
+ ]);
369
+ const report = this.transport.getReport();
370
+ return {
371
+ ok: outcome === "complete" &&
372
+ this.transport.getFailureSequence() === 0 &&
373
+ report.pendingSpans + report.pendingLogs === 0,
374
+ timedOut: outcome === "timeout",
375
+ report,
376
+ };
377
+ }
378
+ catch {
379
+ return { ok: false, timedOut: false, report: this.transport.getReport() };
380
+ }
381
+ finally {
382
+ clearTimeout(timer);
383
+ }
384
+ }
261
385
  flush() {
262
386
  // Each caller needs a drain after its own preceding span/log emissions.
263
387
  // Joining an earlier drain can acknowledge records that were not in its batch.
@@ -281,8 +405,8 @@ export class HueClient {
281
405
  }
282
406
  async flushOnce() {
283
407
  const results = await Promise.allSettled([
284
- this.tracerProvider.forceFlush(),
285
- this.loggerProvider.forceFlush(),
408
+ Promise.resolve().then(() => this.tracerProvider.forceFlush()),
409
+ Promise.resolve().then(() => this.loggerProvider.forceFlush()),
286
410
  ]);
287
411
  for (const [index, result] of results.entries())
288
412
  if (result.status === "rejected")
@@ -311,3 +435,14 @@ export class HueClient {
311
435
  export function createHue(options) {
312
436
  return new HueClient(options);
313
437
  }
438
+ /** Fail-open initialization for production; strict createHue remains available for setup/CI. */
439
+ export function createHueSafe(options) {
440
+ try {
441
+ return new HueClient(options);
442
+ }
443
+ catch {
444
+ const disabled = new HueClient({ enabled: false, captureContent: false });
445
+ disabled.transport.instrumentationFailure();
446
+ return disabled;
447
+ }
448
+ }
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { HueOptions } from "./types.js";
2
2
  export declare const MAX_BODY_BYTES: number;
3
3
  export declare const MAX_CONTENT_BYTES: number;
4
- export declare function validateOptions(options: HueOptions): Required<Pick<HueOptions, "apiKey" | "serviceName" | "baseUrl" | "captureContent" | "timeoutMillis">> & HueOptions;
4
+ export declare function validateOptions(options: HueOptions): Required<Pick<HueOptions, "apiKey" | "serviceName" | "baseUrl" | "captureContent" | "timeoutMillis" | "maxQueueBytes">> & HueOptions;
package/dist/config.js CHANGED
@@ -3,6 +3,18 @@ export const MAX_CONTENT_BYTES = 256 * 1024;
3
3
  export function validateOptions(options) {
4
4
  if (typeof options.captureContent !== "boolean")
5
5
  throw new TypeError("Choose captureContent explicitly: true or false");
6
+ if (options.enabled !== undefined && typeof options.enabled !== "boolean")
7
+ throw new TypeError("enabled must be a boolean");
8
+ if (options.enabled === false)
9
+ return {
10
+ captureContent: options.captureContent,
11
+ enabled: false,
12
+ apiKey: "",
13
+ serviceName: "hue-disabled",
14
+ baseUrl: "https://app.hue.run",
15
+ timeoutMillis: 10000,
16
+ maxQueueBytes: 8 * 1024 * 1024,
17
+ };
6
18
  if (typeof options.apiKey !== "string" ||
7
19
  !options.apiKey ||
8
20
  options.apiKey.length > 4096 ||
@@ -28,5 +40,10 @@ export function validateOptions(options) {
28
40
  const timeoutMillis = options.timeoutMillis ?? 10000;
29
41
  if (!Number.isInteger(timeoutMillis) || timeoutMillis < 100 || timeoutMillis > 60000)
30
42
  throw new TypeError("timeoutMillis must be 100–60000");
31
- return { ...options, baseUrl: url.origin, timeoutMillis };
43
+ const maxQueueBytes = options.maxQueueBytes ?? 8 * 1024 * 1024;
44
+ if (!Number.isSafeInteger(maxQueueBytes) ||
45
+ maxQueueBytes < 1024 ||
46
+ maxQueueBytes > 64 * 1024 * 1024)
47
+ throw new TypeError("maxQueueBytes must be 1024–67108864");
48
+ return { ...options, baseUrl: url.origin, timeoutMillis, maxQueueBytes };
32
49
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createHue, HueClient, HueConnectionError, type ExistingHueProviders } from "./client.js";
1
+ export { createHue, createHueSafe, HueClient, HueConnectionError, type ExistingHueProviders, } from "./client.js";
2
2
  export { createHueTransport, HueTransport, HueExportError } from "./transport.js";
3
3
  export { HueTraceVerificationError } from "./receipt.js";
4
4
  export type * from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { createHue, HueClient, HueConnectionError } from "./client.js";
1
+ export { createHue, createHueSafe, HueClient, HueConnectionError, } from "./client.js";
2
2
  export { createHueTransport, HueTransport, HueExportError } from "./transport.js";
3
3
  export { HueTraceVerificationError } from "./receipt.js";