@syncular/core 0.1.3 → 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/dist/crypto.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side encryption primitives (SPEC.md §5.11).
|
|
3
|
+
*
|
|
4
|
+
* The byte-exact ciphertext envelope, the declared-type value serializer,
|
|
5
|
+
* and AES-256-GCM encrypt/decrypt over WebCrypto (a global — this keeps
|
|
6
|
+
* `@syncular/core` dependency-free). These are the codec-level pieces shared
|
|
7
|
+
* by the wire boundary: the client encrypts a configured column's plaintext
|
|
8
|
+
* value here before the row codec (§2.4) sees it as a `bytes` value, and
|
|
9
|
+
* decrypts on apply. The row codec itself never touches this file.
|
|
10
|
+
*
|
|
11
|
+
* X25519 sealed-box key wrapping (the "async encryption" utilities) lives in
|
|
12
|
+
* `@syncular/crypto` (it needs an audited curve implementation); this module
|
|
13
|
+
* is the symmetric core and the envelope both sides agree on byte-for-byte.
|
|
14
|
+
*/
|
|
15
|
+
import { ByteReader, ByteWriter } from './bytes.js';
|
|
16
|
+
/** Envelope version byte (SPEC.md §5.11). */
|
|
17
|
+
export const ENVELOPE_VERSION = 0x01;
|
|
18
|
+
/** AES-GCM nonce length (96-bit, the GCM standard). */
|
|
19
|
+
export const NONCE_LENGTH = 12;
|
|
20
|
+
/** Symmetric key length: AES-256. */
|
|
21
|
+
export const KEY_LENGTH = 32;
|
|
22
|
+
/**
|
|
23
|
+
* Client-local decrypt failure (SPEC.md §5.11, §10.3). Never on the wire —
|
|
24
|
+
* the `client.` family is client-only. Raised at the apply seam for an
|
|
25
|
+
* unknown envelope version, unknown `keyId`, GCM auth failure (wrong key), a
|
|
26
|
+
* malformed envelope, or a post-decrypt value-parse failure. Not retryable.
|
|
27
|
+
*/
|
|
28
|
+
export class DecryptError extends Error {
|
|
29
|
+
name = 'DecryptError';
|
|
30
|
+
code = 'client.decrypt_failed';
|
|
31
|
+
retryable = false;
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function secureRandomNonce() {
|
|
37
|
+
const nonce = new Uint8Array(NONCE_LENGTH);
|
|
38
|
+
crypto.getRandomValues(nonce);
|
|
39
|
+
return nonce;
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Value serializer — declared type ⇄ canonical plaintext bytes (§5.11)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const textEncoder = new TextEncoder();
|
|
45
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: true });
|
|
46
|
+
/**
|
|
47
|
+
* Serialize a declared-type value to the canonical plaintext bytes fed to
|
|
48
|
+
* GCM (SPEC.md §5.11 value serializer). Not a re-run of the row codec — this
|
|
49
|
+
* is the self-describing per-`declaredType` encoding both cores agree on.
|
|
50
|
+
*/
|
|
51
|
+
export function serializePlain(declaredType, value) {
|
|
52
|
+
switch (declaredType) {
|
|
53
|
+
case 'string':
|
|
54
|
+
case 'json':
|
|
55
|
+
case 'blob_ref':
|
|
56
|
+
if (typeof value !== 'string') {
|
|
57
|
+
throw new Error(`encrypted ${declaredType} column requires a string`);
|
|
58
|
+
}
|
|
59
|
+
return textEncoder.encode(value);
|
|
60
|
+
case 'integer': {
|
|
61
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
|
|
62
|
+
throw new Error('encrypted integer column requires a safe integer');
|
|
63
|
+
}
|
|
64
|
+
const out = new Uint8Array(8);
|
|
65
|
+
new DataView(out.buffer).setBigInt64(0, BigInt(value), true);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
case 'float': {
|
|
69
|
+
if (typeof value !== 'number') {
|
|
70
|
+
throw new Error('encrypted float column requires a number');
|
|
71
|
+
}
|
|
72
|
+
const out = new Uint8Array(8);
|
|
73
|
+
new DataView(out.buffer).setFloat64(0, value, true);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
case 'boolean':
|
|
77
|
+
if (typeof value !== 'boolean') {
|
|
78
|
+
throw new Error('encrypted boolean column requires a boolean');
|
|
79
|
+
}
|
|
80
|
+
return new Uint8Array([value ? 1 : 0]);
|
|
81
|
+
case 'bytes':
|
|
82
|
+
if (!(value instanceof Uint8Array)) {
|
|
83
|
+
throw new Error('encrypted bytes column requires a Uint8Array');
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parse the decrypted plaintext bytes back to a declared-type value (SPEC.md
|
|
90
|
+
* §5.11). A `json`/`blob_ref` value is re-validated as the row codec would
|
|
91
|
+
* (§2.4); any parse failure is a decrypt failure.
|
|
92
|
+
*/
|
|
93
|
+
export function deserializePlain(declaredType, bytes) {
|
|
94
|
+
switch (declaredType) {
|
|
95
|
+
case 'string':
|
|
96
|
+
case 'json':
|
|
97
|
+
case 'blob_ref': {
|
|
98
|
+
let text;
|
|
99
|
+
try {
|
|
100
|
+
text = textDecoder.decode(bytes);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new DecryptError(`decrypted ${declaredType} value is not valid UTF-8`);
|
|
104
|
+
}
|
|
105
|
+
if (declaredType === 'json') {
|
|
106
|
+
try {
|
|
107
|
+
JSON.parse(text);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new DecryptError('decrypted json value does not parse');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return text;
|
|
114
|
+
}
|
|
115
|
+
case 'integer': {
|
|
116
|
+
if (bytes.length !== 8) {
|
|
117
|
+
throw new DecryptError('decrypted integer must be 8 bytes');
|
|
118
|
+
}
|
|
119
|
+
const big = new DataView(bytes.buffer, bytes.byteOffset, 8).getBigInt64(0, true);
|
|
120
|
+
const num = Number(big);
|
|
121
|
+
if (!Number.isSafeInteger(num)) {
|
|
122
|
+
throw new DecryptError('decrypted integer outside safe range');
|
|
123
|
+
}
|
|
124
|
+
return num;
|
|
125
|
+
}
|
|
126
|
+
case 'float': {
|
|
127
|
+
if (bytes.length !== 8) {
|
|
128
|
+
throw new DecryptError('decrypted float must be 8 bytes');
|
|
129
|
+
}
|
|
130
|
+
return new DataView(bytes.buffer, bytes.byteOffset, 8).getFloat64(0, true);
|
|
131
|
+
}
|
|
132
|
+
case 'boolean': {
|
|
133
|
+
if (bytes.length !== 1 || (bytes[0] !== 0 && bytes[0] !== 1)) {
|
|
134
|
+
throw new DecryptError('decrypted boolean must be one 0x00/0x01 byte');
|
|
135
|
+
}
|
|
136
|
+
return bytes[0] === 1;
|
|
137
|
+
}
|
|
138
|
+
case 'bytes':
|
|
139
|
+
return bytes;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function encodeEnvelope(envelope) {
|
|
143
|
+
const keyIdBytes = textEncoder.encode(envelope.keyId);
|
|
144
|
+
if (keyIdBytes.length > 0xff) {
|
|
145
|
+
throw new Error('keyId exceeds 255 UTF-8 bytes');
|
|
146
|
+
}
|
|
147
|
+
if (envelope.nonce.length !== NONCE_LENGTH) {
|
|
148
|
+
throw new Error(`nonce must be ${NONCE_LENGTH} bytes`);
|
|
149
|
+
}
|
|
150
|
+
const writer = new ByteWriter();
|
|
151
|
+
writer.u8(ENVELOPE_VERSION);
|
|
152
|
+
writer.u8(keyIdBytes.length);
|
|
153
|
+
writer.raw(keyIdBytes);
|
|
154
|
+
writer.raw(envelope.nonce);
|
|
155
|
+
writer.raw(envelope.ciphertext);
|
|
156
|
+
return writer.finish();
|
|
157
|
+
}
|
|
158
|
+
export function decodeEnvelope(bytes) {
|
|
159
|
+
const reader = new ByteReader(bytes);
|
|
160
|
+
let version;
|
|
161
|
+
let keyIdLen;
|
|
162
|
+
try {
|
|
163
|
+
version = reader.u8();
|
|
164
|
+
keyIdLen = reader.u8();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new DecryptError('encrypted value: envelope truncated');
|
|
168
|
+
}
|
|
169
|
+
if (version !== ENVELOPE_VERSION) {
|
|
170
|
+
throw new DecryptError(`encrypted value: unknown envelope version 0x${version.toString(16)}`);
|
|
171
|
+
}
|
|
172
|
+
let keyIdBytes;
|
|
173
|
+
let nonce;
|
|
174
|
+
try {
|
|
175
|
+
keyIdBytes = reader.raw(keyIdLen);
|
|
176
|
+
nonce = reader.raw(NONCE_LENGTH);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
throw new DecryptError('encrypted value: envelope truncated');
|
|
180
|
+
}
|
|
181
|
+
// The remaining bytes are ciphertext+tag; must be at least the 16-byte tag.
|
|
182
|
+
const ciphertext = bytes.subarray(2 + keyIdLen + NONCE_LENGTH);
|
|
183
|
+
if (ciphertext.length < 16) {
|
|
184
|
+
throw new DecryptError('encrypted value: ciphertext shorter than GCM tag');
|
|
185
|
+
}
|
|
186
|
+
let keyId;
|
|
187
|
+
try {
|
|
188
|
+
keyId = textDecoder.decode(keyIdBytes);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new DecryptError('encrypted value: keyId is not valid UTF-8');
|
|
192
|
+
}
|
|
193
|
+
return { keyId, nonce, ciphertext: ciphertext.slice() };
|
|
194
|
+
}
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// AES-256-GCM over WebCrypto
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
/**
|
|
199
|
+
* Copy into a fresh `ArrayBuffer`-backed view. WebCrypto's `BufferSource`
|
|
200
|
+
* type rejects a `Uint8Array<ArrayBufferLike>` (it could be SharedArrayBuffer);
|
|
201
|
+
* a plain copy is unambiguously `ArrayBuffer`-backed.
|
|
202
|
+
*/
|
|
203
|
+
function toBuffer(bytes) {
|
|
204
|
+
const copy = new Uint8Array(bytes.length);
|
|
205
|
+
copy.set(bytes);
|
|
206
|
+
return copy.buffer;
|
|
207
|
+
}
|
|
208
|
+
async function importAesKey(key) {
|
|
209
|
+
if (key.length !== KEY_LENGTH) {
|
|
210
|
+
throw new Error(`AES-256-GCM key must be ${KEY_LENGTH} bytes`);
|
|
211
|
+
}
|
|
212
|
+
return crypto.subtle.importKey('raw', toBuffer(key), 'AES-GCM', false, [
|
|
213
|
+
'encrypt',
|
|
214
|
+
'decrypt',
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Encrypt one declared-type value into a §5.11 envelope. `nonceSource`
|
|
219
|
+
* defaults to a secure RNG; vectors inject a fixed nonce.
|
|
220
|
+
*/
|
|
221
|
+
export async function encryptValue(declaredType, value, keyId, key, nonceSource = secureRandomNonce) {
|
|
222
|
+
const plaintext = serializePlain(declaredType, value);
|
|
223
|
+
const nonce = nonceSource();
|
|
224
|
+
if (nonce.length !== NONCE_LENGTH) {
|
|
225
|
+
throw new Error(`nonce source must yield ${NONCE_LENGTH} bytes`);
|
|
226
|
+
}
|
|
227
|
+
const cryptoKey = await importAesKey(key);
|
|
228
|
+
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: toBuffer(nonce), tagLength: 128 }, cryptoKey, toBuffer(plaintext)));
|
|
229
|
+
return encodeEnvelope({ keyId, nonce, ciphertext: ct });
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Decrypt a §5.11 envelope back to a declared-type value. `keyProvider`
|
|
233
|
+
* resolves the envelope's `keyId` to key bytes; a missing key or GCM tag
|
|
234
|
+
* mismatch is {@link DecryptError} (`client.decrypt_failed`).
|
|
235
|
+
*/
|
|
236
|
+
export async function decryptValue(declaredType, envelopeBytes, keyProvider) {
|
|
237
|
+
const envelope = decodeEnvelope(envelopeBytes);
|
|
238
|
+
const key = keyProvider(envelope.keyId);
|
|
239
|
+
if (key === undefined) {
|
|
240
|
+
throw new DecryptError(`no key for keyId ${JSON.stringify(envelope.keyId)}`);
|
|
241
|
+
}
|
|
242
|
+
const cryptoKey = await importAesKey(key);
|
|
243
|
+
let plaintext;
|
|
244
|
+
try {
|
|
245
|
+
plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: toBuffer(envelope.nonce), tagLength: 128 }, cryptoKey, toBuffer(envelope.ciphertext)));
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw new DecryptError(`GCM authentication failed for keyId ${JSON.stringify(envelope.keyId)} (wrong key or corrupt ciphertext)`);
|
|
249
|
+
}
|
|
250
|
+
return deserializePlain(declaredType, plaintext);
|
|
251
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named decode errors (SPEC.md Conventions + §10).
|
|
3
|
+
*
|
|
4
|
+
* Every decode failure in the reference codec throws a `DecodeError` whose
|
|
5
|
+
* `code` is a stable identifier from the SPEC.md §10 catalog (structural
|
|
6
|
+
* failures use `sync.invalid_request`; frame-specific rules use their named
|
|
7
|
+
* code, e.g. `sync.empty_commit`). Golden vector negative cases assert on
|
|
8
|
+
* `code`, never on `message`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class DecodeError extends Error {
|
|
11
|
+
readonly name = "DecodeError";
|
|
12
|
+
readonly code: string;
|
|
13
|
+
constructor(code: string, message: string);
|
|
14
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named decode errors (SPEC.md Conventions + §10).
|
|
3
|
+
*
|
|
4
|
+
* Every decode failure in the reference codec throws a `DecodeError` whose
|
|
5
|
+
* `code` is a stable identifier from the SPEC.md §10 catalog (structural
|
|
6
|
+
* failures use `sync.invalid_request`; frame-specific rules use their named
|
|
7
|
+
* code, e.g. `sync.empty_commit`). Golden vector negative cases assert on
|
|
8
|
+
* `code`, never on `message`.
|
|
9
|
+
*/
|
|
10
|
+
export class DecodeError extends Error {
|
|
11
|
+
name = 'DecodeError';
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/frames.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Frame type registry for wire version 1 (SPEC.md §1.2). */
|
|
2
|
+
export declare const FrameType: {
|
|
3
|
+
readonly END: 0;
|
|
4
|
+
readonly REQ_HEADER: 1;
|
|
5
|
+
readonly PUSH_COMMIT: 2;
|
|
6
|
+
readonly PULL_HEADER: 3;
|
|
7
|
+
readonly SUBSCRIPTION: 4;
|
|
8
|
+
readonly RESP_HEADER: 16;
|
|
9
|
+
readonly PUSH_RESULT: 17;
|
|
10
|
+
readonly SUB_START: 18;
|
|
11
|
+
readonly COMMIT: 19;
|
|
12
|
+
readonly SEGMENT_REF: 20;
|
|
13
|
+
readonly SEGMENT_INLINE: 21;
|
|
14
|
+
readonly SUB_END: 22;
|
|
15
|
+
readonly LEASE: 25;
|
|
16
|
+
readonly ERROR: 31;
|
|
17
|
+
};
|
|
18
|
+
export type FrameTypeName = keyof typeof FrameType;
|
|
19
|
+
/** Known request frame types (excluding END). */
|
|
20
|
+
export declare const REQUEST_FRAME_TYPES: ReadonlySet<number>;
|
|
21
|
+
/** Known response frame types (excluding END). */
|
|
22
|
+
export declare const RESPONSE_FRAME_TYPES: ReadonlySet<number>;
|
|
23
|
+
/** Every frame type with a defined layout in wire version 1. */
|
|
24
|
+
export declare const KNOWN_FRAME_TYPES: ReadonlySet<number>;
|
package/dist/frames.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Frame type registry for wire version 1 (SPEC.md §1.2). */
|
|
2
|
+
export const FrameType = {
|
|
3
|
+
END: 0x00,
|
|
4
|
+
REQ_HEADER: 0x01,
|
|
5
|
+
PUSH_COMMIT: 0x02,
|
|
6
|
+
PULL_HEADER: 0x03,
|
|
7
|
+
SUBSCRIPTION: 0x04,
|
|
8
|
+
RESP_HEADER: 0x10,
|
|
9
|
+
PUSH_RESULT: 0x11,
|
|
10
|
+
SUB_START: 0x12,
|
|
11
|
+
COMMIT: 0x13,
|
|
12
|
+
SEGMENT_REF: 0x14,
|
|
13
|
+
SEGMENT_INLINE: 0x15,
|
|
14
|
+
SUB_END: 0x16,
|
|
15
|
+
LEASE: 0x19,
|
|
16
|
+
ERROR: 0x1f,
|
|
17
|
+
};
|
|
18
|
+
/** Known request frame types (excluding END). */
|
|
19
|
+
export const REQUEST_FRAME_TYPES = new Set([
|
|
20
|
+
FrameType.REQ_HEADER,
|
|
21
|
+
FrameType.PUSH_COMMIT,
|
|
22
|
+
FrameType.PULL_HEADER,
|
|
23
|
+
FrameType.SUBSCRIPTION,
|
|
24
|
+
]);
|
|
25
|
+
/** Known response frame types (excluding END). */
|
|
26
|
+
export const RESPONSE_FRAME_TYPES = new Set([
|
|
27
|
+
FrameType.RESP_HEADER,
|
|
28
|
+
FrameType.PUSH_RESULT,
|
|
29
|
+
FrameType.SUB_START,
|
|
30
|
+
FrameType.COMMIT,
|
|
31
|
+
FrameType.SEGMENT_REF,
|
|
32
|
+
FrameType.SEGMENT_INLINE,
|
|
33
|
+
FrameType.SUB_END,
|
|
34
|
+
FrameType.LEASE,
|
|
35
|
+
FrameType.ERROR,
|
|
36
|
+
]);
|
|
37
|
+
/** Every frame type with a defined layout in wire version 1. */
|
|
38
|
+
export const KNOWN_FRAME_TYPES = new Set([
|
|
39
|
+
FrameType.END,
|
|
40
|
+
...REQUEST_FRAME_TYPES,
|
|
41
|
+
...RESPONSE_FRAME_TYPES,
|
|
42
|
+
]);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @syncular/core
|
|
2
|
+
* @syncular/core — reference SSP2 codec (SPEC.md is normative).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - Data transformation hooks (optional)
|
|
9
|
-
* - Blob types for media/binary handling
|
|
10
|
-
* - Zod schemas for runtime validation and OpenAPI
|
|
4
|
+
* Dependency-free: primitives (Conventions), envelope + framing (§1),
|
|
5
|
+
* schema-IR row codec (§2.4), SSG2 rows segments (§5.2), message codecs
|
|
6
|
+
* (§1.5/§1.6), realtime control messages (§8), the §11 canonical JSON
|
|
7
|
+
* debug rendering, and §11.2 canonical JSON for digests.
|
|
11
8
|
*/
|
|
12
|
-
export * from './
|
|
13
|
-
export * from './
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
16
|
-
export * from './
|
|
17
|
-
export * from './
|
|
18
|
-
export * from './
|
|
19
|
-
export * from './
|
|
20
|
-
export * from './
|
|
21
|
-
export * from './
|
|
22
|
-
export * from './
|
|
23
|
-
export * from './
|
|
24
|
-
export * from './
|
|
25
|
-
export * from './telemetry';
|
|
26
|
-
export * from './transforms';
|
|
27
|
-
export * from './types';
|
|
28
|
-
export * from './utils';
|
|
29
|
-
//# sourceMappingURL=index.d.ts.map
|
|
9
|
+
export * from './blob-ref.js';
|
|
10
|
+
export * from './bytes.js';
|
|
11
|
+
export * from './canonical-json.js';
|
|
12
|
+
export * from './constants.js';
|
|
13
|
+
export * from './crypto.js';
|
|
14
|
+
export * from './errors.js';
|
|
15
|
+
export * from './frames.js';
|
|
16
|
+
export * from './message.js';
|
|
17
|
+
export * from './realtime.js';
|
|
18
|
+
export * from './render.js';
|
|
19
|
+
export * from './row-codec.js';
|
|
20
|
+
export * from './segment.js';
|
|
21
|
+
export * from './stream.js';
|
package/dist/index.js
CHANGED
|
@@ -1,46 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @syncular/core
|
|
2
|
+
* @syncular/core — reference SSP2 codec (SPEC.md is normative).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - Data transformation hooks (optional)
|
|
9
|
-
* - Blob types for media/binary handling
|
|
10
|
-
* - Zod schemas for runtime validation and OpenAPI
|
|
4
|
+
* Dependency-free: primitives (Conventions), envelope + framing (§1),
|
|
5
|
+
* schema-IR row codec (§2.4), SSG2 rows segments (§5.2), message codecs
|
|
6
|
+
* (§1.5/§1.6), realtime control messages (§8), the §11 canonical JSON
|
|
7
|
+
* debug rendering, and §11.2 canonical JSON for digests.
|
|
11
8
|
*/
|
|
12
|
-
|
|
13
|
-
export * from './
|
|
14
|
-
|
|
15
|
-
export * from './
|
|
16
|
-
|
|
17
|
-
export * from './
|
|
18
|
-
|
|
19
|
-
export * from './
|
|
20
|
-
|
|
21
|
-
export * from './
|
|
22
|
-
|
|
23
|
-
export * from './
|
|
24
|
-
|
|
25
|
-
export * from './kysely-column-codecs.js';
|
|
26
|
-
// Logging utilities
|
|
27
|
-
export * from './logger.js';
|
|
28
|
-
// Proxy protocol types
|
|
29
|
-
export * from './proxy/index.js';
|
|
30
|
-
// Schemas (Zod)
|
|
31
|
-
export * from './schemas/index.js';
|
|
32
|
-
// Scope types, patterns, and utilities
|
|
33
|
-
export * from './scopes/index.js';
|
|
34
|
-
// Snapshot chunk encoding helpers
|
|
35
|
-
export * from './snapshot-chunks.js';
|
|
36
|
-
// Sync response pack encoding helpers
|
|
37
|
-
export * from './sync-packs.js';
|
|
38
|
-
// Telemetry abstraction
|
|
39
|
-
export * from './telemetry.js';
|
|
40
|
-
// Data transformation hooks
|
|
41
|
-
export * from './transforms.js';
|
|
42
|
-
// Transport and conflict types (protocol types come from ./schemas)
|
|
43
|
-
export * from './types.js';
|
|
44
|
-
// Shared runtime utilities
|
|
45
|
-
export * from './utils/index.js';
|
|
46
|
-
//# sourceMappingURL=index.js.map
|
|
9
|
+
export * from './blob-ref.js';
|
|
10
|
+
export * from './bytes.js';
|
|
11
|
+
export * from './canonical-json.js';
|
|
12
|
+
export * from './constants.js';
|
|
13
|
+
export * from './crypto.js';
|
|
14
|
+
export * from './errors.js';
|
|
15
|
+
export * from './frames.js';
|
|
16
|
+
export * from './message.js';
|
|
17
|
+
export * from './realtime.js';
|
|
18
|
+
export * from './render.js';
|
|
19
|
+
export * from './row-codec.js';
|
|
20
|
+
export * from './segment.js';
|
|
21
|
+
export * from './stream.js';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export type ScopeMap = Record<string, string[]>;
|
|
2
|
+
export interface ReqHeaderFrame {
|
|
3
|
+
type: 'REQ_HEADER';
|
|
4
|
+
clientId: string;
|
|
5
|
+
schemaVersion: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PushOperation {
|
|
8
|
+
table: string;
|
|
9
|
+
rowId: string;
|
|
10
|
+
op: 'upsert' | 'delete';
|
|
11
|
+
/** Optimistic-concurrency token (§6.2); absent = last-write-wins. */
|
|
12
|
+
baseVersion?: number;
|
|
13
|
+
/** Generated-row-codec bytes; present iff `op` is `upsert` (§6.1). */
|
|
14
|
+
payload?: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
export interface PushCommitFrame {
|
|
17
|
+
type: 'PUSH_COMMIT';
|
|
18
|
+
clientCommitId: string;
|
|
19
|
+
operations: PushOperation[];
|
|
20
|
+
}
|
|
21
|
+
export interface PullHeaderFrame {
|
|
22
|
+
type: 'PULL_HEADER';
|
|
23
|
+
limitCommits: number;
|
|
24
|
+
limitSnapshotRows: number;
|
|
25
|
+
maxSnapshotPages: number;
|
|
26
|
+
/** Bitmask (§4.2): bit 0 inline rows, 1 external rows, 2 sqlite, 3 signed URLs. */
|
|
27
|
+
accept: number;
|
|
28
|
+
}
|
|
29
|
+
export interface SubscriptionFrame {
|
|
30
|
+
type: 'SUBSCRIPTION';
|
|
31
|
+
id: string;
|
|
32
|
+
table: string;
|
|
33
|
+
scopes: ScopeMap;
|
|
34
|
+
/** Host-opaque JSON document, preserved verbatim. */
|
|
35
|
+
params?: string;
|
|
36
|
+
cursor: number;
|
|
37
|
+
/** Opaque resume token JSON, round-tripped byte-for-byte (§4.7). */
|
|
38
|
+
bootstrapState?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface UnknownFrame {
|
|
41
|
+
type: 'UNKNOWN';
|
|
42
|
+
frameType: number;
|
|
43
|
+
payload: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
export type RequestFrame = ReqHeaderFrame | PushCommitFrame | PullHeaderFrame | SubscriptionFrame | UnknownFrame;
|
|
46
|
+
export interface RespHeaderFrame {
|
|
47
|
+
type: 'RESP_HEADER';
|
|
48
|
+
requiredSchemaVersion?: number;
|
|
49
|
+
latestSchemaVersion?: number;
|
|
50
|
+
}
|
|
51
|
+
/** §7.3.2: a server-issued auth lease delivered to the client (opaque). */
|
|
52
|
+
export interface LeaseFrame {
|
|
53
|
+
type: 'LEASE';
|
|
54
|
+
leaseId: string;
|
|
55
|
+
expiresAtMs: number;
|
|
56
|
+
}
|
|
57
|
+
export type PushOperationResult = {
|
|
58
|
+
opIndex: number;
|
|
59
|
+
status: 'applied';
|
|
60
|
+
} | {
|
|
61
|
+
opIndex: number;
|
|
62
|
+
status: 'conflict';
|
|
63
|
+
code: string;
|
|
64
|
+
message: string;
|
|
65
|
+
serverVersion: number;
|
|
66
|
+
serverRow: Uint8Array;
|
|
67
|
+
} | {
|
|
68
|
+
opIndex: number;
|
|
69
|
+
status: 'error';
|
|
70
|
+
code: string;
|
|
71
|
+
message: string;
|
|
72
|
+
retryable: boolean;
|
|
73
|
+
};
|
|
74
|
+
export interface PushResultFrame {
|
|
75
|
+
type: 'PUSH_RESULT';
|
|
76
|
+
clientCommitId: string;
|
|
77
|
+
status: 'applied' | 'cached' | 'rejected';
|
|
78
|
+
/** Present iff `status` is `applied` or `cached` (§6.3). */
|
|
79
|
+
commitSeq?: number;
|
|
80
|
+
results: PushOperationResult[];
|
|
81
|
+
}
|
|
82
|
+
export interface SubStartFrame {
|
|
83
|
+
type: 'SUB_START';
|
|
84
|
+
id: string;
|
|
85
|
+
status: 'active' | 'revoked' | 'reset';
|
|
86
|
+
reasonCode: string;
|
|
87
|
+
effectiveScopes: ScopeMap;
|
|
88
|
+
bootstrap: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface CommitChange {
|
|
91
|
+
tableIndex: number;
|
|
92
|
+
rowId: string;
|
|
93
|
+
op: 'upsert' | 'delete';
|
|
94
|
+
/** Present iff `op` is `upsert` (§4.5). */
|
|
95
|
+
rowVersion?: number;
|
|
96
|
+
scopes: Record<string, string>;
|
|
97
|
+
/** Generated-row-codec bytes; present iff `op` is `upsert` (§4.5). */
|
|
98
|
+
row?: Uint8Array;
|
|
99
|
+
}
|
|
100
|
+
export interface CommitFrame {
|
|
101
|
+
type: 'COMMIT';
|
|
102
|
+
commitSeq: number;
|
|
103
|
+
createdAtMs: number;
|
|
104
|
+
actorId: string;
|
|
105
|
+
tables: string[];
|
|
106
|
+
changes: CommitChange[];
|
|
107
|
+
}
|
|
108
|
+
export interface SegmentRefFrame {
|
|
109
|
+
type: 'SEGMENT_REF';
|
|
110
|
+
segmentId: string;
|
|
111
|
+
mediaType: 'rows' | 'sqlite';
|
|
112
|
+
table: string;
|
|
113
|
+
byteLength: number;
|
|
114
|
+
rowCount: number;
|
|
115
|
+
asOfCommitSeq: number;
|
|
116
|
+
scopeDigest: string;
|
|
117
|
+
rowCursor?: string;
|
|
118
|
+
nextRowCursor?: string;
|
|
119
|
+
url?: string;
|
|
120
|
+
/** Present iff `url` is (§5.4). */
|
|
121
|
+
urlExpiresAtMs?: number;
|
|
122
|
+
}
|
|
123
|
+
export interface SegmentInlineFrame {
|
|
124
|
+
type: 'SEGMENT_INLINE';
|
|
125
|
+
/** One complete SSG2 rows segment, including magic (§5.7). */
|
|
126
|
+
payload: Uint8Array;
|
|
127
|
+
}
|
|
128
|
+
export interface SubEndFrame {
|
|
129
|
+
type: 'SUB_END';
|
|
130
|
+
nextCursor: number;
|
|
131
|
+
/** Present iff the bootstrap is incomplete (§4.4). */
|
|
132
|
+
bootstrapState?: string;
|
|
133
|
+
}
|
|
134
|
+
export interface ErrorFrame {
|
|
135
|
+
type: 'ERROR';
|
|
136
|
+
code: string;
|
|
137
|
+
message: string;
|
|
138
|
+
category: string;
|
|
139
|
+
retryable: boolean;
|
|
140
|
+
recommendedAction: string;
|
|
141
|
+
details?: string;
|
|
142
|
+
}
|
|
143
|
+
export type ResponseFrame = RespHeaderFrame | LeaseFrame | PushResultFrame | SubStartFrame | CommitFrame | SegmentRefFrame | SegmentInlineFrame | SubEndFrame | ErrorFrame | UnknownFrame;
|
|
144
|
+
export interface RequestMessage {
|
|
145
|
+
wireVersion: number;
|
|
146
|
+
msgKind: 'request';
|
|
147
|
+
frames: RequestFrame[];
|
|
148
|
+
}
|
|
149
|
+
export interface ResponseMessage {
|
|
150
|
+
wireVersion: number;
|
|
151
|
+
msgKind: 'response';
|
|
152
|
+
frames: ResponseFrame[];
|
|
153
|
+
}
|
|
154
|
+
export type SyncMessage = RequestMessage | ResponseMessage;
|
|
155
|
+
export declare function validateFrameSequence(message: SyncMessage): void;
|
|
156
|
+
export declare function encodeMessage(message: SyncMessage): Uint8Array;
|
|
157
|
+
export declare function decodeMessage(bytes: Uint8Array): SyncMessage;
|