@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.
- package/README.md +5 -19
- package/dist/blob-ref.d.ts +21 -0
- package/dist/blob-ref.js +97 -0
- package/dist/bytes.d.ts +45 -0
- package/dist/bytes.js +242 -0
- package/dist/canonical-json.d.ts +10 -0
- package/dist/canonical-json.js +18 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/crypto.d.ts +60 -0
- package/dist/crypto.js +251 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/frames.d.ts +24 -0
- package/dist/frames.js +42 -0
- package/dist/index.d.ts +18 -26
- package/dist/index.js +18 -43
- package/dist/message.d.ts +157 -0
- package/dist/message.js +786 -0
- package/dist/realtime.d.ts +92 -0
- package/dist/realtime.js +222 -0
- package/dist/render.d.ts +22 -0
- package/dist/render.js +273 -0
- package/dist/row-codec.d.ts +39 -0
- package/dist/row-codec.js +176 -0
- package/dist/segment.d.ts +18 -0
- package/dist/segment.js +105 -0
- package/dist/stream.d.ts +20 -0
- package/dist/stream.js +102 -0
- package/package.json +7 -59
- package/src/blob-ref.ts +135 -0
- package/src/bytes.ts +301 -0
- package/src/canonical-json.ts +20 -0
- package/src/constants.ts +7 -0
- package/src/crypto.ts +335 -0
- package/src/errors.ts +18 -0
- package/src/frames.ts +47 -0
- package/src/index.ts +18 -43
- package/src/message.ts +1024 -0
- package/src/realtime.ts +340 -0
- package/src/render.ts +325 -0
- package/src/row-codec.ts +265 -0
- package/src/segment.ts +150 -0
- package/src/stream.ts +128 -0
- package/dist/blobs.d.ts +0 -153
- package/dist/blobs.d.ts.map +0 -1
- package/dist/blobs.js +0 -47
- package/dist/blobs.js.map +0 -1
- package/dist/column-codecs.d.ts +0 -59
- package/dist/column-codecs.d.ts.map +0 -1
- package/dist/column-codecs.js +0 -159
- package/dist/column-codecs.js.map +0 -1
- package/dist/conflict.d.ts +0 -22
- package/dist/conflict.d.ts.map +0 -1
- package/dist/conflict.js +0 -81
- package/dist/conflict.js.map +0 -1
- package/dist/database.d.ts +0 -7
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js +0 -14
- package/dist/database.js.map +0 -1
- package/dist/dialect.d.ts +0 -7
- package/dist/dialect.d.ts.map +0 -1
- package/dist/dialect.js +0 -2
- package/dist/dialect.js.map +0 -1
- package/dist/error-responses.d.ts +0 -393
- package/dist/error-responses.d.ts.map +0 -1
- package/dist/error-responses.js +0 -393
- package/dist/error-responses.js.map +0 -1
- package/dist/http/api-client.d.ts +0 -11
- package/dist/http/api-client.d.ts.map +0 -1
- package/dist/http/api-client.js +0 -30
- package/dist/http/api-client.js.map +0 -1
- package/dist/http/blob.d.ts +0 -6
- package/dist/http/blob.d.ts.map +0 -1
- package/dist/http/blob.js +0 -46
- package/dist/http/blob.js.map +0 -1
- package/dist/http/generated/api.d.ts +0 -5241
- package/dist/http/generated/api.d.ts.map +0 -1
- package/dist/http/generated/api.js +0 -6
- package/dist/http/generated/api.js.map +0 -1
- package/dist/http/index.d.ts +0 -18
- package/dist/http/index.d.ts.map +0 -1
- package/dist/http/index.js +0 -212
- package/dist/http/index.js.map +0 -1
- package/dist/http/shared.d.ts +0 -45
- package/dist/http/shared.d.ts.map +0 -1
- package/dist/http/shared.js +0 -122
- package/dist/http/shared.js.map +0 -1
- package/dist/http/transport-client.d.ts +0 -31
- package/dist/http/transport-client.d.ts.map +0 -1
- package/dist/http/transport-client.js +0 -193
- package/dist/http/transport-client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/kysely-column-codecs.d.ts +0 -31
- package/dist/kysely-column-codecs.d.ts.map +0 -1
- package/dist/kysely-column-codecs.js +0 -452
- package/dist/kysely-column-codecs.js.map +0 -1
- package/dist/logger.d.ts +0 -29
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -26
- package/dist/logger.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -54
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -7
- package/dist/proxy/index.js.map +0 -1
- package/dist/schemas/blobs.d.ts +0 -82
- package/dist/schemas/blobs.d.ts.map +0 -1
- package/dist/schemas/blobs.js +0 -64
- package/dist/schemas/blobs.js.map +0 -1
- package/dist/schemas/common.d.ts +0 -100
- package/dist/schemas/common.d.ts.map +0 -1
- package/dist/schemas/common.js +0 -64
- package/dist/schemas/common.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -7
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -7
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/sync.d.ts +0 -1065
- package/dist/schemas/sync.d.ts.map +0 -1
- package/dist/schemas/sync.js +0 -361
- package/dist/schemas/sync.js.map +0 -1
- package/dist/scopes/index.d.ts +0 -113
- package/dist/scopes/index.d.ts.map +0 -1
- package/dist/scopes/index.js +0 -103
- package/dist/scopes/index.js.map +0 -1
- package/dist/sentry.d.ts +0 -31
- package/dist/sentry.d.ts.map +0 -1
- package/dist/sentry.js +0 -147
- package/dist/sentry.js.map +0 -1
- package/dist/snapshot-chunks.d.ts +0 -110
- package/dist/snapshot-chunks.d.ts.map +0 -1
- package/dist/snapshot-chunks.js +0 -682
- package/dist/snapshot-chunks.js.map +0 -1
- package/dist/sync-packs.d.ts +0 -33
- package/dist/sync-packs.d.ts.map +0 -1
- package/dist/sync-packs.js +0 -821
- package/dist/sync-packs.js.map +0 -1
- package/dist/telemetry.d.ts +0 -114
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -114
- package/dist/telemetry.js.map +0 -1
- package/dist/transforms.d.ts +0 -146
- package/dist/transforms.d.ts.map +0 -1
- package/dist/transforms.js +0 -155
- package/dist/transforms.js.map +0 -1
- package/dist/types.d.ts +0 -210
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -20
- package/dist/types.js.map +0 -1
- package/dist/utils/bytes.d.ts +0 -4
- package/dist/utils/bytes.d.ts.map +0 -1
- package/dist/utils/bytes.js +0 -66
- package/dist/utils/bytes.js.map +0 -1
- package/dist/utils/compression.d.ts +0 -22
- package/dist/utils/compression.d.ts.map +0 -1
- package/dist/utils/compression.js +0 -129
- package/dist/utils/compression.js.map +0 -1
- package/dist/utils/crypto.d.ts +0 -13
- package/dist/utils/crypto.d.ts.map +0 -1
- package/dist/utils/crypto.js +0 -107
- package/dist/utils/crypto.js.map +0 -1
- package/dist/utils/id.d.ts +0 -3
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -25
- package/dist/utils/id.js.map +0 -1
- package/dist/utils/index.d.ts +0 -10
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -10
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/internal-runtime.d.ts +0 -16
- package/dist/utils/internal-runtime.d.ts.map +0 -1
- package/dist/utils/internal-runtime.js +0 -14
- package/dist/utils/internal-runtime.js.map +0 -1
- package/dist/utils/object.d.ts +0 -2
- package/dist/utils/object.d.ts.map +0 -1
- package/dist/utils/object.js +0 -4
- package/dist/utils/object.js.map +0 -1
- package/dist/utils/realtime-change-index.d.ts +0 -9
- package/dist/utils/realtime-change-index.d.ts.map +0 -1
- package/dist/utils/realtime-change-index.js +0 -36
- package/dist/utils/realtime-change-index.js.map +0 -1
- package/dist/utils/realtime-connection-registry.d.ts +0 -40
- package/dist/utils/realtime-connection-registry.d.ts.map +0 -1
- package/dist/utils/realtime-connection-registry.js +0 -237
- package/dist/utils/realtime-connection-registry.js.map +0 -1
- package/dist/utils/table-registry.d.ts +0 -10
- package/dist/utils/table-registry.d.ts.map +0 -1
- package/dist/utils/table-registry.js +0 -54
- package/dist/utils/table-registry.js.map +0 -1
- package/dist/utils/url.d.ts +0 -3
- package/dist/utils/url.d.ts.map +0 -1
- package/dist/utils/url.js +0 -20
- package/dist/utils/url.js.map +0 -1
- package/src/blobs.ts +0 -219
- package/src/column-codecs.ts +0 -262
- package/src/conflict.ts +0 -92
- package/src/database.ts +0 -21
- package/src/dialect.ts +0 -8
- package/src/error-responses.ts +0 -423
- package/src/http/api-client.ts +0 -39
- package/src/http/blob.ts +0 -95
- package/src/http/generated/api.ts +0 -5241
- package/src/http/index.ts +0 -365
- package/src/http/shared.ts +0 -220
- package/src/http/transport-client.ts +0 -314
- package/src/kysely-column-codecs.ts +0 -585
- package/src/logger.ts +0 -38
- package/src/proxy/index.ts +0 -57
- package/src/schemas/blobs.ts +0 -110
- package/src/schemas/common.ts +0 -89
- package/src/schemas/index.ts +0 -7
- package/src/schemas/sync.ts +0 -517
- package/src/scopes/index.ts +0 -237
- package/src/sentry.ts +0 -200
- package/src/snapshot-chunks.ts +0 -964
- package/src/sync-packs.ts +0 -1096
- package/src/telemetry.ts +0 -243
- package/src/transforms.ts +0 -256
- package/src/types.ts +0 -267
- package/src/utils/bytes.ts +0 -75
- package/src/utils/compression.ts +0 -187
- package/src/utils/crypto.ts +0 -146
- package/src/utils/id.ts +0 -34
- package/src/utils/index.ts +0 -9
- package/src/utils/internal-runtime.ts +0 -39
- package/src/utils/object.ts +0 -3
- package/src/utils/realtime-change-index.ts +0 -44
- package/src/utils/realtime-connection-registry.ts +0 -286
- package/src/utils/table-registry.ts +0 -67
- 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
|
|
3
|
+
Core protocol types and shared utilities for the Syncular sync framework.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
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;
|
package/dist/blob-ref.js
ADDED
|
@@ -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
|
+
}
|
package/dist/bytes.d.ts
ADDED
|
@@ -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";
|
package/dist/crypto.d.ts
ADDED
|
@@ -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>;
|