@hush-hush/sdk 3.0.0 → 4.1.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/README.md +3 -3
- package/dist/index.cjs +282 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +276 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/alrayyes/hush-hush-node/actions/workflows/ci.yml)
|
|
4
4
|
[](https://codecov.io/gh/alrayyes/hush-hush-node)
|
|
5
|
-
[](https://www.npmjs.com/package/@hush-hush/sdk)
|
|
6
6
|
[](https://github.com/alrayyes/hush-hush-node/releases)
|
|
7
7
|
[](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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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":[]}
|