@debros/orama 0.122.4-nightly
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/LICENSE +21 -0
- package/README.md +665 -0
- package/dist/index.d.ts +1334 -0
- package/dist/index.js +2553 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/auth/client.ts +276 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/types.ts +62 -0
- package/src/cache/client.ts +203 -0
- package/src/cache/index.ts +14 -0
- package/src/core/http.ts +541 -0
- package/src/core/index.ts +10 -0
- package/src/core/interfaces/IAuthStrategy.ts +28 -0
- package/src/core/interfaces/IHttpTransport.ts +73 -0
- package/src/core/interfaces/IRetryPolicy.ts +20 -0
- package/src/core/interfaces/IWebSocketClient.ts +60 -0
- package/src/core/interfaces/index.ts +4 -0
- package/src/core/transport/AuthHeaderStrategy.ts +108 -0
- package/src/core/transport/RequestLogger.ts +116 -0
- package/src/core/transport/RequestRetryPolicy.ts +53 -0
- package/src/core/transport/TLSConfiguration.ts +53 -0
- package/src/core/transport/index.ts +4 -0
- package/src/core/ws.ts +246 -0
- package/src/db/client.ts +126 -0
- package/src/db/index.ts +13 -0
- package/src/db/qb.ts +111 -0
- package/src/db/repository.ts +128 -0
- package/src/db/types.ts +67 -0
- package/src/errors.ts +38 -0
- package/src/functions/client.ts +62 -0
- package/src/functions/index.ts +2 -0
- package/src/functions/types.ts +21 -0
- package/src/index.ts +201 -0
- package/src/network/client.ts +119 -0
- package/src/network/index.ts +7 -0
- package/src/pubsub/client.ts +361 -0
- package/src/pubsub/index.ts +12 -0
- package/src/pubsub/types.ts +46 -0
- package/src/storage/client.ts +272 -0
- package/src/storage/index.ts +7 -0
- package/src/utils/codec.ts +68 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/platform.ts +44 -0
- package/src/utils/retry.ts +58 -0
- package/src/vault/auth.ts +98 -0
- package/src/vault/client.ts +197 -0
- package/src/vault/crypto/aes.ts +271 -0
- package/src/vault/crypto/hkdf.ts +42 -0
- package/src/vault/crypto/index.ts +27 -0
- package/src/vault/crypto/shamir.ts +173 -0
- package/src/vault/index.ts +65 -0
- package/src/vault/quorum.ts +16 -0
- package/src/vault/transport/fanout.ts +94 -0
- package/src/vault/transport/guardian.ts +285 -0
- package/src/vault/transport/index.ts +19 -0
- package/src/vault/transport/types.ts +101 -0
- package/src/vault/types.ts +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2553 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var SDKError = class _SDKError extends Error {
|
|
3
|
+
constructor(message, httpStatus = 500, code = "SDK_ERROR", details = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "SDKError";
|
|
6
|
+
this.httpStatus = httpStatus;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
static fromResponse(status, body, message) {
|
|
11
|
+
const errorMsg = message || body?.error || `HTTP ${status}`;
|
|
12
|
+
const code = body?.code || `HTTP_${status}`;
|
|
13
|
+
return new _SDKError(errorMsg, status, code, body);
|
|
14
|
+
}
|
|
15
|
+
toJSON() {
|
|
16
|
+
return {
|
|
17
|
+
name: this.name,
|
|
18
|
+
message: this.message,
|
|
19
|
+
httpStatus: this.httpStatus,
|
|
20
|
+
code: this.code,
|
|
21
|
+
details: this.details
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/core/http.ts
|
|
27
|
+
function createFetchWithTLSConfig() {
|
|
28
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
29
|
+
const isDevelopmentOrStaging = process.env.NODE_ENV !== "production" || process.env.DEBROS_ALLOW_STAGING_CERTS === "true" || process.env.DEBROS_USE_HTTPS === "true";
|
|
30
|
+
if (isDevelopmentOrStaging) {
|
|
31
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return globalThis.fetch;
|
|
35
|
+
}
|
|
36
|
+
var HttpClient = class {
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.baseURL = config.baseURL.replace(/\/$/, "");
|
|
39
|
+
this.timeout = config.timeout ?? 6e4;
|
|
40
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
41
|
+
this.retryDelayMs = config.retryDelayMs ?? 1e3;
|
|
42
|
+
this.fetch = config.fetch ?? createFetchWithTLSConfig();
|
|
43
|
+
this.debug = config.debug ?? false;
|
|
44
|
+
this.onNetworkError = config.onNetworkError;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Set the network error callback
|
|
48
|
+
*/
|
|
49
|
+
setOnNetworkError(callback) {
|
|
50
|
+
this.onNetworkError = callback;
|
|
51
|
+
}
|
|
52
|
+
setApiKey(apiKey) {
|
|
53
|
+
this.apiKey = apiKey;
|
|
54
|
+
}
|
|
55
|
+
setJwt(jwt) {
|
|
56
|
+
this.jwt = jwt;
|
|
57
|
+
if (typeof console !== "undefined") {
|
|
58
|
+
console.log(
|
|
59
|
+
"[HttpClient] JWT set:",
|
|
60
|
+
!!jwt,
|
|
61
|
+
"API key still present:",
|
|
62
|
+
!!this.apiKey
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
getAuthHeaders(path) {
|
|
67
|
+
const headers = {};
|
|
68
|
+
const isDbOperation = path.includes("/v1/rqlite/");
|
|
69
|
+
const isPubSubOperation = path.includes("/v1/pubsub/");
|
|
70
|
+
const isProxyOperation = path.includes("/v1/proxy/");
|
|
71
|
+
const isCacheOperation = path.includes("/v1/cache/");
|
|
72
|
+
const isAuthOperation = path.includes("/v1/auth/");
|
|
73
|
+
if (isDbOperation || isPubSubOperation || isProxyOperation || isCacheOperation) {
|
|
74
|
+
if (this.apiKey) {
|
|
75
|
+
headers["X-API-Key"] = this.apiKey;
|
|
76
|
+
} else if (this.jwt) {
|
|
77
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
78
|
+
}
|
|
79
|
+
} else if (isAuthOperation) {
|
|
80
|
+
if (this.apiKey) {
|
|
81
|
+
headers["X-API-Key"] = this.apiKey;
|
|
82
|
+
}
|
|
83
|
+
if (this.jwt) {
|
|
84
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
if (this.jwt) {
|
|
88
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
89
|
+
}
|
|
90
|
+
if (this.apiKey) {
|
|
91
|
+
headers["X-API-Key"] = this.apiKey;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return headers;
|
|
95
|
+
}
|
|
96
|
+
getAuthToken() {
|
|
97
|
+
return this.jwt || this.apiKey;
|
|
98
|
+
}
|
|
99
|
+
getApiKey() {
|
|
100
|
+
return this.apiKey;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get the base URL
|
|
104
|
+
*/
|
|
105
|
+
getBaseURL() {
|
|
106
|
+
return this.baseURL;
|
|
107
|
+
}
|
|
108
|
+
async request(method, path, options = {}) {
|
|
109
|
+
const startTime = performance.now();
|
|
110
|
+
const url = new URL(this.baseURL + path);
|
|
111
|
+
if (options.query) {
|
|
112
|
+
Object.entries(options.query).forEach(([key, value]) => {
|
|
113
|
+
url.searchParams.append(key, String(value));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const headers = {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
...this.getAuthHeaders(path),
|
|
119
|
+
...options.headers
|
|
120
|
+
};
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const requestTimeout = options.timeout ?? this.timeout;
|
|
123
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
124
|
+
const fetchOptions = {
|
|
125
|
+
method,
|
|
126
|
+
headers,
|
|
127
|
+
signal: controller.signal
|
|
128
|
+
};
|
|
129
|
+
if (options.body !== void 0) {
|
|
130
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
131
|
+
}
|
|
132
|
+
const isRqliteOperation = path.includes("/v1/rqlite/");
|
|
133
|
+
let queryDetails = null;
|
|
134
|
+
if (isRqliteOperation && options.body) {
|
|
135
|
+
try {
|
|
136
|
+
const body = typeof options.body === "string" ? JSON.parse(options.body) : options.body;
|
|
137
|
+
if (body.sql) {
|
|
138
|
+
queryDetails = `SQL: ${body.sql}`;
|
|
139
|
+
if (body.args && body.args.length > 0) {
|
|
140
|
+
queryDetails += ` | Args: [${body.args.map((a) => typeof a === "string" ? `"${a}"` : a).join(", ")}]`;
|
|
141
|
+
}
|
|
142
|
+
} else if (body.table) {
|
|
143
|
+
queryDetails = `Table: ${body.table}`;
|
|
144
|
+
if (body.criteria && Object.keys(body.criteria).length > 0) {
|
|
145
|
+
queryDetails += ` | Criteria: ${JSON.stringify(body.criteria)}`;
|
|
146
|
+
}
|
|
147
|
+
if (body.options) {
|
|
148
|
+
queryDetails += ` | Options: ${JSON.stringify(body.options)}`;
|
|
149
|
+
}
|
|
150
|
+
if (body.select) {
|
|
151
|
+
queryDetails += ` | Select: ${JSON.stringify(body.select)}`;
|
|
152
|
+
}
|
|
153
|
+
if (body.where) {
|
|
154
|
+
queryDetails += ` | Where: ${JSON.stringify(body.where)}`;
|
|
155
|
+
}
|
|
156
|
+
if (body.limit) {
|
|
157
|
+
queryDetails += ` | Limit: ${body.limit}`;
|
|
158
|
+
}
|
|
159
|
+
if (body.offset) {
|
|
160
|
+
queryDetails += ` | Offset: ${body.offset}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const result = await this.requestWithRetry(
|
|
168
|
+
url.toString(),
|
|
169
|
+
fetchOptions,
|
|
170
|
+
0,
|
|
171
|
+
startTime
|
|
172
|
+
);
|
|
173
|
+
const duration = performance.now() - startTime;
|
|
174
|
+
if (typeof console !== "undefined") {
|
|
175
|
+
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(
|
|
176
|
+
2
|
|
177
|
+
)}ms`;
|
|
178
|
+
if (queryDetails && this.debug) {
|
|
179
|
+
console.log(logMessage);
|
|
180
|
+
console.log(`[HttpClient] ${queryDetails}`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(logMessage);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const duration = performance.now() - startTime;
|
|
188
|
+
if (typeof console !== "undefined") {
|
|
189
|
+
const is404FindOne = path === "/v1/rqlite/find-one" && error instanceof SDKError && error.httpStatus === 404;
|
|
190
|
+
if (is404FindOne) {
|
|
191
|
+
console.warn(
|
|
192
|
+
`[HttpClient] ${method} ${path} returned 404 after ${duration.toFixed(
|
|
193
|
+
2
|
|
194
|
+
)}ms (expected for optional lookups)`
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
const errorMessage = `[HttpClient] ${method} ${path} failed after ${duration.toFixed(
|
|
198
|
+
2
|
|
199
|
+
)}ms:`;
|
|
200
|
+
console.error(errorMessage, error);
|
|
201
|
+
if (queryDetails && this.debug) {
|
|
202
|
+
console.error(`[HttpClient] ${queryDetails}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (this.onNetworkError) {
|
|
207
|
+
const sdkError = error instanceof SDKError ? error : new SDKError(
|
|
208
|
+
error instanceof Error ? error.message : String(error),
|
|
209
|
+
0,
|
|
210
|
+
// httpStatus 0 indicates network-level failure
|
|
211
|
+
"NETWORK_ERROR"
|
|
212
|
+
);
|
|
213
|
+
this.onNetworkError(sdkError, {
|
|
214
|
+
method,
|
|
215
|
+
path,
|
|
216
|
+
isRetry: false,
|
|
217
|
+
attempt: this.maxRetries
|
|
218
|
+
// All retries exhausted
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
} finally {
|
|
223
|
+
clearTimeout(timeoutId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async requestWithRetry(url, options, attempt = 0, startTime) {
|
|
227
|
+
try {
|
|
228
|
+
const response = await this.fetch(url, options);
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
let body;
|
|
231
|
+
try {
|
|
232
|
+
body = await response.json();
|
|
233
|
+
} catch {
|
|
234
|
+
body = { error: response.statusText };
|
|
235
|
+
}
|
|
236
|
+
throw SDKError.fromResponse(response.status, body);
|
|
237
|
+
}
|
|
238
|
+
const contentType = response.headers.get("content-type");
|
|
239
|
+
if (contentType?.includes("application/json")) {
|
|
240
|
+
return response.json();
|
|
241
|
+
}
|
|
242
|
+
return response.text();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const isRetryableError = error instanceof SDKError && [408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
|
245
|
+
if (isRetryableError && attempt < this.maxRetries) {
|
|
246
|
+
if (typeof console !== "undefined") {
|
|
247
|
+
console.warn(
|
|
248
|
+
`[HttpClient] Retrying request (attempt ${attempt + 1}/${this.maxRetries})`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
await new Promise(
|
|
252
|
+
(resolve) => setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
|
253
|
+
);
|
|
254
|
+
return this.requestWithRetry(url, options, attempt + 1, startTime);
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async get(path, options) {
|
|
260
|
+
return this.request("GET", path, options);
|
|
261
|
+
}
|
|
262
|
+
async post(path, body, options) {
|
|
263
|
+
return this.request("POST", path, { ...options, body });
|
|
264
|
+
}
|
|
265
|
+
async put(path, body, options) {
|
|
266
|
+
return this.request("PUT", path, { ...options, body });
|
|
267
|
+
}
|
|
268
|
+
async delete(path, options) {
|
|
269
|
+
return this.request("DELETE", path, options);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Upload a file using multipart/form-data
|
|
273
|
+
* This is a special method for file uploads that bypasses JSON serialization
|
|
274
|
+
*/
|
|
275
|
+
async uploadFile(path, formData, options) {
|
|
276
|
+
const startTime = performance.now();
|
|
277
|
+
const url = new URL(this.baseURL + path);
|
|
278
|
+
const headers = {
|
|
279
|
+
...this.getAuthHeaders(path)
|
|
280
|
+
// Don't set Content-Type - browser will set it with boundary
|
|
281
|
+
};
|
|
282
|
+
const controller = new AbortController();
|
|
283
|
+
const requestTimeout = options?.timeout ?? this.timeout * 5;
|
|
284
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
285
|
+
const fetchOptions = {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers,
|
|
288
|
+
body: formData,
|
|
289
|
+
signal: controller.signal
|
|
290
|
+
};
|
|
291
|
+
try {
|
|
292
|
+
const result = await this.requestWithRetry(
|
|
293
|
+
url.toString(),
|
|
294
|
+
fetchOptions,
|
|
295
|
+
0,
|
|
296
|
+
startTime
|
|
297
|
+
);
|
|
298
|
+
const duration = performance.now() - startTime;
|
|
299
|
+
if (typeof console !== "undefined") {
|
|
300
|
+
console.log(
|
|
301
|
+
`[HttpClient] POST ${path} (upload) completed in ${duration.toFixed(
|
|
302
|
+
2
|
|
303
|
+
)}ms`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const duration = performance.now() - startTime;
|
|
309
|
+
if (typeof console !== "undefined") {
|
|
310
|
+
console.error(
|
|
311
|
+
`[HttpClient] POST ${path} (upload) failed after ${duration.toFixed(
|
|
312
|
+
2
|
|
313
|
+
)}ms:`,
|
|
314
|
+
error
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (this.onNetworkError) {
|
|
318
|
+
const sdkError = error instanceof SDKError ? error : new SDKError(
|
|
319
|
+
error instanceof Error ? error.message : String(error),
|
|
320
|
+
0,
|
|
321
|
+
"NETWORK_ERROR"
|
|
322
|
+
);
|
|
323
|
+
this.onNetworkError(sdkError, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
path,
|
|
326
|
+
isRetry: false,
|
|
327
|
+
attempt: this.maxRetries
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
} finally {
|
|
332
|
+
clearTimeout(timeoutId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get a binary response (returns Response object for streaming)
|
|
337
|
+
*/
|
|
338
|
+
async getBinary(path) {
|
|
339
|
+
const url = new URL(this.baseURL + path);
|
|
340
|
+
const headers = {
|
|
341
|
+
...this.getAuthHeaders(path)
|
|
342
|
+
};
|
|
343
|
+
const controller = new AbortController();
|
|
344
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout * 5);
|
|
345
|
+
const fetchOptions = {
|
|
346
|
+
method: "GET",
|
|
347
|
+
headers,
|
|
348
|
+
signal: controller.signal
|
|
349
|
+
};
|
|
350
|
+
try {
|
|
351
|
+
const response = await this.fetch(url.toString(), fetchOptions);
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
clearTimeout(timeoutId);
|
|
354
|
+
const errorBody = await response.json().catch(() => ({
|
|
355
|
+
error: response.statusText
|
|
356
|
+
}));
|
|
357
|
+
throw SDKError.fromResponse(response.status, errorBody);
|
|
358
|
+
}
|
|
359
|
+
return response;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
clearTimeout(timeoutId);
|
|
362
|
+
if (this.onNetworkError) {
|
|
363
|
+
const sdkError = error instanceof SDKError ? error : new SDKError(
|
|
364
|
+
error instanceof Error ? error.message : String(error),
|
|
365
|
+
0,
|
|
366
|
+
"NETWORK_ERROR"
|
|
367
|
+
);
|
|
368
|
+
this.onNetworkError(sdkError, {
|
|
369
|
+
method: "GET",
|
|
370
|
+
path,
|
|
371
|
+
isRetry: false,
|
|
372
|
+
attempt: 0
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
getToken() {
|
|
379
|
+
return this.getAuthToken();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/auth/types.ts
|
|
384
|
+
var MemoryStorage = class {
|
|
385
|
+
constructor() {
|
|
386
|
+
this.storage = /* @__PURE__ */ new Map();
|
|
387
|
+
}
|
|
388
|
+
async get(key) {
|
|
389
|
+
return this.storage.get(key) ?? null;
|
|
390
|
+
}
|
|
391
|
+
async set(key, value) {
|
|
392
|
+
this.storage.set(key, value);
|
|
393
|
+
}
|
|
394
|
+
async clear() {
|
|
395
|
+
this.storage.clear();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var LocalStorageAdapter = class {
|
|
399
|
+
constructor() {
|
|
400
|
+
this.prefix = "@network/sdk:";
|
|
401
|
+
}
|
|
402
|
+
async get(key) {
|
|
403
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
404
|
+
return globalThis.localStorage.getItem(this.prefix + key);
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
async set(key, value) {
|
|
409
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
410
|
+
globalThis.localStorage.setItem(this.prefix + key, value);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async clear() {
|
|
414
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
415
|
+
const keysToDelete = [];
|
|
416
|
+
for (let i = 0; i < globalThis.localStorage.length; i++) {
|
|
417
|
+
const key = globalThis.localStorage.key(i);
|
|
418
|
+
if (key?.startsWith(this.prefix)) {
|
|
419
|
+
keysToDelete.push(key);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
keysToDelete.forEach((key) => globalThis.localStorage.removeItem(key));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/auth/client.ts
|
|
428
|
+
var AuthClient = class {
|
|
429
|
+
constructor(config) {
|
|
430
|
+
this.httpClient = config.httpClient;
|
|
431
|
+
this.storage = config.storage ?? new MemoryStorage();
|
|
432
|
+
this.currentApiKey = config.apiKey;
|
|
433
|
+
this.currentJwt = config.jwt;
|
|
434
|
+
if (this.currentApiKey) {
|
|
435
|
+
this.httpClient.setApiKey(this.currentApiKey);
|
|
436
|
+
}
|
|
437
|
+
if (this.currentJwt) {
|
|
438
|
+
this.httpClient.setJwt(this.currentJwt);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
setApiKey(apiKey) {
|
|
442
|
+
this.currentApiKey = apiKey;
|
|
443
|
+
this.httpClient.setApiKey(apiKey);
|
|
444
|
+
this.storage.set("apiKey", apiKey);
|
|
445
|
+
}
|
|
446
|
+
setJwt(jwt) {
|
|
447
|
+
this.currentJwt = jwt;
|
|
448
|
+
this.httpClient.setJwt(jwt);
|
|
449
|
+
this.storage.set("jwt", jwt);
|
|
450
|
+
}
|
|
451
|
+
getToken() {
|
|
452
|
+
return this.httpClient.getToken();
|
|
453
|
+
}
|
|
454
|
+
async whoami() {
|
|
455
|
+
try {
|
|
456
|
+
const response = await this.httpClient.get("/v1/auth/whoami");
|
|
457
|
+
return response;
|
|
458
|
+
} catch {
|
|
459
|
+
return { authenticated: false };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Exchange a stored refresh token for a fresh access token.
|
|
464
|
+
*
|
|
465
|
+
* Pulls the refresh token (and the namespace it was issued for) out of
|
|
466
|
+
* storage — both are persisted by `verify()` after a successful wallet
|
|
467
|
+
* sign-in. The gateway returns a new access token and may rotate the
|
|
468
|
+
* refresh token; we persist the rotated one if present.
|
|
469
|
+
*
|
|
470
|
+
* Bug #239: previously this method (a) sent no body and (b) read the
|
|
471
|
+
* wrong response field, so the call always 400-ed AND silently wrote
|
|
472
|
+
* `undefined` as the in-memory JWT. Both issues fixed.
|
|
473
|
+
*/
|
|
474
|
+
async refresh() {
|
|
475
|
+
const refreshToken = await this.storage.get("refreshToken");
|
|
476
|
+
if (!refreshToken) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
"refresh failed: no refresh token in storage \u2014 call verify() first"
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const namespace = await this.storage.get("namespace") ?? "default";
|
|
482
|
+
const response = await this.httpClient.post("/v1/auth/refresh", { refresh_token: refreshToken, namespace });
|
|
483
|
+
if (!response?.access_token) {
|
|
484
|
+
throw new Error("refresh failed: server returned no access_token");
|
|
485
|
+
}
|
|
486
|
+
this.setJwt(response.access_token);
|
|
487
|
+
if (response.refresh_token && response.refresh_token !== refreshToken) {
|
|
488
|
+
await this.storage.set("refreshToken", response.refresh_token);
|
|
489
|
+
}
|
|
490
|
+
return response.access_token;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Logout user and clear JWT, but preserve API key
|
|
494
|
+
* Use this for user logout in apps where API key is app-level credential
|
|
495
|
+
*/
|
|
496
|
+
async logoutUser() {
|
|
497
|
+
if (this.currentJwt) {
|
|
498
|
+
try {
|
|
499
|
+
await this.httpClient.post("/v1/auth/logout", { all: true });
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.warn(
|
|
502
|
+
"Server-side logout failed, continuing with local cleanup:",
|
|
503
|
+
error
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
this.currentJwt = void 0;
|
|
508
|
+
this.httpClient.setJwt(void 0);
|
|
509
|
+
await this.storage.set("jwt", "");
|
|
510
|
+
if (!this.currentApiKey) {
|
|
511
|
+
const storedApiKey = await this.storage.get("apiKey");
|
|
512
|
+
if (storedApiKey) {
|
|
513
|
+
this.currentApiKey = storedApiKey;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (this.currentApiKey) {
|
|
517
|
+
this.httpClient.setApiKey(this.currentApiKey);
|
|
518
|
+
console.log("[Auth] API key restored after user logout");
|
|
519
|
+
} else {
|
|
520
|
+
console.warn("[Auth] No API key available after logout");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Full logout - clears both JWT and API key
|
|
525
|
+
* Use this to completely reset authentication state
|
|
526
|
+
*/
|
|
527
|
+
async logout() {
|
|
528
|
+
if (this.currentJwt) {
|
|
529
|
+
try {
|
|
530
|
+
await this.httpClient.post("/v1/auth/logout", { all: true });
|
|
531
|
+
} catch (error) {
|
|
532
|
+
console.warn(
|
|
533
|
+
"Server-side logout failed, continuing with local cleanup:",
|
|
534
|
+
error
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
this.currentApiKey = void 0;
|
|
539
|
+
this.currentJwt = void 0;
|
|
540
|
+
this.httpClient.setApiKey(void 0);
|
|
541
|
+
this.httpClient.setJwt(void 0);
|
|
542
|
+
await this.storage.clear();
|
|
543
|
+
}
|
|
544
|
+
async clear() {
|
|
545
|
+
this.currentApiKey = void 0;
|
|
546
|
+
this.currentJwt = void 0;
|
|
547
|
+
this.httpClient.setApiKey(void 0);
|
|
548
|
+
this.httpClient.setJwt(void 0);
|
|
549
|
+
await this.storage.clear();
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Request a challenge nonce for wallet authentication
|
|
553
|
+
*/
|
|
554
|
+
async challenge(params) {
|
|
555
|
+
const response = await this.httpClient.post("/v1/auth/challenge", {
|
|
556
|
+
wallet: params.wallet,
|
|
557
|
+
purpose: params.purpose || "authentication",
|
|
558
|
+
namespace: params.namespace || "default"
|
|
559
|
+
});
|
|
560
|
+
return response;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Verify wallet signature and get JWT token
|
|
564
|
+
*/
|
|
565
|
+
async verify(params) {
|
|
566
|
+
const response = await this.httpClient.post("/v1/auth/verify", {
|
|
567
|
+
wallet: params.wallet,
|
|
568
|
+
nonce: params.nonce,
|
|
569
|
+
signature: params.signature,
|
|
570
|
+
namespace: params.namespace || "default",
|
|
571
|
+
chain_type: params.chain_type || "ETH"
|
|
572
|
+
});
|
|
573
|
+
this.setJwt(response.access_token);
|
|
574
|
+
if (response.api_key) {
|
|
575
|
+
this.setApiKey(response.api_key);
|
|
576
|
+
}
|
|
577
|
+
if (response.refresh_token) {
|
|
578
|
+
await this.storage.set("refreshToken", response.refresh_token);
|
|
579
|
+
}
|
|
580
|
+
const issuedNamespace = response.namespace || params.namespace || "default";
|
|
581
|
+
await this.storage.set("namespace", issuedNamespace);
|
|
582
|
+
return response;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Get API key for wallet (creates namespace ownership)
|
|
586
|
+
*/
|
|
587
|
+
async getApiKey(params) {
|
|
588
|
+
const response = await this.httpClient.post("/v1/auth/api-key", {
|
|
589
|
+
wallet: params.wallet,
|
|
590
|
+
nonce: params.nonce,
|
|
591
|
+
signature: params.signature,
|
|
592
|
+
namespace: params.namespace || "default",
|
|
593
|
+
chain_type: params.chain_type || "ETH"
|
|
594
|
+
});
|
|
595
|
+
this.setApiKey(response.api_key);
|
|
596
|
+
return response;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/db/qb.ts
|
|
601
|
+
var QueryBuilder = class {
|
|
602
|
+
constructor(httpClient, table) {
|
|
603
|
+
this.options = {};
|
|
604
|
+
this.httpClient = httpClient;
|
|
605
|
+
this.table = table;
|
|
606
|
+
}
|
|
607
|
+
select(...columns) {
|
|
608
|
+
this.options.select = columns;
|
|
609
|
+
return this;
|
|
610
|
+
}
|
|
611
|
+
innerJoin(table, on) {
|
|
612
|
+
if (!this.options.joins) this.options.joins = [];
|
|
613
|
+
this.options.joins.push({ kind: "INNER", table, on });
|
|
614
|
+
return this;
|
|
615
|
+
}
|
|
616
|
+
leftJoin(table, on) {
|
|
617
|
+
if (!this.options.joins) this.options.joins = [];
|
|
618
|
+
this.options.joins.push({ kind: "LEFT", table, on });
|
|
619
|
+
return this;
|
|
620
|
+
}
|
|
621
|
+
rightJoin(table, on) {
|
|
622
|
+
if (!this.options.joins) this.options.joins = [];
|
|
623
|
+
this.options.joins.push({ kind: "RIGHT", table, on });
|
|
624
|
+
return this;
|
|
625
|
+
}
|
|
626
|
+
where(expr, args) {
|
|
627
|
+
if (!this.options.where) this.options.where = [];
|
|
628
|
+
this.options.where.push({ conj: "AND", expr, args });
|
|
629
|
+
return this;
|
|
630
|
+
}
|
|
631
|
+
andWhere(expr, args) {
|
|
632
|
+
return this.where(expr, args);
|
|
633
|
+
}
|
|
634
|
+
orWhere(expr, args) {
|
|
635
|
+
if (!this.options.where) this.options.where = [];
|
|
636
|
+
this.options.where.push({ conj: "OR", expr, args });
|
|
637
|
+
return this;
|
|
638
|
+
}
|
|
639
|
+
groupBy(...columns) {
|
|
640
|
+
this.options.group_by = columns;
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
orderBy(...columns) {
|
|
644
|
+
this.options.order_by = columns;
|
|
645
|
+
return this;
|
|
646
|
+
}
|
|
647
|
+
limit(n) {
|
|
648
|
+
this.options.limit = n;
|
|
649
|
+
return this;
|
|
650
|
+
}
|
|
651
|
+
offset(n) {
|
|
652
|
+
this.options.offset = n;
|
|
653
|
+
return this;
|
|
654
|
+
}
|
|
655
|
+
async getMany(ctx) {
|
|
656
|
+
const response = await this.httpClient.post(
|
|
657
|
+
"/v1/rqlite/select",
|
|
658
|
+
{
|
|
659
|
+
table: this.table,
|
|
660
|
+
...this.options
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
return response.items || [];
|
|
664
|
+
}
|
|
665
|
+
async getOne(ctx) {
|
|
666
|
+
const response = await this.httpClient.post(
|
|
667
|
+
"/v1/rqlite/select",
|
|
668
|
+
{
|
|
669
|
+
table: this.table,
|
|
670
|
+
...this.options,
|
|
671
|
+
one: true,
|
|
672
|
+
limit: 1
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
const items = response.items || [];
|
|
676
|
+
return items.length > 0 ? items[0] : null;
|
|
677
|
+
}
|
|
678
|
+
async count() {
|
|
679
|
+
const response = await this.httpClient.post(
|
|
680
|
+
"/v1/rqlite/select",
|
|
681
|
+
{
|
|
682
|
+
table: this.table,
|
|
683
|
+
select: ["COUNT(*) AS count"],
|
|
684
|
+
where: this.options.where,
|
|
685
|
+
one: true
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
const items = response.items || [];
|
|
689
|
+
return items.length > 0 ? items[0].count : 0;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/db/repository.ts
|
|
694
|
+
var Repository = class {
|
|
695
|
+
constructor(httpClient, tableName, primaryKey = "id") {
|
|
696
|
+
this.httpClient = httpClient;
|
|
697
|
+
this.tableName = tableName;
|
|
698
|
+
this.primaryKey = primaryKey;
|
|
699
|
+
}
|
|
700
|
+
createQueryBuilder() {
|
|
701
|
+
return new QueryBuilder(this.httpClient, this.tableName);
|
|
702
|
+
}
|
|
703
|
+
async find(criteria = {}, options = {}) {
|
|
704
|
+
const response = await this.httpClient.post(
|
|
705
|
+
"/v1/rqlite/find",
|
|
706
|
+
{
|
|
707
|
+
table: this.tableName,
|
|
708
|
+
criteria,
|
|
709
|
+
options
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
return response.items || [];
|
|
713
|
+
}
|
|
714
|
+
async findOne(criteria) {
|
|
715
|
+
try {
|
|
716
|
+
const response = await this.httpClient.post(
|
|
717
|
+
"/v1/rqlite/find-one",
|
|
718
|
+
{
|
|
719
|
+
table: this.tableName,
|
|
720
|
+
criteria
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
return response;
|
|
724
|
+
} catch (error) {
|
|
725
|
+
if (error instanceof SDKError && error.httpStatus === 404) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async save(entity) {
|
|
732
|
+
const pkValue = entity[this.primaryKey];
|
|
733
|
+
if (!pkValue) {
|
|
734
|
+
const response = await this.httpClient.post("/v1/rqlite/exec", {
|
|
735
|
+
sql: this.buildInsertSql(entity),
|
|
736
|
+
args: this.buildInsertArgs(entity)
|
|
737
|
+
});
|
|
738
|
+
if (response.last_insert_id) {
|
|
739
|
+
entity[this.primaryKey] = response.last_insert_id;
|
|
740
|
+
}
|
|
741
|
+
return entity;
|
|
742
|
+
} else {
|
|
743
|
+
await this.httpClient.post("/v1/rqlite/exec", {
|
|
744
|
+
sql: this.buildUpdateSql(entity),
|
|
745
|
+
args: this.buildUpdateArgs(entity)
|
|
746
|
+
});
|
|
747
|
+
return entity;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async remove(entity) {
|
|
751
|
+
const pkValue = entity[this.primaryKey];
|
|
752
|
+
if (!pkValue) {
|
|
753
|
+
throw new SDKError(
|
|
754
|
+
`Primary key "${this.primaryKey}" is required for remove`,
|
|
755
|
+
400,
|
|
756
|
+
"MISSING_PK"
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
await this.httpClient.post("/v1/rqlite/exec", {
|
|
760
|
+
sql: `DELETE FROM ${this.tableName} WHERE ${this.primaryKey} = ?`,
|
|
761
|
+
args: [pkValue]
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
buildInsertSql(entity) {
|
|
765
|
+
const columns = Object.keys(entity).filter((k) => entity[k] !== void 0);
|
|
766
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
767
|
+
return `INSERT INTO ${this.tableName} (${columns.join(
|
|
768
|
+
", "
|
|
769
|
+
)}) VALUES (${placeholders})`;
|
|
770
|
+
}
|
|
771
|
+
buildInsertArgs(entity) {
|
|
772
|
+
return Object.entries(entity).filter(([, v]) => v !== void 0).map(([, v]) => v);
|
|
773
|
+
}
|
|
774
|
+
buildUpdateSql(entity) {
|
|
775
|
+
const columns = Object.keys(entity).filter((k) => entity[k] !== void 0 && k !== this.primaryKey).map((k) => `${k} = ?`);
|
|
776
|
+
return `UPDATE ${this.tableName} SET ${columns.join(", ")} WHERE ${this.primaryKey} = ?`;
|
|
777
|
+
}
|
|
778
|
+
buildUpdateArgs(entity) {
|
|
779
|
+
const args = Object.entries(entity).filter(([k, v]) => v !== void 0 && k !== this.primaryKey).map(([, v]) => v);
|
|
780
|
+
args.push(entity[this.primaryKey]);
|
|
781
|
+
return args;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// src/db/client.ts
|
|
786
|
+
var DBClient = class {
|
|
787
|
+
constructor(httpClient) {
|
|
788
|
+
this.httpClient = httpClient;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Execute a write/DDL SQL statement.
|
|
792
|
+
*/
|
|
793
|
+
async exec(sql, args = []) {
|
|
794
|
+
return this.httpClient.post("/v1/rqlite/exec", { sql, args });
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Execute a SELECT query.
|
|
798
|
+
*/
|
|
799
|
+
async query(sql, args = []) {
|
|
800
|
+
const response = await this.httpClient.post(
|
|
801
|
+
"/v1/rqlite/query",
|
|
802
|
+
{ sql, args }
|
|
803
|
+
);
|
|
804
|
+
return response.items || [];
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Find rows with map-based criteria.
|
|
808
|
+
*/
|
|
809
|
+
async find(table, criteria = {}, options = {}) {
|
|
810
|
+
const response = await this.httpClient.post(
|
|
811
|
+
"/v1/rqlite/find",
|
|
812
|
+
{
|
|
813
|
+
table,
|
|
814
|
+
criteria,
|
|
815
|
+
options
|
|
816
|
+
}
|
|
817
|
+
);
|
|
818
|
+
return response.items || [];
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Find a single row with map-based criteria.
|
|
822
|
+
*/
|
|
823
|
+
async findOne(table, criteria) {
|
|
824
|
+
return this.httpClient.post("/v1/rqlite/find-one", {
|
|
825
|
+
table,
|
|
826
|
+
criteria
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Create a fluent QueryBuilder for complex SELECT queries.
|
|
831
|
+
*/
|
|
832
|
+
createQueryBuilder(table) {
|
|
833
|
+
return new QueryBuilder(this.httpClient, table);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Create a Repository for entity-based operations.
|
|
837
|
+
*/
|
|
838
|
+
repository(tableName, primaryKey = "id") {
|
|
839
|
+
return new Repository(this.httpClient, tableName, primaryKey);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Execute multiple operations atomically.
|
|
843
|
+
*/
|
|
844
|
+
async transaction(ops, returnResults = true) {
|
|
845
|
+
const response = await this.httpClient.post(
|
|
846
|
+
"/v1/rqlite/transaction",
|
|
847
|
+
{
|
|
848
|
+
ops,
|
|
849
|
+
return_results: returnResults
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
return response.results || [];
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Create a table from DDL SQL.
|
|
856
|
+
*/
|
|
857
|
+
async createTable(schema) {
|
|
858
|
+
await this.httpClient.post("/v1/rqlite/create-table", { schema });
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Drop a table.
|
|
862
|
+
*/
|
|
863
|
+
async dropTable(table) {
|
|
864
|
+
await this.httpClient.post("/v1/rqlite/drop-table", { table });
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Get current database schema.
|
|
868
|
+
*/
|
|
869
|
+
async getSchema() {
|
|
870
|
+
return this.httpClient.get("/v1/rqlite/schema");
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// src/core/ws.ts
|
|
875
|
+
import WebSocket from "isomorphic-ws";
|
|
876
|
+
var WSClient = class {
|
|
877
|
+
constructor(config) {
|
|
878
|
+
this.messageHandlers = /* @__PURE__ */ new Set();
|
|
879
|
+
this.errorHandlers = /* @__PURE__ */ new Set();
|
|
880
|
+
this.closeHandlers = /* @__PURE__ */ new Set();
|
|
881
|
+
this.openHandlers = /* @__PURE__ */ new Set();
|
|
882
|
+
this.isClosed = false;
|
|
883
|
+
this.wsURL = config.wsURL;
|
|
884
|
+
this.timeout = config.timeout ?? 3e4;
|
|
885
|
+
this.authToken = config.authToken;
|
|
886
|
+
this.WebSocketClass = config.WebSocket ?? WebSocket;
|
|
887
|
+
this.onNetworkError = config.onNetworkError;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Set the network error callback
|
|
891
|
+
*/
|
|
892
|
+
setOnNetworkError(callback) {
|
|
893
|
+
this.onNetworkError = callback;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Get the current WebSocket URL
|
|
897
|
+
*/
|
|
898
|
+
get url() {
|
|
899
|
+
return this.wsURL;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Connect to WebSocket server
|
|
903
|
+
*/
|
|
904
|
+
connect() {
|
|
905
|
+
return new Promise((resolve, reject) => {
|
|
906
|
+
try {
|
|
907
|
+
const wsUrl = this.buildWSUrl();
|
|
908
|
+
this.ws = new this.WebSocketClass(wsUrl);
|
|
909
|
+
this.isClosed = false;
|
|
910
|
+
const timeout = setTimeout(() => {
|
|
911
|
+
this.ws?.close();
|
|
912
|
+
const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT");
|
|
913
|
+
if (this.onNetworkError) {
|
|
914
|
+
this.onNetworkError(error, {
|
|
915
|
+
method: "WS",
|
|
916
|
+
path: this.wsURL,
|
|
917
|
+
isRetry: false,
|
|
918
|
+
attempt: 0
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
reject(error);
|
|
922
|
+
}, this.timeout);
|
|
923
|
+
this.ws.addEventListener("open", () => {
|
|
924
|
+
clearTimeout(timeout);
|
|
925
|
+
console.log("[WSClient] Connected to", this.wsURL);
|
|
926
|
+
this.openHandlers.forEach((handler) => handler());
|
|
927
|
+
resolve();
|
|
928
|
+
});
|
|
929
|
+
this.ws.addEventListener("message", (event) => {
|
|
930
|
+
const msgEvent = event;
|
|
931
|
+
this.messageHandlers.forEach((handler) => handler(msgEvent.data));
|
|
932
|
+
});
|
|
933
|
+
this.ws.addEventListener("error", (event) => {
|
|
934
|
+
console.error("[WSClient] WebSocket error:", event);
|
|
935
|
+
clearTimeout(timeout);
|
|
936
|
+
const details = { type: event.type };
|
|
937
|
+
if ("message" in event) {
|
|
938
|
+
details.message = event.message;
|
|
939
|
+
}
|
|
940
|
+
const error = new SDKError("WebSocket error", 0, "WS_ERROR", details);
|
|
941
|
+
if (this.onNetworkError) {
|
|
942
|
+
this.onNetworkError(error, {
|
|
943
|
+
method: "WS",
|
|
944
|
+
path: this.wsURL,
|
|
945
|
+
isRetry: false,
|
|
946
|
+
attempt: 0
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
this.errorHandlers.forEach((handler) => handler(error));
|
|
950
|
+
reject(error);
|
|
951
|
+
});
|
|
952
|
+
this.ws.addEventListener("close", (event) => {
|
|
953
|
+
clearTimeout(timeout);
|
|
954
|
+
const closeEvent = event;
|
|
955
|
+
const code = closeEvent.code ?? 1006;
|
|
956
|
+
const reason = closeEvent.reason ?? "";
|
|
957
|
+
console.log(`[WSClient] Connection closed (code: ${code}, reason: ${reason || "none"})`);
|
|
958
|
+
this.closeHandlers.forEach((handler) => handler(code, reason));
|
|
959
|
+
});
|
|
960
|
+
} catch (error) {
|
|
961
|
+
reject(error);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Build WebSocket URL with auth token
|
|
967
|
+
*/
|
|
968
|
+
buildWSUrl() {
|
|
969
|
+
let url = this.wsURL;
|
|
970
|
+
if (this.authToken) {
|
|
971
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
972
|
+
const paramName = this.authToken.startsWith("ak_") ? "api_key" : "token";
|
|
973
|
+
const encodedToken = this.authToken.startsWith("ak_") ? this.authToken : encodeURIComponent(this.authToken);
|
|
974
|
+
url += `${separator}${paramName}=${encodedToken}`;
|
|
975
|
+
}
|
|
976
|
+
return url;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Register message handler
|
|
980
|
+
*/
|
|
981
|
+
onMessage(handler) {
|
|
982
|
+
this.messageHandlers.add(handler);
|
|
983
|
+
return () => this.messageHandlers.delete(handler);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Unregister message handler
|
|
987
|
+
*/
|
|
988
|
+
offMessage(handler) {
|
|
989
|
+
this.messageHandlers.delete(handler);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Register error handler
|
|
993
|
+
*/
|
|
994
|
+
onError(handler) {
|
|
995
|
+
this.errorHandlers.add(handler);
|
|
996
|
+
return () => this.errorHandlers.delete(handler);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Unregister error handler
|
|
1000
|
+
*/
|
|
1001
|
+
offError(handler) {
|
|
1002
|
+
this.errorHandlers.delete(handler);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Register close handler
|
|
1006
|
+
*/
|
|
1007
|
+
onClose(handler) {
|
|
1008
|
+
this.closeHandlers.add(handler);
|
|
1009
|
+
return () => this.closeHandlers.delete(handler);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Unregister close handler
|
|
1013
|
+
*/
|
|
1014
|
+
offClose(handler) {
|
|
1015
|
+
this.closeHandlers.delete(handler);
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Register open handler
|
|
1019
|
+
*/
|
|
1020
|
+
onOpen(handler) {
|
|
1021
|
+
this.openHandlers.add(handler);
|
|
1022
|
+
return () => this.openHandlers.delete(handler);
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Send data through WebSocket
|
|
1026
|
+
*/
|
|
1027
|
+
send(data) {
|
|
1028
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
1029
|
+
throw new SDKError("WebSocket is not connected", 0, "WS_NOT_CONNECTED");
|
|
1030
|
+
}
|
|
1031
|
+
this.ws.send(data);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Close WebSocket connection
|
|
1035
|
+
*/
|
|
1036
|
+
close() {
|
|
1037
|
+
if (this.isClosed) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
this.isClosed = true;
|
|
1041
|
+
this.ws?.close();
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Check if WebSocket is connected
|
|
1045
|
+
*/
|
|
1046
|
+
isConnected() {
|
|
1047
|
+
return !this.isClosed && this.ws?.readyState === WebSocket.OPEN;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Update auth token
|
|
1051
|
+
*/
|
|
1052
|
+
setAuthToken(token) {
|
|
1053
|
+
this.authToken = token;
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// src/pubsub/client.ts
|
|
1058
|
+
function base64Encode(str) {
|
|
1059
|
+
if (typeof Buffer !== "undefined") {
|
|
1060
|
+
return Buffer.from(str).toString("base64");
|
|
1061
|
+
} else if (typeof btoa !== "undefined") {
|
|
1062
|
+
return btoa(
|
|
1063
|
+
encodeURIComponent(str).replace(
|
|
1064
|
+
/%([0-9A-F]{2})/g,
|
|
1065
|
+
(match, p1) => String.fromCharCode(parseInt(p1, 16))
|
|
1066
|
+
)
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
throw new Error("No base64 encoding method available");
|
|
1070
|
+
}
|
|
1071
|
+
function base64EncodeBytes(bytes) {
|
|
1072
|
+
if (typeof Buffer !== "undefined") {
|
|
1073
|
+
return Buffer.from(bytes).toString("base64");
|
|
1074
|
+
} else if (typeof btoa !== "undefined") {
|
|
1075
|
+
let binary = "";
|
|
1076
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1077
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1078
|
+
}
|
|
1079
|
+
return btoa(binary);
|
|
1080
|
+
}
|
|
1081
|
+
throw new Error("No base64 encoding method available");
|
|
1082
|
+
}
|
|
1083
|
+
function base64Decode(b64) {
|
|
1084
|
+
if (typeof Buffer !== "undefined") {
|
|
1085
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
1086
|
+
} else if (typeof atob !== "undefined") {
|
|
1087
|
+
const binary = atob(b64);
|
|
1088
|
+
const bytes = new Uint8Array(binary.length);
|
|
1089
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1090
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1091
|
+
}
|
|
1092
|
+
return new TextDecoder().decode(bytes);
|
|
1093
|
+
}
|
|
1094
|
+
throw new Error("No base64 decoding method available");
|
|
1095
|
+
}
|
|
1096
|
+
var PubSubClient = class {
|
|
1097
|
+
constructor(httpClient, wsConfig = {}) {
|
|
1098
|
+
this.httpClient = httpClient;
|
|
1099
|
+
this.wsConfig = wsConfig;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Publish a message to a topic via HTTP
|
|
1103
|
+
*/
|
|
1104
|
+
async publish(topic, data) {
|
|
1105
|
+
let dataBase64;
|
|
1106
|
+
if (typeof data === "string") {
|
|
1107
|
+
dataBase64 = base64Encode(data);
|
|
1108
|
+
} else {
|
|
1109
|
+
dataBase64 = base64EncodeBytes(data);
|
|
1110
|
+
}
|
|
1111
|
+
await this.httpClient.post(
|
|
1112
|
+
"/v1/pubsub/publish",
|
|
1113
|
+
{
|
|
1114
|
+
topic,
|
|
1115
|
+
data_base64: dataBase64
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
timeout: 3e4
|
|
1119
|
+
}
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* List active topics in the current namespace
|
|
1124
|
+
*/
|
|
1125
|
+
async topics() {
|
|
1126
|
+
const response = await this.httpClient.get(
|
|
1127
|
+
"/v1/pubsub/topics"
|
|
1128
|
+
);
|
|
1129
|
+
return response.topics || [];
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Get current presence for a topic without subscribing
|
|
1133
|
+
*/
|
|
1134
|
+
async getPresence(topic) {
|
|
1135
|
+
const response = await this.httpClient.get(
|
|
1136
|
+
`/v1/pubsub/presence?topic=${encodeURIComponent(topic)}`
|
|
1137
|
+
);
|
|
1138
|
+
return response;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Subscribe to a topic via WebSocket
|
|
1142
|
+
* Creates one WebSocket connection per topic
|
|
1143
|
+
*/
|
|
1144
|
+
async subscribe(topic, options = {}) {
|
|
1145
|
+
const wsUrl = new URL(this.wsConfig.wsURL || "ws://127.0.0.1:6001");
|
|
1146
|
+
wsUrl.pathname = "/v1/pubsub/ws";
|
|
1147
|
+
wsUrl.searchParams.set("topic", topic);
|
|
1148
|
+
let presence;
|
|
1149
|
+
if (options.presence?.enabled) {
|
|
1150
|
+
presence = options.presence;
|
|
1151
|
+
wsUrl.searchParams.set("presence", "true");
|
|
1152
|
+
wsUrl.searchParams.set("member_id", presence.memberId);
|
|
1153
|
+
if (presence.meta) {
|
|
1154
|
+
wsUrl.searchParams.set("member_meta", JSON.stringify(presence.meta));
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const authToken = this.httpClient.getApiKey() ?? this.httpClient.getToken();
|
|
1158
|
+
const wsClient = new WSClient({
|
|
1159
|
+
...this.wsConfig,
|
|
1160
|
+
wsURL: wsUrl.toString(),
|
|
1161
|
+
authToken
|
|
1162
|
+
});
|
|
1163
|
+
await wsClient.connect();
|
|
1164
|
+
const subscription = new Subscription(
|
|
1165
|
+
wsClient,
|
|
1166
|
+
topic,
|
|
1167
|
+
presence,
|
|
1168
|
+
() => this.getPresence(topic)
|
|
1169
|
+
);
|
|
1170
|
+
if (options.onMessage) {
|
|
1171
|
+
subscription.onMessage(options.onMessage);
|
|
1172
|
+
}
|
|
1173
|
+
if (options.onError) {
|
|
1174
|
+
subscription.onError(options.onError);
|
|
1175
|
+
}
|
|
1176
|
+
if (options.onClose) {
|
|
1177
|
+
subscription.onClose(options.onClose);
|
|
1178
|
+
}
|
|
1179
|
+
return subscription;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
var Subscription = class {
|
|
1183
|
+
constructor(wsClient, topic, presenceOptions, getPresenceFn) {
|
|
1184
|
+
this.messageHandlers = /* @__PURE__ */ new Set();
|
|
1185
|
+
this.errorHandlers = /* @__PURE__ */ new Set();
|
|
1186
|
+
this.closeHandlers = /* @__PURE__ */ new Set();
|
|
1187
|
+
this.isClosed = false;
|
|
1188
|
+
this.wsMessageHandler = null;
|
|
1189
|
+
this.wsErrorHandler = null;
|
|
1190
|
+
this.wsCloseHandler = null;
|
|
1191
|
+
this.wsClient = wsClient;
|
|
1192
|
+
this.topic = topic;
|
|
1193
|
+
this.presenceOptions = presenceOptions;
|
|
1194
|
+
this.getPresenceFn = getPresenceFn;
|
|
1195
|
+
this.wsMessageHandler = (data) => {
|
|
1196
|
+
try {
|
|
1197
|
+
const envelope = JSON.parse(data);
|
|
1198
|
+
if (!envelope || typeof envelope !== "object") {
|
|
1199
|
+
throw new Error("Invalid envelope: not an object");
|
|
1200
|
+
}
|
|
1201
|
+
if (envelope.type === "presence.join" || envelope.type === "presence.leave") {
|
|
1202
|
+
if (!envelope.member_id) {
|
|
1203
|
+
console.warn("[Subscription] Presence event missing member_id");
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const presenceMember = {
|
|
1207
|
+
memberId: envelope.member_id,
|
|
1208
|
+
joinedAt: envelope.timestamp,
|
|
1209
|
+
meta: envelope.meta
|
|
1210
|
+
};
|
|
1211
|
+
if (envelope.type === "presence.join" && this.presenceOptions?.onJoin) {
|
|
1212
|
+
this.presenceOptions.onJoin(presenceMember);
|
|
1213
|
+
} else if (envelope.type === "presence.leave" && this.presenceOptions?.onLeave) {
|
|
1214
|
+
this.presenceOptions.onLeave(presenceMember);
|
|
1215
|
+
}
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (!envelope.data || typeof envelope.data !== "string") {
|
|
1219
|
+
throw new Error("Invalid envelope: missing or invalid data field");
|
|
1220
|
+
}
|
|
1221
|
+
if (!envelope.topic || typeof envelope.topic !== "string") {
|
|
1222
|
+
throw new Error("Invalid envelope: missing or invalid topic field");
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof envelope.timestamp !== "number") {
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
"Invalid envelope: missing or invalid timestamp field"
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
const messageData = base64Decode(envelope.data);
|
|
1230
|
+
const message = {
|
|
1231
|
+
topic: envelope.topic,
|
|
1232
|
+
data: messageData,
|
|
1233
|
+
timestamp: envelope.timestamp
|
|
1234
|
+
};
|
|
1235
|
+
console.log("[Subscription] Received message on topic:", this.topic);
|
|
1236
|
+
this.messageHandlers.forEach((handler) => handler(message));
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
console.error("[Subscription] Error processing message:", error);
|
|
1239
|
+
this.errorHandlers.forEach(
|
|
1240
|
+
(handler) => handler(error instanceof Error ? error : new Error(String(error)))
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
this.wsClient.onMessage(this.wsMessageHandler);
|
|
1245
|
+
this.wsErrorHandler = (error) => {
|
|
1246
|
+
this.errorHandlers.forEach((handler) => handler(error));
|
|
1247
|
+
};
|
|
1248
|
+
this.wsClient.onError(this.wsErrorHandler);
|
|
1249
|
+
this.wsCloseHandler = (code, reason) => {
|
|
1250
|
+
this.closeHandlers.forEach((handler) => handler(code, reason));
|
|
1251
|
+
};
|
|
1252
|
+
this.wsClient.onClose(this.wsCloseHandler);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Get current presence (requires presence.enabled on subscribe)
|
|
1256
|
+
*/
|
|
1257
|
+
async getPresence() {
|
|
1258
|
+
if (!this.presenceOptions?.enabled) {
|
|
1259
|
+
throw new Error("Presence is not enabled for this subscription");
|
|
1260
|
+
}
|
|
1261
|
+
const response = await this.getPresenceFn();
|
|
1262
|
+
return response.members;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Check if presence is enabled for this subscription
|
|
1266
|
+
*/
|
|
1267
|
+
hasPresence() {
|
|
1268
|
+
return !!this.presenceOptions?.enabled;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Register message handler
|
|
1272
|
+
*/
|
|
1273
|
+
onMessage(handler) {
|
|
1274
|
+
this.messageHandlers.add(handler);
|
|
1275
|
+
return () => this.messageHandlers.delete(handler);
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Register error handler
|
|
1279
|
+
*/
|
|
1280
|
+
onError(handler) {
|
|
1281
|
+
this.errorHandlers.add(handler);
|
|
1282
|
+
return () => this.errorHandlers.delete(handler);
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Register close handler
|
|
1286
|
+
*/
|
|
1287
|
+
onClose(handler) {
|
|
1288
|
+
this.closeHandlers.add(handler);
|
|
1289
|
+
return () => this.closeHandlers.delete(handler);
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Close subscription and underlying WebSocket
|
|
1293
|
+
*/
|
|
1294
|
+
close() {
|
|
1295
|
+
if (this.isClosed) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
this.isClosed = true;
|
|
1299
|
+
if (this.wsMessageHandler) {
|
|
1300
|
+
this.wsClient.offMessage(this.wsMessageHandler);
|
|
1301
|
+
this.wsMessageHandler = null;
|
|
1302
|
+
}
|
|
1303
|
+
if (this.wsErrorHandler) {
|
|
1304
|
+
this.wsClient.offError(this.wsErrorHandler);
|
|
1305
|
+
this.wsErrorHandler = null;
|
|
1306
|
+
}
|
|
1307
|
+
if (this.wsCloseHandler) {
|
|
1308
|
+
this.wsClient.offClose(this.wsCloseHandler);
|
|
1309
|
+
this.wsCloseHandler = null;
|
|
1310
|
+
}
|
|
1311
|
+
this.messageHandlers.clear();
|
|
1312
|
+
this.errorHandlers.clear();
|
|
1313
|
+
this.closeHandlers.clear();
|
|
1314
|
+
this.wsClient.close();
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Check if subscription is active
|
|
1318
|
+
*/
|
|
1319
|
+
isConnected() {
|
|
1320
|
+
return !this.isClosed && this.wsClient.isConnected();
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
// src/network/client.ts
|
|
1325
|
+
var NetworkClient = class {
|
|
1326
|
+
constructor(httpClient) {
|
|
1327
|
+
this.httpClient = httpClient;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Check gateway health.
|
|
1331
|
+
*/
|
|
1332
|
+
async health() {
|
|
1333
|
+
try {
|
|
1334
|
+
await this.httpClient.get("/v1/health");
|
|
1335
|
+
return true;
|
|
1336
|
+
} catch {
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Get network status.
|
|
1342
|
+
*/
|
|
1343
|
+
async status() {
|
|
1344
|
+
const response = await this.httpClient.get(
|
|
1345
|
+
"/v1/network/status"
|
|
1346
|
+
);
|
|
1347
|
+
return response;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Get connected peers.
|
|
1351
|
+
*/
|
|
1352
|
+
async peers() {
|
|
1353
|
+
const response = await this.httpClient.get(
|
|
1354
|
+
"/v1/network/peers"
|
|
1355
|
+
);
|
|
1356
|
+
return response.peers || [];
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Connect to a peer.
|
|
1360
|
+
*/
|
|
1361
|
+
async connect(peerAddr) {
|
|
1362
|
+
await this.httpClient.post("/v1/network/connect", { peer_addr: peerAddr });
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Disconnect from a peer.
|
|
1366
|
+
*/
|
|
1367
|
+
async disconnect(peerId) {
|
|
1368
|
+
await this.httpClient.post("/v1/network/disconnect", { peer_id: peerId });
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Proxy an HTTP request through the Anyone network.
|
|
1372
|
+
* Requires authentication (API key or JWT).
|
|
1373
|
+
*
|
|
1374
|
+
* @param request - The proxy request configuration
|
|
1375
|
+
* @returns The proxied response
|
|
1376
|
+
* @throws {SDKError} If the Anyone proxy is not available or the request fails
|
|
1377
|
+
*
|
|
1378
|
+
* @example
|
|
1379
|
+
* ```ts
|
|
1380
|
+
* const response = await client.network.proxyAnon({
|
|
1381
|
+
* url: 'https://api.example.com/data',
|
|
1382
|
+
* method: 'GET',
|
|
1383
|
+
* headers: {
|
|
1384
|
+
* 'Accept': 'application/json'
|
|
1385
|
+
* }
|
|
1386
|
+
* });
|
|
1387
|
+
*
|
|
1388
|
+
* console.log(response.status_code); // 200
|
|
1389
|
+
* console.log(response.body); // Response data
|
|
1390
|
+
* ```
|
|
1391
|
+
*/
|
|
1392
|
+
async proxyAnon(request) {
|
|
1393
|
+
const response = await this.httpClient.post(
|
|
1394
|
+
"/v1/proxy/anon",
|
|
1395
|
+
request
|
|
1396
|
+
);
|
|
1397
|
+
if (response.error) {
|
|
1398
|
+
throw new Error(`Proxy request failed: ${response.error}`);
|
|
1399
|
+
}
|
|
1400
|
+
return response;
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
// src/cache/client.ts
|
|
1405
|
+
var CacheClient = class {
|
|
1406
|
+
constructor(httpClient) {
|
|
1407
|
+
this.httpClient = httpClient;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Check cache service health
|
|
1411
|
+
*/
|
|
1412
|
+
async health() {
|
|
1413
|
+
return this.httpClient.get("/v1/cache/health");
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Get a value from cache
|
|
1417
|
+
* Returns null if the key is not found (cache miss/expired), which is normal behavior
|
|
1418
|
+
*/
|
|
1419
|
+
async get(dmap, key) {
|
|
1420
|
+
try {
|
|
1421
|
+
return await this.httpClient.post("/v1/cache/get", {
|
|
1422
|
+
dmap,
|
|
1423
|
+
key
|
|
1424
|
+
});
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
if (error instanceof SDKError && (error.httpStatus === 404 || error.httpStatus === 500 && error.message?.toLowerCase().includes("key not found"))) {
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
throw error;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Put a value into cache
|
|
1434
|
+
*/
|
|
1435
|
+
async put(dmap, key, value, ttl) {
|
|
1436
|
+
return this.httpClient.post("/v1/cache/put", {
|
|
1437
|
+
dmap,
|
|
1438
|
+
key,
|
|
1439
|
+
value,
|
|
1440
|
+
ttl
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Delete a value from cache
|
|
1445
|
+
*/
|
|
1446
|
+
async delete(dmap, key) {
|
|
1447
|
+
return this.httpClient.post("/v1/cache/delete", {
|
|
1448
|
+
dmap,
|
|
1449
|
+
key
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Get multiple values from cache in a single request
|
|
1454
|
+
* Returns a map of key -> value (or null if not found)
|
|
1455
|
+
* Gracefully handles 404 errors (endpoint not implemented) by returning empty results
|
|
1456
|
+
*/
|
|
1457
|
+
async multiGet(dmap, keys) {
|
|
1458
|
+
try {
|
|
1459
|
+
if (keys.length === 0) {
|
|
1460
|
+
return /* @__PURE__ */ new Map();
|
|
1461
|
+
}
|
|
1462
|
+
const response = await this.httpClient.post(
|
|
1463
|
+
"/v1/cache/mget",
|
|
1464
|
+
{
|
|
1465
|
+
dmap,
|
|
1466
|
+
keys
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1469
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1470
|
+
keys.forEach((key) => {
|
|
1471
|
+
resultMap.set(key, null);
|
|
1472
|
+
});
|
|
1473
|
+
if (response.results) {
|
|
1474
|
+
response.results.forEach(({ key, value }) => {
|
|
1475
|
+
resultMap.set(key, value);
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
return resultMap;
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
if (error instanceof SDKError && error.httpStatus === 404) {
|
|
1481
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
1482
|
+
keys.forEach((key) => {
|
|
1483
|
+
resultMap2.set(key, null);
|
|
1484
|
+
});
|
|
1485
|
+
return resultMap2;
|
|
1486
|
+
}
|
|
1487
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1488
|
+
keys.forEach((key) => {
|
|
1489
|
+
resultMap.set(key, null);
|
|
1490
|
+
});
|
|
1491
|
+
console.error(`[CacheClient] Error in multiGet for ${dmap}:`, error);
|
|
1492
|
+
return resultMap;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Scan keys in a distributed map, optionally matching a regex pattern
|
|
1497
|
+
*/
|
|
1498
|
+
async scan(dmap, match) {
|
|
1499
|
+
return this.httpClient.post("/v1/cache/scan", {
|
|
1500
|
+
dmap,
|
|
1501
|
+
match
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
// src/storage/client.ts
|
|
1507
|
+
var StorageClient = class {
|
|
1508
|
+
constructor(httpClient) {
|
|
1509
|
+
this.httpClient = httpClient;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Upload content to IPFS and optionally pin it.
|
|
1513
|
+
* Supports both File objects (browser) and Buffer/ReadableStream (Node.js).
|
|
1514
|
+
*
|
|
1515
|
+
* @param file - File to upload (File, Blob, or Buffer)
|
|
1516
|
+
* @param name - Optional filename
|
|
1517
|
+
* @param options - Optional upload options
|
|
1518
|
+
* @param options.pin - Whether to pin the content (default: true). Pinning happens asynchronously on the backend.
|
|
1519
|
+
* @returns Upload result with CID
|
|
1520
|
+
*
|
|
1521
|
+
* @example
|
|
1522
|
+
* ```ts
|
|
1523
|
+
* // Browser
|
|
1524
|
+
* const fileInput = document.querySelector('input[type="file"]');
|
|
1525
|
+
* const file = fileInput.files[0];
|
|
1526
|
+
* const result = await client.storage.upload(file, file.name);
|
|
1527
|
+
* console.log(result.cid);
|
|
1528
|
+
*
|
|
1529
|
+
* // Node.js
|
|
1530
|
+
* const fs = require('fs');
|
|
1531
|
+
* const fileBuffer = fs.readFileSync('image.jpg');
|
|
1532
|
+
* const result = await client.storage.upload(fileBuffer, 'image.jpg', { pin: true });
|
|
1533
|
+
* ```
|
|
1534
|
+
*/
|
|
1535
|
+
async upload(file, name, options) {
|
|
1536
|
+
const formData = new FormData();
|
|
1537
|
+
if (file instanceof File) {
|
|
1538
|
+
formData.append("file", file);
|
|
1539
|
+
} else if (file instanceof Blob) {
|
|
1540
|
+
formData.append("file", file, name);
|
|
1541
|
+
} else if (file instanceof ArrayBuffer) {
|
|
1542
|
+
const blob = new Blob([file]);
|
|
1543
|
+
formData.append("file", blob, name);
|
|
1544
|
+
} else if (file instanceof Uint8Array) {
|
|
1545
|
+
const buffer = file.buffer.slice(
|
|
1546
|
+
file.byteOffset,
|
|
1547
|
+
file.byteOffset + file.byteLength
|
|
1548
|
+
);
|
|
1549
|
+
const blob = new Blob([buffer], { type: "application/octet-stream" });
|
|
1550
|
+
formData.append("file", blob, name);
|
|
1551
|
+
} else if (file instanceof ReadableStream) {
|
|
1552
|
+
const chunks = [];
|
|
1553
|
+
const reader = file.getReader();
|
|
1554
|
+
while (true) {
|
|
1555
|
+
const { done, value } = await reader.read();
|
|
1556
|
+
if (done) break;
|
|
1557
|
+
const buffer = value.buffer.slice(
|
|
1558
|
+
value.byteOffset,
|
|
1559
|
+
value.byteOffset + value.byteLength
|
|
1560
|
+
);
|
|
1561
|
+
chunks.push(buffer);
|
|
1562
|
+
}
|
|
1563
|
+
const blob = new Blob(chunks);
|
|
1564
|
+
formData.append("file", blob, name);
|
|
1565
|
+
} else {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
"Unsupported file type. Use File, Blob, ArrayBuffer, Uint8Array, or ReadableStream."
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
const shouldPin = options?.pin !== false;
|
|
1571
|
+
formData.append("pin", shouldPin ? "true" : "false");
|
|
1572
|
+
return this.httpClient.uploadFile(
|
|
1573
|
+
"/v1/storage/upload",
|
|
1574
|
+
formData,
|
|
1575
|
+
{ timeout: 3e5 }
|
|
1576
|
+
// 5 minute timeout for large files
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Pin an existing CID
|
|
1581
|
+
*
|
|
1582
|
+
* @param cid - Content ID to pin
|
|
1583
|
+
* @param name - Optional name for the pin
|
|
1584
|
+
* @returns Pin result
|
|
1585
|
+
*/
|
|
1586
|
+
async pin(cid, name) {
|
|
1587
|
+
return this.httpClient.post("/v1/storage/pin", {
|
|
1588
|
+
cid,
|
|
1589
|
+
name
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Get the pin status for a CID
|
|
1594
|
+
*
|
|
1595
|
+
* @param cid - Content ID to check
|
|
1596
|
+
* @returns Pin status information
|
|
1597
|
+
*/
|
|
1598
|
+
async status(cid) {
|
|
1599
|
+
return this.httpClient.get(`/v1/storage/status/${cid}`);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Retrieve content from IPFS by CID
|
|
1603
|
+
*
|
|
1604
|
+
* @param cid - Content ID to retrieve
|
|
1605
|
+
* @returns ReadableStream of the content
|
|
1606
|
+
*
|
|
1607
|
+
* @example
|
|
1608
|
+
* ```ts
|
|
1609
|
+
* const stream = await client.storage.get(cid);
|
|
1610
|
+
* const reader = stream.getReader();
|
|
1611
|
+
* while (true) {
|
|
1612
|
+
* const { done, value } = await reader.read();
|
|
1613
|
+
* if (done) break;
|
|
1614
|
+
* // Process chunk
|
|
1615
|
+
* }
|
|
1616
|
+
* ```
|
|
1617
|
+
*/
|
|
1618
|
+
async get(cid) {
|
|
1619
|
+
const maxAttempts = 8;
|
|
1620
|
+
let lastError = null;
|
|
1621
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1622
|
+
try {
|
|
1623
|
+
const response = await this.httpClient.getBinary(
|
|
1624
|
+
`/v1/storage/get/${cid}`
|
|
1625
|
+
);
|
|
1626
|
+
if (!response.body) {
|
|
1627
|
+
throw new Error("Response body is null");
|
|
1628
|
+
}
|
|
1629
|
+
return response.body;
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
lastError = error;
|
|
1632
|
+
const isNotFound = error?.httpStatus === 404 || error?.message?.includes("not found") || error?.message?.includes("404");
|
|
1633
|
+
if (!isNotFound || attempt === maxAttempts) {
|
|
1634
|
+
throw error;
|
|
1635
|
+
}
|
|
1636
|
+
const backoffMs = Math.min(attempt * 1e3, 3e3);
|
|
1637
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
throw lastError || new Error("Failed to retrieve content");
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Retrieve content from IPFS by CID and return the full Response object
|
|
1644
|
+
* Useful when you need access to response headers (e.g., content-length)
|
|
1645
|
+
*
|
|
1646
|
+
* @param cid - Content ID to retrieve
|
|
1647
|
+
* @returns Response object with body stream and headers
|
|
1648
|
+
*
|
|
1649
|
+
* @example
|
|
1650
|
+
* ```ts
|
|
1651
|
+
* const response = await client.storage.getBinary(cid);
|
|
1652
|
+
* const contentLength = response.headers.get('content-length');
|
|
1653
|
+
* const reader = response.body.getReader();
|
|
1654
|
+
* // ... read stream
|
|
1655
|
+
* ```
|
|
1656
|
+
*/
|
|
1657
|
+
async getBinary(cid) {
|
|
1658
|
+
const maxAttempts = 8;
|
|
1659
|
+
let lastError = null;
|
|
1660
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1661
|
+
try {
|
|
1662
|
+
const response = await this.httpClient.getBinary(
|
|
1663
|
+
`/v1/storage/get/${cid}`
|
|
1664
|
+
);
|
|
1665
|
+
if (!response) {
|
|
1666
|
+
throw new Error("Response is null");
|
|
1667
|
+
}
|
|
1668
|
+
return response;
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
lastError = error;
|
|
1671
|
+
const isNotFound = error?.httpStatus === 404 || error?.message?.includes("not found") || error?.message?.includes("404");
|
|
1672
|
+
if (!isNotFound || attempt === maxAttempts) {
|
|
1673
|
+
throw error;
|
|
1674
|
+
}
|
|
1675
|
+
const backoffMs = Math.min(attempt * 1e3, 3e3);
|
|
1676
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
throw lastError || new Error("Failed to retrieve content");
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Unpin a CID
|
|
1683
|
+
*
|
|
1684
|
+
* @param cid - Content ID to unpin
|
|
1685
|
+
*/
|
|
1686
|
+
async unpin(cid) {
|
|
1687
|
+
await this.httpClient.delete(`/v1/storage/unpin/${cid}`);
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
// src/functions/client.ts
|
|
1692
|
+
var FunctionsClient = class {
|
|
1693
|
+
constructor(httpClient, config) {
|
|
1694
|
+
this.httpClient = httpClient;
|
|
1695
|
+
this.gatewayURL = config?.gatewayURL;
|
|
1696
|
+
this.namespace = config?.namespace ?? "default";
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Invoke a serverless function by name
|
|
1700
|
+
*
|
|
1701
|
+
* @param functionName - Name of the function to invoke
|
|
1702
|
+
* @param input - Input payload for the function
|
|
1703
|
+
* @returns The function response
|
|
1704
|
+
*/
|
|
1705
|
+
async invoke(functionName, input) {
|
|
1706
|
+
const url = this.gatewayURL ? `${this.gatewayURL}/v1/invoke/${this.namespace}/${functionName}` : `/v1/invoke/${this.namespace}/${functionName}`;
|
|
1707
|
+
try {
|
|
1708
|
+
const response = await this.httpClient.post(url, input);
|
|
1709
|
+
return response;
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
if (error instanceof SDKError) {
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
throw new SDKError(
|
|
1715
|
+
`Function ${functionName} failed`,
|
|
1716
|
+
500,
|
|
1717
|
+
error instanceof Error ? error.message : String(error)
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1723
|
+
// src/vault/transport/guardian.ts
|
|
1724
|
+
var GuardianError = class extends Error {
|
|
1725
|
+
constructor(code, message) {
|
|
1726
|
+
super(message);
|
|
1727
|
+
this.code = code;
|
|
1728
|
+
this.name = "GuardianError";
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1732
|
+
var GuardianClient = class {
|
|
1733
|
+
constructor(endpoint, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1734
|
+
this.sessionToken = null;
|
|
1735
|
+
this.baseUrl = `http://${endpoint.address}:${endpoint.port}`;
|
|
1736
|
+
this.timeoutMs = timeoutMs;
|
|
1737
|
+
}
|
|
1738
|
+
/** Set a session token for authenticated V2 requests. */
|
|
1739
|
+
setSessionToken(token) {
|
|
1740
|
+
this.sessionToken = token;
|
|
1741
|
+
}
|
|
1742
|
+
/** Get the current session token. */
|
|
1743
|
+
getSessionToken() {
|
|
1744
|
+
return this.sessionToken;
|
|
1745
|
+
}
|
|
1746
|
+
/** Clear the session token. */
|
|
1747
|
+
clearSessionToken() {
|
|
1748
|
+
this.sessionToken = null;
|
|
1749
|
+
}
|
|
1750
|
+
// ── V1 endpoints ────────────────────────────────────────────────────
|
|
1751
|
+
/** GET /v1/vault/health */
|
|
1752
|
+
async health() {
|
|
1753
|
+
return this.get("/v1/vault/health");
|
|
1754
|
+
}
|
|
1755
|
+
/** GET /v1/vault/status */
|
|
1756
|
+
async status() {
|
|
1757
|
+
return this.get("/v1/vault/status");
|
|
1758
|
+
}
|
|
1759
|
+
/** GET /v1/vault/guardians */
|
|
1760
|
+
async guardians() {
|
|
1761
|
+
return this.get("/v1/vault/guardians");
|
|
1762
|
+
}
|
|
1763
|
+
/** POST /v1/vault/push — store a share (V1). */
|
|
1764
|
+
async push(identity, share) {
|
|
1765
|
+
return this.post("/v1/vault/push", {
|
|
1766
|
+
identity,
|
|
1767
|
+
share: uint8ToBase64(share)
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
/** POST /v1/vault/pull — retrieve a share (V1). */
|
|
1771
|
+
async pull(identity) {
|
|
1772
|
+
const resp = await this.post("/v1/vault/pull", { identity });
|
|
1773
|
+
return base64ToUint8(resp.share);
|
|
1774
|
+
}
|
|
1775
|
+
/** Check if this guardian is reachable. */
|
|
1776
|
+
async isReachable() {
|
|
1777
|
+
try {
|
|
1778
|
+
await this.health();
|
|
1779
|
+
return true;
|
|
1780
|
+
} catch {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
// ── V2 auth endpoints ───────────────────────────────────────────────
|
|
1785
|
+
/** POST /v2/vault/auth/challenge — request an auth challenge. */
|
|
1786
|
+
async requestChallenge(identity) {
|
|
1787
|
+
return this.post("/v2/vault/auth/challenge", { identity });
|
|
1788
|
+
}
|
|
1789
|
+
/** POST /v2/vault/auth/session — exchange challenge for session token. */
|
|
1790
|
+
async createSession(identity, nonce, created_ns, tag) {
|
|
1791
|
+
return this.post("/v2/vault/auth/session", {
|
|
1792
|
+
identity,
|
|
1793
|
+
nonce,
|
|
1794
|
+
created_ns,
|
|
1795
|
+
tag
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
// ── V2 secrets CRUD ─────────────────────────────────────────────────
|
|
1799
|
+
/** PUT /v2/vault/secrets/{name} — store a secret. Requires session token. */
|
|
1800
|
+
async putSecret(name, share, version) {
|
|
1801
|
+
return this.authedRequest("PUT", `/v2/vault/secrets/${encodeURIComponent(name)}`, {
|
|
1802
|
+
share: uint8ToBase64(share),
|
|
1803
|
+
version
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
/** GET /v2/vault/secrets/{name} — retrieve a secret. Requires session token. */
|
|
1807
|
+
async getSecret(name) {
|
|
1808
|
+
const resp = await this.authedRequest("GET", `/v2/vault/secrets/${encodeURIComponent(name)}`);
|
|
1809
|
+
return {
|
|
1810
|
+
share: base64ToUint8(resp.share),
|
|
1811
|
+
name: resp.name,
|
|
1812
|
+
version: resp.version,
|
|
1813
|
+
created_ns: resp.created_ns,
|
|
1814
|
+
updated_ns: resp.updated_ns
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
/** DELETE /v2/vault/secrets/{name} — delete a secret. Requires session token. */
|
|
1818
|
+
async deleteSecret(name) {
|
|
1819
|
+
return this.authedRequest("DELETE", `/v2/vault/secrets/${encodeURIComponent(name)}`);
|
|
1820
|
+
}
|
|
1821
|
+
/** GET /v2/vault/secrets — list all secrets. Requires session token. */
|
|
1822
|
+
async listSecrets() {
|
|
1823
|
+
return this.authedRequest("GET", "/v2/vault/secrets");
|
|
1824
|
+
}
|
|
1825
|
+
// ── Internal HTTP methods ───────────────────────────────────────────
|
|
1826
|
+
async authedRequest(method, path, body) {
|
|
1827
|
+
if (!this.sessionToken) {
|
|
1828
|
+
throw new GuardianError("AUTH", "No session token set. Call authenticate() first.");
|
|
1829
|
+
}
|
|
1830
|
+
const controller = new AbortController();
|
|
1831
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1832
|
+
try {
|
|
1833
|
+
const headers = {
|
|
1834
|
+
"X-Session-Token": this.sessionToken
|
|
1835
|
+
};
|
|
1836
|
+
const init = {
|
|
1837
|
+
method,
|
|
1838
|
+
headers,
|
|
1839
|
+
signal: controller.signal
|
|
1840
|
+
};
|
|
1841
|
+
if (body !== void 0) {
|
|
1842
|
+
headers["Content-Type"] = "application/json";
|
|
1843
|
+
init.body = JSON.stringify(body);
|
|
1844
|
+
}
|
|
1845
|
+
const resp = await fetch(`${this.baseUrl}${path}`, init);
|
|
1846
|
+
if (!resp.ok) {
|
|
1847
|
+
const errBody = await resp.json().catch(() => ({}));
|
|
1848
|
+
const msg = errBody.error || `HTTP ${resp.status}`;
|
|
1849
|
+
throw new GuardianError(classifyHttpStatus(resp.status), msg);
|
|
1850
|
+
}
|
|
1851
|
+
return await resp.json();
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
throw classifyError(err);
|
|
1854
|
+
} finally {
|
|
1855
|
+
clearTimeout(timeout);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async get(path) {
|
|
1859
|
+
const controller = new AbortController();
|
|
1860
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1861
|
+
try {
|
|
1862
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
1863
|
+
method: "GET",
|
|
1864
|
+
signal: controller.signal
|
|
1865
|
+
});
|
|
1866
|
+
if (!resp.ok) {
|
|
1867
|
+
const body = await resp.json().catch(() => ({}));
|
|
1868
|
+
const msg = body.error || `HTTP ${resp.status}`;
|
|
1869
|
+
throw new GuardianError(classifyHttpStatus(resp.status), msg);
|
|
1870
|
+
}
|
|
1871
|
+
return await resp.json();
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
throw classifyError(err);
|
|
1874
|
+
} finally {
|
|
1875
|
+
clearTimeout(timeout);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async post(path, body) {
|
|
1879
|
+
const controller = new AbortController();
|
|
1880
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1881
|
+
try {
|
|
1882
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
1883
|
+
method: "POST",
|
|
1884
|
+
headers: { "Content-Type": "application/json" },
|
|
1885
|
+
body: JSON.stringify(body),
|
|
1886
|
+
signal: controller.signal
|
|
1887
|
+
});
|
|
1888
|
+
if (!resp.ok) {
|
|
1889
|
+
const errBody = await resp.json().catch(() => ({}));
|
|
1890
|
+
const msg = errBody.error || `HTTP ${resp.status}`;
|
|
1891
|
+
throw new GuardianError(classifyHttpStatus(resp.status), msg);
|
|
1892
|
+
}
|
|
1893
|
+
return await resp.json();
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
throw classifyError(err);
|
|
1896
|
+
} finally {
|
|
1897
|
+
clearTimeout(timeout);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
function classifyHttpStatus(status) {
|
|
1902
|
+
if (status === 404) return "NOT_FOUND";
|
|
1903
|
+
if (status === 401 || status === 403) return "AUTH";
|
|
1904
|
+
if (status === 409) return "CONFLICT";
|
|
1905
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
1906
|
+
return "NETWORK";
|
|
1907
|
+
}
|
|
1908
|
+
function classifyError(err) {
|
|
1909
|
+
if (err instanceof GuardianError) return err;
|
|
1910
|
+
if (err instanceof Error) {
|
|
1911
|
+
if (err.name === "AbortError") {
|
|
1912
|
+
return new GuardianError("TIMEOUT", `Request timed out: ${err.message}`);
|
|
1913
|
+
}
|
|
1914
|
+
if (err.name === "TypeError" || err.message.includes("fetch")) {
|
|
1915
|
+
return new GuardianError("NETWORK", `Network error: ${err.message}`);
|
|
1916
|
+
}
|
|
1917
|
+
return new GuardianError("NETWORK", err.message);
|
|
1918
|
+
}
|
|
1919
|
+
return new GuardianError("NETWORK", String(err));
|
|
1920
|
+
}
|
|
1921
|
+
function uint8ToBase64(bytes) {
|
|
1922
|
+
if (typeof Buffer !== "undefined") {
|
|
1923
|
+
return Buffer.from(bytes).toString("base64");
|
|
1924
|
+
}
|
|
1925
|
+
let binary = "";
|
|
1926
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1927
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1928
|
+
}
|
|
1929
|
+
return btoa(binary);
|
|
1930
|
+
}
|
|
1931
|
+
function base64ToUint8(b64) {
|
|
1932
|
+
if (typeof Buffer !== "undefined") {
|
|
1933
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
1934
|
+
}
|
|
1935
|
+
const binary = atob(b64);
|
|
1936
|
+
const bytes = new Uint8Array(binary.length);
|
|
1937
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1938
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1939
|
+
}
|
|
1940
|
+
return bytes;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// src/vault/auth.ts
|
|
1944
|
+
var AuthClient2 = class {
|
|
1945
|
+
constructor(identityHex, timeoutMs = 1e4) {
|
|
1946
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
1947
|
+
this.identityHex = identityHex;
|
|
1948
|
+
this.timeoutMs = timeoutMs;
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Authenticate with a guardian and cache the session token.
|
|
1952
|
+
* Returns a GuardianClient with the session token set.
|
|
1953
|
+
*/
|
|
1954
|
+
async authenticate(endpoint) {
|
|
1955
|
+
const key = `${endpoint.address}:${endpoint.port}`;
|
|
1956
|
+
const cached = this.sessions.get(key);
|
|
1957
|
+
if (cached) {
|
|
1958
|
+
const nowNs = Date.now() * 1e6;
|
|
1959
|
+
if (cached.expiryNs > nowNs + 3e10) {
|
|
1960
|
+
const client2 = new GuardianClient(endpoint, this.timeoutMs);
|
|
1961
|
+
client2.setSessionToken(cached.token);
|
|
1962
|
+
return client2;
|
|
1963
|
+
}
|
|
1964
|
+
this.sessions.delete(key);
|
|
1965
|
+
}
|
|
1966
|
+
const client = new GuardianClient(endpoint, this.timeoutMs);
|
|
1967
|
+
const challenge = await client.requestChallenge(this.identityHex);
|
|
1968
|
+
const session = await client.createSession(
|
|
1969
|
+
this.identityHex,
|
|
1970
|
+
challenge.nonce,
|
|
1971
|
+
challenge.created_ns,
|
|
1972
|
+
challenge.tag
|
|
1973
|
+
);
|
|
1974
|
+
const token = `${session.identity}:${session.expiry_ns}:${session.tag}`;
|
|
1975
|
+
client.setSessionToken(token);
|
|
1976
|
+
this.sessions.set(key, { token, expiryNs: session.expiry_ns });
|
|
1977
|
+
return client;
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Authenticate with multiple guardians in parallel.
|
|
1981
|
+
* Returns authenticated GuardianClients for all that succeed.
|
|
1982
|
+
*/
|
|
1983
|
+
async authenticateAll(endpoints) {
|
|
1984
|
+
const results = await Promise.allSettled(
|
|
1985
|
+
endpoints.map(async (ep) => {
|
|
1986
|
+
const client = await this.authenticate(ep);
|
|
1987
|
+
return { client, endpoint: ep };
|
|
1988
|
+
})
|
|
1989
|
+
);
|
|
1990
|
+
const authenticated = [];
|
|
1991
|
+
for (const r of results) {
|
|
1992
|
+
if (r.status === "fulfilled") {
|
|
1993
|
+
authenticated.push(r.value);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return authenticated;
|
|
1997
|
+
}
|
|
1998
|
+
/** Clear all cached sessions. */
|
|
1999
|
+
clearSessions() {
|
|
2000
|
+
this.sessions.clear();
|
|
2001
|
+
}
|
|
2002
|
+
/** Get the identity hex string. */
|
|
2003
|
+
getIdentityHex() {
|
|
2004
|
+
return this.identityHex;
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
|
|
2008
|
+
// src/vault/transport/fanout.ts
|
|
2009
|
+
async function fanOut(guardians, operation) {
|
|
2010
|
+
const results = await Promise.allSettled(
|
|
2011
|
+
guardians.map(async (endpoint) => {
|
|
2012
|
+
const client = new GuardianClient(endpoint);
|
|
2013
|
+
const result = await operation(client);
|
|
2014
|
+
return { endpoint, result, error: null };
|
|
2015
|
+
})
|
|
2016
|
+
);
|
|
2017
|
+
return results.map((r, i) => {
|
|
2018
|
+
if (r.status === "fulfilled") return r.value;
|
|
2019
|
+
const reason = r.reason;
|
|
2020
|
+
const errorCode = reason instanceof GuardianError ? reason.code : void 0;
|
|
2021
|
+
return {
|
|
2022
|
+
endpoint: guardians[i],
|
|
2023
|
+
result: null,
|
|
2024
|
+
error: reason.message,
|
|
2025
|
+
errorCode
|
|
2026
|
+
};
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
async function fanOutIndexed(guardians, operation) {
|
|
2030
|
+
const results = await Promise.allSettled(
|
|
2031
|
+
guardians.map(async (endpoint, i) => {
|
|
2032
|
+
const client = new GuardianClient(endpoint);
|
|
2033
|
+
const result = await operation(client, i);
|
|
2034
|
+
return { endpoint, result, error: null };
|
|
2035
|
+
})
|
|
2036
|
+
);
|
|
2037
|
+
return results.map((r, i) => {
|
|
2038
|
+
if (r.status === "fulfilled") return r.value;
|
|
2039
|
+
const reason = r.reason;
|
|
2040
|
+
const errorCode = reason instanceof GuardianError ? reason.code : void 0;
|
|
2041
|
+
return {
|
|
2042
|
+
endpoint: guardians[i],
|
|
2043
|
+
result: null,
|
|
2044
|
+
error: reason.message,
|
|
2045
|
+
errorCode
|
|
2046
|
+
};
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
function withTimeout(promise, ms) {
|
|
2050
|
+
return Promise.race([
|
|
2051
|
+
promise,
|
|
2052
|
+
new Promise(
|
|
2053
|
+
(_, reject) => setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
|
|
2054
|
+
)
|
|
2055
|
+
]);
|
|
2056
|
+
}
|
|
2057
|
+
async function withRetry(fn, attempts = 3) {
|
|
2058
|
+
let lastError;
|
|
2059
|
+
for (let i = 0; i < attempts; i++) {
|
|
2060
|
+
try {
|
|
2061
|
+
return await fn();
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
lastError = err;
|
|
2064
|
+
if (err instanceof GuardianError && (err.code === "AUTH" || err.code === "NOT_FOUND")) {
|
|
2065
|
+
throw err;
|
|
2066
|
+
}
|
|
2067
|
+
if (i < attempts - 1) {
|
|
2068
|
+
await new Promise((r) => setTimeout(r, 200 * Math.pow(2, i)));
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
throw lastError;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/vault/crypto/shamir.ts
|
|
2076
|
+
import { randomBytes } from "@noble/ciphers/webcrypto";
|
|
2077
|
+
var IRREDUCIBLE = 283;
|
|
2078
|
+
var EXP_TABLE = new Uint8Array(512);
|
|
2079
|
+
var LOG_TABLE = new Uint8Array(256);
|
|
2080
|
+
(function buildTables() {
|
|
2081
|
+
let x = 1;
|
|
2082
|
+
for (let i = 0; i < 255; i++) {
|
|
2083
|
+
EXP_TABLE[i] = x;
|
|
2084
|
+
LOG_TABLE[x] = i;
|
|
2085
|
+
x = x ^ x << 1;
|
|
2086
|
+
if (x >= 256) x ^= IRREDUCIBLE;
|
|
2087
|
+
}
|
|
2088
|
+
for (let i = 255; i < 512; i++) {
|
|
2089
|
+
EXP_TABLE[i] = EXP_TABLE[i - 255];
|
|
2090
|
+
}
|
|
2091
|
+
})();
|
|
2092
|
+
function gfAdd(a, b) {
|
|
2093
|
+
return a ^ b;
|
|
2094
|
+
}
|
|
2095
|
+
function gfMul(a, b) {
|
|
2096
|
+
if (a === 0 || b === 0) return 0;
|
|
2097
|
+
return EXP_TABLE[LOG_TABLE[a] + LOG_TABLE[b]];
|
|
2098
|
+
}
|
|
2099
|
+
function gfDiv(a, b) {
|
|
2100
|
+
if (b === 0) throw new Error("GF(2^8): division by zero");
|
|
2101
|
+
if (a === 0) return 0;
|
|
2102
|
+
return EXP_TABLE[(LOG_TABLE[a] - LOG_TABLE[b] + 255) % 255];
|
|
2103
|
+
}
|
|
2104
|
+
function split(secret, n, k) {
|
|
2105
|
+
if (k < 2) throw new Error("Threshold K must be at least 2");
|
|
2106
|
+
if (n < k) throw new Error("Share count N must be >= threshold K");
|
|
2107
|
+
if (n > 255) throw new Error("Maximum 255 shares (GF(2^8) limit)");
|
|
2108
|
+
if (secret.length === 0) throw new Error("Secret must not be empty");
|
|
2109
|
+
const coefficients = new Array(secret.length);
|
|
2110
|
+
for (let i = 0; i < secret.length; i++) {
|
|
2111
|
+
const poly = new Uint8Array(k);
|
|
2112
|
+
poly[0] = secret[i];
|
|
2113
|
+
const rand = randomBytes(k - 1);
|
|
2114
|
+
poly.set(rand, 1);
|
|
2115
|
+
coefficients[i] = poly;
|
|
2116
|
+
}
|
|
2117
|
+
const shares = [];
|
|
2118
|
+
for (let xi = 1; xi <= n; xi++) {
|
|
2119
|
+
const y = new Uint8Array(secret.length);
|
|
2120
|
+
for (let byteIdx = 0; byteIdx < secret.length; byteIdx++) {
|
|
2121
|
+
y[byteIdx] = evaluatePolynomial(coefficients[byteIdx], xi);
|
|
2122
|
+
}
|
|
2123
|
+
shares.push({ x: xi, y });
|
|
2124
|
+
}
|
|
2125
|
+
for (const poly of coefficients) {
|
|
2126
|
+
poly.fill(0);
|
|
2127
|
+
}
|
|
2128
|
+
return shares;
|
|
2129
|
+
}
|
|
2130
|
+
function evaluatePolynomial(coeffs, x) {
|
|
2131
|
+
let result = 0;
|
|
2132
|
+
for (let i = coeffs.length - 1; i >= 0; i--) {
|
|
2133
|
+
result = gfAdd(gfMul(result, x), coeffs[i]);
|
|
2134
|
+
}
|
|
2135
|
+
return result;
|
|
2136
|
+
}
|
|
2137
|
+
function combine(shares) {
|
|
2138
|
+
if (shares.length < 2) throw new Error("Need at least 2 shares");
|
|
2139
|
+
const secretLength = shares[0].y.length;
|
|
2140
|
+
for (const share of shares) {
|
|
2141
|
+
if (share.y.length !== secretLength) {
|
|
2142
|
+
throw new Error("All shares must have the same data length");
|
|
2143
|
+
}
|
|
2144
|
+
if (share.x === 0) {
|
|
2145
|
+
throw new Error("Share index must not be 0");
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const xValues = new Set(shares.map((s) => s.x));
|
|
2149
|
+
if (xValues.size !== shares.length) {
|
|
2150
|
+
throw new Error("Duplicate share indices");
|
|
2151
|
+
}
|
|
2152
|
+
const secret = new Uint8Array(secretLength);
|
|
2153
|
+
for (let byteIdx = 0; byteIdx < secretLength; byteIdx++) {
|
|
2154
|
+
let value = 0;
|
|
2155
|
+
for (let i = 0; i < shares.length; i++) {
|
|
2156
|
+
const xi = shares[i].x;
|
|
2157
|
+
const yi = shares[i].y[byteIdx];
|
|
2158
|
+
let basis = 1;
|
|
2159
|
+
for (let j = 0; j < shares.length; j++) {
|
|
2160
|
+
if (i === j) continue;
|
|
2161
|
+
const xj = shares[j].x;
|
|
2162
|
+
basis = gfMul(basis, gfDiv(xj, gfAdd(xi, xj)));
|
|
2163
|
+
}
|
|
2164
|
+
value = gfAdd(value, gfMul(yi, basis));
|
|
2165
|
+
}
|
|
2166
|
+
secret[byteIdx] = value;
|
|
2167
|
+
}
|
|
2168
|
+
return secret;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// src/vault/quorum.ts
|
|
2172
|
+
function adaptiveThreshold(n) {
|
|
2173
|
+
return Math.max(3, Math.floor(n / 3));
|
|
2174
|
+
}
|
|
2175
|
+
function writeQuorum(n) {
|
|
2176
|
+
if (n === 0) return 0;
|
|
2177
|
+
if (n <= 2) return n;
|
|
2178
|
+
return Math.ceil(2 * n / 3);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/vault/client.ts
|
|
2182
|
+
var PULL_TIMEOUT_MS = 1e4;
|
|
2183
|
+
var VaultClient = class {
|
|
2184
|
+
constructor(config) {
|
|
2185
|
+
this.config = config;
|
|
2186
|
+
this.auth = new AuthClient2(config.identityHex, config.timeoutMs);
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Store a secret across guardian nodes using Shamir splitting.
|
|
2190
|
+
*
|
|
2191
|
+
* @param name - Secret name (alphanumeric, _, -, max 128 chars)
|
|
2192
|
+
* @param data - Secret data to store
|
|
2193
|
+
* @param version - Monotonic version number (must be > previous)
|
|
2194
|
+
*/
|
|
2195
|
+
async store(name, data, version) {
|
|
2196
|
+
const guardians = this.config.guardians;
|
|
2197
|
+
const n = guardians.length;
|
|
2198
|
+
const k = adaptiveThreshold(n);
|
|
2199
|
+
const shares = split(data, n, k);
|
|
2200
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
2201
|
+
const results = await Promise.allSettled(
|
|
2202
|
+
authed.map(async ({ client, endpoint }, _i) => {
|
|
2203
|
+
const guardianIdx = guardians.indexOf(endpoint);
|
|
2204
|
+
const share = shares[guardianIdx];
|
|
2205
|
+
if (!share) throw new Error("share index out of bounds");
|
|
2206
|
+
const shareBytes = new Uint8Array(1 + share.y.length);
|
|
2207
|
+
shareBytes[0] = share.x;
|
|
2208
|
+
shareBytes.set(share.y, 1);
|
|
2209
|
+
return withRetry(() => client.putSecret(name, shareBytes, version));
|
|
2210
|
+
})
|
|
2211
|
+
);
|
|
2212
|
+
for (const share of shares) {
|
|
2213
|
+
share.y.fill(0);
|
|
2214
|
+
}
|
|
2215
|
+
const guardianResults = authed.map(({ endpoint }, i) => {
|
|
2216
|
+
const ep = `${endpoint.address}:${endpoint.port}`;
|
|
2217
|
+
const r = results[i];
|
|
2218
|
+
if (r.status === "fulfilled") {
|
|
2219
|
+
return { endpoint: ep, success: true };
|
|
2220
|
+
}
|
|
2221
|
+
return { endpoint: ep, success: false, error: r.reason.message };
|
|
2222
|
+
});
|
|
2223
|
+
const ackCount = results.filter((r) => r.status === "fulfilled").length;
|
|
2224
|
+
const failCount = results.filter((r) => r.status === "rejected").length;
|
|
2225
|
+
const w = writeQuorum(n);
|
|
2226
|
+
return {
|
|
2227
|
+
ackCount,
|
|
2228
|
+
totalContacted: authed.length,
|
|
2229
|
+
failCount,
|
|
2230
|
+
quorumMet: ackCount >= w,
|
|
2231
|
+
guardianResults
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Retrieve and reconstruct a secret from guardian nodes.
|
|
2236
|
+
*
|
|
2237
|
+
* @param name - Secret name
|
|
2238
|
+
*/
|
|
2239
|
+
async retrieve(name) {
|
|
2240
|
+
const guardians = this.config.guardians;
|
|
2241
|
+
const n = guardians.length;
|
|
2242
|
+
const k = adaptiveThreshold(n);
|
|
2243
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
2244
|
+
const pullResults = await Promise.allSettled(
|
|
2245
|
+
authed.map(async ({ client }) => {
|
|
2246
|
+
const resp = await withTimeout(client.getSecret(name), PULL_TIMEOUT_MS);
|
|
2247
|
+
const shareBytes = resp.share;
|
|
2248
|
+
if (shareBytes.length < 2) throw new Error("Share too short");
|
|
2249
|
+
return {
|
|
2250
|
+
x: shareBytes[0],
|
|
2251
|
+
y: shareBytes.slice(1)
|
|
2252
|
+
};
|
|
2253
|
+
})
|
|
2254
|
+
);
|
|
2255
|
+
const shares = [];
|
|
2256
|
+
for (const r of pullResults) {
|
|
2257
|
+
if (r.status === "fulfilled") {
|
|
2258
|
+
shares.push(r.value);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (shares.length < k) {
|
|
2262
|
+
throw new Error(
|
|
2263
|
+
`Not enough shares: collected ${shares.length} of ${k} required (contacted ${authed.length} guardians)`
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
const data = combine(shares);
|
|
2267
|
+
for (const share of shares) {
|
|
2268
|
+
share.y.fill(0);
|
|
2269
|
+
}
|
|
2270
|
+
return {
|
|
2271
|
+
data,
|
|
2272
|
+
sharesCollected: shares.length
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* List all secrets for this identity.
|
|
2277
|
+
* Queries the first reachable guardian (metadata is replicated).
|
|
2278
|
+
*/
|
|
2279
|
+
async list() {
|
|
2280
|
+
const guardians = this.config.guardians;
|
|
2281
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
2282
|
+
if (authed.length === 0) {
|
|
2283
|
+
throw new Error("No guardians reachable");
|
|
2284
|
+
}
|
|
2285
|
+
const resp = await authed[0].client.listSecrets();
|
|
2286
|
+
return { secrets: resp.secrets };
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Delete a secret from all guardian nodes.
|
|
2290
|
+
*
|
|
2291
|
+
* @param name - Secret name to delete
|
|
2292
|
+
*/
|
|
2293
|
+
async delete(name) {
|
|
2294
|
+
const guardians = this.config.guardians;
|
|
2295
|
+
const n = guardians.length;
|
|
2296
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
2297
|
+
const results = await Promise.allSettled(
|
|
2298
|
+
authed.map(async ({ client }) => {
|
|
2299
|
+
return withRetry(() => client.deleteSecret(name));
|
|
2300
|
+
})
|
|
2301
|
+
);
|
|
2302
|
+
const ackCount = results.filter((r) => r.status === "fulfilled").length;
|
|
2303
|
+
const w = writeQuorum(n);
|
|
2304
|
+
return {
|
|
2305
|
+
ackCount,
|
|
2306
|
+
totalContacted: authed.length,
|
|
2307
|
+
quorumMet: ackCount >= w
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
/** Clear all cached auth sessions. */
|
|
2311
|
+
clearSessions() {
|
|
2312
|
+
this.auth.clearSessions();
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
// src/vault/crypto/aes.ts
|
|
2317
|
+
import { gcm } from "@noble/ciphers/aes";
|
|
2318
|
+
import { randomBytes as randomBytes2 } from "@noble/ciphers/webcrypto";
|
|
2319
|
+
import { bytesToHex, hexToBytes, concatBytes } from "@noble/hashes/utils";
|
|
2320
|
+
var KEY_SIZE = 32;
|
|
2321
|
+
var NONCE_SIZE = 12;
|
|
2322
|
+
var TAG_SIZE = 16;
|
|
2323
|
+
function encrypt(plaintext, key, aad) {
|
|
2324
|
+
validateKey(key);
|
|
2325
|
+
const nonce = randomBytes2(NONCE_SIZE);
|
|
2326
|
+
const cipher = gcm(key, nonce, aad);
|
|
2327
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
2328
|
+
return {
|
|
2329
|
+
ciphertext,
|
|
2330
|
+
nonce,
|
|
2331
|
+
aad
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function decrypt(encryptedData, key) {
|
|
2335
|
+
validateKey(key);
|
|
2336
|
+
validateNonce(encryptedData.nonce);
|
|
2337
|
+
const cipher = gcm(key, encryptedData.nonce, encryptedData.aad);
|
|
2338
|
+
try {
|
|
2339
|
+
return cipher.decrypt(encryptedData.ciphertext);
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
throw new Error("Decryption failed: invalid ciphertext or authentication tag");
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
function encryptString(message, key, aad) {
|
|
2345
|
+
const plaintext = new TextEncoder().encode(message);
|
|
2346
|
+
try {
|
|
2347
|
+
return encrypt(plaintext, key, aad);
|
|
2348
|
+
} finally {
|
|
2349
|
+
plaintext.fill(0);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function decryptString(encryptedData, key) {
|
|
2353
|
+
const plaintext = decrypt(encryptedData, key);
|
|
2354
|
+
try {
|
|
2355
|
+
return new TextDecoder().decode(plaintext);
|
|
2356
|
+
} finally {
|
|
2357
|
+
plaintext.fill(0);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function serialize(encryptedData) {
|
|
2361
|
+
const data = concatBytes(encryptedData.nonce, encryptedData.ciphertext);
|
|
2362
|
+
return {
|
|
2363
|
+
data,
|
|
2364
|
+
aad: encryptedData.aad
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
function deserialize(serialized) {
|
|
2368
|
+
if (serialized.data.length < NONCE_SIZE + TAG_SIZE) {
|
|
2369
|
+
throw new Error("Invalid serialized data: too short");
|
|
2370
|
+
}
|
|
2371
|
+
const nonce = serialized.data.slice(0, NONCE_SIZE);
|
|
2372
|
+
const ciphertext = serialized.data.slice(NONCE_SIZE);
|
|
2373
|
+
return {
|
|
2374
|
+
ciphertext,
|
|
2375
|
+
nonce,
|
|
2376
|
+
aad: serialized.aad
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
function encryptAndSerialize(plaintext, key, aad) {
|
|
2380
|
+
const encrypted = encrypt(plaintext, key, aad);
|
|
2381
|
+
return serialize(encrypted);
|
|
2382
|
+
}
|
|
2383
|
+
function deserializeAndDecrypt(serialized, key) {
|
|
2384
|
+
const encrypted = deserialize(serialized);
|
|
2385
|
+
return decrypt(encrypted, key);
|
|
2386
|
+
}
|
|
2387
|
+
function toHex(encryptedData) {
|
|
2388
|
+
const serialized = serialize(encryptedData);
|
|
2389
|
+
return bytesToHex(serialized.data);
|
|
2390
|
+
}
|
|
2391
|
+
function fromHex(hex, aad) {
|
|
2392
|
+
const normalized = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
2393
|
+
const data = hexToBytes(normalized);
|
|
2394
|
+
return deserialize({ data, aad });
|
|
2395
|
+
}
|
|
2396
|
+
function toBase64(encryptedData) {
|
|
2397
|
+
const serialized = serialize(encryptedData);
|
|
2398
|
+
if (typeof btoa === "function") {
|
|
2399
|
+
return btoa(String.fromCharCode(...serialized.data));
|
|
2400
|
+
} else {
|
|
2401
|
+
return Buffer.from(serialized.data).toString("base64");
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
function fromBase64(base64, aad) {
|
|
2405
|
+
let data;
|
|
2406
|
+
if (typeof atob === "function") {
|
|
2407
|
+
const binary = atob(base64);
|
|
2408
|
+
data = new Uint8Array(binary.length);
|
|
2409
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2410
|
+
data[i] = binary.charCodeAt(i);
|
|
2411
|
+
}
|
|
2412
|
+
} else {
|
|
2413
|
+
data = new Uint8Array(Buffer.from(base64, "base64"));
|
|
2414
|
+
}
|
|
2415
|
+
return deserialize({ data, aad });
|
|
2416
|
+
}
|
|
2417
|
+
function validateKey(key) {
|
|
2418
|
+
if (!(key instanceof Uint8Array)) {
|
|
2419
|
+
throw new Error("Key must be a Uint8Array");
|
|
2420
|
+
}
|
|
2421
|
+
if (key.length !== KEY_SIZE) {
|
|
2422
|
+
throw new Error(`Invalid key length: expected ${KEY_SIZE}, got ${key.length}`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
function validateNonce(nonce) {
|
|
2426
|
+
if (!(nonce instanceof Uint8Array)) {
|
|
2427
|
+
throw new Error("Nonce must be a Uint8Array");
|
|
2428
|
+
}
|
|
2429
|
+
if (nonce.length !== NONCE_SIZE) {
|
|
2430
|
+
throw new Error(`Invalid nonce length: expected ${NONCE_SIZE}, got ${nonce.length}`);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
function generateKey() {
|
|
2434
|
+
return randomBytes2(KEY_SIZE);
|
|
2435
|
+
}
|
|
2436
|
+
function generateNonce() {
|
|
2437
|
+
return randomBytes2(NONCE_SIZE);
|
|
2438
|
+
}
|
|
2439
|
+
function clearKey(key) {
|
|
2440
|
+
key.fill(0);
|
|
2441
|
+
}
|
|
2442
|
+
function isValidEncryptedData(data) {
|
|
2443
|
+
return data.nonce instanceof Uint8Array && data.nonce.length === NONCE_SIZE && data.ciphertext instanceof Uint8Array && data.ciphertext.length >= TAG_SIZE;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// src/vault/crypto/hkdf.ts
|
|
2447
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
2448
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
2449
|
+
var DEFAULT_KEY_LENGTH = 32;
|
|
2450
|
+
var MAX_KEY_LENGTH = 255 * 32;
|
|
2451
|
+
function deriveKeyHKDF(ikm, salt, info, length = DEFAULT_KEY_LENGTH) {
|
|
2452
|
+
if (!ikm || ikm.length === 0) {
|
|
2453
|
+
throw new Error("HKDF: input key material must not be empty");
|
|
2454
|
+
}
|
|
2455
|
+
if (length <= 0 || length > MAX_KEY_LENGTH) {
|
|
2456
|
+
throw new Error(`HKDF: output length must be between 1 and ${MAX_KEY_LENGTH}`);
|
|
2457
|
+
}
|
|
2458
|
+
const saltBytes = typeof salt === "string" ? new TextEncoder().encode(salt) : salt;
|
|
2459
|
+
const infoBytes = typeof info === "string" ? new TextEncoder().encode(info) : info;
|
|
2460
|
+
return hkdf(sha256, ikm, saltBytes, infoBytes, length);
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/index.ts
|
|
2464
|
+
function createClient(config) {
|
|
2465
|
+
const httpClient = new HttpClient({
|
|
2466
|
+
baseURL: config.baseURL,
|
|
2467
|
+
timeout: config.timeout,
|
|
2468
|
+
maxRetries: config.maxRetries,
|
|
2469
|
+
retryDelayMs: config.retryDelayMs,
|
|
2470
|
+
debug: config.debug,
|
|
2471
|
+
fetch: config.fetch,
|
|
2472
|
+
onNetworkError: config.onNetworkError
|
|
2473
|
+
});
|
|
2474
|
+
const auth = new AuthClient({
|
|
2475
|
+
httpClient,
|
|
2476
|
+
storage: config.storage,
|
|
2477
|
+
apiKey: config.apiKey,
|
|
2478
|
+
jwt: config.jwt
|
|
2479
|
+
});
|
|
2480
|
+
const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
|
2481
|
+
const db = new DBClient(httpClient);
|
|
2482
|
+
const pubsub = new PubSubClient(httpClient, {
|
|
2483
|
+
...config.wsConfig,
|
|
2484
|
+
wsURL,
|
|
2485
|
+
onNetworkError: config.onNetworkError
|
|
2486
|
+
});
|
|
2487
|
+
const network = new NetworkClient(httpClient);
|
|
2488
|
+
const cache = new CacheClient(httpClient);
|
|
2489
|
+
const storage = new StorageClient(httpClient);
|
|
2490
|
+
const functions = new FunctionsClient(httpClient, config.functionsConfig);
|
|
2491
|
+
const vault = config.vaultConfig ? new VaultClient(config.vaultConfig) : null;
|
|
2492
|
+
return {
|
|
2493
|
+
auth,
|
|
2494
|
+
db,
|
|
2495
|
+
pubsub,
|
|
2496
|
+
network,
|
|
2497
|
+
cache,
|
|
2498
|
+
storage,
|
|
2499
|
+
functions,
|
|
2500
|
+
vault
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
export {
|
|
2504
|
+
AuthClient,
|
|
2505
|
+
CacheClient,
|
|
2506
|
+
DBClient,
|
|
2507
|
+
FunctionsClient,
|
|
2508
|
+
GuardianClient,
|
|
2509
|
+
GuardianError,
|
|
2510
|
+
HttpClient,
|
|
2511
|
+
KEY_SIZE,
|
|
2512
|
+
LocalStorageAdapter,
|
|
2513
|
+
MemoryStorage,
|
|
2514
|
+
NONCE_SIZE,
|
|
2515
|
+
NetworkClient,
|
|
2516
|
+
PubSubClient,
|
|
2517
|
+
QueryBuilder,
|
|
2518
|
+
Repository,
|
|
2519
|
+
SDKError,
|
|
2520
|
+
StorageClient,
|
|
2521
|
+
Subscription,
|
|
2522
|
+
TAG_SIZE,
|
|
2523
|
+
AuthClient2 as VaultAuthClient,
|
|
2524
|
+
VaultClient,
|
|
2525
|
+
WSClient,
|
|
2526
|
+
adaptiveThreshold,
|
|
2527
|
+
clearKey,
|
|
2528
|
+
createClient,
|
|
2529
|
+
decrypt,
|
|
2530
|
+
decryptString,
|
|
2531
|
+
deriveKeyHKDF,
|
|
2532
|
+
deserializeAndDecrypt,
|
|
2533
|
+
deserialize as deserializeEncrypted,
|
|
2534
|
+
encrypt,
|
|
2535
|
+
encryptAndSerialize,
|
|
2536
|
+
encryptString,
|
|
2537
|
+
fromBase64 as encryptedFromBase64,
|
|
2538
|
+
fromHex as encryptedFromHex,
|
|
2539
|
+
toBase64 as encryptedToBase64,
|
|
2540
|
+
toHex as encryptedToHex,
|
|
2541
|
+
fanOut,
|
|
2542
|
+
fanOutIndexed,
|
|
2543
|
+
generateKey,
|
|
2544
|
+
generateNonce,
|
|
2545
|
+
isValidEncryptedData,
|
|
2546
|
+
serialize as serializeEncrypted,
|
|
2547
|
+
combine as shamirCombine,
|
|
2548
|
+
split as shamirSplit,
|
|
2549
|
+
withRetry,
|
|
2550
|
+
withTimeout,
|
|
2551
|
+
writeQuorum
|
|
2552
|
+
};
|
|
2553
|
+
//# sourceMappingURL=index.js.map
|