@loro-dev/streams-crdt 0.14.1 → 0.15.1

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.
@@ -379,8 +379,7 @@ type TransportError =
379
379
  } | {
380
380
  readonly code: "payload_protection_error";
381
381
  readonly retryable: false;
382
- readonly reason: "plaintext_forbidden" | "missing_read_key" | "decrypt_failed" | "invalid_envelope" | "wrong_payload_kind" | "encrypt_failed";
383
- readonly keyId?: string;
382
+ readonly reason: PayloadProtectionFailureReason;
384
383
  readonly message: string;
385
384
  };
386
385
  interface WriteOnlyAppendResult<TVersion extends JsonObject> {
@@ -523,34 +522,74 @@ interface SnapshotCodec {
523
522
  readonly compress: SnapshotTransformHook;
524
523
  readonly decompress: SnapshotTransformHook;
525
524
  }
525
+ type PayloadProtectionFailureReason = "plaintext_forbidden" | "missing_read_key" | "decrypt_failed" | "invalid_envelope" | "wrong_payload_kind" | "encrypt_failed";
526
526
  type PayloadProtectionReadPolicy = "encrypted-only" | "allow-plaintext";
527
527
  type PayloadProtectionWritePolicy = "encrypt" | "plaintext";
528
- type PayloadProtectionScope = string | {
529
- readonly bucketId: string;
530
- readonly streamId: string;
531
- };
532
- interface PayloadProtectionKey {
533
- readonly id: string;
534
- readonly key: CryptoKey | Uint8Array;
528
+ /** Durable CRDT payload class authenticated by the protection boundary. */
529
+ type PayloadProtectionKind = "update_batch" | "snapshot";
530
+ /** Stable envelope context shared by provider-backed seal/open calls. */
531
+ interface PayloadProtectionContext {
532
+ readonly protocol: "loro-streams-crdt-payload-protection";
533
+ readonly version: 2;
534
+ readonly kind: PayloadProtectionKind;
535
535
  }
536
- type PayloadProtectionKeyProvider = () => MaybePromise<readonly PayloadProtectionKey[]>;
537
- interface PayloadProtectionEncryptionOptions {
536
+ interface PayloadProtectionSealInput {
537
+ readonly plaintext: Uint8Array;
538
+ readonly context: PayloadProtectionContext;
538
539
  /**
539
- * Stable encryption context authenticated with every encrypted payload.
540
+ * Returns the exact AAD for the opaque authenticated header the provider
541
+ * will return.
540
542
  *
541
- * Prefer bucket/stream identity or an application-stable string. Do not use
542
- * a full gateway URL unless old encrypted bytes should become unreadable
543
- * after host/proxy migration.
543
+ * Providers MUST pass their final header to this function exactly once
544
+ * before sealing and MUST authenticate the returned bytes. A provider may
545
+ * frame and append caller-owned room/application AAD before passing the
546
+ * combined value to its AEAD. streams-crdt deliberately does not know or
547
+ * derive that application identity.
544
548
  */
545
- readonly scope: PayloadProtectionScope;
546
- /** Key used for new encrypted writes. Required when writePolicy is "encrypt". */
547
- readonly writeKey?: PayloadProtectionKey;
549
+ readonly additionalData: (header: Uint8Array) => Uint8Array;
550
+ }
551
+ interface PayloadProtectionSealResult {
552
+ /** Opaque authenticated provider header; limited to 512 bytes. */
553
+ readonly header: Uint8Array;
548
554
  /**
549
- * Keys accepted for remote encrypted reads.
550
- *
551
- * When omitted, `writeKey` is also used as the only read key.
555
+ * Provider-defined sealed bytes. Nonces and authentication tags stay opaque
556
+ * to streams-crdt, so providers may use formats such as
557
+ * `nonce[24] || XChaCha20-Poly1305 ciphertext+tag`.
558
+ */
559
+ readonly sealed: Uint8Array;
560
+ }
561
+ interface PayloadProtectionOpenInput {
562
+ readonly sealed: Uint8Array;
563
+ /** Opaque authenticated provider header returned by the writer. */
564
+ readonly header: Uint8Array;
565
+ readonly context: PayloadProtectionContext;
566
+ /** Exact envelope AAD that the provider must authenticate. */
567
+ readonly additionalData: Uint8Array;
568
+ }
569
+ /**
570
+ * Caller-owned authenticated payload codec.
571
+ *
572
+ * streams-crdt owns only the versioned envelope, its fixed envelope AAD,
573
+ * placement at the framing boundary, and fail-closed policies. The provider
574
+ * owns the audited AEAD primitive, nonce generation, application/room AAD,
575
+ * current write-key selection, and historical key lookup.
576
+ */
577
+ interface PayloadProtectionProvider {
578
+ /**
579
+ * Maximum `header.byteLength + sealed.byteLength - plaintext.byteLength`
580
+ * this provider can return. This lets streams-crdt batch without performing
581
+ * a speculative seal or consuming a nonce. The runtime rejects a seal that
582
+ * exceeds the declared bound.
552
583
  */
553
- readonly readKeys?: readonly PayloadProtectionKey[] | PayloadProtectionKeyProvider;
584
+ readonly maxSealOverheadBytes: number;
585
+ seal(input: PayloadProtectionSealInput): MaybePromise<PayloadProtectionSealResult>;
586
+ open(input: PayloadProtectionOpenInput): MaybePromise<Uint8Array>;
587
+ }
588
+ /** Scope-free provider config suitable for direct repo/room forwarding. */
589
+ interface PayloadProtectionProviderConfig {
590
+ readonly provider: PayloadProtectionProvider;
591
+ readonly readPolicy?: PayloadProtectionReadPolicy;
592
+ readonly writePolicy?: PayloadProtectionWritePolicy;
554
593
  }
555
594
  interface PayloadProtectionOptions {
556
595
  /**
@@ -562,14 +601,20 @@ interface PayloadProtectionOptions {
562
601
  * Local write policy. Defaults to "encrypt" when E2EE is provided.
563
602
  */
564
603
  readonly writePolicy?: PayloadProtectionWritePolicy;
565
- readonly encryption?: PayloadProtectionEncryptionOptions;
604
+ /**
605
+ * Provider for protected writes and reads.
606
+ *
607
+ * The provider owns the opaque header used to resolve historical keys. A
608
+ * write-key transition is an application session boundary: drain pending
609
+ * appends, close this transport, then construct a replacement with the new
610
+ * finalized write epoch. streams-crdt does not invent that rekey barrier.
611
+ */
612
+ readonly provider?: PayloadProtectionProvider;
566
613
  }
567
614
  type E2eeReadPolicy = PayloadProtectionReadPolicy;
568
615
  type E2eeWritePolicy = PayloadProtectionWritePolicy;
569
- type E2eeScope = PayloadProtectionScope;
570
- type E2eeKey = PayloadProtectionKey;
571
- type E2eeKeyProvider = PayloadProtectionKeyProvider;
572
- type E2eeEncryptionOptions = PayloadProtectionEncryptionOptions;
616
+ type E2eeProvider = PayloadProtectionProvider;
617
+ type E2eeProviderConfig = PayloadProtectionProviderConfig;
573
618
  type E2eeOptions = PayloadProtectionOptions;
574
619
  /**
575
620
  * Active live subscription returned by `join()`.
@@ -798,7 +843,11 @@ interface EphemeralStreamCrdtOptions {
798
843
  * Each instance is bound to exactly one stream URL and one local CRDT adapter.
799
844
  */
800
845
  interface StreamsCrdtOptions<TVersion extends JsonObject> {
801
- /** Full stream URL, typically built with `createStreamUrl()`. */
846
+ /**
847
+ * Full, opaque Durable Streams URL used for HTTP requests and cursor keys.
848
+ * Payload-protection providers own any separate application/room identity
849
+ * they authenticate; streams-crdt never parses this URL to derive AAD.
850
+ */
802
851
  readonly streamUrl: string;
803
852
  /** CRDT adapter for the local document or replica. */
804
853
  readonly adapter: CrdtAdapter<TVersion>;
@@ -861,11 +910,15 @@ interface StreamsCrdtOptions<TVersion extends JsonObject> {
861
910
  */
862
911
  readonly e2ee?: E2eeOptions;
863
912
  /**
864
- * Deprecated alias for `e2ee`.
913
+ * Fail construction when `e2ee` is absent.
914
+ * Repo integrations can set this after enabling protection globally so a
915
+ * missing per-room config never becomes an implicit plaintext room.
865
916
  *
866
- * Prefer `e2ee`. Passing both `e2ee` and `payloadProtection` is an error.
917
+ * This is a missing-config guard, not a policy override. An explicitly
918
+ * supplied config may still opt into `allow-plaintext` / `plaintext`, for
919
+ * example for the public write-only mode.
867
920
  */
868
- readonly payloadProtection?: PayloadProtectionOptions;
921
+ readonly payloadProtectionRequired?: boolean;
869
922
  readonly snapshotUpload?: SnapshotUploadOptions;
870
923
  /** Optional origin pools for routing transport requests. */
871
924
  readonly shardUrls?: StreamsCrdtShardUrlsOptions;
@@ -1046,6 +1099,35 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
1046
1099
  private disposeJoinState;
1047
1100
  private runInitialSyncWithDeadline;
1048
1101
  private startJoin;
1102
+ /**
1103
+ * Join-time salvage for a stream whose head cannot be fully applied
1104
+ * (apply_incomplete at `up_to_date`, even after bootstrap): before the
1105
+ * join surfaces the error, durably append everything this replica holds
1106
+ * that the server does not.
1107
+ *
1108
+ * Why this exists: the dual-author wedge is usually two-sided — the stream
1109
+ * is missing some author's ops, and a replica that HOLDS those ops (e.g.
1110
+ * received out-of-band over a local data plane) may itself be unable to
1111
+ * complete the join because of a second gap. The regular join-time export
1112
+ * runs only AFTER a successful initial sync, so without this salvage such
1113
+ * a replica can never publish the healing ops, and its own backlog from a
1114
+ * previous process run stays local for as long as the stream stays
1115
+ * poisoned.
1116
+ *
1117
+ * `error.appliedVersion` advances only from success spans, so
1118
+ * `exportUpdates(appliedVersion)` is exactly "ops this replica holds that
1119
+ * the server does not" — the failed initial sync already imported
1120
+ * everything the server DOES have, so the delta cannot re-append
1121
+ * server-known data. Cursor safety is untouched: the append is write-only
1122
+ * style (producer-acked, no read catch-up, no durable-cursor save), and
1123
+ * the retried initial sync re-derives its cursor from the server.
1124
+ *
1125
+ * Returns `true` when anything was durably appended; the caller then
1126
+ * retries the initial sync once. Best-effort: a salvage failure logs and
1127
+ * returns `false` so the original apply-incomplete error is what callers
1128
+ * observe.
1129
+ */
1130
+ private salvageLocalBacklogOnIncompleteInitialSync;
1049
1131
  private syncOnce;
1050
1132
  private performInitialJoinSync;
1051
1133
  private createInitialCursor;
@@ -1160,6 +1242,12 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
1160
1242
  * `"error"` write status (with `lastWriteError` set) and then retries via
1161
1243
  * this flush's own backoff timer (`maybeScheduleWriteRetry`) — it does not
1162
1244
  * depend on a future local op to re-trigger the flush.
1245
+ *
1246
+ * While the cursor is incomplete (unresolved remote spans), the flush
1247
+ * degrades to an append-only mode instead of pausing: batches still reach
1248
+ * the server, but read progress, durable-cursor saves, and snapshot
1249
+ * scheduling stay untouched. See the inline comment in the incomplete
1250
+ * branch and specs/loro-pending.md "Local Append Rules".
1163
1251
  */
1164
1252
  private flushPendingLocal;
1165
1253
  /**
@@ -1265,33 +1353,16 @@ declare class LocalAppendFailedError extends Error {
1265
1353
  }
1266
1354
  //#endregion
1267
1355
  //#region src/payload-protection.d.ts
1268
- type PayloadProtectionFailureReason = "plaintext_forbidden" | "missing_read_key" | "decrypt_failed" | "invalid_envelope" | "wrong_payload_kind" | "encrypt_failed";
1269
1356
  declare class PayloadProtectionError extends Error {
1270
1357
  readonly reason: PayloadProtectionFailureReason;
1271
- readonly keyId?: string;
1272
- constructor(reason: PayloadProtectionFailureReason, message: string, options?: {
1273
- readonly keyId?: string;
1274
- readonly cause?: unknown;
1275
- });
1358
+ constructor(reason: PayloadProtectionFailureReason, message?: string);
1276
1359
  }
1277
1360
  //#endregion
1278
1361
  //#region src/stream-id.d.ts
1279
1362
  /**
1280
- * Validates a stream id accepted by `createStreamUrl()`.
1363
+ * Validates a Durable Streams stream id.
1281
1364
  */
1282
1365
  declare function isValidRillId(id: string): boolean;
1283
- /**
1284
- * Validates a bucket id accepted by `createStreamUrl()`.
1285
- */
1286
- declare function isValidBucketId(id: string): boolean;
1287
- /**
1288
- * Builds a Durable Streams HTTP URL for one `(bucketId, streamId)` pair.
1289
- */
1290
- declare function createStreamUrl(input: {
1291
- bucketId: string;
1292
- streamId: string;
1293
- baseUrl?: string;
1294
- }): string;
1295
1366
  //#endregion
1296
- export { WriteOnlyAppendResult as $, PayloadProtectionKeyProvider as A, StreamsAuthProvider as B, EphemeralStreamSubscription as C, LocalSyncStats as D, JsonValue as E, Result as F, TransportCreateStreamSuccess as G, StreamsCrdtShardUrlsOptions as H, SnapshotCodec as I, TransportJoinParams as J, TransportDeleteStreamSuccess as K, SnapshotTransformHook as L, PayloadProtectionReadPolicy as M, PayloadProtectionScope as N, PayloadProtectionEncryptionOptions as O, PayloadProtectionWritePolicy as P, TransportSyncSuccess as Q, SnapshotUploadOptions as R, EphemeralStreamJoinParams as S, JsonObject as T, TransportCatchupParams as U, StreamsCrdtOptions as V, TransportCatchupSuccess as W, TransportSnapshotUploadSuccess as X, TransportRoomStatus as Y, TransportSubscription as Z, E2eeReadPolicy as _, LocalAppendFailedError as a, RemoteCursor as at, EphemeralStreamAdaptor as b, CrdtAdapter as c, createInitialRemoteCursor as ct, CrdtUnresolvedSpan as d, BeforeRemoteCursorSaveContext as et, CrdtUpdateBatch as f, E2eeOptions as g, E2eeKeyProvider as h, PayloadProtectionError as i, IndexedDbRemoteCursorStoreOptions as it, PayloadProtectionOptions as j, PayloadProtectionKey as k, CrdtApplyOutcome as l, E2eeKey as m, isValidBucketId as n, InMemoryRemoteCursorStore as nt, EphemeralStreamCrdt as o, RemoteCursorSaveSource as ot, E2eeEncryptionOptions as p, TransportError as q, isValidRillId as r, IndexedDbRemoteCursorStore as rt, StreamsCrdt as s, RemoteCursorStore as st, createStreamUrl as t, BeforeRemoteCursorSaveHook as tt, CrdtApplyReturn as u, E2eeScope as v, IsolatedCrdtAdapter as w, EphemeralStreamCrdtOptions as x, E2eeWritePolicy as y, StreamsAuthContext as z };
1297
- //# sourceMappingURL=stream-id-BuXMoCvo.d.ts.map
1367
+ export { WriteOnlyAppendResult as $, PayloadProtectionProviderConfig as A, StreamsAuthProvider as B, LocalSyncStats as C, PayloadProtectionOpenInput as D, PayloadProtectionKind as E, Result as F, TransportCreateStreamSuccess as G, StreamsCrdtShardUrlsOptions as H, SnapshotCodec as I, TransportJoinParams as J, TransportDeleteStreamSuccess as K, SnapshotTransformHook as L, PayloadProtectionSealInput as M, PayloadProtectionSealResult as N, PayloadProtectionOptions as O, PayloadProtectionWritePolicy as P, TransportSyncSuccess as Q, SnapshotUploadOptions as R, JsonValue as S, PayloadProtectionFailureReason as T, TransportCatchupParams as U, StreamsCrdtOptions as V, TransportCatchupSuccess as W, TransportSnapshotUploadSuccess as X, TransportRoomStatus as Y, TransportSubscription as Z, EphemeralStreamCrdtOptions as _, StreamsCrdt as a, RemoteCursor as at, IsolatedCrdtAdapter as b, CrdtApplyReturn as c, createInitialRemoteCursor as ct, E2eeOptions as d, BeforeRemoteCursorSaveContext as et, E2eeProvider as f, EphemeralStreamAdaptor as g, E2eeWritePolicy as h, EphemeralStreamCrdt as i, IndexedDbRemoteCursorStoreOptions as it, PayloadProtectionReadPolicy as j, PayloadProtectionProvider as k, CrdtUnresolvedSpan as l, E2eeReadPolicy as m, PayloadProtectionError as n, InMemoryRemoteCursorStore as nt, CrdtAdapter as o, RemoteCursorSaveSource as ot, E2eeProviderConfig as p, TransportError as q, LocalAppendFailedError as r, IndexedDbRemoteCursorStore as rt, CrdtApplyOutcome as s, RemoteCursorStore as st, isValidRillId as t, BeforeRemoteCursorSaveHook as tt, CrdtUpdateBatch as u, EphemeralStreamJoinParams as v, PayloadProtectionContext as w, JsonObject as x, EphemeralStreamSubscription as y, StreamsAuthContext as z };
1368
+ //# sourceMappingURL=stream-id-CVCOLgSp.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loro-dev/streams-crdt",
3
3
  "description": "Transport/runtime layer for synchronizing CRDT state over Durable Streams.",
4
- "version": "0.14.1",
4
+ "version": "0.15.1",
5
5
  "license": "MIT",
6
6
  "author": "Loro Team",
7
7
  "homepage": "https://streams.loro.dev",