@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,65 @@
|
|
|
1
|
+
// High-level vault client
|
|
2
|
+
export { VaultClient } from './client';
|
|
3
|
+
export { adaptiveThreshold, writeQuorum } from './quorum';
|
|
4
|
+
export type {
|
|
5
|
+
VaultConfig,
|
|
6
|
+
SecretMeta,
|
|
7
|
+
StoreResult,
|
|
8
|
+
RetrieveResult,
|
|
9
|
+
ListResult,
|
|
10
|
+
DeleteResult,
|
|
11
|
+
GuardianResult,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
// Vault auth (renamed to avoid collision with top-level AuthClient)
|
|
15
|
+
export { AuthClient as VaultAuthClient } from './auth';
|
|
16
|
+
|
|
17
|
+
// Transport (guardian communication)
|
|
18
|
+
export { GuardianClient, GuardianError } from './transport';
|
|
19
|
+
export { fanOut, fanOutIndexed, withTimeout, withRetry } from './transport';
|
|
20
|
+
export type {
|
|
21
|
+
GuardianEndpoint,
|
|
22
|
+
GuardianErrorCode,
|
|
23
|
+
GuardianInfo,
|
|
24
|
+
HealthResponse as GuardianHealthResponse,
|
|
25
|
+
StatusResponse as GuardianStatusResponse,
|
|
26
|
+
PushResponse,
|
|
27
|
+
PullResponse,
|
|
28
|
+
StoreSecretResponse,
|
|
29
|
+
GetSecretResponse,
|
|
30
|
+
DeleteSecretResponse,
|
|
31
|
+
ListSecretsResponse,
|
|
32
|
+
SecretEntry,
|
|
33
|
+
ChallengeResponse as GuardianChallengeResponse,
|
|
34
|
+
SessionResponse as GuardianSessionResponse,
|
|
35
|
+
FanOutResult,
|
|
36
|
+
} from './transport';
|
|
37
|
+
|
|
38
|
+
// Crypto primitives
|
|
39
|
+
export {
|
|
40
|
+
encrypt,
|
|
41
|
+
decrypt,
|
|
42
|
+
encryptString,
|
|
43
|
+
decryptString,
|
|
44
|
+
serialize as serializeEncrypted,
|
|
45
|
+
deserialize as deserializeEncrypted,
|
|
46
|
+
encryptAndSerialize,
|
|
47
|
+
deserializeAndDecrypt,
|
|
48
|
+
toHex as encryptedToHex,
|
|
49
|
+
fromHex as encryptedFromHex,
|
|
50
|
+
toBase64 as encryptedToBase64,
|
|
51
|
+
fromBase64 as encryptedFromBase64,
|
|
52
|
+
generateKey,
|
|
53
|
+
generateNonce,
|
|
54
|
+
clearKey,
|
|
55
|
+
isValidEncryptedData,
|
|
56
|
+
KEY_SIZE,
|
|
57
|
+
NONCE_SIZE,
|
|
58
|
+
TAG_SIZE,
|
|
59
|
+
} from './crypto';
|
|
60
|
+
export type { EncryptedData, SerializedEncryptedData } from './crypto';
|
|
61
|
+
|
|
62
|
+
export { deriveKeyHKDF } from './crypto';
|
|
63
|
+
|
|
64
|
+
export { shamirSplit, shamirCombine } from './crypto';
|
|
65
|
+
export type { ShamirShare } from './crypto';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quorum calculations for distributed vault operations.
|
|
3
|
+
* Must match orama-vault (Zig side).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Adaptive Shamir threshold: max(3, floor(N/3)). */
|
|
7
|
+
export function adaptiveThreshold(n: number): number {
|
|
8
|
+
return Math.max(3, Math.floor(n / 3));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Write quorum: ceil(2N/3). Requires majority for consistency. */
|
|
12
|
+
export function writeQuorum(n: number): number {
|
|
13
|
+
if (n === 0) return 0;
|
|
14
|
+
if (n <= 2) return n;
|
|
15
|
+
return Math.ceil((2 * n) / 3);
|
|
16
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { GuardianClient, GuardianError } from './guardian';
|
|
2
|
+
import type { GuardianEndpoint, GuardianErrorCode, FanOutResult } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fan out an operation to multiple guardians in parallel.
|
|
6
|
+
* Returns results from all guardians (both successes and failures).
|
|
7
|
+
*/
|
|
8
|
+
export async function fanOut<T>(
|
|
9
|
+
guardians: GuardianEndpoint[],
|
|
10
|
+
operation: (client: GuardianClient) => Promise<T>,
|
|
11
|
+
): Promise<FanOutResult<T>[]> {
|
|
12
|
+
const results = await Promise.allSettled(
|
|
13
|
+
guardians.map(async (endpoint) => {
|
|
14
|
+
const client = new GuardianClient(endpoint);
|
|
15
|
+
const result = await operation(client);
|
|
16
|
+
return { endpoint, result, error: null } as FanOutResult<T>;
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return results.map((r, i) => {
|
|
21
|
+
if (r.status === 'fulfilled') return r.value;
|
|
22
|
+
const reason = r.reason as Error;
|
|
23
|
+
const errorCode: GuardianErrorCode | undefined = reason instanceof GuardianError ? reason.code : undefined;
|
|
24
|
+
return {
|
|
25
|
+
endpoint: guardians[i]!,
|
|
26
|
+
result: null,
|
|
27
|
+
error: reason.message,
|
|
28
|
+
errorCode,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fan out an indexed operation to multiple guardians in parallel.
|
|
35
|
+
* The operation receives the index so each guardian can get a different share.
|
|
36
|
+
*/
|
|
37
|
+
export async function fanOutIndexed<T>(
|
|
38
|
+
guardians: GuardianEndpoint[],
|
|
39
|
+
operation: (client: GuardianClient, index: number) => Promise<T>,
|
|
40
|
+
): Promise<FanOutResult<T>[]> {
|
|
41
|
+
const results = await Promise.allSettled(
|
|
42
|
+
guardians.map(async (endpoint, i) => {
|
|
43
|
+
const client = new GuardianClient(endpoint);
|
|
44
|
+
const result = await operation(client, i);
|
|
45
|
+
return { endpoint, result, error: null } as FanOutResult<T>;
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return results.map((r, i) => {
|
|
50
|
+
if (r.status === 'fulfilled') return r.value;
|
|
51
|
+
const reason = r.reason as Error;
|
|
52
|
+
const errorCode: GuardianErrorCode | undefined = reason instanceof GuardianError ? reason.code : undefined;
|
|
53
|
+
return {
|
|
54
|
+
endpoint: guardians[i]!,
|
|
55
|
+
result: null,
|
|
56
|
+
error: reason.message,
|
|
57
|
+
errorCode,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Race a promise against a timeout.
|
|
64
|
+
*/
|
|
65
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
66
|
+
return Promise.race([
|
|
67
|
+
promise,
|
|
68
|
+
new Promise<never>((_, reject) =>
|
|
69
|
+
setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms),
|
|
70
|
+
),
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Retry a function with exponential backoff.
|
|
76
|
+
* Does not retry auth or not-found errors.
|
|
77
|
+
*/
|
|
78
|
+
export async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
|
|
79
|
+
let lastError: Error | undefined;
|
|
80
|
+
for (let i = 0; i < attempts; i++) {
|
|
81
|
+
try {
|
|
82
|
+
return await fn();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
lastError = err as Error;
|
|
85
|
+
if (err instanceof GuardianError && (err.code === 'AUTH' || err.code === 'NOT_FOUND')) {
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
if (i < attempts - 1) {
|
|
89
|
+
await new Promise((r) => setTimeout(r, 200 * Math.pow(2, i)));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw lastError!;
|
|
94
|
+
}
|
|
@@ -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
|
+
}
|