@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface PubSubMessage {
|
|
2
|
+
data: string;
|
|
3
|
+
topic: string;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RawEnvelope {
|
|
8
|
+
type?: string;
|
|
9
|
+
data: string; // base64-encoded
|
|
10
|
+
timestamp: number;
|
|
11
|
+
topic: string;
|
|
12
|
+
member_id?: string;
|
|
13
|
+
meta?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PresenceMember {
|
|
17
|
+
memberId: string;
|
|
18
|
+
joinedAt: number;
|
|
19
|
+
meta?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PresenceResponse {
|
|
23
|
+
topic: string;
|
|
24
|
+
members: PresenceMember[];
|
|
25
|
+
count: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PresenceOptions {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
memberId: string;
|
|
31
|
+
meta?: Record<string, unknown>;
|
|
32
|
+
onJoin?: (member: PresenceMember) => void;
|
|
33
|
+
onLeave?: (member: PresenceMember) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SubscribeOptions {
|
|
37
|
+
onMessage?: MessageHandler;
|
|
38
|
+
onError?: ErrorHandler;
|
|
39
|
+
onClose?: CloseHandler;
|
|
40
|
+
presence?: PresenceOptions;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type MessageHandler = (message: PubSubMessage) => void;
|
|
44
|
+
export type ErrorHandler = (error: Error) => void;
|
|
45
|
+
export type CloseHandler = (code: number, reason: string) => void;
|
|
46
|
+
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { HttpClient } from "../core/http";
|
|
2
|
+
|
|
3
|
+
export interface StorageUploadResponse {
|
|
4
|
+
cid: string;
|
|
5
|
+
name: string;
|
|
6
|
+
size: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StoragePinRequest {
|
|
10
|
+
cid: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StoragePinResponse {
|
|
15
|
+
cid: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StorageStatus {
|
|
20
|
+
cid: string;
|
|
21
|
+
name: string;
|
|
22
|
+
status: string; // "pinned", "pinning", "queued", "unpinned", "error"
|
|
23
|
+
replication_min: number;
|
|
24
|
+
replication_max: number;
|
|
25
|
+
replication_factor: number;
|
|
26
|
+
peers: string[];
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class StorageClient {
|
|
31
|
+
private httpClient: HttpClient;
|
|
32
|
+
|
|
33
|
+
constructor(httpClient: HttpClient) {
|
|
34
|
+
this.httpClient = httpClient;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Upload content to IPFS and optionally pin it.
|
|
39
|
+
* Supports both File objects (browser) and Buffer/ReadableStream (Node.js).
|
|
40
|
+
*
|
|
41
|
+
* @param file - File to upload (File, Blob, or Buffer)
|
|
42
|
+
* @param name - Optional filename
|
|
43
|
+
* @param options - Optional upload options
|
|
44
|
+
* @param options.pin - Whether to pin the content (default: true). Pinning happens asynchronously on the backend.
|
|
45
|
+
* @returns Upload result with CID
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* // Browser
|
|
50
|
+
* const fileInput = document.querySelector('input[type="file"]');
|
|
51
|
+
* const file = fileInput.files[0];
|
|
52
|
+
* const result = await client.storage.upload(file, file.name);
|
|
53
|
+
* console.log(result.cid);
|
|
54
|
+
*
|
|
55
|
+
* // Node.js
|
|
56
|
+
* const fs = require('fs');
|
|
57
|
+
* const fileBuffer = fs.readFileSync('image.jpg');
|
|
58
|
+
* const result = await client.storage.upload(fileBuffer, 'image.jpg', { pin: true });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async upload(
|
|
62
|
+
file: File | Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>,
|
|
63
|
+
name?: string,
|
|
64
|
+
options?: {
|
|
65
|
+
pin?: boolean;
|
|
66
|
+
}
|
|
67
|
+
): Promise<StorageUploadResponse> {
|
|
68
|
+
// Create FormData for multipart upload
|
|
69
|
+
const formData = new FormData();
|
|
70
|
+
|
|
71
|
+
// Handle different input types
|
|
72
|
+
if (file instanceof File) {
|
|
73
|
+
formData.append("file", file);
|
|
74
|
+
} else if (file instanceof Blob) {
|
|
75
|
+
formData.append("file", file, name);
|
|
76
|
+
} else if (file instanceof ArrayBuffer) {
|
|
77
|
+
const blob = new Blob([file]);
|
|
78
|
+
formData.append("file", blob, name);
|
|
79
|
+
} else if (file instanceof Uint8Array) {
|
|
80
|
+
// Convert Uint8Array to ArrayBuffer for Blob constructor
|
|
81
|
+
const buffer = file.buffer.slice(
|
|
82
|
+
file.byteOffset,
|
|
83
|
+
file.byteOffset + file.byteLength
|
|
84
|
+
) as ArrayBuffer;
|
|
85
|
+
const blob = new Blob([buffer], { type: "application/octet-stream" });
|
|
86
|
+
formData.append("file", blob, name);
|
|
87
|
+
} else if (file instanceof ReadableStream) {
|
|
88
|
+
// For ReadableStream, we need to read it into a blob first
|
|
89
|
+
// This is a limitation - in practice, pass File/Blob/Buffer
|
|
90
|
+
const chunks: ArrayBuffer[] = [];
|
|
91
|
+
const reader = file.getReader();
|
|
92
|
+
while (true) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done) break;
|
|
95
|
+
const buffer = value.buffer.slice(
|
|
96
|
+
value.byteOffset,
|
|
97
|
+
value.byteOffset + value.byteLength
|
|
98
|
+
) as ArrayBuffer;
|
|
99
|
+
chunks.push(buffer);
|
|
100
|
+
}
|
|
101
|
+
const blob = new Blob(chunks);
|
|
102
|
+
formData.append("file", blob, name);
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Unsupported file type. Use File, Blob, ArrayBuffer, Uint8Array, or ReadableStream."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add pin flag (default: true)
|
|
110
|
+
const shouldPin = options?.pin !== false; // Default to true
|
|
111
|
+
formData.append("pin", shouldPin ? "true" : "false");
|
|
112
|
+
|
|
113
|
+
return this.httpClient.uploadFile<StorageUploadResponse>(
|
|
114
|
+
"/v1/storage/upload",
|
|
115
|
+
formData,
|
|
116
|
+
{ timeout: 300000 } // 5 minute timeout for large files
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pin an existing CID
|
|
122
|
+
*
|
|
123
|
+
* @param cid - Content ID to pin
|
|
124
|
+
* @param name - Optional name for the pin
|
|
125
|
+
* @returns Pin result
|
|
126
|
+
*/
|
|
127
|
+
async pin(cid: string, name?: string): Promise<StoragePinResponse> {
|
|
128
|
+
return this.httpClient.post<StoragePinResponse>("/v1/storage/pin", {
|
|
129
|
+
cid,
|
|
130
|
+
name,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the pin status for a CID
|
|
136
|
+
*
|
|
137
|
+
* @param cid - Content ID to check
|
|
138
|
+
* @returns Pin status information
|
|
139
|
+
*/
|
|
140
|
+
async status(cid: string): Promise<StorageStatus> {
|
|
141
|
+
return this.httpClient.get<StorageStatus>(`/v1/storage/status/${cid}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Retrieve content from IPFS by CID
|
|
146
|
+
*
|
|
147
|
+
* @param cid - Content ID to retrieve
|
|
148
|
+
* @returns ReadableStream of the content
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* const stream = await client.storage.get(cid);
|
|
153
|
+
* const reader = stream.getReader();
|
|
154
|
+
* while (true) {
|
|
155
|
+
* const { done, value } = await reader.read();
|
|
156
|
+
* if (done) break;
|
|
157
|
+
* // Process chunk
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
async get(cid: string): Promise<ReadableStream<Uint8Array>> {
|
|
162
|
+
// Retry logic for content retrieval - content may not be immediately available
|
|
163
|
+
// after upload due to eventual consistency in IPFS Cluster
|
|
164
|
+
// IPFS Cluster pins can take 2-3+ seconds to complete across all nodes
|
|
165
|
+
const maxAttempts = 8;
|
|
166
|
+
let lastError: Error | null = null;
|
|
167
|
+
|
|
168
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
169
|
+
try {
|
|
170
|
+
const response = await this.httpClient.getBinary(
|
|
171
|
+
`/v1/storage/get/${cid}`
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (!response.body) {
|
|
175
|
+
throw new Error("Response body is null");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return response.body;
|
|
179
|
+
} catch (error: any) {
|
|
180
|
+
lastError = error;
|
|
181
|
+
|
|
182
|
+
// Check if this is a 404 error (content not found)
|
|
183
|
+
const isNotFound =
|
|
184
|
+
error?.httpStatus === 404 ||
|
|
185
|
+
error?.message?.includes("not found") ||
|
|
186
|
+
error?.message?.includes("404");
|
|
187
|
+
|
|
188
|
+
// If it's not a 404 error, or this is the last attempt, give up
|
|
189
|
+
if (!isNotFound || attempt === maxAttempts) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Wait before retrying with bounded exponential backoff
|
|
194
|
+
// Max 3 seconds per retry to fit within 30s test timeout
|
|
195
|
+
// Total: 1s + 2s + 3s + 3s + 3s + 3s + 3s + 3s = 21 seconds
|
|
196
|
+
const backoffMs = Math.min(attempt * 1000, 3000);
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// This should never be reached, but TypeScript needs it
|
|
202
|
+
throw lastError || new Error("Failed to retrieve content");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Retrieve content from IPFS by CID and return the full Response object
|
|
207
|
+
* Useful when you need access to response headers (e.g., content-length)
|
|
208
|
+
*
|
|
209
|
+
* @param cid - Content ID to retrieve
|
|
210
|
+
* @returns Response object with body stream and headers
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* const response = await client.storage.getBinary(cid);
|
|
215
|
+
* const contentLength = response.headers.get('content-length');
|
|
216
|
+
* const reader = response.body.getReader();
|
|
217
|
+
* // ... read stream
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
async getBinary(cid: string): Promise<Response> {
|
|
221
|
+
// Retry logic for content retrieval - content may not be immediately available
|
|
222
|
+
// after upload due to eventual consistency in IPFS Cluster
|
|
223
|
+
// IPFS Cluster pins can take 2-3+ seconds to complete across all nodes
|
|
224
|
+
const maxAttempts = 8;
|
|
225
|
+
let lastError: Error | null = null;
|
|
226
|
+
|
|
227
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
228
|
+
try {
|
|
229
|
+
const response = await this.httpClient.getBinary(
|
|
230
|
+
`/v1/storage/get/${cid}`
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (!response) {
|
|
234
|
+
throw new Error("Response is null");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return response;
|
|
238
|
+
} catch (error: any) {
|
|
239
|
+
lastError = error;
|
|
240
|
+
|
|
241
|
+
// Check if this is a 404 error (content not found)
|
|
242
|
+
const isNotFound =
|
|
243
|
+
error?.httpStatus === 404 ||
|
|
244
|
+
error?.message?.includes("not found") ||
|
|
245
|
+
error?.message?.includes("404");
|
|
246
|
+
|
|
247
|
+
// If it's not a 404 error, or this is the last attempt, give up
|
|
248
|
+
if (!isNotFound || attempt === maxAttempts) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Wait before retrying with bounded exponential backoff
|
|
253
|
+
// Max 3 seconds per retry to fit within 30s test timeout
|
|
254
|
+
// Total: 1s + 2s + 3s + 3s + 3s + 3s + 3s + 3s = 21 seconds
|
|
255
|
+
const backoffMs = Math.min(attempt * 1000, 3000);
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// This should never be reached, but TypeScript needs it
|
|
261
|
+
throw lastError || new Error("Failed to retrieve content");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Unpin a CID
|
|
266
|
+
*
|
|
267
|
+
* @param cid - Content ID to unpin
|
|
268
|
+
*/
|
|
269
|
+
async unpin(cid: string): Promise<void> {
|
|
270
|
+
await this.httpClient.delete(`/v1/storage/unpin/${cid}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 Codec for cross-platform encoding/decoding
|
|
3
|
+
* Works in both Node.js and browser environments
|
|
4
|
+
*/
|
|
5
|
+
export class Base64Codec {
|
|
6
|
+
/**
|
|
7
|
+
* Encode string or Uint8Array to base64
|
|
8
|
+
*/
|
|
9
|
+
static encode(input: string | Uint8Array): string {
|
|
10
|
+
if (typeof input === "string") {
|
|
11
|
+
return this.encodeString(input);
|
|
12
|
+
}
|
|
13
|
+
return this.encodeBytes(input);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encode string to base64
|
|
18
|
+
*/
|
|
19
|
+
static encodeString(str: string): string {
|
|
20
|
+
if (this.isNode()) {
|
|
21
|
+
return Buffer.from(str).toString("base64");
|
|
22
|
+
}
|
|
23
|
+
// Browser
|
|
24
|
+
return btoa(
|
|
25
|
+
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) =>
|
|
26
|
+
String.fromCharCode(parseInt(p1, 16))
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Encode Uint8Array to base64
|
|
33
|
+
*/
|
|
34
|
+
static encodeBytes(bytes: Uint8Array): string {
|
|
35
|
+
if (this.isNode()) {
|
|
36
|
+
return Buffer.from(bytes).toString("base64");
|
|
37
|
+
}
|
|
38
|
+
// Browser
|
|
39
|
+
let binary = "";
|
|
40
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
41
|
+
binary += String.fromCharCode(bytes[i]);
|
|
42
|
+
}
|
|
43
|
+
return btoa(binary);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decode base64 to string
|
|
48
|
+
*/
|
|
49
|
+
static decode(b64: string): string {
|
|
50
|
+
if (this.isNode()) {
|
|
51
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
52
|
+
}
|
|
53
|
+
// Browser
|
|
54
|
+
const binary = atob(b64);
|
|
55
|
+
const bytes = new Uint8Array(binary.length);
|
|
56
|
+
for (let i = 0; i < binary.length; i++) {
|
|
57
|
+
bytes[i] = binary.charCodeAt(i);
|
|
58
|
+
}
|
|
59
|
+
return new TextDecoder().decode(bytes);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if running in Node.js environment
|
|
64
|
+
*/
|
|
65
|
+
private static isNode(): boolean {
|
|
66
|
+
return typeof Buffer !== "undefined";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utilities
|
|
3
|
+
* Helps determine runtime environment (Node.js vs Browser)
|
|
4
|
+
*/
|
|
5
|
+
export const Platform = {
|
|
6
|
+
/**
|
|
7
|
+
* Check if running in Node.js
|
|
8
|
+
*/
|
|
9
|
+
isNode: (): boolean => {
|
|
10
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if running in browser
|
|
15
|
+
*/
|
|
16
|
+
isBrowser: (): boolean => {
|
|
17
|
+
return typeof window !== "undefined";
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if localStorage is available
|
|
22
|
+
*/
|
|
23
|
+
hasLocalStorage: (): boolean => {
|
|
24
|
+
try {
|
|
25
|
+
return typeof localStorage !== "undefined" && localStorage !== null;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if Buffer is available (Node.js)
|
|
33
|
+
*/
|
|
34
|
+
hasBuffer: (): boolean => {
|
|
35
|
+
return typeof Buffer !== "undefined";
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if btoa/atob are available (Browser)
|
|
40
|
+
*/
|
|
41
|
+
hasBase64: (): boolean => {
|
|
42
|
+
return typeof btoa !== "undefined" && typeof atob !== "undefined";
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface RetryConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Maximum number of retry attempts
|
|
7
|
+
*/
|
|
8
|
+
maxAttempts: number;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Function to calculate backoff delay in milliseconds
|
|
12
|
+
*/
|
|
13
|
+
backoffMs: (attempt: number) => number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Function to determine if error should trigger retry
|
|
17
|
+
*/
|
|
18
|
+
shouldRetry: (error: any) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Retry an operation with exponential backoff
|
|
23
|
+
* @param operation - The async operation to retry
|
|
24
|
+
* @param config - Retry configuration
|
|
25
|
+
* @returns Promise resolving to operation result
|
|
26
|
+
* @throws Last error if all retries exhausted
|
|
27
|
+
*/
|
|
28
|
+
export async function retryWithBackoff<T>(
|
|
29
|
+
operation: () => Promise<T>,
|
|
30
|
+
config: RetryConfig
|
|
31
|
+
): Promise<T> {
|
|
32
|
+
let lastError: Error | null = null;
|
|
33
|
+
|
|
34
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
35
|
+
try {
|
|
36
|
+
return await operation();
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
39
|
+
|
|
40
|
+
// Check if we should retry this error
|
|
41
|
+
if (!config.shouldRetry(error)) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If this was the last attempt, throw
|
|
46
|
+
if (attempt === config.maxAttempts) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wait before next attempt
|
|
51
|
+
const delay = config.backoffMs(attempt);
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback (should never reach here)
|
|
57
|
+
throw lastError || new Error("Retry failed");
|
|
58
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { GuardianClient } from './transport/guardian';
|
|
2
|
+
import type { GuardianEndpoint } from './transport/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles challenge-response authentication with guardian nodes.
|
|
6
|
+
* Caches session tokens per guardian endpoint.
|
|
7
|
+
*
|
|
8
|
+
* Auth flow:
|
|
9
|
+
* 1. POST /v2/vault/auth/challenge with identity → get {nonce, created_ns, tag}
|
|
10
|
+
* 2. POST /v2/vault/auth/session with identity + challenge fields → get session token
|
|
11
|
+
* 3. Use session token as X-Session-Token header for V2 requests
|
|
12
|
+
*
|
|
13
|
+
* The session token format is: `<identity_hex>:<expiry_ns>:<tag_hex>`
|
|
14
|
+
*/
|
|
15
|
+
export class AuthClient {
|
|
16
|
+
private sessions = new Map<string, { token: string; expiryNs: number }>();
|
|
17
|
+
private identityHex: string;
|
|
18
|
+
private timeoutMs: number;
|
|
19
|
+
|
|
20
|
+
constructor(identityHex: string, timeoutMs = 10_000) {
|
|
21
|
+
this.identityHex = identityHex;
|
|
22
|
+
this.timeoutMs = timeoutMs;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Authenticate with a guardian and cache the session token.
|
|
27
|
+
* Returns a GuardianClient with the session token set.
|
|
28
|
+
*/
|
|
29
|
+
async authenticate(endpoint: GuardianEndpoint): Promise<GuardianClient> {
|
|
30
|
+
const key = `${endpoint.address}:${endpoint.port}`;
|
|
31
|
+
const cached = this.sessions.get(key);
|
|
32
|
+
|
|
33
|
+
// Check if we have a valid cached session (with 30s safety margin)
|
|
34
|
+
if (cached) {
|
|
35
|
+
const nowNs = Date.now() * 1_000_000;
|
|
36
|
+
if (cached.expiryNs > nowNs + 30_000_000_000) {
|
|
37
|
+
const client = new GuardianClient(endpoint, this.timeoutMs);
|
|
38
|
+
client.setSessionToken(cached.token);
|
|
39
|
+
return client;
|
|
40
|
+
}
|
|
41
|
+
// Expired, remove
|
|
42
|
+
this.sessions.delete(key);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const client = new GuardianClient(endpoint, this.timeoutMs);
|
|
46
|
+
|
|
47
|
+
// Step 1: Request challenge
|
|
48
|
+
const challenge = await client.requestChallenge(this.identityHex);
|
|
49
|
+
|
|
50
|
+
// Step 2: Exchange for session
|
|
51
|
+
const session = await client.createSession(
|
|
52
|
+
this.identityHex,
|
|
53
|
+
challenge.nonce,
|
|
54
|
+
challenge.created_ns,
|
|
55
|
+
challenge.tag,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Build token string: identity:expiry_ns:tag
|
|
59
|
+
const token = `${session.identity}:${session.expiry_ns}:${session.tag}`;
|
|
60
|
+
client.setSessionToken(token);
|
|
61
|
+
|
|
62
|
+
// Cache
|
|
63
|
+
this.sessions.set(key, { token, expiryNs: session.expiry_ns });
|
|
64
|
+
|
|
65
|
+
return client;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Authenticate with multiple guardians in parallel.
|
|
70
|
+
* Returns authenticated GuardianClients for all that succeed.
|
|
71
|
+
*/
|
|
72
|
+
async authenticateAll(endpoints: GuardianEndpoint[]): Promise<{ client: GuardianClient; endpoint: GuardianEndpoint }[]> {
|
|
73
|
+
const results = await Promise.allSettled(
|
|
74
|
+
endpoints.map(async (ep) => {
|
|
75
|
+
const client = await this.authenticate(ep);
|
|
76
|
+
return { client, endpoint: ep };
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const authenticated: { client: GuardianClient; endpoint: GuardianEndpoint }[] = [];
|
|
81
|
+
for (const r of results) {
|
|
82
|
+
if (r.status === 'fulfilled') {
|
|
83
|
+
authenticated.push(r.value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return authenticated;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Clear all cached sessions. */
|
|
90
|
+
clearSessions(): void {
|
|
91
|
+
this.sessions.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Get the identity hex string. */
|
|
95
|
+
getIdentityHex(): string {
|
|
96
|
+
return this.identityHex;
|
|
97
|
+
}
|
|
98
|
+
}
|