@tinycloud/sdk-services 2.0.1 → 2.0.2-beta.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.
Files changed (139) hide show
  1. package/dist/{types.d.ts → BaseService-D9BFm_rV.d.cts} +179 -27
  2. package/dist/BaseService-D9BFm_rV.d.ts +440 -0
  3. package/dist/index.cjs +3221 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1843 -0
  6. package/dist/index.d.ts +1826 -41
  7. package/dist/index.js +3136 -58
  8. package/dist/index.js.map +1 -1
  9. package/dist/kv/index.cjs +909 -0
  10. package/dist/kv/index.cjs.map +1 -0
  11. package/dist/kv/index.d.cts +748 -0
  12. package/dist/kv/index.d.ts +745 -7
  13. package/dist/kv/index.js +877 -9
  14. package/dist/kv/index.js.map +1 -1
  15. package/dist/sql/index.cjs +596 -0
  16. package/dist/sql/index.cjs.map +1 -0
  17. package/dist/sql/index.d.cts +228 -0
  18. package/dist/sql/index.d.ts +225 -7
  19. package/dist/sql/index.js +566 -8
  20. package/dist/sql/index.js.map +1 -1
  21. package/package.json +7 -6
  22. package/dist/base/BaseService.d.ts +0 -151
  23. package/dist/base/BaseService.d.ts.map +0 -1
  24. package/dist/base/BaseService.js +0 -221
  25. package/dist/base/BaseService.js.map +0 -1
  26. package/dist/base/index.d.ts +0 -6
  27. package/dist/base/index.d.ts.map +0 -1
  28. package/dist/base/index.js +0 -6
  29. package/dist/base/index.js.map +0 -1
  30. package/dist/base/types.d.ts +0 -36
  31. package/dist/base/types.d.ts.map +0 -1
  32. package/dist/base/types.js +0 -7
  33. package/dist/base/types.js.map +0 -1
  34. package/dist/context.d.ts +0 -142
  35. package/dist/context.d.ts.map +0 -1
  36. package/dist/context.js +0 -218
  37. package/dist/context.js.map +0 -1
  38. package/dist/duckdb/DuckDbDatabaseHandle.d.ts +0 -23
  39. package/dist/duckdb/DuckDbDatabaseHandle.d.ts.map +0 -1
  40. package/dist/duckdb/DuckDbDatabaseHandle.js +0 -36
  41. package/dist/duckdb/DuckDbDatabaseHandle.js.map +0 -1
  42. package/dist/duckdb/DuckDbService.d.ts +0 -50
  43. package/dist/duckdb/DuckDbService.d.ts.map +0 -1
  44. package/dist/duckdb/DuckDbService.js +0 -285
  45. package/dist/duckdb/DuckDbService.js.map +0 -1
  46. package/dist/duckdb/IDuckDbService.d.ts +0 -84
  47. package/dist/duckdb/IDuckDbService.d.ts.map +0 -1
  48. package/dist/duckdb/IDuckDbService.js +0 -7
  49. package/dist/duckdb/IDuckDbService.js.map +0 -1
  50. package/dist/duckdb/index.d.ts +0 -10
  51. package/dist/duckdb/index.d.ts.map +0 -1
  52. package/dist/duckdb/index.js +0 -9
  53. package/dist/duckdb/index.js.map +0 -1
  54. package/dist/duckdb/types.d.ts +0 -148
  55. package/dist/duckdb/types.d.ts.map +0 -1
  56. package/dist/duckdb/types.js +0 -19
  57. package/dist/duckdb/types.js.map +0 -1
  58. package/dist/errors.d.ts +0 -62
  59. package/dist/errors.d.ts.map +0 -1
  60. package/dist/errors.js +0 -149
  61. package/dist/errors.js.map +0 -1
  62. package/dist/index.d.ts.map +0 -1
  63. package/dist/kv/IKVService.d.ts +0 -148
  64. package/dist/kv/IKVService.d.ts.map +0 -1
  65. package/dist/kv/IKVService.js +0 -8
  66. package/dist/kv/IKVService.js.map +0 -1
  67. package/dist/kv/KVService.d.ts +0 -155
  68. package/dist/kv/KVService.d.ts.map +0 -1
  69. package/dist/kv/KVService.js +0 -419
  70. package/dist/kv/KVService.js.map +0 -1
  71. package/dist/kv/PrefixedKVService.d.ts +0 -246
  72. package/dist/kv/PrefixedKVService.d.ts.map +0 -1
  73. package/dist/kv/PrefixedKVService.js +0 -145
  74. package/dist/kv/PrefixedKVService.js.map +0 -1
  75. package/dist/kv/index.d.ts.map +0 -1
  76. package/dist/kv/types.d.ts +0 -204
  77. package/dist/kv/types.d.ts.map +0 -1
  78. package/dist/kv/types.js +0 -16
  79. package/dist/kv/types.js.map +0 -1
  80. package/dist/quota/TinyCloudQuota.d.ts +0 -27
  81. package/dist/quota/TinyCloudQuota.d.ts.map +0 -1
  82. package/dist/quota/TinyCloudQuota.js +0 -31
  83. package/dist/quota/TinyCloudQuota.js.map +0 -1
  84. package/dist/quota/index.d.ts +0 -3
  85. package/dist/quota/index.d.ts.map +0 -1
  86. package/dist/quota/index.js +0 -2
  87. package/dist/quota/index.js.map +0 -1
  88. package/dist/sql/DatabaseHandle.d.ts +0 -20
  89. package/dist/sql/DatabaseHandle.d.ts.map +0 -1
  90. package/dist/sql/DatabaseHandle.js +0 -27
  91. package/dist/sql/DatabaseHandle.js.map +0 -1
  92. package/dist/sql/ISQLService.d.ts +0 -67
  93. package/dist/sql/ISQLService.d.ts.map +0 -1
  94. package/dist/sql/ISQLService.js +0 -7
  95. package/dist/sql/ISQLService.js.map +0 -1
  96. package/dist/sql/SQLService.d.ts +0 -44
  97. package/dist/sql/SQLService.d.ts.map +0 -1
  98. package/dist/sql/SQLService.js +0 -216
  99. package/dist/sql/SQLService.js.map +0 -1
  100. package/dist/sql/index.d.ts.map +0 -1
  101. package/dist/sql/types.d.ts +0 -102
  102. package/dist/sql/types.d.ts.map +0 -1
  103. package/dist/sql/types.js +0 -21
  104. package/dist/sql/types.js.map +0 -1
  105. package/dist/types.d.ts.map +0 -1
  106. package/dist/types.js +0 -94
  107. package/dist/types.js.map +0 -1
  108. package/dist/types.schema.d.ts +0 -712
  109. package/dist/types.schema.d.ts.map +0 -1
  110. package/dist/types.schema.js +0 -342
  111. package/dist/types.schema.js.map +0 -1
  112. package/dist/types.schema.test.d.ts +0 -5
  113. package/dist/types.schema.test.d.ts.map +0 -1
  114. package/dist/types.schema.test.js +0 -677
  115. package/dist/types.schema.test.js.map +0 -1
  116. package/dist/vault/DataVaultService.d.ts +0 -267
  117. package/dist/vault/DataVaultService.d.ts.map +0 -1
  118. package/dist/vault/DataVaultService.js +0 -1040
  119. package/dist/vault/DataVaultService.js.map +0 -1
  120. package/dist/vault/IDataVaultService.d.ts +0 -158
  121. package/dist/vault/IDataVaultService.d.ts.map +0 -1
  122. package/dist/vault/IDataVaultService.js +0 -8
  123. package/dist/vault/IDataVaultService.js.map +0 -1
  124. package/dist/vault/SignatureCache.d.ts +0 -20
  125. package/dist/vault/SignatureCache.d.ts.map +0 -1
  126. package/dist/vault/SignatureCache.js +0 -167
  127. package/dist/vault/SignatureCache.js.map +0 -1
  128. package/dist/vault/createVaultCrypto.d.ts +0 -16
  129. package/dist/vault/createVaultCrypto.d.ts.map +0 -1
  130. package/dist/vault/createVaultCrypto.js +0 -12
  131. package/dist/vault/createVaultCrypto.js.map +0 -1
  132. package/dist/vault/index.d.ts +0 -11
  133. package/dist/vault/index.d.ts.map +0 -1
  134. package/dist/vault/index.js +0 -12
  135. package/dist/vault/index.js.map +0 -1
  136. package/dist/vault/types.d.ts +0 -141
  137. package/dist/vault/types.d.ts.map +0 -1
  138. package/dist/vault/types.js +0 -31
  139. package/dist/vault/types.js.map +0 -1
@@ -1,1040 +0,0 @@
1
- /**
2
- * DataVaultService - Encrypted key-value storage service implementation.
3
- *
4
- * Platform-agnostic encrypted KV service that wraps KVService internally.
5
- * Uses dependency injection via VaultCrypto for WASM crypto operations
6
- * and DataVaultServiceConfig for platform dependencies.
7
- *
8
- * Architecture:
9
- * - Extends BaseService (not KVService)
10
- * - Wraps two KV instances: dataKV (prefix "vault/") and keyKV (prefix "keys/")
11
- * - Master key and encryption identity live in memory only (cleared on lock)
12
- */
13
- import { BaseService } from "../base/BaseService";
14
- import { ok, } from "../types";
15
- import { VaultHeaders, VaultVersionConfig, CURRENT_VAULT_VERSION, } from "./types";
16
- import { loadCachedSignature, cacheSignature, clearSignatureCache, } from "./SignatureCache";
17
- // =============================================================================
18
- // Helper Functions
19
- // =============================================================================
20
- /** Convert a caught value to an Error. WASM throws plain objects, not Error instances. */
21
- function toError(error) {
22
- if (error instanceof Error)
23
- return error;
24
- if (typeof error === "object" && error !== null) {
25
- return new Error(JSON.stringify(error));
26
- }
27
- return new Error(String(error));
28
- }
29
- function toBytes(str) {
30
- return new TextEncoder().encode(str);
31
- }
32
- function fromBytes(bytes) {
33
- return new TextDecoder().decode(bytes);
34
- }
35
- function hexEncode(bytes) {
36
- return Array.from(bytes)
37
- .map((b) => b.toString(16).padStart(2, "0"))
38
- .join("");
39
- }
40
- function concatBytes(...arrays) {
41
- const total = arrays.reduce((acc, arr) => acc + arr.length, 0);
42
- const result = new Uint8Array(total);
43
- let offset = 0;
44
- for (const arr of arrays) {
45
- result.set(arr, offset);
46
- offset += arr.length;
47
- }
48
- return result;
49
- }
50
- function base64Encode(bytes) {
51
- let binary = "";
52
- for (let i = 0; i < bytes.length; i++) {
53
- binary += String.fromCharCode(bytes[i]);
54
- }
55
- return btoa(binary);
56
- }
57
- function base64Decode(str) {
58
- const binary = atob(str);
59
- const bytes = new Uint8Array(binary.length);
60
- for (let i = 0; i < binary.length; i++) {
61
- bytes[i] = binary.charCodeAt(i);
62
- }
63
- return bytes;
64
- }
65
- function defaultVaultMessage(input) {
66
- switch (input.code) {
67
- case "DECRYPTION_FAILED": return input.message ?? "Decryption failed";
68
- case "KEY_NOT_FOUND": return input.message ?? `Key not found: ${input.key}`;
69
- case "INTEGRITY_ERROR": return input.message ?? "Integrity check failed";
70
- case "GRANT_NOT_FOUND": return input.message ?? `Grant not found: ${input.grantor} / ${input.key}`;
71
- case "VAULT_LOCKED": return input.message ?? "Vault is locked";
72
- case "PUBLIC_KEY_NOT_FOUND": return input.message ?? `Public key not found for ${input.did}`;
73
- case "STORAGE_ERROR": return input.message ?? input.cause.message;
74
- }
75
- }
76
- function vaultError(input) {
77
- const error = {
78
- ...input,
79
- service: "vault",
80
- message: defaultVaultMessage(input),
81
- };
82
- return { ok: false, error };
83
- }
84
- // =============================================================================
85
- // DataVaultService
86
- // =============================================================================
87
- /**
88
- * Data Vault service implementation.
89
- *
90
- * Provides encrypted key-value storage with client-side encryption,
91
- * key management, and sharing via X25519 grants.
92
- *
93
- * @example
94
- * ```typescript
95
- * // Unlock the vault
96
- * await vault.unlock(signer);
97
- *
98
- * // Store encrypted data
99
- * await vault.put('secret/notes', { content: 'Hello' });
100
- *
101
- * // Retrieve and decrypt
102
- * const entry = await vault.get<{ content: string }>('secret/notes');
103
- * if (entry.ok) {
104
- * console.log(entry.data.value.content); // 'Hello'
105
- * }
106
- *
107
- * // Share with another user
108
- * await vault.grant('secret/notes', recipientDID);
109
- * ```
110
- */
111
- export class DataVaultService extends BaseService {
112
- /**
113
- * Create a new DataVaultService instance.
114
- *
115
- * @param config - Service configuration including crypto and tc references
116
- */
117
- constructor(config) {
118
- super();
119
- this.masterKey = null;
120
- this.encryptionIdentity = null;
121
- this._isUnlocked = false;
122
- this.vaultConfig = config;
123
- this._config = config;
124
- }
125
- /**
126
- * Get the service configuration.
127
- */
128
- get config() {
129
- return this._config;
130
- }
131
- /**
132
- * Whether the vault is currently unlocked.
133
- */
134
- get isUnlocked() {
135
- return this._isUnlocked;
136
- }
137
- /**
138
- * The vault's public encryption key (X25519).
139
- * Throws if vault is locked.
140
- */
141
- get publicKey() {
142
- if (!this.encryptionIdentity) {
143
- throw new Error("Vault is locked");
144
- }
145
- return this.encryptionIdentity.publicKey;
146
- }
147
- /**
148
- * Convenience accessor for crypto operations.
149
- */
150
- get crypto() {
151
- return this.vaultConfig.crypto;
152
- }
153
- /**
154
- * Convenience accessor for TinyCloud instance.
155
- */
156
- get tc() {
157
- return this.vaultConfig.tc;
158
- }
159
- /**
160
- * Get the host URL.
161
- */
162
- get host() {
163
- return this.tc.hosts[0];
164
- }
165
- // =========================================================================
166
- // Phase 1: Core Operations
167
- // =========================================================================
168
- /**
169
- * Unlock the vault. Derives keys from two wallet signatures:
170
- * 1. Master signature (per-space) — used to derive the master encryption key
171
- * 2. Identity signature (per-address) — used to derive X25519 encryption identity
172
- *
173
- * If the identity public key already exists in the public space, the identity
174
- * signature is skipped entirely (no wallet popup). The identity private key is
175
- * only needed for sharing operations.
176
- *
177
- * @param signer - Object with signMessage method. Optional when cached
178
- * signatures exist (browser only).
179
- */
180
- async unlock(signer) {
181
- return this.withTelemetry("unlock", undefined, async () => {
182
- const spaceId = this.vaultConfig.spaceId;
183
- const versionConfig = VaultVersionConfig[CURRENT_VAULT_VERSION];
184
- const masterCacheKey = `vault-master:${spaceId}`;
185
- const identityCacheKey = `vault-identity:${this.tc.address}`;
186
- try {
187
- // -----------------------------------------------------------------
188
- // Step 1: Master signature → master key
189
- // -----------------------------------------------------------------
190
- let masterSigBytes = await loadCachedSignature(masterCacheKey);
191
- if (!masterSigBytes) {
192
- if (!signer) {
193
- return vaultError({
194
- code: "VAULT_LOCKED",
195
- message: "Signer is required when no cached master signature exists",
196
- });
197
- }
198
- const s = signer;
199
- const sig = await s.signMessage(versionConfig.masterMessage(spaceId));
200
- masterSigBytes = toBytes(sig);
201
- await cacheSignature(masterCacheKey, masterSigBytes);
202
- }
203
- // Derive master key: deriveKey(sigBytes, sha256(spaceId), "vault-master")
204
- this.masterKey = this.crypto.deriveKey(masterSigBytes, this.crypto.sha256(toBytes(spaceId)), toBytes("vault-master"));
205
- // -----------------------------------------------------------------
206
- // Step 2: Identity — check public space first, then sign if needed
207
- // -----------------------------------------------------------------
208
- const publicSpaceId = this.tc.makePublicSpaceId(this.tc.address, this.tc.chainId);
209
- // Check public space for existing vault pubkey
210
- let existingPubKey = null;
211
- try {
212
- const existing = await this.tc.readPublicSpace(this.host, publicSpaceId, ".well-known/vault-pubkey");
213
- if (existing.ok && existing.data) {
214
- existingPubKey = existing.data;
215
- }
216
- }
217
- catch {
218
- // Read failed — treat as missing
219
- }
220
- if (existingPubKey) {
221
- // Public key exists — trust it, skip identity signing entirely
222
- this.encryptionIdentity = {
223
- publicKey: base64Decode(existingPubKey),
224
- privateKey: new Uint8Array(0), // private key not available without signing
225
- };
226
- }
227
- else {
228
- // Public key missing — need identity signature to derive keypair
229
- let identitySigBytes = await loadCachedSignature(identityCacheKey);
230
- if (!identitySigBytes) {
231
- if (!signer) {
232
- // No signer available — skip identity derivation.
233
- // Vault still works for get/put (only needs master key).
234
- this.encryptionIdentity = null;
235
- this._isUnlocked = true;
236
- return ok(undefined);
237
- }
238
- const s = signer;
239
- const sig = await s.signMessage(versionConfig.identityMessage);
240
- identitySigBytes = toBytes(sig);
241
- await cacheSignature(identityCacheKey, identitySigBytes);
242
- }
243
- // Derive X25519 keypair from identity signature
244
- const seed = this.crypto.deriveKey(identitySigBytes, toBytes("tinycloud-x25519"), toBytes("encryption-identity"));
245
- this.encryptionIdentity = this.crypto.x25519FromSeed(seed);
246
- // Publish public key to public space
247
- try {
248
- const pubKeyB64 = base64Encode(this.encryptionIdentity.publicKey);
249
- await this.tc.ensurePublicSpace();
250
- await this.tc.publicKV.put(".well-known/vault-pubkey", pubKeyB64);
251
- await this.tc.publicKV.put(".well-known/vault-version", CURRENT_VAULT_VERSION);
252
- await this.tc.publicKV.put(".well-known/vault-space", this.vaultConfig.spaceId);
253
- }
254
- catch {
255
- // Publishing failed — vault still usable
256
- }
257
- }
258
- this._isUnlocked = true;
259
- return ok(undefined);
260
- }
261
- catch (error) {
262
- // Clear key material on failure
263
- this.masterKey = null;
264
- this.encryptionIdentity = null;
265
- return vaultError({
266
- code: "STORAGE_ERROR",
267
- cause: toError(error),
268
- });
269
- }
270
- });
271
- }
272
- /**
273
- * Clear the cached vault signatures.
274
- *
275
- * @param spaceId - Clear only this space's master cache. If omitted, clears all.
276
- */
277
- async clearCache(spaceId) {
278
- if (spaceId) {
279
- await clearSignatureCache(`vault-master:${spaceId}`);
280
- }
281
- else {
282
- await clearSignatureCache();
283
- }
284
- }
285
- /**
286
- * Lock the vault, clearing all key material from memory.
287
- */
288
- lock() {
289
- this.masterKey = null;
290
- this.encryptionIdentity = null;
291
- this._isUnlocked = false;
292
- }
293
- /**
294
- * Called when SDK signs out. Locks the vault and aborts operations.
295
- */
296
- onSignOut() {
297
- this.lock();
298
- super.onSignOut();
299
- }
300
- /**
301
- * Encrypt and store a value at the given key.
302
- *
303
- * @param key - The key to store under
304
- * @param value - The value to encrypt and store
305
- * @param options - Optional put configuration
306
- */
307
- async put(key, value, options) {
308
- return this.withTelemetry("put", key, async () => {
309
- if (!this._isUnlocked || !this.masterKey) {
310
- return vaultError({
311
- code: "VAULT_LOCKED",
312
- message: "Vault must be unlocked before storing data",
313
- });
314
- }
315
- if (!this.requireAuth()) {
316
- return vaultError({
317
- code: "VAULT_LOCKED",
318
- message: "Authentication required",
319
- });
320
- }
321
- try {
322
- // Serialize value
323
- let plaintext;
324
- if (value instanceof Uint8Array) {
325
- plaintext = value;
326
- }
327
- else if (options?.serialize) {
328
- plaintext = options.serialize(value);
329
- }
330
- else if (typeof value === "string") {
331
- plaintext = toBytes(value);
332
- }
333
- else {
334
- plaintext = toBytes(JSON.stringify(value));
335
- }
336
- const contentType = options?.contentType ??
337
- (value instanceof Uint8Array
338
- ? "application/octet-stream"
339
- : "application/json");
340
- // Generate per-entry key
341
- const entryKey = this.crypto.randomBytes(32);
342
- const keyId = hexEncode(this.crypto.sha256(entryKey)).slice(0, 16);
343
- // Encrypt value with entry key
344
- const encrypted = this.crypto.encrypt(entryKey, plaintext);
345
- // Encrypt entry key with master key
346
- const keyBlob = this.crypto.encrypt(this.masterKey, entryKey);
347
- // Build metadata
348
- const metadata = {
349
- [VaultHeaders.VERSION]: "1",
350
- [VaultHeaders.CIPHER]: "aes-256-gcm",
351
- [VaultHeaders.KEY_ID]: keyId,
352
- [VaultHeaders.CONTENT_TYPE]: contentType,
353
- [VaultHeaders.KDF]: "hkdf-sha256",
354
- [VaultHeaders.KEY_ROTATION]: this.vaultConfig.keyRotation ?? "per-write",
355
- ...(options?.metadata ?? {}),
356
- };
357
- // Store encrypted entry key in key space
358
- const keyMetadata = JSON.stringify({
359
- keyId,
360
- contentType,
361
- ...metadata,
362
- });
363
- const keyPayload = JSON.stringify({
364
- key: base64Encode(keyBlob),
365
- metadata: keyMetadata,
366
- });
367
- const keyPutResult = await this.tc.kv.put(`keys/${key}`, keyPayload);
368
- if (!keyPutResult.ok) {
369
- return vaultError({
370
- code: "STORAGE_ERROR",
371
- cause: new Error(`Failed to store key blob: ${keyPutResult.error.message}`),
372
- });
373
- }
374
- // Store encrypted value in data space
375
- const valuePayload = JSON.stringify({
376
- data: base64Encode(encrypted),
377
- metadata,
378
- });
379
- const valuePutResult = await this.tc.kv.put(`vault/${key}`, valuePayload);
380
- if (!valuePutResult.ok) {
381
- return vaultError({
382
- code: "STORAGE_ERROR",
383
- cause: new Error(`Failed to store encrypted value: ${valuePutResult.error.message}`),
384
- });
385
- }
386
- return ok(undefined);
387
- }
388
- catch (error) {
389
- return vaultError({
390
- code: "STORAGE_ERROR",
391
- cause: toError(error),
392
- });
393
- }
394
- });
395
- }
396
- /**
397
- * Retrieve and decrypt a value by key.
398
- *
399
- * @param key - The key to retrieve
400
- * @param options - Optional get configuration
401
- * @returns Result with the decrypted entry
402
- */
403
- async get(key, options) {
404
- return this.withTelemetry("get", key, async () => {
405
- if (!this._isUnlocked || !this.masterKey) {
406
- return vaultError({
407
- code: "VAULT_LOCKED",
408
- message: "Vault must be unlocked before reading data",
409
- });
410
- }
411
- if (!this.requireAuth()) {
412
- return vaultError({
413
- code: "VAULT_LOCKED",
414
- message: "Authentication required",
415
- });
416
- }
417
- try {
418
- // Fetch encrypted entry key from key space
419
- const keyResult = await this.tc.kv.get(`keys/${key}`, {
420
- raw: true,
421
- });
422
- if (!keyResult.ok) {
423
- return vaultError({ code: "KEY_NOT_FOUND", key });
424
- }
425
- const keyEnvelope = JSON.parse(keyResult.data.data);
426
- const keyBlobBytes = base64Decode(keyEnvelope.key);
427
- const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
428
- // Fetch encrypted value from data space
429
- const valueResult = await this.tc.kv.get(`vault/${key}`, {
430
- raw: true,
431
- });
432
- if (!valueResult.ok) {
433
- return vaultError({ code: "KEY_NOT_FOUND", key });
434
- }
435
- const valueEnvelope = JSON.parse(valueResult.data.data);
436
- const encryptedBytes = base64Decode(valueEnvelope.data);
437
- const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
438
- // Read metadata
439
- const metadata = valueEnvelope.metadata ?? {};
440
- const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
441
- const keyId = metadata[VaultHeaders.KEY_ID] ?? "";
442
- // Deserialize
443
- let value;
444
- if (options?.raw) {
445
- value = plaintext;
446
- }
447
- else if (options?.deserialize) {
448
- value = options.deserialize(plaintext);
449
- }
450
- else if (contentType === "application/json") {
451
- value = JSON.parse(fromBytes(plaintext));
452
- }
453
- else {
454
- value = plaintext;
455
- }
456
- return ok({ value, metadata, keyId });
457
- }
458
- catch (error) {
459
- if (error instanceof Error &&
460
- error.message.includes("decryption")) {
461
- return vaultError({
462
- code: "DECRYPTION_FAILED",
463
- message: error.message,
464
- });
465
- }
466
- return vaultError({
467
- code: "STORAGE_ERROR",
468
- cause: toError(error),
469
- });
470
- }
471
- });
472
- }
473
- /**
474
- * Delete an encrypted key.
475
- * Removes both the encrypted value and the key blob.
476
- *
477
- * @param key - The key to delete
478
- */
479
- async delete(key) {
480
- return this.withTelemetry("delete", key, async () => {
481
- if (!this._isUnlocked) {
482
- return vaultError({
483
- code: "VAULT_LOCKED",
484
- message: "Vault must be unlocked before deleting data",
485
- });
486
- }
487
- if (!this.requireAuth()) {
488
- return vaultError({
489
- code: "VAULT_LOCKED",
490
- message: "Authentication required",
491
- });
492
- }
493
- try {
494
- // Delete from both key space and data space
495
- const [keyDelResult, valueDelResult] = await Promise.all([
496
- this.tc.kv.delete(`keys/${key}`),
497
- this.tc.kv.delete(`vault/${key}`),
498
- ]);
499
- if (!keyDelResult.ok && !valueDelResult.ok) {
500
- return vaultError({ code: "KEY_NOT_FOUND", key });
501
- }
502
- return ok(undefined);
503
- }
504
- catch (error) {
505
- return vaultError({
506
- code: "STORAGE_ERROR",
507
- cause: toError(error),
508
- });
509
- }
510
- });
511
- }
512
- /**
513
- * List vault keys with optional prefix filtering.
514
- *
515
- * @param options - Optional list configuration
516
- * @returns Result with array of key names (vault/ prefix stripped)
517
- */
518
- async list(options) {
519
- return this.withTelemetry("list", options?.prefix, async () => {
520
- if (!this._isUnlocked) {
521
- return vaultError({
522
- code: "VAULT_LOCKED",
523
- message: "Vault must be unlocked before listing data",
524
- });
525
- }
526
- if (!this.requireAuth()) {
527
- return vaultError({
528
- code: "VAULT_LOCKED",
529
- message: "Authentication required",
530
- });
531
- }
532
- try {
533
- const listPrefix = options?.prefix
534
- ? `vault/${options.prefix}`
535
- : "vault/";
536
- const listResult = await this.tc.kv.list({
537
- prefix: listPrefix,
538
- removePrefix: true,
539
- });
540
- if (!listResult.ok) {
541
- return vaultError({
542
- code: "STORAGE_ERROR",
543
- cause: new Error(`Failed to list vault keys: ${listResult.error.message}`),
544
- });
545
- }
546
- // Keys are already stripped of the "vault/" prefix by removePrefix
547
- let keys = listResult.data.keys;
548
- // If a user prefix was provided, strip it too if requested
549
- if (options?.removePrefix && options.prefix) {
550
- const userPrefix = options.prefix.endsWith("/")
551
- ? options.prefix
552
- : `${options.prefix}/`;
553
- keys = keys.map((k) => k.startsWith(userPrefix) ? k.slice(userPrefix.length) : k);
554
- }
555
- return ok(keys);
556
- }
557
- catch (error) {
558
- return vaultError({
559
- code: "STORAGE_ERROR",
560
- cause: toError(error),
561
- });
562
- }
563
- });
564
- }
565
- /**
566
- * Get envelope metadata for a key without decrypting the value.
567
- *
568
- * @param key - The key to inspect
569
- * @returns Result with metadata headers
570
- */
571
- async head(key) {
572
- return this.withTelemetry("head", key, async () => {
573
- if (!this._isUnlocked) {
574
- return vaultError({
575
- code: "VAULT_LOCKED",
576
- message: "Vault must be unlocked before reading metadata",
577
- });
578
- }
579
- if (!this.requireAuth()) {
580
- return vaultError({
581
- code: "VAULT_LOCKED",
582
- message: "Authentication required",
583
- });
584
- }
585
- try {
586
- // Fetch envelope without decrypting
587
- const valueResult = await this.tc.kv.get(`vault/${key}`, {
588
- raw: true,
589
- });
590
- if (!valueResult.ok) {
591
- return vaultError({ code: "KEY_NOT_FOUND", key });
592
- }
593
- const valueEnvelope = JSON.parse(valueResult.data.data);
594
- const metadata = valueEnvelope.metadata ?? {};
595
- return ok(metadata);
596
- }
597
- catch (error) {
598
- return vaultError({
599
- code: "STORAGE_ERROR",
600
- cause: toError(error),
601
- });
602
- }
603
- });
604
- }
605
- // =========================================================================
606
- // Batch Operations
607
- // =========================================================================
608
- /**
609
- * Encrypt and store multiple entries.
610
- *
611
- * @param entries - Array of key/value pairs with optional per-entry options
612
- * @returns Array of results, one per entry
613
- */
614
- async putMany(entries) {
615
- return Promise.all(entries.map((entry) => this.put(entry.key, entry.value, entry.options)));
616
- }
617
- /**
618
- * Retrieve and decrypt multiple keys.
619
- *
620
- * @param keys - Array of keys to retrieve
621
- * @param options - Optional get configuration applied to all entries
622
- * @returns Array of results, one per key
623
- */
624
- async getMany(keys, options) {
625
- return Promise.all(keys.map((key) => this.get(key, options)));
626
- }
627
- // =========================================================================
628
- // Phase 2: Sharing
629
- // =========================================================================
630
- /**
631
- * Re-encrypt a vault key for another user (renamed from grant).
632
- * Re-encrypts the data key to the recipient's public key via X25519 DH.
633
- *
634
- * @param key - The key to share
635
- * @param recipientDID - The recipient's primary DID (did:pkh:...)
636
- * @param options - Optional grant configuration
637
- */
638
- async reencrypt(key, recipientDID, options) {
639
- return this.withTelemetry("reencrypt", key, async () => {
640
- if (!this._isUnlocked || !this.masterKey) {
641
- return vaultError({
642
- code: "VAULT_LOCKED",
643
- message: "Vault must be unlocked before granting access",
644
- });
645
- }
646
- if (!this.requireAuth()) {
647
- return vaultError({
648
- code: "VAULT_LOCKED",
649
- message: "Authentication required",
650
- });
651
- }
652
- try {
653
- // Step 1: Resolve recipient's public key
654
- const pubKeyResult = await this.resolvePublicKey(recipientDID);
655
- if (!pubKeyResult.ok) {
656
- return pubKeyResult;
657
- }
658
- const bobPubKey = pubKeyResult.data;
659
- // Step 2: Fetch and decrypt entry key from key space
660
- const keyResult = await this.tc.kv.get(`keys/${key}`, {
661
- raw: true,
662
- });
663
- if (!keyResult.ok) {
664
- return vaultError({ code: "KEY_NOT_FOUND", key });
665
- }
666
- const keyEnvelope = JSON.parse(keyResult.data.data);
667
- const keyBlobBytes = base64Decode(keyEnvelope.key);
668
- const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
669
- // Step 3: Create ephemeral X25519 key pair
670
- const ephemeralSeed = this.crypto.randomBytes(32);
671
- const ephemeralKeyPair = this.crypto.x25519FromSeed(ephemeralSeed);
672
- // Step 4: Compute shared secret via DH
673
- const sharedSecret = this.crypto.x25519Dh(ephemeralKeyPair.privateKey, bobPubKey);
674
- // Step 5: Derive encryption key from shared secret
675
- const encryptionKey = this.crypto.deriveKey(sharedSecret, toBytes("tinycloud-x25519"), toBytes("vault-grant"));
676
- // Step 6: Encrypt entry key with derived key
677
- const encryptedGrant = this.crypto.encrypt(encryptionKey, entryKey);
678
- // Step 7: Concatenate ephemeral public key + encrypted grant
679
- const grantBlob = concatBytes(ephemeralKeyPair.publicKey, encryptedGrant);
680
- // Step 8: Store grant in key space
681
- const grantPayload = JSON.stringify({
682
- grant: base64Encode(grantBlob),
683
- spaceId: this.vaultConfig.spaceId,
684
- metadata: {
685
- [VaultHeaders.GRANT_VERSION]: "1",
686
- [VaultHeaders.GRANTOR]: this.tc.did,
687
- ...(options?.metadata ?? {}),
688
- },
689
- });
690
- // Store grant in the vault's space (main space)
691
- const grantPutResult = await this.tc.kv.put(`grants/${recipientDID}/${key}`, grantPayload);
692
- if (!grantPutResult.ok) {
693
- return vaultError({
694
- code: "STORAGE_ERROR",
695
- cause: new Error(`Failed to store grant: ${grantPutResult.error.message}`),
696
- });
697
- }
698
- return ok(undefined);
699
- }
700
- catch (error) {
701
- return vaultError({
702
- code: "STORAGE_ERROR",
703
- cause: toError(error),
704
- });
705
- }
706
- });
707
- }
708
- /**
709
- * @deprecated Use reencrypt() instead.
710
- */
711
- async grant(key, recipientDID, options) {
712
- return this.reencrypt(key, recipientDID, options);
713
- }
714
- /**
715
- * Retrieve and decrypt a value shared by another user.
716
- *
717
- * @param grantorDID - The DID of the user who shared the data
718
- * @param key - The key that was shared
719
- * @param options - Optional get configuration
720
- * @returns Result with the decrypted entry
721
- */
722
- async getShared(grantorDID, key, options) {
723
- return this.withTelemetry("getShared", key, async () => {
724
- if (!this._isUnlocked ||
725
- !this.masterKey ||
726
- !this.encryptionIdentity) {
727
- return vaultError({
728
- code: "VAULT_LOCKED",
729
- message: "Vault must be unlocked before reading shared data",
730
- });
731
- }
732
- if (!this.requireAuth()) {
733
- return vaultError({
734
- code: "VAULT_LOCKED",
735
- message: "Authentication required",
736
- });
737
- }
738
- try {
739
- const myDID = this.tc.did;
740
- const grantorKV = options?.kv;
741
- if (!grantorKV) {
742
- return vaultError({
743
- code: "STORAGE_ERROR",
744
- cause: new Error("getShared requires a delegated KV service via options.kv. " +
745
- "Use useDelegation() to get delegated access, then pass { kv: access.kv }."),
746
- });
747
- }
748
- // Step 1: Fetch grant from grantor's space via delegated KV
749
- const grantResult = await grantorKV.get(`grants/${myDID}/${key}`, {
750
- raw: true,
751
- });
752
- if (!grantResult.ok) {
753
- return vaultError({
754
- code: "GRANT_NOT_FOUND",
755
- grantor: grantorDID,
756
- key,
757
- });
758
- }
759
- const grantEnvelope = typeof grantResult.data?.data === "string"
760
- ? JSON.parse(grantResult.data.data)
761
- : grantResult.data?.data;
762
- const grantBlobBytes = base64Decode(grantEnvelope.grant);
763
- // Step 2: Extract ephemeral public key and encrypted grant
764
- const ephemeralPubKey = grantBlobBytes.slice(0, 32);
765
- const encryptedGrant = grantBlobBytes.slice(32);
766
- // Step 3: Compute shared secret using our private key
767
- const sharedSecret = this.crypto.x25519Dh(this.encryptionIdentity.privateKey, ephemeralPubKey);
768
- // Step 4: Derive decryption key
769
- const encryptionKey = this.crypto.deriveKey(sharedSecret, toBytes("tinycloud-x25519"), toBytes("vault-grant"));
770
- // Step 5: Decrypt entry key
771
- const entryKey = this.crypto.decrypt(encryptionKey, encryptedGrant);
772
- // Step 6: Fetch encrypted value from grantor's space via delegated KV
773
- const valueResult = await grantorKV.get(`vault/${key}`, {
774
- raw: true,
775
- });
776
- if (!valueResult.ok) {
777
- return vaultError({
778
- code: "KEY_NOT_FOUND",
779
- key,
780
- });
781
- }
782
- const valueEnvelope = typeof valueResult.data?.data === "string"
783
- ? JSON.parse(valueResult.data.data)
784
- : valueResult.data?.data;
785
- const encryptedBytes = base64Decode(valueEnvelope.data);
786
- const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
787
- // Read metadata
788
- const metadata = valueEnvelope.metadata ?? {};
789
- const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
790
- const keyId = metadata[VaultHeaders.KEY_ID] ?? "";
791
- // Deserialize
792
- let value;
793
- if (options?.raw) {
794
- value = plaintext;
795
- }
796
- else if (options?.deserialize) {
797
- value = options.deserialize(plaintext);
798
- }
799
- else if (contentType === "application/json") {
800
- value = JSON.parse(fromBytes(plaintext));
801
- }
802
- else {
803
- value = plaintext;
804
- }
805
- return ok({ value, metadata, keyId });
806
- }
807
- catch (error) {
808
- if (error instanceof Error &&
809
- error.message.includes("decryption")) {
810
- return vaultError({
811
- code: "DECRYPTION_FAILED",
812
- message: error.message,
813
- });
814
- }
815
- return vaultError({
816
- code: "STORAGE_ERROR",
817
- cause: toError(error),
818
- });
819
- }
820
- });
821
- }
822
- /**
823
- * Resolve another user's public encryption key from their DID.
824
- *
825
- * @param did - The DID to resolve (did:pkh:eip155:{chainId}:{address})
826
- * @returns Result with the public key bytes
827
- */
828
- async resolvePublicKey(did) {
829
- try {
830
- const parts = this.parseDID(did);
831
- if (!parts) {
832
- return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
833
- }
834
- const spaceId = this.tc.makePublicSpaceId(parts.address, parts.chainId);
835
- const result = await this.tc.readPublicSpace(this.host, spaceId, ".well-known/vault-pubkey");
836
- if (!result.ok) {
837
- return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
838
- }
839
- const pubKeyBytes = base64Decode(result.data);
840
- return { ok: true, data: pubKeyBytes };
841
- }
842
- catch (error) {
843
- return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
844
- }
845
- }
846
- /**
847
- * List DIDs that have been granted access to a key.
848
- *
849
- * @param key - The key to list grants for
850
- * @returns Result with array of recipient DIDs
851
- */
852
- async listGrants(key) {
853
- return this.withTelemetry("listGrants", key, async () => {
854
- if (!this._isUnlocked) {
855
- return vaultError({
856
- code: "VAULT_LOCKED",
857
- message: "Vault must be unlocked before listing grants",
858
- });
859
- }
860
- if (!this.requireAuth()) {
861
- return vaultError({
862
- code: "VAULT_LOCKED",
863
- message: "Authentication required",
864
- });
865
- }
866
- try {
867
- const listResult = await this.tc.kv.list({
868
- prefix: "grants/",
869
- removePrefix: true,
870
- });
871
- if (!listResult.ok) {
872
- return vaultError({
873
- code: "STORAGE_ERROR",
874
- cause: new Error(`Failed to list grants: ${listResult.error.message}`),
875
- });
876
- }
877
- // Grant paths are: {recipientDID}/{key}
878
- // Filter for the specific key and extract DIDs
879
- const dids = [];
880
- for (const grantPath of listResult.data.keys) {
881
- // Path format: {recipientDID}/{key}
882
- // The key may contain slashes, so we need to match the suffix
883
- if (grantPath.endsWith(`/${key}`)) {
884
- const did = grantPath.slice(0, grantPath.length - key.length - 1);
885
- if (did) {
886
- dids.push(did);
887
- }
888
- }
889
- }
890
- return ok(dids);
891
- }
892
- catch (error) {
893
- return vaultError({
894
- code: "STORAGE_ERROR",
895
- cause: toError(error),
896
- });
897
- }
898
- });
899
- }
900
- // =========================================================================
901
- // Phase 3: Key Rotation / Revocation
902
- // =========================================================================
903
- /**
904
- * Revoke a previously issued grant.
905
- *
906
- * This performs a full key rotation:
907
- * 1. Lists current grantees
908
- * 2. Removes the revoked recipient
909
- * 3. Re-encrypts the value with a new entry key
910
- * 4. Re-issues grants to remaining recipients
911
- *
912
- * @param key - The key to revoke access to
913
- * @param recipientDID - The recipient whose access to revoke
914
- */
915
- async revoke(key, recipientDID) {
916
- return this.withTelemetry("revoke", key, async () => {
917
- if (!this._isUnlocked || !this.masterKey) {
918
- return vaultError({
919
- code: "VAULT_LOCKED",
920
- message: "Vault must be unlocked before revoking access",
921
- });
922
- }
923
- if (!this.requireAuth()) {
924
- return vaultError({
925
- code: "VAULT_LOCKED",
926
- message: "Authentication required",
927
- });
928
- }
929
- try {
930
- // Step 1: List all current grantees
931
- const granteesResult = await this.listGrants(key);
932
- if (!granteesResult.ok) {
933
- return granteesResult;
934
- }
935
- const remainingGrantees = granteesResult.data.filter((did) => did !== recipientDID);
936
- // Step 2: Delete the grant for the revoked recipient
937
- const deleteGrantResult = await this.tc.kv.delete(`grants/${recipientDID}/${key}`);
938
- // Grant may already be deleted, that's fine
939
- // Step 3: Fetch and decrypt current value
940
- const getResult = await this.get(key);
941
- if (!getResult.ok) {
942
- return getResult;
943
- }
944
- const currentEntry = getResult.data;
945
- // Step 4: Generate new entry key
946
- const newEntryKey = this.crypto.randomBytes(32);
947
- const newKeyId = hexEncode(this.crypto.sha256(newEntryKey)).slice(0, 16);
948
- // Step 5: Re-serialize and re-encrypt value with new key
949
- let plaintext;
950
- if (currentEntry.value instanceof Uint8Array) {
951
- plaintext = currentEntry.value;
952
- }
953
- else {
954
- plaintext = toBytes(JSON.stringify(currentEntry.value));
955
- }
956
- const encrypted = this.crypto.encrypt(newEntryKey, plaintext);
957
- // Step 6: Encrypt new entry key with master key
958
- const newKeyBlob = this.crypto.encrypt(this.masterKey, newEntryKey);
959
- // Step 7: Update metadata with new key ID
960
- const metadata = {
961
- ...currentEntry.metadata,
962
- [VaultHeaders.KEY_ID]: newKeyId,
963
- };
964
- // Step 8: Store updated key blob
965
- const keyPayload = JSON.stringify({
966
- key: base64Encode(newKeyBlob),
967
- metadata: JSON.stringify({
968
- keyId: newKeyId,
969
- ...metadata,
970
- }),
971
- });
972
- const keyPutResult = await this.tc.kv.put(`keys/${key}`, keyPayload);
973
- if (!keyPutResult.ok) {
974
- return vaultError({
975
- code: "STORAGE_ERROR",
976
- cause: new Error(`Failed to store rotated key blob: ${keyPutResult.error.message}`),
977
- });
978
- }
979
- // Step 9: Store re-encrypted value
980
- const valuePayload = JSON.stringify({
981
- data: base64Encode(encrypted),
982
- metadata,
983
- });
984
- const valuePutResult = await this.tc.kv.put(`vault/${key}`, valuePayload);
985
- if (!valuePutResult.ok) {
986
- return vaultError({
987
- code: "STORAGE_ERROR",
988
- cause: new Error(`Failed to store re-encrypted value: ${valuePutResult.error.message}`),
989
- });
990
- }
991
- // Step 10: Re-issue grants to remaining recipients
992
- for (const did of remainingGrantees) {
993
- const grantResult = await this.reencrypt(key, did);
994
- if (!grantResult.ok) {
995
- // Continue re-issuing to other recipients even if one fails
996
- // The failed grant can be re-issued manually
997
- }
998
- }
999
- return ok(undefined);
1000
- }
1001
- catch (error) {
1002
- return vaultError({
1003
- code: "STORAGE_ERROR",
1004
- cause: toError(error),
1005
- });
1006
- }
1007
- });
1008
- }
1009
- // =========================================================================
1010
- // Internal Helpers
1011
- // =========================================================================
1012
- /**
1013
- * Parse a DID string to extract address and chainId.
1014
- * Expected format: did:pkh:eip155:{chainId}:{address}
1015
- *
1016
- * @param did - The DID to parse
1017
- * @returns Parsed address and chainId, or null if invalid
1018
- */
1019
- parseDID(did) {
1020
- // did:pkh:eip155:{chainId}:{address}
1021
- const parts = did.split(":");
1022
- if (parts.length !== 5 ||
1023
- parts[0] !== "did" ||
1024
- parts[1] !== "pkh" ||
1025
- parts[2] !== "eip155") {
1026
- return null;
1027
- }
1028
- const chainId = parseInt(parts[3], 10);
1029
- const address = parts[4];
1030
- if (isNaN(chainId) || !address) {
1031
- return null;
1032
- }
1033
- return { address, chainId };
1034
- }
1035
- }
1036
- /**
1037
- * Service identifier for registration.
1038
- */
1039
- DataVaultService.serviceName = "vault";
1040
- //# sourceMappingURL=DataVaultService.js.map