@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.
Files changed (58) hide show
  1. package/ENVIRONMENTS.md +182 -0
  2. package/EVALUATIONS.md +12 -0
  3. package/README.md +204 -21
  4. package/dist/ai-sdk.d.ts +9 -1
  5. package/dist/ai-sdk.js +37 -2
  6. package/dist/client.d.ts +130 -5
  7. package/dist/client.js +518 -110
  8. package/dist/config.d.ts +11 -2
  9. package/dist/config.js +50 -4
  10. package/dist/environment/client.d.ts +73 -0
  11. package/dist/environment/client.js +209 -0
  12. package/dist/environment/tools.d.ts +30 -0
  13. package/dist/environment/tools.js +24 -0
  14. package/dist/environment/types.d.ts +429 -0
  15. package/dist/environment/types.js +1 -0
  16. package/dist/environment.d.ts +5 -0
  17. package/dist/environment.js +2 -0
  18. package/dist/evals/attempt.d.ts +454 -0
  19. package/dist/evals/attempt.js +687 -0
  20. package/dist/evals/client.d.ts +99 -5
  21. package/dist/evals/client.js +136 -7
  22. package/dist/evals/environment-evidence.d.ts +6 -0
  23. package/dist/evals/environment-evidence.js +123 -0
  24. package/dist/evals/environment-json.d.ts +3 -0
  25. package/dist/evals/environment-json.js +76 -0
  26. package/dist/evals/json.d.ts +9 -1
  27. package/dist/evals/json.js +14 -6
  28. package/dist/evals/runner.d.ts +61 -2
  29. package/dist/evals/runner.js +71 -9
  30. package/dist/evals/scorer-publication.d.ts +2 -0
  31. package/dist/evals/scorer-publication.js +84 -0
  32. package/dist/evals/scorers.d.ts +11 -0
  33. package/dist/evals/scorers.js +56 -5
  34. package/dist/evals/simulation.d.ts +184 -0
  35. package/dist/evals/simulation.js +603 -0
  36. package/dist/evals/types.d.ts +304 -0
  37. package/dist/evals.d.ts +5 -1
  38. package/dist/evals.js +3 -1
  39. package/dist/experimental-telemetry.d.ts +8 -0
  40. package/dist/experimental-telemetry.js +13 -0
  41. package/dist/index.d.ts +4 -1
  42. package/dist/index.js +3 -1
  43. package/dist/managed.d.ts +51 -1
  44. package/dist/managed.js +11 -1
  45. package/dist/privacy.d.ts +2 -0
  46. package/dist/privacy.js +54 -21
  47. package/dist/receipt.d.ts +12 -1
  48. package/dist/receipt.js +10 -1
  49. package/dist/safety.d.ts +7 -0
  50. package/dist/safety.js +179 -0
  51. package/dist/snapshot.d.ts +12 -0
  52. package/dist/snapshot.js +200 -0
  53. package/dist/transport.d.ts +46 -8
  54. package/dist/transport.js +266 -48
  55. package/dist/types.d.ts +167 -8
  56. package/dist/version.d.ts +2 -0
  57. package/dist/version.js +3 -0
  58. package/package.json +51 -15
package/dist/transport.js CHANGED
@@ -1,25 +1,68 @@
1
1
  import { ExportResultCode } from "@opentelemetry/core";
2
2
  import { CompressionAlgorithm, OTLPExporterBase, OTLPExporterError, } from "@opentelemetry/otlp-exporter-base";
3
- import { convertLegacyHttpOptions, createOtlpHttpExportDelegate, } from "@opentelemetry/otlp-exporter-base/node-http";
3
+ import { createOtlpHttpExportDelegate } from "@opentelemetry/otlp-exporter-base/node-http";
4
4
  import { LogsExporterMetricsHelper, ProtobufLogsSerializer, ProtobufTraceSerializer, TraceExporterMetricsHelper, } from "@opentelemetry/otlp-transformer";
5
5
  import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace";
6
6
  import { BatchLogRecordProcessor, } from "@opentelemetry/sdk-logs";
7
- import { MAX_BODY_BYTES, validateOptions } from "./config.js";
7
+ import { isInsecureOrigin, MAX_BODY_BYTES, validateOptions } from "./config.js";
8
+ import { estimateRecordBytes } from "./safety.js";
9
+ import { snapshotLog, snapshotSpan } from "./snapshot.js";
8
10
  import { redactLog, redactSpan } from "./privacy.js";
11
+ import { sdkVersion } from "./version.js";
12
+ /** Per-record allowance for protobuf length prefixes that grow when records are grouped. */
13
+ const RECORD_FRAMING_BYTES = 64;
14
+ function recordData(record, signal) {
15
+ if (signal === "traces") {
16
+ const span = record;
17
+ return {
18
+ name: span.name,
19
+ attributes: span.attributes,
20
+ events: span.events,
21
+ links: span.links,
22
+ status: span.status,
23
+ resource: span.resource.attributes,
24
+ scope: span.instrumentationScope,
25
+ };
26
+ }
27
+ const log = record;
28
+ return {
29
+ body: log.body,
30
+ attributes: log.attributes,
31
+ eventName: log.eventName,
32
+ severityText: log.severityText,
33
+ resource: log.resource.attributes,
34
+ scope: log.instrumentationScope,
35
+ };
36
+ }
37
+ /**
38
+ * Rejection of {@link HueClient.flush} and {@link HueClient.shutdown}: telemetry was not fully
39
+ * accepted. Carries sanitized counts only, never server response text or content.
40
+ */
9
41
  export class HueExportError extends Error {
10
42
  issues;
11
43
  report;
12
- constructor(issues, report) {
44
+ constructor(
45
+ /** Non-warning issues observed since the failing drain began. */
46
+ issues,
47
+ /** Cumulative counters and current gauges at the time of the failure. */
48
+ report) {
13
49
  super("Hue could not accept all telemetry. Inspect issues and report for sanitized counts.");
14
50
  this.issues = issues;
15
51
  this.report = report;
16
52
  this.name = "HueExportError";
17
53
  }
18
54
  }
19
- /** Owned transport components; attach processors during provider construction. */
55
+ /**
56
+ * Hue's export pipeline: OTLP/HTTP exporters behind bounded batch processors, with cumulative
57
+ * counters and a sanitized issue history. A client owns one; in attach mode the application attaches
58
+ * `spanProcessor` and `logRecordProcessor` to its own providers while constructing them.
59
+ */
20
60
  export class HueTransport {
61
+ /** Validated options with defaults applied; `baseUrl` is the origin. Not enumerable, so it does not leak the key when logged. */
21
62
  options;
63
+ /** Span processor to attach to a tracer provider; a no-op when disabled. */
22
64
  spanProcessor;
65
+ /** Log record processor to attach to a logger provider; a no-op when disabled. */
23
66
  logRecordProcessor;
24
67
  sequence = 0;
25
68
  observedSequence = 0;
@@ -28,8 +71,13 @@ export class HueTransport {
28
71
  accepted = { traces: 0, logs: 0 };
29
72
  rejected = { traces: 0, logs: 0 };
30
73
  failed = { traces: 0, logs: 0 };
31
- spans = new Set();
32
- logs = new Set();
74
+ spans = new Map();
75
+ logs = new Map();
76
+ pendingBytes = 0;
77
+ dropped = { traces: 0, logs: 0 };
78
+ instrumentationFailures = 0;
79
+ diagnosticPending = false;
80
+ lastDiagnosticAt = -Infinity;
33
81
  traceExporter;
34
82
  logExporter;
35
83
  closed = false;
@@ -40,6 +88,13 @@ export class HueTransport {
40
88
  Object.defineProperty(this, "options", { enumerable: false });
41
89
  this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache) => redactSpan(span, this.options, cache));
42
90
  this.logExporter = new ReportingExporter(this, "logs", ProtobufLogsSerializer, LogsExporterMetricsHelper, (log, cache) => redactLog(log, this.options, cache));
91
+ if (this.options.enabled === false) {
92
+ this.spanProcessor = { onStart() { }, onEnd() { }, async forceFlush() { }, async shutdown() { } };
93
+ this.logRecordProcessor = { onEmit() { }, async forceFlush() { }, async shutdown() { } };
94
+ return;
95
+ }
96
+ if (isInsecureOrigin(this.options.baseUrl))
97
+ this.issue("traces", "warning", 0, "allowInsecureHttp is set: telemetry and the project key are sent over plain HTTP to a host that is not loopback");
43
98
  const batching = {
44
99
  maxQueueSize: 2048,
45
100
  maxExportBatchSize: 128,
@@ -49,22 +104,49 @@ export class HueTransport {
49
104
  const spans = new BatchSpanProcessor({ exporter: this.traceExporter, ...batching });
50
105
  const logs = new BatchLogRecordProcessor({ exporter: this.logExporter, ...batching });
51
106
  this.spanProcessor = {
52
- onStart: (span, parent) => spans.onStart(span, parent),
107
+ onStart: (span, parent) => {
108
+ try {
109
+ spans.onStart(span, parent);
110
+ }
111
+ catch {
112
+ this.instrumentationFailure();
113
+ }
114
+ },
53
115
  onEnd: (span) => {
54
- if (!(span.spanContext().traceFlags & 1))
55
- return;
56
- if (!this.enqueue("traces", span))
57
- return;
58
- spans.onEnd(span);
116
+ let admitted;
117
+ try {
118
+ if (!(span.spanContext().traceFlags & 1))
119
+ return;
120
+ const queued = this.enqueue("traces", span);
121
+ if (!queued)
122
+ return;
123
+ admitted = queued;
124
+ spans.onEnd(admitted);
125
+ }
126
+ catch {
127
+ if (admitted)
128
+ this.finish("traces", [admitted]);
129
+ this.issue("traces", "invalid", 1, "Telemetry processor could not accept a record");
130
+ }
59
131
  },
60
132
  forceFlush: () => spans.forceFlush(),
61
133
  shutdown: () => spans.shutdown(),
62
134
  };
63
135
  this.logRecordProcessor = {
64
136
  onEmit: (log) => {
65
- if (!this.enqueue("logs", log))
66
- return;
67
- logs.onEmit(log);
137
+ let admitted;
138
+ try {
139
+ const queued = this.enqueue("logs", log);
140
+ if (!queued)
141
+ return;
142
+ admitted = queued;
143
+ logs.onEmit(queued);
144
+ }
145
+ catch {
146
+ if (admitted)
147
+ this.finish("logs", [admitted]);
148
+ this.issue("logs", "invalid", 1, "Telemetry processor could not accept a record");
149
+ }
68
150
  },
69
151
  forceFlush: () => logs.forceFlush(),
70
152
  shutdown: () => logs.shutdown(),
@@ -76,26 +158,43 @@ export class HueTransport {
76
158
  this.issue(signal, "dropped", 1, this.closed
77
159
  ? "Telemetry emitted after transport shutdown"
78
160
  : "Telemetry queue reached 2048 records");
79
- return false;
161
+ return undefined;
162
+ }
163
+ try {
164
+ const remaining = this.options.maxQueueBytes - this.pendingBytes;
165
+ const snapshot = signal === "traces"
166
+ ? snapshotSpan(record, remaining)
167
+ : snapshotLog(record, remaining);
168
+ this.pendingBytes += snapshot.bytes;
169
+ if (signal === "traces")
170
+ this.spans.set(snapshot.record, snapshot.bytes);
171
+ else
172
+ this.logs.set(snapshot.record, snapshot.bytes);
173
+ if (snapshot.unresolvedResource)
174
+ this.issue(signal, "warning", 0, "Unresolved resource attributes omitted from the telemetry snapshot");
175
+ return snapshot.record;
176
+ }
177
+ catch {
178
+ this.issue(signal, "dropped", 1, "Telemetry snapshot exceeded its byte or complexity budget or contained unsupported data");
179
+ return undefined;
80
180
  }
81
- if (signal === "traces")
82
- this.spans.add(record);
83
- else
84
- this.logs.add(record);
85
- return true;
86
181
  }
182
+ /** @internal Exporter callback: releases queued records after an export attempt settles. */
87
183
  finish(signal, records) {
88
184
  for (const record of records) {
89
- if (signal === "traces")
90
- this.spans.delete(record);
91
- else
92
- this.logs.delete(record);
185
+ const pending = signal === "traces" ? this.spans : this.logs;
186
+ this.pendingBytes -= pending.get(record) ?? 0;
187
+ pending.delete(record);
93
188
  }
94
189
  }
190
+ /** @internal Exporter callback: counts records the collector acknowledged. */
95
191
  acceptedRecords(signal, count) {
96
192
  this.accepted[signal] += count;
97
193
  }
194
+ /** @internal Records a sanitized issue, updates counters and rate-limits the diagnostic callback. */
98
195
  issue(signal, kind, count, message, status) {
196
+ if (kind === "dropped")
197
+ this.dropped[signal] += count;
99
198
  if (kind === "rejected")
100
199
  this.rejected[signal] += count;
101
200
  else if (kind !== "warning")
@@ -113,13 +212,31 @@ export class HueTransport {
113
212
  this.issues.push(issue);
114
213
  if (this.issues.length > 128)
115
214
  this.issues.shift();
116
- try {
117
- this.options.onExportIssue?.({ ...issue });
118
- }
119
- catch {
120
- /* Diagnostics callbacks cannot interrupt the customer's application. */
215
+ // One diagnostic task at a time, at most once a second. No unbounded promise
216
+ // queue if a user callback never settles; all issues remain in counts/history.
217
+ if (this.options.onExportIssue &&
218
+ !this.diagnosticPending &&
219
+ Date.now() - this.lastDiagnosticAt >= 1000) {
220
+ this.diagnosticPending = true;
221
+ // Warnings (for example the allowInsecureHttp notice) do not consume the slot, so the first
222
+ // real failure still reaches the callback promptly.
223
+ if (kind !== "warning")
224
+ this.lastDiagnosticAt = Date.now();
225
+ void Promise.resolve()
226
+ .then(() => this.options.onExportIssue?.({ ...issue }))
227
+ .then(() => {
228
+ this.diagnosticPending = false;
229
+ }, () => {
230
+ this.diagnosticPending = false;
231
+ });
121
232
  }
122
233
  }
234
+ /** @internal Counts a helper capture or instrumentation failure that preserved application execution. */
235
+ instrumentationFailure(signal = "traces", message = "Telemetry capture or instrumentation failed; application execution was preserved") {
236
+ this.instrumentationFailures++;
237
+ this.issue(signal, "invalid", 0, message);
238
+ }
239
+ /** Cumulative counters and current queue gauges. */
123
240
  getReport() {
124
241
  return {
125
242
  acceptedSpans: this.accepted.traces,
@@ -130,8 +247,13 @@ export class HueTransport {
130
247
  failedLogs: this.failed.logs,
131
248
  pendingSpans: this.spans.size,
132
249
  pendingLogs: this.logs.size,
250
+ droppedSpans: this.dropped.traces,
251
+ droppedLogs: this.dropped.logs,
252
+ pendingBytes: this.pendingBytes,
253
+ instrumentationFailures: this.instrumentationFailures,
133
254
  };
134
255
  }
256
+ /** Copies of the latest 128 sanitized issues, oldest first. */
135
257
  getIssues() {
136
258
  return this.issues.map((issue) => ({ ...issue }));
137
259
  }
@@ -139,6 +261,11 @@ export class HueTransport {
139
261
  getFailureSequence() {
140
262
  return this.failureSequence;
141
263
  }
264
+ /**
265
+ * Waits for the processors' and exporters' in-flight work; drain the providers first.
266
+ *
267
+ * @throws HueExportError when a new non-warning issue was recorded since the previous observation.
268
+ */
142
269
  flush() {
143
270
  const from = this.observedSequence;
144
271
  const next = (this.flushPromise ?? Promise.resolve()).then(() => this.flushOnce(from), () => this.flushOnce(from));
@@ -169,6 +296,12 @@ export class HueTransport {
169
296
  throw new HueExportError(issues, report);
170
297
  return report;
171
298
  }
299
+ /**
300
+ * Flushes and stops the processors; records emitted afterwards are dropped and counted. In attach
301
+ * mode call it after shutting down the application's providers.
302
+ *
303
+ * @throws HueExportError when the final flush observed new failures.
304
+ */
172
305
  shutdown() {
173
306
  this.shutdownPromise ??= (async () => {
174
307
  this.closed = true;
@@ -208,40 +341,77 @@ class ReportingExporter {
208
341
  .finally(() => {
209
342
  this.transport.finish(this.signal, records);
210
343
  this.pending.delete(work);
211
- });
344
+ })
345
+ .catch(() => { });
212
346
  this.pending.add(work);
213
347
  }
214
348
  async exportRecords(records) {
215
349
  const accepted = [];
216
350
  const cache = new WeakMap();
217
351
  let failed = false;
352
+ let redactedBytes = 0;
353
+ const resourceDeadline = Date.now() + this.transport.options.timeoutMillis;
218
354
  for (const record of records) {
219
355
  try {
220
- await record.resource.waitForAsyncAttributes?.();
221
- accepted.push(this.redact(record, cache));
356
+ const ready = record.resource.waitForAsyncAttributes?.();
357
+ if (ready) {
358
+ let timer;
359
+ try {
360
+ await Promise.race([
361
+ ready,
362
+ new Promise((_resolve, reject) => {
363
+ timer = setTimeout(() => reject(new Error("Resource deadline exceeded")), Math.max(1, resourceDeadline - Date.now()));
364
+ }),
365
+ ]);
366
+ }
367
+ finally {
368
+ clearTimeout(timer);
369
+ }
370
+ }
371
+ const redacted = this.redact(record, cache);
372
+ const bytes = 512 +
373
+ estimateRecordBytes(recordData(redacted, this.signal), this.transport.options.maxQueueBytes - redactedBytes);
374
+ if (redactedBytes + bytes > this.transport.options.maxQueueBytes)
375
+ throw new RangeError("Redacted batch exceeds byte budget");
376
+ redactedBytes += bytes;
377
+ accepted.push(redacted);
222
378
  }
223
379
  catch {
224
380
  failed = true;
225
381
  this.transport.issue(this.signal, "invalid", 1, "Telemetry record could not be redacted or exceeds supported content limits");
226
382
  }
227
383
  }
384
+ // Each record is encoded once to measure it; a request is encoded once more when it is sent.
385
+ // Records sharing a resource and scope are grouped on the wire, so the sum of the individual
386
+ // encodings plus a fixed framing margin bounds the request size. Room is left for gzip
387
+ // headers/blocks when otherwise incompressible data is near the wire cap.
388
+ const limit = MAX_BODY_BYTES - 1024;
228
389
  let batch = [];
390
+ let batchBytes = 0;
229
391
  for (const record of accepted) {
230
- const candidate = [...batch, record];
231
- // Leave room for gzip headers/blocks when otherwise incompressible data is near the wire cap.
232
- if ((this.serializer.serializeRequest(candidate)?.byteLength ?? 0) <= MAX_BODY_BYTES - 1024) {
233
- batch = candidate;
234
- continue;
392
+ let recordBytes;
393
+ try {
394
+ recordBytes = this.serializer.serializeRequest([record])?.byteLength ?? Infinity;
235
395
  }
236
- if (batch.length && !(await this.send(batch)))
396
+ catch {
237
397
  failed = true;
238
- batch = [];
239
- if ((this.serializer.serializeRequest([record])?.byteLength ?? 0) > MAX_BODY_BYTES - 1024) {
398
+ this.transport.issue(this.signal, "invalid", 1, "Telemetry record could not be serialized");
399
+ continue;
400
+ }
401
+ const framedBytes = recordBytes + RECORD_FRAMING_BYTES;
402
+ if (batch.length && batchBytes + framedBytes > limit) {
403
+ if (!(await this.send(batch)))
404
+ failed = true;
405
+ batch = [];
406
+ batchBytes = 0;
407
+ }
408
+ if (recordBytes > limit) {
240
409
  failed = true;
241
410
  this.transport.issue(this.signal, "invalid", 1, "Telemetry record exceeds the 1 MiB request limit");
411
+ continue;
242
412
  }
243
- else
244
- batch = [record];
413
+ batch.push(record);
414
+ batchBytes += framedBytes;
245
415
  }
246
416
  if (batch.length && !(await this.send(batch)))
247
417
  failed = true;
@@ -252,9 +422,17 @@ class ReportingExporter {
252
422
  const options = this.transport.options;
253
423
  let rejected = 0;
254
424
  let validResponse = true;
425
+ let receivedResponse = false;
426
+ let expired = false;
427
+ const agents = new Set();
428
+ const deadline = Date.now() + options.timeoutMillis;
429
+ let timer;
255
430
  const serializer = {
256
431
  serializeRequest: (data) => this.serializer.serializeRequest(data),
257
432
  deserializeResponse: (bytes) => {
433
+ if (expired)
434
+ return {};
435
+ receivedResponse = true;
258
436
  try {
259
437
  const response = this.serializer.deserializeResponse(bytes);
260
438
  const partial = response.partialSuccess;
@@ -277,17 +455,46 @@ class ReportingExporter {
277
455
  },
278
456
  };
279
457
  const endpoint = `${options.baseUrl}/api/v1/otlp/v1/${this.signal}`;
280
- const delegate = createOtlpHttpExportDelegate(convertLegacyHttpOptions({
458
+ // Explicit configuration only. OTEL_EXPORTER_OTLP_* environment variables are meant
459
+ // for generic exporters; merging them here could send another vendor's headers to Hue.
460
+ const delegate = createOtlpHttpExportDelegate({
281
461
  url: endpoint,
282
- headers: { Authorization: `Bearer ${options.apiKey}` },
462
+ headers: async () => ({
463
+ "Content-Type": "application/x-protobuf",
464
+ Authorization: `Bearer ${options.apiKey}`,
465
+ }),
466
+ // The transport prefixes this to OpenTelemetry's own User-Agent token.
467
+ userAgent: `hue-sdk-typescript/${sdkVersion}`,
283
468
  timeoutMillis: options.timeoutMillis,
284
469
  concurrencyLimit: 1,
285
470
  compression: CompressionAlgorithm.GZIP,
286
- }, this.signal === "traces" ? "TRACES" : "LOGS", `v1/${this.signal}`, { "Content-Type": "application/x-protobuf" }), serializer, this.signal === "traces" ? "otlp_http_span_exporter" : "otlp_http_log_exporter", this.metrics, undefined);
471
+ agentFactory: async (protocol) => {
472
+ if (expired || Date.now() >= deadline)
473
+ throw new Error("Hue export deadline exceeded");
474
+ const { Agent } = await import(protocol === "https:" ? "node:https" : "node:http");
475
+ const agent = new Agent({ keepAlive: false });
476
+ agents.add(agent);
477
+ if (expired)
478
+ agent.destroy();
479
+ return agent;
480
+ },
481
+ }, serializer, this.signal === "traces" ? "otlp_http_span_exporter" : "otlp_http_log_exporter", this.metrics, undefined);
287
482
  const exporter = new OTLPExporterBase(delegate);
288
483
  try {
289
- const result = await new Promise((resolve) => exporter.export(records, resolve));
484
+ const result = await new Promise((resolve) => {
485
+ timer = setTimeout(() => {
486
+ expired = true;
487
+ for (const agent of agents)
488
+ agent.destroy();
489
+ resolve({ code: ExportResultCode.FAILED });
490
+ }, Math.max(1, deadline - Date.now()));
491
+ exporter.export(records, resolve);
492
+ });
290
493
  if (result.code === ExportResultCode.SUCCESS) {
494
+ if (!receivedResponse) {
495
+ this.transport.issue(this.signal, "failed", records.length, "Hue response ended without a complete OTLP acknowledgement; acceptance is uncertain");
496
+ return false;
497
+ }
291
498
  if (validResponse)
292
499
  this.transport.acceptedRecords(this.signal, records.length - rejected);
293
500
  return validResponse;
@@ -305,7 +512,12 @@ class ReportingExporter {
305
512
  return false;
306
513
  }
307
514
  finally {
308
- await exporter.shutdown();
515
+ clearTimeout(timer);
516
+ for (const agent of agents)
517
+ agent.destroy();
518
+ // Delegate cleanup cannot extend the hard request wait. Sockets are closed
519
+ // and the cleanup promise is always observed, even after a caller timeout.
520
+ void exporter.shutdown().catch(() => { });
309
521
  }
310
522
  }
311
523
  async forceFlush() {
@@ -315,6 +527,12 @@ class ReportingExporter {
315
527
  await this.forceFlush();
316
528
  }
317
529
  }
530
+ /**
531
+ * Creates the export pipeline for attach mode; pass it with the application's providers to
532
+ * {@link createHue}. Validates options like an owned client.
533
+ *
534
+ * @throws TypeError for invalid options; see {@link createHue}.
535
+ */
318
536
  export function createHueTransport(options) {
319
537
  return new HueTransport(options);
320
538
  }