@debros/network-ts-sdk 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ import type {
2
+ GuardianEndpoint,
3
+ GuardianErrorCode,
4
+ GuardianErrorBody,
5
+ HealthResponse,
6
+ StatusResponse,
7
+ GuardianInfo,
8
+ PushResponse,
9
+ PullResponse,
10
+ StoreSecretResponse,
11
+ GetSecretResponse,
12
+ DeleteSecretResponse,
13
+ ListSecretsResponse,
14
+ ChallengeResponse,
15
+ SessionResponse,
16
+ } from './types';
17
+
18
+ export class GuardianError extends Error {
19
+ constructor(public readonly code: GuardianErrorCode, message: string) {
20
+ super(message);
21
+ this.name = 'GuardianError';
22
+ }
23
+ }
24
+
25
+ const DEFAULT_TIMEOUT_MS = 10_000;
26
+
27
+ /**
28
+ * HTTP client for a single orama-vault guardian node.
29
+ * Supports V1 (push/pull) and V2 (CRUD secrets) endpoints.
30
+ */
31
+ export class GuardianClient {
32
+ private baseUrl: string;
33
+ private timeoutMs: number;
34
+ private sessionToken: string | null = null;
35
+
36
+ constructor(endpoint: GuardianEndpoint, timeoutMs = DEFAULT_TIMEOUT_MS) {
37
+ this.baseUrl = `http://${endpoint.address}:${endpoint.port}`;
38
+ this.timeoutMs = timeoutMs;
39
+ }
40
+
41
+ /** Set a session token for authenticated V2 requests. */
42
+ setSessionToken(token: string): void {
43
+ this.sessionToken = token;
44
+ }
45
+
46
+ /** Get the current session token. */
47
+ getSessionToken(): string | null {
48
+ return this.sessionToken;
49
+ }
50
+
51
+ /** Clear the session token. */
52
+ clearSessionToken(): void {
53
+ this.sessionToken = null;
54
+ }
55
+
56
+ // ── V1 endpoints ────────────────────────────────────────────────────
57
+
58
+ /** GET /v1/vault/health */
59
+ async health(): Promise<HealthResponse> {
60
+ return this.get<HealthResponse>('/v1/vault/health');
61
+ }
62
+
63
+ /** GET /v1/vault/status */
64
+ async status(): Promise<StatusResponse> {
65
+ return this.get<StatusResponse>('/v1/vault/status');
66
+ }
67
+
68
+ /** GET /v1/vault/guardians */
69
+ async guardians(): Promise<GuardianInfo> {
70
+ return this.get<GuardianInfo>('/v1/vault/guardians');
71
+ }
72
+
73
+ /** POST /v1/vault/push — store a share (V1). */
74
+ async push(identity: string, share: Uint8Array): Promise<PushResponse> {
75
+ return this.post<PushResponse>('/v1/vault/push', {
76
+ identity,
77
+ share: uint8ToBase64(share),
78
+ });
79
+ }
80
+
81
+ /** POST /v1/vault/pull — retrieve a share (V1). */
82
+ async pull(identity: string): Promise<Uint8Array> {
83
+ const resp = await this.post<PullResponse>('/v1/vault/pull', { identity });
84
+ return base64ToUint8(resp.share);
85
+ }
86
+
87
+ /** Check if this guardian is reachable. */
88
+ async isReachable(): Promise<boolean> {
89
+ try {
90
+ await this.health();
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ // ── V2 auth endpoints ───────────────────────────────────────────────
98
+
99
+ /** POST /v2/vault/auth/challenge — request an auth challenge. */
100
+ async requestChallenge(identity: string): Promise<ChallengeResponse> {
101
+ return this.post<ChallengeResponse>('/v2/vault/auth/challenge', { identity });
102
+ }
103
+
104
+ /** POST /v2/vault/auth/session — exchange challenge for session token. */
105
+ async createSession(identity: string, nonce: string, created_ns: number, tag: string): Promise<SessionResponse> {
106
+ return this.post<SessionResponse>('/v2/vault/auth/session', {
107
+ identity,
108
+ nonce,
109
+ created_ns,
110
+ tag,
111
+ });
112
+ }
113
+
114
+ // ── V2 secrets CRUD ─────────────────────────────────────────────────
115
+
116
+ /** PUT /v2/vault/secrets/{name} — store a secret. Requires session token. */
117
+ async putSecret(name: string, share: Uint8Array, version: number): Promise<StoreSecretResponse> {
118
+ return this.authedRequest<StoreSecretResponse>('PUT', `/v2/vault/secrets/${encodeURIComponent(name)}`, {
119
+ share: uint8ToBase64(share),
120
+ version,
121
+ });
122
+ }
123
+
124
+ /** GET /v2/vault/secrets/{name} — retrieve a secret. Requires session token. */
125
+ async getSecret(name: string): Promise<{ share: Uint8Array; name: string; version: number; created_ns: number; updated_ns: number }> {
126
+ const resp = await this.authedRequest<GetSecretResponse>('GET', `/v2/vault/secrets/${encodeURIComponent(name)}`);
127
+ return {
128
+ share: base64ToUint8(resp.share),
129
+ name: resp.name,
130
+ version: resp.version,
131
+ created_ns: resp.created_ns,
132
+ updated_ns: resp.updated_ns,
133
+ };
134
+ }
135
+
136
+ /** DELETE /v2/vault/secrets/{name} — delete a secret. Requires session token. */
137
+ async deleteSecret(name: string): Promise<DeleteSecretResponse> {
138
+ return this.authedRequest<DeleteSecretResponse>('DELETE', `/v2/vault/secrets/${encodeURIComponent(name)}`);
139
+ }
140
+
141
+ /** GET /v2/vault/secrets — list all secrets. Requires session token. */
142
+ async listSecrets(): Promise<ListSecretsResponse> {
143
+ return this.authedRequest<ListSecretsResponse>('GET', '/v2/vault/secrets');
144
+ }
145
+
146
+ // ── Internal HTTP methods ───────────────────────────────────────────
147
+
148
+ private async authedRequest<T>(method: string, path: string, body?: unknown): Promise<T> {
149
+ if (!this.sessionToken) {
150
+ throw new GuardianError('AUTH', 'No session token set. Call authenticate() first.');
151
+ }
152
+
153
+ const controller = new AbortController();
154
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
155
+
156
+ try {
157
+ const headers: Record<string, string> = {
158
+ 'X-Session-Token': this.sessionToken,
159
+ };
160
+ const init: RequestInit = {
161
+ method,
162
+ headers,
163
+ signal: controller.signal,
164
+ };
165
+
166
+ if (body !== undefined) {
167
+ headers['Content-Type'] = 'application/json';
168
+ init.body = JSON.stringify(body);
169
+ }
170
+
171
+ const resp = await fetch(`${this.baseUrl}${path}`, init);
172
+
173
+ if (!resp.ok) {
174
+ const errBody = (await resp.json().catch(() => ({}))) as GuardianErrorBody;
175
+ const msg = errBody.error || `HTTP ${resp.status}`;
176
+ throw new GuardianError(classifyHttpStatus(resp.status), msg);
177
+ }
178
+
179
+ return (await resp.json()) as T;
180
+ } catch (err) {
181
+ throw classifyError(err);
182
+ } finally {
183
+ clearTimeout(timeout);
184
+ }
185
+ }
186
+
187
+ private async get<T>(path: string): Promise<T> {
188
+ const controller = new AbortController();
189
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
190
+
191
+ try {
192
+ const resp = await fetch(`${this.baseUrl}${path}`, {
193
+ method: 'GET',
194
+ signal: controller.signal,
195
+ });
196
+
197
+ if (!resp.ok) {
198
+ const body = (await resp.json().catch(() => ({}))) as GuardianErrorBody;
199
+ const msg = body.error || `HTTP ${resp.status}`;
200
+ throw new GuardianError(classifyHttpStatus(resp.status), msg);
201
+ }
202
+
203
+ return (await resp.json()) as T;
204
+ } catch (err) {
205
+ throw classifyError(err);
206
+ } finally {
207
+ clearTimeout(timeout);
208
+ }
209
+ }
210
+
211
+ private async post<T>(path: string, body: unknown): Promise<T> {
212
+ const controller = new AbortController();
213
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
214
+
215
+ try {
216
+ const resp = await fetch(`${this.baseUrl}${path}`, {
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify(body),
220
+ signal: controller.signal,
221
+ });
222
+
223
+ if (!resp.ok) {
224
+ const errBody = (await resp.json().catch(() => ({}))) as GuardianErrorBody;
225
+ const msg = errBody.error || `HTTP ${resp.status}`;
226
+ throw new GuardianError(classifyHttpStatus(resp.status), msg);
227
+ }
228
+
229
+ return (await resp.json()) as T;
230
+ } catch (err) {
231
+ throw classifyError(err);
232
+ } finally {
233
+ clearTimeout(timeout);
234
+ }
235
+ }
236
+ }
237
+
238
+ // ── Error classification ──────────────────────────────────────────────────
239
+
240
+ function classifyHttpStatus(status: number): GuardianErrorCode {
241
+ if (status === 404) return 'NOT_FOUND';
242
+ if (status === 401 || status === 403) return 'AUTH';
243
+ if (status === 409) return 'CONFLICT';
244
+ if (status >= 500) return 'SERVER_ERROR';
245
+ return 'NETWORK';
246
+ }
247
+
248
+ function classifyError(err: unknown): GuardianError {
249
+ if (err instanceof GuardianError) return err;
250
+ if (err instanceof Error) {
251
+ if (err.name === 'AbortError') {
252
+ return new GuardianError('TIMEOUT', `Request timed out: ${err.message}`);
253
+ }
254
+ if (err.name === 'TypeError' || err.message.includes('fetch')) {
255
+ return new GuardianError('NETWORK', `Network error: ${err.message}`);
256
+ }
257
+ return new GuardianError('NETWORK', err.message);
258
+ }
259
+ return new GuardianError('NETWORK', String(err));
260
+ }
261
+
262
+ // ── Base64 helpers ────────────────────────────────────────────────────────
263
+
264
+ function uint8ToBase64(bytes: Uint8Array): string {
265
+ if (typeof Buffer !== 'undefined') {
266
+ return Buffer.from(bytes).toString('base64');
267
+ }
268
+ let binary = '';
269
+ for (let i = 0; i < bytes.length; i++) {
270
+ binary += String.fromCharCode(bytes[i]!);
271
+ }
272
+ return btoa(binary);
273
+ }
274
+
275
+ function base64ToUint8(b64: string): Uint8Array {
276
+ if (typeof Buffer !== 'undefined') {
277
+ return new Uint8Array(Buffer.from(b64, 'base64'));
278
+ }
279
+ const binary = atob(b64);
280
+ const bytes = new Uint8Array(binary.length);
281
+ for (let i = 0; i < binary.length; i++) {
282
+ bytes[i] = binary.charCodeAt(i);
283
+ }
284
+ return bytes;
285
+ }
@@ -0,0 +1,19 @@
1
+ export { GuardianClient, GuardianError } from './guardian';
2
+ export { fanOut, fanOutIndexed, withTimeout, withRetry } from './fanout';
3
+ export type {
4
+ GuardianEndpoint,
5
+ GuardianErrorCode,
6
+ GuardianInfo,
7
+ HealthResponse,
8
+ StatusResponse,
9
+ PushResponse,
10
+ PullResponse,
11
+ StoreSecretResponse,
12
+ GetSecretResponse,
13
+ DeleteSecretResponse,
14
+ ListSecretsResponse,
15
+ SecretEntry,
16
+ ChallengeResponse,
17
+ SessionResponse,
18
+ FanOutResult,
19
+ } from './types';
@@ -0,0 +1,101 @@
1
+ /** A guardian node endpoint. */
2
+ export interface GuardianEndpoint {
3
+ address: string;
4
+ port: number;
5
+ }
6
+
7
+ /** V1 push response. */
8
+ export interface PushResponse {
9
+ status: string;
10
+ }
11
+
12
+ /** V1 pull response. */
13
+ export interface PullResponse {
14
+ share: string; // base64
15
+ }
16
+
17
+ /** V2 store response. */
18
+ export interface StoreSecretResponse {
19
+ status: string;
20
+ name: string;
21
+ version: number;
22
+ }
23
+
24
+ /** V2 get response. */
25
+ export interface GetSecretResponse {
26
+ share: string; // base64
27
+ name: string;
28
+ version: number;
29
+ created_ns: number;
30
+ updated_ns: number;
31
+ }
32
+
33
+ /** V2 delete response. */
34
+ export interface DeleteSecretResponse {
35
+ status: string;
36
+ name: string;
37
+ }
38
+
39
+ /** V2 list response. */
40
+ export interface ListSecretsResponse {
41
+ secrets: SecretEntry[];
42
+ }
43
+
44
+ /** An entry in the list secrets response. */
45
+ export interface SecretEntry {
46
+ name: string;
47
+ version: number;
48
+ size: number;
49
+ }
50
+
51
+ /** Health check response. */
52
+ export interface HealthResponse {
53
+ status: string;
54
+ version: string;
55
+ }
56
+
57
+ /** Status response. */
58
+ export interface StatusResponse {
59
+ status: string;
60
+ version: string;
61
+ data_dir: string;
62
+ client_port: number;
63
+ peer_port: number;
64
+ }
65
+
66
+ /** Guardian info response. */
67
+ export interface GuardianInfo {
68
+ guardians: Array<{ address: string; port: number }>;
69
+ threshold: number;
70
+ total: number;
71
+ }
72
+
73
+ /** Challenge response from auth endpoint. */
74
+ export interface ChallengeResponse {
75
+ nonce: string;
76
+ created_ns: number;
77
+ tag: string;
78
+ }
79
+
80
+ /** Session token response from auth endpoint. */
81
+ export interface SessionResponse {
82
+ identity: string;
83
+ expiry_ns: number;
84
+ tag: string;
85
+ }
86
+
87
+ /** Error body from guardian. */
88
+ export interface GuardianErrorBody {
89
+ error: string;
90
+ }
91
+
92
+ /** Error classification codes. */
93
+ export type GuardianErrorCode = 'TIMEOUT' | 'NOT_FOUND' | 'AUTH' | 'SERVER_ERROR' | 'NETWORK' | 'CONFLICT';
94
+
95
+ /** Fan-out result for a single guardian. */
96
+ export interface FanOutResult<T> {
97
+ endpoint: GuardianEndpoint;
98
+ result: T | null;
99
+ error: string | null;
100
+ errorCode?: GuardianErrorCode;
101
+ }
@@ -0,0 +1,62 @@
1
+ import type { GuardianEndpoint } from './transport/types';
2
+
3
+ /** Configuration for VaultClient. */
4
+ export interface VaultConfig {
5
+ /** Guardian endpoints to connect to. */
6
+ guardians: GuardianEndpoint[];
7
+ /** HMAC key for authentication (derived from user's secret). */
8
+ hmacKey: Uint8Array;
9
+ /** Identity hash (hex string, 64 chars). */
10
+ identityHex: string;
11
+ /** Request timeout in ms (default: 10000). */
12
+ timeoutMs?: number;
13
+ }
14
+
15
+ /** Metadata for a stored secret. */
16
+ export interface SecretMeta {
17
+ name: string;
18
+ version: number;
19
+ size: number;
20
+ }
21
+
22
+ /** Result of a store operation. */
23
+ export interface StoreResult {
24
+ /** Number of guardians that acknowledged. */
25
+ ackCount: number;
26
+ /** Total guardians contacted. */
27
+ totalContacted: number;
28
+ /** Number of failures. */
29
+ failCount: number;
30
+ /** Whether write quorum was met. */
31
+ quorumMet: boolean;
32
+ /** Per-guardian results. */
33
+ guardianResults: GuardianResult[];
34
+ }
35
+
36
+ /** Result of a retrieve operation. */
37
+ export interface RetrieveResult {
38
+ /** The reconstructed secret data. */
39
+ data: Uint8Array;
40
+ /** Number of shares collected. */
41
+ sharesCollected: number;
42
+ }
43
+
44
+ /** Result of a list operation. */
45
+ export interface ListResult {
46
+ secrets: SecretMeta[];
47
+ }
48
+
49
+ /** Result of a delete operation. */
50
+ export interface DeleteResult {
51
+ /** Number of guardians that acknowledged. */
52
+ ackCount: number;
53
+ totalContacted: number;
54
+ quorumMet: boolean;
55
+ }
56
+
57
+ /** Per-guardian operation result. */
58
+ export interface GuardianResult {
59
+ endpoint: string;
60
+ success: boolean;
61
+ error?: string;
62
+ }