@honeyhive/control-plane-sdk 1.1.0 → 1.3.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/CHANGELOG.md +20 -0
- package/README.md +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/apiKeys.d.ts +46 -0
- package/dist/apiKeys.d.ts.map +1 -0
- package/dist/apiKeys.js +98 -0
- package/dist/apiKeys.js.map +1 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +50 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/apiTypes.d.ts +4 -0
- package/dist/generated/apiTypes.d.ts.map +1 -1
- package/dist/generated/client.d.ts +20 -11
- package/dist/generated/client.d.ts.map +1 -1
- package/dist/generated/client.js +53 -42
- package/dist/generated/client.js.map +1 -1
- package/dist/generated/types.d.ts +22 -26
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/util.d.ts +13 -14
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +24 -75
- package/dist/util.js.map +1 -1
- package/package.json +2 -2
- package/release_notes.md +3 -10
- package/src/apiKeys.ts +105 -0
- package/src/errors.ts +55 -0
- package/src/generated/apiTypes.ts +4 -0
- package/src/generated/client.ts +66 -42
- package/src/generated/types.ts +22 -26
- package/src/generated/version.ts +1 -1
- package/src/index.ts +1 -1
- package/src/util.ts +34 -87
package/src/util.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
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 { HoneyHiveError, MissingApiKeyError } from './errors.js';
|
|
6
|
+
import { type ApiClients } from './generated/client.js';
|
|
7
|
+
import { type paths } from './generated/types.js';
|
|
4
8
|
import { SDK_VERSION } from './generated/version.js';
|
|
5
9
|
|
|
6
10
|
// The control plane's public, API-key entrypoint. This is deliberately not the
|
|
@@ -36,68 +40,6 @@ function getEnv(key: string, defaultValue?: string): string | undefined {
|
|
|
36
40
|
return defaultValue;
|
|
37
41
|
}
|
|
38
42
|
|
|
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
43
|
/**
|
|
102
44
|
* Configuration options for the HoneyHive Control Plane client. They extend the
|
|
103
45
|
* options from openapi-fetch, but replace 'baseUrl' with 'controlPlaneUrl' so
|
|
@@ -106,9 +48,10 @@ function maskApiKey(apiKey: string): string {
|
|
|
106
48
|
export interface ClientConfig extends Omit<ClientOptions, 'baseUrl' | 'headers'> {
|
|
107
49
|
/**
|
|
108
50
|
* A fine-grained control-plane API key (`hh_fgcp_…`), the only credential the
|
|
109
|
-
* control plane's API accepts
|
|
110
|
-
*
|
|
111
|
-
*
|
|
51
|
+
* control plane's API accepts. Defaults to the `HH_CONTROL_PLANE_API_KEY`
|
|
52
|
+
* environment variable. The value is trimmed, and one that is not a
|
|
53
|
+
* well-formed fine-grained key throws at construction, naming the option or
|
|
54
|
+
* variable it came from. An empty value counts as no key.
|
|
112
55
|
*/
|
|
113
56
|
apiKey?: string;
|
|
114
57
|
controlPlaneUrl?: string;
|
|
@@ -154,13 +97,18 @@ function querySerializer(queryParams: Record<string, unknown>): string {
|
|
|
154
97
|
return uri.startsWith('?') ? uri.slice(1) : uri;
|
|
155
98
|
}
|
|
156
99
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Resolves the client's credential and returns the openapi-fetch client that
|
|
102
|
+
* carries it, keyed by the security scheme the spec defines. Every generated
|
|
103
|
+
* method indexes the result by its operation's scheme.
|
|
104
|
+
*/
|
|
105
|
+
export function createApiClient(options: ClientConfig): ApiClients {
|
|
161
106
|
const { apiKey, controlPlaneUrl, middleware, verbose, _internal_provenance, ...clientOptions } =
|
|
162
107
|
options;
|
|
163
|
-
|
|
108
|
+
// Option > env var. The source travels with the value so the shape check
|
|
109
|
+
// below can name where a bad value came from.
|
|
110
|
+
const [rawApiKey, apiKeySource] =
|
|
111
|
+
apiKey !== undefined ? [apiKey, 'apiKey'] : [getEnv('HH_CONTROL_PLANE_API_KEY'), 'HH_CONTROL_PLANE_API_KEY']; // prettier-ignore
|
|
164
112
|
|
|
165
113
|
// Resolution order: option > env var > default. For the option, any
|
|
166
114
|
// non-undefined value wins (so explicit undefined falls back). For the env
|
|
@@ -177,22 +125,25 @@ export function createApiClient<Paths extends {}>(
|
|
|
177
125
|
version: SDK_VERSION,
|
|
178
126
|
};
|
|
179
127
|
|
|
180
|
-
// Log before
|
|
181
|
-
// resolve when construction is about to
|
|
128
|
+
// Log before either check that can throw (a malformed key, a missing key) so
|
|
129
|
+
// verbose users can see what *did* resolve when construction is about to
|
|
130
|
+
// fail. The mask redacts a malformed key wholesale.
|
|
182
131
|
if (resolvedVerbose) {
|
|
183
132
|
console.error(`Control plane URL: ${resolvedControlPlaneUrl}`);
|
|
184
|
-
console.error(`API key: ${
|
|
133
|
+
console.error(`API key: ${rawApiKey ? maskApiKey(rawApiKey) : '(none)'}`);
|
|
185
134
|
console.error(`Package: ${provenance.package} v${provenance.version}`);
|
|
186
135
|
}
|
|
187
136
|
|
|
137
|
+
// A present key is checked for shape here, at construction, whether or not
|
|
138
|
+
// middleware is supplied. An empty key is no key (below), not a bad one.
|
|
139
|
+
const resolvedApiKey = rawApiKey ? checkControlPlaneApiKey(rawApiKey, apiKeySource) : rawApiKey;
|
|
140
|
+
|
|
188
141
|
// When middleware is supplied, it is assumed to handle authentication itself
|
|
189
142
|
// (for example by attaching a short-lived token per request), so no key is
|
|
190
143
|
// required. The URL always resolves (option > env > default), so only the key
|
|
191
144
|
// can be missing here.
|
|
192
145
|
if (!resolvedApiKey && !middleware?.length) {
|
|
193
|
-
throw new
|
|
194
|
-
'Missing API key: provide apiKey in options or set the HH_CONTROL_PLANE_API_KEY environment variable',
|
|
195
|
-
);
|
|
146
|
+
throw new MissingApiKeyError();
|
|
196
147
|
}
|
|
197
148
|
|
|
198
149
|
const headers: Record<string, string> = {
|
|
@@ -204,7 +155,7 @@ export function createApiClient<Paths extends {}>(
|
|
|
204
155
|
headers.Authorization = `Bearer ${resolvedApiKey}`;
|
|
205
156
|
}
|
|
206
157
|
|
|
207
|
-
const client = createClient<
|
|
158
|
+
const client = createClient<paths>({
|
|
208
159
|
...clientOptions,
|
|
209
160
|
querySerializer,
|
|
210
161
|
// Always set (option > env > default). Middleware, when supplied, may still
|
|
@@ -221,7 +172,12 @@ export function createApiClient<Paths extends {}>(
|
|
|
221
172
|
client.use(...middleware);
|
|
222
173
|
}
|
|
223
174
|
|
|
224
|
-
|
|
175
|
+
// The control plane takes one credential kind, the fine-grained control
|
|
176
|
+
// plane key, so every scheme maps to the one client. Keyed by the schemes
|
|
177
|
+
// the spec defines, so a scheme added to or removed from the spec fails to
|
|
178
|
+
// compile here until this table says which client serves it.
|
|
179
|
+
const clients: ApiClients = { ControlPlaneApiKey: client };
|
|
180
|
+
return clients;
|
|
225
181
|
}
|
|
226
182
|
|
|
227
183
|
/**
|
|
@@ -253,15 +209,6 @@ type FetchResult<T = unknown, E = unknown> =
|
|
|
253
209
|
| { data: T; error?: undefined; response: Response }
|
|
254
210
|
| { data?: undefined; error: E; response: Response };
|
|
255
211
|
|
|
256
|
-
/**
|
|
257
|
-
* HoneyHiveError is a base class for all errors thrown by the HoneyHive control
|
|
258
|
-
* plane client.
|
|
259
|
-
*
|
|
260
|
-
* This error is never thrown directly, but is useful for determining if an
|
|
261
|
-
* error is from the client with `err instanceof HoneyHiveError`
|
|
262
|
-
*/
|
|
263
|
-
export class HoneyHiveError extends Error {}
|
|
264
|
-
|
|
265
212
|
/**
|
|
266
213
|
* Type guard that returns the payload as ErrorResponse if it matches the
|
|
267
214
|
* canonical shape, or undefined otherwise.
|