@korajs/sync 1.0.0-beta.11 → 1.0.0-beta.13

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.
@@ -1,5 +1,6 @@
1
- import { c as SyncTransport, T as TransportOptions, a as SyncMessage, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, Q as QueueStorage } from './transport-iXX0KA_W.js';
2
- import { Operation } from '@korajs/core';
1
+ import { d as SyncTransport, T as TransportOptions, b as SyncMessage, e as TransportMessageHandler, f as TransportCloseHandler, g as TransportErrorHandler, Q as QueueStorage, M as INVALID_OPERATION_ID } from './transport-Bemtqwep.js';
2
+ export { aF as buildScopeSnapshot, aG as matchesScopePredicate, aH as recordMatchesScopePredicates } from './transport-Bemtqwep.js';
3
+ import { Operation, SchemaDefinition } from '@korajs/core';
3
4
 
4
5
  /**
5
6
  * In-memory transport for testing. Provides instant, synchronous message delivery
@@ -59,10 +60,123 @@ declare function createMemoryTransportPair(): {
59
60
  */
60
61
  declare class MemoryQueueStorage implements QueueStorage {
61
62
  private operations;
63
+ private readonly sent;
62
64
  load(): Promise<Operation[]>;
63
65
  enqueue(op: Operation): Promise<void>;
64
66
  dequeue(ids: string[]): Promise<void>;
67
+ markSent(ops: Operation[]): Promise<void>;
68
+ loadSentIds(): Promise<string[]>;
65
69
  count(): Promise<number>;
66
70
  }
67
71
 
68
- export { MemoryQueueStorage, MemoryTransport, createMemoryTransportPair };
72
+ /**
73
+ * Canonical key of a downlink scope map (SYNC-11). The client keys its per-view
74
+ * delivery watermark by the scope the server ACCEPTED and reports that scope's key at
75
+ * the next handshake; the server computes the key of the scope it resolves now and
76
+ * resumes the delivery stream from the reported watermark only when the two match.
77
+ * Both sides must therefore compute it the same way, from scopes that may have been
78
+ * re-ordered or round-tripped through JSON: object keys are sorted, `undefined` entries
79
+ * dropped, and `$in` lists de-duplicated and sorted. No scope (undefined or null) is
80
+ * the empty string.
81
+ */
82
+ declare function scopeViewKey(scope: Record<string, Record<string, unknown>> | null | undefined): string;
83
+
84
+ /** Outcome of {@link verifyInboundOperation}. */
85
+ type InboundVerification = {
86
+ ok: true;
87
+ /** Whether the id was checked (false: not content-addressed, or not declared). */
88
+ verified: boolean;
89
+ /**
90
+ * The hash version the id matched, when it declared none (`'verify-ids'`): 1, or 2
91
+ * for a version-2 id whose declaration was lost on the way.
92
+ */
93
+ matchedVersion?: 1 | 2;
94
+ /**
95
+ * False when the id matched only through the schema-dependent rebuild of a
96
+ * beta.12 hash (declared nested object members that were `undefined`): a
97
+ * receiver without the schema cannot repeat that check, so the server must not
98
+ * declare the matched version on the stored copy. Absent: true.
99
+ */
100
+ declarable?: boolean;
101
+ /**
102
+ * The update's data with each top-level member beta.12 hashed as `undefined`
103
+ * restored as `null` (RT-71): beta.12 cleared those fields locally, and `null`
104
+ * hashes identically under version 1, so the stored copy carries the write the
105
+ * beta.12 client made, under the same verified id. Present only when the id
106
+ * matched through that rebuild.
107
+ */
108
+ restoredData?: Operation['data'];
109
+ } | {
110
+ ok: false;
111
+ code: typeof INVALID_OPERATION_ID;
112
+ message: string;
113
+ };
114
+ /** How {@link verifyInboundOperation} treats an operation that declares no hash version. */
115
+ type AbsentHashVersionPolicy =
116
+ /**
117
+ * Not checked (clients). The server declares the version of every id it verified
118
+ * (1 or 2) when it stores the operation; an absent version marks an id the server
119
+ * could not verify: a log row from before beta.13, a server-side schema transform
120
+ * (which rewrites data under the original id), or a protocol-1 encrypted payload.
121
+ */
122
+ 'skip'
123
+ /**
124
+ * Checked (the server, on upload: omitting the version skips nothing): the id must be
125
+ * the operation's version-1 or version-2 content hash.
126
+ */
127
+ | 'verify-ids';
128
+ /**
129
+ * Verify a delivered (or uploaded) operation's content-addressed id (CORE-1, RT-64).
130
+ * Runs AFTER decryption, on the plaintext the id was computed over, and before any
131
+ * transform or apply. A mismatch means the operation was altered after it was created
132
+ * (in transit, by a relay, or a ciphertext swap that kept its id), or that its id was
133
+ * chosen rather than computed (squatting an id the server derives).
134
+ *
135
+ * Rules:
136
+ * - Scope entries and server-derived operations (reserved `kora:` nodes, no declared hash
137
+ * version: keyed HMAC ids) are not checked. A reserved-node operation that declares a
138
+ * hash version (a server route write) is checked like any other.
139
+ * - An operation that arrived in an encryption envelope is always checked, against
140
+ * the hash version it declares (bound into the envelope's authenticated data).
141
+ * - A plaintext operation declaring hash version 2 or 1 is checked against that version.
142
+ * Version 1 covers type, collection, recordId, data, timestamp, nodeId and atomicOps;
143
+ * binary values are accepted in either of their two forms (bytes, or the canonical
144
+ * `{ $koraBytes }` form a JSON round trip produces), since a version-1 hash was
145
+ * computed over whichever form the writer held.
146
+ * - A plaintext operation declaring no version follows `absentVersion`: skipped, or
147
+ * accepted when its id is its version-1 or version-2 content hash.
148
+ * - An unknown declared version fails closed.
149
+ *
150
+ * @param op - The operation, decrypted
151
+ * @param context - Whether it arrived in an encryption envelope, and the policy for an
152
+ * operation that declares no hash version (default `'skip'`)
153
+ * @returns ok, or the refusal with a code and an explanation
154
+ */
155
+ declare function verifyInboundOperation(op: Operation, context: {
156
+ encrypted: boolean;
157
+ absentVersion?: AbsentHashVersionPolicy;
158
+ /** The schema, for rebuilding beta.12 hashes of `undefined` nested members (RT-71). */
159
+ schema?: SchemaDefinition | null;
160
+ }): Promise<InboundVerification>;
161
+ /**
162
+ * Whether the operation's id is its content hash under its declared version (absent:
163
+ * 1). A version-1 id is also accepted over the other form of its binary values, and
164
+ * over the beta.12 form of `undefined` members (see {@link matchOperationId}).
165
+ *
166
+ * @param op - The operation (plaintext)
167
+ * @param schema - Optional schema, to rebuild declared nested members
168
+ * @returns true when the id is the content hash
169
+ */
170
+ declare function operationIdMatches(op: Operation, schema?: SchemaDefinition | null): Promise<boolean>;
171
+ /**
172
+ * A beta.12 update's data with every `previousData` key it lacks restored as `null`
173
+ * (RT-71). beta.12 writes `previousData[key]` for every key it validated, so such a key
174
+ * held `undefined` in `data`: hashed as `null`, applied locally as a cleared field, and
175
+ * dropped by the JSON upload. Returns `op.data` itself when there is none to restore.
176
+ *
177
+ * @param op - A plaintext operation
178
+ * @returns The restored data, or `op.data` unchanged
179
+ */
180
+ declare function restoreUndefinedFromPrevious(op: Operation): Operation['data'];
181
+
182
+ export { type InboundVerification, MemoryQueueStorage, MemoryTransport, createMemoryTransportPair, operationIdMatches, restoreUndefinedFromPrevious, scopeViewKey, verifyInboundOperation };
package/dist/internal.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import {
2
- MemoryQueueStorage
3
- } from "./chunk-TU345YUC.js";
2
+ MemoryQueueStorage,
3
+ buildScopeSnapshot,
4
+ matchesScopePredicate,
5
+ operationIdMatches,
6
+ recordMatchesScopePredicates,
7
+ restoreUndefinedFromPrevious,
8
+ scopeViewKey,
9
+ verifyInboundOperation
10
+ } from "./chunk-5S6U7EVU.js";
4
11
 
5
12
  // src/transport/memory-transport.ts
6
13
  import { SyncError } from "@korajs/core";
@@ -97,6 +104,13 @@ function createMemoryTransportPair() {
97
104
  export {
98
105
  MemoryQueueStorage,
99
106
  MemoryTransport,
100
- createMemoryTransportPair
107
+ buildScopeSnapshot,
108
+ createMemoryTransportPair,
109
+ matchesScopePredicate,
110
+ operationIdMatches,
111
+ recordMatchesScopePredicates,
112
+ restoreUndefinedFromPrevious,
113
+ scopeViewKey,
114
+ verifyInboundOperation
101
115
  };
102
116
  //# sourceMappingURL=internal.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/transport/memory-transport.ts"],"sourcesContent":["import { SyncError } from '@korajs/core'\nimport type { SyncMessage } from '../protocol/messages'\nimport type {\n\tSyncTransport,\n\tTransportCloseHandler,\n\tTransportErrorHandler,\n\tTransportMessageHandler,\n\tTransportOptions,\n} from './transport'\n\n/**\n * In-memory transport for testing. Provides instant, synchronous message delivery\n * between a linked pair of transports (simulating client ↔ server).\n */\nexport class MemoryTransport implements SyncTransport {\n\tprivate connected = false\n\tprivate messageHandler: TransportMessageHandler | null = null\n\tprivate closeHandler: TransportCloseHandler | null = null\n\tprivate errorHandler: TransportErrorHandler | null = null\n\tprivate peer: MemoryTransport | null = null\n\tprivate readonly sentMessages: SyncMessage[] = []\n\n\t/** Link this transport to its peer (the other end of the connection) */\n\tlinkPeer(peer: MemoryTransport): void {\n\t\tthis.peer = peer\n\t}\n\n\tasync connect(_url: string, _options?: TransportOptions): Promise<void> {\n\t\tif (!this.peer) {\n\t\t\tthrow new SyncError('MemoryTransport has no linked peer', {\n\t\t\t\treason: 'not-linked',\n\t\t\t})\n\t\t}\n\t\tthis.connected = true\n\t\tthis.peer.connected = true\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (!this.connected) return\n\t\tthis.connected = false\n\t\t// Notify peer that we disconnected\n\t\tif (this.peer?.connected) {\n\t\t\tthis.peer.connected = false\n\t\t\tthis.peer.closeHandler?.('peer disconnected')\n\t\t}\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tif (!this.connected) {\n\t\t\tthrow new SyncError('Cannot send message: transport is not connected', {\n\t\t\t\tmessageType: message.type,\n\t\t\t})\n\t\t}\n\t\tthis.sentMessages.push(message)\n\t\t// Deliver to peer's message handler\n\t\tthis.peer?.messageHandler?.(message)\n\t}\n\n\tonMessage(handler: TransportMessageHandler): void {\n\t\tthis.messageHandler = handler\n\t}\n\n\tonClose(handler: TransportCloseHandler): void {\n\t\tthis.closeHandler = handler\n\t}\n\n\tonError(handler: TransportErrorHandler): void {\n\t\tthis.errorHandler = handler\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.connected\n\t}\n\n\t// --- Testing helpers ---\n\n\t/**\n\t * Simulate an incoming message (as if received from peer).\n\t * Useful for testing without a linked peer.\n\t */\n\tsimulateIncoming(message: SyncMessage): void {\n\t\tthis.messageHandler?.(message)\n\t}\n\n\t/**\n\t * Simulate a disconnect from the remote side.\n\t */\n\tsimulateDisconnect(reason: string): void {\n\t\tthis.connected = false\n\t\tthis.closeHandler?.(reason)\n\t}\n\n\t/**\n\t * Simulate a transport error.\n\t */\n\tsimulateError(error: Error): void {\n\t\tthis.errorHandler?.(error)\n\t}\n\n\t/**\n\t * Get all messages sent through this transport.\n\t */\n\tgetSentMessages(): SyncMessage[] {\n\t\treturn [...this.sentMessages]\n\t}\n\n\t/**\n\t * Clear the sent messages history.\n\t */\n\tclearSentMessages(): void {\n\t\tthis.sentMessages.length = 0\n\t}\n}\n\n/**\n * Create a linked pair of memory transports for testing.\n * Messages sent on `client` arrive at `server` and vice versa.\n */\nexport function createMemoryTransportPair(): { client: MemoryTransport; server: MemoryTransport } {\n\tconst client = new MemoryTransport()\n\tconst server = new MemoryTransport()\n\tclient.linkPeer(server)\n\tserver.linkPeer(client)\n\treturn { client, server }\n}\n"],"mappings":";;;;;AAAA,SAAS,iBAAiB;AAcnB,IAAM,kBAAN,MAA+C;AAAA,EAC7C,YAAY;AAAA,EACZ,iBAAiD;AAAA,EACjD,eAA6C;AAAA,EAC7C,eAA6C;AAAA,EAC7C,OAA+B;AAAA,EACtB,eAA8B,CAAC;AAAA;AAAA,EAGhD,SAAS,MAA6B;AACrC,SAAK,OAAO;AAAA,EACb;AAAA,EAEA,MAAM,QAAQ,MAAc,UAA4C;AACvE,QAAI,CAAC,KAAK,MAAM;AACf,YAAM,IAAI,UAAU,sCAAsC;AAAA,QACzD,QAAQ;AAAA,MACT,CAAC;AAAA,IACF;AACA,SAAK,YAAY;AACjB,SAAK,KAAK,YAAY;AAAA,EACvB;AAAA,EAEA,MAAM,aAA4B;AACjC,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,YAAY;AAEjB,QAAI,KAAK,MAAM,WAAW;AACzB,WAAK,KAAK,YAAY;AACtB,WAAK,KAAK,eAAe,mBAAmB;AAAA,IAC7C;AAAA,EACD;AAAA,EAEA,KAAK,SAA4B;AAChC,QAAI,CAAC,KAAK,WAAW;AACpB,YAAM,IAAI,UAAU,mDAAmD;AAAA,QACtE,aAAa,QAAQ;AAAA,MACtB,CAAC;AAAA,IACF;AACA,SAAK,aAAa,KAAK,OAAO;AAE9B,SAAK,MAAM,iBAAiB,OAAO;AAAA,EACpC;AAAA,EAEA,UAAU,SAAwC;AACjD,SAAK,iBAAiB;AAAA,EACvB;AAAA,EAEA,QAAQ,SAAsC;AAC7C,SAAK,eAAe;AAAA,EACrB;AAAA,EAEA,QAAQ,SAAsC;AAC7C,SAAK,eAAe;AAAA,EACrB;AAAA,EAEA,cAAuB;AACtB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,SAA4B;AAC5C,SAAK,iBAAiB,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,QAAsB;AACxC,SAAK,YAAY;AACjB,SAAK,eAAe,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,OAAoB;AACjC,SAAK,eAAe,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAiC;AAChC,WAAO,CAAC,GAAG,KAAK,YAAY;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,oBAA0B;AACzB,SAAK,aAAa,SAAS;AAAA,EAC5B;AACD;AAMO,SAAS,4BAAkF;AACjG,QAAM,SAAS,IAAI,gBAAgB;AACnC,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,SAAS,MAAM;AACtB,SAAO,SAAS,MAAM;AACtB,SAAO,EAAE,QAAQ,OAAO;AACzB;","names":[]}
1
+ {"version":3,"sources":["../src/transport/memory-transport.ts"],"sourcesContent":["import { SyncError } from '@korajs/core'\nimport type { SyncMessage } from '../protocol/messages'\nimport type {\n\tSyncTransport,\n\tTransportCloseHandler,\n\tTransportErrorHandler,\n\tTransportMessageHandler,\n\tTransportOptions,\n} from './transport'\n\n/**\n * In-memory transport for testing. Provides instant, synchronous message delivery\n * between a linked pair of transports (simulating client ↔ server).\n */\nexport class MemoryTransport implements SyncTransport {\n\tprivate connected = false\n\tprivate messageHandler: TransportMessageHandler | null = null\n\tprivate closeHandler: TransportCloseHandler | null = null\n\tprivate errorHandler: TransportErrorHandler | null = null\n\tprivate peer: MemoryTransport | null = null\n\tprivate readonly sentMessages: SyncMessage[] = []\n\n\t/** Link this transport to its peer (the other end of the connection) */\n\tlinkPeer(peer: MemoryTransport): void {\n\t\tthis.peer = peer\n\t}\n\n\tasync connect(_url: string, _options?: TransportOptions): Promise<void> {\n\t\tif (!this.peer) {\n\t\t\tthrow new SyncError('MemoryTransport has no linked peer', {\n\t\t\t\treason: 'not-linked',\n\t\t\t})\n\t\t}\n\t\tthis.connected = true\n\t\tthis.peer.connected = true\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (!this.connected) return\n\t\tthis.connected = false\n\t\t// Notify peer that we disconnected\n\t\tif (this.peer?.connected) {\n\t\t\tthis.peer.connected = false\n\t\t\tthis.peer.closeHandler?.('peer disconnected')\n\t\t}\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tif (!this.connected) {\n\t\t\tthrow new SyncError('Cannot send message: transport is not connected', {\n\t\t\t\tmessageType: message.type,\n\t\t\t})\n\t\t}\n\t\tthis.sentMessages.push(message)\n\t\t// Deliver to peer's message handler\n\t\tthis.peer?.messageHandler?.(message)\n\t}\n\n\tonMessage(handler: TransportMessageHandler): void {\n\t\tthis.messageHandler = handler\n\t}\n\n\tonClose(handler: TransportCloseHandler): void {\n\t\tthis.closeHandler = handler\n\t}\n\n\tonError(handler: TransportErrorHandler): void {\n\t\tthis.errorHandler = handler\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.connected\n\t}\n\n\t// --- Testing helpers ---\n\n\t/**\n\t * Simulate an incoming message (as if received from peer).\n\t * Useful for testing without a linked peer.\n\t */\n\tsimulateIncoming(message: SyncMessage): void {\n\t\tthis.messageHandler?.(message)\n\t}\n\n\t/**\n\t * Simulate a disconnect from the remote side.\n\t */\n\tsimulateDisconnect(reason: string): void {\n\t\tthis.connected = false\n\t\tthis.closeHandler?.(reason)\n\t}\n\n\t/**\n\t * Simulate a transport error.\n\t */\n\tsimulateError(error: Error): void {\n\t\tthis.errorHandler?.(error)\n\t}\n\n\t/**\n\t * Get all messages sent through this transport.\n\t */\n\tgetSentMessages(): SyncMessage[] {\n\t\treturn [...this.sentMessages]\n\t}\n\n\t/**\n\t * Clear the sent messages history.\n\t */\n\tclearSentMessages(): void {\n\t\tthis.sentMessages.length = 0\n\t}\n}\n\n/**\n * Create a linked pair of memory transports for testing.\n * Messages sent on `client` arrive at `server` and vice versa.\n */\nexport function createMemoryTransportPair(): { client: MemoryTransport; server: MemoryTransport } {\n\tconst client = new MemoryTransport()\n\tconst server = new MemoryTransport()\n\tclient.linkPeer(server)\n\tserver.linkPeer(client)\n\treturn { client, server }\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,iBAAiB;AAcnB,IAAM,kBAAN,MAA+C;AAAA,EAC7C,YAAY;AAAA,EACZ,iBAAiD;AAAA,EACjD,eAA6C;AAAA,EAC7C,eAA6C;AAAA,EAC7C,OAA+B;AAAA,EACtB,eAA8B,CAAC;AAAA;AAAA,EAGhD,SAAS,MAA6B;AACrC,SAAK,OAAO;AAAA,EACb;AAAA,EAEA,MAAM,QAAQ,MAAc,UAA4C;AACvE,QAAI,CAAC,KAAK,MAAM;AACf,YAAM,IAAI,UAAU,sCAAsC;AAAA,QACzD,QAAQ;AAAA,MACT,CAAC;AAAA,IACF;AACA,SAAK,YAAY;AACjB,SAAK,KAAK,YAAY;AAAA,EACvB;AAAA,EAEA,MAAM,aAA4B;AACjC,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,YAAY;AAEjB,QAAI,KAAK,MAAM,WAAW;AACzB,WAAK,KAAK,YAAY;AACtB,WAAK,KAAK,eAAe,mBAAmB;AAAA,IAC7C;AAAA,EACD;AAAA,EAEA,KAAK,SAA4B;AAChC,QAAI,CAAC,KAAK,WAAW;AACpB,YAAM,IAAI,UAAU,mDAAmD;AAAA,QACtE,aAAa,QAAQ;AAAA,MACtB,CAAC;AAAA,IACF;AACA,SAAK,aAAa,KAAK,OAAO;AAE9B,SAAK,MAAM,iBAAiB,OAAO;AAAA,EACpC;AAAA,EAEA,UAAU,SAAwC;AACjD,SAAK,iBAAiB;AAAA,EACvB;AAAA,EAEA,QAAQ,SAAsC;AAC7C,SAAK,eAAe;AAAA,EACrB;AAAA,EAEA,QAAQ,SAAsC;AAC7C,SAAK,eAAe;AAAA,EACrB;AAAA,EAEA,cAAuB;AACtB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,SAA4B;AAC5C,SAAK,iBAAiB,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,QAAsB;AACxC,SAAK,YAAY;AACjB,SAAK,eAAe,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,OAAoB;AACjC,SAAK,eAAe,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAiC;AAChC,WAAO,CAAC,GAAG,KAAK,YAAY;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,oBAA0B;AACzB,SAAK,aAAa,SAAS;AAAA,EAC5B;AACD;AAMO,SAAS,4BAAkF;AACjG,QAAM,SAAS,IAAI,gBAAgB;AACnC,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,SAAS,MAAM;AACtB,SAAO,SAAS,MAAM;AACtB,SAAO,EAAE,QAAQ,OAAO;AACzB;","names":[]}