@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.
- package/README.md +191 -0
- package/dist/index.d.ts +457 -5
- package/dist/index.js +790 -9
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
- package/src/core/interfaces/IWebSocketClient.ts +2 -2
- package/src/core/ws.ts +14 -6
- package/src/index.ts +66 -0
- package/src/pubsub/client.ts +3 -3
- package/src/pubsub/types.ts +1 -1
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@debros/network-ts-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "TypeScript SDK for DeBros Network Gateway - Database, PubSub, Cache, Storage, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
"wasm",
|
|
24
24
|
"serverless",
|
|
25
25
|
"distributed",
|
|
26
|
-
"gateway"
|
|
26
|
+
"gateway",
|
|
27
|
+
"vault",
|
|
28
|
+
"secrets",
|
|
29
|
+
"shamir",
|
|
30
|
+
"encryption",
|
|
31
|
+
"guardian"
|
|
27
32
|
],
|
|
28
33
|
"repository": {
|
|
29
34
|
"type": "git",
|
|
@@ -53,6 +58,8 @@
|
|
|
53
58
|
"release:gh": "npm publish --registry=https://npm.pkg.github.com"
|
|
54
59
|
},
|
|
55
60
|
"dependencies": {
|
|
61
|
+
"@noble/ciphers": "^0.5.3",
|
|
62
|
+
"@noble/hashes": "^1.4.0",
|
|
56
63
|
"isomorphic-ws": "^5.0.0"
|
|
57
64
|
},
|
|
58
65
|
"devDependencies": {
|
|
@@ -41,12 +41,12 @@ export interface IWebSocketClient {
|
|
|
41
41
|
/**
|
|
42
42
|
* Register close handler
|
|
43
43
|
*/
|
|
44
|
-
onClose(handler: () => void): void;
|
|
44
|
+
onClose(handler: (code: number, reason: string) => void): void;
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Unregister close handler
|
|
48
48
|
*/
|
|
49
|
-
offClose(handler: () => void): void;
|
|
49
|
+
offClose(handler: (code: number, reason: string) => void): void;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Check if WebSocket is connected
|
package/src/core/ws.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface WSClientConfig {
|
|
|
16
16
|
|
|
17
17
|
export type WSMessageHandler = (data: string) => void;
|
|
18
18
|
export type WSErrorHandler = (error: Error) => void;
|
|
19
|
-
export type WSCloseHandler = () => void;
|
|
19
|
+
export type WSCloseHandler = (code: number, reason: string) => void;
|
|
20
20
|
export type WSOpenHandler = () => void;
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -102,7 +102,12 @@ export class WSClient {
|
|
|
102
102
|
this.ws.addEventListener("error", (event: Event) => {
|
|
103
103
|
console.error("[WSClient] WebSocket error:", event);
|
|
104
104
|
clearTimeout(timeout);
|
|
105
|
-
|
|
105
|
+
// Extract useful details from the event — raw Event objects don't serialize
|
|
106
|
+
const details: Record<string, any> = { type: event.type };
|
|
107
|
+
if ("message" in event) {
|
|
108
|
+
details.message = (event as ErrorEvent).message;
|
|
109
|
+
}
|
|
110
|
+
const error = new SDKError("WebSocket error", 0, "WS_ERROR", details);
|
|
106
111
|
|
|
107
112
|
// Call the network error callback if configured
|
|
108
113
|
if (this.onNetworkError) {
|
|
@@ -118,10 +123,13 @@ export class WSClient {
|
|
|
118
123
|
reject(error);
|
|
119
124
|
});
|
|
120
125
|
|
|
121
|
-
this.ws.addEventListener("close", () => {
|
|
126
|
+
this.ws.addEventListener("close", (event: Event) => {
|
|
122
127
|
clearTimeout(timeout);
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
const closeEvent = event as CloseEvent;
|
|
129
|
+
const code = closeEvent.code ?? 1006;
|
|
130
|
+
const reason = closeEvent.reason ?? "";
|
|
131
|
+
console.log(`[WSClient] Connection closed (code: ${code}, reason: ${reason || "none"})`);
|
|
132
|
+
this.closeHandlers.forEach((handler) => handler(code, reason));
|
|
125
133
|
});
|
|
126
134
|
} catch (error) {
|
|
127
135
|
reject(error);
|
|
@@ -206,7 +214,7 @@ export class WSClient {
|
|
|
206
214
|
*/
|
|
207
215
|
send(data: string): void {
|
|
208
216
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
209
|
-
throw new SDKError("WebSocket is not connected",
|
|
217
|
+
throw new SDKError("WebSocket is not connected", 0, "WS_NOT_CONNECTED");
|
|
210
218
|
}
|
|
211
219
|
this.ws.send(data);
|
|
212
220
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,14 @@ import { NetworkClient } from "./network/client";
|
|
|
6
6
|
import { CacheClient } from "./cache/client";
|
|
7
7
|
import { StorageClient } from "./storage/client";
|
|
8
8
|
import { FunctionsClient, FunctionsClientConfig } from "./functions/client";
|
|
9
|
+
import { VaultClient } from "./vault/client";
|
|
9
10
|
import { WSClientConfig } from "./core/ws";
|
|
10
11
|
import {
|
|
11
12
|
StorageAdapter,
|
|
12
13
|
MemoryStorage,
|
|
13
14
|
LocalStorageAdapter,
|
|
14
15
|
} from "./auth/types";
|
|
16
|
+
import type { VaultConfig } from "./vault/types";
|
|
15
17
|
|
|
16
18
|
export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
|
|
17
19
|
apiKey?: string;
|
|
@@ -25,6 +27,8 @@ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
|
|
|
25
27
|
* Use this to trigger gateway failover at the application layer.
|
|
26
28
|
*/
|
|
27
29
|
onNetworkError?: NetworkErrorCallback;
|
|
30
|
+
/** Configuration for the vault (distributed secrets store). */
|
|
31
|
+
vaultConfig?: VaultConfig;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export interface Client {
|
|
@@ -35,6 +39,7 @@ export interface Client {
|
|
|
35
39
|
cache: CacheClient;
|
|
36
40
|
storage: StorageClient;
|
|
37
41
|
functions: FunctionsClient;
|
|
42
|
+
vault: VaultClient | null;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function createClient(config: ClientConfig): Client {
|
|
@@ -68,6 +73,9 @@ export function createClient(config: ClientConfig): Client {
|
|
|
68
73
|
const cache = new CacheClient(httpClient);
|
|
69
74
|
const storage = new StorageClient(httpClient);
|
|
70
75
|
const functions = new FunctionsClient(httpClient, config.functionsConfig);
|
|
76
|
+
const vault = config.vaultConfig
|
|
77
|
+
? new VaultClient(config.vaultConfig)
|
|
78
|
+
: null;
|
|
71
79
|
|
|
72
80
|
return {
|
|
73
81
|
auth,
|
|
@@ -77,6 +85,7 @@ export function createClient(config: ClientConfig): Client {
|
|
|
77
85
|
cache,
|
|
78
86
|
storage,
|
|
79
87
|
functions,
|
|
88
|
+
vault,
|
|
80
89
|
};
|
|
81
90
|
}
|
|
82
91
|
|
|
@@ -133,3 +142,60 @@ export type {
|
|
|
133
142
|
} from "./storage/client";
|
|
134
143
|
export type { FunctionsClientConfig } from "./functions/client";
|
|
135
144
|
export type * from "./functions/types";
|
|
145
|
+
// Vault module
|
|
146
|
+
export { VaultClient } from "./vault/client";
|
|
147
|
+
export { AuthClient as VaultAuthClient } from "./vault/auth";
|
|
148
|
+
export { GuardianClient, GuardianError } from "./vault/transport";
|
|
149
|
+
export { fanOut, fanOutIndexed, withTimeout, withRetry } from "./vault/transport";
|
|
150
|
+
export { adaptiveThreshold, writeQuorum } from "./vault/quorum";
|
|
151
|
+
export {
|
|
152
|
+
encrypt,
|
|
153
|
+
decrypt,
|
|
154
|
+
encryptString,
|
|
155
|
+
decryptString,
|
|
156
|
+
serializeEncrypted,
|
|
157
|
+
deserializeEncrypted,
|
|
158
|
+
encryptAndSerialize,
|
|
159
|
+
deserializeAndDecrypt,
|
|
160
|
+
encryptedToHex,
|
|
161
|
+
encryptedFromHex,
|
|
162
|
+
encryptedToBase64,
|
|
163
|
+
encryptedFromBase64,
|
|
164
|
+
generateKey,
|
|
165
|
+
generateNonce,
|
|
166
|
+
clearKey,
|
|
167
|
+
isValidEncryptedData,
|
|
168
|
+
KEY_SIZE,
|
|
169
|
+
NONCE_SIZE,
|
|
170
|
+
TAG_SIZE,
|
|
171
|
+
deriveKeyHKDF,
|
|
172
|
+
shamirSplit,
|
|
173
|
+
shamirCombine,
|
|
174
|
+
} from "./vault";
|
|
175
|
+
export type {
|
|
176
|
+
VaultConfig,
|
|
177
|
+
SecretMeta,
|
|
178
|
+
StoreResult,
|
|
179
|
+
RetrieveResult,
|
|
180
|
+
ListResult,
|
|
181
|
+
DeleteResult,
|
|
182
|
+
GuardianResult as VaultGuardianResult,
|
|
183
|
+
EncryptedData,
|
|
184
|
+
SerializedEncryptedData,
|
|
185
|
+
ShamirShare,
|
|
186
|
+
GuardianEndpoint,
|
|
187
|
+
GuardianErrorCode,
|
|
188
|
+
GuardianInfo,
|
|
189
|
+
GuardianHealthResponse,
|
|
190
|
+
GuardianStatusResponse,
|
|
191
|
+
PushResponse,
|
|
192
|
+
PullResponse,
|
|
193
|
+
StoreSecretResponse,
|
|
194
|
+
GetSecretResponse,
|
|
195
|
+
DeleteSecretResponse,
|
|
196
|
+
ListSecretsResponse,
|
|
197
|
+
SecretEntry,
|
|
198
|
+
GuardianChallengeResponse,
|
|
199
|
+
GuardianSessionResponse,
|
|
200
|
+
FanOutResult,
|
|
201
|
+
} from "./vault";
|
package/src/pubsub/client.ts
CHANGED
|
@@ -176,7 +176,7 @@ export class Subscription {
|
|
|
176
176
|
private isClosed = false;
|
|
177
177
|
private wsMessageHandler: ((data: string) => void) | null = null;
|
|
178
178
|
private wsErrorHandler: ((error: Error) => void) | null = null;
|
|
179
|
-
private wsCloseHandler: (() => void) | null = null;
|
|
179
|
+
private wsCloseHandler: ((code: number, reason: string) => void) | null = null;
|
|
180
180
|
private getPresenceFn: () => Promise<PresenceResponse>;
|
|
181
181
|
|
|
182
182
|
constructor(
|
|
@@ -271,8 +271,8 @@ export class Subscription {
|
|
|
271
271
|
this.wsClient.onError(this.wsErrorHandler);
|
|
272
272
|
|
|
273
273
|
// Register close handler
|
|
274
|
-
this.wsCloseHandler = () => {
|
|
275
|
-
this.closeHandlers.forEach((handler) => handler());
|
|
274
|
+
this.wsCloseHandler = (code: number, reason: string) => {
|
|
275
|
+
this.closeHandlers.forEach((handler) => handler(code, reason));
|
|
276
276
|
};
|
|
277
277
|
this.wsClient.onClose(this.wsCloseHandler);
|
|
278
278
|
}
|
package/src/pubsub/types.ts
CHANGED
|
@@ -42,5 +42,5 @@ export interface SubscribeOptions {
|
|
|
42
42
|
|
|
43
43
|
export type MessageHandler = (message: PubSubMessage) => void;
|
|
44
44
|
export type ErrorHandler = (error: Error) => void;
|
|
45
|
-
export type CloseHandler = () => void;
|
|
45
|
+
export type CloseHandler = (code: number, reason: string) => void;
|
|
46
46
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { AuthClient } from './auth';
|
|
2
|
+
import type { GuardianClient } from './transport/guardian';
|
|
3
|
+
import { withTimeout, withRetry } from './transport/fanout';
|
|
4
|
+
import { split, combine } from './crypto/shamir';
|
|
5
|
+
import type { Share } from './crypto/shamir';
|
|
6
|
+
import { adaptiveThreshold, writeQuorum } from './quorum';
|
|
7
|
+
import type {
|
|
8
|
+
VaultConfig,
|
|
9
|
+
StoreResult,
|
|
10
|
+
RetrieveResult,
|
|
11
|
+
ListResult,
|
|
12
|
+
DeleteResult,
|
|
13
|
+
GuardianResult,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
const PULL_TIMEOUT_MS = 10_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* High-level client for the orama-vault distributed secrets store.
|
|
20
|
+
*
|
|
21
|
+
* Handles:
|
|
22
|
+
* - Authentication with guardian nodes
|
|
23
|
+
* - Shamir split/combine for data distribution
|
|
24
|
+
* - Quorum-based writes and reads
|
|
25
|
+
* - V2 CRUD operations (store, retrieve, list, delete)
|
|
26
|
+
*/
|
|
27
|
+
export class VaultClient {
|
|
28
|
+
private config: VaultConfig;
|
|
29
|
+
private auth: AuthClient;
|
|
30
|
+
|
|
31
|
+
constructor(config: VaultConfig) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.auth = new AuthClient(config.identityHex, config.timeoutMs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Store a secret across guardian nodes using Shamir splitting.
|
|
38
|
+
*
|
|
39
|
+
* @param name - Secret name (alphanumeric, _, -, max 128 chars)
|
|
40
|
+
* @param data - Secret data to store
|
|
41
|
+
* @param version - Monotonic version number (must be > previous)
|
|
42
|
+
*/
|
|
43
|
+
async store(name: string, data: Uint8Array, version: number): Promise<StoreResult> {
|
|
44
|
+
const guardians = this.config.guardians;
|
|
45
|
+
const n = guardians.length;
|
|
46
|
+
const k = adaptiveThreshold(n);
|
|
47
|
+
|
|
48
|
+
// Shamir split the data
|
|
49
|
+
const shares = split(data, n, k);
|
|
50
|
+
|
|
51
|
+
// Authenticate and push to all guardians
|
|
52
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
53
|
+
|
|
54
|
+
const results = await Promise.allSettled(
|
|
55
|
+
authed.map(async ({ client, endpoint }, _i) => {
|
|
56
|
+
// Find the share for this guardian's index
|
|
57
|
+
const guardianIdx = guardians.indexOf(endpoint);
|
|
58
|
+
const share = shares[guardianIdx];
|
|
59
|
+
if (!share) throw new Error('share index out of bounds');
|
|
60
|
+
|
|
61
|
+
// Encode share as [x:1byte][y:rest]
|
|
62
|
+
const shareBytes = new Uint8Array(1 + share.y.length);
|
|
63
|
+
shareBytes[0] = share.x;
|
|
64
|
+
shareBytes.set(share.y, 1);
|
|
65
|
+
|
|
66
|
+
return withRetry(() => client.putSecret(name, shareBytes, version));
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Wipe shares
|
|
71
|
+
for (const share of shares) {
|
|
72
|
+
share.y.fill(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const guardianResults: GuardianResult[] = authed.map(({ endpoint }, i) => {
|
|
76
|
+
const ep = `${endpoint.address}:${endpoint.port}`;
|
|
77
|
+
const r = results[i]!;
|
|
78
|
+
if (r.status === 'fulfilled') {
|
|
79
|
+
return { endpoint: ep, success: true };
|
|
80
|
+
}
|
|
81
|
+
return { endpoint: ep, success: false, error: (r.reason as Error).message };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const ackCount = results.filter((r) => r.status === 'fulfilled').length;
|
|
85
|
+
const failCount = results.filter((r) => r.status === 'rejected').length;
|
|
86
|
+
const w = writeQuorum(n);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ackCount,
|
|
90
|
+
totalContacted: authed.length,
|
|
91
|
+
failCount,
|
|
92
|
+
quorumMet: ackCount >= w,
|
|
93
|
+
guardianResults,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Retrieve and reconstruct a secret from guardian nodes.
|
|
99
|
+
*
|
|
100
|
+
* @param name - Secret name
|
|
101
|
+
*/
|
|
102
|
+
async retrieve(name: string): Promise<RetrieveResult> {
|
|
103
|
+
const guardians = this.config.guardians;
|
|
104
|
+
const n = guardians.length;
|
|
105
|
+
const k = adaptiveThreshold(n);
|
|
106
|
+
|
|
107
|
+
// Authenticate and pull from all guardians
|
|
108
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
109
|
+
|
|
110
|
+
const pullResults = await Promise.allSettled(
|
|
111
|
+
authed.map(async ({ client }) => {
|
|
112
|
+
const resp = await withTimeout(client.getSecret(name), PULL_TIMEOUT_MS);
|
|
113
|
+
const shareBytes = resp.share;
|
|
114
|
+
if (shareBytes.length < 2) throw new Error('Share too short');
|
|
115
|
+
return {
|
|
116
|
+
x: shareBytes[0]!,
|
|
117
|
+
y: shareBytes.slice(1),
|
|
118
|
+
} as Share;
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const shares: Share[] = [];
|
|
123
|
+
for (const r of pullResults) {
|
|
124
|
+
if (r.status === 'fulfilled') {
|
|
125
|
+
shares.push(r.value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (shares.length < k) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Not enough shares: collected ${shares.length} of ${k} required (contacted ${authed.length} guardians)`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Reconstruct
|
|
136
|
+
const data = combine(shares);
|
|
137
|
+
|
|
138
|
+
// Wipe collected shares
|
|
139
|
+
for (const share of shares) {
|
|
140
|
+
share.y.fill(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
data,
|
|
145
|
+
sharesCollected: shares.length,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List all secrets for this identity.
|
|
151
|
+
* Queries the first reachable guardian (metadata is replicated).
|
|
152
|
+
*/
|
|
153
|
+
async list(): Promise<ListResult> {
|
|
154
|
+
const guardians = this.config.guardians;
|
|
155
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
156
|
+
|
|
157
|
+
if (authed.length === 0) {
|
|
158
|
+
throw new Error('No guardians reachable');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Query first authenticated guardian
|
|
162
|
+
const resp = await authed[0]!.client.listSecrets();
|
|
163
|
+
return { secrets: resp.secrets };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Delete a secret from all guardian nodes.
|
|
168
|
+
*
|
|
169
|
+
* @param name - Secret name to delete
|
|
170
|
+
*/
|
|
171
|
+
async delete(name: string): Promise<DeleteResult> {
|
|
172
|
+
const guardians = this.config.guardians;
|
|
173
|
+
const n = guardians.length;
|
|
174
|
+
|
|
175
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
176
|
+
|
|
177
|
+
const results = await Promise.allSettled(
|
|
178
|
+
authed.map(async ({ client }) => {
|
|
179
|
+
return withRetry(() => client.deleteSecret(name));
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const ackCount = results.filter((r) => r.status === 'fulfilled').length;
|
|
184
|
+
const w = writeQuorum(n);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
ackCount,
|
|
188
|
+
totalContacted: authed.length,
|
|
189
|
+
quorumMet: ackCount >= w,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Clear all cached auth sessions. */
|
|
194
|
+
clearSessions(): void {
|
|
195
|
+
this.auth.clearSessions();
|
|
196
|
+
}
|
|
197
|
+
}
|