@swal/vault 0.0.0-stage → 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.
@@ -0,0 +1,249 @@
1
+ // web/vault-client.ts — VaultClient para navegador (PWA). Solo WebCrypto + IndexedDB.
2
+ // Nunca realiza peticiones de red. Nada sale del dispositivo salvo lo que el llamador decida
3
+ // hacer con el share de recuperacion o con la meshKey.
4
+ //
5
+ // Modelo de claves:
6
+ // * identidad de dispositivo: ECDSA P-256, CryptoKey NO extraible guardada en IndexedDB.
7
+ // * KEK (AES-GCM, no extraible): derivada de passphrase (PBKDF2-SHA256) o de passkey PRF (HKDF).
8
+ // * meshKey (32 bytes): se guarda siempre envuelta (AES-GCM) con la KEK.
9
+ // * recuperacion: Shamir 2-de-3 sobre la meshKey; share 1 queda en el dispositivo,
10
+ // share 2 se muestra una sola vez al usuario, share 3 se devuelve para custodia opcional.
11
+ import { bs, fromBase64Url, randomBytes, sha256, toBase64Url, utf8 } from '../bytes.js';
12
+ import { reconstructDEK, splitDEK } from '../sealedPack.js';
13
+ export const MIN_PBKDF2_ITERATIONS = 310_000;
14
+ export const DEFAULT_PBKDF2_ITERATIONS = 600_000;
15
+ export class VaultError extends Error {
16
+ code;
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.code = code;
20
+ this.name = 'VaultError';
21
+ }
22
+ }
23
+ // ── IndexedDB mini wrapper ────────────────────────────────────────────────────
24
+ const STORE = 'kv';
25
+ function openDb(name) {
26
+ return new Promise((resolve, reject) => {
27
+ const req = indexedDB.open(name, 1);
28
+ req.onupgradeneeded = () => req.result.createObjectStore(STORE);
29
+ req.onsuccess = () => resolve(req.result);
30
+ req.onerror = () => reject(req.error);
31
+ });
32
+ }
33
+ function tx(db, mode, fn) {
34
+ return new Promise((resolve, reject) => {
35
+ const t = db.transaction(STORE, mode);
36
+ const r = fn(t.objectStore(STORE));
37
+ t.oncomplete = () => resolve(r.result);
38
+ t.onerror = () => reject(t.error);
39
+ t.onabort = () => reject(t.error);
40
+ });
41
+ }
42
+ // ── Primitivas ────────────────────────────────────────────────────────────────
43
+ const subtle = () => globalThis.crypto.subtle;
44
+ async function kekFromPassphrase(passphrase, salt, iterations) {
45
+ const base = await subtle().importKey('raw', bs(utf8(passphrase.normalize('NFKC'))), 'PBKDF2', false, ['deriveKey']);
46
+ return subtle().deriveKey({ name: 'PBKDF2', hash: 'SHA-256', salt: bs(salt), iterations }, base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
47
+ }
48
+ async function kekFromPrf(prf, salt) {
49
+ const base = await subtle().importKey('raw', bs(prf), 'HKDF', false, ['deriveKey']);
50
+ return subtle().deriveKey({ name: 'HKDF', hash: 'SHA-256', salt: bs(salt), info: bs(utf8('swal-vault/kek/v1')) }, base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
51
+ }
52
+ /** Formato: "v1.<iv b64url>.<ct+tag b64url>" */
53
+ async function wrapWith(raw, kek) {
54
+ const iv = randomBytes(12);
55
+ const ct = new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: bs(iv) }, kek, bs(raw)));
56
+ return `v1.${toBase64Url(iv)}.${toBase64Url(ct)}`;
57
+ }
58
+ async function unwrapWith(wrapped, kek) {
59
+ const parts = wrapped.split('.');
60
+ if (parts.length !== 3 || parts[0] !== 'v1')
61
+ throw new VaultError('BAD_FORMAT', 'formato de clave envuelta invalido');
62
+ try {
63
+ const pt = await subtle().decrypt({ name: 'AES-GCM', iv: bs(fromBase64Url(parts[1])) }, kek, bs(fromBase64Url(parts[2])));
64
+ return new Uint8Array(pt);
65
+ }
66
+ catch {
67
+ throw new VaultError('BAD_KEY', 'no se pudo desenvolver la clave (KEK incorrecta o datos alterados)');
68
+ }
69
+ }
70
+ function defaultPasskeyProvider(kv, rpId) {
71
+ if (typeof PublicKeyCredential === 'undefined' || !globalThis.navigator?.credentials)
72
+ return undefined;
73
+ return {
74
+ async getPrfOutput(salt) {
75
+ const cred = globalThis.navigator.credentials;
76
+ const idRaw = await kv.get('passkeyCredId');
77
+ const ext = { prf: { eval: { first: salt } } };
78
+ let credential;
79
+ if (!idRaw) {
80
+ credential = (await cred.create({
81
+ publicKey: {
82
+ rp: { name: 'SWAL Vault', ...(rpId ? { id: rpId } : {}) },
83
+ user: { id: bs(randomBytes(16)), name: 'swal-vault', displayName: 'SWAL Vault' },
84
+ challenge: bs(randomBytes(32)),
85
+ pubKeyCredParams: [{ type: 'public-key', alg: -7 }, { type: 'public-key', alg: -257 }],
86
+ authenticatorSelection: { residentKey: 'preferred', userVerification: 'required' },
87
+ extensions: { prf: {} },
88
+ },
89
+ }));
90
+ if (!credential)
91
+ throw new VaultError('NO_PASSKEY', 'creacion de passkey cancelada');
92
+ if (!credential.getClientExtensionResults().prf?.enabled)
93
+ throw new VaultError('NO_PASSKEY', 'el autenticador no soporta PRF');
94
+ await kv.put('passkeyCredId', new Uint8Array(credential.rawId));
95
+ }
96
+ const id = (await kv.get('passkeyCredId'));
97
+ const asr = (await cred.get({
98
+ publicKey: {
99
+ challenge: bs(randomBytes(32)),
100
+ allowCredentials: [{ type: 'public-key', id: bs(id) }],
101
+ userVerification: 'required',
102
+ extensions: ext,
103
+ ...(rpId ? { rpId } : {}),
104
+ },
105
+ }));
106
+ const first = asr?.getClientExtensionResults()?.prf?.results?.first;
107
+ if (!first)
108
+ throw new VaultError('NO_PASSKEY', 'el passkey no devolvio salida PRF');
109
+ return new Uint8Array(first);
110
+ },
111
+ };
112
+ }
113
+ // ── Fabrica ───────────────────────────────────────────────────────────────────
114
+ export function createVaultClient(opts) {
115
+ if (opts.storage !== 'idb')
116
+ throw new VaultError('BAD_PARAMS', "solo storage: 'idb' esta soportado");
117
+ const iterations = opts.pbkdf2Iterations ?? DEFAULT_PBKDF2_ITERATIONS;
118
+ if (iterations < MIN_PBKDF2_ITERATIONS)
119
+ throw new VaultError('BAD_PARAMS', `pbkdf2Iterations debe ser >= ${MIN_PBKDF2_ITERATIONS}`);
120
+ const dbName = opts.dbName ?? 'swal-vault';
121
+ let dbp = null;
122
+ const db = () => (dbp ??= openDb(dbName));
123
+ const get = async (k) => (await tx(await db(), 'readonly', (s) => s.get(k)));
124
+ const put = async (k, v) => {
125
+ await tx(await db(), 'readwrite', (s) => s.put(v, k));
126
+ };
127
+ const passkey = opts.passkey ?? defaultPasskeyProvider({ get, put }, opts.rpId);
128
+ let kek = null;
129
+ const needKek = (k) => {
130
+ const v = k ?? kek;
131
+ if (!v)
132
+ throw new VaultError('LOCKED', 'vault bloqueado: llama unlock() primero');
133
+ return v;
134
+ };
135
+ async function salt() {
136
+ let s = await get('kekSalt');
137
+ if (!s) {
138
+ s = randomBytes(16);
139
+ await put('kekSalt', s);
140
+ }
141
+ return s;
142
+ }
143
+ async function deriveKek(o) {
144
+ const s = await salt();
145
+ if ('passkey' in o) {
146
+ if (!passkey)
147
+ throw new VaultError('NO_PASSKEY', 'passkeys no disponibles en este entorno');
148
+ return kekFromPrf(await passkey.getPrfOutput(s), s);
149
+ }
150
+ return kekFromPassphrase(o.passphrase, s, iterations);
151
+ }
152
+ async function toMeshKey(raw) {
153
+ const key = await subtle().importKey('raw', bs(raw), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
154
+ return { raw, key };
155
+ }
156
+ const self = {
157
+ async getDeviceIdentity() {
158
+ let pair = await get('deviceKeyPair');
159
+ if (!pair) {
160
+ pair = (await subtle().generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign', 'verify']));
161
+ await put('deviceKeyPair', pair);
162
+ }
163
+ const spki = new Uint8Array(await subtle().exportKey('spki', pair.publicKey));
164
+ const publicKeyJwk = await subtle().exportKey('jwk', pair.publicKey);
165
+ return {
166
+ deviceId: toBase64Url((await sha256(spki)).subarray(0, 16)),
167
+ publicKey: toBase64Url(spki),
168
+ publicKeyJwk,
169
+ };
170
+ },
171
+ async sign(data) {
172
+ await self.getDeviceIdentity();
173
+ const pair = (await get('deviceKeyPair'));
174
+ return new Uint8Array(await subtle().sign({ name: 'ECDSA', hash: 'SHA-256' }, pair.privateKey, bs(data)));
175
+ },
176
+ async verify(jwk, data, signature) {
177
+ const key = await subtle().importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
178
+ return subtle().verify({ name: 'ECDSA', hash: 'SHA-256' }, key, bs(signature), bs(data));
179
+ },
180
+ isPasskeyPrfAvailable: () => !!passkey,
181
+ async unlock(o) {
182
+ const candidate = await deriveKek(o);
183
+ // Si ya existe meshKey, valida la KEK (falla con BAD_KEY si es incorrecta).
184
+ const wrapped = await get('meshKeyWrapped');
185
+ if (wrapped)
186
+ await unwrapWith(wrapped, candidate);
187
+ kek = candidate;
188
+ },
189
+ async unlockPreferPasskey(passphrase) {
190
+ if (passkey) {
191
+ try {
192
+ await self.unlock({ passkey: true });
193
+ return 'passkey';
194
+ }
195
+ catch (e) {
196
+ if (!(e instanceof VaultError) || e.code !== 'NO_PASSKEY') {
197
+ // cancelacion del usuario u otro error de WebAuthn: cae a passphrase
198
+ }
199
+ }
200
+ }
201
+ await self.unlock({ passphrase });
202
+ return 'passphrase';
203
+ },
204
+ lock() {
205
+ kek = null;
206
+ },
207
+ isUnlocked: () => kek !== null,
208
+ wrapKey: (raw32, k) => {
209
+ if (raw32.length !== 32)
210
+ throw new VaultError('BAD_PARAMS', 'se esperan 32 bytes');
211
+ return wrapWith(raw32, needKek(k));
212
+ },
213
+ unwrapKey: (wrapped, k) => unwrapWith(wrapped, needKek(k)),
214
+ async getOrCreateMeshKey() {
215
+ const k = needKek();
216
+ const wrapped = await get('meshKeyWrapped');
217
+ if (wrapped)
218
+ return toMeshKey(await unwrapWith(wrapped, k));
219
+ const raw = randomBytes(32);
220
+ await put('meshKeyWrapped', await wrapWith(raw, k));
221
+ return toMeshKey(raw);
222
+ },
223
+ async exportRecoveryShare() {
224
+ const { raw } = await self.getOrCreateMeshKey();
225
+ if (await get('recoveryShown'))
226
+ throw new VaultError('SHARE_ALREADY_SHOWN', 'el share de recuperacion ya se mostro y no puede repetirse');
227
+ const [device, recoveryShare, cloudShare] = splitDEK(raw);
228
+ await put('deviceShare', device); // un solo share no revela nada (umbral 2)
229
+ await put('recoveryShown', true);
230
+ return { recoveryShare, cloudShare };
231
+ },
232
+ async recoverMeshKey(recoveryShare) {
233
+ const device = await get('deviceShare');
234
+ if (!device)
235
+ throw new VaultError('NO_MESH_KEY', 'no hay share de dispositivo');
236
+ return reconstructDEK([device, recoveryShare]);
237
+ },
238
+ async resetPassphrase(recoveryShare, newPassphrase) {
239
+ const raw = await self.recoverMeshKey(recoveryShare);
240
+ const s = randomBytes(16);
241
+ const newKek = await kekFromPassphrase(newPassphrase, s, iterations);
242
+ await put('kekSalt', s);
243
+ await put('meshKeyWrapped', await wrapWith(raw, newKek));
244
+ kek = newKek;
245
+ },
246
+ };
247
+ return self;
248
+ }
249
+ //# sourceMappingURL=vault-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault-client.js","sourceRoot":"","sources":["../../src/web/vault-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
1
  {
2
2
  "name": "@swal/vault",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "SWAL Vault Core: browser-safe end-to-end encryption for local-first apps (AES-256-GCM AEAD, Shamir 2-of-3 key shares, Ed25519-signed sealed packs, optional R2 cloud share). WebCrypto only.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./web": {
12
+ "types": "./dist/web/vault-client.d.ts",
13
+ "import": "./dist/web/vault-client.js"
14
+ },
15
+ "./web.js": {
16
+ "types": "./dist/web/vault-client.d.ts",
17
+ "import": "./dist/web/vault-client.js"
18
+ },
19
+ "./sealedPack": {
20
+ "types": "./dist/sealedPack.d.ts",
21
+ "import": "./dist/sealedPack.js"
22
+ },
23
+ "./sealedPack.js": {
24
+ "types": "./dist/sealedPack.d.ts",
25
+ "import": "./dist/sealedPack.js"
26
+ },
27
+ "./r2Client": {
28
+ "types": "./dist/r2Client.d.ts",
29
+ "import": "./dist/r2Client.js"
30
+ },
31
+ "./r2Client.js": {
32
+ "types": "./dist/r2Client.d.ts",
33
+ "import": "./dist/r2Client.js"
34
+ },
35
+ "./cloudShare": {
36
+ "types": "./dist/cloudShare.d.ts",
37
+ "import": "./dist/cloudShare.js"
38
+ },
39
+ "./cloudShare.js": {
40
+ "types": "./dist/cloudShare.d.ts",
41
+ "import": "./dist/cloudShare.js"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "test": "vitest run",
46
+ "typecheck": "tsc --noEmit",
47
+ "build": "tsc -p tsconfig.build.json",
48
+ "prepublishOnly": "npm run build && npm test"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "26.6.4",
52
+ "fake-indexeddb": "6.2.5",
53
+ "typescript": "7.0.2",
54
+ "vitest": "5.0.3"
55
+ },
56
+ "license": "Apache-2.0",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/iberi22/swal-vault-core.git"
60
+ },
61
+ "files": [
62
+ "dist",
63
+ "src",
64
+ "README.md",
65
+ "LICENSE"
66
+ ],
67
+ "main": "./dist/index.js",
68
+ "types": "./dist/index.d.ts",
69
+ "sideEffects": false,
70
+ "engines": {
71
+ "node": ">=20"
72
+ },
73
+ "keywords": [
74
+ "e2ee",
75
+ "webcrypto",
76
+ "aes-gcm",
77
+ "shamir",
78
+ "local-first",
79
+ "encryption"
80
+ ]
81
+ }
package/src/bytes.ts ADDED
@@ -0,0 +1,126 @@
1
+ // bytes.ts — helpers Uint8Array <-> base64 / base64url / hex (sin Buffer, aptos para navegador)
2
+
3
+ export function utf8(s: string): Uint8Array {
4
+ return new TextEncoder().encode(s);
5
+ }
6
+
7
+ export function fromUtf8(b: Uint8Array): string {
8
+ return new TextDecoder().decode(b);
9
+ }
10
+
11
+ export function concatBytes(...parts: Uint8Array[]): Uint8Array {
12
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
13
+ let o = 0;
14
+ for (const p of parts) {
15
+ out.set(p, o);
16
+ o += p.length;
17
+ }
18
+ return out;
19
+ }
20
+
21
+ export function toBase64(b: Uint8Array): string {
22
+ let s = '';
23
+ for (let i = 0; i < b.length; i += 0x8000) s += String.fromCharCode(...b.subarray(i, i + 0x8000));
24
+ return btoa(s);
25
+ }
26
+
27
+ export function fromBase64(s: string): Uint8Array {
28
+ const bin = atob(s);
29
+ const out = new Uint8Array(bin.length);
30
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
31
+ return out;
32
+ }
33
+
34
+ export function toBase64Url(b: Uint8Array): string {
35
+ return toBase64(b).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
36
+ }
37
+
38
+ export function fromBase64Url(s: string): Uint8Array {
39
+ const t = s.replace(/-/g, '+').replace(/_/g, '/');
40
+ return fromBase64(t + '='.repeat((4 - (t.length % 4)) % 4));
41
+ }
42
+
43
+ export function toHex(b: Uint8Array): string {
44
+ let s = '';
45
+ for (const x of b) s += x.toString(16).padStart(2, '0');
46
+ return s;
47
+ }
48
+
49
+ export function fromHex(h: string): Uint8Array {
50
+ if (h.length % 2) throw new Error('hex invalido');
51
+ const out = new Uint8Array(h.length / 2);
52
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
53
+ return out;
54
+ }
55
+
56
+ /** Bytes aleatorios criptograficos (WebCrypto). */
57
+ export function randomBytes(n: number): Uint8Array {
58
+ const out = new Uint8Array(n);
59
+ globalThis.crypto.getRandomValues(out);
60
+ return out;
61
+ }
62
+
63
+ /** Entero uniforme en [min, max) sin sesgo (rejection sampling). */
64
+ export function randomInt(min: number, max: number): number {
65
+ const range = max - min;
66
+ const limit = Math.floor(0x100000000 / range) * range;
67
+ const buf = new Uint32Array(1);
68
+ do globalThis.crypto.getRandomValues(buf);
69
+ while (buf[0] >= limit);
70
+ return min + (buf[0] % range);
71
+ }
72
+
73
+ /** Cast para TS >=5.7 (Uint8Array<ArrayBufferLike> -> BufferSource). */
74
+ export function bs(b: Uint8Array): BufferSource {
75
+ return b as unknown as BufferSource;
76
+ }
77
+
78
+ /** SHA-256 con WebCrypto. */
79
+ export async function sha256(b: Uint8Array): Promise<Uint8Array> {
80
+ return new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', bs(b)));
81
+ }
82
+
83
+ /** SHA-256 sincrono en JS puro (solo para el API legacy computeContentHash sincrono). */
84
+ export function sha256Sync(msg: Uint8Array): Uint8Array {
85
+ const K = new Uint32Array([
86
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
87
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
88
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
89
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
90
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
91
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
92
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
93
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
94
+ ]);
95
+ const H = new Uint32Array([
96
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
97
+ ]);
98
+ const len = msg.length;
99
+ const padded = new Uint8Array(((len + 9 + 63) >> 6) << 6);
100
+ padded.set(msg);
101
+ padded[len] = 0x80;
102
+ const dv = new DataView(padded.buffer);
103
+ dv.setUint32(padded.length - 8, Math.floor((len * 8) / 0x100000000));
104
+ dv.setUint32(padded.length - 4, (len * 8) >>> 0);
105
+ const w = new Uint32Array(64);
106
+ const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n));
107
+ for (let off = 0; off < padded.length; off += 64) {
108
+ for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4);
109
+ for (let i = 16; i < 64; i++) {
110
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
111
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
112
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
113
+ }
114
+ let [a, b, c, d, e, f, g, h] = H;
115
+ for (let i = 0; i < 64; i++) {
116
+ const t1 = (h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
117
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
118
+ h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
119
+ }
120
+ H[0] += a; H[1] += b; H[2] += c; H[3] += d; H[4] += e; H[5] += f; H[6] += g; H[7] += h;
121
+ }
122
+ const out = new Uint8Array(32);
123
+ const odv = new DataView(out.buffer);
124
+ for (let i = 0; i < 8; i++) odv.setUint32(i * 4, H[i]);
125
+ return out;
126
+ }
@@ -0,0 +1,27 @@
1
+ // cloudShare.ts — interfaz agnostica de almacenamiento para el unico share que la nube puede tener (x=3).
2
+ // Implementala con D1 (recomendado: 100.000 filas/dia), KV (1 escritura por registro; cupo de 1.000
3
+ // escrituras/dia por cuenta de Cloudflare) o memoria en tests. El vault no depende de ninguna.
4
+ import { assertCloudSharesSafe, CLOUD_SHARE_INDEX, cloudBoundShare, type DEKShare, type SealedPack } from './sealedPack.js';
5
+
6
+ export interface CloudShareStore {
7
+ /** Guarda el share x=3 del pack `packId`. */
8
+ put(packId: string, share: DEKShare): Promise<void>;
9
+ get(packId: string): Promise<DEKShare | null>;
10
+ delete(packId: string): Promise<void>;
11
+ }
12
+
13
+ /** Guarda en `store` el unico share permitido (x=3) de un pack recien sellado. Lanza si no existe. */
14
+ export async function storeCloudShare(store: CloudShareStore, pack: SealedPack): Promise<void> {
15
+ const share = cloudBoundShare(pack.shares);
16
+ if (!share) throw new Error(`El pack no tiene el share x=${CLOUD_SHARE_INDEX}`);
17
+ assertCloudSharesSafe(1);
18
+ await store.put(pack.id, share);
19
+ }
20
+
21
+ /** Implementacion en memoria (tests / mesh-only). */
22
+ export class MemoryCloudShareStore implements CloudShareStore {
23
+ private m = new Map<string, DEKShare>();
24
+ async put(id: string, s: DEKShare) { if (s.index !== CLOUD_SHARE_INDEX) throw new Error('solo se admite el share x=3'); this.m.set(id, { ...s }); }
25
+ async get(id: string) { const s = this.m.get(id); return s ? { ...s } : null; }
26
+ async delete(id: string) { this.m.delete(id); }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ // @swal/vault — entrypoint reusable
2
+ export * from './sealedPack.js';
3
+ export * from './r2Client.js';
4
+ export * from './cloudShare.js';
@@ -0,0 +1,123 @@
1
+ // r2Client.ts — core reusable R2 vault (antes gara-g especifico)
2
+ // Reusable para cualquier app SWAL: gara-g, hosteler-ia, flota. Pay-per-use + mesh-only fallback.
3
+ // Si hay binding SWAL_R2 (Cloudflare), usa R2 real via fetch/Worker. Si no, memoryStore (mesh-only P2P replica).
4
+ // Path generico: packs/{appId}/{instanceId}/{entity}/{content_hash}.json
5
+ import type { SealedPack, SealedPackMetadata } from './sealedPack.js';
6
+ import { assertCloudSharesSafe, packWithoutShares, verifySealedPackHash } from './sealedPack.js';
7
+
8
+ export interface R2ClientConfig {
9
+ bucketName?: string;
10
+ endpointUrl?: string;
11
+ accessKeyId?: string;
12
+ secretAccessKey?: string;
13
+ // Cloudflare binding real (cuando corre en Worker/Pages)
14
+ r2Binding?: any; // R2Bucket
15
+ appId?: string;
16
+ instanceId?: string;
17
+ }
18
+
19
+ export class R2StorageClient {
20
+ private bucketName: string;
21
+ private endpointUrl: string;
22
+ private r2Binding: any | null;
23
+ private appId: string;
24
+ private instanceId: string;
25
+ private memoryStore: Map<string, SealedPack> = new Map();
26
+
27
+ constructor(config: R2ClientConfig = {}) {
28
+ this.bucketName = config.bucketName || `swal-${config.appId || 'app'}-vault`;
29
+ this.endpointUrl = config.endpointUrl || 'https://r2.swal.dev';
30
+ this.r2Binding = config.r2Binding ?? null;
31
+ this.appId = config.appId || 'my-app';
32
+ this.instanceId = config.instanceId || 'default';
33
+ }
34
+
35
+ getBucketName(): string { return this.bucketName; }
36
+ isCloudEnabled(): boolean { return !!this.r2Binding; }
37
+
38
+ private r2KeyForPack(pack: SealedPack): string {
39
+ const m: any = pack.metadata;
40
+ const app = (m.appId as string) || this.appId;
41
+ const inst = (m.instanceId as string) || this.instanceId;
42
+ const entity = (m.entity as string) || 'generic';
43
+ return `packs/${app}/${inst}/${entity}/${pack.content_hash}.json`;
44
+ }
45
+
46
+ /**
47
+ * Sube el pack SIN shares (frontera E2EE). Lanza si el pack trae >=2 shares: el llamador debe
48
+ * quitar los shares 1 y 2 y guardar el 3 aparte (ver `CloudShareStore`).
49
+ */
50
+ async uploadSealedPack(input: SealedPack): Promise<{ url: string; contentHash: string; key: string }> {
51
+ if (!input?.content_hash) throw new Error('SealedPack sin content_hash');
52
+ assertCloudSharesSafe(input.shares?.length ?? 0);
53
+ const pack = packWithoutShares(input);
54
+ const ok = await verifySealedPackHash(pack);
55
+ if (!ok) throw new Error('SealedPack content_hash mismatch');
56
+
57
+ const key = this.r2KeyForPack(pack);
58
+
59
+ // Cloud real si hay binding
60
+ if (this.r2Binding?.put) {
61
+ await this.r2Binding.put(key, JSON.stringify(pack), { httpMetadata: { contentType: 'application/json' } });
62
+ }
63
+ // Siempre memory replica (mesh fallback)
64
+ this.memoryStore.set(key, JSON.parse(JSON.stringify(pack)));
65
+ this.memoryStore.set(pack.content_hash, JSON.parse(JSON.stringify(pack)));
66
+
67
+ const url = `${this.endpointUrl}/${this.bucketName}/${key}`;
68
+ return { url, contentHash: pack.content_hash, key };
69
+ }
70
+
71
+ async getSealedPack(keyOrHash: string): Promise<SealedPack | null> {
72
+ // Intenta R2 real primero
73
+ if (this.r2Binding?.get) {
74
+ try {
75
+ const obj = await this.r2Binding.get(keyOrHash);
76
+ if (obj) return JSON.parse(await obj.text());
77
+ // intenta como hash -> busca key generica
78
+ } catch {}
79
+ }
80
+ const p = this.memoryStore.get(keyOrHash);
81
+ return p ? JSON.parse(JSON.stringify(p)) : null;
82
+ }
83
+
84
+ async deleteSealedPack(keyOrHash: string): Promise<boolean> {
85
+ if (this.r2Binding?.delete) try { await this.r2Binding.delete(keyOrHash); } catch {}
86
+ const pack = this.memoryStore.get(keyOrHash);
87
+ if (!pack) return false;
88
+ this.memoryStore.delete(keyOrHash);
89
+ this.memoryStore.delete(pack.content_hash);
90
+ this.memoryStore.delete(this.r2KeyForPack(pack));
91
+ return true;
92
+ }
93
+
94
+ async verifyR2PackIntegrity(keyOrHash: string): Promise<boolean> {
95
+ const p = await this.getSealedPack(keyOrHash);
96
+ if (!p) return false;
97
+ return verifySealedPackHash(p);
98
+ }
99
+
100
+ async listSealedPacks(prefix?: string): Promise<string[]> {
101
+ if (this.r2Binding?.list) {
102
+ try {
103
+ const listed = await this.r2Binding.list({ prefix: prefix ?? `packs/${this.appId}/` });
104
+ return (listed.objects ?? []).map((o: any) => o.key);
105
+ } catch {}
106
+ }
107
+ const keys = new Set<string>();
108
+ for (const k of this.memoryStore.keys()) if (k.startsWith('packs/')) keys.add(k);
109
+ const pref = prefix ?? `packs/${this.appId}/`;
110
+ return [...keys].filter((k) => k.startsWith(pref));
111
+ }
112
+
113
+ // Helper pay-per-use: calcula bytes para metering
114
+ async estimateStoredBytes(): Promise<number> {
115
+ const keys = await this.listSealedPacks();
116
+ let total = 0;
117
+ for (const k of keys) {
118
+ const p = await this.getSealedPack(k);
119
+ if (p) total += JSON.stringify(p).length;
120
+ }
121
+ return total;
122
+ }
123
+ }