@noy-db/hub 0.1.0-pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/aggregate/index.cjs +476 -0
- package/dist/aggregate/index.cjs.map +1 -0
- package/dist/aggregate/index.d.cts +38 -0
- package/dist/aggregate/index.d.ts +38 -0
- package/dist/aggregate/index.js +53 -0
- package/dist/aggregate/index.js.map +1 -0
- package/dist/blobs/index.cjs +1480 -0
- package/dist/blobs/index.cjs.map +1 -0
- package/dist/blobs/index.d.cts +45 -0
- package/dist/blobs/index.d.ts +45 -0
- package/dist/blobs/index.js +48 -0
- package/dist/blobs/index.js.map +1 -0
- package/dist/bundle/index.cjs +436 -0
- package/dist/bundle/index.cjs.map +1 -0
- package/dist/bundle/index.d.cts +7 -0
- package/dist/bundle/index.d.ts +7 -0
- package/dist/bundle/index.js +40 -0
- package/dist/bundle/index.js.map +1 -0
- package/dist/chunk-2QR2PQTT.js +217 -0
- package/dist/chunk-2QR2PQTT.js.map +1 -0
- package/dist/chunk-4OWFYIDQ.js +79 -0
- package/dist/chunk-4OWFYIDQ.js.map +1 -0
- package/dist/chunk-5AATM2M2.js +90 -0
- package/dist/chunk-5AATM2M2.js.map +1 -0
- package/dist/chunk-ACLDOTNQ.js +543 -0
- package/dist/chunk-ACLDOTNQ.js.map +1 -0
- package/dist/chunk-BTDCBVJW.js +160 -0
- package/dist/chunk-BTDCBVJW.js.map +1 -0
- package/dist/chunk-CIMZBAZB.js +72 -0
- package/dist/chunk-CIMZBAZB.js.map +1 -0
- package/dist/chunk-E445ICYI.js +365 -0
- package/dist/chunk-E445ICYI.js.map +1 -0
- package/dist/chunk-EXQRC2L4.js +722 -0
- package/dist/chunk-EXQRC2L4.js.map +1 -0
- package/dist/chunk-FZU343FL.js +32 -0
- package/dist/chunk-FZU343FL.js.map +1 -0
- package/dist/chunk-GJILMRPO.js +354 -0
- package/dist/chunk-GJILMRPO.js.map +1 -0
- package/dist/chunk-GOUT6DND.js +1285 -0
- package/dist/chunk-GOUT6DND.js.map +1 -0
- package/dist/chunk-J66GRPNH.js +111 -0
- package/dist/chunk-J66GRPNH.js.map +1 -0
- package/dist/chunk-M2F2JAWB.js +464 -0
- package/dist/chunk-M2F2JAWB.js.map +1 -0
- package/dist/chunk-M5INGEFC.js +84 -0
- package/dist/chunk-M5INGEFC.js.map +1 -0
- package/dist/chunk-M62XNWRA.js +72 -0
- package/dist/chunk-M62XNWRA.js.map +1 -0
- package/dist/chunk-MR4424N3.js +275 -0
- package/dist/chunk-MR4424N3.js.map +1 -0
- package/dist/chunk-NPC4LFV5.js +132 -0
- package/dist/chunk-NPC4LFV5.js.map +1 -0
- package/dist/chunk-NXFEYLVG.js +311 -0
- package/dist/chunk-NXFEYLVG.js.map +1 -0
- package/dist/chunk-R36SIKES.js +79 -0
- package/dist/chunk-R36SIKES.js.map +1 -0
- package/dist/chunk-TDR6T5CJ.js +381 -0
- package/dist/chunk-TDR6T5CJ.js.map +1 -0
- package/dist/chunk-UF3BUNQZ.js +1 -0
- package/dist/chunk-UF3BUNQZ.js.map +1 -0
- package/dist/chunk-UQFSPSWG.js +1109 -0
- package/dist/chunk-UQFSPSWG.js.map +1 -0
- package/dist/chunk-USKYUS74.js +793 -0
- package/dist/chunk-USKYUS74.js.map +1 -0
- package/dist/chunk-XCL3WP6J.js +121 -0
- package/dist/chunk-XCL3WP6J.js.map +1 -0
- package/dist/chunk-XHFOENR2.js +680 -0
- package/dist/chunk-XHFOENR2.js.map +1 -0
- package/dist/chunk-ZFKD4QMV.js +430 -0
- package/dist/chunk-ZFKD4QMV.js.map +1 -0
- package/dist/chunk-ZLMV3TUA.js +490 -0
- package/dist/chunk-ZLMV3TUA.js.map +1 -0
- package/dist/chunk-ZRG4V3F5.js +17 -0
- package/dist/chunk-ZRG4V3F5.js.map +1 -0
- package/dist/consent/index.cjs +204 -0
- package/dist/consent/index.cjs.map +1 -0
- package/dist/consent/index.d.cts +24 -0
- package/dist/consent/index.d.ts +24 -0
- package/dist/consent/index.js +23 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/crdt/index.cjs +152 -0
- package/dist/crdt/index.cjs.map +1 -0
- package/dist/crdt/index.d.cts +30 -0
- package/dist/crdt/index.d.ts +30 -0
- package/dist/crdt/index.js +24 -0
- package/dist/crdt/index.js.map +1 -0
- package/dist/crypto-IVKU7YTT.js +44 -0
- package/dist/crypto-IVKU7YTT.js.map +1 -0
- package/dist/delegation-XDJCBTI2.js +16 -0
- package/dist/delegation-XDJCBTI2.js.map +1 -0
- package/dist/dev-unlock-CeXic1xC.d.cts +263 -0
- package/dist/dev-unlock-KrKkcqD3.d.ts +263 -0
- package/dist/hash-9KO1BGxh.d.cts +63 -0
- package/dist/hash-ChfJjRjQ.d.ts +63 -0
- package/dist/history/index.cjs +1215 -0
- package/dist/history/index.cjs.map +1 -0
- package/dist/history/index.d.cts +62 -0
- package/dist/history/index.d.ts +62 -0
- package/dist/history/index.js +79 -0
- package/dist/history/index.js.map +1 -0
- package/dist/i18n/index.cjs +746 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.cts +38 -0
- package/dist/i18n/index.d.ts +38 -0
- package/dist/i18n/index.js +55 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index-BRHBCmLt.d.ts +1940 -0
- package/dist/index-C8kQtmOk.d.ts +380 -0
- package/dist/index-DN-J-5wT.d.cts +1940 -0
- package/dist/index-DhjMjz7L.d.cts +380 -0
- package/dist/index.cjs +14756 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +6085 -0
- package/dist/index.js.map +1 -0
- package/dist/indexing/index.cjs +736 -0
- package/dist/indexing/index.cjs.map +1 -0
- package/dist/indexing/index.d.cts +36 -0
- package/dist/indexing/index.d.ts +36 -0
- package/dist/indexing/index.js +77 -0
- package/dist/indexing/index.js.map +1 -0
- package/dist/lazy-builder-BwEoBQZ9.d.ts +304 -0
- package/dist/lazy-builder-CZVLKh0Z.d.cts +304 -0
- package/dist/ledger-2NX4L7PN.js +33 -0
- package/dist/ledger-2NX4L7PN.js.map +1 -0
- package/dist/mime-magic-CBBSOkjm.d.cts +50 -0
- package/dist/mime-magic-CBBSOkjm.d.ts +50 -0
- package/dist/periods/index.cjs +1035 -0
- package/dist/periods/index.cjs.map +1 -0
- package/dist/periods/index.d.cts +21 -0
- package/dist/periods/index.d.ts +21 -0
- package/dist/periods/index.js +25 -0
- package/dist/periods/index.js.map +1 -0
- package/dist/predicate-SBHmi6D0.d.cts +161 -0
- package/dist/predicate-SBHmi6D0.d.ts +161 -0
- package/dist/query/index.cjs +1957 -0
- package/dist/query/index.cjs.map +1 -0
- package/dist/query/index.d.cts +3 -0
- package/dist/query/index.d.ts +3 -0
- package/dist/query/index.js +62 -0
- package/dist/query/index.js.map +1 -0
- package/dist/session/index.cjs +487 -0
- package/dist/session/index.cjs.map +1 -0
- package/dist/session/index.d.cts +45 -0
- package/dist/session/index.d.ts +45 -0
- package/dist/session/index.js +44 -0
- package/dist/session/index.js.map +1 -0
- package/dist/shadow/index.cjs +133 -0
- package/dist/shadow/index.cjs.map +1 -0
- package/dist/shadow/index.d.cts +16 -0
- package/dist/shadow/index.d.ts +16 -0
- package/dist/shadow/index.js +20 -0
- package/dist/shadow/index.js.map +1 -0
- package/dist/store/index.cjs +1069 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +491 -0
- package/dist/store/index.d.ts +491 -0
- package/dist/store/index.js +34 -0
- package/dist/store/index.js.map +1 -0
- package/dist/strategy-BSxFXGzb.d.cts +110 -0
- package/dist/strategy-BSxFXGzb.d.ts +110 -0
- package/dist/strategy-D-SrOLCl.d.cts +548 -0
- package/dist/strategy-D-SrOLCl.d.ts +548 -0
- package/dist/sync/index.cjs +1062 -0
- package/dist/sync/index.cjs.map +1 -0
- package/dist/sync/index.d.cts +42 -0
- package/dist/sync/index.d.ts +42 -0
- package/dist/sync/index.js +28 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/team/index.cjs +1233 -0
- package/dist/team/index.cjs.map +1 -0
- package/dist/team/index.d.cts +117 -0
- package/dist/team/index.d.ts +117 -0
- package/dist/team/index.js +39 -0
- package/dist/team/index.js.map +1 -0
- package/dist/tx/index.cjs +212 -0
- package/dist/tx/index.cjs.map +1 -0
- package/dist/tx/index.d.cts +20 -0
- package/dist/tx/index.d.ts +20 -0
- package/dist/tx/index.js +20 -0
- package/dist/tx/index.js.map +1 -0
- package/dist/types-BZpCZB8N.d.ts +7526 -0
- package/dist/types-Bfs0qr5F.d.cts +7526 -0
- package/dist/ulid-COREQ2RQ.js +9 -0
- package/dist/ulid-COREQ2RQ.js.map +1 -0
- package/dist/util/index.cjs +230 -0
- package/dist/util/index.cjs.map +1 -0
- package/dist/util/index.d.cts +77 -0
- package/dist/util/index.d.ts +77 -0
- package/dist/util/index.js +190 -0
- package/dist/util/index.js.map +1 -0
- package/package.json +244 -0
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/errors.ts
|
|
24
|
+
var NoydbError, DecryptionError, TamperedError, InvalidKeyError, ConflictError, NotFoundError;
|
|
25
|
+
var init_errors = __esm({
|
|
26
|
+
"src/errors.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
NoydbError = class extends Error {
|
|
29
|
+
/** Machine-readable error code. Stable across library versions. */
|
|
30
|
+
code;
|
|
31
|
+
constructor(code, message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "NoydbError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
DecryptionError = class extends NoydbError {
|
|
38
|
+
constructor(message = "Decryption failed") {
|
|
39
|
+
super("DECRYPTION_FAILED", message);
|
|
40
|
+
this.name = "DecryptionError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
TamperedError = class extends NoydbError {
|
|
44
|
+
constructor(message = "Data integrity check failed \u2014 record may have been tampered with") {
|
|
45
|
+
super("TAMPERED", message);
|
|
46
|
+
this.name = "TamperedError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
InvalidKeyError = class extends NoydbError {
|
|
50
|
+
constructor(message = "Invalid key \u2014 wrong passphrase or corrupted keyring") {
|
|
51
|
+
super("INVALID_KEY", message);
|
|
52
|
+
this.name = "InvalidKeyError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
ConflictError = class extends NoydbError {
|
|
56
|
+
/** The actual stored version at the time of conflict. */
|
|
57
|
+
version;
|
|
58
|
+
constructor(version, message = "Version conflict") {
|
|
59
|
+
super("CONFLICT", message);
|
|
60
|
+
this.name = "ConflictError";
|
|
61
|
+
this.version = version;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
NotFoundError = class extends NoydbError {
|
|
65
|
+
constructor(message = "Record not found") {
|
|
66
|
+
super("NOT_FOUND", message);
|
|
67
|
+
this.name = "NotFoundError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/crypto.ts
|
|
74
|
+
var crypto_exports = {};
|
|
75
|
+
__export(crypto_exports, {
|
|
76
|
+
base64ToBuffer: () => base64ToBuffer,
|
|
77
|
+
bufferToBase64: () => bufferToBase64,
|
|
78
|
+
decrypt: () => decrypt,
|
|
79
|
+
decryptBytes: () => decryptBytes,
|
|
80
|
+
decryptBytesWithAAD: () => decryptBytesWithAAD,
|
|
81
|
+
decryptDeterministic: () => decryptDeterministic,
|
|
82
|
+
deriveKey: () => deriveKey,
|
|
83
|
+
derivePresenceKey: () => derivePresenceKey,
|
|
84
|
+
encrypt: () => encrypt,
|
|
85
|
+
encryptBytes: () => encryptBytes,
|
|
86
|
+
encryptBytesWithAAD: () => encryptBytesWithAAD,
|
|
87
|
+
encryptDeterministic: () => encryptDeterministic,
|
|
88
|
+
generateDEK: () => generateDEK,
|
|
89
|
+
generateIV: () => generateIV,
|
|
90
|
+
generateSalt: () => generateSalt,
|
|
91
|
+
hmacSha256Hex: () => hmacSha256Hex,
|
|
92
|
+
sha256Hex: () => sha256Hex,
|
|
93
|
+
unwrapKey: () => unwrapKey,
|
|
94
|
+
wrapKey: () => wrapKey
|
|
95
|
+
});
|
|
96
|
+
async function deriveKey(passphrase, salt) {
|
|
97
|
+
const keyMaterial = await subtle.importKey(
|
|
98
|
+
"raw",
|
|
99
|
+
new TextEncoder().encode(passphrase),
|
|
100
|
+
"PBKDF2",
|
|
101
|
+
false,
|
|
102
|
+
["deriveKey"]
|
|
103
|
+
);
|
|
104
|
+
return subtle.deriveKey(
|
|
105
|
+
{
|
|
106
|
+
name: "PBKDF2",
|
|
107
|
+
salt,
|
|
108
|
+
iterations: PBKDF2_ITERATIONS,
|
|
109
|
+
hash: "SHA-256"
|
|
110
|
+
},
|
|
111
|
+
keyMaterial,
|
|
112
|
+
{ name: "AES-KW", length: KEY_BITS },
|
|
113
|
+
false,
|
|
114
|
+
["wrapKey", "unwrapKey"]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
async function generateDEK() {
|
|
118
|
+
return subtle.generateKey(
|
|
119
|
+
{ name: "AES-GCM", length: KEY_BITS },
|
|
120
|
+
true,
|
|
121
|
+
// extractable — needed for AES-KW wrapping
|
|
122
|
+
["encrypt", "decrypt"]
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
async function wrapKey(dek, kek) {
|
|
126
|
+
const wrapped = await subtle.wrapKey("raw", dek, kek, "AES-KW");
|
|
127
|
+
return bufferToBase64(wrapped);
|
|
128
|
+
}
|
|
129
|
+
async function unwrapKey(wrappedBase64, kek) {
|
|
130
|
+
try {
|
|
131
|
+
return await subtle.unwrapKey(
|
|
132
|
+
"raw",
|
|
133
|
+
base64ToBuffer(wrappedBase64),
|
|
134
|
+
kek,
|
|
135
|
+
"AES-KW",
|
|
136
|
+
{ name: "AES-GCM", length: KEY_BITS },
|
|
137
|
+
true,
|
|
138
|
+
["encrypt", "decrypt"]
|
|
139
|
+
);
|
|
140
|
+
} catch {
|
|
141
|
+
throw new InvalidKeyError();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function encrypt(plaintext, dek) {
|
|
145
|
+
const iv = generateIV();
|
|
146
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
147
|
+
const ciphertext = await subtle.encrypt(
|
|
148
|
+
{ name: "AES-GCM", iv },
|
|
149
|
+
dek,
|
|
150
|
+
encoded
|
|
151
|
+
);
|
|
152
|
+
return {
|
|
153
|
+
iv: bufferToBase64(iv),
|
|
154
|
+
data: bufferToBase64(ciphertext)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function decrypt(ivBase64, dataBase64, dek) {
|
|
158
|
+
const iv = base64ToBuffer(ivBase64);
|
|
159
|
+
const ciphertext = base64ToBuffer(dataBase64);
|
|
160
|
+
try {
|
|
161
|
+
const plaintext = await subtle.decrypt(
|
|
162
|
+
{ name: "AES-GCM", iv },
|
|
163
|
+
dek,
|
|
164
|
+
ciphertext
|
|
165
|
+
);
|
|
166
|
+
return new TextDecoder().decode(plaintext);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err instanceof Error && err.name === "OperationError") {
|
|
169
|
+
throw new TamperedError();
|
|
170
|
+
}
|
|
171
|
+
throw new DecryptionError(
|
|
172
|
+
err instanceof Error ? err.message : "Decryption failed"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function encryptBytes(data, dek) {
|
|
177
|
+
const iv = generateIV();
|
|
178
|
+
const ciphertext = await subtle.encrypt(
|
|
179
|
+
{ name: "AES-GCM", iv },
|
|
180
|
+
dek,
|
|
181
|
+
data
|
|
182
|
+
);
|
|
183
|
+
return {
|
|
184
|
+
iv: bufferToBase64(iv),
|
|
185
|
+
data: bufferToBase64(ciphertext)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function decryptBytes(ivBase64, dataBase64, dek) {
|
|
189
|
+
const iv = base64ToBuffer(ivBase64);
|
|
190
|
+
const ciphertext = base64ToBuffer(dataBase64);
|
|
191
|
+
try {
|
|
192
|
+
const plaintext = await subtle.decrypt(
|
|
193
|
+
{ name: "AES-GCM", iv },
|
|
194
|
+
dek,
|
|
195
|
+
ciphertext
|
|
196
|
+
);
|
|
197
|
+
return new Uint8Array(plaintext);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err instanceof Error && err.name === "OperationError") {
|
|
200
|
+
throw new TamperedError();
|
|
201
|
+
}
|
|
202
|
+
throw new DecryptionError(
|
|
203
|
+
err instanceof Error ? err.message : "Decryption failed"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function sha256Hex(data) {
|
|
208
|
+
const hash = await subtle.digest("SHA-256", data);
|
|
209
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
210
|
+
}
|
|
211
|
+
async function hmacSha256Hex(key, data) {
|
|
212
|
+
const rawKey = await subtle.exportKey("raw", key);
|
|
213
|
+
const hmacKey = await subtle.importKey(
|
|
214
|
+
"raw",
|
|
215
|
+
rawKey,
|
|
216
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
217
|
+
false,
|
|
218
|
+
["sign"]
|
|
219
|
+
);
|
|
220
|
+
const sig = await subtle.sign("HMAC", hmacKey, data);
|
|
221
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
222
|
+
}
|
|
223
|
+
async function encryptBytesWithAAD(data, dek, aad) {
|
|
224
|
+
const iv = generateIV();
|
|
225
|
+
const ciphertext = await subtle.encrypt(
|
|
226
|
+
{
|
|
227
|
+
name: "AES-GCM",
|
|
228
|
+
iv,
|
|
229
|
+
additionalData: aad
|
|
230
|
+
},
|
|
231
|
+
dek,
|
|
232
|
+
data
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
iv: bufferToBase64(iv),
|
|
236
|
+
data: bufferToBase64(ciphertext)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function decryptBytesWithAAD(ivBase64, dataBase64, dek, aad) {
|
|
240
|
+
const iv = base64ToBuffer(ivBase64);
|
|
241
|
+
const ciphertext = base64ToBuffer(dataBase64);
|
|
242
|
+
try {
|
|
243
|
+
const plaintext = await subtle.decrypt(
|
|
244
|
+
{
|
|
245
|
+
name: "AES-GCM",
|
|
246
|
+
iv,
|
|
247
|
+
additionalData: aad
|
|
248
|
+
},
|
|
249
|
+
dek,
|
|
250
|
+
ciphertext
|
|
251
|
+
);
|
|
252
|
+
return new Uint8Array(plaintext);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (err instanceof Error && err.name === "OperationError") {
|
|
255
|
+
throw new TamperedError();
|
|
256
|
+
}
|
|
257
|
+
throw new DecryptionError(
|
|
258
|
+
err instanceof Error ? err.message : "Decryption failed"
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function derivePresenceKey(dek, collectionName) {
|
|
263
|
+
const rawDek = await subtle.exportKey("raw", dek);
|
|
264
|
+
const hkdfKey = await subtle.importKey(
|
|
265
|
+
"raw",
|
|
266
|
+
rawDek,
|
|
267
|
+
"HKDF",
|
|
268
|
+
false,
|
|
269
|
+
["deriveBits"]
|
|
270
|
+
);
|
|
271
|
+
const salt = new TextEncoder().encode("noydb-presence");
|
|
272
|
+
const info = new TextEncoder().encode(collectionName);
|
|
273
|
+
const bits = await subtle.deriveBits(
|
|
274
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
275
|
+
hkdfKey,
|
|
276
|
+
KEY_BITS
|
|
277
|
+
);
|
|
278
|
+
return subtle.importKey(
|
|
279
|
+
"raw",
|
|
280
|
+
bits,
|
|
281
|
+
{ name: "AES-GCM", length: KEY_BITS },
|
|
282
|
+
false,
|
|
283
|
+
["encrypt", "decrypt"]
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
async function deriveDeterministicIV(dek, context, plaintext) {
|
|
287
|
+
const rawDek = await subtle.exportKey("raw", dek);
|
|
288
|
+
const hkdfKey = await subtle.importKey("raw", rawDek, "HKDF", false, ["deriveBits"]);
|
|
289
|
+
const salt = new TextEncoder().encode("noydb-deterministic-v1");
|
|
290
|
+
const info = new TextEncoder().encode(`${context}\0${plaintext}`);
|
|
291
|
+
const bits = await subtle.deriveBits(
|
|
292
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
293
|
+
hkdfKey,
|
|
294
|
+
IV_BYTES * 8
|
|
295
|
+
);
|
|
296
|
+
return new Uint8Array(bits);
|
|
297
|
+
}
|
|
298
|
+
async function encryptDeterministic(plaintext, dek, context) {
|
|
299
|
+
const iv = await deriveDeterministicIV(dek, context, plaintext);
|
|
300
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
301
|
+
const ciphertext = await subtle.encrypt(
|
|
302
|
+
{ name: "AES-GCM", iv },
|
|
303
|
+
dek,
|
|
304
|
+
encoded
|
|
305
|
+
);
|
|
306
|
+
return {
|
|
307
|
+
iv: bufferToBase64(iv),
|
|
308
|
+
data: bufferToBase64(ciphertext)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async function decryptDeterministic(ivBase64, dataBase64, dek) {
|
|
312
|
+
return decrypt(ivBase64, dataBase64, dek);
|
|
313
|
+
}
|
|
314
|
+
function generateIV() {
|
|
315
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
316
|
+
}
|
|
317
|
+
function generateSalt() {
|
|
318
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
319
|
+
}
|
|
320
|
+
function bufferToBase64(buffer) {
|
|
321
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
322
|
+
let binary = "";
|
|
323
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
324
|
+
binary += String.fromCharCode(bytes[i]);
|
|
325
|
+
}
|
|
326
|
+
return btoa(binary);
|
|
327
|
+
}
|
|
328
|
+
function base64ToBuffer(base64) {
|
|
329
|
+
const binary = atob(base64);
|
|
330
|
+
const bytes = new Uint8Array(binary.length);
|
|
331
|
+
for (let i = 0; i < binary.length; i++) {
|
|
332
|
+
bytes[i] = binary.charCodeAt(i);
|
|
333
|
+
}
|
|
334
|
+
return bytes;
|
|
335
|
+
}
|
|
336
|
+
var PBKDF2_ITERATIONS, SALT_BYTES, IV_BYTES, KEY_BITS, subtle;
|
|
337
|
+
var init_crypto = __esm({
|
|
338
|
+
"src/crypto.ts"() {
|
|
339
|
+
"use strict";
|
|
340
|
+
init_errors();
|
|
341
|
+
PBKDF2_ITERATIONS = 6e5;
|
|
342
|
+
SALT_BYTES = 32;
|
|
343
|
+
IV_BYTES = 12;
|
|
344
|
+
KEY_BITS = 256;
|
|
345
|
+
subtle = globalThis.crypto.subtle;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// src/blobs/index.ts
|
|
350
|
+
var blobs_exports = {};
|
|
351
|
+
__export(blobs_exports, {
|
|
352
|
+
BLOB_CHUNKS_COLLECTION: () => BLOB_CHUNKS_COLLECTION,
|
|
353
|
+
BLOB_COLLECTION: () => BLOB_COLLECTION,
|
|
354
|
+
BLOB_EVICTION_AUDIT_COLLECTION: () => BLOB_EVICTION_AUDIT_COLLECTION,
|
|
355
|
+
BLOB_INDEX_COLLECTION: () => BLOB_INDEX_COLLECTION,
|
|
356
|
+
BLOB_SLOTS_PREFIX: () => BLOB_SLOTS_PREFIX,
|
|
357
|
+
BLOB_VERSIONS_PREFIX: () => BLOB_VERSIONS_PREFIX,
|
|
358
|
+
BlobSet: () => BlobSet,
|
|
359
|
+
DEFAULT_CHUNK_SIZE: () => DEFAULT_CHUNK_SIZE,
|
|
360
|
+
EXPORT_AUDIT_COLLECTION: () => EXPORT_AUDIT_COLLECTION,
|
|
361
|
+
ExportBlobsAbortedError: () => ExportBlobsAbortedError,
|
|
362
|
+
createExportBlobsHandle: () => createExportBlobsHandle,
|
|
363
|
+
detectMagic: () => detectMagic,
|
|
364
|
+
detectMimeType: () => detectMimeType,
|
|
365
|
+
isPreCompressed: () => isPreCompressed,
|
|
366
|
+
runCompaction: () => runCompaction,
|
|
367
|
+
withBlobs: () => withBlobs
|
|
368
|
+
});
|
|
369
|
+
module.exports = __toCommonJS(blobs_exports);
|
|
370
|
+
|
|
371
|
+
// src/types.ts
|
|
372
|
+
var NOYDB_FORMAT_VERSION = 1;
|
|
373
|
+
|
|
374
|
+
// src/blobs/blob-set.ts
|
|
375
|
+
init_crypto();
|
|
376
|
+
init_errors();
|
|
377
|
+
|
|
378
|
+
// src/blobs/mime-magic.ts
|
|
379
|
+
function hex(s) {
|
|
380
|
+
return new Uint8Array(s.split(" ").map((b) => parseInt(b, 16)));
|
|
381
|
+
}
|
|
382
|
+
var MAGIC_RULES = [
|
|
383
|
+
// ── Images ───────────────────────────────────────────────────────────
|
|
384
|
+
// #2 PNG — full 8-byte signature (RFC 2083)
|
|
385
|
+
{ mime: "image/png", format: "PNG", bytes: hex("89 50 4E 47 0D 0A 1A 0A"), preCompressed: true },
|
|
386
|
+
// #1 JPEG — FF D8 FF (third byte is start of APP marker, always FF)
|
|
387
|
+
{ mime: "image/jpeg", format: "JPEG", bytes: hex("FF D8 FF"), preCompressed: true },
|
|
388
|
+
// #7 WebP — RIFF compound: bytes 0-3 = RIFF, bytes 8-11 = WEBP
|
|
389
|
+
{
|
|
390
|
+
mime: "image/webp",
|
|
391
|
+
format: "WebP",
|
|
392
|
+
bytes: hex("52 49 46 46"),
|
|
393
|
+
secondaryBytes: hex("57 45 42 50"),
|
|
394
|
+
secondaryOffset: 8,
|
|
395
|
+
preCompressed: true
|
|
396
|
+
},
|
|
397
|
+
// #5 TIFF (little-endian) — II + version 42
|
|
398
|
+
{ mime: "image/tiff", format: "TIFF", bytes: hex("49 49 2A 00") },
|
|
399
|
+
// #6 TIFF (big-endian) — MM + version 42
|
|
400
|
+
{ mime: "image/tiff", format: "TIFF", bytes: hex("4D 4D 00 2A") },
|
|
401
|
+
// #3 GIF — GIF8 (covers GIF87a and GIF89a)
|
|
402
|
+
{ mime: "image/gif", format: "GIF", bytes: hex("47 49 46 38"), preCompressed: true },
|
|
403
|
+
// #4 BMP — BM
|
|
404
|
+
{ mime: "image/bmp", format: "BMP", bytes: hex("42 4D") },
|
|
405
|
+
// PSD — 8BPS
|
|
406
|
+
{ mime: "image/vnd.adobe.photoshop", format: "PSD", bytes: hex("38 42 50 53") },
|
|
407
|
+
// #8 ICO — 00 00 01 00 (note: 00 00 02 00 is CUR cursor format)
|
|
408
|
+
{ mime: "image/x-icon", format: "ICO", bytes: hex("00 00 01 00") },
|
|
409
|
+
// #9 HEIC — ISOBMFF: ftyp at offset 4, brand "heic" at offset 8
|
|
410
|
+
{
|
|
411
|
+
mime: "image/heic",
|
|
412
|
+
format: "HEIC",
|
|
413
|
+
bytes: hex("66 74 79 70"),
|
|
414
|
+
offset: 4,
|
|
415
|
+
secondaryBytes: hex("68 65 69 63"),
|
|
416
|
+
secondaryOffset: 8,
|
|
417
|
+
preCompressed: true
|
|
418
|
+
},
|
|
419
|
+
// ── Documents ────────────────────────────────────────────────────────
|
|
420
|
+
// PDF — %PDF
|
|
421
|
+
{ mime: "application/pdf", format: "PDF", bytes: hex("25 50 44 46") },
|
|
422
|
+
// RTF — {\rtf
|
|
423
|
+
{ mime: "application/rtf", format: "RTF", bytes: hex("7B 5C 72 74 66") },
|
|
424
|
+
// ── Archives & compression ───────────────────────────────────────────
|
|
425
|
+
// RAR v5 — 8-byte signature (test before RAR v4)
|
|
426
|
+
{ mime: "application/vnd.rar", format: "RAR v5", bytes: hex("52 61 72 21 1A 07 01 00"), preCompressed: true },
|
|
427
|
+
// RAR v4 — 7-byte signature
|
|
428
|
+
{ mime: "application/vnd.rar", format: "RAR v4", bytes: hex("52 61 72 21 1A 07 00"), preCompressed: true },
|
|
429
|
+
// 7-Zip — 6-byte signature
|
|
430
|
+
{ mime: "application/x-7z-compressed", format: "7Z", bytes: hex("37 7A BC AF 27 1C"), preCompressed: true },
|
|
431
|
+
// XZ — 6-byte stream header
|
|
432
|
+
{ mime: "application/x-xz", format: "XZ", bytes: hex("FD 37 7A 58 5A 00"), preCompressed: true },
|
|
433
|
+
// ZIP — PK\x03\x04 (local file header)
|
|
434
|
+
{ mime: "application/zip", format: "ZIP", bytes: hex("50 4B 03 04"), preCompressed: true },
|
|
435
|
+
// GZIP — 1F 8B
|
|
436
|
+
{ mime: "application/gzip", format: "GZIP", bytes: hex("1F 8B"), preCompressed: true },
|
|
437
|
+
// BZIP2 — BZh
|
|
438
|
+
{ mime: "application/x-bzip2", format: "BZIP2", bytes: hex("42 5A 68"), preCompressed: true },
|
|
439
|
+
// LZIP — LZIP
|
|
440
|
+
{ mime: "application/x-lzip", format: "LZIP", bytes: hex("4C 5A 49 50"), preCompressed: true },
|
|
441
|
+
// ── Audio ────────────────────────────────────────────────────────────
|
|
442
|
+
// WAV — RIFF compound: bytes 0-3 = RIFF, bytes 8-11 = WAVE
|
|
443
|
+
{
|
|
444
|
+
mime: "audio/wav",
|
|
445
|
+
format: "WAV",
|
|
446
|
+
bytes: hex("52 49 46 46"),
|
|
447
|
+
secondaryBytes: hex("57 41 56 45"),
|
|
448
|
+
secondaryOffset: 8
|
|
449
|
+
},
|
|
450
|
+
// AIFF — FORM compound: bytes 0-3 = FORM, bytes 8-11 = AIFF
|
|
451
|
+
{
|
|
452
|
+
mime: "audio/aiff",
|
|
453
|
+
format: "AIFF",
|
|
454
|
+
bytes: hex("46 4F 52 4D"),
|
|
455
|
+
secondaryBytes: hex("41 49 46 46"),
|
|
456
|
+
secondaryOffset: 8
|
|
457
|
+
},
|
|
458
|
+
// FLAC — fLaC
|
|
459
|
+
{ mime: "audio/flac", format: "FLAC", bytes: hex("66 4C 61 43") },
|
|
460
|
+
// OGG — OggS (container — may hold Vorbis, Opus, Theora, etc.)
|
|
461
|
+
{ mime: "application/ogg", format: "OGG", bytes: hex("4F 67 67 53") },
|
|
462
|
+
// MIDI — MThd
|
|
463
|
+
{ mime: "audio/midi", format: "MIDI", bytes: hex("4D 54 68 64") },
|
|
464
|
+
// MP3 (ID3-tagged) — ID3
|
|
465
|
+
{ mime: "audio/mpeg", format: "MP3", bytes: hex("49 44 33"), preCompressed: true },
|
|
466
|
+
// ── Video ────────────────────────────────────────────────────────────
|
|
467
|
+
// AVI — RIFF compound: bytes 0-3 = RIFF, bytes 8-11 = AVI\x20
|
|
468
|
+
{
|
|
469
|
+
mime: "video/x-msvideo",
|
|
470
|
+
format: "AVI",
|
|
471
|
+
bytes: hex("52 49 46 46"),
|
|
472
|
+
secondaryBytes: hex("41 56 49 20"),
|
|
473
|
+
secondaryOffset: 8,
|
|
474
|
+
preCompressed: true
|
|
475
|
+
},
|
|
476
|
+
// WMV/ASF — 8-byte ASF header GUID prefix
|
|
477
|
+
{ mime: "video/x-ms-wmv", format: "WMV", bytes: hex("30 26 B2 75 8E 66 CF 11"), preCompressed: true },
|
|
478
|
+
// MKV/WebM — EBML header (Matroska container)
|
|
479
|
+
{ mime: "video/x-matroska", format: "MKV", bytes: hex("1A 45 DF A3"), preCompressed: true },
|
|
480
|
+
// FLV — FLV
|
|
481
|
+
{ mime: "video/x-flv", format: "FLV", bytes: hex("46 4C 56"), preCompressed: true },
|
|
482
|
+
// MOV — ISOBMFF: ftyp at offset 4, brand "qt " at offset 8
|
|
483
|
+
{
|
|
484
|
+
mime: "video/quicktime",
|
|
485
|
+
format: "MOV",
|
|
486
|
+
bytes: hex("66 74 79 70"),
|
|
487
|
+
offset: 4,
|
|
488
|
+
secondaryBytes: hex("71 74 20 20"),
|
|
489
|
+
secondaryOffset: 8,
|
|
490
|
+
preCompressed: true
|
|
491
|
+
},
|
|
492
|
+
// MP4 — ISOBMFF: ftyp at offset 4 (brands vary: isom, mp41, mp42, etc.)
|
|
493
|
+
// Tested AFTER MOV and HEIC so their specific brands match first.
|
|
494
|
+
{ mime: "video/mp4", format: "MP4", bytes: hex("66 74 79 70"), offset: 4, preCompressed: true },
|
|
495
|
+
// ── Executables & binaries ───────────────────────────────────────────
|
|
496
|
+
// SQLite — "SQLite 3" (first 8 bytes of the 16-byte header)
|
|
497
|
+
{ mime: "application/vnd.sqlite3", format: "SQLite", bytes: hex("53 51 4C 69 74 65 20 33") },
|
|
498
|
+
// WASM — \0asm
|
|
499
|
+
{ mime: "application/wasm", format: "WASM", bytes: hex("00 61 73 6D") },
|
|
500
|
+
// ELF — \x7FELF
|
|
501
|
+
{ mime: "application/x-elf", format: "ELF", bytes: hex("7F 45 4C 46") },
|
|
502
|
+
// PE (EXE/DLL) — MZ
|
|
503
|
+
{ mime: "application/vnd.microsoft.portable-executable", format: "PE", bytes: hex("4D 5A") },
|
|
504
|
+
// Mach-O — all four single-arch variants
|
|
505
|
+
{ mime: "application/x-mach-binary", format: "Mach-O 64 LE", bytes: hex("CF FA ED FE") },
|
|
506
|
+
{ mime: "application/x-mach-binary", format: "Mach-O 64 BE", bytes: hex("FE ED FA CF") },
|
|
507
|
+
{ mime: "application/x-mach-binary", format: "Mach-O 32 LE", bytes: hex("CE FA ED FE") },
|
|
508
|
+
{ mime: "application/x-mach-binary", format: "Mach-O 32 BE", bytes: hex("FE ED FA CE") },
|
|
509
|
+
// Java Class — CA FE BA BE
|
|
510
|
+
// Note: collides with Mach-O Universal Binary. Disambiguated by checking
|
|
511
|
+
// bytes 4-7: Java class version is >= 0x002D (45), while fat binary
|
|
512
|
+
// arch count is a small number (typically 0x00000002).
|
|
513
|
+
// We place Java after Mach-O single-arch entries so the more common
|
|
514
|
+
// Mach-O variants match first. The CA FE BA BE collision between Java
|
|
515
|
+
// and Mach-O fat binary is resolved by the caller if needed.
|
|
516
|
+
{ mime: "application/java-vm", format: "Java Class", bytes: hex("CA FE BA BE") },
|
|
517
|
+
// DEX — dex\n (Android Dalvik Executable)
|
|
518
|
+
{ mime: "application/vnd.android.dex", format: "DEX", bytes: hex("64 65 78 0A") },
|
|
519
|
+
// ── Package formats ──────────────────────────────────────────────────
|
|
520
|
+
// DEB — !<arch> (ar archive; DEB-specific member follows)
|
|
521
|
+
{ mime: "application/vnd.debian.binary-package", format: "DEB", bytes: hex("21 3C 61 72 63 68 3E") },
|
|
522
|
+
// RPM — ED AB EE DB
|
|
523
|
+
{ mime: "application/x-rpm", format: "RPM", bytes: hex("ED AB EE DB") },
|
|
524
|
+
// CAB — MSCF
|
|
525
|
+
{ mime: "application/vnd.ms-cab-compressed", format: "CAB", bytes: hex("4D 53 43 46"), preCompressed: true },
|
|
526
|
+
// ── Capture & Flash ──────────────────────────────────────────────────
|
|
527
|
+
// PCAP (little-endian) — D4 C3 B2 A1
|
|
528
|
+
{ mime: "application/vnd.tcpdump.pcap", format: "PCAP", bytes: hex("D4 C3 B2 A1") },
|
|
529
|
+
// PCAP (big-endian) — A1 B2 C3 D4
|
|
530
|
+
{ mime: "application/vnd.tcpdump.pcap", format: "PCAP BE", bytes: hex("A1 B2 C3 D4") },
|
|
531
|
+
// PCAPNG — Section Header Block
|
|
532
|
+
{ mime: "application/x-pcapng", format: "PCAPNG", bytes: hex("0A 0D 0D 0A") },
|
|
533
|
+
// SWF — all three variants (uncompressed, zlib, LZMA)
|
|
534
|
+
{ mime: "application/x-shockwave-flash", format: "SWF", bytes: hex("46 57 53") },
|
|
535
|
+
{ mime: "application/x-shockwave-flash", format: "SWF zlib", bytes: hex("43 57 53"), preCompressed: true },
|
|
536
|
+
{ mime: "application/x-shockwave-flash", format: "SWF LZMA", bytes: hex("5A 57 53"), preCompressed: true },
|
|
537
|
+
// ── Data formats ─────────────────────────────────────────────────────
|
|
538
|
+
// Parquet — PAR1 (no registered IANA MIME; using Apache's informal type)
|
|
539
|
+
{ mime: "application/vnd.apache.parquet", format: "Parquet", bytes: hex("50 41 52 31") },
|
|
540
|
+
// Avro Object Container — Obj\x01
|
|
541
|
+
{ mime: "application/avro", format: "Avro", bytes: hex("4F 62 6A 01") },
|
|
542
|
+
// NES ROM — NES\x1A (iNES header)
|
|
543
|
+
{ mime: "application/x-nintendo-nes-rom", format: "NES ROM", bytes: hex("4E 45 53 1A") }
|
|
544
|
+
];
|
|
545
|
+
function isMp3SyncWord(byte0, byte1) {
|
|
546
|
+
return byte0 === 255 && (byte1 & 224) === 224;
|
|
547
|
+
}
|
|
548
|
+
function detectMimeType(header) {
|
|
549
|
+
const result = detectMagic(header);
|
|
550
|
+
return result?.mime ?? "application/octet-stream";
|
|
551
|
+
}
|
|
552
|
+
function detectMagic(header) {
|
|
553
|
+
for (const rule of MAGIC_RULES) {
|
|
554
|
+
if (matchRule(header, rule)) {
|
|
555
|
+
return {
|
|
556
|
+
mime: rule.mime,
|
|
557
|
+
format: rule.format,
|
|
558
|
+
preCompressed: rule.preCompressed ?? false
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (header.length >= 2 && isMp3SyncWord(header[0], header[1])) {
|
|
563
|
+
return { mime: "audio/mpeg", format: "MP3", preCompressed: true };
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
function isPreCompressed(mimeType) {
|
|
568
|
+
return PRE_COMPRESSED_MIMES.has(mimeType);
|
|
569
|
+
}
|
|
570
|
+
function matchRule(header, rule) {
|
|
571
|
+
const offset = rule.offset ?? 0;
|
|
572
|
+
const end = offset + rule.bytes.length;
|
|
573
|
+
if (header.length < end) return false;
|
|
574
|
+
for (let i = 0; i < rule.bytes.length; i++) {
|
|
575
|
+
if (header[offset + i] !== rule.bytes[i]) return false;
|
|
576
|
+
}
|
|
577
|
+
if (rule.secondaryBytes && rule.secondaryOffset !== void 0) {
|
|
578
|
+
const sEnd = rule.secondaryOffset + rule.secondaryBytes.length;
|
|
579
|
+
if (header.length < sEnd) return false;
|
|
580
|
+
for (let i = 0; i < rule.secondaryBytes.length; i++) {
|
|
581
|
+
if (header[rule.secondaryOffset + i] !== rule.secondaryBytes[i]) return false;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
var PRE_COMPRESSED_MIMES = new Set(
|
|
587
|
+
MAGIC_RULES.filter((r) => r.preCompressed).map((r) => r.mime)
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// src/blobs/blob-set.ts
|
|
591
|
+
init_crypto();
|
|
592
|
+
var BLOB_COLLECTION = "_blob";
|
|
593
|
+
var BLOB_INDEX_COLLECTION = "_blob_index";
|
|
594
|
+
var BLOB_CHUNKS_COLLECTION = "_blob_chunks";
|
|
595
|
+
var BLOB_SLOTS_PREFIX = "_blob_slots_";
|
|
596
|
+
var BLOB_VERSIONS_PREFIX = "_blob_versions_";
|
|
597
|
+
var DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
598
|
+
var MAX_CAS_RETRIES = 5;
|
|
599
|
+
async function compressBytes(data) {
|
|
600
|
+
if (typeof CompressionStream === "undefined") {
|
|
601
|
+
return { bytes: data, algorithm: "none" };
|
|
602
|
+
}
|
|
603
|
+
const cs = new CompressionStream("gzip");
|
|
604
|
+
const writer = cs.writable.getWriter();
|
|
605
|
+
await writer.write(data);
|
|
606
|
+
await writer.close();
|
|
607
|
+
const buf = await new Response(cs.readable).arrayBuffer();
|
|
608
|
+
return { bytes: new Uint8Array(buf), algorithm: "gzip" };
|
|
609
|
+
}
|
|
610
|
+
async function decompressBytes(data) {
|
|
611
|
+
if (typeof DecompressionStream === "undefined") {
|
|
612
|
+
throw new Error(
|
|
613
|
+
"[noy-db] DecompressionStream not available \u2014 cannot decompress blob chunk"
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
const ds = new DecompressionStream("gzip");
|
|
617
|
+
const writer = ds.writable.getWriter();
|
|
618
|
+
await writer.write(data);
|
|
619
|
+
await writer.close();
|
|
620
|
+
const buf = await new Response(ds.readable).arrayBuffer();
|
|
621
|
+
return new Uint8Array(buf);
|
|
622
|
+
}
|
|
623
|
+
function concatChunks(chunks) {
|
|
624
|
+
const total = chunks.reduce((s, c) => s + c.byteLength, 0);
|
|
625
|
+
const out = new Uint8Array(total);
|
|
626
|
+
let offset = 0;
|
|
627
|
+
for (const c of chunks) {
|
|
628
|
+
out.set(c, offset);
|
|
629
|
+
offset += c.byteLength;
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
function chunkAAD(eTag, chunkIndex, chunkCount) {
|
|
634
|
+
return new TextEncoder().encode(`${eTag}:${chunkIndex}:${chunkCount}`);
|
|
635
|
+
}
|
|
636
|
+
var BlobSet = class {
|
|
637
|
+
store;
|
|
638
|
+
vault;
|
|
639
|
+
collection;
|
|
640
|
+
recordId;
|
|
641
|
+
getDEK;
|
|
642
|
+
encrypted;
|
|
643
|
+
userId;
|
|
644
|
+
maxBlobBytes;
|
|
645
|
+
constructor(opts) {
|
|
646
|
+
this.store = opts.store;
|
|
647
|
+
this.vault = opts.vault;
|
|
648
|
+
this.collection = opts.collection;
|
|
649
|
+
this.recordId = opts.recordId;
|
|
650
|
+
this.getDEK = opts.getDEK;
|
|
651
|
+
this.encrypted = opts.encrypted;
|
|
652
|
+
this.userId = opts.userId;
|
|
653
|
+
this.maxBlobBytes = opts.maxBlobBytes;
|
|
654
|
+
}
|
|
655
|
+
/** The internal collection that holds slot metadata for this collection's blobs. */
|
|
656
|
+
get slotsCollection() {
|
|
657
|
+
return `${BLOB_SLOTS_PREFIX}${this.collection}`;
|
|
658
|
+
}
|
|
659
|
+
/** The internal collection that holds published versions for this collection's blobs. */
|
|
660
|
+
get versionsCollection() {
|
|
661
|
+
return `${BLOB_VERSIONS_PREFIX}${this.collection}`;
|
|
662
|
+
}
|
|
663
|
+
// ─── Slot Metadata I/O (CAS-protected) ─────────────────────────────
|
|
664
|
+
async loadSlots() {
|
|
665
|
+
const envelope = await this.store.get(this.vault, this.slotsCollection, this.recordId);
|
|
666
|
+
if (!envelope) return { slots: {}, version: 0 };
|
|
667
|
+
if (!this.encrypted) {
|
|
668
|
+
return {
|
|
669
|
+
slots: JSON.parse(envelope._data),
|
|
670
|
+
version: envelope._v
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const dek = await this.getDEK(this.collection);
|
|
674
|
+
const json = await decrypt(envelope._iv, envelope._data, dek);
|
|
675
|
+
return {
|
|
676
|
+
slots: JSON.parse(json),
|
|
677
|
+
version: envelope._v
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async saveSlots(slots, currentVersion) {
|
|
681
|
+
const json = JSON.stringify(slots);
|
|
682
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
683
|
+
let envelope;
|
|
684
|
+
if (this.encrypted) {
|
|
685
|
+
const dek = await this.getDEK(this.collection);
|
|
686
|
+
const { iv, data } = await encrypt(json, dek);
|
|
687
|
+
envelope = {
|
|
688
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
689
|
+
_v: currentVersion + 1,
|
|
690
|
+
_ts: now,
|
|
691
|
+
_iv: iv,
|
|
692
|
+
_data: data
|
|
693
|
+
};
|
|
694
|
+
} else {
|
|
695
|
+
envelope = {
|
|
696
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
697
|
+
_v: currentVersion + 1,
|
|
698
|
+
_ts: now,
|
|
699
|
+
_iv: "",
|
|
700
|
+
_data: json
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
await this.store.put(
|
|
704
|
+
this.vault,
|
|
705
|
+
this.slotsCollection,
|
|
706
|
+
this.recordId,
|
|
707
|
+
envelope,
|
|
708
|
+
currentVersion > 0 ? currentVersion : void 0
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* CAS retry loop for slot metadata updates. Re-reads slots on conflict
|
|
713
|
+
* and re-applies the mutation function.
|
|
714
|
+
*/
|
|
715
|
+
async casUpdateSlots(mutate) {
|
|
716
|
+
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
717
|
+
const { slots, version } = await this.loadSlots();
|
|
718
|
+
const updated = mutate(slots);
|
|
719
|
+
if (updated === null) return;
|
|
720
|
+
try {
|
|
721
|
+
await this.saveSlots(updated, version);
|
|
722
|
+
return;
|
|
723
|
+
} catch (err) {
|
|
724
|
+
if (err instanceof ConflictError && attempt < MAX_CAS_RETRIES - 1) continue;
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// ─── Blob Index I/O (versioned for CAS refCount) ──────────────────
|
|
730
|
+
async loadBlobObject(eTag) {
|
|
731
|
+
const envelope = await this.store.get(this.vault, BLOB_INDEX_COLLECTION, eTag);
|
|
732
|
+
if (!envelope) return null;
|
|
733
|
+
if (!this.encrypted) {
|
|
734
|
+
return { blob: JSON.parse(envelope._data), version: envelope._v };
|
|
735
|
+
}
|
|
736
|
+
const dek = await this.getDEK(BLOB_COLLECTION);
|
|
737
|
+
const json = await decrypt(envelope._iv, envelope._data, dek);
|
|
738
|
+
return { blob: JSON.parse(json), version: envelope._v };
|
|
739
|
+
}
|
|
740
|
+
async writeBlobObject(blob, expectedVersion) {
|
|
741
|
+
const json = JSON.stringify(blob);
|
|
742
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
743
|
+
const newVersion = (expectedVersion ?? 0) + 1;
|
|
744
|
+
let envelope;
|
|
745
|
+
if (this.encrypted) {
|
|
746
|
+
const dek = await this.getDEK(BLOB_COLLECTION);
|
|
747
|
+
const { iv, data } = await encrypt(json, dek);
|
|
748
|
+
envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: newVersion, _ts: now, _iv: iv, _data: data };
|
|
749
|
+
} else {
|
|
750
|
+
envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: newVersion, _ts: now, _iv: "", _data: json };
|
|
751
|
+
}
|
|
752
|
+
await this.store.put(
|
|
753
|
+
this.vault,
|
|
754
|
+
BLOB_INDEX_COLLECTION,
|
|
755
|
+
blob.eTag,
|
|
756
|
+
envelope,
|
|
757
|
+
expectedVersion
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* CAS retry loop for refCount changes on a BlobObject.
|
|
762
|
+
*/
|
|
763
|
+
async casUpdateRefCount(eTag, delta) {
|
|
764
|
+
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
765
|
+
const result = await this.loadBlobObject(eTag);
|
|
766
|
+
if (!result) throw new NotFoundError(`BlobObject ${eTag} not found`);
|
|
767
|
+
const { blob, version } = result;
|
|
768
|
+
const updated = { ...blob, refCount: blob.refCount + delta };
|
|
769
|
+
try {
|
|
770
|
+
await this.writeBlobObject(updated, version);
|
|
771
|
+
return;
|
|
772
|
+
} catch (err) {
|
|
773
|
+
if (err instanceof ConflictError && attempt < MAX_CAS_RETRIES - 1) continue;
|
|
774
|
+
throw err;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// ─── Chunk I/O (with AAD binding) ─────────────────────────────────
|
|
779
|
+
async writeChunk(eTag, index, chunkCount, chunk, dek) {
|
|
780
|
+
const id = `${eTag}_${index}`;
|
|
781
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
782
|
+
let envelope;
|
|
783
|
+
if (dek) {
|
|
784
|
+
const aad = chunkAAD(eTag, index, chunkCount);
|
|
785
|
+
const { iv, data } = await encryptBytesWithAAD(chunk, dek, aad);
|
|
786
|
+
envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: now, _iv: iv, _data: data };
|
|
787
|
+
} else {
|
|
788
|
+
envelope = {
|
|
789
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
790
|
+
_v: 1,
|
|
791
|
+
_ts: now,
|
|
792
|
+
_iv: "",
|
|
793
|
+
_data: bufferToBase64(chunk)
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
await this.store.put(this.vault, BLOB_CHUNKS_COLLECTION, id, envelope);
|
|
797
|
+
}
|
|
798
|
+
async readChunk(eTag, index, chunkCount, dek) {
|
|
799
|
+
const envelope = await this.store.get(this.vault, BLOB_CHUNKS_COLLECTION, `${eTag}_${index}`);
|
|
800
|
+
if (!envelope) return null;
|
|
801
|
+
if (dek) {
|
|
802
|
+
const aad = chunkAAD(eTag, index, chunkCount);
|
|
803
|
+
return await decryptBytesWithAAD(envelope._iv, envelope._data, dek, aad);
|
|
804
|
+
}
|
|
805
|
+
return base64ToBuffer(envelope._data);
|
|
806
|
+
}
|
|
807
|
+
// ─── Version record I/O ───────────────────────────────────────────
|
|
808
|
+
versionKey(slotName, label) {
|
|
809
|
+
return `${this.recordId}::${slotName}::${label}`;
|
|
810
|
+
}
|
|
811
|
+
async loadVersionRecord(slotName, label) {
|
|
812
|
+
const key = this.versionKey(slotName, label);
|
|
813
|
+
const envelope = await this.store.get(this.vault, this.versionsCollection, key);
|
|
814
|
+
if (!envelope) return null;
|
|
815
|
+
if (!this.encrypted) {
|
|
816
|
+
return JSON.parse(envelope._data);
|
|
817
|
+
}
|
|
818
|
+
const dek = await this.getDEK(this.collection);
|
|
819
|
+
const json = await decrypt(envelope._iv, envelope._data, dek);
|
|
820
|
+
return JSON.parse(json);
|
|
821
|
+
}
|
|
822
|
+
async writeVersionRecord(slotName, record) {
|
|
823
|
+
const key = this.versionKey(slotName, record.label);
|
|
824
|
+
const json = JSON.stringify(record);
|
|
825
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
826
|
+
let envelope;
|
|
827
|
+
if (this.encrypted) {
|
|
828
|
+
const dek = await this.getDEK(this.collection);
|
|
829
|
+
const { iv, data } = await encrypt(json, dek);
|
|
830
|
+
envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: now, _iv: iv, _data: data };
|
|
831
|
+
} else {
|
|
832
|
+
envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: now, _iv: "", _data: json };
|
|
833
|
+
}
|
|
834
|
+
await this.store.put(this.vault, this.versionsCollection, key, envelope);
|
|
835
|
+
}
|
|
836
|
+
async deleteVersionRecord(slotName, label) {
|
|
837
|
+
const key = this.versionKey(slotName, label);
|
|
838
|
+
await this.store.delete(this.vault, this.versionsCollection, key);
|
|
839
|
+
}
|
|
840
|
+
// ─── Effective chunk size ─────────────────────────────────────────
|
|
841
|
+
effectiveChunkSize(opts) {
|
|
842
|
+
if (opts?.chunkSize) return opts.chunkSize;
|
|
843
|
+
if (this.maxBlobBytes) return this.maxBlobBytes;
|
|
844
|
+
return DEFAULT_CHUNK_SIZE;
|
|
845
|
+
}
|
|
846
|
+
// ─── Fetch all chunks for a blob ──────────────────────────────────
|
|
847
|
+
async fetchAllChunks(blob) {
|
|
848
|
+
const blobDEK = this.encrypted ? await this.getDEK(BLOB_COLLECTION) : null;
|
|
849
|
+
const chunks = [];
|
|
850
|
+
for (let i = 0; i < blob.chunkCount; i++) {
|
|
851
|
+
const chunk = await this.readChunk(blob.eTag, i, blob.chunkCount, blobDEK);
|
|
852
|
+
if (!chunk) {
|
|
853
|
+
throw new NotFoundError(
|
|
854
|
+
`Blob chunk ${i}/${blob.chunkCount} missing for eTag "${blob.eTag}" on record "${this.recordId}"`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
chunks.push(chunk);
|
|
858
|
+
}
|
|
859
|
+
const assembled = concatChunks(chunks);
|
|
860
|
+
return blob.compression === "gzip" ? await decompressBytes(assembled) : assembled;
|
|
861
|
+
}
|
|
862
|
+
// ─── Public API: Slot management ──────────────────────────────────
|
|
863
|
+
/**
|
|
864
|
+
* Upload bytes and attach them to this record under `slotName`.
|
|
865
|
+
*
|
|
866
|
+
* 1. Computes `eTag = HMAC-SHA-256(blobDEK, plaintext)` for keyed content-addressing.
|
|
867
|
+
* 2. Auto-detects MIME type from magic bytes if not provided.
|
|
868
|
+
* 3. If a blob with this eTag already exists, skips chunk upload (deduplication)
|
|
869
|
+
* and CAS-increments refCount.
|
|
870
|
+
* 4. Otherwise: compresses → splits into chunks → encrypts each chunk with
|
|
871
|
+
* AAD binding → writes `_blob_chunks` → writes `BlobObject` to `_blob_index`.
|
|
872
|
+
* 5. CAS-updates the slot metadata in `_blob_slots_{collection}`.
|
|
873
|
+
* If overwriting an existing slot, decrements the old eTag's refCount.
|
|
874
|
+
*/
|
|
875
|
+
async put(slotName, data, opts) {
|
|
876
|
+
const blobDEK = this.encrypted ? await this.getDEK(BLOB_COLLECTION) : null;
|
|
877
|
+
const eTag = blobDEK ? await hmacSha256Hex(blobDEK, data) : await plainSha256Hex(data);
|
|
878
|
+
let mimeType = opts?.mimeType;
|
|
879
|
+
if (!mimeType) {
|
|
880
|
+
const detected = detectMagic(data.subarray(0, 16));
|
|
881
|
+
if (detected) mimeType = detected.mime;
|
|
882
|
+
}
|
|
883
|
+
let shouldCompress;
|
|
884
|
+
if (opts?.compress !== void 0) {
|
|
885
|
+
shouldCompress = opts.compress;
|
|
886
|
+
} else if (mimeType && isPreCompressed(mimeType)) {
|
|
887
|
+
shouldCompress = false;
|
|
888
|
+
} else {
|
|
889
|
+
shouldCompress = true;
|
|
890
|
+
}
|
|
891
|
+
const existingBlob = await this.loadBlobObject(eTag);
|
|
892
|
+
if (existingBlob) {
|
|
893
|
+
await this.casUpdateRefCount(eTag, 1);
|
|
894
|
+
} else {
|
|
895
|
+
const { bytes: compressed, algorithm } = shouldCompress ? await compressBytes(data) : { bytes: data, algorithm: "none" };
|
|
896
|
+
const chunkSize = this.effectiveChunkSize(opts);
|
|
897
|
+
const chunkCount = Math.max(1, Math.ceil(compressed.byteLength / chunkSize));
|
|
898
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
899
|
+
const start = i * chunkSize;
|
|
900
|
+
await this.writeChunk(
|
|
901
|
+
eTag,
|
|
902
|
+
i,
|
|
903
|
+
chunkCount,
|
|
904
|
+
compressed.subarray(start, start + chunkSize),
|
|
905
|
+
blobDEK
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
await this.writeBlobObject({
|
|
909
|
+
eTag,
|
|
910
|
+
size: data.byteLength,
|
|
911
|
+
compressedSize: compressed.byteLength,
|
|
912
|
+
compression: algorithm,
|
|
913
|
+
chunkSize,
|
|
914
|
+
chunkCount,
|
|
915
|
+
...mimeType !== void 0 ? { mimeType } : {},
|
|
916
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
917
|
+
refCount: 1
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const uploaderUserId = opts?.uploadedBy ?? this.userId;
|
|
921
|
+
await this.casUpdateSlots((slots) => {
|
|
922
|
+
const oldETag = slots[slotName]?.eTag;
|
|
923
|
+
slots[slotName] = {
|
|
924
|
+
eTag,
|
|
925
|
+
filename: slotName,
|
|
926
|
+
size: data.byteLength,
|
|
927
|
+
...mimeType !== void 0 ? { mimeType } : {},
|
|
928
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
929
|
+
...uploaderUserId !== void 0 ? { uploadedBy: uploaderUserId } : {}
|
|
930
|
+
};
|
|
931
|
+
if (oldETag && oldETag !== eTag) {
|
|
932
|
+
this._deferredRefDecrement = oldETag;
|
|
933
|
+
}
|
|
934
|
+
return slots;
|
|
935
|
+
});
|
|
936
|
+
if (this._deferredRefDecrement) {
|
|
937
|
+
const oldETag = this._deferredRefDecrement;
|
|
938
|
+
this._deferredRefDecrement = void 0;
|
|
939
|
+
await this.casUpdateRefCount(oldETag, -1).catch(() => {
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
_deferredRefDecrement;
|
|
944
|
+
/**
|
|
945
|
+
* Fetch all bytes for the named slot.
|
|
946
|
+
* Returns `null` if the slot does not exist.
|
|
947
|
+
* Throws `NotFoundError` if the index entry exists but a chunk is missing.
|
|
948
|
+
*/
|
|
949
|
+
async get(slotName) {
|
|
950
|
+
const { slots } = await this.loadSlots();
|
|
951
|
+
const slot = slots[slotName];
|
|
952
|
+
if (!slot) return null;
|
|
953
|
+
const result = await this.loadBlobObject(slot.eTag);
|
|
954
|
+
if (!result) return null;
|
|
955
|
+
return this.fetchAllChunks(result.blob);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* List all slot entries for this record.
|
|
959
|
+
* Returns metadata only — no chunk data is loaded.
|
|
960
|
+
*/
|
|
961
|
+
async list() {
|
|
962
|
+
const { slots } = await this.loadSlots();
|
|
963
|
+
return Object.entries(slots).map(([name, slot]) => ({ name, ...slot }));
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Delete the named slot from this record.
|
|
967
|
+
* Decrements refCount on the blob. Chunks are GC'd by `vault.blobGC()`.
|
|
968
|
+
*/
|
|
969
|
+
async delete(slotName) {
|
|
970
|
+
let eTagToDecrement;
|
|
971
|
+
await this.casUpdateSlots((slots) => {
|
|
972
|
+
if (!(slotName in slots)) return null;
|
|
973
|
+
eTagToDecrement = slots[slotName].eTag;
|
|
974
|
+
delete slots[slotName];
|
|
975
|
+
return slots;
|
|
976
|
+
});
|
|
977
|
+
if (eTagToDecrement) {
|
|
978
|
+
await this.casUpdateRefCount(eTagToDecrement, -1).catch(() => {
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Return a native `Response` whose body streams the decrypted,
|
|
984
|
+
* decompressed blob bytes with full HTTP metadata headers.
|
|
985
|
+
*
|
|
986
|
+
* Note: implementation is buffered — all chunks are loaded into
|
|
987
|
+
* memory before being enqueued. True streaming deferred to.
|
|
988
|
+
*
|
|
989
|
+
* Returns `null` if the slot does not exist.
|
|
990
|
+
*/
|
|
991
|
+
async response(slotName, opts) {
|
|
992
|
+
const { slots } = await this.loadSlots();
|
|
993
|
+
const slot = slots[slotName];
|
|
994
|
+
if (!slot) return null;
|
|
995
|
+
const result = await this.loadBlobObject(slot.eTag);
|
|
996
|
+
if (!result) return null;
|
|
997
|
+
return this.buildResponse(slot, result.blob, opts);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Decrypt the slot and wrap the bytes in a browser ObjectURL ready
|
|
1001
|
+
* to feed into `<img src>`, `<a href>`, etc. The caller MUST call
|
|
1002
|
+
* `revoke()` when the URL is no longer needed — otherwise the URL
|
|
1003
|
+
* (and the underlying decrypted Blob) are pinned for the lifetime
|
|
1004
|
+
* of the document, which leaks memory in long-lived pages.
|
|
1005
|
+
*
|
|
1006
|
+
* Returns `null` when the slot does not exist.
|
|
1007
|
+
*
|
|
1008
|
+
* Throws when `URL.createObjectURL` is unavailable in the host
|
|
1009
|
+
* environment (Node without DOM, restricted workers). Framework
|
|
1010
|
+
* adapters — `useBlobURL` in `@noy-db/in-vue`, etc. — guard against
|
|
1011
|
+
* this for SSR contexts and stay at `null` instead of propagating.
|
|
1012
|
+
*/
|
|
1013
|
+
async objectURL(slotName, opts) {
|
|
1014
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
"BlobSet.objectURL: URL.createObjectURL is unavailable in this environment. Call this from the browser, or use BlobSet.get() and create the URL yourself."
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const bytes = await this.get(slotName);
|
|
1020
|
+
if (!bytes) return null;
|
|
1021
|
+
const { slots } = await this.loadSlots();
|
|
1022
|
+
const slot = slots[slotName];
|
|
1023
|
+
const type = opts?.mimeType ?? slot?.mimeType ?? "application/octet-stream";
|
|
1024
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1025
|
+
const blob = new Blob([buffer], { type });
|
|
1026
|
+
const url = URL.createObjectURL(blob);
|
|
1027
|
+
let revoked = false;
|
|
1028
|
+
const revoke = () => {
|
|
1029
|
+
if (revoked) return;
|
|
1030
|
+
revoked = true;
|
|
1031
|
+
URL.revokeObjectURL(url);
|
|
1032
|
+
};
|
|
1033
|
+
return { url, revoke };
|
|
1034
|
+
}
|
|
1035
|
+
// ─── Public API: Published versions (UC-3 amendment versioning) ───
|
|
1036
|
+
/**
|
|
1037
|
+
* Publish the current slot content as a named version snapshot.
|
|
1038
|
+
*
|
|
1039
|
+
* The published version holds an independent refCount reference to
|
|
1040
|
+
* the blob. Even if the slot is later overwritten or deleted, the
|
|
1041
|
+
* published version keeps the blob data alive.
|
|
1042
|
+
*
|
|
1043
|
+
* Publishing with an existing label overwrites it — if the eTags differ,
|
|
1044
|
+
* refCounts are adjusted accordingly.
|
|
1045
|
+
*/
|
|
1046
|
+
async publish(slotName, label) {
|
|
1047
|
+
const { slots } = await this.loadSlots();
|
|
1048
|
+
const slot = slots[slotName];
|
|
1049
|
+
if (!slot) throw new NotFoundError(`Slot "${slotName}" not found on record "${this.recordId}"`);
|
|
1050
|
+
const existing = await this.loadVersionRecord(slotName, label);
|
|
1051
|
+
if (existing && existing.eTag === slot.eTag) return;
|
|
1052
|
+
const record = {
|
|
1053
|
+
label,
|
|
1054
|
+
eTag: slot.eTag,
|
|
1055
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1056
|
+
...this.userId !== void 0 ? { publishedBy: this.userId } : {}
|
|
1057
|
+
};
|
|
1058
|
+
await this.writeVersionRecord(slotName, record);
|
|
1059
|
+
await this.casUpdateRefCount(slot.eTag, 1);
|
|
1060
|
+
if (existing && existing.eTag !== slot.eTag) {
|
|
1061
|
+
await this.casUpdateRefCount(existing.eTag, -1).catch(() => {
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Fetch bytes for a published version.
|
|
1067
|
+
* Returns `null` if the version does not exist.
|
|
1068
|
+
*/
|
|
1069
|
+
async getVersion(slotName, label) {
|
|
1070
|
+
const record = await this.loadVersionRecord(slotName, label);
|
|
1071
|
+
if (!record) return null;
|
|
1072
|
+
const result = await this.loadBlobObject(record.eTag);
|
|
1073
|
+
if (!result) return null;
|
|
1074
|
+
return this.fetchAllChunks(result.blob);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* List all published versions for a slot.
|
|
1078
|
+
*/
|
|
1079
|
+
async listVersions(slotName) {
|
|
1080
|
+
const prefix = `${this.recordId}::${slotName}::`;
|
|
1081
|
+
const allKeys = await this.store.list(this.vault, this.versionsCollection);
|
|
1082
|
+
const matchingKeys = allKeys.filter((k) => k.startsWith(prefix));
|
|
1083
|
+
const versions = [];
|
|
1084
|
+
for (const key of matchingKeys) {
|
|
1085
|
+
const envelope = await this.store.get(this.vault, this.versionsCollection, key);
|
|
1086
|
+
if (!envelope) continue;
|
|
1087
|
+
if (!this.encrypted) {
|
|
1088
|
+
versions.push(JSON.parse(envelope._data));
|
|
1089
|
+
} else {
|
|
1090
|
+
const dek = await this.getDEK(this.collection);
|
|
1091
|
+
const json = await decrypt(envelope._iv, envelope._data, dek);
|
|
1092
|
+
versions.push(JSON.parse(json));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return versions;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Delete a published version. Decrements refCount on its blob.
|
|
1099
|
+
*/
|
|
1100
|
+
async deleteVersion(slotName, label) {
|
|
1101
|
+
const record = await this.loadVersionRecord(slotName, label);
|
|
1102
|
+
if (!record) return;
|
|
1103
|
+
await this.deleteVersionRecord(slotName, label);
|
|
1104
|
+
await this.casUpdateRefCount(record.eTag, -1).catch(() => {
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Return a `Response` for a published version — same as `response()`
|
|
1109
|
+
* but reads from the version record's eTag instead of the current slot.
|
|
1110
|
+
*/
|
|
1111
|
+
async responseVersion(slotName, label, opts) {
|
|
1112
|
+
const record = await this.loadVersionRecord(slotName, label);
|
|
1113
|
+
if (!record) return null;
|
|
1114
|
+
const result = await this.loadBlobObject(record.eTag);
|
|
1115
|
+
if (!result) return null;
|
|
1116
|
+
const slotLike = {
|
|
1117
|
+
eTag: record.eTag,
|
|
1118
|
+
filename: opts?.filename ?? `${slotName}-${label}`,
|
|
1119
|
+
size: result.blob.size,
|
|
1120
|
+
...result.blob.mimeType !== void 0 ? { mimeType: result.blob.mimeType } : {},
|
|
1121
|
+
uploadedAt: record.publishedAt,
|
|
1122
|
+
...record.publishedBy !== void 0 ? { uploadedBy: record.publishedBy } : {}
|
|
1123
|
+
};
|
|
1124
|
+
return this.buildResponse(slotLike, result.blob, opts);
|
|
1125
|
+
}
|
|
1126
|
+
// ─── Diagnostics ──────────────────────────────────────────────────
|
|
1127
|
+
/**
|
|
1128
|
+
* Return the `BlobObject` metadata for the named slot.
|
|
1129
|
+
* Returns `null` if the slot or blob does not exist.
|
|
1130
|
+
*/
|
|
1131
|
+
async blobInfo(slotName) {
|
|
1132
|
+
const { slots } = await this.loadSlots();
|
|
1133
|
+
const slot = slots[slotName];
|
|
1134
|
+
if (!slot) return null;
|
|
1135
|
+
const result = await this.loadBlobObject(slot.eTag);
|
|
1136
|
+
return result?.blob ?? null;
|
|
1137
|
+
}
|
|
1138
|
+
// ─── Presigned URL (E5) ────────────────────────────────────────────
|
|
1139
|
+
/**
|
|
1140
|
+
* Generate a presigned URL for direct client download of the blob's
|
|
1141
|
+
* ciphertext. Only works when the blob store supports `presignUrl`.
|
|
1142
|
+
*
|
|
1143
|
+
* **Important:** The URL returns encrypted data. The caller must
|
|
1144
|
+
* decrypt client-side using `decryptResponse()` or a service worker.
|
|
1145
|
+
*
|
|
1146
|
+
* Returns `null` if the slot doesn't exist or the store doesn't support presigning.
|
|
1147
|
+
*/
|
|
1148
|
+
async presignedUrl(slotName, expiresInSeconds = 3600) {
|
|
1149
|
+
const { slots } = await this.loadSlots();
|
|
1150
|
+
const slot = slots[slotName];
|
|
1151
|
+
if (!slot) return null;
|
|
1152
|
+
const result = await this.loadBlobObject(slot.eTag);
|
|
1153
|
+
if (!result) return null;
|
|
1154
|
+
if (result.blob.chunkCount !== 1) return null;
|
|
1155
|
+
if (!this.store.presignUrl) return null;
|
|
1156
|
+
const chunkId = `${slot.eTag}_0`;
|
|
1157
|
+
return this.store.presignUrl(this.vault, "_blob_chunks", chunkId, expiresInSeconds);
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Decrypt a ciphertext Response (e.g. from a presigned URL fetch)
|
|
1161
|
+
* back into a plaintext Response with correct headers.
|
|
1162
|
+
*
|
|
1163
|
+
* Usage with service worker or client-side fetch:
|
|
1164
|
+
* ```ts
|
|
1165
|
+
* const url = await blobs.presignedUrl('invoice.pdf')
|
|
1166
|
+
* const cipherResponse = await fetch(url)
|
|
1167
|
+
* const plainResponse = await blobs.decryptResponse('invoice.pdf', cipherResponse)
|
|
1168
|
+
* ```
|
|
1169
|
+
*/
|
|
1170
|
+
async decryptResponse(slotName, cipherResponse) {
|
|
1171
|
+
const { slots } = await this.loadSlots();
|
|
1172
|
+
const slot = slots[slotName];
|
|
1173
|
+
if (!slot) return null;
|
|
1174
|
+
const result = await this.loadBlobObject(slot.eTag);
|
|
1175
|
+
if (!result) return null;
|
|
1176
|
+
const text = await cipherResponse.text();
|
|
1177
|
+
const envelope = JSON.parse(text);
|
|
1178
|
+
const blobDEK = this.encrypted ? await this.getDEK("_blob") : null;
|
|
1179
|
+
if (!blobDEK) {
|
|
1180
|
+
return this.buildResponse(slot, result.blob, { inline: true });
|
|
1181
|
+
}
|
|
1182
|
+
const aad = chunkAAD(slot.eTag, 0, result.blob.chunkCount);
|
|
1183
|
+
const { decryptBytesWithAAD: decryptAAD } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1184
|
+
const decrypted = await decryptAAD(envelope._iv, envelope._data, blobDEK, aad);
|
|
1185
|
+
const plaintext = result.blob.compression === "gzip" ? await decompressBytes(decrypted) : decrypted;
|
|
1186
|
+
const body = new ReadableStream({
|
|
1187
|
+
start(controller) {
|
|
1188
|
+
controller.enqueue(plaintext);
|
|
1189
|
+
controller.close();
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
const filename = slot.filename;
|
|
1193
|
+
return new Response(body, {
|
|
1194
|
+
headers: {
|
|
1195
|
+
"Content-Type": slot.mimeType ?? "application/octet-stream",
|
|
1196
|
+
"Content-Length": String(slot.size),
|
|
1197
|
+
"ETag": `"${slot.eTag}"`,
|
|
1198
|
+
"Content-Disposition": `inline; filename="${filename}"`,
|
|
1199
|
+
"Last-Modified": new Date(slot.uploadedAt).toUTCString()
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
// ─── Internal: build Response from slot + blob ────────────────────
|
|
1204
|
+
async buildResponse(slot, blob, opts) {
|
|
1205
|
+
const fetchAllChunks = this.fetchAllChunks.bind(this);
|
|
1206
|
+
const body = new ReadableStream({
|
|
1207
|
+
async start(controller) {
|
|
1208
|
+
try {
|
|
1209
|
+
const output = await fetchAllChunks(blob);
|
|
1210
|
+
controller.enqueue(output);
|
|
1211
|
+
controller.close();
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
controller.error(err);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
const filename = opts?.filename ?? slot.filename;
|
|
1218
|
+
const disposition = opts?.inline ? `inline; filename="${filename}"` : `attachment; filename="${filename}"`;
|
|
1219
|
+
return new Response(body, {
|
|
1220
|
+
headers: {
|
|
1221
|
+
"Content-Type": slot.mimeType ?? "application/octet-stream",
|
|
1222
|
+
"Content-Length": String(slot.size),
|
|
1223
|
+
"ETag": `"${slot.eTag}"`,
|
|
1224
|
+
"Content-Disposition": disposition,
|
|
1225
|
+
"Last-Modified": new Date(slot.uploadedAt).toUTCString()
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
async function plainSha256Hex(data) {
|
|
1231
|
+
return sha256Hex(data);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/blobs/active.ts
|
|
1235
|
+
function withBlobs() {
|
|
1236
|
+
return {
|
|
1237
|
+
openSlot(args) {
|
|
1238
|
+
return new BlobSet(args);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/blobs/blob-compaction.ts
|
|
1244
|
+
init_crypto();
|
|
1245
|
+
var BLOB_EVICTION_AUDIT_COLLECTION = "_blob_eviction_audit";
|
|
1246
|
+
async function runCompaction(ctx, options = {}) {
|
|
1247
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1248
|
+
const maxEvictions = options.maxEvictions ?? Infinity;
|
|
1249
|
+
const dryRun = options.dryRun === true;
|
|
1250
|
+
const allCollections = await ctx.listCollections();
|
|
1251
|
+
const byCollection = {};
|
|
1252
|
+
let evicted = 0;
|
|
1253
|
+
let records = 0;
|
|
1254
|
+
let auditEntries = 0;
|
|
1255
|
+
let collectionsWithPolicy = 0;
|
|
1256
|
+
outer: for (const collectionName of allCollections) {
|
|
1257
|
+
if (collectionName.startsWith("_")) continue;
|
|
1258
|
+
const config = ctx.getBlobFields(collectionName);
|
|
1259
|
+
if (!config) continue;
|
|
1260
|
+
const configuredSlots = Object.keys(config);
|
|
1261
|
+
if (configuredSlots.length === 0) continue;
|
|
1262
|
+
collectionsWithPolicy += 1;
|
|
1263
|
+
byCollection[collectionName] = { records: 0, evicted: 0 };
|
|
1264
|
+
const ids = await ctx.listRecords(collectionName);
|
|
1265
|
+
for (const recordId of ids) {
|
|
1266
|
+
if (evicted >= maxEvictions) break outer;
|
|
1267
|
+
const record = await ctx.getRecord(collectionName, recordId).catch(() => null);
|
|
1268
|
+
if (record === null) continue;
|
|
1269
|
+
records += 1;
|
|
1270
|
+
byCollection[collectionName].records += 1;
|
|
1271
|
+
const slots = await ctx.listSlots(collectionName, recordId).catch(() => []);
|
|
1272
|
+
for (const slot of slots) {
|
|
1273
|
+
if (evicted >= maxEvictions) break outer;
|
|
1274
|
+
const policy = config[slot.name];
|
|
1275
|
+
if (!policy) continue;
|
|
1276
|
+
const reason = evaluatePolicy(policy, record, slot, now);
|
|
1277
|
+
if (!reason) continue;
|
|
1278
|
+
if (!dryRun) {
|
|
1279
|
+
await ctx.deleteSlot(collectionName, recordId, slot.name);
|
|
1280
|
+
await writeAuditEntry(ctx, {
|
|
1281
|
+
id: generateEvictionId(collectionName, recordId, slot.name),
|
|
1282
|
+
collection: collectionName,
|
|
1283
|
+
recordId,
|
|
1284
|
+
slotName: slot.name,
|
|
1285
|
+
blobHash: slot.eTag,
|
|
1286
|
+
reason,
|
|
1287
|
+
evictedAt: now.toISOString(),
|
|
1288
|
+
actor: ctx.actor
|
|
1289
|
+
});
|
|
1290
|
+
auditEntries += 1;
|
|
1291
|
+
}
|
|
1292
|
+
evicted += 1;
|
|
1293
|
+
byCollection[collectionName].evicted += 1;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
evicted,
|
|
1299
|
+
records,
|
|
1300
|
+
collections: collectionsWithPolicy,
|
|
1301
|
+
auditEntries,
|
|
1302
|
+
byCollection
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function evaluatePolicy(policy, record, slot, now) {
|
|
1306
|
+
let ttlTriggered = false;
|
|
1307
|
+
let predicateTriggered = false;
|
|
1308
|
+
if (policy.retainDays !== void 0 && policy.retainDays > 0) {
|
|
1309
|
+
const uploadedAt = Date.parse(slot.uploadedAt);
|
|
1310
|
+
if (Number.isFinite(uploadedAt)) {
|
|
1311
|
+
const ageMs = now.getTime() - uploadedAt;
|
|
1312
|
+
const limitMs = policy.retainDays * 864e5;
|
|
1313
|
+
if (ageMs > limitMs) ttlTriggered = true;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (policy.evictWhen) {
|
|
1317
|
+
try {
|
|
1318
|
+
if (policy.evictWhen(record)) predicateTriggered = true;
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (ttlTriggered && predicateTriggered) return "both";
|
|
1323
|
+
if (ttlTriggered) return "ttl";
|
|
1324
|
+
if (predicateTriggered) return "predicate";
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
function generateEvictionId(collection, recordId, slotName) {
|
|
1328
|
+
const rand = globalThis.crypto.getRandomValues(new Uint8Array(8));
|
|
1329
|
+
let suffix = "";
|
|
1330
|
+
for (const b of rand) suffix += b.toString(16).padStart(2, "0");
|
|
1331
|
+
return `${collection}__${recordId}__${slotName}__${suffix}`;
|
|
1332
|
+
}
|
|
1333
|
+
async function writeAuditEntry(ctx, entry) {
|
|
1334
|
+
const json = JSON.stringify(entry);
|
|
1335
|
+
let envelope;
|
|
1336
|
+
if (ctx.encrypted) {
|
|
1337
|
+
const dek = await ctx.getDEK(BLOB_EVICTION_AUDIT_COLLECTION);
|
|
1338
|
+
const { iv, data } = await encrypt(json, dek);
|
|
1339
|
+
envelope = {
|
|
1340
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1341
|
+
_v: 1,
|
|
1342
|
+
_ts: entry.evictedAt,
|
|
1343
|
+
_iv: iv,
|
|
1344
|
+
_data: data,
|
|
1345
|
+
_by: entry.actor
|
|
1346
|
+
};
|
|
1347
|
+
} else {
|
|
1348
|
+
envelope = {
|
|
1349
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1350
|
+
_v: 1,
|
|
1351
|
+
_ts: entry.evictedAt,
|
|
1352
|
+
_iv: "",
|
|
1353
|
+
_data: json,
|
|
1354
|
+
_by: entry.actor
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
await ctx.adapter.put(ctx.vault, BLOB_EVICTION_AUDIT_COLLECTION, entry.id, envelope);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/blobs/export-blobs.ts
|
|
1361
|
+
var ExportBlobsAbortedError = class extends Error {
|
|
1362
|
+
constructor(reason) {
|
|
1363
|
+
super(`exportBlobs aborted: ${reason}`);
|
|
1364
|
+
this.name = "ExportBlobsAbortedError";
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
var EXPORT_AUDIT_COLLECTION = "_export_audit";
|
|
1368
|
+
function createExportBlobsHandle(actor, listAccessibleCollections, getCollection, writeAudit, options) {
|
|
1369
|
+
let aborted = false;
|
|
1370
|
+
const abort = () => {
|
|
1371
|
+
aborted = true;
|
|
1372
|
+
};
|
|
1373
|
+
if (options.signal) {
|
|
1374
|
+
if (options.signal.aborted) aborted = true;
|
|
1375
|
+
options.signal.addEventListener("abort", () => {
|
|
1376
|
+
aborted = true;
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
function assertLive() {
|
|
1380
|
+
if (aborted) throw new ExportBlobsAbortedError("aborted by caller");
|
|
1381
|
+
}
|
|
1382
|
+
const allowlist = options.collections ? new Set(options.collections) : null;
|
|
1383
|
+
let auditPromise = null;
|
|
1384
|
+
function writeAuditOnce() {
|
|
1385
|
+
if (!auditPromise) {
|
|
1386
|
+
auditPromise = writeAudit({
|
|
1387
|
+
id: generateBatchId(),
|
|
1388
|
+
mechanism: "exportBlobs",
|
|
1389
|
+
actor,
|
|
1390
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1391
|
+
collections: options.collections ?? null,
|
|
1392
|
+
predicate: Boolean(options.where),
|
|
1393
|
+
afterBlobId: options.afterBlobId ?? null
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
return auditPromise;
|
|
1397
|
+
}
|
|
1398
|
+
async function* generate() {
|
|
1399
|
+
await writeAuditOnce();
|
|
1400
|
+
assertLive();
|
|
1401
|
+
const allCollections = await listAccessibleCollections();
|
|
1402
|
+
const targets = allCollections.filter((name) => {
|
|
1403
|
+
if (name.startsWith("_")) return false;
|
|
1404
|
+
if (allowlist && !allowlist.has(name)) return false;
|
|
1405
|
+
return true;
|
|
1406
|
+
});
|
|
1407
|
+
let resumeCursorHit = options.afterBlobId === void 0;
|
|
1408
|
+
for (const collectionName of targets) {
|
|
1409
|
+
if (aborted) return;
|
|
1410
|
+
const coll = getCollection(collectionName);
|
|
1411
|
+
const records = await coll.list().catch(() => []);
|
|
1412
|
+
for (const record of records) {
|
|
1413
|
+
if (aborted) return;
|
|
1414
|
+
assertLive();
|
|
1415
|
+
const idField = record.id;
|
|
1416
|
+
if (typeof idField !== "string") continue;
|
|
1417
|
+
if (options.where && !options.where(record, { collection: collectionName, id: idField })) continue;
|
|
1418
|
+
const blobSet = coll.blob(idField);
|
|
1419
|
+
const slots = await blobSet.list().catch(() => []);
|
|
1420
|
+
for (const slot of slots) {
|
|
1421
|
+
if (aborted) return;
|
|
1422
|
+
if (!resumeCursorHit) {
|
|
1423
|
+
if (slot.eTag === options.afterBlobId) {
|
|
1424
|
+
resumeCursorHit = true;
|
|
1425
|
+
}
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
const bytes = await blobSet.get(slot.name);
|
|
1429
|
+
if (!bytes) continue;
|
|
1430
|
+
const item = {
|
|
1431
|
+
blobId: slot.eTag,
|
|
1432
|
+
recordRef: { collection: collectionName, id: idField, slot: slot.name },
|
|
1433
|
+
bytes,
|
|
1434
|
+
meta: {
|
|
1435
|
+
size: slot.size,
|
|
1436
|
+
filename: slot.filename,
|
|
1437
|
+
...slot.mimeType !== void 0 && { mimeType: slot.mimeType },
|
|
1438
|
+
...slot.uploadedAt !== void 0 && { createdAt: slot.uploadedAt }
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
yield item;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const handle = {
|
|
1447
|
+
abort,
|
|
1448
|
+
get aborted() {
|
|
1449
|
+
return aborted;
|
|
1450
|
+
},
|
|
1451
|
+
[Symbol.asyncIterator]: () => generate()
|
|
1452
|
+
};
|
|
1453
|
+
return handle;
|
|
1454
|
+
}
|
|
1455
|
+
function generateBatchId() {
|
|
1456
|
+
const raw = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
1457
|
+
let s = "";
|
|
1458
|
+
for (const b of raw) s += b.toString(16).padStart(2, "0");
|
|
1459
|
+
return `batch-${Date.now().toString(36)}-${s.slice(0, 12)}`;
|
|
1460
|
+
}
|
|
1461
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1462
|
+
0 && (module.exports = {
|
|
1463
|
+
BLOB_CHUNKS_COLLECTION,
|
|
1464
|
+
BLOB_COLLECTION,
|
|
1465
|
+
BLOB_EVICTION_AUDIT_COLLECTION,
|
|
1466
|
+
BLOB_INDEX_COLLECTION,
|
|
1467
|
+
BLOB_SLOTS_PREFIX,
|
|
1468
|
+
BLOB_VERSIONS_PREFIX,
|
|
1469
|
+
BlobSet,
|
|
1470
|
+
DEFAULT_CHUNK_SIZE,
|
|
1471
|
+
EXPORT_AUDIT_COLLECTION,
|
|
1472
|
+
ExportBlobsAbortedError,
|
|
1473
|
+
createExportBlobsHandle,
|
|
1474
|
+
detectMagic,
|
|
1475
|
+
detectMimeType,
|
|
1476
|
+
isPreCompressed,
|
|
1477
|
+
runCompaction,
|
|
1478
|
+
withBlobs
|
|
1479
|
+
});
|
|
1480
|
+
//# sourceMappingURL=index.cjs.map
|