@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.
@@ -208,59 +208,51 @@ const MAGIC = new Uint8Array([
208
208
  67,
209
209
  69
210
210
  ]);
211
- const ENVELOPE_VERSION = 1;
212
- const SUITE_AES_256_GCM = 1;
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 AES_GCM_NONCE_BYTES = 12;
216
- const HEADER_BYTES = 10;
217
- const AES_GCM_TAG_BITS = 128;
218
- const RAW_AES_256_KEY_BYTES = 32;
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
- keyId;
224
- constructor(reason, message, options = {}) {
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
- this.keyId = options.keyId;
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
- scope;
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.scope = options.scope;
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.encryptPayload("update_batch", encodeItems(clearUpdates))];
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
- const writeKey = this.writeKey;
253
- if (writeKey == null || this.scope == null) throw new PayloadProtectionError("encrypt_failed", "E2EE write encryption requires encryption.writeKey and encryption.scope");
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.decryptPayload("update_batch", item);
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.encryptPayload("snapshot", snapshot);
266
+ return await this.sealPayload("snapshot", snapshot);
275
267
  }
276
268
  async decodeSnapshot(snapshot) {
277
- if (isEncryptedEnvelope(snapshot)) return await this.decryptPayload("snapshot", snapshot);
269
+ if (isEncryptedEnvelope(snapshot)) return await this.openPayload("snapshot", snapshot);
278
270
  this.assertPlaintextAllowed("snapshot");
279
271
  return snapshot;
280
272
  }
281
- async encryptPayload(kind, plaintext) {
282
- const writeKey = this.writeKey;
283
- if (writeKey == null || this.scope == null) throw new PayloadProtectionError("encrypt_failed", "E2EE write encryption requires encryption.writeKey and encryption.scope");
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
- const key = await (this.importedWriteKey ??= importAesGcmKey(writeKey.key));
286
- const nonce = createNonce();
287
- return concatBytes(createEnvelopeHeader(kind, writeKey.id, nonce), new Uint8Array(await getSubtleCrypto().encrypt({
288
- name: "AES-GCM",
289
- iv: toArrayBuffer(nonce),
290
- tagLength: AES_GCM_TAG_BITS,
291
- additionalData: toArrayBuffer(createAad({
292
- kind,
293
- keyId: writeKey.id,
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
- return new Uint8Array(await getSubtleCrypto().decrypt({
312
- name: "AES-GCM",
313
- iv: toArrayBuffer(parsed.nonce),
314
- tagLength: AES_GCM_TAG_BITS,
315
- additionalData: toArrayBuffer(createAad({
316
- kind: parsed.kind,
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
- throw new PayloadProtectionError("decrypt_failed", "failed to decrypt streams-crdt payload", {
323
- keyId: parsed.keyId,
324
- cause: error
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 getReadKey(keyId) {
329
- const cached = this.importedReadKeys.get(keyId);
330
- if (cached != null) return await cached;
331
- const imported = importAesGcmKey((await this.resolveReadKey(keyId)).key);
332
- this.importedReadKeys.set(keyId, imported);
333
- return await imported;
334
- }
335
- async resolveReadKey(keyId) {
336
- for (const key of await this.resolveReadKeys()) if (key.id === keyId) return key;
337
- throw new PayloadProtectionError("missing_read_key", `missing read key for protected streams-crdt payload '${keyId}'`, { keyId });
338
- }
339
- async resolveReadKeys() {
340
- const readKeys = typeof this.readKeys === "function" ? await this.readKeys() : this.readKeys;
341
- if (readKeys != null) return readKeys;
342
- return this.writeKey == null ? [] : [this.writeKey];
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 encryption = options.encryption;
356
- if (encryption != null && encryption.scope == null) throw new Error(`${label}.encryption.scope is required when encryption is configured`);
357
- if (writePolicy === "encrypt" && encryption?.writeKey == null) throw new Error(`${label}.encryption.writeKey is required when writePolicy is 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
- scope: encryption?.scope,
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 createEnvelopeHeader(kind, keyId, nonce) {
376
- const keyIdBytes = textEncoder$1.encode(keyId);
377
- if (keyIdBytes.byteLength === 0 || keyIdBytes.byteLength > 255) throw new PayloadProtectionError("encrypt_failed", "E2EE key id must be 1..255 UTF-8 bytes", { keyId });
378
- if (nonce.byteLength !== AES_GCM_NONCE_BYTES) throw new PayloadProtectionError("encrypt_failed", `AES-GCM nonce must be ${AES_GCM_NONCE_BYTES} bytes`, { keyId });
379
- const header = new Uint8Array(HEADER_BYTES + keyIdBytes.byteLength + nonce.byteLength);
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] = ENVELOPE_VERSION;
385
+ header[4] = PROVIDER_ENVELOPE_VERSION;
382
386
  header[5] = encodeKind(kind);
383
- header[6] = SUITE_AES_256_GCM;
384
- header[7] = keyIdBytes.byteLength;
385
- header[8] = nonce.byteLength;
387
+ view.setUint16(6, normalizedHeader.byteLength, false);
388
+ header[8] = 0;
386
389
  header[9] = 0;
387
- header.set(keyIdBytes, HEADER_BYTES);
388
- header.set(nonce, HEADER_BYTES + keyIdBytes.byteLength);
390
+ header.set(normalizedHeader, PROVIDER_PREFIX_BYTES);
389
391
  return header;
390
392
  }
391
- function parseEnvelope(payload) {
392
- if (!isEncryptedEnvelope(payload) || payload.byteLength < HEADER_BYTES) throw new PayloadProtectionError("invalid_envelope", "invalid streams-crdt encrypted payload envelope");
393
- const version = payload[4];
394
- const suite = payload[6];
395
- const keyIdLen = payload[7] ?? 0;
396
- const nonceLen = payload[8] ?? 0;
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
- keyId: textDecoder.decode(payload.slice(keyIdStart, nonceStart)),
407
- nonce: payload.slice(nonceStart, ciphertextStart),
408
- ciphertext: payload.slice(ciphertextStart)
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 createNonce() {
420
- const nonce = new Uint8Array(AES_GCM_NONCE_BYTES);
421
- getCrypto().getRandomValues(nonce);
422
- return nonce;
423
- }
424
- function createAad(input) {
425
- return textEncoder$1.encode(JSON.stringify({
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
- keyId: error.keyId,
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.e2ee != null && options.payloadProtection != null) throw new Error("Specify only one of e2ee or payloadProtection");
2484
- const payloadProtectionOption = options.e2ee ?? options.payloadProtection;
2485
- const payloadProtectionLabel = options.e2ee != null ? "e2ee" : "payloadProtection";
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 accepted by `createStreamUrl()`.
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 { InMemoryRemoteCursorStore as a, EphemeralStreamCrdt as c, StreamsCrdt as i, LocalAppendFailedError as l, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t, PayloadProtectionError as u };
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-CEp4zEQJ.js.map
3948
+ //# sourceMappingURL=stream-id-CggbAC6b.js.map