@honeyhive/control-plane-sdk 1.1.0 → 1.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.
@@ -1,5 +1,7 @@
1
1
  // AUTO-GENERATED — do not edit manually. Run `pnpm turbo run generate` to regenerate.
2
2
 
3
+ import { type Client as OpenapiFetchClient } from 'openapi-fetch';
4
+
3
5
  import { type paths } from './types.js';
4
6
  import {
5
7
  type ListAlertsRequest,
@@ -35,12 +37,23 @@ import {
35
37
  } from './apiTypes.js';
36
38
  import { type ClientConfig, type FetchOptions, createApiClient, unwrap } from '../util.js';
37
39
 
40
+ /** Every security scheme the API's OpenAPI spec defines. */
41
+ export type SecurityScheme = 'ControlPlaneApiKey';
42
+
43
+ /**
44
+ * What `createApiClient` returns: for every security scheme the spec defines,
45
+ * the openapi-fetch client that carries the credential for it. The hand-written
46
+ * chassis builds this table, so a scheme added to or removed from the spec
47
+ * fails to compile there until the table says which client serves it.
48
+ */
49
+ export type ApiClients = Record<SecurityScheme, OpenapiFetchClient<paths>>;
50
+
38
51
  /** @inline */
39
52
  class AlertsNamespace {
40
- #client: ReturnType<typeof createApiClient<paths>>;
53
+ #clients: ApiClients;
41
54
 
42
- constructor(client: ReturnType<typeof createApiClient<paths>>) {
43
- this.#client = client;
55
+ constructor(clients: ApiClients) {
56
+ this.#clients = clients;
44
57
  }
45
58
 
46
59
  /**
@@ -52,8 +65,9 @@ class AlertsNamespace {
52
65
  */
53
66
  public list(request: ListAlertsRequest, options?: FetchOptions): Promise<ListAlertsResponse> {
54
67
  const { project_id, page, limit, status, sort_by, sort_order } = request;
68
+ const httpClient = this.#clients.ControlPlaneApiKey;
55
69
  return unwrap(
56
- this.#client.GET('/v1/projects/{project_id}/alerts', {
70
+ httpClient.GET('/v1/projects/{project_id}/alerts', {
57
71
  params: { path: { project_id }, query: { page, limit, status, sort_by, sort_order } },
58
72
  ...options,
59
73
  }),
@@ -71,8 +85,9 @@ class AlertsNamespace {
71
85
  */
72
86
  public create(request: CreateAlertRequest, options?: FetchOptions): Promise<CreateAlertResponse> {
73
87
  const { project_id, ...body } = request;
88
+ const httpClient = this.#clients.ControlPlaneApiKey;
74
89
  return unwrap(
75
- this.#client.POST('/v1/projects/{project_id}/alerts', {
90
+ httpClient.POST('/v1/projects/{project_id}/alerts', {
76
91
  params: { path: { project_id } },
77
92
  body,
78
93
  ...options,
@@ -89,8 +104,9 @@ class AlertsNamespace {
89
104
  */
90
105
  public get(request: GetAlertRequest, options?: FetchOptions): Promise<GetAlertResponse> {
91
106
  const { project_id, alert_id } = request;
107
+ const httpClient = this.#clients.ControlPlaneApiKey;
92
108
  return unwrap(
93
- this.#client.GET('/v1/projects/{project_id}/alerts/{alert_id}', {
109
+ httpClient.GET('/v1/projects/{project_id}/alerts/{alert_id}', {
94
110
  params: { path: { project_id, alert_id } },
95
111
  ...options,
96
112
  }),
@@ -100,10 +116,10 @@ class AlertsNamespace {
100
116
 
101
117
  /** @inline */
102
118
  class WorkspacesNamespace {
103
- #client: ReturnType<typeof createApiClient<paths>>;
119
+ #clients: ApiClients;
104
120
 
105
- constructor(client: ReturnType<typeof createApiClient<paths>>) {
106
- this.#client = client;
121
+ constructor(clients: ApiClients) {
122
+ this.#clients = clients;
107
123
  }
108
124
 
109
125
  /**
@@ -140,8 +156,9 @@ class WorkspacesNamespace {
140
156
  options?: FetchOptions,
141
157
  ): Promise<CreateWorkspaceResponse> {
142
158
  const { virtual_dataplane_id, ...body } = request;
159
+ const httpClient = this.#clients.ControlPlaneApiKey;
143
160
  return unwrap(
144
- this.#client.POST('/v1/virtual_dataplanes/{virtual_dataplane_id}/workspaces', {
161
+ httpClient.POST('/v1/virtual_dataplanes/{virtual_dataplane_id}/workspaces', {
145
162
  params: { path: { virtual_dataplane_id } },
146
163
  body,
147
164
  ...options,
@@ -160,8 +177,9 @@ class WorkspacesNamespace {
160
177
  */
161
178
  public get(request: GetWorkspaceRequest, options?: FetchOptions): Promise<GetWorkspaceResponse> {
162
179
  const { workspace_id } = request;
180
+ const httpClient = this.#clients.ControlPlaneApiKey;
163
181
  return unwrap(
164
- this.#client.GET('/v1/workspaces/{workspace_id}', {
182
+ httpClient.GET('/v1/workspaces/{workspace_id}', {
165
183
  params: { path: { workspace_id } },
166
184
  ...options,
167
185
  }),
@@ -182,8 +200,9 @@ class WorkspacesNamespace {
182
200
  options?: FetchOptions,
183
201
  ): Promise<UpdateWorkspaceResponse> {
184
202
  const { workspace_id, ...body } = request;
203
+ const httpClient = this.#clients.ControlPlaneApiKey;
185
204
  return unwrap(
186
- this.#client.PUT('/v1/workspaces/{workspace_id}', {
205
+ httpClient.PUT('/v1/workspaces/{workspace_id}', {
187
206
  params: { path: { workspace_id } },
188
207
  body,
189
208
  ...options,
@@ -213,8 +232,9 @@ class WorkspacesNamespace {
213
232
  options?: FetchOptions,
214
233
  ): Promise<DeleteWorkspaceResponse> {
215
234
  const { workspace_id, dangerously_delete_child_scopes } = request;
235
+ const httpClient = this.#clients.ControlPlaneApiKey;
216
236
  return unwrap(
217
- this.#client.DELETE('/v1/workspaces/{workspace_id}', {
237
+ httpClient.DELETE('/v1/workspaces/{workspace_id}', {
218
238
  params: { path: { workspace_id }, query: { dangerously_delete_child_scopes } },
219
239
  ...options,
220
240
  }),
@@ -224,10 +244,10 @@ class WorkspacesNamespace {
224
244
 
225
245
  /** @inline */
226
246
  class ProjectsNamespace {
227
- #client: ReturnType<typeof createApiClient<paths>>;
247
+ #clients: ApiClients;
228
248
 
229
- constructor(client: ReturnType<typeof createApiClient<paths>>) {
230
- this.#client = client;
249
+ constructor(clients: ApiClients) {
250
+ this.#clients = clients;
231
251
  }
232
252
 
233
253
  /**
@@ -259,8 +279,9 @@ class ProjectsNamespace {
259
279
  options?: FetchOptions,
260
280
  ): Promise<CreateProjectResponse> {
261
281
  const { workspace_id, ...body } = request;
282
+ const httpClient = this.#clients.ControlPlaneApiKey;
262
283
  return unwrap(
263
- this.#client.POST('/v1/workspaces/{workspace_id}/projects', {
284
+ httpClient.POST('/v1/workspaces/{workspace_id}/projects', {
264
285
  params: { path: { workspace_id } },
265
286
  body,
266
287
  ...options,
@@ -276,11 +297,9 @@ class ProjectsNamespace {
276
297
  */
277
298
  public get(request: GetProjectRequest, options?: FetchOptions): Promise<GetProjectResponse> {
278
299
  const { project_id } = request;
300
+ const httpClient = this.#clients.ControlPlaneApiKey;
279
301
  return unwrap(
280
- this.#client.GET('/v1/projects/{project_id}', {
281
- params: { path: { project_id } },
282
- ...options,
283
- }),
302
+ httpClient.GET('/v1/projects/{project_id}', { params: { path: { project_id } }, ...options }),
284
303
  );
285
304
  }
286
305
 
@@ -296,8 +315,9 @@ class ProjectsNamespace {
296
315
  options?: FetchOptions,
297
316
  ): Promise<UpdateProjectResponse> {
298
317
  const { project_id, ...body } = request;
318
+ const httpClient = this.#clients.ControlPlaneApiKey;
299
319
  return unwrap(
300
- this.#client.PUT('/v1/projects/{project_id}', {
320
+ httpClient.PUT('/v1/projects/{project_id}', {
301
321
  params: { path: { project_id } },
302
322
  body,
303
323
  ...options,
@@ -317,8 +337,9 @@ class ProjectsNamespace {
317
337
  options?: FetchOptions,
318
338
  ): Promise<DeleteProjectResponse> {
319
339
  const { project_id } = request;
340
+ const httpClient = this.#clients.ControlPlaneApiKey;
320
341
  return unwrap(
321
- this.#client.DELETE('/v1/projects/{project_id}', {
342
+ httpClient.DELETE('/v1/projects/{project_id}', {
322
343
  params: { path: { project_id } },
323
344
  ...options,
324
345
  }),
@@ -328,10 +349,10 @@ class ProjectsNamespace {
328
349
 
329
350
  /** @inline */
330
351
  class VirtualDataplanesNamespace {
331
- #client: ReturnType<typeof createApiClient<paths>>;
352
+ #clients: ApiClients;
332
353
 
333
- constructor(client: ReturnType<typeof createApiClient<paths>>) {
334
- this.#client = client;
354
+ constructor(clients: ApiClients) {
355
+ this.#clients = clients;
335
356
  }
336
357
 
337
358
  /**
@@ -355,12 +376,11 @@ class VirtualDataplanesNamespace {
355
376
  * A cluster admits an organization only if your control plane administrator configured it to,
356
377
  * so a `cluster_id` you can read from a sibling virtual data plane is not necessarily one you
357
378
  * may place a new virtual data plane on. A cluster that does not admit this organization
358
- * returns a 403. When you named the cluster, the fix is a configuration change your
359
- * administrator makes. When this endpoint inferred the cluster, send an explicit `cluster_id`
360
- * to place the virtual data plane on another cluster.
379
+ * returns a 404. When this endpoint inferred the cluster, send an explicit `cluster_id` to
380
+ * place the virtual data plane on another cluster.
361
381
  *
362
382
  * Deployments differ in who places virtual data planes. Where HoneyHive assigns them to
363
- * organizations automatically, this endpoint returns a 403 for every request and the
383
+ * organizations automatically, this endpoint returns a 404 for every request and the
364
384
  * placement is not yours to make. Deployments whose administrators own the scope tree are
365
385
  * the ones this endpoint serves.
366
386
  *
@@ -385,8 +405,9 @@ class VirtualDataplanesNamespace {
385
405
  options?: FetchOptions,
386
406
  ): Promise<CreateVirtualDataplaneResponse> {
387
407
  const { org_id, ...body } = request;
408
+ const httpClient = this.#clients.ControlPlaneApiKey;
388
409
  return unwrap(
389
- this.#client.POST('/v1/orgs/{org_id}/virtual_dataplanes', {
410
+ httpClient.POST('/v1/orgs/{org_id}/virtual_dataplanes', {
390
411
  params: { path: { org_id } },
391
412
  body,
392
413
  ...options,
@@ -413,8 +434,9 @@ class VirtualDataplanesNamespace {
413
434
  options?: FetchOptions,
414
435
  ): Promise<GetVirtualDataplaneResponse> {
415
436
  const { virtual_dataplane_id } = request;
437
+ const httpClient = this.#clients.ControlPlaneApiKey;
416
438
  return unwrap(
417
- this.#client.GET('/v1/virtual_dataplanes/{virtual_dataplane_id}', {
439
+ httpClient.GET('/v1/virtual_dataplanes/{virtual_dataplane_id}', {
418
440
  params: { path: { virtual_dataplane_id } },
419
441
  ...options,
420
442
  }),
@@ -433,15 +455,16 @@ class VirtualDataplanesNamespace {
433
455
  * `name`, or sends the current one, changes nothing.
434
456
  *
435
457
  * This endpoint is unavailable where HoneyHive assigns virtual data planes automatically,
436
- * and returns a 403 there.
458
+ * and returns a 404 there.
437
459
  */
438
460
  public update(
439
461
  request: UpdateVirtualDataplaneRequest,
440
462
  options?: FetchOptions,
441
463
  ): Promise<UpdateVirtualDataplaneResponse> {
442
464
  const { virtual_dataplane_id, ...body } = request;
465
+ const httpClient = this.#clients.ControlPlaneApiKey;
443
466
  return unwrap(
444
- this.#client.PUT('/v1/virtual_dataplanes/{virtual_dataplane_id}', {
467
+ httpClient.PUT('/v1/virtual_dataplanes/{virtual_dataplane_id}', {
445
468
  params: { path: { virtual_dataplane_id } },
446
469
  body,
447
470
  ...options,
@@ -467,15 +490,16 @@ class VirtualDataplanesNamespace {
467
490
  * archived, so read that first if you need the list.
468
491
  *
469
492
  * This endpoint is unavailable where HoneyHive assigns virtual data planes automatically,
470
- * and returns a 403 there.
493
+ * and returns a 404 there.
471
494
  */
472
495
  public delete(
473
496
  request: DeleteVirtualDataplaneRequest,
474
497
  options?: FetchOptions,
475
498
  ): Promise<DeleteVirtualDataplaneResponse> {
476
499
  const { virtual_dataplane_id, dangerously_delete_child_scopes } = request;
500
+ const httpClient = this.#clients.ControlPlaneApiKey;
477
501
  return unwrap(
478
- this.#client.DELETE('/v1/virtual_dataplanes/{virtual_dataplane_id}', {
502
+ httpClient.DELETE('/v1/virtual_dataplanes/{virtual_dataplane_id}', {
479
503
  params: { path: { virtual_dataplane_id }, query: { dangerously_delete_child_scopes } },
480
504
  ...options,
481
505
  }),
@@ -484,17 +508,17 @@ class VirtualDataplanesNamespace {
484
508
  }
485
509
 
486
510
  export class Client {
487
- #client: ReturnType<typeof createApiClient<paths>>;
511
+ #clients: ApiClients;
488
512
  readonly alerts: AlertsNamespace;
489
513
  readonly workspaces: WorkspacesNamespace;
490
514
  readonly projects: ProjectsNamespace;
491
515
  readonly virtualDataplanes: VirtualDataplanesNamespace;
492
516
 
493
517
  constructor(options: ClientConfig = {}) {
494
- this.#client = createApiClient<paths>(options);
495
- this.alerts = new AlertsNamespace(this.#client);
496
- this.workspaces = new WorkspacesNamespace(this.#client);
497
- this.projects = new ProjectsNamespace(this.#client);
498
- this.virtualDataplanes = new VirtualDataplanesNamespace(this.#client);
518
+ this.#clients = createApiClient(options);
519
+ this.alerts = new AlertsNamespace(this.#clients);
520
+ this.workspaces = new WorkspacesNamespace(this.#clients);
521
+ this.projects = new ProjectsNamespace(this.#clients);
522
+ this.virtualDataplanes = new VirtualDataplanesNamespace(this.#clients);
499
523
  }
500
524
  }
@@ -244,12 +244,11 @@ export interface paths {
244
244
  * A cluster admits an organization only if your control plane administrator configured it to,
245
245
  * so a `cluster_id` you can read from a sibling virtual data plane is not necessarily one you
246
246
  * may place a new virtual data plane on. A cluster that does not admit this organization
247
- * returns a 403. When you named the cluster, the fix is a configuration change your
248
- * administrator makes. When this endpoint inferred the cluster, send an explicit `cluster_id`
249
- * to place the virtual data plane on another cluster.
247
+ * returns a 404. When this endpoint inferred the cluster, send an explicit `cluster_id` to
248
+ * place the virtual data plane on another cluster.
250
249
  *
251
250
  * Deployments differ in who places virtual data planes. Where HoneyHive assigns them to
252
- * organizations automatically, this endpoint returns a 403 for every request and the
251
+ * organizations automatically, this endpoint returns a 404 for every request and the
253
252
  * placement is not yours to make. Deployments whose administrators own the scope tree are
254
253
  * the ones this endpoint serves.
255
254
  *
@@ -308,7 +307,7 @@ export interface paths {
308
307
  * `name`, or sends the current one, changes nothing.
309
308
  *
310
309
  * This endpoint is unavailable where HoneyHive assigns virtual data planes automatically,
311
- * and returns a 403 there.
310
+ * and returns a 404 there.
312
311
  */
313
312
  put: operations['updateVirtualDataplane'];
314
313
  post?: never;
@@ -329,7 +328,7 @@ export interface paths {
329
328
  * archived, so read that first if you need the list.
330
329
  *
331
330
  * This endpoint is unavailable where HoneyHive assigns virtual data planes automatically,
332
- * and returns a 403 there.
331
+ * and returns a 404 there.
333
332
  */
334
333
  delete: operations['deleteVirtualDataplane'];
335
334
  options?: never;
@@ -560,6 +559,8 @@ export interface components {
560
559
  PostVirtualDataplaneRequest: {
561
560
  /** @description Virtual dataplane display name. Allowed characters are letters, digits, space, underscore, hyphen, apostrophe and ampersand. The name must contain at least one letter or digit, and must not start with a space. */
562
561
  name: string;
562
+ /** @description Globally unique identifier for the virtual dataplane, letters, digits and underscores only. Omit it and the server derives one from the name with a random suffix appended. A slug already in use returns 409. */
563
+ slug?: string;
563
564
  /** @description Physical cluster to host this virtual dataplane. Omit to place it alongside the existing virtual dataplanes in this org; required when the org has none yet, or when its virtual dataplanes span more than one cluster. */
564
565
  cluster_id?: string;
565
566
  /**
@@ -607,6 +608,8 @@ export interface components {
607
608
  PostWorkspaceRequest: {
608
609
  /** @description Workspace display name. Allowed characters are letters, digits, space, underscore, hyphen, apostrophe and ampersand. The name must contain at least one letter or digit, and must not start with a space. */
609
610
  name: string;
611
+ /** @description Globally unique identifier for the workspace, letters, digits and underscores only. Omit it and the server derives one from the name with a random suffix appended. Supply it when the identifier has to match a value maintained outside HoneyHive, such as an identity provider group that grants access to this workspace. A slug already in use returns 409. */
612
+ slug?: string;
610
613
  /** @description Workspace description */
611
614
  description?: string;
612
615
  /**
@@ -899,6 +902,13 @@ export interface operations {
899
902
  };
900
903
  content?: never;
901
904
  };
905
+ /** @description The `slug` is already taken by another scope */
906
+ 409: {
907
+ headers: {
908
+ [name: string]: unknown;
909
+ };
910
+ content?: never;
911
+ };
902
912
  };
903
913
  };
904
914
  createProject: {
@@ -1196,15 +1206,15 @@ export interface operations {
1196
1206
  };
1197
1207
  content?: never;
1198
1208
  };
1199
- /** @description The caller may not create a virtual data plane here: the permission is missing at this organization, the API key's root scope does not contain it, this deployment assigns virtual data planes automatically, or the cluster (named or inferred) does not admit this organization */
1200
- 403: {
1209
+ /** @description Organization or cluster not found, or the caller may not create a virtual data plane here. The response does not say which. */
1210
+ 404: {
1201
1211
  headers: {
1202
1212
  [name: string]: unknown;
1203
1213
  };
1204
1214
  content?: never;
1205
1215
  };
1206
- /** @description Organization or cluster not found */
1207
- 404: {
1216
+ /** @description The `slug` is already taken by another scope */
1217
+ 409: {
1208
1218
  headers: {
1209
1219
  [name: string]: unknown;
1210
1220
  };
@@ -1274,14 +1284,7 @@ export interface operations {
1274
1284
  };
1275
1285
  content?: never;
1276
1286
  };
1277
- /** @description The caller may not update this virtual data plane, or this deployment assigns virtual data planes automatically */
1278
- 403: {
1279
- headers: {
1280
- [name: string]: unknown;
1281
- };
1282
- content?: never;
1283
- };
1284
- /** @description Virtual data plane not found */
1287
+ /** @description Virtual data plane not found, or the caller may not update it, or this deployment assigns virtual data planes automatically. The response does not say which. */
1285
1288
  404: {
1286
1289
  headers: {
1287
1290
  [name: string]: unknown;
@@ -1325,14 +1328,7 @@ export interface operations {
1325
1328
  };
1326
1329
  content?: never;
1327
1330
  };
1328
- /** @description The caller may not delete this virtual data plane, or this deployment assigns virtual data planes automatically */
1329
- 403: {
1330
- headers: {
1331
- [name: string]: unknown;
1332
- };
1333
- content?: never;
1334
- };
1335
- /** @description Virtual data plane not found */
1331
+ /** @description Virtual data plane not found, or the caller may not delete it, or this deployment assigns virtual data planes automatically. The response does not say which. */
1336
1332
  404: {
1337
1333
  headers: {
1338
1334
  [name: string]: unknown;
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit manually. Run `pnpm turbo run generate` to regenerate.
2
2
 
3
- export const SDK_VERSION = '1.1.0';
3
+ export const SDK_VERSION = '1.2.0';
package/src/util.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import axios from 'axios';
2
2
  import createClient, { type ClientOptions, type Middleware } from 'openapi-fetch';
3
3
 
4
+ import { checkControlPlaneApiKey, maskApiKey } from './apiKeys.js';
5
+ import { type ApiClients } from './generated/client.js';
6
+ import { type paths } from './generated/types.js';
4
7
  import { SDK_VERSION } from './generated/version.js';
5
8
 
6
9
  // The control plane's public, API-key entrypoint. This is deliberately not the
@@ -36,68 +39,6 @@ function getEnv(key: string, defaultValue?: string): string | undefined {
36
39
  return defaultValue;
37
40
  }
38
41
 
39
- /**
40
- * The prefix of a fine-grained control-plane API key, whose values have the
41
- * shape `hh_fgcp_<key id>_<key secret>`. In practice this is the only credential
42
- * the control plane's API accepts, but the SDK does not enforce that — the
43
- * prefix's job here is masking: it identifies the values whose key id can be
44
- * shown, and everything else is redacted wholesale.
45
- */
46
- const FGCP_KEY_PREFIX = 'hh_fgcp_';
47
-
48
- /**
49
- * The shape of the key id segment of a fine-grained key value: exactly 24
50
- * alphanumeric characters, with `_` and `-` excluded so that an id can never
51
- * read as two segments.
52
- *
53
- * The length and the alphabet are fixed properties of the key format, so a value
54
- * whose id segment doesn't match this either wasn't issued by HoneyHive or has
55
- * been altered in transit — either way it is redacted rather than rendered.
56
- */
57
- const FGCP_KEY_ID_PATTERN = /^[A-Za-z0-9]{24}$/;
58
-
59
- /**
60
- * Returns a display-safe rendering of an API key for verbose logging.
61
- *
62
- * Renders `hh_fgcp_<key id>_******` — the key's id and none of its secret. This
63
- * is character-for-character the masked form HoneyHive displays for that key, so
64
- * a verbose log line can be matched directly against a key in your account.
65
- * Masking from the id rather than from the secret is what makes it unique per
66
- * key and free of secret material.
67
- *
68
- * The id is `_`-free by construction, so it is everything up to the first `_`
69
- * after the prefix.
70
- *
71
- * Anything else collapses to 8 fixed-width asterisks, revealing neither its
72
- * length nor its content. That covers both a value that isn't a fine-grained key
73
- * at all — the SDK forwards whatever it is given, so this function must assume
74
- * it may be handed a coarse-grained HoneyHive key or a credential belonging to
75
- * some other system entirely — and a fine-grained value whose id segment is
76
- * truncated or mangled, where the characters after the prefix could be secret
77
- * material rather than an id.
78
- *
79
- * Both guards are load-bearing, and the prefix guard especially: without it the
80
- * unconditional slice below would chop 8 characters off an arbitrary token and
81
- * any value whose next 24 characters happened to be alphanumerics followed by
82
- * `_` would render as `hh_fgcp_<24 chars of that token>_******`, echoing part of
83
- * a foreign credential under our own prefix. So the output is always either
84
- * exactly the server's mask or fully redacted, never a partial echo.
85
- */
86
- function maskApiKey(apiKey: string): string {
87
- if (!apiKey.startsWith(FGCP_KEY_PREFIX)) {
88
- return '********';
89
- }
90
- const rest = apiKey.slice(FGCP_KEY_PREFIX.length);
91
- const separator = rest.indexOf('_');
92
- // An empty id never matches the pattern, so a value with no separator at all
93
- // takes the redacted path without a second branch.
94
- const keyId = separator === -1 ? '' : rest.slice(0, separator);
95
- if (!FGCP_KEY_ID_PATTERN.test(keyId)) {
96
- return '********';
97
- }
98
- return `${FGCP_KEY_PREFIX}${keyId}_******`;
99
- }
100
-
101
42
  /**
102
43
  * Configuration options for the HoneyHive Control Plane client. They extend the
103
44
  * options from openapi-fetch, but replace 'baseUrl' with 'controlPlaneUrl' so
@@ -106,9 +47,10 @@ function maskApiKey(apiKey: string): string {
106
47
  export interface ClientConfig extends Omit<ClientOptions, 'baseUrl' | 'headers'> {
107
48
  /**
108
49
  * A fine-grained control-plane API key (`hh_fgcp_…`), the only credential the
109
- * control plane's API accepts in practice. Sent as a bearer token as-is; the
110
- * control plane is what rejects a key it doesn't accept. Defaults to the
111
- * `HH_CONTROL_PLANE_API_KEY` environment variable.
50
+ * control plane's API accepts. Defaults to the `HH_CONTROL_PLANE_API_KEY`
51
+ * environment variable. The value is trimmed, and one that is not a
52
+ * well-formed fine-grained key throws at construction, naming the option or
53
+ * variable it came from. An empty value counts as no key.
112
54
  */
113
55
  apiKey?: string;
114
56
  controlPlaneUrl?: string;
@@ -154,13 +96,18 @@ function querySerializer(queryParams: Record<string, unknown>): string {
154
96
  return uri.startsWith('?') ? uri.slice(1) : uri;
155
97
  }
156
98
 
157
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- needs to match openapi-fetch's own createClient<Paths extends {}> signature
158
- export function createApiClient<Paths extends {}>(
159
- options: ClientConfig,
160
- ): ReturnType<typeof createClient<Paths>> {
99
+ /**
100
+ * Resolves the client's credential and returns the openapi-fetch client that
101
+ * carries it, keyed by the security scheme the spec defines. Every generated
102
+ * method indexes the result by its operation's scheme.
103
+ */
104
+ export function createApiClient(options: ClientConfig): ApiClients {
161
105
  const { apiKey, controlPlaneUrl, middleware, verbose, _internal_provenance, ...clientOptions } =
162
106
  options;
163
- const resolvedApiKey = apiKey ?? getEnv('HH_CONTROL_PLANE_API_KEY');
107
+ // Option > env var. The source travels with the value so the shape check
108
+ // below can name where a bad value came from.
109
+ const [rawApiKey, apiKeySource] =
110
+ apiKey !== undefined ? [apiKey, 'apiKey'] : [getEnv('HH_CONTROL_PLANE_API_KEY'), 'HH_CONTROL_PLANE_API_KEY']; // prettier-ignore
164
111
 
165
112
  // Resolution order: option > env var > default. For the option, any
166
113
  // non-undefined value wins (so explicit undefined falls back). For the env
@@ -177,14 +124,19 @@ export function createApiClient<Paths extends {}>(
177
124
  version: SDK_VERSION,
178
125
  };
179
126
 
180
- // Log before the missing-key check so verbose users can see what *did*
181
- // resolve when construction is about to fail.
127
+ // Log before either check that can throw (a malformed key, a missing key) so
128
+ // verbose users can see what *did* resolve when construction is about to
129
+ // fail. The mask redacts a malformed key wholesale.
182
130
  if (resolvedVerbose) {
183
131
  console.error(`Control plane URL: ${resolvedControlPlaneUrl}`);
184
- console.error(`API key: ${resolvedApiKey ? maskApiKey(resolvedApiKey) : '(none)'}`);
132
+ console.error(`API key: ${rawApiKey ? maskApiKey(rawApiKey) : '(none)'}`);
185
133
  console.error(`Package: ${provenance.package} v${provenance.version}`);
186
134
  }
187
135
 
136
+ // A present key is checked for shape here, at construction, whether or not
137
+ // middleware is supplied. An empty key is no key (below), not a bad one.
138
+ const resolvedApiKey = rawApiKey ? checkControlPlaneApiKey(rawApiKey, apiKeySource) : rawApiKey;
139
+
188
140
  // When middleware is supplied, it is assumed to handle authentication itself
189
141
  // (for example by attaching a short-lived token per request), so no key is
190
142
  // required. The URL always resolves (option > env > default), so only the key
@@ -204,7 +156,7 @@ export function createApiClient<Paths extends {}>(
204
156
  headers.Authorization = `Bearer ${resolvedApiKey}`;
205
157
  }
206
158
 
207
- const client = createClient<Paths>({
159
+ const client = createClient<paths>({
208
160
  ...clientOptions,
209
161
  querySerializer,
210
162
  // Always set (option > env > default). Middleware, when supplied, may still
@@ -221,7 +173,12 @@ export function createApiClient<Paths extends {}>(
221
173
  client.use(...middleware);
222
174
  }
223
175
 
224
- return client;
176
+ // The control plane takes one credential kind, the fine-grained control
177
+ // plane key, so every scheme maps to the one client. Keyed by the schemes
178
+ // the spec defines, so a scheme added to or removed from the spec fails to
179
+ // compile here until this table says which client serves it.
180
+ const clients: ApiClients = { ControlPlaneApiKey: client };
181
+ return clients;
225
182
  }
226
183
 
227
184
  /**