@hush-hush/sdk 3.0.0 → 4.1.1

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![ci](https://github.com/alrayyes/hush-hush-node/actions/workflows/ci.yml/badge.svg)](https://github.com/alrayyes/hush-hush-node/actions/workflows/ci.yml)
4
4
  [![Codecov](https://codecov.io/gh/alrayyes/hush-hush-node/graph/badge.svg)](https://codecov.io/gh/alrayyes/hush-hush-node)
5
- [![npm](https://img.shields.io/npm/v/hush-hush)](https://www.npmjs.com/package/hush-hush)
5
+ [![npm](https://img.shields.io/npm/v/%40hush-hush%2Fsdk)](https://www.npmjs.com/package/@hush-hush/sdk)
6
6
  [![release](https://img.shields.io/github/v/release/alrayyes/hush-hush-node)](https://github.com/alrayyes/hush-hush-node/releases)
7
7
  [![license](https://img.shields.io/github/license/alrayyes/hush-hush-node)](LICENSE)
8
8
 
@@ -13,7 +13,7 @@ OpenAPI spec and kept in sync with it automatically.
13
13
  ## Install
14
14
 
15
15
  ```sh
16
- npm install hush-hush
16
+ npm install @hush-hush/sdk
17
17
  ```
18
18
 
19
19
  Requires Node.js 22 or newer.
@@ -21,7 +21,7 @@ Requires Node.js 22 or newer.
21
21
  ## Quickstart
22
22
 
23
23
  ```ts
24
- import { Client } from "hush-hush";
24
+ import { Client } from "@hush-hush/sdk";
25
25
 
26
26
  const client = new Client("https://hush-hush.example.com", {
27
27
  apiKey: "your-api-key", // or set HUSH_HUSH_API_KEY
package/dist/index.cjs ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ APIError: () => APIError,
24
+ Client: () => Client,
25
+ HushHushError: () => HushHushError
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/errors.ts
30
+ var HushHushError = class extends Error {
31
+ };
32
+ var APIError = class _APIError extends HushHushError {
33
+ /** The HTTP status hush-hush responded with. */
34
+ status;
35
+ /** The response's request-ID header, when hush-hush's spec documents one for it. */
36
+ requestId;
37
+ /** The parsed `error` field from hush-hush's error body, if present. */
38
+ apiMessage;
39
+ /** The raw, unparsed response body, for a caller that needs more than `apiMessage`. */
40
+ body;
41
+ constructor(status, body, requestId) {
42
+ const apiMessage = _APIError.parseMessage(body);
43
+ super(
44
+ apiMessage !== void 0 ? `hush-hush: ${status}: ${apiMessage}` : `hush-hush: unexpected status ${status}`
45
+ );
46
+ this.name = "APIError";
47
+ this.status = status;
48
+ this.requestId = requestId;
49
+ this.apiMessage = apiMessage;
50
+ this.body = body;
51
+ }
52
+ static parseMessage(body) {
53
+ try {
54
+ const parsed = JSON.parse(new TextDecoder().decode(body));
55
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
56
+ const value = parsed.error;
57
+ if (typeof value === "string") return value;
58
+ }
59
+ } catch {
60
+ }
61
+ return void 0;
62
+ }
63
+ };
64
+
65
+ // src/retry.ts
66
+ var DEFAULT_MAX_RETRIES = 3;
67
+ function isRetryableStatus(status) {
68
+ return status >= 500 || status === 429;
69
+ }
70
+ function retryAfterMs(response) {
71
+ const value = response.headers.get("retry-after");
72
+ if (!value) return void 0;
73
+ const seconds = Number(value);
74
+ if (Number.isFinite(seconds)) {
75
+ return seconds >= 0 ? seconds * 1e3 : void 0;
76
+ }
77
+ const when = Date.parse(value);
78
+ if (Number.isNaN(when)) return void 0;
79
+ return Math.max(when - Date.now(), 0);
80
+ }
81
+ function backoffMs(attempt, retryAfterOverrideMs) {
82
+ if (retryAfterOverrideMs !== void 0) return retryAfterOverrideMs;
83
+ const base = 100 * 2 ** (attempt - 1);
84
+ return base + Math.random() * base;
85
+ }
86
+ function delay(ms) {
87
+ return new Promise((resolve) => setTimeout(resolve, ms));
88
+ }
89
+ async function fetchWithRetry(fetchImpl, input, init, maxRetries) {
90
+ let nextDelayOverrideMs;
91
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
92
+ if (attempt > 0) {
93
+ await delay(backoffMs(attempt, nextDelayOverrideMs));
94
+ nextDelayOverrideMs = void 0;
95
+ }
96
+ let response;
97
+ try {
98
+ response = await fetchImpl(input, init);
99
+ } catch (err) {
100
+ if (attempt === maxRetries) throw err;
101
+ continue;
102
+ }
103
+ if (!isRetryableStatus(response.status) || attempt === maxRetries) {
104
+ return response;
105
+ }
106
+ nextDelayOverrideMs = retryAfterMs(response);
107
+ await response.body?.cancel();
108
+ }
109
+ throw new Error("fetchWithRetry: unreachable \u2014 maxRetries must be >= 0");
110
+ }
111
+
112
+ // src/client.ts
113
+ var API_KEY_ENV_VAR = "HUSH_HUSH_API_KEY";
114
+ var DEFAULT_TIMEOUT_MS = 3e4;
115
+ var Client = class {
116
+ baseUrl;
117
+ apiKey;
118
+ timeoutMs;
119
+ maxRetries;
120
+ fetchImpl;
121
+ /**
122
+ * @param baseUrl - hush-hush's base URL, e.g. `https://hush-hush.example.com`.
123
+ * @param options - Credential, timeout, and retry configuration.
124
+ */
125
+ constructor(baseUrl, options = {}) {
126
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
127
+ this.apiKey = options.apiKey ?? process.env[API_KEY_ENV_VAR];
128
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
129
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
130
+ this.fetchImpl = options.fetch ?? fetch;
131
+ }
132
+ /** Answers whether the server process is up. Needs no credential. */
133
+ async health() {
134
+ const response = await this.request("GET", "/healthz");
135
+ return await response.json();
136
+ }
137
+ /**
138
+ * Stores an already-sealed value under a new object id. Requires a credential.
139
+ *
140
+ * @param id - The new object's id. Must match hush-hush's id pattern (lowercase alphanumeric, `-`/`_`).
141
+ * @param value - The already-sealed (encrypted) value. This SDK never encrypts or decrypts anything.
142
+ * @param options.usedBy - Consumers (repos or hosts) recorded as depending on this object.
143
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
144
+ * @throws {APIError} If the server responds with anything other than 201 (e.g. 409 if the id exists).
145
+ */
146
+ async createObject(id, value, options = {}) {
147
+ const response = await this.request("POST", "/objects", {
148
+ authenticated: true,
149
+ caller: options.caller,
150
+ jsonBody: {
151
+ id,
152
+ value: base64Encode(value),
153
+ ...options.usedBy !== void 0 ? { used_by: options.usedBy } : {}
154
+ }
155
+ });
156
+ return await response.json();
157
+ }
158
+ /**
159
+ * Fetches an object's sealed ciphertext exactly as stored — this SDK never
160
+ * decrypts it, the same as the server. Needs no credential.
161
+ *
162
+ * @param id - The object's id.
163
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
164
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
165
+ */
166
+ async getObject(id, options = {}) {
167
+ const response = await this.request("GET", `/objects/${encodeURIComponent(id)}`, {
168
+ caller: options.caller
169
+ });
170
+ return new Uint8Array(await response.arrayBuffer());
171
+ }
172
+ /**
173
+ * Replaces the stored ciphertext for an existing object. The object's id
174
+ * and used-by metadata are unchanged. Requires a credential.
175
+ *
176
+ * @param id - The existing object's id.
177
+ * @param value - The new already-sealed (encrypted) value.
178
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
179
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 401 or 404).
180
+ */
181
+ async updateObject(id, value, options = {}) {
182
+ const response = await this.request("PUT", `/objects/${encodeURIComponent(id)}`, {
183
+ authenticated: true,
184
+ caller: options.caller,
185
+ jsonBody: { value: base64Encode(value) }
186
+ });
187
+ return await response.json();
188
+ }
189
+ /**
190
+ * Permanently removes an object. A subsequent fetch by this id returns 404. Requires a credential.
191
+ *
192
+ * @param id - The object's id.
193
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
194
+ * @throws {APIError} If the server responds with anything other than 204 (e.g. 401 or 404).
195
+ */
196
+ async deleteObject(id, options = {}) {
197
+ await this.request("DELETE", `/objects/${encodeURIComponent(id)}`, {
198
+ authenticated: true,
199
+ caller: options.caller
200
+ });
201
+ }
202
+ /**
203
+ * Returns the recorded list of consumers for an object — the "what
204
+ * depends on this" mapping set at creation. Needs no credential.
205
+ *
206
+ * @param id - The object's id.
207
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
208
+ */
209
+ async getObjectUsedBy(id) {
210
+ const response = await this.request("GET", `/objects/${encodeURIComponent(id)}/used-by`);
211
+ return await response.json();
212
+ }
213
+ /**
214
+ * Queries the audit log — every create, read, update, and delete call is
215
+ * recorded here. Needs no credential. Filters combine with AND when more
216
+ * than one is given.
217
+ *
218
+ * hush-hush's `/audit-log` endpoint has no pagination parameters, so this
219
+ * always resolves with the full matching result set as a single array,
220
+ * never a page plus a cursor.
221
+ *
222
+ * @param filter - Optional `objectId`/`caller`/`from`/`to` filters.
223
+ */
224
+ async queryAuditLog(filter = {}) {
225
+ const response = await this.request("GET", "/audit-log", {
226
+ query: {
227
+ object_id: filter.objectId,
228
+ caller: filter.caller,
229
+ from: filter.from,
230
+ to: filter.to
231
+ }
232
+ });
233
+ return await response.json();
234
+ }
235
+ async request(method, path, options = {}) {
236
+ const url = new URL(`${this.baseUrl}${path}`);
237
+ for (const [key, value] of Object.entries(options.query ?? {})) {
238
+ if (value !== void 0) url.searchParams.set(key, value);
239
+ }
240
+ const headers = new Headers();
241
+ if (options.caller !== void 0) headers.set("X-Caller", options.caller);
242
+ if (options.authenticated === true && this.apiKey !== void 0) {
243
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
244
+ }
245
+ let body = options.body;
246
+ if (options.jsonBody !== void 0) {
247
+ headers.set("Content-Type", "application/json");
248
+ body = JSON.stringify(options.jsonBody);
249
+ }
250
+ const controller = new AbortController();
251
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
252
+ try {
253
+ const response = await fetchWithRetry(
254
+ this.fetchImpl,
255
+ url.toString(),
256
+ { method, headers, signal: controller.signal, ...body !== void 0 ? { body } : {} },
257
+ this.maxRetries
258
+ );
259
+ if (!response.ok) {
260
+ const responseBody = new Uint8Array(await response.arrayBuffer());
261
+ throw new APIError(
262
+ response.status,
263
+ responseBody,
264
+ response.headers.get("x-request-id") ?? void 0
265
+ );
266
+ }
267
+ return response;
268
+ } finally {
269
+ clearTimeout(timeout);
270
+ }
271
+ }
272
+ };
273
+ function base64Encode(value) {
274
+ return Buffer.from(value).toString("base64");
275
+ }
276
+ // Annotate the CommonJS export names for ESM import in node:
277
+ 0 && (module.exports = {
278
+ APIError,
279
+ Client,
280
+ HushHushError
281
+ });
282
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/retry.ts","../src/client.ts"],"sourcesContent":["export type {\n AuditLogAction,\n AuditLogEntry,\n AuditLogFilter,\n ClientOptions,\n Health,\n ObjectMetadata,\n UsedBy,\n} from \"./client.js\";\nexport { Client } from \"./client.js\";\nexport { APIError, HushHushError } from \"./errors.js\";\n","/** Base class for every error this SDK raises. */\nexport class HushHushError extends Error {}\n\n/**\n * Raised for any non-2xx response from hush-hush.\n *\n * `requestId` is populated only when the response carries a documented\n * request-ID header; hush-hush's spec doesn't currently document one, so\n * this is usually `undefined`. Kept as a property rather than omitted so a\n * future spec addition doesn't change this type's shape.\n */\nexport class APIError extends HushHushError {\n /** The HTTP status hush-hush responded with. */\n readonly status: number;\n /** The response's request-ID header, when hush-hush's spec documents one for it. */\n readonly requestId: string | undefined;\n /** The parsed `error` field from hush-hush's error body, if present. */\n readonly apiMessage: string | undefined;\n /** The raw, unparsed response body, for a caller that needs more than `apiMessage`. */\n readonly body: Uint8Array;\n\n constructor(status: number, body: Uint8Array, requestId: string | undefined) {\n const apiMessage = APIError.parseMessage(body);\n super(\n apiMessage !== undefined\n ? `hush-hush: ${status}: ${apiMessage}`\n : `hush-hush: unexpected status ${status}`,\n );\n this.name = \"APIError\";\n this.status = status;\n this.requestId = requestId;\n this.apiMessage = apiMessage;\n this.body = body;\n }\n\n private static parseMessage(body: Uint8Array): string | undefined {\n try {\n const parsed: unknown = JSON.parse(new TextDecoder().decode(body));\n if (typeof parsed === \"object\" && parsed !== null && \"error\" in parsed) {\n const value = (parsed as { error?: unknown }).error;\n if (typeof value === \"string\") return value;\n }\n } catch {\n // Not JSON, or not decodable as UTF-8 — apiMessage stays undefined.\n }\n return undefined;\n }\n}\n","/**\n * Retries a request only on network failure or an HTTP 5xx/429 response,\n * using exponential backoff with jitter, and honors a `Retry-After` response\n * header ahead of the computed backoff delay when present. Any other 4xx is\n * never retried — it won't succeed on a second attempt, and retrying only\n * delays the real error reaching the caller.\n */\n\nexport const DEFAULT_MAX_RETRIES = 3;\n\nfunction isRetryableStatus(status: number): boolean {\n return status >= 500 || status === 429;\n}\n\nfunction retryAfterMs(response: Response): number | undefined {\n const value = response.headers.get(\"retry-after\");\n if (!value) return undefined;\n\n const seconds = Number(value);\n if (Number.isFinite(seconds)) {\n return seconds >= 0 ? seconds * 1000 : undefined;\n }\n\n const when = Date.parse(value);\n if (Number.isNaN(when)) return undefined;\n return Math.max(when - Date.now(), 0);\n}\n\nfunction backoffMs(attempt: number, retryAfterOverrideMs: number | undefined): number {\n if (retryAfterOverrideMs !== undefined) return retryAfterOverrideMs;\n const base = 100 * 2 ** (attempt - 1);\n return base + Math.random() * base;\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport async function fetchWithRetry(\n fetchImpl: typeof fetch,\n input: string,\n init: RequestInit,\n maxRetries: number,\n): Promise<Response> {\n let nextDelayOverrideMs: number | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n if (attempt > 0) {\n await delay(backoffMs(attempt, nextDelayOverrideMs));\n nextDelayOverrideMs = undefined;\n }\n\n let response: Response;\n try {\n response = await fetchImpl(input, init);\n } catch (err) {\n if (attempt === maxRetries) throw err;\n continue;\n }\n\n if (!isRetryableStatus(response.status) || attempt === maxRetries) {\n return response;\n }\n nextDelayOverrideMs = retryAfterMs(response);\n await response.body?.cancel();\n }\n\n throw new Error(\"fetchWithRetry: unreachable — maxRetries must be >= 0\");\n}\n","import { APIError } from \"./errors.js\";\nimport type { components } from \"./generated/types.js\";\nimport { DEFAULT_MAX_RETRIES, fetchWithRetry } from \"./retry.js\";\n\ntype _ObjectMetadata = components[\"schemas\"][\"ObjectMetadata\"];\ntype _UsedBy = components[\"schemas\"][\"UsedBy\"];\ntype _Health = components[\"schemas\"][\"Health\"];\ntype _AuditLogEntry = components[\"schemas\"][\"AuditLogEntry\"];\n\n/** An object's id and its recorded consumers. */\nexport interface ObjectMetadata extends _ObjectMetadata {}\n/** The consumers (repos or hosts) recorded as depending on an object. */\nexport interface UsedBy extends _UsedBy {}\n/** hush-hush's liveness response. */\nexport interface Health extends _Health {}\n/** One recorded create, read, update, or delete call. */\nexport interface AuditLogEntry extends _AuditLogEntry {}\n/** The kind of call an {@link AuditLogEntry} recorded. */\nexport type AuditLogAction = AuditLogEntry[\"action\"];\n\nconst API_KEY_ENV_VAR = \"HUSH_HUSH_API_KEY\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/** Options accepted by the {@link Client} constructor. */\nexport interface ClientOptions {\n /**\n * Bearer credential for write paths (create/update/delete). Falls back to\n * the `HUSH_HUSH_API_KEY` environment variable when not supplied. Read\n * paths (get, used-by, audit-log query) need no credential at all.\n */\n apiKey?: string;\n /** Per-request timeout, in milliseconds. Defaults to 30000. */\n timeoutMs?: number;\n /** Maximum retry attempts for network failures and 5xx/429 responses. Defaults to 3. */\n maxRetries?: number;\n /** Override for the `fetch` implementation, mainly for tests. Defaults to global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/** Optional filters for {@link Client.queryAuditLog}. Filters combine with AND when more than one is set. */\nexport interface AuditLogFilter {\n /** Restrict to entries for this object id. */\n objectId?: string;\n /** Restrict to entries recorded with this caller identity. */\n caller?: string;\n /** Restrict to entries at or after this ISO-8601 timestamp. */\n from?: string;\n /** Restrict to entries at or before this ISO-8601 timestamp. */\n to?: string;\n}\n\ninterface RequestOptions {\n authenticated?: boolean;\n caller?: string | undefined;\n query?: Record<string, string | undefined>;\n body?: RequestInit[\"body\"];\n jsonBody?: unknown;\n}\n\n/**\n * A typed client for hush-hush, a standalone secrets object store.\n *\n * @example\n * ```ts\n * const client = new Client(\"https://hush-hush.example.com\");\n * const meta = await client.createObject(\"my-object\", sealedBytes);\n * ```\n */\nexport class Client {\n private readonly baseUrl: string;\n private readonly apiKey: string | undefined;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n /**\n * @param baseUrl - hush-hush's base URL, e.g. `https://hush-hush.example.com`.\n * @param options - Credential, timeout, and retry configuration.\n */\n constructor(baseUrl: string, options: ClientOptions = {}) {\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = options.apiKey ?? process.env[API_KEY_ENV_VAR];\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n /** Answers whether the server process is up. Needs no credential. */\n async health(): Promise<Health> {\n const response = await this.request(\"GET\", \"/healthz\");\n return (await response.json()) as Health;\n }\n\n /**\n * Stores an already-sealed value under a new object id. Requires a credential.\n *\n * @param id - The new object's id. Must match hush-hush's id pattern (lowercase alphanumeric, `-`/`_`).\n * @param value - The already-sealed (encrypted) value. This SDK never encrypts or decrypts anything.\n * @param options.usedBy - Consumers (repos or hosts) recorded as depending on this object.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 201 (e.g. 409 if the id exists).\n */\n async createObject(\n id: string,\n value: Uint8Array,\n options: { usedBy?: string[]; caller?: string } = {},\n ): Promise<ObjectMetadata> {\n const response = await this.request(\"POST\", \"/objects\", {\n authenticated: true,\n caller: options.caller,\n jsonBody: {\n id,\n value: base64Encode(value),\n ...(options.usedBy !== undefined ? { used_by: options.usedBy } : {}),\n },\n });\n return (await response.json()) as ObjectMetadata;\n }\n\n /**\n * Fetches an object's sealed ciphertext exactly as stored — this SDK never\n * decrypts it, the same as the server. Needs no credential.\n *\n * @param id - The object's id.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).\n */\n async getObject(id: string, options: { caller?: string } = {}): Promise<Uint8Array> {\n const response = await this.request(\"GET\", `/objects/${encodeURIComponent(id)}`, {\n caller: options.caller,\n });\n return new Uint8Array(await response.arrayBuffer());\n }\n\n /**\n * Replaces the stored ciphertext for an existing object. The object's id\n * and used-by metadata are unchanged. Requires a credential.\n *\n * @param id - The existing object's id.\n * @param value - The new already-sealed (encrypted) value.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 200 (e.g. 401 or 404).\n */\n async updateObject(\n id: string,\n value: Uint8Array,\n options: { caller?: string } = {},\n ): Promise<ObjectMetadata> {\n const response = await this.request(\"PUT\", `/objects/${encodeURIComponent(id)}`, {\n authenticated: true,\n caller: options.caller,\n jsonBody: { value: base64Encode(value) },\n });\n return (await response.json()) as ObjectMetadata;\n }\n\n /**\n * Permanently removes an object. A subsequent fetch by this id returns 404. Requires a credential.\n *\n * @param id - The object's id.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 204 (e.g. 401 or 404).\n */\n async deleteObject(id: string, options: { caller?: string } = {}): Promise<void> {\n await this.request(\"DELETE\", `/objects/${encodeURIComponent(id)}`, {\n authenticated: true,\n caller: options.caller,\n });\n }\n\n /**\n * Returns the recorded list of consumers for an object — the \"what\n * depends on this\" mapping set at creation. Needs no credential.\n *\n * @param id - The object's id.\n * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).\n */\n async getObjectUsedBy(id: string): Promise<UsedBy> {\n const response = await this.request(\"GET\", `/objects/${encodeURIComponent(id)}/used-by`);\n return (await response.json()) as UsedBy;\n }\n\n /**\n * Queries the audit log — every create, read, update, and delete call is\n * recorded here. Needs no credential. Filters combine with AND when more\n * than one is given.\n *\n * hush-hush's `/audit-log` endpoint has no pagination parameters, so this\n * always resolves with the full matching result set as a single array,\n * never a page plus a cursor.\n *\n * @param filter - Optional `objectId`/`caller`/`from`/`to` filters.\n */\n async queryAuditLog(filter: AuditLogFilter = {}): Promise<AuditLogEntry[]> {\n const response = await this.request(\"GET\", \"/audit-log\", {\n query: {\n object_id: filter.objectId,\n caller: filter.caller,\n from: filter.from,\n to: filter.to,\n },\n });\n return (await response.json()) as AuditLogEntry[];\n }\n\n private async request(\n method: string,\n path: string,\n options: RequestOptions = {},\n ): Promise<Response> {\n const url = new URL(`${this.baseUrl}${path}`);\n for (const [key, value] of Object.entries(options.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n\n const headers = new Headers();\n if (options.caller !== undefined) headers.set(\"X-Caller\", options.caller);\n if (options.authenticated === true && this.apiKey !== undefined) {\n headers.set(\"Authorization\", `Bearer ${this.apiKey}`);\n }\n\n let body: RequestInit[\"body\"] = options.body;\n if (options.jsonBody !== undefined) {\n headers.set(\"Content-Type\", \"application/json\");\n body = JSON.stringify(options.jsonBody);\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n const response = await fetchWithRetry(\n this.fetchImpl,\n url.toString(),\n { method, headers, signal: controller.signal, ...(body !== undefined ? { body } : {}) },\n this.maxRetries,\n );\n if (!response.ok) {\n const responseBody = new Uint8Array(await response.arrayBuffer());\n throw new APIError(\n response.status,\n responseBody,\n response.headers.get(\"x-request-id\") ?? undefined,\n );\n }\n return response;\n } finally {\n clearTimeout(timeout);\n }\n }\n}\n\nfunction base64Encode(value: Uint8Array): string {\n return Buffer.from(value).toString(\"base64\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,276 @@
1
+ interface components {
2
+ schemas: {
3
+ /** @example mattermost_deploy_webhook */
4
+ ObjectId: string;
5
+ CreateObjectRequest: {
6
+ id: components["schemas"]["ObjectId"];
7
+ /**
8
+ * Format: byte
9
+ * @description The sealed (encrypted) value, base64-encoded.
10
+ */
11
+ value: string;
12
+ used_by?: components["schemas"]["UsedByList"];
13
+ };
14
+ UpdateObjectRequest: {
15
+ /**
16
+ * Format: byte
17
+ * @description The new sealed (encrypted) value, base64-encoded.
18
+ */
19
+ value: string;
20
+ };
21
+ ObjectMetadata: {
22
+ id: components["schemas"]["ObjectId"];
23
+ used_by?: components["schemas"]["UsedByList"];
24
+ };
25
+ /**
26
+ * @description The consumers (repos or hosts) recorded as depending on this
27
+ * object. Set at creation; unaffected by later value updates.
28
+ * @example [
29
+ * "homelab/vps-docker"
30
+ * ]
31
+ */
32
+ UsedByList: string[];
33
+ UsedBy: {
34
+ used_by: components["schemas"]["UsedByList"];
35
+ };
36
+ AuditLogEntry: {
37
+ object_id: components["schemas"]["ObjectId"];
38
+ /** @enum {string} */
39
+ action: "create" | "read" | "update" | "delete";
40
+ /** Format: date-time */
41
+ timestamp: string;
42
+ /** @description The caller's presented identity, if any. */
43
+ caller?: string;
44
+ /**
45
+ * @description The request's source IP. Unlike caller, this is never
46
+ * self-reported - it's the one piece of the request nobody gets
47
+ * to lie about via a header. Currently the immediate TCP peer's
48
+ * address; this service isn't deployed behind a reverse proxy or
49
+ * load balancer, so X-Forwarded-For (or similar) support isn't
50
+ * implemented yet.
51
+ */
52
+ ip: string;
53
+ };
54
+ Health: {
55
+ /** @constant */
56
+ status: "ok";
57
+ };
58
+ Error: {
59
+ /**
60
+ * @description What went wrong, in terms safe to show a caller. Nothing here
61
+ * handles authentication with a detail worth holding back.
62
+ * @example unknown object
63
+ */
64
+ error: string;
65
+ };
66
+ };
67
+ responses: {
68
+ /** @description The request is missing a valid bearer token. */
69
+ Unauthorized: {
70
+ headers: {
71
+ [name: string]: unknown;
72
+ };
73
+ content: {
74
+ /**
75
+ * @example {
76
+ * "error": "missing or invalid bearer token"
77
+ * }
78
+ */
79
+ "application/json": components["schemas"]["Error"];
80
+ };
81
+ };
82
+ /** @description No object is stored under that id. */
83
+ NotFound: {
84
+ headers: {
85
+ [name: string]: unknown;
86
+ };
87
+ content: {
88
+ /**
89
+ * @example {
90
+ * "error": "unknown object"
91
+ * }
92
+ */
93
+ "application/json": components["schemas"]["Error"];
94
+ };
95
+ };
96
+ };
97
+ parameters: {
98
+ /**
99
+ * @description The object's id, which is the last path segment and so has to
100
+ * survive being in a URL.
101
+ */
102
+ id: components["schemas"]["ObjectId"];
103
+ /**
104
+ * @description The caller's own identity - a repo or host name, whatever the
105
+ * caller wants attributed to it in the audit log. Not authenticated
106
+ * or verified: a courtesy label a caller presents about itself, not
107
+ * an identity check the service performs. Absent means the
108
+ * resulting audit log entry's caller field is empty.
109
+ */
110
+ caller: string;
111
+ };
112
+ requestBodies: never;
113
+ headers: never;
114
+ pathItems: never;
115
+ }
116
+
117
+ type _ObjectMetadata = components["schemas"]["ObjectMetadata"];
118
+ type _UsedBy = components["schemas"]["UsedBy"];
119
+ type _Health = components["schemas"]["Health"];
120
+ type _AuditLogEntry = components["schemas"]["AuditLogEntry"];
121
+ /** An object's id and its recorded consumers. */
122
+ interface ObjectMetadata extends _ObjectMetadata {
123
+ }
124
+ /** The consumers (repos or hosts) recorded as depending on an object. */
125
+ interface UsedBy extends _UsedBy {
126
+ }
127
+ /** hush-hush's liveness response. */
128
+ interface Health extends _Health {
129
+ }
130
+ /** One recorded create, read, update, or delete call. */
131
+ interface AuditLogEntry extends _AuditLogEntry {
132
+ }
133
+ /** The kind of call an {@link AuditLogEntry} recorded. */
134
+ type AuditLogAction = AuditLogEntry["action"];
135
+ /** Options accepted by the {@link Client} constructor. */
136
+ interface ClientOptions {
137
+ /**
138
+ * Bearer credential for write paths (create/update/delete). Falls back to
139
+ * the `HUSH_HUSH_API_KEY` environment variable when not supplied. Read
140
+ * paths (get, used-by, audit-log query) need no credential at all.
141
+ */
142
+ apiKey?: string;
143
+ /** Per-request timeout, in milliseconds. Defaults to 30000. */
144
+ timeoutMs?: number;
145
+ /** Maximum retry attempts for network failures and 5xx/429 responses. Defaults to 3. */
146
+ maxRetries?: number;
147
+ /** Override for the `fetch` implementation, mainly for tests. Defaults to global `fetch`. */
148
+ fetch?: typeof fetch;
149
+ }
150
+ /** Optional filters for {@link Client.queryAuditLog}. Filters combine with AND when more than one is set. */
151
+ interface AuditLogFilter {
152
+ /** Restrict to entries for this object id. */
153
+ objectId?: string;
154
+ /** Restrict to entries recorded with this caller identity. */
155
+ caller?: string;
156
+ /** Restrict to entries at or after this ISO-8601 timestamp. */
157
+ from?: string;
158
+ /** Restrict to entries at or before this ISO-8601 timestamp. */
159
+ to?: string;
160
+ }
161
+ /**
162
+ * A typed client for hush-hush, a standalone secrets object store.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const client = new Client("https://hush-hush.example.com");
167
+ * const meta = await client.createObject("my-object", sealedBytes);
168
+ * ```
169
+ */
170
+ declare class Client {
171
+ private readonly baseUrl;
172
+ private readonly apiKey;
173
+ private readonly timeoutMs;
174
+ private readonly maxRetries;
175
+ private readonly fetchImpl;
176
+ /**
177
+ * @param baseUrl - hush-hush's base URL, e.g. `https://hush-hush.example.com`.
178
+ * @param options - Credential, timeout, and retry configuration.
179
+ */
180
+ constructor(baseUrl: string, options?: ClientOptions);
181
+ /** Answers whether the server process is up. Needs no credential. */
182
+ health(): Promise<Health>;
183
+ /**
184
+ * Stores an already-sealed value under a new object id. Requires a credential.
185
+ *
186
+ * @param id - The new object's id. Must match hush-hush's id pattern (lowercase alphanumeric, `-`/`_`).
187
+ * @param value - The already-sealed (encrypted) value. This SDK never encrypts or decrypts anything.
188
+ * @param options.usedBy - Consumers (repos or hosts) recorded as depending on this object.
189
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
190
+ * @throws {APIError} If the server responds with anything other than 201 (e.g. 409 if the id exists).
191
+ */
192
+ createObject(id: string, value: Uint8Array, options?: {
193
+ usedBy?: string[];
194
+ caller?: string;
195
+ }): Promise<ObjectMetadata>;
196
+ /**
197
+ * Fetches an object's sealed ciphertext exactly as stored — this SDK never
198
+ * decrypts it, the same as the server. Needs no credential.
199
+ *
200
+ * @param id - The object's id.
201
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
202
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
203
+ */
204
+ getObject(id: string, options?: {
205
+ caller?: string;
206
+ }): Promise<Uint8Array>;
207
+ /**
208
+ * Replaces the stored ciphertext for an existing object. The object's id
209
+ * and used-by metadata are unchanged. Requires a credential.
210
+ *
211
+ * @param id - The existing object's id.
212
+ * @param value - The new already-sealed (encrypted) value.
213
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
214
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 401 or 404).
215
+ */
216
+ updateObject(id: string, value: Uint8Array, options?: {
217
+ caller?: string;
218
+ }): Promise<ObjectMetadata>;
219
+ /**
220
+ * Permanently removes an object. A subsequent fetch by this id returns 404. Requires a credential.
221
+ *
222
+ * @param id - The object's id.
223
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
224
+ * @throws {APIError} If the server responds with anything other than 204 (e.g. 401 or 404).
225
+ */
226
+ deleteObject(id: string, options?: {
227
+ caller?: string;
228
+ }): Promise<void>;
229
+ /**
230
+ * Returns the recorded list of consumers for an object — the "what
231
+ * depends on this" mapping set at creation. Needs no credential.
232
+ *
233
+ * @param id - The object's id.
234
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
235
+ */
236
+ getObjectUsedBy(id: string): Promise<UsedBy>;
237
+ /**
238
+ * Queries the audit log — every create, read, update, and delete call is
239
+ * recorded here. Needs no credential. Filters combine with AND when more
240
+ * than one is given.
241
+ *
242
+ * hush-hush's `/audit-log` endpoint has no pagination parameters, so this
243
+ * always resolves with the full matching result set as a single array,
244
+ * never a page plus a cursor.
245
+ *
246
+ * @param filter - Optional `objectId`/`caller`/`from`/`to` filters.
247
+ */
248
+ queryAuditLog(filter?: AuditLogFilter): Promise<AuditLogEntry[]>;
249
+ private request;
250
+ }
251
+
252
+ /** Base class for every error this SDK raises. */
253
+ declare class HushHushError extends Error {
254
+ }
255
+ /**
256
+ * Raised for any non-2xx response from hush-hush.
257
+ *
258
+ * `requestId` is populated only when the response carries a documented
259
+ * request-ID header; hush-hush's spec doesn't currently document one, so
260
+ * this is usually `undefined`. Kept as a property rather than omitted so a
261
+ * future spec addition doesn't change this type's shape.
262
+ */
263
+ declare class APIError extends HushHushError {
264
+ /** The HTTP status hush-hush responded with. */
265
+ readonly status: number;
266
+ /** The response's request-ID header, when hush-hush's spec documents one for it. */
267
+ readonly requestId: string | undefined;
268
+ /** The parsed `error` field from hush-hush's error body, if present. */
269
+ readonly apiMessage: string | undefined;
270
+ /** The raw, unparsed response body, for a caller that needs more than `apiMessage`. */
271
+ readonly body: Uint8Array;
272
+ constructor(status: number, body: Uint8Array, requestId: string | undefined);
273
+ private static parseMessage;
274
+ }
275
+
276
+ export { APIError, type AuditLogAction, type AuditLogEntry, type AuditLogFilter, Client, type ClientOptions, type Health, HushHushError, type ObjectMetadata, type UsedBy };
@@ -0,0 +1,276 @@
1
+ interface components {
2
+ schemas: {
3
+ /** @example mattermost_deploy_webhook */
4
+ ObjectId: string;
5
+ CreateObjectRequest: {
6
+ id: components["schemas"]["ObjectId"];
7
+ /**
8
+ * Format: byte
9
+ * @description The sealed (encrypted) value, base64-encoded.
10
+ */
11
+ value: string;
12
+ used_by?: components["schemas"]["UsedByList"];
13
+ };
14
+ UpdateObjectRequest: {
15
+ /**
16
+ * Format: byte
17
+ * @description The new sealed (encrypted) value, base64-encoded.
18
+ */
19
+ value: string;
20
+ };
21
+ ObjectMetadata: {
22
+ id: components["schemas"]["ObjectId"];
23
+ used_by?: components["schemas"]["UsedByList"];
24
+ };
25
+ /**
26
+ * @description The consumers (repos or hosts) recorded as depending on this
27
+ * object. Set at creation; unaffected by later value updates.
28
+ * @example [
29
+ * "homelab/vps-docker"
30
+ * ]
31
+ */
32
+ UsedByList: string[];
33
+ UsedBy: {
34
+ used_by: components["schemas"]["UsedByList"];
35
+ };
36
+ AuditLogEntry: {
37
+ object_id: components["schemas"]["ObjectId"];
38
+ /** @enum {string} */
39
+ action: "create" | "read" | "update" | "delete";
40
+ /** Format: date-time */
41
+ timestamp: string;
42
+ /** @description The caller's presented identity, if any. */
43
+ caller?: string;
44
+ /**
45
+ * @description The request's source IP. Unlike caller, this is never
46
+ * self-reported - it's the one piece of the request nobody gets
47
+ * to lie about via a header. Currently the immediate TCP peer's
48
+ * address; this service isn't deployed behind a reverse proxy or
49
+ * load balancer, so X-Forwarded-For (or similar) support isn't
50
+ * implemented yet.
51
+ */
52
+ ip: string;
53
+ };
54
+ Health: {
55
+ /** @constant */
56
+ status: "ok";
57
+ };
58
+ Error: {
59
+ /**
60
+ * @description What went wrong, in terms safe to show a caller. Nothing here
61
+ * handles authentication with a detail worth holding back.
62
+ * @example unknown object
63
+ */
64
+ error: string;
65
+ };
66
+ };
67
+ responses: {
68
+ /** @description The request is missing a valid bearer token. */
69
+ Unauthorized: {
70
+ headers: {
71
+ [name: string]: unknown;
72
+ };
73
+ content: {
74
+ /**
75
+ * @example {
76
+ * "error": "missing or invalid bearer token"
77
+ * }
78
+ */
79
+ "application/json": components["schemas"]["Error"];
80
+ };
81
+ };
82
+ /** @description No object is stored under that id. */
83
+ NotFound: {
84
+ headers: {
85
+ [name: string]: unknown;
86
+ };
87
+ content: {
88
+ /**
89
+ * @example {
90
+ * "error": "unknown object"
91
+ * }
92
+ */
93
+ "application/json": components["schemas"]["Error"];
94
+ };
95
+ };
96
+ };
97
+ parameters: {
98
+ /**
99
+ * @description The object's id, which is the last path segment and so has to
100
+ * survive being in a URL.
101
+ */
102
+ id: components["schemas"]["ObjectId"];
103
+ /**
104
+ * @description The caller's own identity - a repo or host name, whatever the
105
+ * caller wants attributed to it in the audit log. Not authenticated
106
+ * or verified: a courtesy label a caller presents about itself, not
107
+ * an identity check the service performs. Absent means the
108
+ * resulting audit log entry's caller field is empty.
109
+ */
110
+ caller: string;
111
+ };
112
+ requestBodies: never;
113
+ headers: never;
114
+ pathItems: never;
115
+ }
116
+
117
+ type _ObjectMetadata = components["schemas"]["ObjectMetadata"];
118
+ type _UsedBy = components["schemas"]["UsedBy"];
119
+ type _Health = components["schemas"]["Health"];
120
+ type _AuditLogEntry = components["schemas"]["AuditLogEntry"];
121
+ /** An object's id and its recorded consumers. */
122
+ interface ObjectMetadata extends _ObjectMetadata {
123
+ }
124
+ /** The consumers (repos or hosts) recorded as depending on an object. */
125
+ interface UsedBy extends _UsedBy {
126
+ }
127
+ /** hush-hush's liveness response. */
128
+ interface Health extends _Health {
129
+ }
130
+ /** One recorded create, read, update, or delete call. */
131
+ interface AuditLogEntry extends _AuditLogEntry {
132
+ }
133
+ /** The kind of call an {@link AuditLogEntry} recorded. */
134
+ type AuditLogAction = AuditLogEntry["action"];
135
+ /** Options accepted by the {@link Client} constructor. */
136
+ interface ClientOptions {
137
+ /**
138
+ * Bearer credential for write paths (create/update/delete). Falls back to
139
+ * the `HUSH_HUSH_API_KEY` environment variable when not supplied. Read
140
+ * paths (get, used-by, audit-log query) need no credential at all.
141
+ */
142
+ apiKey?: string;
143
+ /** Per-request timeout, in milliseconds. Defaults to 30000. */
144
+ timeoutMs?: number;
145
+ /** Maximum retry attempts for network failures and 5xx/429 responses. Defaults to 3. */
146
+ maxRetries?: number;
147
+ /** Override for the `fetch` implementation, mainly for tests. Defaults to global `fetch`. */
148
+ fetch?: typeof fetch;
149
+ }
150
+ /** Optional filters for {@link Client.queryAuditLog}. Filters combine with AND when more than one is set. */
151
+ interface AuditLogFilter {
152
+ /** Restrict to entries for this object id. */
153
+ objectId?: string;
154
+ /** Restrict to entries recorded with this caller identity. */
155
+ caller?: string;
156
+ /** Restrict to entries at or after this ISO-8601 timestamp. */
157
+ from?: string;
158
+ /** Restrict to entries at or before this ISO-8601 timestamp. */
159
+ to?: string;
160
+ }
161
+ /**
162
+ * A typed client for hush-hush, a standalone secrets object store.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const client = new Client("https://hush-hush.example.com");
167
+ * const meta = await client.createObject("my-object", sealedBytes);
168
+ * ```
169
+ */
170
+ declare class Client {
171
+ private readonly baseUrl;
172
+ private readonly apiKey;
173
+ private readonly timeoutMs;
174
+ private readonly maxRetries;
175
+ private readonly fetchImpl;
176
+ /**
177
+ * @param baseUrl - hush-hush's base URL, e.g. `https://hush-hush.example.com`.
178
+ * @param options - Credential, timeout, and retry configuration.
179
+ */
180
+ constructor(baseUrl: string, options?: ClientOptions);
181
+ /** Answers whether the server process is up. Needs no credential. */
182
+ health(): Promise<Health>;
183
+ /**
184
+ * Stores an already-sealed value under a new object id. Requires a credential.
185
+ *
186
+ * @param id - The new object's id. Must match hush-hush's id pattern (lowercase alphanumeric, `-`/`_`).
187
+ * @param value - The already-sealed (encrypted) value. This SDK never encrypts or decrypts anything.
188
+ * @param options.usedBy - Consumers (repos or hosts) recorded as depending on this object.
189
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
190
+ * @throws {APIError} If the server responds with anything other than 201 (e.g. 409 if the id exists).
191
+ */
192
+ createObject(id: string, value: Uint8Array, options?: {
193
+ usedBy?: string[];
194
+ caller?: string;
195
+ }): Promise<ObjectMetadata>;
196
+ /**
197
+ * Fetches an object's sealed ciphertext exactly as stored — this SDK never
198
+ * decrypts it, the same as the server. Needs no credential.
199
+ *
200
+ * @param id - The object's id.
201
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
202
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
203
+ */
204
+ getObject(id: string, options?: {
205
+ caller?: string;
206
+ }): Promise<Uint8Array>;
207
+ /**
208
+ * Replaces the stored ciphertext for an existing object. The object's id
209
+ * and used-by metadata are unchanged. Requires a credential.
210
+ *
211
+ * @param id - The existing object's id.
212
+ * @param value - The new already-sealed (encrypted) value.
213
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
214
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 401 or 404).
215
+ */
216
+ updateObject(id: string, value: Uint8Array, options?: {
217
+ caller?: string;
218
+ }): Promise<ObjectMetadata>;
219
+ /**
220
+ * Permanently removes an object. A subsequent fetch by this id returns 404. Requires a credential.
221
+ *
222
+ * @param id - The object's id.
223
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
224
+ * @throws {APIError} If the server responds with anything other than 204 (e.g. 401 or 404).
225
+ */
226
+ deleteObject(id: string, options?: {
227
+ caller?: string;
228
+ }): Promise<void>;
229
+ /**
230
+ * Returns the recorded list of consumers for an object — the "what
231
+ * depends on this" mapping set at creation. Needs no credential.
232
+ *
233
+ * @param id - The object's id.
234
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
235
+ */
236
+ getObjectUsedBy(id: string): Promise<UsedBy>;
237
+ /**
238
+ * Queries the audit log — every create, read, update, and delete call is
239
+ * recorded here. Needs no credential. Filters combine with AND when more
240
+ * than one is given.
241
+ *
242
+ * hush-hush's `/audit-log` endpoint has no pagination parameters, so this
243
+ * always resolves with the full matching result set as a single array,
244
+ * never a page plus a cursor.
245
+ *
246
+ * @param filter - Optional `objectId`/`caller`/`from`/`to` filters.
247
+ */
248
+ queryAuditLog(filter?: AuditLogFilter): Promise<AuditLogEntry[]>;
249
+ private request;
250
+ }
251
+
252
+ /** Base class for every error this SDK raises. */
253
+ declare class HushHushError extends Error {
254
+ }
255
+ /**
256
+ * Raised for any non-2xx response from hush-hush.
257
+ *
258
+ * `requestId` is populated only when the response carries a documented
259
+ * request-ID header; hush-hush's spec doesn't currently document one, so
260
+ * this is usually `undefined`. Kept as a property rather than omitted so a
261
+ * future spec addition doesn't change this type's shape.
262
+ */
263
+ declare class APIError extends HushHushError {
264
+ /** The HTTP status hush-hush responded with. */
265
+ readonly status: number;
266
+ /** The response's request-ID header, when hush-hush's spec documents one for it. */
267
+ readonly requestId: string | undefined;
268
+ /** The parsed `error` field from hush-hush's error body, if present. */
269
+ readonly apiMessage: string | undefined;
270
+ /** The raw, unparsed response body, for a caller that needs more than `apiMessage`. */
271
+ readonly body: Uint8Array;
272
+ constructor(status: number, body: Uint8Array, requestId: string | undefined);
273
+ private static parseMessage;
274
+ }
275
+
276
+ export { APIError, type AuditLogAction, type AuditLogEntry, type AuditLogFilter, Client, type ClientOptions, type Health, HushHushError, type ObjectMetadata, type UsedBy };
package/dist/index.js ADDED
@@ -0,0 +1,253 @@
1
+ // src/errors.ts
2
+ var HushHushError = class extends Error {
3
+ };
4
+ var APIError = class _APIError extends HushHushError {
5
+ /** The HTTP status hush-hush responded with. */
6
+ status;
7
+ /** The response's request-ID header, when hush-hush's spec documents one for it. */
8
+ requestId;
9
+ /** The parsed `error` field from hush-hush's error body, if present. */
10
+ apiMessage;
11
+ /** The raw, unparsed response body, for a caller that needs more than `apiMessage`. */
12
+ body;
13
+ constructor(status, body, requestId) {
14
+ const apiMessage = _APIError.parseMessage(body);
15
+ super(
16
+ apiMessage !== void 0 ? `hush-hush: ${status}: ${apiMessage}` : `hush-hush: unexpected status ${status}`
17
+ );
18
+ this.name = "APIError";
19
+ this.status = status;
20
+ this.requestId = requestId;
21
+ this.apiMessage = apiMessage;
22
+ this.body = body;
23
+ }
24
+ static parseMessage(body) {
25
+ try {
26
+ const parsed = JSON.parse(new TextDecoder().decode(body));
27
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
28
+ const value = parsed.error;
29
+ if (typeof value === "string") return value;
30
+ }
31
+ } catch {
32
+ }
33
+ return void 0;
34
+ }
35
+ };
36
+
37
+ // src/retry.ts
38
+ var DEFAULT_MAX_RETRIES = 3;
39
+ function isRetryableStatus(status) {
40
+ return status >= 500 || status === 429;
41
+ }
42
+ function retryAfterMs(response) {
43
+ const value = response.headers.get("retry-after");
44
+ if (!value) return void 0;
45
+ const seconds = Number(value);
46
+ if (Number.isFinite(seconds)) {
47
+ return seconds >= 0 ? seconds * 1e3 : void 0;
48
+ }
49
+ const when = Date.parse(value);
50
+ if (Number.isNaN(when)) return void 0;
51
+ return Math.max(when - Date.now(), 0);
52
+ }
53
+ function backoffMs(attempt, retryAfterOverrideMs) {
54
+ if (retryAfterOverrideMs !== void 0) return retryAfterOverrideMs;
55
+ const base = 100 * 2 ** (attempt - 1);
56
+ return base + Math.random() * base;
57
+ }
58
+ function delay(ms) {
59
+ return new Promise((resolve) => setTimeout(resolve, ms));
60
+ }
61
+ async function fetchWithRetry(fetchImpl, input, init, maxRetries) {
62
+ let nextDelayOverrideMs;
63
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
64
+ if (attempt > 0) {
65
+ await delay(backoffMs(attempt, nextDelayOverrideMs));
66
+ nextDelayOverrideMs = void 0;
67
+ }
68
+ let response;
69
+ try {
70
+ response = await fetchImpl(input, init);
71
+ } catch (err) {
72
+ if (attempt === maxRetries) throw err;
73
+ continue;
74
+ }
75
+ if (!isRetryableStatus(response.status) || attempt === maxRetries) {
76
+ return response;
77
+ }
78
+ nextDelayOverrideMs = retryAfterMs(response);
79
+ await response.body?.cancel();
80
+ }
81
+ throw new Error("fetchWithRetry: unreachable \u2014 maxRetries must be >= 0");
82
+ }
83
+
84
+ // src/client.ts
85
+ var API_KEY_ENV_VAR = "HUSH_HUSH_API_KEY";
86
+ var DEFAULT_TIMEOUT_MS = 3e4;
87
+ var Client = class {
88
+ baseUrl;
89
+ apiKey;
90
+ timeoutMs;
91
+ maxRetries;
92
+ fetchImpl;
93
+ /**
94
+ * @param baseUrl - hush-hush's base URL, e.g. `https://hush-hush.example.com`.
95
+ * @param options - Credential, timeout, and retry configuration.
96
+ */
97
+ constructor(baseUrl, options = {}) {
98
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
99
+ this.apiKey = options.apiKey ?? process.env[API_KEY_ENV_VAR];
100
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
101
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
102
+ this.fetchImpl = options.fetch ?? fetch;
103
+ }
104
+ /** Answers whether the server process is up. Needs no credential. */
105
+ async health() {
106
+ const response = await this.request("GET", "/healthz");
107
+ return await response.json();
108
+ }
109
+ /**
110
+ * Stores an already-sealed value under a new object id. Requires a credential.
111
+ *
112
+ * @param id - The new object's id. Must match hush-hush's id pattern (lowercase alphanumeric, `-`/`_`).
113
+ * @param value - The already-sealed (encrypted) value. This SDK never encrypts or decrypts anything.
114
+ * @param options.usedBy - Consumers (repos or hosts) recorded as depending on this object.
115
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
116
+ * @throws {APIError} If the server responds with anything other than 201 (e.g. 409 if the id exists).
117
+ */
118
+ async createObject(id, value, options = {}) {
119
+ const response = await this.request("POST", "/objects", {
120
+ authenticated: true,
121
+ caller: options.caller,
122
+ jsonBody: {
123
+ id,
124
+ value: base64Encode(value),
125
+ ...options.usedBy !== void 0 ? { used_by: options.usedBy } : {}
126
+ }
127
+ });
128
+ return await response.json();
129
+ }
130
+ /**
131
+ * Fetches an object's sealed ciphertext exactly as stored — this SDK never
132
+ * decrypts it, the same as the server. Needs no credential.
133
+ *
134
+ * @param id - The object's id.
135
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
136
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
137
+ */
138
+ async getObject(id, options = {}) {
139
+ const response = await this.request("GET", `/objects/${encodeURIComponent(id)}`, {
140
+ caller: options.caller
141
+ });
142
+ return new Uint8Array(await response.arrayBuffer());
143
+ }
144
+ /**
145
+ * Replaces the stored ciphertext for an existing object. The object's id
146
+ * and used-by metadata are unchanged. Requires a credential.
147
+ *
148
+ * @param id - The existing object's id.
149
+ * @param value - The new already-sealed (encrypted) value.
150
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
151
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 401 or 404).
152
+ */
153
+ async updateObject(id, value, options = {}) {
154
+ const response = await this.request("PUT", `/objects/${encodeURIComponent(id)}`, {
155
+ authenticated: true,
156
+ caller: options.caller,
157
+ jsonBody: { value: base64Encode(value) }
158
+ });
159
+ return await response.json();
160
+ }
161
+ /**
162
+ * Permanently removes an object. A subsequent fetch by this id returns 404. Requires a credential.
163
+ *
164
+ * @param id - The object's id.
165
+ * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.
166
+ * @throws {APIError} If the server responds with anything other than 204 (e.g. 401 or 404).
167
+ */
168
+ async deleteObject(id, options = {}) {
169
+ await this.request("DELETE", `/objects/${encodeURIComponent(id)}`, {
170
+ authenticated: true,
171
+ caller: options.caller
172
+ });
173
+ }
174
+ /**
175
+ * Returns the recorded list of consumers for an object — the "what
176
+ * depends on this" mapping set at creation. Needs no credential.
177
+ *
178
+ * @param id - The object's id.
179
+ * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).
180
+ */
181
+ async getObjectUsedBy(id) {
182
+ const response = await this.request("GET", `/objects/${encodeURIComponent(id)}/used-by`);
183
+ return await response.json();
184
+ }
185
+ /**
186
+ * Queries the audit log — every create, read, update, and delete call is
187
+ * recorded here. Needs no credential. Filters combine with AND when more
188
+ * than one is given.
189
+ *
190
+ * hush-hush's `/audit-log` endpoint has no pagination parameters, so this
191
+ * always resolves with the full matching result set as a single array,
192
+ * never a page plus a cursor.
193
+ *
194
+ * @param filter - Optional `objectId`/`caller`/`from`/`to` filters.
195
+ */
196
+ async queryAuditLog(filter = {}) {
197
+ const response = await this.request("GET", "/audit-log", {
198
+ query: {
199
+ object_id: filter.objectId,
200
+ caller: filter.caller,
201
+ from: filter.from,
202
+ to: filter.to
203
+ }
204
+ });
205
+ return await response.json();
206
+ }
207
+ async request(method, path, options = {}) {
208
+ const url = new URL(`${this.baseUrl}${path}`);
209
+ for (const [key, value] of Object.entries(options.query ?? {})) {
210
+ if (value !== void 0) url.searchParams.set(key, value);
211
+ }
212
+ const headers = new Headers();
213
+ if (options.caller !== void 0) headers.set("X-Caller", options.caller);
214
+ if (options.authenticated === true && this.apiKey !== void 0) {
215
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
216
+ }
217
+ let body = options.body;
218
+ if (options.jsonBody !== void 0) {
219
+ headers.set("Content-Type", "application/json");
220
+ body = JSON.stringify(options.jsonBody);
221
+ }
222
+ const controller = new AbortController();
223
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
224
+ try {
225
+ const response = await fetchWithRetry(
226
+ this.fetchImpl,
227
+ url.toString(),
228
+ { method, headers, signal: controller.signal, ...body !== void 0 ? { body } : {} },
229
+ this.maxRetries
230
+ );
231
+ if (!response.ok) {
232
+ const responseBody = new Uint8Array(await response.arrayBuffer());
233
+ throw new APIError(
234
+ response.status,
235
+ responseBody,
236
+ response.headers.get("x-request-id") ?? void 0
237
+ );
238
+ }
239
+ return response;
240
+ } finally {
241
+ clearTimeout(timeout);
242
+ }
243
+ }
244
+ };
245
+ function base64Encode(value) {
246
+ return Buffer.from(value).toString("base64");
247
+ }
248
+ export {
249
+ APIError,
250
+ Client,
251
+ HushHushError
252
+ };
253
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/retry.ts","../src/client.ts"],"sourcesContent":["/** Base class for every error this SDK raises. */\nexport class HushHushError extends Error {}\n\n/**\n * Raised for any non-2xx response from hush-hush.\n *\n * `requestId` is populated only when the response carries a documented\n * request-ID header; hush-hush's spec doesn't currently document one, so\n * this is usually `undefined`. Kept as a property rather than omitted so a\n * future spec addition doesn't change this type's shape.\n */\nexport class APIError extends HushHushError {\n /** The HTTP status hush-hush responded with. */\n readonly status: number;\n /** The response's request-ID header, when hush-hush's spec documents one for it. */\n readonly requestId: string | undefined;\n /** The parsed `error` field from hush-hush's error body, if present. */\n readonly apiMessage: string | undefined;\n /** The raw, unparsed response body, for a caller that needs more than `apiMessage`. */\n readonly body: Uint8Array;\n\n constructor(status: number, body: Uint8Array, requestId: string | undefined) {\n const apiMessage = APIError.parseMessage(body);\n super(\n apiMessage !== undefined\n ? `hush-hush: ${status}: ${apiMessage}`\n : `hush-hush: unexpected status ${status}`,\n );\n this.name = \"APIError\";\n this.status = status;\n this.requestId = requestId;\n this.apiMessage = apiMessage;\n this.body = body;\n }\n\n private static parseMessage(body: Uint8Array): string | undefined {\n try {\n const parsed: unknown = JSON.parse(new TextDecoder().decode(body));\n if (typeof parsed === \"object\" && parsed !== null && \"error\" in parsed) {\n const value = (parsed as { error?: unknown }).error;\n if (typeof value === \"string\") return value;\n }\n } catch {\n // Not JSON, or not decodable as UTF-8 — apiMessage stays undefined.\n }\n return undefined;\n }\n}\n","/**\n * Retries a request only on network failure or an HTTP 5xx/429 response,\n * using exponential backoff with jitter, and honors a `Retry-After` response\n * header ahead of the computed backoff delay when present. Any other 4xx is\n * never retried — it won't succeed on a second attempt, and retrying only\n * delays the real error reaching the caller.\n */\n\nexport const DEFAULT_MAX_RETRIES = 3;\n\nfunction isRetryableStatus(status: number): boolean {\n return status >= 500 || status === 429;\n}\n\nfunction retryAfterMs(response: Response): number | undefined {\n const value = response.headers.get(\"retry-after\");\n if (!value) return undefined;\n\n const seconds = Number(value);\n if (Number.isFinite(seconds)) {\n return seconds >= 0 ? seconds * 1000 : undefined;\n }\n\n const when = Date.parse(value);\n if (Number.isNaN(when)) return undefined;\n return Math.max(when - Date.now(), 0);\n}\n\nfunction backoffMs(attempt: number, retryAfterOverrideMs: number | undefined): number {\n if (retryAfterOverrideMs !== undefined) return retryAfterOverrideMs;\n const base = 100 * 2 ** (attempt - 1);\n return base + Math.random() * base;\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport async function fetchWithRetry(\n fetchImpl: typeof fetch,\n input: string,\n init: RequestInit,\n maxRetries: number,\n): Promise<Response> {\n let nextDelayOverrideMs: number | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n if (attempt > 0) {\n await delay(backoffMs(attempt, nextDelayOverrideMs));\n nextDelayOverrideMs = undefined;\n }\n\n let response: Response;\n try {\n response = await fetchImpl(input, init);\n } catch (err) {\n if (attempt === maxRetries) throw err;\n continue;\n }\n\n if (!isRetryableStatus(response.status) || attempt === maxRetries) {\n return response;\n }\n nextDelayOverrideMs = retryAfterMs(response);\n await response.body?.cancel();\n }\n\n throw new Error(\"fetchWithRetry: unreachable — maxRetries must be >= 0\");\n}\n","import { APIError } from \"./errors.js\";\nimport type { components } from \"./generated/types.js\";\nimport { DEFAULT_MAX_RETRIES, fetchWithRetry } from \"./retry.js\";\n\ntype _ObjectMetadata = components[\"schemas\"][\"ObjectMetadata\"];\ntype _UsedBy = components[\"schemas\"][\"UsedBy\"];\ntype _Health = components[\"schemas\"][\"Health\"];\ntype _AuditLogEntry = components[\"schemas\"][\"AuditLogEntry\"];\n\n/** An object's id and its recorded consumers. */\nexport interface ObjectMetadata extends _ObjectMetadata {}\n/** The consumers (repos or hosts) recorded as depending on an object. */\nexport interface UsedBy extends _UsedBy {}\n/** hush-hush's liveness response. */\nexport interface Health extends _Health {}\n/** One recorded create, read, update, or delete call. */\nexport interface AuditLogEntry extends _AuditLogEntry {}\n/** The kind of call an {@link AuditLogEntry} recorded. */\nexport type AuditLogAction = AuditLogEntry[\"action\"];\n\nconst API_KEY_ENV_VAR = \"HUSH_HUSH_API_KEY\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/** Options accepted by the {@link Client} constructor. */\nexport interface ClientOptions {\n /**\n * Bearer credential for write paths (create/update/delete). Falls back to\n * the `HUSH_HUSH_API_KEY` environment variable when not supplied. Read\n * paths (get, used-by, audit-log query) need no credential at all.\n */\n apiKey?: string;\n /** Per-request timeout, in milliseconds. Defaults to 30000. */\n timeoutMs?: number;\n /** Maximum retry attempts for network failures and 5xx/429 responses. Defaults to 3. */\n maxRetries?: number;\n /** Override for the `fetch` implementation, mainly for tests. Defaults to global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/** Optional filters for {@link Client.queryAuditLog}. Filters combine with AND when more than one is set. */\nexport interface AuditLogFilter {\n /** Restrict to entries for this object id. */\n objectId?: string;\n /** Restrict to entries recorded with this caller identity. */\n caller?: string;\n /** Restrict to entries at or after this ISO-8601 timestamp. */\n from?: string;\n /** Restrict to entries at or before this ISO-8601 timestamp. */\n to?: string;\n}\n\ninterface RequestOptions {\n authenticated?: boolean;\n caller?: string | undefined;\n query?: Record<string, string | undefined>;\n body?: RequestInit[\"body\"];\n jsonBody?: unknown;\n}\n\n/**\n * A typed client for hush-hush, a standalone secrets object store.\n *\n * @example\n * ```ts\n * const client = new Client(\"https://hush-hush.example.com\");\n * const meta = await client.createObject(\"my-object\", sealedBytes);\n * ```\n */\nexport class Client {\n private readonly baseUrl: string;\n private readonly apiKey: string | undefined;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n /**\n * @param baseUrl - hush-hush's base URL, e.g. `https://hush-hush.example.com`.\n * @param options - Credential, timeout, and retry configuration.\n */\n constructor(baseUrl: string, options: ClientOptions = {}) {\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = options.apiKey ?? process.env[API_KEY_ENV_VAR];\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n /** Answers whether the server process is up. Needs no credential. */\n async health(): Promise<Health> {\n const response = await this.request(\"GET\", \"/healthz\");\n return (await response.json()) as Health;\n }\n\n /**\n * Stores an already-sealed value under a new object id. Requires a credential.\n *\n * @param id - The new object's id. Must match hush-hush's id pattern (lowercase alphanumeric, `-`/`_`).\n * @param value - The already-sealed (encrypted) value. This SDK never encrypts or decrypts anything.\n * @param options.usedBy - Consumers (repos or hosts) recorded as depending on this object.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 201 (e.g. 409 if the id exists).\n */\n async createObject(\n id: string,\n value: Uint8Array,\n options: { usedBy?: string[]; caller?: string } = {},\n ): Promise<ObjectMetadata> {\n const response = await this.request(\"POST\", \"/objects\", {\n authenticated: true,\n caller: options.caller,\n jsonBody: {\n id,\n value: base64Encode(value),\n ...(options.usedBy !== undefined ? { used_by: options.usedBy } : {}),\n },\n });\n return (await response.json()) as ObjectMetadata;\n }\n\n /**\n * Fetches an object's sealed ciphertext exactly as stored — this SDK never\n * decrypts it, the same as the server. Needs no credential.\n *\n * @param id - The object's id.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).\n */\n async getObject(id: string, options: { caller?: string } = {}): Promise<Uint8Array> {\n const response = await this.request(\"GET\", `/objects/${encodeURIComponent(id)}`, {\n caller: options.caller,\n });\n return new Uint8Array(await response.arrayBuffer());\n }\n\n /**\n * Replaces the stored ciphertext for an existing object. The object's id\n * and used-by metadata are unchanged. Requires a credential.\n *\n * @param id - The existing object's id.\n * @param value - The new already-sealed (encrypted) value.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 200 (e.g. 401 or 404).\n */\n async updateObject(\n id: string,\n value: Uint8Array,\n options: { caller?: string } = {},\n ): Promise<ObjectMetadata> {\n const response = await this.request(\"PUT\", `/objects/${encodeURIComponent(id)}`, {\n authenticated: true,\n caller: options.caller,\n jsonBody: { value: base64Encode(value) },\n });\n return (await response.json()) as ObjectMetadata;\n }\n\n /**\n * Permanently removes an object. A subsequent fetch by this id returns 404. Requires a credential.\n *\n * @param id - The object's id.\n * @param options.caller - Recorded in the audit log as the calling program's self-reported identity.\n * @throws {APIError} If the server responds with anything other than 204 (e.g. 401 or 404).\n */\n async deleteObject(id: string, options: { caller?: string } = {}): Promise<void> {\n await this.request(\"DELETE\", `/objects/${encodeURIComponent(id)}`, {\n authenticated: true,\n caller: options.caller,\n });\n }\n\n /**\n * Returns the recorded list of consumers for an object — the \"what\n * depends on this\" mapping set at creation. Needs no credential.\n *\n * @param id - The object's id.\n * @throws {APIError} If the server responds with anything other than 200 (e.g. 404).\n */\n async getObjectUsedBy(id: string): Promise<UsedBy> {\n const response = await this.request(\"GET\", `/objects/${encodeURIComponent(id)}/used-by`);\n return (await response.json()) as UsedBy;\n }\n\n /**\n * Queries the audit log — every create, read, update, and delete call is\n * recorded here. Needs no credential. Filters combine with AND when more\n * than one is given.\n *\n * hush-hush's `/audit-log` endpoint has no pagination parameters, so this\n * always resolves with the full matching result set as a single array,\n * never a page plus a cursor.\n *\n * @param filter - Optional `objectId`/`caller`/`from`/`to` filters.\n */\n async queryAuditLog(filter: AuditLogFilter = {}): Promise<AuditLogEntry[]> {\n const response = await this.request(\"GET\", \"/audit-log\", {\n query: {\n object_id: filter.objectId,\n caller: filter.caller,\n from: filter.from,\n to: filter.to,\n },\n });\n return (await response.json()) as AuditLogEntry[];\n }\n\n private async request(\n method: string,\n path: string,\n options: RequestOptions = {},\n ): Promise<Response> {\n const url = new URL(`${this.baseUrl}${path}`);\n for (const [key, value] of Object.entries(options.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n\n const headers = new Headers();\n if (options.caller !== undefined) headers.set(\"X-Caller\", options.caller);\n if (options.authenticated === true && this.apiKey !== undefined) {\n headers.set(\"Authorization\", `Bearer ${this.apiKey}`);\n }\n\n let body: RequestInit[\"body\"] = options.body;\n if (options.jsonBody !== undefined) {\n headers.set(\"Content-Type\", \"application/json\");\n body = JSON.stringify(options.jsonBody);\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n const response = await fetchWithRetry(\n this.fetchImpl,\n url.toString(),\n { method, headers, signal: controller.signal, ...(body !== undefined ? { body } : {}) },\n this.maxRetries,\n );\n if (!response.ok) {\n const responseBody = new Uint8Array(await response.arrayBuffer());\n throw new APIError(\n response.status,\n responseBody,\n response.headers.get(\"x-request-id\") ?? undefined,\n );\n }\n return response;\n } finally {\n clearTimeout(timeout);\n }\n }\n}\n\nfunction base64Encode(value: Uint8Array): string {\n return Buffer.from(value).toString(\"base64\");\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hush-hush/sdk",
3
- "version": "3.0.0",
3
+ "version": "4.1.1",
4
4
  "description": "Official Node.js/TypeScript SDK for hush-hush, generated from its OpenAPI spec",
5
5
  "keywords": [
6
6
  "hush-hush",