@yallet/rwa-sdk 0.2.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 (58) hide show
  1. package/README.md +365 -0
  2. package/dist/credential-nft-server.d.ts +75 -0
  3. package/dist/credential-nft-server.d.ts.map +1 -0
  4. package/dist/credential-nft-server.js +122 -0
  5. package/dist/credential-nft-server.js.map +1 -0
  6. package/dist/credential-nft.d.ts +31 -0
  7. package/dist/credential-nft.d.ts.map +1 -0
  8. package/dist/credential-nft.js +38 -0
  9. package/dist/credential-nft.js.map +1 -0
  10. package/dist/encryption.d.ts +107 -0
  11. package/dist/encryption.d.ts.map +1 -0
  12. package/dist/encryption.js +176 -0
  13. package/dist/encryption.js.map +1 -0
  14. package/dist/index.d.ts +56 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +66 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/metadata.d.ts +161 -0
  19. package/dist/metadata.d.ts.map +1 -0
  20. package/dist/metadata.js +180 -0
  21. package/dist/metadata.js.map +1 -0
  22. package/dist/minting.d.ts +63 -0
  23. package/dist/minting.d.ts.map +1 -0
  24. package/dist/minting.js +194 -0
  25. package/dist/minting.js.map +1 -0
  26. package/dist/payloads.d.ts +120 -0
  27. package/dist/payloads.d.ts.map +1 -0
  28. package/dist/payloads.js +118 -0
  29. package/dist/payloads.js.map +1 -0
  30. package/dist/registry.d.ts +77 -0
  31. package/dist/registry.d.ts.map +1 -0
  32. package/dist/registry.js +173 -0
  33. package/dist/registry.js.map +1 -0
  34. package/dist/sdk.d.ts +158 -0
  35. package/dist/sdk.d.ts.map +1 -0
  36. package/dist/sdk.js +346 -0
  37. package/dist/sdk.js.map +1 -0
  38. package/dist/storage.d.ts +80 -0
  39. package/dist/storage.d.ts.map +1 -0
  40. package/dist/storage.js +284 -0
  41. package/dist/storage.js.map +1 -0
  42. package/dist/types.d.ts +278 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +19 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +64 -0
  47. package/src/credential-nft-server.ts +201 -0
  48. package/src/credential-nft.ts +64 -0
  49. package/src/encryption.ts +308 -0
  50. package/src/index.ts +151 -0
  51. package/src/metadata.ts +336 -0
  52. package/src/minting.ts +266 -0
  53. package/src/payloads.ts +238 -0
  54. package/src/registry.ts +236 -0
  55. package/src/sdk.ts +507 -0
  56. package/src/storage.ts +364 -0
  57. package/src/types.ts +318 -0
  58. package/wasm/README.md +33 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Yault credential NFT — server-side mint flow.
3
+ *
4
+ * Encrypts credential payload locally with recipient's xidentity, then POSTs
5
+ * to upload-and-mint API. No registry, signer, or client-side mint needed.
6
+ */
7
+
8
+ import { AssetType } from './types.js';
9
+ import { ECIESEncryptor } from './encryption.js';
10
+ import { encryptAsset } from './encryption.js';
11
+
12
+ /** Payload shape agreed with Yault webapp */
13
+ export interface CredentialNftPayload {
14
+ user_cred: string;
15
+ mnemonic?: string | null;
16
+ index: number;
17
+ label: string;
18
+ /** Optional memo (e.g. "letter to future heirs") — encrypted together with credential, delivered when release triggers. */
19
+ memo?: string | null;
20
+ }
21
+
22
+ /** Result shape expected by Yault */
23
+ export interface CredentialMintResult {
24
+ success: boolean;
25
+ txId?: string;
26
+ error?: string;
27
+ }
28
+
29
+ /** Options for server-side mint */
30
+ export interface MintCredentialNftViaServerOptions {
31
+ /** Recipient's xidentity (base64). Required. */
32
+ xidentity: string;
33
+ /** Upload-and-mint API URL. If omitted, uses built-in prod/dev endpoint. */
34
+ uploadAndMintApiUrl?: string;
35
+ /** When true, uses built-in dev endpoint (if uploadAndMintApiUrl not set). */
36
+ dev?: boolean;
37
+ /** Optional: custom headers (e.g. Authorization) for the API request */
38
+ headers?: Record<string, string>;
39
+ /** Network for mint (mainnet | devnet). Default: mainnet */
40
+ network?: 'mainnet' | 'devnet';
41
+ }
42
+
43
+ /** Options for preparing payload only (no POST). Same as MintCredentialNftViaServerOptions minus upload/headers. */
44
+ export interface PrepareCredentialNftPayloadOptions {
45
+ /** Recipient's xidentity (base64). Required. */
46
+ xidentity: string;
47
+ /** Network for mint (mainnet | devnet). Default: mainnet */
48
+ network?: 'mainnet' | 'devnet';
49
+ }
50
+
51
+ /** Result of prepareCredentialNftPayload: the exact body to POST to upload-and-mint later. */
52
+ export interface PreparedCredentialNftPayload {
53
+ /** Request body to POST to upload-and-mint API (data, contentType, tags, leafOwner, name, description, network). */
54
+ body: Record<string, unknown>;
55
+ /** Recipient Solana address (leafOwner). */
56
+ recipientSolanaAddress: string;
57
+ }
58
+
59
+ /** Built-in endpoints for upload-and-mint API */
60
+ export const RWA_UPLOAD_AND_MINT_ENDPOINTS = {
61
+ prod: 'https://api.yallet.xyz/api/v1/storage/rwa/upload-and-mint',
62
+ dev: 'https://api.yallet.xyz/api/v1/storage/rwa/upload-and-mint',
63
+ } as const;
64
+
65
+ const NOTE_TITLE = 'Yault Release Credentials';
66
+
67
+ function bytesToBase64(bytes: Uint8Array): string {
68
+ let binary = '';
69
+ for (let i = 0; i < bytes.length; i++) {
70
+ binary += String.fromCharCode(bytes[i]);
71
+ }
72
+ return btoa(binary);
73
+ }
74
+
75
+ /**
76
+ * Build the same upload-and-mint request body as mintCredentialNftViaServer, without sending.
77
+ * Used to store the encrypted note server-side (e.g. Oracle binding) and send only after attestation.
78
+ *
79
+ * @param recipientSolanaAddress - Recipient's Solana address (leafOwner)
80
+ * @param payload - { user_cred, mnemonic?, index, label }
81
+ * @param options - { xidentity, network? }
82
+ * @returns { body, recipientSolanaAddress } — body is the exact JSON to POST to upload-and-mint later
83
+ */
84
+ export async function prepareCredentialNftPayload(
85
+ recipientSolanaAddress: string,
86
+ payload: CredentialNftPayload,
87
+ options: PrepareCredentialNftPayloadOptions
88
+ ): Promise<PreparedCredentialNftPayload> {
89
+ const { xidentity, network = 'mainnet' } = options;
90
+
91
+ if (!xidentity || !xidentity.trim()) {
92
+ throw new Error('xidentity is required');
93
+ }
94
+
95
+ const note = {
96
+ title: NOTE_TITLE,
97
+ content: JSON.stringify(payload),
98
+ metadata: { createdAt: new Date().toISOString() },
99
+ };
100
+
101
+ const name = `${NOTE_TITLE} — ${payload.label}`;
102
+ const description = `Path index ${payload.index}. Decrypt in Yallet to view credentials.`;
103
+
104
+ const encryptor = new ECIESEncryptor();
105
+ const encrypted = await encryptAsset(note, AssetType.NOTE, xidentity.trim(), encryptor);
106
+
107
+ const payloadForStorage = {
108
+ encrypted: {
109
+ ephemeral_pub: encrypted.ephemeral_pub,
110
+ encrypted_aes_key: encrypted.encrypted_aes_key,
111
+ iv: encrypted.iv,
112
+ encrypted_data: encrypted.encrypted_data,
113
+ },
114
+ metadata: {
115
+ assetType: AssetType.NOTE,
116
+ direction: 'received' as const,
117
+ network,
118
+ version: encrypted.version,
119
+ uuid: encrypted.uuid,
120
+ timestamp: encrypted.timestamp,
121
+ dataSize: encrypted.dataSize,
122
+ uploadedAt: Date.now(),
123
+ },
124
+ };
125
+
126
+ const jsonString = JSON.stringify(payloadForStorage);
127
+ const dataBase64 = bytesToBase64(new TextEncoder().encode(jsonString));
128
+
129
+ const body: Record<string, unknown> = {
130
+ data: dataBase64,
131
+ contentType: 'application/json',
132
+ tags: { 'Yallet-Type': 'encrypted-asset', 'Asset-Type': AssetType.NOTE, 'Direction': 'received' },
133
+ leafOwner: recipientSolanaAddress,
134
+ name,
135
+ description,
136
+ network,
137
+ };
138
+
139
+ return { body, recipientSolanaAddress };
140
+ }
141
+
142
+ /**
143
+ * Mint credential payload via server (upload-and-mint API).
144
+ *
145
+ * Caller provides xidentity from recipient-addresses. No registry or signer.
146
+ *
147
+ * @param recipientSolanaAddress - Recipient's Solana address (leafOwner)
148
+ * @param payload - { user_cred, mnemonic?, index, label }
149
+ * @param options - { xidentity, uploadAndMintApiUrl?, dev?, headers?, network? }
150
+ * @returns { success, txId?, error? }
151
+ */
152
+ export async function mintCredentialNftViaServer(
153
+ recipientSolanaAddress: string,
154
+ payload: CredentialNftPayload,
155
+ options: MintCredentialNftViaServerOptions
156
+ ): Promise<CredentialMintResult> {
157
+ const { xidentity, uploadAndMintApiUrl, dev = false, headers = {}, network = 'mainnet' } = options;
158
+
159
+ if (!xidentity || !xidentity.trim()) {
160
+ return { success: false, error: 'xidentity is required' };
161
+ }
162
+
163
+ const apiUrl =
164
+ uploadAndMintApiUrl ||
165
+ (dev ? RWA_UPLOAD_AND_MINT_ENDPOINTS.dev : RWA_UPLOAD_AND_MINT_ENDPOINTS.prod);
166
+
167
+ const { body } = await prepareCredentialNftPayload(recipientSolanaAddress, payload, { xidentity, network });
168
+
169
+ try {
170
+ const response = await fetch(apiUrl, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'Content-Type': 'application/json',
174
+ ...headers,
175
+ },
176
+ body: JSON.stringify(body),
177
+ });
178
+
179
+ const result = (await response.json().catch(() => ({}))) as {
180
+ success?: boolean;
181
+ mint?: { signature?: string };
182
+ error?: string;
183
+ };
184
+
185
+ if (!response.ok) {
186
+ return {
187
+ success: false,
188
+ error: result?.error || response.statusText || 'Upload-and-mint failed',
189
+ };
190
+ }
191
+
192
+ const signature = result?.mint?.signature;
193
+ return {
194
+ success: true,
195
+ txId: signature,
196
+ };
197
+ } catch (err) {
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ return { success: false, error: message };
200
+ }
201
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Yault credential NFT — mint release credentials (user_cred + optional mnemonic)
3
+ * as an encrypted message cNFT to the recipient.
4
+ *
5
+ * Used by Yault webapp after Plan submit: custody WASM generates credentials,
6
+ * this function mints them to the recipient's Solana address via RWA SDK mintMessage.
7
+ */
8
+
9
+ import type { YalletRWASDK } from './sdk.js';
10
+ import type { MintResult } from './types.js';
11
+
12
+ /** Payload shape agreed with Yault webapp (mnemonic optional until path design includes it). */
13
+ export interface CredentialNftPayload {
14
+ user_cred: string;
15
+ mnemonic?: string | null;
16
+ index: number;
17
+ label: string;
18
+ }
19
+
20
+ /** Result shape expected by Yault: success, txId (assetId or signature), error. */
21
+ export interface CredentialMintResult {
22
+ success: boolean;
23
+ txId?: string;
24
+ error?: string;
25
+ }
26
+
27
+ const NOTE_TITLE = 'Yault Release Credentials';
28
+
29
+ /**
30
+ * Mint credential payload as an encrypted note cNFT to the recipient.
31
+ *
32
+ * @param sdk - Initialized YalletRWASDK (config, signer, registry set)
33
+ * @param recipientSolanaAddress - Recipient's Solana address (must be registered in SDK registry with xidentity)
34
+ * @param payload - { user_cred, mnemonic?, index, label }
35
+ * @returns { success, txId?, error? } for Yault UI
36
+ */
37
+ export async function mintCredentialNft(
38
+ sdk: YalletRWASDK,
39
+ recipientSolanaAddress: string,
40
+ payload: CredentialNftPayload
41
+ ): Promise<CredentialMintResult> {
42
+ const note = {
43
+ title: NOTE_TITLE,
44
+ content: JSON.stringify(payload),
45
+ metadata: { createdAt: new Date().toISOString() },
46
+ };
47
+
48
+ const result: MintResult = await sdk.mintMessage(note, recipientSolanaAddress, {
49
+ name: `${NOTE_TITLE} — ${payload.label}`,
50
+ description: `Path index ${payload.index}. Decrypt in Yallet to view credentials.`,
51
+ });
52
+
53
+ if (result.success) {
54
+ return {
55
+ success: true,
56
+ txId: result.assetId ?? result.signature ?? undefined,
57
+ };
58
+ }
59
+
60
+ return {
61
+ success: false,
62
+ error: result.error ?? 'Mint failed',
63
+ };
64
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Yallet RWA SDK - Encryption Module
3
+ *
4
+ * Provides ECIES encryption for RWA assets using xidentity public keys.
5
+ * Compatible with the Yallet WASM encryption module.
6
+ */
7
+
8
+ import type {
9
+ AssetType,
10
+ AssetEnvelope,
11
+ EncryptedPayload,
12
+ EncryptFunction,
13
+ } from './types.js';
14
+
15
+ // ======================================
16
+ // Pure JavaScript ECIES Implementation
17
+ // ======================================
18
+
19
+ /**
20
+ * ECIES encryption using Web Crypto API.
21
+ * Must match dev.yallet.chrome-extension (acegf WASM) so the extension can decrypt:
22
+ * dh_key = SHA256(shared_secret), encrypted_aes_key = AES-GCM(dh_key).encrypt(iv, aes_key), encrypted_data = AES-GCM(aes_key).encrypt(iv, plaintext).
23
+ */
24
+ export class ECIESEncryptor {
25
+ /**
26
+ * Encrypt data for a recipient's xidentity public key.
27
+ * Scheme (same as acegf encrypt): ECDH → SHA256(dh) = master_key → AES-GCM(master_key).encrypt(iv, random_aes_key) → AES-GCM(aes_key).encrypt(iv, plaintext).
28
+ *
29
+ * @param recipientXidentityB64 - Recipient's X25519 public key (base64)
30
+ * @param plaintext - Data to encrypt
31
+ * @returns Encrypted payload
32
+ */
33
+ async encrypt(
34
+ recipientXidentityB64: string,
35
+ plaintext: Uint8Array
36
+ ): Promise<EncryptedPayload> {
37
+ const recipientPubKey = base64ToBytes(recipientXidentityB64);
38
+
39
+ const ephemeralKeyPair = (await crypto.subtle.generateKey(
40
+ { name: 'X25519' },
41
+ true,
42
+ ['deriveBits']
43
+ )) as CryptoKeyPair;
44
+
45
+ const ephemeralPubKeyRaw = await crypto.subtle.exportKey(
46
+ 'raw',
47
+ ephemeralKeyPair.publicKey
48
+ );
49
+ const ephemeralPubKey = new Uint8Array(ephemeralPubKeyRaw);
50
+
51
+ const recipientKey = await crypto.subtle.importKey(
52
+ 'raw',
53
+ recipientPubKey as BufferSource,
54
+ { name: 'X25519' },
55
+ false,
56
+ []
57
+ );
58
+
59
+ const sharedSecretRaw = await crypto.subtle.deriveBits(
60
+ { name: 'X25519', public: recipientKey },
61
+ ephemeralKeyPair.privateKey,
62
+ 256
63
+ );
64
+ const sharedSecret = new Uint8Array(sharedSecretRaw);
65
+
66
+ // Match extension WASM: dh_key = SHA256(shared_secret), then AES-GCM(dh_key) encrypts the random AES key
67
+ const dhKeyHash = await crypto.subtle.digest('SHA-256', sharedSecret);
68
+ const dhKey = new Uint8Array(dhKeyHash);
69
+
70
+ const masterKey = await crypto.subtle.importKey(
71
+ 'raw',
72
+ dhKey,
73
+ { name: 'AES-GCM' },
74
+ false,
75
+ ['encrypt']
76
+ );
77
+
78
+ const aesKeyBytes = crypto.getRandomValues(new Uint8Array(32));
79
+ const aesKey = await crypto.subtle.importKey(
80
+ 'raw',
81
+ aesKeyBytes,
82
+ { name: 'AES-GCM' },
83
+ false,
84
+ ['encrypt']
85
+ );
86
+
87
+ const iv = crypto.getRandomValues(new Uint8Array(12));
88
+
89
+ const encryptedAesKeyRaw = await crypto.subtle.encrypt(
90
+ { name: 'AES-GCM', iv },
91
+ masterKey,
92
+ aesKeyBytes
93
+ );
94
+ const encryptedAesKey = new Uint8Array(encryptedAesKeyRaw);
95
+
96
+ const encryptedDataRaw = await crypto.subtle.encrypt(
97
+ { name: 'AES-GCM', iv },
98
+ aesKey,
99
+ plaintext as BufferSource
100
+ );
101
+ const encryptedData = new Uint8Array(encryptedDataRaw);
102
+
103
+ return {
104
+ ephemeral_pub: bytesToBase64(ephemeralPubKey),
105
+ encrypted_aes_key: bytesToBase64(encryptedAesKey),
106
+ iv: bytesToBase64(iv),
107
+ encrypted_data: bytesToBase64(encryptedData),
108
+ };
109
+ }
110
+ }
111
+
112
+ // ======================================
113
+ // WASM-Compatible Encryption Wrapper
114
+ // ======================================
115
+
116
+ /**
117
+ * Wrapper for WASM encrypt function
118
+ *
119
+ * Use this when you have access to the Yallet WASM module
120
+ */
121
+ export function createWasmEncryptor(wasmEncryptFn: EncryptFunction) {
122
+ return {
123
+ encrypt(
124
+ recipientXidentityB64: string,
125
+ plaintext: Uint8Array
126
+ ): EncryptedPayload {
127
+ const result = wasmEncryptFn(recipientXidentityB64, plaintext);
128
+
129
+ if (result.startsWith('error:')) {
130
+ throw new Error(`Encryption failed: ${result.substring(6)}`);
131
+ }
132
+
133
+ return JSON.parse(result) as EncryptedPayload;
134
+ },
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Initialize acegf WASM and return the encrypt function for createRWASDK.
140
+ * Pass the default export of acegf.js (init) and optionally the wasm path/buffer.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * import init from './wasm/acegf.js';
145
+ * import { createAcegfEncryptor, createRWASDK } from '@yallet/rwa-sdk';
146
+ * const wasmEncryptFn = await createAcegfEncryptor(init, '/path/to/acegf_bg.wasm');
147
+ * const sdk = createRWASDK(config, { wasmEncryptFn });
148
+ * ```
149
+ */
150
+ export async function createAcegfEncryptor(
151
+ initFn: (
152
+ moduleOrPath?: string | URL | ArrayBuffer | object
153
+ ) => Promise<{ acegf_encrypt_for_xidentity: EncryptFunction }>,
154
+ wasmModuleOrPath?: string | URL | ArrayBuffer | object
155
+ ): Promise<EncryptFunction> {
156
+ const acegf = await initFn(wasmModuleOrPath);
157
+ return acegf.acegf_encrypt_for_xidentity;
158
+ }
159
+
160
+ // ======================================
161
+ // Asset Preparation
162
+ // ======================================
163
+
164
+ /**
165
+ * Generate a UUID v4
166
+ */
167
+ function generateUUID(): string {
168
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
169
+ return crypto.randomUUID();
170
+ }
171
+ // Fallback for environments without crypto.randomUUID
172
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
173
+ const r = (Math.random() * 16) | 0;
174
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
175
+ return v.toString(16);
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Options for preparing asset data
181
+ */
182
+ export interface PrepareAssetOptions {
183
+ /** Custom UUID (auto-generated if not provided) */
184
+ uuid?: string;
185
+ /** Custom timestamp (current time if not provided) */
186
+ timestamp?: number;
187
+ /** Schema identifier */
188
+ schema?: string;
189
+ /** Previous token for version chain */
190
+ previousToken?: string;
191
+ }
192
+
193
+ /**
194
+ * Prepare asset data for encryption
195
+ *
196
+ * @param asset - Asset data object
197
+ * @param assetType - Type of asset
198
+ * @param options - Optional configuration
199
+ * @returns Object containing serialized data and metadata
200
+ */
201
+ export function prepareAssetData(
202
+ asset: unknown,
203
+ assetType: AssetType,
204
+ options: PrepareAssetOptions = {}
205
+ ): { data: Uint8Array; uuid: string; timestamp: number } {
206
+ const uuid = options.uuid || generateUUID();
207
+ const timestamp = options.timestamp || Date.now();
208
+
209
+ const envelope: AssetEnvelope = {
210
+ version: 1,
211
+ type: assetType,
212
+ timestamp,
213
+ uuid,
214
+ schema: options.schema,
215
+ previousToken: options.previousToken,
216
+ data: asset,
217
+ };
218
+
219
+ const jsonString = JSON.stringify(envelope);
220
+ return {
221
+ data: new TextEncoder().encode(jsonString),
222
+ uuid,
223
+ timestamp,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Options for encrypting an asset
229
+ */
230
+ export interface EncryptAssetOptions {
231
+ /** Custom UUID (auto-generated if not provided) */
232
+ uuid?: string;
233
+ /** Custom timestamp (current time if not provided) */
234
+ timestamp?: number;
235
+ /** Schema identifier */
236
+ schema?: string;
237
+ /** Previous token for version chain */
238
+ previousToken?: string;
239
+ }
240
+
241
+ /**
242
+ * Result of encrypting an asset
243
+ */
244
+ export interface EncryptedAssetResult extends EncryptedPayload {
245
+ assetType: AssetType;
246
+ version: number;
247
+ uuid: string;
248
+ timestamp: number;
249
+ dataSize: number;
250
+ }
251
+
252
+ /**
253
+ * Encrypt an asset for a recipient
254
+ *
255
+ * @param asset - Asset data
256
+ * @param assetType - Type of asset
257
+ * @param recipientXidentity - Recipient's xidentity (base64)
258
+ * @param encryptor - Encryptor instance (WASM or JS)
259
+ * @param options - Optional configuration
260
+ * @returns Encrypted payload with metadata
261
+ */
262
+ export async function encryptAsset(
263
+ asset: unknown,
264
+ assetType: AssetType,
265
+ recipientXidentity: string,
266
+ encryptor: ECIESEncryptor | ReturnType<typeof createWasmEncryptor>,
267
+ options: EncryptAssetOptions = {}
268
+ ): Promise<EncryptedAssetResult> {
269
+ const prepared = prepareAssetData(asset, assetType, options);
270
+
271
+ let encrypted: EncryptedPayload;
272
+
273
+ if (encryptor instanceof ECIESEncryptor) {
274
+ encrypted = await encryptor.encrypt(recipientXidentity, prepared.data);
275
+ } else {
276
+ encrypted = encryptor.encrypt(recipientXidentity, prepared.data);
277
+ }
278
+
279
+ return {
280
+ ...encrypted,
281
+ assetType,
282
+ version: 1,
283
+ uuid: prepared.uuid,
284
+ timestamp: prepared.timestamp,
285
+ dataSize: prepared.data.length,
286
+ };
287
+ }
288
+
289
+ // ======================================
290
+ // Utility Functions
291
+ // ======================================
292
+
293
+ function base64ToBytes(base64: string): Uint8Array {
294
+ const binaryString = atob(base64);
295
+ const bytes = new Uint8Array(binaryString.length);
296
+ for (let i = 0; i < binaryString.length; i++) {
297
+ bytes[i] = binaryString.charCodeAt(i);
298
+ }
299
+ return bytes;
300
+ }
301
+
302
+ function bytesToBase64(bytes: Uint8Array): string {
303
+ let binaryString = '';
304
+ for (let i = 0; i < bytes.length; i++) {
305
+ binaryString += String.fromCharCode(bytes[i]);
306
+ }
307
+ return btoa(binaryString);
308
+ }