@loro-dev/streams-crdt 0.14.0 → 0.15.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.
- package/README.md +461 -147
- package/dist/flock.d.ts +2 -2
- package/dist/flock.js +4 -3
- package/dist/flock.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/loro.d.ts +2 -2
- package/dist/loro.js +2 -2
- package/dist/{stream-id-BuXMoCvo.d.ts → stream-id-CO65-58E.d.ts} +88 -52
- package/dist/{stream-id-CEp4zEQJ.js → stream-id-CggbAC6b.js} +142 -213
- package/dist/stream-id-CggbAC6b.js.map +1 -0
- package/package.json +1 -1
- package/dist/stream-id-CEp4zEQJ.js.map +0 -1
|
@@ -208,59 +208,51 @@ const MAGIC = new Uint8Array([
|
|
|
208
208
|
67,
|
|
209
209
|
69
|
|
210
210
|
]);
|
|
211
|
-
const
|
|
212
|
-
const
|
|
211
|
+
const PROVIDER_ENVELOPE_VERSION = 2;
|
|
212
|
+
const PAYLOAD_PROTECTION_PROTOCOL = "loro-streams-crdt-payload-protection";
|
|
213
213
|
const UPDATE_BATCH_KIND = 1;
|
|
214
214
|
const SNAPSHOT_KIND = 2;
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
const textEncoder$1 = new TextEncoder();
|
|
220
|
-
const textDecoder = new TextDecoder();
|
|
215
|
+
const PROVIDER_PREFIX_BYTES = 10;
|
|
216
|
+
const MAX_PROVIDER_HEADER_BYTES = 512;
|
|
217
|
+
const MAX_PROVIDER_SEAL_OVERHEAD_BYTES = 4 * 1024;
|
|
218
|
+
const providerAadDomain = new TextEncoder().encode(`${PAYLOAD_PROTECTION_PROTOCOL}/v2\0`);
|
|
221
219
|
var PayloadProtectionError = class extends Error {
|
|
222
220
|
reason;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
super(message, options.cause == null ? void 0 : { cause: options.cause });
|
|
221
|
+
constructor(reason, message = payloadProtectionFailureMessage(reason)) {
|
|
222
|
+
super(message);
|
|
226
223
|
this.name = "PayloadProtectionError";
|
|
227
224
|
this.reason = reason;
|
|
228
|
-
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var ProviderSealContractError = class extends PayloadProtectionError {
|
|
228
|
+
constructor(message) {
|
|
229
|
+
super("encrypt_failed", message);
|
|
230
|
+
this.name = "ProviderSealContractError";
|
|
229
231
|
}
|
|
230
232
|
};
|
|
231
233
|
var PayloadProtectionRuntime = class {
|
|
232
234
|
readPolicy;
|
|
233
235
|
writePolicy;
|
|
234
|
-
|
|
235
|
-
writeKey;
|
|
236
|
-
readKeys;
|
|
237
|
-
importedWriteKey;
|
|
238
|
-
importedReadKeys = /* @__PURE__ */ new Map();
|
|
236
|
+
provider;
|
|
239
237
|
constructor(options) {
|
|
240
238
|
this.readPolicy = options.readPolicy;
|
|
241
239
|
this.writePolicy = options.writePolicy;
|
|
242
|
-
this.
|
|
243
|
-
this.writeKey = options.writeKey;
|
|
244
|
-
this.readKeys = options.readKeys;
|
|
240
|
+
this.provider = options.provider;
|
|
245
241
|
}
|
|
246
242
|
async encodeUpdateItems(clearUpdates) {
|
|
247
243
|
if (this.writePolicy === "plaintext") return clearUpdates;
|
|
248
|
-
return [await this.
|
|
244
|
+
return [await this.sealPayload("update_batch", encodeItems(clearUpdates))];
|
|
249
245
|
}
|
|
250
246
|
measureEncodedUpdateItemsByteLength(clearUpdates) {
|
|
251
247
|
if (this.writePolicy === "plaintext") return encodedItemsByteLength(clearUpdates);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const clearBatchBytes = encodedItemsByteLength(clearUpdates);
|
|
255
|
-
const keyIdBytes = textEncoder$1.encode(writeKey.id);
|
|
256
|
-
if (keyIdBytes.byteLength === 0 || keyIdBytes.byteLength > 255) throw new PayloadProtectionError("encrypt_failed", "E2EE key id must be 1..255 UTF-8 bytes", { keyId: writeKey.id });
|
|
257
|
-
return 4 + (HEADER_BYTES + keyIdBytes.byteLength + AES_GCM_NONCE_BYTES + clearBatchBytes + AES_GCM_TAG_BITS / 8);
|
|
248
|
+
if (this.provider == null) throw new PayloadProtectionError("encrypt_failed", "provider-backed E2EE writes require a provider");
|
|
249
|
+
return 4 + PROVIDER_PREFIX_BYTES + encodedItemsByteLength(clearUpdates) + this.provider.maxSealOverheadBytes;
|
|
258
250
|
}
|
|
259
251
|
async decodeUpdateItems(wireItems) {
|
|
260
252
|
const out = [];
|
|
261
253
|
for (const item of wireItems) {
|
|
262
254
|
if (isEncryptedEnvelope(item)) {
|
|
263
|
-
const clearBatch = await this.
|
|
255
|
+
const clearBatch = await this.openPayload("update_batch", item);
|
|
264
256
|
out.push(...decodeItems(clearBatch));
|
|
265
257
|
continue;
|
|
266
258
|
}
|
|
@@ -271,75 +263,76 @@ var PayloadProtectionRuntime = class {
|
|
|
271
263
|
}
|
|
272
264
|
async encodeSnapshot(snapshot) {
|
|
273
265
|
if (this.writePolicy === "plaintext") return snapshot;
|
|
274
|
-
return await this.
|
|
266
|
+
return await this.sealPayload("snapshot", snapshot);
|
|
275
267
|
}
|
|
276
268
|
async decodeSnapshot(snapshot) {
|
|
277
|
-
if (isEncryptedEnvelope(snapshot)) return await this.
|
|
269
|
+
if (isEncryptedEnvelope(snapshot)) return await this.openPayload("snapshot", snapshot);
|
|
278
270
|
this.assertPlaintextAllowed("snapshot");
|
|
279
271
|
return snapshot;
|
|
280
272
|
}
|
|
281
|
-
async
|
|
282
|
-
|
|
283
|
-
|
|
273
|
+
async sealPayload(kind, plaintext) {
|
|
274
|
+
return await this.sealProviderPayload(kind, plaintext);
|
|
275
|
+
}
|
|
276
|
+
async openPayload(expectedKind, envelope) {
|
|
277
|
+
const version = envelope[4];
|
|
278
|
+
if (version === PROVIDER_ENVELOPE_VERSION) return await this.openProviderPayload(expectedKind, envelope);
|
|
279
|
+
throw new PayloadProtectionError("invalid_envelope", `unsupported streams-crdt protected payload envelope version '${String(version)}'`);
|
|
280
|
+
}
|
|
281
|
+
async sealProviderPayload(kind, plaintext) {
|
|
282
|
+
const provider = this.provider;
|
|
283
|
+
if (provider == null) throw new PayloadProtectionError("encrypt_failed", "provider-backed E2EE writes require a provider");
|
|
284
|
+
const context = createProviderContext(kind);
|
|
285
|
+
let authenticatedHeader;
|
|
286
|
+
let additionalDataCalls = 0;
|
|
287
|
+
let result;
|
|
284
288
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
kind,
|
|
293
|
-
|
|
294
|
-
scope: this.scope
|
|
295
|
-
}))
|
|
296
|
-
}, key, toArrayBuffer(plaintext))));
|
|
297
|
-
} catch (error) {
|
|
298
|
-
if (error instanceof PayloadProtectionError) throw error;
|
|
299
|
-
throw new PayloadProtectionError("encrypt_failed", "failed to encrypt streams-crdt payload", {
|
|
300
|
-
keyId: writeKey.id,
|
|
301
|
-
cause: error
|
|
289
|
+
result = await provider.seal({
|
|
290
|
+
plaintext,
|
|
291
|
+
context,
|
|
292
|
+
additionalData: (header) => {
|
|
293
|
+
additionalDataCalls += 1;
|
|
294
|
+
if (additionalDataCalls !== 1) throw new ProviderSealContractError("payload protection provider must request additionalData exactly once");
|
|
295
|
+
authenticatedHeader = normalizeProviderHeader(header).slice();
|
|
296
|
+
return createProviderAad(createProviderEnvelopeHeader(kind, authenticatedHeader));
|
|
297
|
+
}
|
|
302
298
|
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof ProviderSealContractError) throw error;
|
|
301
|
+
const reason = error instanceof PayloadProtectionError ? error.reason : "encrypt_failed";
|
|
302
|
+
throw new PayloadProtectionError(reason, payloadProtectionFailureMessage(reason));
|
|
303
303
|
}
|
|
304
|
-
}
|
|
305
|
-
async decryptPayload(expectedKind, envelope) {
|
|
306
|
-
const parsed = parseEnvelope(envelope);
|
|
307
|
-
if (parsed.kind !== expectedKind) throw new PayloadProtectionError("wrong_payload_kind", `expected ${expectedKind} payload, got ${parsed.kind}`, { keyId: parsed.keyId });
|
|
308
|
-
if (this.scope == null) throw new PayloadProtectionError("missing_read_key", "E2EE read decryption requires encryption.scope", { keyId: parsed.keyId });
|
|
309
|
-
const key = await this.getReadKey(parsed.keyId);
|
|
310
304
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
keyId: parsed.keyId,
|
|
318
|
-
scope: this.scope
|
|
319
|
-
}))
|
|
320
|
-
}, key, toArrayBuffer(parsed.ciphertext)));
|
|
305
|
+
const returnedHeader = normalizeProviderHeader(result.header);
|
|
306
|
+
if (additionalDataCalls !== 1) throw new ProviderSealContractError("payload protection provider must request additionalData exactly once");
|
|
307
|
+
if (authenticatedHeader == null || !equalBytes(authenticatedHeader, returnedHeader)) throw new ProviderSealContractError("payload protection provider must authenticate the exact header it returns");
|
|
308
|
+
if (!(result.sealed instanceof Uint8Array) || result.sealed.byteLength === 0) throw new ProviderSealContractError("payload protection provider returned empty or invalid sealed bytes");
|
|
309
|
+
if (returnedHeader.byteLength + result.sealed.byteLength - plaintext.byteLength > provider.maxSealOverheadBytes) throw new ProviderSealContractError("payload protection provider exceeded maxSealOverheadBytes");
|
|
310
|
+
return concatBytes(createProviderEnvelopeHeader(kind, returnedHeader), result.sealed);
|
|
321
311
|
} catch (error) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
});
|
|
312
|
+
if (error instanceof ProviderSealContractError) throw error;
|
|
313
|
+
const reason = error instanceof PayloadProtectionError ? error.reason : "encrypt_failed";
|
|
314
|
+
throw new PayloadProtectionError(reason, payloadProtectionFailureMessage(reason));
|
|
326
315
|
}
|
|
327
316
|
}
|
|
328
|
-
async
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
317
|
+
async openProviderPayload(expectedKind, envelope) {
|
|
318
|
+
const parsed = parseProviderEnvelope(envelope);
|
|
319
|
+
if (parsed.kind !== expectedKind) throw new PayloadProtectionError("wrong_payload_kind", `expected ${expectedKind} payload, got ${parsed.kind}`);
|
|
320
|
+
const provider = this.provider;
|
|
321
|
+
if (provider == null) throw new PayloadProtectionError("missing_read_key", "provider-backed E2EE reads require a provider");
|
|
322
|
+
const context = createProviderContext(parsed.kind);
|
|
323
|
+
try {
|
|
324
|
+
const plaintext = await provider.open({
|
|
325
|
+
sealed: parsed.sealed,
|
|
326
|
+
header: parsed.header,
|
|
327
|
+
context,
|
|
328
|
+
additionalData: createProviderAad(parsed.authenticatedHeader)
|
|
329
|
+
});
|
|
330
|
+
if (!(plaintext instanceof Uint8Array)) throw new PayloadProtectionError("decrypt_failed", "payload protection provider returned invalid plaintext bytes");
|
|
331
|
+
return plaintext;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
const reason = error instanceof PayloadProtectionError ? error.reason : "decrypt_failed";
|
|
334
|
+
throw new PayloadProtectionError(reason, payloadProtectionFailureMessage(reason));
|
|
335
|
+
}
|
|
343
336
|
}
|
|
344
337
|
assertPlaintextAllowed(label) {
|
|
345
338
|
if (this.readPolicy === "allow-plaintext") return;
|
|
@@ -352,62 +345,74 @@ function normalizePayloadProtection(options, label = "payloadProtection") {
|
|
|
352
345
|
const writePolicy = options.writePolicy ?? "encrypt";
|
|
353
346
|
if (readPolicy !== "encrypted-only" && readPolicy !== "allow-plaintext") throw new Error(`${label}.readPolicy must be encrypted-only or allow-plaintext`);
|
|
354
347
|
if (writePolicy !== "encrypt" && writePolicy !== "plaintext") throw new Error(`${label}.writePolicy must be encrypt or plaintext`);
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
if (writePolicy === "encrypt" &&
|
|
358
|
-
if (writePolicy === "encrypt" && encryption?.scope == null) throw new Error(`${label}.encryption.scope is required when writePolicy is encrypt`);
|
|
359
|
-
validateKey(encryption?.writeKey, `${label}.encryption.writeKey`);
|
|
360
|
-
const readKeys = normalizeReadKeys(encryption?.readKeys, `${label}.encryption.readKeys`);
|
|
348
|
+
const provider = options.provider;
|
|
349
|
+
if (provider != null && (typeof provider.seal !== "function" || typeof provider.open !== "function" || !Number.isSafeInteger(provider.maxSealOverheadBytes) || provider.maxSealOverheadBytes < 0 || provider.maxSealOverheadBytes > MAX_PROVIDER_SEAL_OVERHEAD_BYTES)) throw new Error(`${label}.provider must implement seal()/open() and declare maxSealOverheadBytes as a safe integer from 0 through ${MAX_PROVIDER_SEAL_OVERHEAD_BYTES}`);
|
|
350
|
+
if (writePolicy === "encrypt" && provider == null) throw new Error(`${label}.provider is required when writePolicy is encrypt`);
|
|
361
351
|
return new PayloadProtectionRuntime({
|
|
362
352
|
readPolicy,
|
|
363
353
|
writePolicy,
|
|
364
|
-
|
|
365
|
-
writeKey: encryption?.writeKey,
|
|
366
|
-
readKeys
|
|
354
|
+
provider
|
|
367
355
|
});
|
|
368
356
|
}
|
|
369
357
|
function isPayloadProtectionError(error) {
|
|
370
358
|
return error instanceof PayloadProtectionError;
|
|
371
359
|
}
|
|
360
|
+
function payloadProtectionFailureMessage(reason) {
|
|
361
|
+
switch (reason) {
|
|
362
|
+
case "plaintext_forbidden": return "plaintext payload rejected by protected-read policy";
|
|
363
|
+
case "missing_read_key": return "protected payload key is unavailable";
|
|
364
|
+
case "decrypt_failed": return "protected payload authentication failed";
|
|
365
|
+
case "invalid_envelope": return "invalid protected payload envelope";
|
|
366
|
+
case "wrong_payload_kind": return "protected payload kind mismatch";
|
|
367
|
+
case "encrypt_failed": return "protected payload sealing failed";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
372
370
|
function isEncryptedEnvelope(payload) {
|
|
373
371
|
return payload.byteLength >= MAGIC.byteLength && MAGIC.every((byte, index) => payload[index] === byte);
|
|
374
372
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
373
|
+
function createProviderContext(kind) {
|
|
374
|
+
return {
|
|
375
|
+
protocol: PAYLOAD_PROTECTION_PROTOCOL,
|
|
376
|
+
version: PROVIDER_ENVELOPE_VERSION,
|
|
377
|
+
kind
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function createProviderEnvelopeHeader(kind, providerHeader) {
|
|
381
|
+
const normalizedHeader = normalizeProviderHeader(providerHeader);
|
|
382
|
+
const header = new Uint8Array(PROVIDER_PREFIX_BYTES + normalizedHeader.byteLength);
|
|
383
|
+
const view = new DataView(header.buffer);
|
|
380
384
|
header.set(MAGIC, 0);
|
|
381
|
-
header[4] =
|
|
385
|
+
header[4] = PROVIDER_ENVELOPE_VERSION;
|
|
382
386
|
header[5] = encodeKind(kind);
|
|
383
|
-
|
|
384
|
-
header[
|
|
385
|
-
header[8] = nonce.byteLength;
|
|
387
|
+
view.setUint16(6, normalizedHeader.byteLength, false);
|
|
388
|
+
header[8] = 0;
|
|
386
389
|
header[9] = 0;
|
|
387
|
-
header.set(
|
|
388
|
-
header.set(nonce, HEADER_BYTES + keyIdBytes.byteLength);
|
|
390
|
+
header.set(normalizedHeader, PROVIDER_PREFIX_BYTES);
|
|
389
391
|
return header;
|
|
390
392
|
}
|
|
391
|
-
function
|
|
392
|
-
if (!isEncryptedEnvelope(payload) || payload.byteLength <
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
const reserved = payload[9];
|
|
398
|
-
if (version !== ENVELOPE_VERSION || suite !== SUITE_AES_256_GCM || nonceLen !== AES_GCM_NONCE_BYTES || reserved !== 0) throw new PayloadProtectionError("invalid_envelope", "unsupported streams-crdt encrypted payload envelope");
|
|
399
|
-
const kind = decodeKind(payload[5]);
|
|
400
|
-
const keyIdStart = HEADER_BYTES;
|
|
401
|
-
const nonceStart = keyIdStart + keyIdLen;
|
|
402
|
-
const ciphertextStart = nonceStart + nonceLen;
|
|
403
|
-
if (keyIdLen === 0 || payload.byteLength <= ciphertextStart) throw new PayloadProtectionError("invalid_envelope", "truncated streams-crdt encrypted payload envelope");
|
|
393
|
+
function parseProviderEnvelope(payload) {
|
|
394
|
+
if (!isEncryptedEnvelope(payload) || payload.byteLength < PROVIDER_PREFIX_BYTES) throw new PayloadProtectionError("invalid_envelope", "invalid streams-crdt provider payload envelope");
|
|
395
|
+
const providerHeaderLength = new DataView(payload.buffer, payload.byteOffset, payload.byteLength).getUint16(6, false);
|
|
396
|
+
if (payload[4] !== PROVIDER_ENVELOPE_VERSION || providerHeaderLength === 0 || providerHeaderLength > MAX_PROVIDER_HEADER_BYTES || payload[8] !== 0 || payload[9] !== 0) throw new PayloadProtectionError("invalid_envelope", "unsupported streams-crdt provider payload envelope");
|
|
397
|
+
const headerEnd = PROVIDER_PREFIX_BYTES + providerHeaderLength;
|
|
398
|
+
if (payload.byteLength <= headerEnd) throw new PayloadProtectionError("invalid_envelope", "truncated streams-crdt provider payload envelope");
|
|
404
399
|
return {
|
|
405
|
-
kind,
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
400
|
+
kind: decodeKind(payload[5]),
|
|
401
|
+
header: payload.slice(PROVIDER_PREFIX_BYTES, headerEnd),
|
|
402
|
+
authenticatedHeader: payload.slice(0, headerEnd),
|
|
403
|
+
sealed: payload.slice(headerEnd)
|
|
409
404
|
};
|
|
410
405
|
}
|
|
406
|
+
function normalizeProviderHeader(header) {
|
|
407
|
+
if (!(header instanceof Uint8Array) || header.byteLength === 0 || header.byteLength > MAX_PROVIDER_HEADER_BYTES) throw new ProviderSealContractError(`payload protection provider header must be 1..${MAX_PROVIDER_HEADER_BYTES} bytes`);
|
|
408
|
+
return header;
|
|
409
|
+
}
|
|
410
|
+
function equalBytes(left, right) {
|
|
411
|
+
return left.byteLength === right.byteLength && left.every((byte, index) => byte === right[index]);
|
|
412
|
+
}
|
|
413
|
+
function createProviderAad(authenticatedHeader) {
|
|
414
|
+
return concatBytes(providerAadDomain, authenticatedHeader);
|
|
415
|
+
}
|
|
411
416
|
function encodeKind(kind) {
|
|
412
417
|
return kind === "update_batch" ? UPDATE_BATCH_KIND : SNAPSHOT_KIND;
|
|
413
418
|
}
|
|
@@ -416,74 +421,18 @@ function decodeKind(encoded) {
|
|
|
416
421
|
if (encoded === SNAPSHOT_KIND) return "snapshot";
|
|
417
422
|
throw new PayloadProtectionError("invalid_envelope", "unsupported streams-crdt encrypted payload kind");
|
|
418
423
|
}
|
|
419
|
-
function
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
keyId: input.keyId,
|
|
427
|
-
kind: input.kind,
|
|
428
|
-
protocol: "loro-streams-crdt-payload-protection",
|
|
429
|
-
scope: normalizeScope(input.scope),
|
|
430
|
-
suite: "aes_256_gcm",
|
|
431
|
-
version: ENVELOPE_VERSION
|
|
432
|
-
}));
|
|
433
|
-
}
|
|
434
|
-
function normalizeScope(scope) {
|
|
435
|
-
return typeof scope === "string" ? scope : `bucket:${scope.bucketId}\nstream:${scope.streamId}`;
|
|
436
|
-
}
|
|
437
|
-
function concatBytes(left, right) {
|
|
438
|
-
const out = new Uint8Array(left.byteLength + right.byteLength);
|
|
439
|
-
out.set(left);
|
|
440
|
-
out.set(right, left.byteLength);
|
|
424
|
+
function concatBytes(...parts) {
|
|
425
|
+
const out = new Uint8Array(parts.reduce((length, part) => length + part.byteLength, 0));
|
|
426
|
+
let cursor = 0;
|
|
427
|
+
for (const part of parts) {
|
|
428
|
+
out.set(part, cursor);
|
|
429
|
+
cursor += part.byteLength;
|
|
430
|
+
}
|
|
441
431
|
return out;
|
|
442
432
|
}
|
|
443
433
|
function encodedItemsByteLength(items) {
|
|
444
434
|
return items.reduce((sum, item) => sum + 4 + item.byteLength, 0);
|
|
445
435
|
}
|
|
446
|
-
async function importAesGcmKey(key) {
|
|
447
|
-
if (isCryptoKey(key)) return key;
|
|
448
|
-
validateRawKey(key);
|
|
449
|
-
return await getSubtleCrypto().importKey("raw", toArrayBuffer(key), "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
450
|
-
}
|
|
451
|
-
function toArrayBuffer(bytes) {
|
|
452
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
453
|
-
}
|
|
454
|
-
function validateKey(key, label) {
|
|
455
|
-
if (key == null) return;
|
|
456
|
-
if (key.id.trim().length === 0) throw new Error(`${label}.id must be non-empty`);
|
|
457
|
-
if (key.key instanceof Uint8Array) validateRawKey(key.key);
|
|
458
|
-
}
|
|
459
|
-
function normalizeReadKeys(readKeys, label) {
|
|
460
|
-
if (readKeys == null) return;
|
|
461
|
-
if (typeof readKeys === "function") return async () => {
|
|
462
|
-
const resolved = await readKeys();
|
|
463
|
-
validateKeys(resolved, label);
|
|
464
|
-
return resolved;
|
|
465
|
-
};
|
|
466
|
-
validateKeys(readKeys, label);
|
|
467
|
-
return readKeys;
|
|
468
|
-
}
|
|
469
|
-
function validateKeys(keys, label) {
|
|
470
|
-
for (const [index, key] of keys.entries()) validateKey(key, `${label}[${index}]`);
|
|
471
|
-
}
|
|
472
|
-
function isCryptoKey(key) {
|
|
473
|
-
return typeof CryptoKey !== "undefined" && key instanceof CryptoKey;
|
|
474
|
-
}
|
|
475
|
-
function validateRawKey(key) {
|
|
476
|
-
if (key.byteLength !== RAW_AES_256_KEY_BYTES) throw new Error(`E2EE raw AES-GCM keys must be ${RAW_AES_256_KEY_BYTES} bytes`);
|
|
477
|
-
}
|
|
478
|
-
function getCrypto() {
|
|
479
|
-
if (globalThis.crypto == null) throw new PayloadProtectionError("encrypt_failed", "Web Crypto is unavailable in this runtime");
|
|
480
|
-
return globalThis.crypto;
|
|
481
|
-
}
|
|
482
|
-
function getSubtleCrypto() {
|
|
483
|
-
const subtle = getCrypto().subtle;
|
|
484
|
-
if (subtle == null) throw new PayloadProtectionError("encrypt_failed", "SubtleCrypto is unavailable in this runtime");
|
|
485
|
-
return subtle;
|
|
486
|
-
}
|
|
487
436
|
//#endregion
|
|
488
437
|
//#region src/transport-internal.ts
|
|
489
438
|
function createProducerId() {
|
|
@@ -844,8 +793,7 @@ function toTransportError(error) {
|
|
|
844
793
|
code: "payload_protection_error",
|
|
845
794
|
retryable: false,
|
|
846
795
|
reason: error.reason,
|
|
847
|
-
|
|
848
|
-
message: error.message
|
|
796
|
+
message: payloadProtectionFailureMessage(error.reason)
|
|
849
797
|
};
|
|
850
798
|
if (error instanceof TypeError) return {
|
|
851
799
|
code: "network_error",
|
|
@@ -2480,11 +2428,9 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2480
2428
|
this.debug = options.debug ?? false;
|
|
2481
2429
|
this.beforeRemoteCursorSave = composeBeforeRemoteCursorSaveHooks(options.beforeRemoteCursorSave, ...options.beforeRemoteCursorSaveHooks ?? []);
|
|
2482
2430
|
const snapshotCodec = normalizeSnapshotCodec(options.snapshotCodec);
|
|
2483
|
-
if (options.
|
|
2484
|
-
const
|
|
2485
|
-
|
|
2486
|
-
const payloadProtection = normalizePayloadProtection(payloadProtectionOption, payloadProtectionLabel);
|
|
2487
|
-
if (payloadProtection?.writePolicy === "plaintext" && options.snapshotUpload != null && options.snapshotUpload.enabled !== false) throw new Error(`snapshotUpload is not supported when ${payloadProtectionLabel}.writePolicy is plaintext`);
|
|
2431
|
+
if (options.payloadProtectionRequired === true && options.e2ee == null) throw new Error("payloadProtectionRequired is true but this room has no e2ee config");
|
|
2432
|
+
const payloadProtection = normalizePayloadProtection(options.e2ee, "e2ee");
|
|
2433
|
+
if (payloadProtection?.writePolicy === "plaintext" && options.snapshotUpload != null && options.snapshotUpload.enabled !== false) throw new Error("snapshotUpload is not supported when e2ee.writePolicy is plaintext");
|
|
2488
2434
|
const snapshotUploadConfig = normalizeSnapshotUploadConfig(options.snapshotUpload);
|
|
2489
2435
|
this.decodeSnapshot = this.composeSnapshotDecoder(payloadProtection, snapshotCodec?.decompress);
|
|
2490
2436
|
this.encodeSnapshot = this.composeSnapshotEncoder(payloadProtection, snapshotCodec?.compress);
|
|
@@ -3987,33 +3933,16 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3987
3933
|
//#region src/stream-id.ts
|
|
3988
3934
|
const MAX_STREAM_ID_BYTES = 512;
|
|
3989
3935
|
const textEncoder = new TextEncoder();
|
|
3990
|
-
const DEFAULT_BASE_URL = "https://streams-api.loro.dev";
|
|
3991
3936
|
function utf8ByteLength(value) {
|
|
3992
3937
|
return textEncoder.encode(value).byteLength;
|
|
3993
3938
|
}
|
|
3994
3939
|
/**
|
|
3995
|
-
* Validates a stream id
|
|
3940
|
+
* Validates a Durable Streams stream id.
|
|
3996
3941
|
*/
|
|
3997
3942
|
function isValidRillId(id) {
|
|
3998
|
-
return id.length > 0 && utf8ByteLength(id) <= MAX_STREAM_ID_BYTES && !id.includes("/") && !id.includes("\0") && !id.includes("..");
|
|
3999
|
-
}
|
|
4000
|
-
/**
|
|
4001
|
-
* Validates a bucket id accepted by `createStreamUrl()`.
|
|
4002
|
-
*/
|
|
4003
|
-
function isValidBucketId(id) {
|
|
4004
|
-
return isValidRillId(id);
|
|
4005
|
-
}
|
|
4006
|
-
/**
|
|
4007
|
-
* Builds a Durable Streams HTTP URL for one `(bucketId, streamId)` pair.
|
|
4008
|
-
*/
|
|
4009
|
-
function createStreamUrl(input) {
|
|
4010
|
-
const baseUrl = (input.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/g, "");
|
|
4011
|
-
if (!isValidBucketId(input.bucketId)) throw new Error(`invalid bucketId: ${JSON.stringify(input.bucketId)} (must be non-empty UTF-8, <= ${MAX_STREAM_ID_BYTES} bytes, and exclude "/", "\\0", "..")`);
|
|
4012
|
-
if (!isValidRillId(input.streamId)) throw new Error(`invalid streamId: ${JSON.stringify(input.streamId)} (must be non-empty UTF-8, <= ${MAX_STREAM_ID_BYTES} bytes, and exclude "/", "\\0", "..")`);
|
|
4013
|
-
const streamId = input.streamId;
|
|
4014
|
-
return `${baseUrl}/ds/${encodeURIComponent(input.bucketId)}/${encodeURIComponent(streamId)}`;
|
|
3943
|
+
return id.length > 0 && utf8ByteLength(id) <= MAX_STREAM_ID_BYTES && id !== "." && !id.includes("/") && !id.includes("\0") && !id.includes("..");
|
|
4015
3944
|
}
|
|
4016
3945
|
//#endregion
|
|
4017
|
-
export {
|
|
3946
|
+
export { createInitialRemoteCursor as a, PayloadProtectionError as c, IndexedDbRemoteCursorStore as i, StreamsCrdt as n, EphemeralStreamCrdt as o, InMemoryRemoteCursorStore as r, LocalAppendFailedError as s, isValidRillId as t };
|
|
4018
3947
|
|
|
4019
|
-
//# sourceMappingURL=stream-id-
|
|
3948
|
+
//# sourceMappingURL=stream-id-CggbAC6b.js.map
|