@syncular/core 0.1.2 → 0.2.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.
Files changed (231) hide show
  1. package/README.md +5 -19
  2. package/dist/blob-ref.d.ts +21 -0
  3. package/dist/blob-ref.js +97 -0
  4. package/dist/bytes.d.ts +45 -0
  5. package/dist/bytes.js +242 -0
  6. package/dist/canonical-json.d.ts +10 -0
  7. package/dist/canonical-json.js +18 -0
  8. package/dist/constants.d.ts +5 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/crypto.d.ts +60 -0
  11. package/dist/crypto.js +251 -0
  12. package/dist/errors.d.ts +14 -0
  13. package/dist/errors.js +17 -0
  14. package/dist/frames.d.ts +24 -0
  15. package/dist/frames.js +42 -0
  16. package/dist/index.d.ts +18 -26
  17. package/dist/index.js +18 -43
  18. package/dist/message.d.ts +157 -0
  19. package/dist/message.js +786 -0
  20. package/dist/realtime.d.ts +92 -0
  21. package/dist/realtime.js +222 -0
  22. package/dist/render.d.ts +22 -0
  23. package/dist/render.js +273 -0
  24. package/dist/row-codec.d.ts +39 -0
  25. package/dist/row-codec.js +176 -0
  26. package/dist/segment.d.ts +18 -0
  27. package/dist/segment.js +105 -0
  28. package/dist/stream.d.ts +20 -0
  29. package/dist/stream.js +102 -0
  30. package/package.json +7 -59
  31. package/src/blob-ref.ts +135 -0
  32. package/src/bytes.ts +301 -0
  33. package/src/canonical-json.ts +20 -0
  34. package/src/constants.ts +7 -0
  35. package/src/crypto.ts +335 -0
  36. package/src/errors.ts +18 -0
  37. package/src/frames.ts +47 -0
  38. package/src/index.ts +18 -43
  39. package/src/message.ts +1024 -0
  40. package/src/realtime.ts +340 -0
  41. package/src/render.ts +325 -0
  42. package/src/row-codec.ts +265 -0
  43. package/src/segment.ts +150 -0
  44. package/src/stream.ts +128 -0
  45. package/dist/blobs.d.ts +0 -153
  46. package/dist/blobs.d.ts.map +0 -1
  47. package/dist/blobs.js +0 -47
  48. package/dist/blobs.js.map +0 -1
  49. package/dist/column-codecs.d.ts +0 -59
  50. package/dist/column-codecs.d.ts.map +0 -1
  51. package/dist/column-codecs.js +0 -159
  52. package/dist/column-codecs.js.map +0 -1
  53. package/dist/conflict.d.ts +0 -22
  54. package/dist/conflict.d.ts.map +0 -1
  55. package/dist/conflict.js +0 -81
  56. package/dist/conflict.js.map +0 -1
  57. package/dist/database.d.ts +0 -7
  58. package/dist/database.d.ts.map +0 -1
  59. package/dist/database.js +0 -14
  60. package/dist/database.js.map +0 -1
  61. package/dist/dialect.d.ts +0 -7
  62. package/dist/dialect.d.ts.map +0 -1
  63. package/dist/dialect.js +0 -2
  64. package/dist/dialect.js.map +0 -1
  65. package/dist/error-responses.d.ts +0 -393
  66. package/dist/error-responses.d.ts.map +0 -1
  67. package/dist/error-responses.js +0 -393
  68. package/dist/error-responses.js.map +0 -1
  69. package/dist/http/api-client.d.ts +0 -11
  70. package/dist/http/api-client.d.ts.map +0 -1
  71. package/dist/http/api-client.js +0 -30
  72. package/dist/http/api-client.js.map +0 -1
  73. package/dist/http/blob.d.ts +0 -6
  74. package/dist/http/blob.d.ts.map +0 -1
  75. package/dist/http/blob.js +0 -46
  76. package/dist/http/blob.js.map +0 -1
  77. package/dist/http/generated/api.d.ts +0 -5241
  78. package/dist/http/generated/api.d.ts.map +0 -1
  79. package/dist/http/generated/api.js +0 -6
  80. package/dist/http/generated/api.js.map +0 -1
  81. package/dist/http/index.d.ts +0 -18
  82. package/dist/http/index.d.ts.map +0 -1
  83. package/dist/http/index.js +0 -212
  84. package/dist/http/index.js.map +0 -1
  85. package/dist/http/shared.d.ts +0 -45
  86. package/dist/http/shared.d.ts.map +0 -1
  87. package/dist/http/shared.js +0 -122
  88. package/dist/http/shared.js.map +0 -1
  89. package/dist/http/transport-client.d.ts +0 -31
  90. package/dist/http/transport-client.d.ts.map +0 -1
  91. package/dist/http/transport-client.js +0 -193
  92. package/dist/http/transport-client.js.map +0 -1
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js.map +0 -1
  95. package/dist/kysely-column-codecs.d.ts +0 -31
  96. package/dist/kysely-column-codecs.d.ts.map +0 -1
  97. package/dist/kysely-column-codecs.js +0 -452
  98. package/dist/kysely-column-codecs.js.map +0 -1
  99. package/dist/logger.d.ts +0 -29
  100. package/dist/logger.d.ts.map +0 -1
  101. package/dist/logger.js +0 -26
  102. package/dist/logger.js.map +0 -1
  103. package/dist/proxy/index.d.ts +0 -54
  104. package/dist/proxy/index.d.ts.map +0 -1
  105. package/dist/proxy/index.js +0 -7
  106. package/dist/proxy/index.js.map +0 -1
  107. package/dist/schemas/blobs.d.ts +0 -82
  108. package/dist/schemas/blobs.d.ts.map +0 -1
  109. package/dist/schemas/blobs.js +0 -64
  110. package/dist/schemas/blobs.js.map +0 -1
  111. package/dist/schemas/common.d.ts +0 -100
  112. package/dist/schemas/common.d.ts.map +0 -1
  113. package/dist/schemas/common.js +0 -64
  114. package/dist/schemas/common.js.map +0 -1
  115. package/dist/schemas/index.d.ts +0 -7
  116. package/dist/schemas/index.d.ts.map +0 -1
  117. package/dist/schemas/index.js +0 -7
  118. package/dist/schemas/index.js.map +0 -1
  119. package/dist/schemas/sync.d.ts +0 -1065
  120. package/dist/schemas/sync.d.ts.map +0 -1
  121. package/dist/schemas/sync.js +0 -361
  122. package/dist/schemas/sync.js.map +0 -1
  123. package/dist/scopes/index.d.ts +0 -113
  124. package/dist/scopes/index.d.ts.map +0 -1
  125. package/dist/scopes/index.js +0 -103
  126. package/dist/scopes/index.js.map +0 -1
  127. package/dist/sentry.d.ts +0 -31
  128. package/dist/sentry.d.ts.map +0 -1
  129. package/dist/sentry.js +0 -147
  130. package/dist/sentry.js.map +0 -1
  131. package/dist/snapshot-chunks.d.ts +0 -110
  132. package/dist/snapshot-chunks.d.ts.map +0 -1
  133. package/dist/snapshot-chunks.js +0 -682
  134. package/dist/snapshot-chunks.js.map +0 -1
  135. package/dist/sync-packs.d.ts +0 -33
  136. package/dist/sync-packs.d.ts.map +0 -1
  137. package/dist/sync-packs.js +0 -821
  138. package/dist/sync-packs.js.map +0 -1
  139. package/dist/telemetry.d.ts +0 -114
  140. package/dist/telemetry.d.ts.map +0 -1
  141. package/dist/telemetry.js +0 -114
  142. package/dist/telemetry.js.map +0 -1
  143. package/dist/transforms.d.ts +0 -146
  144. package/dist/transforms.d.ts.map +0 -1
  145. package/dist/transforms.js +0 -155
  146. package/dist/transforms.js.map +0 -1
  147. package/dist/types.d.ts +0 -210
  148. package/dist/types.d.ts.map +0 -1
  149. package/dist/types.js +0 -20
  150. package/dist/types.js.map +0 -1
  151. package/dist/utils/bytes.d.ts +0 -4
  152. package/dist/utils/bytes.d.ts.map +0 -1
  153. package/dist/utils/bytes.js +0 -66
  154. package/dist/utils/bytes.js.map +0 -1
  155. package/dist/utils/compression.d.ts +0 -22
  156. package/dist/utils/compression.d.ts.map +0 -1
  157. package/dist/utils/compression.js +0 -129
  158. package/dist/utils/compression.js.map +0 -1
  159. package/dist/utils/crypto.d.ts +0 -13
  160. package/dist/utils/crypto.d.ts.map +0 -1
  161. package/dist/utils/crypto.js +0 -107
  162. package/dist/utils/crypto.js.map +0 -1
  163. package/dist/utils/id.d.ts +0 -3
  164. package/dist/utils/id.d.ts.map +0 -1
  165. package/dist/utils/id.js +0 -25
  166. package/dist/utils/id.js.map +0 -1
  167. package/dist/utils/index.d.ts +0 -10
  168. package/dist/utils/index.d.ts.map +0 -1
  169. package/dist/utils/index.js +0 -10
  170. package/dist/utils/index.js.map +0 -1
  171. package/dist/utils/internal-runtime.d.ts +0 -16
  172. package/dist/utils/internal-runtime.d.ts.map +0 -1
  173. package/dist/utils/internal-runtime.js +0 -14
  174. package/dist/utils/internal-runtime.js.map +0 -1
  175. package/dist/utils/object.d.ts +0 -2
  176. package/dist/utils/object.d.ts.map +0 -1
  177. package/dist/utils/object.js +0 -4
  178. package/dist/utils/object.js.map +0 -1
  179. package/dist/utils/realtime-change-index.d.ts +0 -9
  180. package/dist/utils/realtime-change-index.d.ts.map +0 -1
  181. package/dist/utils/realtime-change-index.js +0 -36
  182. package/dist/utils/realtime-change-index.js.map +0 -1
  183. package/dist/utils/realtime-connection-registry.d.ts +0 -40
  184. package/dist/utils/realtime-connection-registry.d.ts.map +0 -1
  185. package/dist/utils/realtime-connection-registry.js +0 -237
  186. package/dist/utils/realtime-connection-registry.js.map +0 -1
  187. package/dist/utils/table-registry.d.ts +0 -10
  188. package/dist/utils/table-registry.d.ts.map +0 -1
  189. package/dist/utils/table-registry.js +0 -54
  190. package/dist/utils/table-registry.js.map +0 -1
  191. package/dist/utils/url.d.ts +0 -3
  192. package/dist/utils/url.d.ts.map +0 -1
  193. package/dist/utils/url.js +0 -20
  194. package/dist/utils/url.js.map +0 -1
  195. package/src/blobs.ts +0 -219
  196. package/src/column-codecs.ts +0 -262
  197. package/src/conflict.ts +0 -92
  198. package/src/database.ts +0 -21
  199. package/src/dialect.ts +0 -8
  200. package/src/error-responses.ts +0 -423
  201. package/src/http/api-client.ts +0 -39
  202. package/src/http/blob.ts +0 -95
  203. package/src/http/generated/api.ts +0 -5241
  204. package/src/http/index.ts +0 -365
  205. package/src/http/shared.ts +0 -220
  206. package/src/http/transport-client.ts +0 -314
  207. package/src/kysely-column-codecs.ts +0 -585
  208. package/src/logger.ts +0 -38
  209. package/src/proxy/index.ts +0 -57
  210. package/src/schemas/blobs.ts +0 -110
  211. package/src/schemas/common.ts +0 -89
  212. package/src/schemas/index.ts +0 -7
  213. package/src/schemas/sync.ts +0 -517
  214. package/src/scopes/index.ts +0 -237
  215. package/src/sentry.ts +0 -200
  216. package/src/snapshot-chunks.ts +0 -964
  217. package/src/sync-packs.ts +0 -1096
  218. package/src/telemetry.ts +0 -243
  219. package/src/transforms.ts +0 -256
  220. package/src/types.ts +0 -267
  221. package/src/utils/bytes.ts +0 -75
  222. package/src/utils/compression.ts +0 -187
  223. package/src/utils/crypto.ts +0 -146
  224. package/src/utils/id.ts +0 -34
  225. package/src/utils/index.ts +0 -9
  226. package/src/utils/internal-runtime.ts +0 -39
  227. package/src/utils/object.ts +0 -3
  228. package/src/utils/realtime-change-index.ts +0 -44
  229. package/src/utils/realtime-connection-registry.ts +0 -286
  230. package/src/utils/table-registry.ts +0 -67
  231. package/src/utils/url.ts +0 -29
package/README.md CHANGED
@@ -1,24 +1,10 @@
1
1
  # @syncular/core
2
2
 
3
- Core protocol types, schemas, and shared utilities for Syncular (scopes, commit-log types, telemetry, blob types, etc.).
3
+ Core protocol types and shared utilities for the Syncular sync framework.
4
4
 
5
- Most apps won't depend on this directly. It is the shared foundation used by `@syncular/client`, `@syncular/server`, and the transports.
5
+ Part of [Syncular](https://syncular.dev) — an offline-first sync framework.
6
+ See the [Syncular repository](https://github.com/syncular/syncular) for docs.
6
7
 
7
- ## Install
8
-
9
- ```bash
10
- npm install @syncular/core
11
- ```
12
-
13
- ## Documentation
14
-
15
- - Overview: https://syncular.dev/docs/introduction/what-is-syncular
16
- - Architecture (commit log, push/pull, snapshots): https://syncular.dev/docs/introduction/architecture
17
-
18
- ## Links
19
-
20
- - GitHub: https://github.com/syncular/syncular
21
- - Issues: https://github.com/syncular/syncular/issues
22
-
23
- > Status: Alpha. APIs and storage layouts may change between releases.
8
+ ## License
24
9
 
10
+ Apache-2.0
@@ -0,0 +1,21 @@
1
+ export interface BlobRef {
2
+ /** `"sha256:" + lowercase-hex SHA-256` of the blob bytes (§5.9.1). */
3
+ readonly blobId: string;
4
+ /** Non-negative uncompressed size within the i64 safe-integer contract. */
5
+ readonly byteLength: number;
6
+ /** Advisory MIME type; never parsed or trusted by the server. */
7
+ readonly mediaType?: string;
8
+ /** Advisory display filename. */
9
+ readonly name?: string;
10
+ }
11
+ /**
12
+ * Parse and validate a raw `blob_ref` string against the §5.9.1 shape. A
13
+ * failure is a row-codec decode error (`sync.invalid_request`), the same
14
+ * class as the tag-5 json parse failure — so codec and §11 rendering can
15
+ * never disagree.
16
+ */
17
+ export declare function parseBlobRef(raw: string): BlobRef;
18
+ /** Serialize a BlobRef to its canonical string form (§5.9.1 key order). */
19
+ export declare function serializeBlobRef(ref: BlobRef): string;
20
+ /** True iff `raw` is a valid canonical BlobRef document (no throw). */
21
+ export declare function isBlobRef(raw: string): boolean;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Canonical BlobRef documents (SPEC.md §5.9.1) — the value carried by a
3
+ * `blob_ref` column (§2.4 tag 7).
4
+ *
5
+ * A `blob_ref` value is a `str` holding a JSON document with a pinned shape
6
+ * and pinned key order: `blobId` (content address), `byteLength`, then the
7
+ * optional `mediaType` and `name`. On the wire and in the codec it is
8
+ * byte-for-byte a `str`, validated at decode exactly like `json` (tag 5)
9
+ * plus the shape check below; the raw string is preserved verbatim for
10
+ * re-encoding. Nothing here re-serializes a decoded value — round-trip
11
+ * fidelity is the same rule the `json` tag obeys.
12
+ */
13
+ import { DecodeError } from './errors.js';
14
+ /** The content-address prefix, identical in form to `segmentId` (§5.1). */
15
+ const BLOB_ID_RE = /^sha256:[0-9a-f]{64}$/;
16
+ /** Canonical key order (§5.9.1): blobId, byteLength, mediaType, name. */
17
+ const CANONICAL_KEYS = [
18
+ 'blobId',
19
+ 'byteLength',
20
+ 'mediaType',
21
+ 'name',
22
+ ];
23
+ /**
24
+ * Parse and validate a raw `blob_ref` string against the §5.9.1 shape. A
25
+ * failure is a row-codec decode error (`sync.invalid_request`), the same
26
+ * class as the tag-5 json parse failure — so codec and §11 rendering can
27
+ * never disagree.
28
+ */
29
+ export function parseBlobRef(raw) {
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(raw);
33
+ }
34
+ catch {
35
+ throw new DecodeError('sync.invalid_request', 'blob_ref value does not parse as a JSON document (§5.9.1)');
36
+ }
37
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
38
+ throw new DecodeError('sync.invalid_request', 'blob_ref value must be a JSON object (§5.9.1)');
39
+ }
40
+ const obj = parsed;
41
+ for (const key of Object.keys(obj)) {
42
+ if (!CANONICAL_KEYS.includes(key)) {
43
+ throw new DecodeError('sync.invalid_request', `blob_ref has unknown key ${JSON.stringify(key)} (§5.9.1)`);
44
+ }
45
+ }
46
+ const { blobId, byteLength, mediaType, name } = obj;
47
+ if (typeof blobId !== 'string' || !BLOB_ID_RE.test(blobId)) {
48
+ throw new DecodeError('sync.invalid_request', 'blob_ref.blobId must be "sha256:<64 hex>" (§5.9.1)');
49
+ }
50
+ if (typeof byteLength !== 'number' ||
51
+ !Number.isSafeInteger(byteLength) ||
52
+ byteLength < 0) {
53
+ throw new DecodeError('sync.invalid_request', 'blob_ref.byteLength must be a non-negative safe integer (§5.9.1)');
54
+ }
55
+ if (mediaType !== undefined && typeof mediaType !== 'string') {
56
+ throw new DecodeError('sync.invalid_request', 'blob_ref.mediaType must be a string when present (§5.9.1)');
57
+ }
58
+ if (name !== undefined && typeof name !== 'string') {
59
+ throw new DecodeError('sync.invalid_request', 'blob_ref.name must be a string when present (§5.9.1)');
60
+ }
61
+ // Canonical key order: the raw string's keys, in appearance order, must be
62
+ // a prefix-consistent subsequence of CANONICAL_KEYS (present keys in the
63
+ // pinned order, absent keys omitted).
64
+ const presentInOrder = Object.keys(obj);
65
+ const expectedOrder = CANONICAL_KEYS.filter((k) => Object.hasOwn(obj, k));
66
+ if (presentInOrder.join(',') !== expectedOrder.join(',')) {
67
+ throw new DecodeError('sync.invalid_request', 'blob_ref keys are not in canonical order (§5.9.1)');
68
+ }
69
+ return {
70
+ blobId,
71
+ byteLength,
72
+ ...(mediaType !== undefined ? { mediaType } : {}),
73
+ ...(name !== undefined ? { name } : {}),
74
+ };
75
+ }
76
+ /** Serialize a BlobRef to its canonical string form (§5.9.1 key order). */
77
+ export function serializeBlobRef(ref) {
78
+ const obj = {
79
+ blobId: ref.blobId,
80
+ byteLength: ref.byteLength,
81
+ };
82
+ if (ref.mediaType !== undefined)
83
+ obj.mediaType = ref.mediaType;
84
+ if (ref.name !== undefined)
85
+ obj.name = ref.name;
86
+ return JSON.stringify(obj);
87
+ }
88
+ /** True iff `raw` is a valid canonical BlobRef document (no throw). */
89
+ export function isBlobRef(raw) {
90
+ try {
91
+ parseBlobRef(raw);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
@@ -0,0 +1,45 @@
1
+ export declare function utf8Encode(value: string): Uint8Array;
2
+ /** Growable little-endian byte sink. Encoder misuse throws plain `Error`. */
3
+ export declare class ByteWriter {
4
+ #private;
5
+ u8(value: number): void;
6
+ u16(value: number): void;
7
+ u32(value: number): void;
8
+ i32(value: number): void;
9
+ i64(value: number): void;
10
+ f64(value: number): void;
11
+ bool(value: boolean): void;
12
+ str(value: string): void;
13
+ bytes(value: Uint8Array): void;
14
+ raw(value: Uint8Array): void;
15
+ opt<T>(value: T | undefined, write: (value: T) => void): void;
16
+ finish(): Uint8Array;
17
+ }
18
+ /** Bounds-checked little-endian reader. Failures throw `DecodeError`. */
19
+ export declare class ByteReader {
20
+ #private;
21
+ constructor(data: Uint8Array);
22
+ get remaining(): number;
23
+ u8(): number;
24
+ u16(): number;
25
+ u32(): number;
26
+ i32(): number;
27
+ i64(): number;
28
+ f64(): number;
29
+ bool(): boolean;
30
+ raw(size: number): Uint8Array;
31
+ bytes(): Uint8Array;
32
+ str(): string;
33
+ opt<T>(read: () => T): T | undefined;
34
+ expectFullyConsumed(what: string): void;
35
+ }
36
+ /**
37
+ * `map` of `str` → `list(str)` (scope maps, SPEC.md Conventions).
38
+ * Encoders emit keys in ascending code-unit order; the reader rejects
39
+ * duplicate or out-of-order keys (canonical encoding).
40
+ */
41
+ export declare function writeStringListMap(writer: ByteWriter, map: Readonly<Record<string, readonly string[]>>): void;
42
+ export declare function readStringListMap(reader: ByteReader): Record<string, string[]>;
43
+ /** `map` of `str` → `str` (stored scopes on change records, SPEC.md §4.5). */
44
+ export declare function writeStringMap(writer: ByteWriter, map: Readonly<Record<string, string>>): void;
45
+ export declare function readStringMap(reader: ByteReader): Record<string, string>;
package/dist/bytes.js ADDED
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Primitive readers/writers for the SPEC.md Conventions table.
3
+ *
4
+ * All multi-byte integers are little-endian, fixed-width (no varints).
5
+ * The encoding is canonical: for every value there is exactly one valid
6
+ * byte sequence, so the reader rejects non-minimal presence bytes,
7
+ * out-of-order map keys, out-of-range bools, and i64 values outside the
8
+ * JS safe-integer contract.
9
+ */
10
+ import { DecodeError } from './errors.js';
11
+ const MAX_SAFE_I64 = 9007199254740991n; // 2^53 - 1
12
+ const MIN_SAFE_I64 = -9007199254740991n;
13
+ const textEncoder = new TextEncoder();
14
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
15
+ export function utf8Encode(value) {
16
+ return textEncoder.encode(value);
17
+ }
18
+ /** Growable little-endian byte sink. Encoder misuse throws plain `Error`. */
19
+ export class ByteWriter {
20
+ #buffer = new Uint8Array(256);
21
+ #view = new DataView(this.#buffer.buffer);
22
+ #length = 0;
23
+ #reserve(extra) {
24
+ const offset = this.#length;
25
+ const needed = offset + extra;
26
+ if (needed > this.#buffer.length) {
27
+ let capacity = this.#buffer.length;
28
+ while (capacity < needed)
29
+ capacity *= 2;
30
+ const grown = new Uint8Array(capacity);
31
+ grown.set(this.#buffer.subarray(0, offset));
32
+ this.#buffer = grown;
33
+ this.#view = new DataView(grown.buffer);
34
+ }
35
+ this.#length = needed;
36
+ return offset;
37
+ }
38
+ u8(value) {
39
+ if (!Number.isInteger(value) || value < 0 || value > 0xff) {
40
+ throw new Error(`u8 out of range: ${value}`);
41
+ }
42
+ const offset = this.#reserve(1);
43
+ this.#view.setUint8(offset, value);
44
+ }
45
+ u16(value) {
46
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
47
+ throw new Error(`u16 out of range: ${value}`);
48
+ }
49
+ const offset = this.#reserve(2);
50
+ this.#view.setUint16(offset, value, true);
51
+ }
52
+ u32(value) {
53
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
54
+ throw new Error(`u32 out of range: ${value}`);
55
+ }
56
+ const offset = this.#reserve(4);
57
+ this.#view.setUint32(offset, value, true);
58
+ }
59
+ i32(value) {
60
+ if (!Number.isInteger(value) || value < -0x80000000 || value > 0x7fffffff) {
61
+ throw new Error(`i32 out of range: ${value}`);
62
+ }
63
+ const offset = this.#reserve(4);
64
+ this.#view.setInt32(offset, value, true);
65
+ }
66
+ i64(value) {
67
+ if (!Number.isSafeInteger(value)) {
68
+ throw new Error(`i64 outside the safe-integer contract: ${value}`);
69
+ }
70
+ const offset = this.#reserve(8);
71
+ this.#view.setBigInt64(offset, BigInt(value), true);
72
+ }
73
+ f64(value) {
74
+ const offset = this.#reserve(8);
75
+ this.#view.setFloat64(offset, value, true);
76
+ }
77
+ bool(value) {
78
+ this.u8(value ? 1 : 0);
79
+ }
80
+ str(value) {
81
+ const encoded = textEncoder.encode(value);
82
+ this.u32(encoded.length);
83
+ this.raw(encoded);
84
+ }
85
+ bytes(value) {
86
+ this.u32(value.length);
87
+ this.raw(value);
88
+ }
89
+ raw(value) {
90
+ const offset = this.#reserve(value.length);
91
+ this.#buffer.set(value, offset);
92
+ }
93
+ opt(value, write) {
94
+ if (value === undefined) {
95
+ this.u8(0);
96
+ }
97
+ else {
98
+ this.u8(1);
99
+ write(value);
100
+ }
101
+ }
102
+ finish() {
103
+ return this.#buffer.slice(0, this.#length);
104
+ }
105
+ }
106
+ /** Bounds-checked little-endian reader. Failures throw `DecodeError`. */
107
+ export class ByteReader {
108
+ #data;
109
+ #view;
110
+ #position = 0;
111
+ constructor(data) {
112
+ this.#data = data;
113
+ this.#view = new DataView(data.buffer, data.byteOffset, data.byteLength);
114
+ }
115
+ get remaining() {
116
+ return this.#data.length - this.#position;
117
+ }
118
+ #take(size) {
119
+ if (this.remaining < size) {
120
+ throw new DecodeError('sync.invalid_request', `truncated input: needed ${size} bytes, have ${this.remaining}`);
121
+ }
122
+ const offset = this.#position;
123
+ this.#position += size;
124
+ return offset;
125
+ }
126
+ u8() {
127
+ return this.#view.getUint8(this.#take(1));
128
+ }
129
+ u16() {
130
+ return this.#view.getUint16(this.#take(2), true);
131
+ }
132
+ u32() {
133
+ return this.#view.getUint32(this.#take(4), true);
134
+ }
135
+ i32() {
136
+ return this.#view.getInt32(this.#take(4), true);
137
+ }
138
+ i64() {
139
+ const value = this.#view.getBigInt64(this.#take(8), true);
140
+ if (value > MAX_SAFE_I64 || value < MIN_SAFE_I64) {
141
+ throw new DecodeError('sync.invalid_request', `i64 outside the safe-integer contract: ${value}`);
142
+ }
143
+ return Number(value);
144
+ }
145
+ f64() {
146
+ return this.#view.getFloat64(this.#take(8), true);
147
+ }
148
+ bool() {
149
+ const byte = this.u8();
150
+ if (byte > 1) {
151
+ throw new DecodeError('sync.invalid_request', `invalid bool byte 0x${byte.toString(16)}`);
152
+ }
153
+ return byte === 1;
154
+ }
155
+ raw(size) {
156
+ const offset = this.#take(size);
157
+ return this.#data.slice(offset, offset + size);
158
+ }
159
+ bytes() {
160
+ return this.raw(this.u32());
161
+ }
162
+ str() {
163
+ const encoded = this.bytes();
164
+ try {
165
+ return textDecoder.decode(encoded);
166
+ }
167
+ catch {
168
+ throw new DecodeError('sync.invalid_request', 'invalid UTF-8 in string');
169
+ }
170
+ }
171
+ opt(read) {
172
+ const presence = this.u8();
173
+ if (presence === 0)
174
+ return undefined;
175
+ if (presence === 1)
176
+ return read();
177
+ throw new DecodeError('sync.invalid_request', `invalid option presence byte 0x${presence.toString(16)}`);
178
+ }
179
+ expectFullyConsumed(what) {
180
+ if (this.remaining !== 0) {
181
+ throw new DecodeError('sync.invalid_request', `trailing bytes after ${what}`);
182
+ }
183
+ }
184
+ }
185
+ /**
186
+ * `map` of `str` → `list(str)` (scope maps, SPEC.md Conventions).
187
+ * Encoders emit keys in ascending code-unit order; the reader rejects
188
+ * duplicate or out-of-order keys (canonical encoding).
189
+ */
190
+ export function writeStringListMap(writer, map) {
191
+ const keys = Object.keys(map).sort();
192
+ writer.u32(keys.length);
193
+ for (const key of keys) {
194
+ writer.str(key);
195
+ const values = map[key] ?? [];
196
+ writer.u32(values.length);
197
+ for (const value of values)
198
+ writer.str(value);
199
+ }
200
+ }
201
+ export function readStringListMap(reader) {
202
+ const count = reader.u32();
203
+ const entries = [];
204
+ let previousKey;
205
+ for (let i = 0; i < count; i++) {
206
+ const key = reader.str();
207
+ if (previousKey !== undefined && key <= previousKey) {
208
+ throw new DecodeError('sync.invalid_request', `map keys must be unique and in ascending code-unit order (saw ${JSON.stringify(key)} after ${JSON.stringify(previousKey)})`);
209
+ }
210
+ previousKey = key;
211
+ const valueCount = reader.u32();
212
+ const values = [];
213
+ for (let j = 0; j < valueCount; j++)
214
+ values.push(reader.str());
215
+ entries.push([key, values]);
216
+ }
217
+ return Object.fromEntries(entries);
218
+ }
219
+ /** `map` of `str` → `str` (stored scopes on change records, SPEC.md §4.5). */
220
+ export function writeStringMap(writer, map) {
221
+ const keys = Object.keys(map).sort();
222
+ writer.u32(keys.length);
223
+ for (const key of keys) {
224
+ writer.str(key);
225
+ const value = map[key];
226
+ writer.str(value ?? '');
227
+ }
228
+ }
229
+ export function readStringMap(reader) {
230
+ const count = reader.u32();
231
+ const entries = [];
232
+ let previousKey;
233
+ for (let i = 0; i < count; i++) {
234
+ const key = reader.str();
235
+ if (previousKey !== undefined && key <= previousKey) {
236
+ throw new DecodeError('sync.invalid_request', `map keys must be unique and in ascending code-unit order (saw ${JSON.stringify(key)} after ${JSON.stringify(previousKey)})`);
237
+ }
238
+ previousKey = key;
239
+ entries.push([key, reader.str()]);
240
+ }
241
+ return Object.fromEntries(entries);
242
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Canonical JSON for digests (SPEC.md §11.2) — contractual across
3
+ * implementations: UTF-8, no insignificant whitespace, object keys sorted
4
+ * by code-unit, scope value lists sorted and deduplicated.
5
+ *
6
+ * Used for the scope digest (§3.5) and the `X-Syncular-Scopes` header
7
+ * (§5.5). Hashing itself is the host's job; this module only produces the
8
+ * canonical text.
9
+ */
10
+ export declare function canonicalScopeJson(scopes: Readonly<Record<string, readonly string[]>>): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Canonical JSON for digests (SPEC.md §11.2) — contractual across
3
+ * implementations: UTF-8, no insignificant whitespace, object keys sorted
4
+ * by code-unit, scope value lists sorted and deduplicated.
5
+ *
6
+ * Used for the scope digest (§3.5) and the `X-Syncular-Scopes` header
7
+ * (§5.5). Hashing itself is the host's job; this module only produces the
8
+ * canonical text.
9
+ */
10
+ export function canonicalScopeJson(scopes) {
11
+ const keys = Object.keys(scopes).sort();
12
+ const parts = keys.map((key) => {
13
+ const values = [...new Set(scopes[key] ?? [])].sort();
14
+ const list = values.map((value) => JSON.stringify(value)).join(',');
15
+ return `${JSON.stringify(key)}:[${list}]`;
16
+ });
17
+ return `{${parts.join(',')}}`;
18
+ }
@@ -0,0 +1,5 @@
1
+ /** Protocol constants (SPEC.md §1.2, §9 are normative). */
2
+ /** SSP2 envelope wire version this codec implements. */
3
+ export declare const PROTOCOL_WIRE_VERSION = 1;
4
+ /** Magic bytes opening every v2 sync envelope (SPEC.md §1.2). */
5
+ export declare const SYNC_PACK_MAGIC = "SSP2";
@@ -0,0 +1,5 @@
1
+ /** Protocol constants (SPEC.md §1.2, §9 are normative). */
2
+ /** SSP2 envelope wire version this codec implements. */
3
+ export const PROTOCOL_WIRE_VERSION = 1;
4
+ /** Magic bytes opening every v2 sync envelope (SPEC.md §1.2). */
5
+ export const SYNC_PACK_MAGIC = 'SSP2';
@@ -0,0 +1,60 @@
1
+ /** The pre-flip application type of an encrypted column (SPEC.md §5.11). */
2
+ export type DeclaredType = 'string' | 'integer' | 'float' | 'boolean' | 'json' | 'blob_ref' | 'bytes';
3
+ /** A non-null row value, per the declared type. `crdt` is never encrypted. */
4
+ export type PlainValue = string | number | boolean | Uint8Array;
5
+ /** Envelope version byte (SPEC.md §5.11). */
6
+ export declare const ENVELOPE_VERSION = 1;
7
+ /** AES-GCM nonce length (96-bit, the GCM standard). */
8
+ export declare const NONCE_LENGTH = 12;
9
+ /** Symmetric key length: AES-256. */
10
+ export declare const KEY_LENGTH = 32;
11
+ /**
12
+ * Client-local decrypt failure (SPEC.md §5.11, §10.3). Never on the wire —
13
+ * the `client.` family is client-only. Raised at the apply seam for an
14
+ * unknown envelope version, unknown `keyId`, GCM auth failure (wrong key), a
15
+ * malformed envelope, or a post-decrypt value-parse failure. Not retryable.
16
+ */
17
+ export declare class DecryptError extends Error {
18
+ readonly name = "DecryptError";
19
+ readonly code = "client.decrypt_failed";
20
+ readonly retryable = false;
21
+ constructor(message: string);
22
+ }
23
+ /**
24
+ * Injectable nonce source (SPEC.md §5.11 nonce discipline). Production uses
25
+ * {@link secureRandomNonce}; crypto golden vectors inject a fixed nonce. A
26
+ * fixed nonce MUST NOT be reachable from a production encode path.
27
+ */
28
+ export type NonceSource = () => Uint8Array;
29
+ export declare function secureRandomNonce(): Uint8Array;
30
+ /**
31
+ * Serialize a declared-type value to the canonical plaintext bytes fed to
32
+ * GCM (SPEC.md §5.11 value serializer). Not a re-run of the row codec — this
33
+ * is the self-describing per-`declaredType` encoding both cores agree on.
34
+ */
35
+ export declare function serializePlain(declaredType: DeclaredType, value: PlainValue): Uint8Array;
36
+ /**
37
+ * Parse the decrypted plaintext bytes back to a declared-type value (SPEC.md
38
+ * §5.11). A `json`/`blob_ref` value is re-validated as the row codec would
39
+ * (§2.4); any parse failure is a decrypt failure.
40
+ */
41
+ export declare function deserializePlain(declaredType: DeclaredType, bytes: Uint8Array): PlainValue;
42
+ export interface Envelope {
43
+ readonly keyId: string;
44
+ readonly nonce: Uint8Array;
45
+ /** AES-256-GCM ciphertext with the 16-byte tag appended. */
46
+ readonly ciphertext: Uint8Array;
47
+ }
48
+ export declare function encodeEnvelope(envelope: Envelope): Uint8Array;
49
+ export declare function decodeEnvelope(bytes: Uint8Array): Envelope;
50
+ /**
51
+ * Encrypt one declared-type value into a §5.11 envelope. `nonceSource`
52
+ * defaults to a secure RNG; vectors inject a fixed nonce.
53
+ */
54
+ export declare function encryptValue(declaredType: DeclaredType, value: PlainValue, keyId: string, key: Uint8Array, nonceSource?: NonceSource): Promise<Uint8Array>;
55
+ /**
56
+ * Decrypt a §5.11 envelope back to a declared-type value. `keyProvider`
57
+ * resolves the envelope's `keyId` to key bytes; a missing key or GCM tag
58
+ * mismatch is {@link DecryptError} (`client.decrypt_failed`).
59
+ */
60
+ export declare function decryptValue(declaredType: DeclaredType, envelopeBytes: Uint8Array, keyProvider: (keyId: string) => Uint8Array | undefined): Promise<PlainValue>;