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