@debros/network-ts-sdk 0.6.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debros/network-ts-sdk",
3
- "version": "0.6.2",
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": {
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";
@@ -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
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * AES-256-GCM Encryption
3
+ *
4
+ * Implements authenticated encryption using AES-256 in Galois/Counter Mode.
5
+ * Uses @noble/ciphers for platform-agnostic, audited cryptographic operations.
6
+ *
7
+ * Features:
8
+ * - Authenticated encryption (confidentiality + integrity)
9
+ * - 256-bit keys for strong security
10
+ * - 96-bit nonces (randomly generated)
11
+ * - 128-bit authentication tags
12
+ *
13
+ * Security considerations:
14
+ * - Never reuse a nonce with the same key
15
+ * - Nonces are randomly generated and prepended to ciphertext
16
+ * - Authentication tags are verified before decryption
17
+ */
18
+
19
+ import { gcm } from '@noble/ciphers/aes';
20
+ import { randomBytes } from '@noble/ciphers/webcrypto';
21
+ import { bytesToHex, hexToBytes, concatBytes } from '@noble/hashes/utils';
22
+
23
+ /**
24
+ * Size constants
25
+ */
26
+ export const KEY_SIZE = 32; // 256 bits
27
+ export const NONCE_SIZE = 12; // 96 bits (recommended for GCM)
28
+ export const TAG_SIZE = 16; // 128 bits
29
+
30
+ /**
31
+ * Encrypted data structure
32
+ */
33
+ export interface EncryptedData {
34
+ /** Ciphertext including authentication tag */
35
+ ciphertext: Uint8Array;
36
+ /** Nonce used for encryption */
37
+ nonce: Uint8Array;
38
+ /** Additional authenticated data (optional) */
39
+ aad?: Uint8Array;
40
+ }
41
+
42
+ /**
43
+ * Serialized encrypted data (nonce prepended to ciphertext)
44
+ */
45
+ export interface SerializedEncryptedData {
46
+ /** Combined nonce + ciphertext + tag */
47
+ data: Uint8Array;
48
+ /** Additional authenticated data (optional) */
49
+ aad?: Uint8Array;
50
+ }
51
+
52
+ /**
53
+ * Encrypts data using AES-256-GCM
54
+ */
55
+ export function encrypt(
56
+ plaintext: Uint8Array,
57
+ key: Uint8Array,
58
+ aad?: Uint8Array
59
+ ): EncryptedData {
60
+ validateKey(key);
61
+
62
+ const nonce = randomBytes(NONCE_SIZE);
63
+ const cipher = gcm(key, nonce, aad);
64
+ const ciphertext = cipher.encrypt(plaintext);
65
+
66
+ return {
67
+ ciphertext,
68
+ nonce,
69
+ aad,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Decrypts data using AES-256-GCM
75
+ */
76
+ export function decrypt(encryptedData: EncryptedData, key: Uint8Array): Uint8Array {
77
+ validateKey(key);
78
+ validateNonce(encryptedData.nonce);
79
+
80
+ const cipher = gcm(key, encryptedData.nonce, encryptedData.aad);
81
+
82
+ try {
83
+ return cipher.decrypt(encryptedData.ciphertext);
84
+ } catch (error) {
85
+ throw new Error('Decryption failed: invalid ciphertext or authentication tag');
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Encrypts a string message
91
+ */
92
+ export function encryptString(
93
+ message: string,
94
+ key: Uint8Array,
95
+ aad?: Uint8Array
96
+ ): EncryptedData {
97
+ const plaintext = new TextEncoder().encode(message);
98
+ try {
99
+ return encrypt(plaintext, key, aad);
100
+ } finally {
101
+ plaintext.fill(0);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Decrypts to a string message
107
+ */
108
+ export function decryptString(encryptedData: EncryptedData, key: Uint8Array): string {
109
+ const plaintext = decrypt(encryptedData, key);
110
+ try {
111
+ return new TextDecoder().decode(plaintext);
112
+ } finally {
113
+ plaintext.fill(0);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Serializes encrypted data (prepends nonce to ciphertext)
119
+ */
120
+ export function serialize(encryptedData: EncryptedData): SerializedEncryptedData {
121
+ const data = concatBytes(encryptedData.nonce, encryptedData.ciphertext);
122
+
123
+ return {
124
+ data,
125
+ aad: encryptedData.aad,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Deserializes encrypted data
131
+ */
132
+ export function deserialize(serialized: SerializedEncryptedData): EncryptedData {
133
+ if (serialized.data.length < NONCE_SIZE + TAG_SIZE) {
134
+ throw new Error('Invalid serialized data: too short');
135
+ }
136
+
137
+ const nonce = serialized.data.slice(0, NONCE_SIZE);
138
+ const ciphertext = serialized.data.slice(NONCE_SIZE);
139
+
140
+ return {
141
+ ciphertext,
142
+ nonce,
143
+ aad: serialized.aad,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Encrypts and serializes data in one step
149
+ */
150
+ export function encryptAndSerialize(
151
+ plaintext: Uint8Array,
152
+ key: Uint8Array,
153
+ aad?: Uint8Array
154
+ ): SerializedEncryptedData {
155
+ const encrypted = encrypt(plaintext, key, aad);
156
+ return serialize(encrypted);
157
+ }
158
+
159
+ /**
160
+ * Deserializes and decrypts data in one step
161
+ */
162
+ export function deserializeAndDecrypt(
163
+ serialized: SerializedEncryptedData,
164
+ key: Uint8Array
165
+ ): Uint8Array {
166
+ const encrypted = deserialize(serialized);
167
+ return decrypt(encrypted, key);
168
+ }
169
+
170
+ /**
171
+ * Converts encrypted data to hex string
172
+ */
173
+ export function toHex(encryptedData: EncryptedData): string {
174
+ const serialized = serialize(encryptedData);
175
+ return bytesToHex(serialized.data);
176
+ }
177
+
178
+ /**
179
+ * Parses encrypted data from hex string
180
+ */
181
+ export function fromHex(hex: string, aad?: Uint8Array): EncryptedData {
182
+ const normalized = hex.startsWith('0x') ? hex.slice(2) : hex;
183
+ const data = hexToBytes(normalized);
184
+
185
+ return deserialize({ data, aad });
186
+ }
187
+
188
+ /**
189
+ * Converts encrypted data to base64 string
190
+ */
191
+ export function toBase64(encryptedData: EncryptedData): string {
192
+ const serialized = serialize(encryptedData);
193
+
194
+ if (typeof btoa === 'function') {
195
+ return btoa(String.fromCharCode(...serialized.data));
196
+ } else {
197
+ return Buffer.from(serialized.data).toString('base64');
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Parses encrypted data from base64 string
203
+ */
204
+ export function fromBase64(base64: string, aad?: Uint8Array): EncryptedData {
205
+ let data: Uint8Array;
206
+
207
+ if (typeof atob === 'function') {
208
+ const binary = atob(base64);
209
+ data = new Uint8Array(binary.length);
210
+ for (let i = 0; i < binary.length; i++) {
211
+ data[i] = binary.charCodeAt(i);
212
+ }
213
+ } else {
214
+ data = new Uint8Array(Buffer.from(base64, 'base64'));
215
+ }
216
+
217
+ return deserialize({ data, aad });
218
+ }
219
+
220
+ function validateKey(key: Uint8Array): void {
221
+ if (!(key instanceof Uint8Array)) {
222
+ throw new Error('Key must be a Uint8Array');
223
+ }
224
+
225
+ if (key.length !== KEY_SIZE) {
226
+ throw new Error(`Invalid key length: expected ${KEY_SIZE}, got ${key.length}`);
227
+ }
228
+ }
229
+
230
+ function validateNonce(nonce: Uint8Array): void {
231
+ if (!(nonce instanceof Uint8Array)) {
232
+ throw new Error('Nonce must be a Uint8Array');
233
+ }
234
+
235
+ if (nonce.length !== NONCE_SIZE) {
236
+ throw new Error(`Invalid nonce length: expected ${NONCE_SIZE}, got ${nonce.length}`);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Generates a random encryption key
242
+ */
243
+ export function generateKey(): Uint8Array {
244
+ return randomBytes(KEY_SIZE);
245
+ }
246
+
247
+ /**
248
+ * Generates a random nonce
249
+ */
250
+ export function generateNonce(): Uint8Array {
251
+ return randomBytes(NONCE_SIZE);
252
+ }
253
+
254
+ /**
255
+ * Securely clears a key from memory
256
+ */
257
+ export function clearKey(key: Uint8Array): void {
258
+ key.fill(0);
259
+ }
260
+
261
+ /**
262
+ * Checks if encrypted data appears valid (basic structure check)
263
+ */
264
+ export function isValidEncryptedData(data: EncryptedData): boolean {
265
+ return (
266
+ data.nonce instanceof Uint8Array &&
267
+ data.nonce.length === NONCE_SIZE &&
268
+ data.ciphertext instanceof Uint8Array &&
269
+ data.ciphertext.length >= TAG_SIZE
270
+ );
271
+ }