@syncular/client 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 (233) hide show
  1. package/README.md +227 -709
  2. package/dist/apply.d.ts +80 -0
  3. package/dist/apply.js +301 -0
  4. package/dist/blob.d.ts +127 -0
  5. package/dist/blob.js +201 -0
  6. package/dist/bun-database.d.ts +22 -0
  7. package/dist/bun-database.js +61 -0
  8. package/dist/client.d.ts +320 -69
  9. package/dist/client.js +1627 -163
  10. package/dist/content-type.d.ts +2 -0
  11. package/dist/content-type.js +2 -0
  12. package/dist/database.d.ts +35 -80
  13. package/dist/database.js +32 -947
  14. package/dist/encryption.d.ts +47 -0
  15. package/dist/encryption.js +75 -0
  16. package/dist/errors.d.ts +8 -22
  17. package/dist/errors.js +10 -207
  18. package/dist/http.d.ts +32 -0
  19. package/dist/http.js +196 -0
  20. package/dist/index.d.ts +28 -16
  21. package/dist/index.js +25 -12
  22. package/dist/invalidation.d.ts +69 -0
  23. package/dist/invalidation.js +84 -0
  24. package/dist/leader-lock.d.ts +28 -0
  25. package/dist/leader-lock.js +38 -0
  26. package/dist/multi-tab.d.ts +134 -0
  27. package/dist/multi-tab.js +399 -0
  28. package/dist/node-database.d.ts +41 -0
  29. package/dist/node-database.js +133 -0
  30. package/dist/outbox.d.ts +56 -0
  31. package/dist/outbox.js +140 -0
  32. package/dist/schema.d.ts +107 -0
  33. package/dist/schema.js +267 -0
  34. package/dist/state.d.ts +40 -0
  35. package/dist/state.js +77 -0
  36. package/dist/transport.d.ts +44 -0
  37. package/dist/transport.js +7 -0
  38. package/dist/wasm-database.d.ts +30 -0
  39. package/dist/wasm-database.js +170 -0
  40. package/dist/window.d.ts +64 -0
  41. package/dist/window.js +0 -0
  42. package/dist/worker-entry.d.ts +16 -2
  43. package/dist/worker-entry.js +300 -456
  44. package/dist/worker-host.d.ts +174 -0
  45. package/dist/worker-host.js +536 -0
  46. package/dist/worker-protocol.d.ts +173 -61
  47. package/dist/worker-protocol.js +7 -16
  48. package/package.json +45 -112
  49. package/src/apply.ts +435 -0
  50. package/src/blob.ts +333 -0
  51. package/src/bun-database.ts +75 -0
  52. package/src/client.ts +2247 -277
  53. package/src/content-type.ts +2 -0
  54. package/src/database.ts +63 -1389
  55. package/src/encryption.ts +123 -0
  56. package/src/errors.ts +11 -265
  57. package/src/http.ts +254 -0
  58. package/src/index.ts +25 -20
  59. package/src/invalidation.ts +128 -0
  60. package/src/leader-lock.ts +68 -0
  61. package/src/multi-tab.ts +550 -0
  62. package/src/node-database.ts +180 -0
  63. package/src/outbox.ts +206 -0
  64. package/src/schema.ts +397 -0
  65. package/src/state.ts +126 -0
  66. package/src/transport.ts +52 -0
  67. package/src/wasm-database.ts +279 -0
  68. package/src/window.ts +0 -0
  69. package/src/worker-entry.ts +391 -545
  70. package/src/worker-host.ts +800 -0
  71. package/src/worker-protocol.ts +204 -99
  72. package/dist/auth-leases.d.ts +0 -11
  73. package/dist/auth-leases.d.ts.map +0 -1
  74. package/dist/auth-leases.js +0 -140
  75. package/dist/auth-leases.js.map +0 -1
  76. package/dist/blob-limits.d.ts +0 -15
  77. package/dist/blob-limits.d.ts.map +0 -1
  78. package/dist/blob-limits.js +0 -66
  79. package/dist/blob-limits.js.map +0 -1
  80. package/dist/bridge-client.d.ts +0 -69
  81. package/dist/bridge-client.d.ts.map +0 -1
  82. package/dist/bridge-client.js +0 -282
  83. package/dist/bridge-client.js.map +0 -1
  84. package/dist/client-config.d.ts +0 -6
  85. package/dist/client-config.d.ts.map +0 -1
  86. package/dist/client-config.js +0 -18
  87. package/dist/client-config.js.map +0 -1
  88. package/dist/client.d.ts.map +0 -1
  89. package/dist/client.js.map +0 -1
  90. package/dist/command-history.d.ts +0 -35
  91. package/dist/command-history.d.ts.map +0 -1
  92. package/dist/command-history.js +0 -378
  93. package/dist/command-history.js.map +0 -1
  94. package/dist/console-diagnostics.d.ts +0 -33
  95. package/dist/console-diagnostics.d.ts.map +0 -1
  96. package/dist/console-diagnostics.js +0 -496
  97. package/dist/console-diagnostics.js.map +0 -1
  98. package/dist/crdt-yjs/index.d.ts +0 -4
  99. package/dist/crdt-yjs/index.d.ts.map +0 -1
  100. package/dist/crdt-yjs/index.js +0 -4
  101. package/dist/crdt-yjs/index.js.map +0 -1
  102. package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
  103. package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
  104. package/dist/crdt-yjs/webview-host-facade.js +0 -284
  105. package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
  106. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
  107. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
  108. package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
  109. package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
  110. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
  111. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
  112. package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
  113. package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
  114. package/dist/database.d.ts.map +0 -1
  115. package/dist/database.js.map +0 -1
  116. package/dist/diagnostics.d.ts +0 -10
  117. package/dist/diagnostics.d.ts.map +0 -1
  118. package/dist/diagnostics.js +0 -83
  119. package/dist/diagnostics.js.map +0 -1
  120. package/dist/errors.d.ts.map +0 -1
  121. package/dist/errors.js.map +0 -1
  122. package/dist/generated-bridge.d.ts +0 -365
  123. package/dist/generated-bridge.d.ts.map +0 -1
  124. package/dist/generated-bridge.js +0 -250
  125. package/dist/generated-bridge.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/mutations.d.ts +0 -72
  129. package/dist/mutations.d.ts.map +0 -1
  130. package/dist/mutations.js +0 -63
  131. package/dist/mutations.js.map +0 -1
  132. package/dist/network.d.ts +0 -3
  133. package/dist/network.d.ts.map +0 -1
  134. package/dist/network.js +0 -17
  135. package/dist/network.js.map +0 -1
  136. package/dist/react/index.d.ts +0 -169
  137. package/dist/react/index.d.ts.map +0 -1
  138. package/dist/react/index.js +0 -628
  139. package/dist/react/index.js.map +0 -1
  140. package/dist/react-native/index.d.ts +0 -35
  141. package/dist/react-native/index.d.ts.map +0 -1
  142. package/dist/react-native/index.js +0 -49
  143. package/dist/react-native/index.js.map +0 -1
  144. package/dist/runtime-contract.d.ts +0 -13
  145. package/dist/runtime-contract.d.ts.map +0 -1
  146. package/dist/runtime-contract.js +0 -24
  147. package/dist/runtime-contract.js.map +0 -1
  148. package/dist/rust-client.d.ts +0 -106
  149. package/dist/rust-client.d.ts.map +0 -1
  150. package/dist/rust-client.js +0 -938
  151. package/dist/rust-client.js.map +0 -1
  152. package/dist/sentry.d.ts +0 -35
  153. package/dist/sentry.d.ts.map +0 -1
  154. package/dist/sentry.js +0 -155
  155. package/dist/sentry.js.map +0 -1
  156. package/dist/sql-safety.d.ts +0 -3
  157. package/dist/sql-safety.d.ts.map +0 -1
  158. package/dist/sql-safety.js +0 -62
  159. package/dist/sql-safety.js.map +0 -1
  160. package/dist/syncular-runtime-artifacts.json +0 -61
  161. package/dist/tauri/index.d.ts +0 -35
  162. package/dist/tauri/index.d.ts.map +0 -1
  163. package/dist/tauri/index.js +0 -114
  164. package/dist/tauri/index.js.map +0 -1
  165. package/dist/types.d.ts +0 -1096
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -2
  168. package/dist/types.js.map +0 -1
  169. package/dist/wasm/.syncular-wasm-profile +0 -1
  170. package/dist/wasm/syncular-runtime-artifact.json +0 -21
  171. package/dist/wasm/syncular.d.ts +0 -207
  172. package/dist/wasm/syncular.js +0 -2341
  173. package/dist/wasm/syncular_bg.wasm +0 -0
  174. package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
  175. package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
  176. package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
  177. package/dist/wasm-bindings/runtime-contract.js +0 -112
  178. package/dist/wasm-bindings/runtime-contract.js.map +0 -1
  179. package/dist/wasm-core/.syncular-wasm-profile +0 -1
  180. package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
  181. package/dist/wasm-core/syncular.d.ts +0 -162
  182. package/dist/wasm-core/syncular.js +0 -1847
  183. package/dist/wasm-core/syncular_bg.wasm +0 -0
  184. package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
  185. package/dist/wasm-perf/.syncular-wasm-profile +0 -1
  186. package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
  187. package/dist/wasm-perf/syncular.d.ts +0 -207
  188. package/dist/wasm-perf/syncular.js +0 -2341
  189. package/dist/wasm-perf/syncular_bg.wasm +0 -0
  190. package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
  191. package/dist/wasm-runtime.d.ts +0 -23
  192. package/dist/wasm-runtime.d.ts.map +0 -1
  193. package/dist/wasm-runtime.js +0 -69
  194. package/dist/wasm-runtime.js.map +0 -1
  195. package/dist/worker-client.d.ts +0 -123
  196. package/dist/worker-client.d.ts.map +0 -1
  197. package/dist/worker-client.js +0 -1735
  198. package/dist/worker-client.js.map +0 -1
  199. package/dist/worker-entry.d.ts.map +0 -1
  200. package/dist/worker-entry.js.map +0 -1
  201. package/dist/worker-protocol.d.ts.map +0 -1
  202. package/dist/worker-protocol.js.map +0 -1
  203. package/dist/worker-realtime.d.ts +0 -39
  204. package/dist/worker-realtime.d.ts.map +0 -1
  205. package/dist/worker-realtime.js +0 -677
  206. package/dist/worker-realtime.js.map +0 -1
  207. package/src/auth-leases.ts +0 -251
  208. package/src/blob-limits.ts +0 -98
  209. package/src/bridge-client.ts +0 -512
  210. package/src/client-config.ts +0 -29
  211. package/src/command-history.ts +0 -623
  212. package/src/console-diagnostics.ts +0 -617
  213. package/src/crdt-yjs/index.ts +0 -3
  214. package/src/crdt-yjs/webview-host-facade.ts +0 -477
  215. package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
  216. package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
  217. package/src/diagnostics.ts +0 -116
  218. package/src/generated-bridge.ts +0 -741
  219. package/src/mutations.ts +0 -168
  220. package/src/network.ts +0 -32
  221. package/src/react/index.ts +0 -1036
  222. package/src/react-native/index.ts +0 -152
  223. package/src/runtime-contract.ts +0 -48
  224. package/src/rust-client.ts +0 -1491
  225. package/src/sentry.ts +0 -215
  226. package/src/sql-safety.ts +0 -61
  227. package/src/tauri/index.ts +0 -211
  228. package/src/types.ts +0 -1397
  229. package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
  230. package/src/wasm-bindings/runtime-contract.ts +0 -158
  231. package/src/wasm-runtime.ts +0 -145
  232. package/src/worker-client.ts +0 -2289
  233. package/src/worker-realtime.ts +0 -843
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Client-side encryption wiring (SPEC.md §5.11).
3
+ *
4
+ * The wire-boundary seam: encrypt configured columns when the outbox encodes
5
+ * a commit for send, decrypt them when a COMMIT/segment applies. The local
6
+ * SQLite mirror stays plaintext; the row codec (§2.4) is untouched — it only
7
+ * ever sees a `bytes` value for an encrypted column. This module is the
8
+ * bridge between the positional `RowValue[]` and the `@syncular/core` §5.11
9
+ * envelope primitives.
10
+ */
11
+ import { type NonceSource, type RowValue } from '@syncular/core';
12
+ import type { CompiledClientTable } from './schema.js';
13
+ /**
14
+ * App-supplied key material and selection. `keyProvider` maps a key-id to its
15
+ * 32-byte key; `keyIdFor` names the key for a given write (default:
16
+ * per-table, `keyId = table`). Keys travel as raw bytes — the app owns key
17
+ * storage, rotation, and distribution (§5.11; the docs give the synced
18
+ * wrapped-keys recipe).
19
+ */
20
+ export interface EncryptionConfig {
21
+ /** `keyId → 32-byte key`, or `undefined` if unknown (decrypt fails loud). */
22
+ readonly keyProvider: (keyId: string) => Uint8Array | undefined;
23
+ /** Choose the key-id for an encrypt. Default: per-table (`table`). */
24
+ readonly keyIdFor?: (table: string, rowId: string) => string;
25
+ /**
26
+ * Nonce source (§5.11). Production omits this (secure RNG). ONLY the crypto
27
+ * golden-vector generator injects a fixed nonce — never a production path.
28
+ */
29
+ readonly nonceSource?: NonceSource;
30
+ }
31
+ /**
32
+ * Encrypt the encrypted columns of a positional row value array in place-safe
33
+ * fashion (returns a new array). Called at the outbox encode-at-send seam
34
+ * (§6.1) BEFORE the row codec serializes: an encrypted column's plaintext
35
+ * value becomes the §5.11 ciphertext-envelope `bytes`. NULLs pass through
36
+ * unencrypted (§5.11).
37
+ */
38
+ export declare function encryptRowValues(config: EncryptionConfig, table: CompiledClientTable, rowId: string, values: readonly RowValue[]): Promise<RowValue[]>;
39
+ /**
40
+ * Decrypt the encrypted columns of a decoded positional row value array
41
+ * (returns a new array). Called at the apply seam (§4.5 COMMIT, §5.6 rows
42
+ * segment) AFTER the row codec decodes: an encrypted column's ciphertext
43
+ * envelope `bytes` becomes its plaintext declared-type value for the local
44
+ * mirror. A wrong/missing key or malformed envelope throws
45
+ * {@link DecryptError} (`client.decrypt_failed`, §5.11).
46
+ */
47
+ export declare function decryptRowValues(config: EncryptionConfig, table: CompiledClientTable, values: readonly RowValue[]): Promise<RowValue[]>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Client-side encryption wiring (SPEC.md §5.11).
3
+ *
4
+ * The wire-boundary seam: encrypt configured columns when the outbox encodes
5
+ * a commit for send, decrypt them when a COMMIT/segment applies. The local
6
+ * SQLite mirror stays plaintext; the row codec (§2.4) is untouched — it only
7
+ * ever sees a `bytes` value for an encrypted column. This module is the
8
+ * bridge between the positional `RowValue[]` and the `@syncular/core` §5.11
9
+ * envelope primitives.
10
+ */
11
+ import { DecryptError, decryptValue, encryptValue, } from '@syncular/core';
12
+ function declaredTypeOf(column) {
13
+ // An encrypted column always carries declaredType (typegen guarantees it;
14
+ // §5.11). Fall back to the wire type defensively.
15
+ return (column.declaredType ?? column.type);
16
+ }
17
+ /**
18
+ * Encrypt the encrypted columns of a positional row value array in place-safe
19
+ * fashion (returns a new array). Called at the outbox encode-at-send seam
20
+ * (§6.1) BEFORE the row codec serializes: an encrypted column's plaintext
21
+ * value becomes the §5.11 ciphertext-envelope `bytes`. NULLs pass through
22
+ * unencrypted (§5.11).
23
+ */
24
+ export async function encryptRowValues(config, table, rowId, values) {
25
+ if (!table.hasEncryptedColumns)
26
+ return values.slice();
27
+ const keyIdFor = config.keyIdFor ?? ((t) => t);
28
+ const out = values.slice();
29
+ for (let i = 0; i < table.columns.length; i++) {
30
+ const column = table.columns[i];
31
+ if (column === undefined || !column.encrypted)
32
+ continue;
33
+ const value = out[i];
34
+ if (value === null || value === undefined)
35
+ continue; // NULL stays NULL
36
+ const keyId = keyIdFor(table.name, rowId);
37
+ const key = config.keyProvider(keyId);
38
+ if (key === undefined) {
39
+ throw new DecryptError(`no encryption key for keyId ${JSON.stringify(keyId)} (table ${table.name})`);
40
+ }
41
+ out[i] = await encryptValue(declaredTypeOf(column), value, keyId, key, config.nonceSource);
42
+ }
43
+ return out;
44
+ }
45
+ /**
46
+ * Decrypt the encrypted columns of a decoded positional row value array
47
+ * (returns a new array). Called at the apply seam (§4.5 COMMIT, §5.6 rows
48
+ * segment) AFTER the row codec decodes: an encrypted column's ciphertext
49
+ * envelope `bytes` becomes its plaintext declared-type value for the local
50
+ * mirror. A wrong/missing key or malformed envelope throws
51
+ * {@link DecryptError} (`client.decrypt_failed`, §5.11).
52
+ */
53
+ export async function decryptRowValues(config, table, values) {
54
+ if (!table.hasEncryptedColumns)
55
+ return values.slice();
56
+ const out = values.slice();
57
+ for (let i = 0; i < table.columns.length; i++) {
58
+ const column = table.columns[i];
59
+ if (column === undefined || !column.encrypted)
60
+ continue;
61
+ const value = out[i];
62
+ if (value === null || value === undefined)
63
+ continue;
64
+ if (!(value instanceof Uint8Array)) {
65
+ throw new DecryptError(`encrypted column ${column.name} decoded to a non-bytes value`);
66
+ }
67
+ out[i] = deserializeGuard(await decryptValue(declaredTypeOf(column), value, config.keyProvider));
68
+ }
69
+ return out;
70
+ }
71
+ // decryptValue already returns a PlainValue; RowValue is a superset, so this
72
+ // is just the type widening (kept explicit for clarity at the seam).
73
+ function deserializeGuard(value) {
74
+ return value;
75
+ }
package/dist/errors.d.ts CHANGED
@@ -1,24 +1,10 @@
1
- import type { SyncularErrorCategory, SyncularErrorCode, SyncularErrorRecommendedAction } from './types';
2
- export interface SyncularErrorEnvelope {
3
- code: SyncularErrorCode;
4
- message: string;
5
- category: SyncularErrorCategory;
6
- retryable: boolean;
7
- recommendedAction: SyncularErrorRecommendedAction;
8
- details?: Record<string, unknown>;
9
- }
10
- export declare class SyncularClientError extends Error {
11
- readonly code: SyncularErrorCode;
12
- readonly category: SyncularErrorCategory;
1
+ /**
2
+ * Client-side errors. Protocol codes come from the SPEC.md §10 catalog;
3
+ * the client never invents wire codes, it surfaces them.
4
+ */
5
+ export declare class ClientSyncError extends Error {
6
+ readonly name = "ClientSyncError";
7
+ readonly code: string;
13
8
  readonly retryable: boolean;
14
- readonly recommendedAction: SyncularErrorRecommendedAction;
15
- readonly details: Record<string, unknown> | undefined;
16
- constructor(envelope: SyncularErrorEnvelope, options?: ErrorOptions);
9
+ constructor(code: string, message: string, retryable?: boolean);
17
10
  }
18
- export declare function toSyncularClientError(error: unknown): Error;
19
- export declare function syncularErrorMessage(error: unknown): string;
20
- export declare function syncularErrorDetails(error: unknown): Record<string, unknown> | undefined;
21
- export declare function classifySyncularError(error: unknown, message?: string, details?: Record<string, unknown> | undefined): Omit<SyncularErrorEnvelope, 'message' | 'details'> | null;
22
- export declare function isSyncularOfflineError(error: unknown): boolean;
23
- export declare function syncularErrorStatus(error: unknown): 401 | 403 | undefined;
24
- //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js CHANGED
@@ -1,211 +1,14 @@
1
- import { SYNCULAR_ERROR_DEFINITIONS } from '@syncular/core';
2
- export class SyncularClientError extends Error {
1
+ /**
2
+ * Client-side errors. Protocol codes come from the SPEC.md §10 catalog;
3
+ * the client never invents wire codes, it surfaces them.
4
+ */
5
+ export class ClientSyncError extends Error {
6
+ name = 'ClientSyncError';
3
7
  code;
4
- category;
5
8
  retryable;
6
- recommendedAction;
7
- details;
8
- constructor(envelope, options) {
9
- super(envelope.message, options);
10
- this.name = 'SyncularClientError';
11
- this.code = envelope.code;
12
- this.category = envelope.category;
13
- this.retryable = envelope.retryable;
14
- this.recommendedAction = envelope.recommendedAction;
15
- this.details = envelope.details;
9
+ constructor(code, message, retryable = false) {
10
+ super(message);
11
+ this.code = code;
12
+ this.retryable = retryable;
16
13
  }
17
14
  }
18
- export function toSyncularClientError(error) {
19
- if (error instanceof SyncularClientError)
20
- return error;
21
- const message = syncularErrorMessage(error);
22
- const details = syncularErrorDetails(error);
23
- const classification = classifySyncularError(error, message, details);
24
- if (!classification) {
25
- return error instanceof Error ? error : new Error(message);
26
- }
27
- return new SyncularClientError({
28
- ...classification,
29
- message,
30
- ...(details ? { details } : {}),
31
- }, error instanceof Error ? { cause: error } : undefined);
32
- }
33
- export function syncularErrorMessage(error) {
34
- return error instanceof Error ? error.message : String(error);
35
- }
36
- export function syncularErrorDetails(error) {
37
- if (error instanceof SyncularClientError)
38
- return error.details;
39
- if (!(error instanceof Error))
40
- return undefined;
41
- const serverError = syncularServerErrorFromMessage(error.message);
42
- const details = {
43
- ...(httpStatusFromMessage(error.message) ?? {}),
44
- ...(serverError
45
- ? {
46
- serverErrorCode: serverError.code,
47
- serverErrorCategory: serverError.category,
48
- serverRetryable: serverError.retryable,
49
- serverRecommendedAction: serverError.recommendedAction,
50
- }
51
- : {}),
52
- ...(serverError?.details ? { serverDetails: serverError.details } : {}),
53
- ...(error.message.includes('full snapshot resync required')
54
- ? { resyncRequired: true }
55
- : {}),
56
- ...(syncularKindFromError(error)
57
- ? { syncularKind: syncularKindFromError(error) }
58
- : {}),
59
- ...(syncularDebugFromError(error)
60
- ? { syncularDebug: syncularDebugFromError(error) }
61
- : {}),
62
- };
63
- return Object.keys(details).length > 0 ? details : undefined;
64
- }
65
- export function classifySyncularError(error, message = syncularErrorMessage(error), details = syncularErrorDetails(error)) {
66
- const serverError = syncularServerErrorFromMessage(message);
67
- if (serverError) {
68
- return {
69
- code: serverError.code,
70
- category: serverError.category,
71
- retryable: serverError.retryable,
72
- recommendedAction: serverError.recommendedAction,
73
- };
74
- }
75
- const status = details &&
76
- 'status' in details &&
77
- (details.status === 401 || details.status === 403)
78
- ? details.status
79
- : httpStatusFromMessage(message)?.status;
80
- if (status === 401) {
81
- return {
82
- code: 'sync.auth_required',
83
- category: 'auth-required',
84
- retryable: true,
85
- recommendedAction: 'refreshAuth',
86
- };
87
- }
88
- if (status === 403) {
89
- return {
90
- code: 'sync.forbidden',
91
- category: 'forbidden',
92
- retryable: false,
93
- recommendedAction: 'checkPermissions',
94
- };
95
- }
96
- const syncularKind = details && typeof details.syncularKind === 'string'
97
- ? details.syncularKind
98
- : syncularKindFromError(error);
99
- const debug = details && typeof details.syncularDebug === 'string'
100
- ? details.syncularDebug
101
- : syncularDebugFromError(error);
102
- const haystack = `${message}\n${debug ?? ''}`;
103
- if (/\boffline\b|network is unreachable/i.test(haystack)) {
104
- return {
105
- code: 'sync.offline',
106
- category: 'offline',
107
- retryable: true,
108
- recommendedAction: 'retryLater',
109
- };
110
- }
111
- if (syncularKind === 'Schema' || /\bschema version\b/i.test(haystack)) {
112
- return {
113
- code: 'sync.schema_mismatch',
114
- category: 'schema-mismatch',
115
- retryable: false,
116
- recommendedAction: 'regenerateClient',
117
- };
118
- }
119
- if (syncularKind === 'Protocol' &&
120
- /(hash mismatch|sha256 mismatch|byte length mismatch|manifest .*mismatch|integrity|chain root|commit root|verified root)/i.test(haystack)) {
121
- return {
122
- code: 'sync.integrity_rejected',
123
- category: 'integrity-rejected',
124
- retryable: false,
125
- recommendedAction: 'forceResync',
126
- };
127
- }
128
- return null;
129
- }
130
- export function isSyncularOfflineError(error) {
131
- if (error instanceof SyncularClientError) {
132
- return error.code === 'sync.offline' || error.category === 'offline';
133
- }
134
- if (error && typeof error === 'object') {
135
- const record = error;
136
- if (record.code === 'sync.offline' || record.category === 'offline') {
137
- return true;
138
- }
139
- }
140
- const classification = classifySyncularError(error);
141
- return (classification?.code === 'sync.offline' ||
142
- classification?.category === 'offline');
143
- }
144
- export function syncularErrorStatus(error) {
145
- const details = error instanceof SyncularClientError ? error.details : undefined;
146
- if (details &&
147
- 'status' in details &&
148
- (details.status === 401 || details.status === 403)) {
149
- return details.status;
150
- }
151
- return httpStatusFromMessage(syncularErrorMessage(error))?.status;
152
- }
153
- function httpStatusFromMessage(message) {
154
- const match = /\bHTTP (401|403)\b/.exec(message);
155
- if (!match)
156
- return undefined;
157
- return { status: match[1] === '401' ? 401 : 403 };
158
- }
159
- function syncularKindFromError(error) {
160
- return error instanceof Error &&
161
- typeof error.syncularKind ===
162
- 'string'
163
- ? error.syncularKind
164
- : undefined;
165
- }
166
- function syncularDebugFromError(error) {
167
- return error instanceof Error &&
168
- typeof error.syncularDebug ===
169
- 'string'
170
- ? error.syncularDebug
171
- : undefined;
172
- }
173
- function syncularServerErrorFromMessage(message) {
174
- const match = /\bHTTP \d{3}\b: (\{.*\})\s*$/s.exec(message);
175
- if (!match)
176
- return null;
177
- let parsed;
178
- try {
179
- parsed = JSON.parse(match[1] ?? '');
180
- }
181
- catch {
182
- return null;
183
- }
184
- if (!parsed || typeof parsed !== 'object')
185
- return null;
186
- const record = parsed;
187
- const rawCode = typeof record.code === 'string'
188
- ? record.code
189
- : typeof record.error === 'string'
190
- ? record.error
191
- : null;
192
- if (!rawCode || !(rawCode in SYNCULAR_ERROR_DEFINITIONS))
193
- return null;
194
- const definition = SYNCULAR_ERROR_DEFINITIONS[rawCode];
195
- return {
196
- code: rawCode,
197
- category: typeof record.category === 'string'
198
- ? record.category
199
- : definition.category,
200
- retryable: typeof record.retryable === 'boolean'
201
- ? record.retryable
202
- : definition.retryable,
203
- recommendedAction: typeof record.recommendedAction === 'string'
204
- ? record.recommendedAction
205
- : definition.recommendedAction,
206
- ...(record.details && typeof record.details === 'object'
207
- ? { details: record.details }
208
- : {}),
209
- };
210
- }
211
- //# sourceMappingURL=errors.js.map
package/dist/http.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Browser transport bindings (§1.1, §5.4/§5.5, §8.1): fetch-based sync
3
+ * transport, segment download with signed-URL preference and direct-serve
4
+ * fallback, and a WebSocket realtime connector. Core tests never use these
5
+ * (the loopback doctrine); B6 exercises them in a real browser.
6
+ */
7
+ import type { BlobTransport } from './blob.js';
8
+ import type { RealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
9
+ export interface HttpTransportOptions {
10
+ readonly headers?: Readonly<Record<string, string>>;
11
+ readonly fetch?: typeof fetch;
12
+ }
13
+ /** POST `<mount>/sync` with SSP2 bodies (§1.1). */
14
+ export declare function httpSyncTransport(syncUrl: string, options?: HttpTransportOptions): SyncTransport;
15
+ /**
16
+ * §5.5 direct endpoint with the `X-Syncular-Scopes` re-authorization
17
+ * header, plus the §5.4 `fetchUrl` capability (advertises accept bit 3).
18
+ * `fetchUrl` sends NO headers at all — the signed URL is the entire
19
+ * grant, and host auth must never leak to CDN/object hosts (§5.4).
20
+ * Resolution (which path a descriptor takes, expiry, no fall-through)
21
+ * lives in the client core, not here.
22
+ */
23
+ export declare function httpSegmentDownloader(segmentsBaseUrl: string, options?: HttpTransportOptions): SegmentDownloader;
24
+ /**
25
+ * §5.9.3/§5.9.5 blob transport: host-authenticated `PUT`/`GET
26
+ * <mount>/blobs/{blobId}`. Both carry normal host auth (the blob id is not
27
+ * a capability — the server re-authorizes downloads against referencing
28
+ * rows). Content-address verification is the client core's job (§5.9.7).
29
+ */
30
+ export declare function httpBlobTransport(blobsBaseUrl: string, options?: HttpTransportOptions): BlobTransport;
31
+ /** WebSocket realtime connector (§8.1): text = control, binary = deltas. */
32
+ export declare function webSocketRealtimeConnector(realtimeUrl: string): RealtimeConnector;
package/dist/http.js ADDED
@@ -0,0 +1,196 @@
1
+ import { SSP2_CONTENT_TYPE } from './content-type.js';
2
+ import { ClientSyncError } from './errors.js';
3
+ async function throwHttpError(response) {
4
+ let code = 'sync.transport_failed';
5
+ let message = `HTTP ${response.status}`;
6
+ let retryable = response.status >= 500 || response.status === 429;
7
+ try {
8
+ const body = (await response.json());
9
+ if (typeof body.code === 'string')
10
+ code = body.code;
11
+ if (typeof body.message === 'string')
12
+ message = body.message;
13
+ if (typeof body.retryable === 'boolean')
14
+ retryable = body.retryable;
15
+ }
16
+ catch {
17
+ // non-JSON error body — keep the HTTP-status defaults
18
+ }
19
+ throw new ClientSyncError(code, message, retryable);
20
+ }
21
+ /** POST `<mount>/sync` with SSP2 bodies (§1.1). */
22
+ export function httpSyncTransport(syncUrl, options) {
23
+ const doFetch = options?.fetch ?? fetch;
24
+ return async (request) => {
25
+ const response = await doFetch(syncUrl, {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': SSP2_CONTENT_TYPE,
29
+ ...options?.headers,
30
+ },
31
+ body: request.slice().buffer,
32
+ });
33
+ if (!response.ok)
34
+ await throwHttpError(response);
35
+ return new Uint8Array(await response.arrayBuffer());
36
+ };
37
+ }
38
+ /**
39
+ * §5.5 direct endpoint with the `X-Syncular-Scopes` re-authorization
40
+ * header, plus the §5.4 `fetchUrl` capability (advertises accept bit 3).
41
+ * `fetchUrl` sends NO headers at all — the signed URL is the entire
42
+ * grant, and host auth must never leak to CDN/object hosts (§5.4).
43
+ * Resolution (which path a descriptor takes, expiry, no fall-through)
44
+ * lives in the client core, not here.
45
+ */
46
+ export function httpSegmentDownloader(segmentsBaseUrl, options) {
47
+ const doFetch = options?.fetch ?? fetch;
48
+ const direct = async (request) => {
49
+ const response = await doFetch(`${segmentsBaseUrl}/${encodeURIComponent(request.segmentId)}`, {
50
+ headers: {
51
+ 'X-Syncular-Scopes': request.requestedScopesJson,
52
+ ...options?.headers,
53
+ },
54
+ });
55
+ if (!response.ok)
56
+ await throwHttpError(response);
57
+ return new Uint8Array(await response.arrayBuffer());
58
+ };
59
+ const fetchUrl = async (url) => {
60
+ // Deliberately headerless: the URL is the bearer grant (§5.4).
61
+ const response = await doFetch(url);
62
+ if (!response.ok) {
63
+ throw new ClientSyncError('sync.transport_failed', `signed-URL fetch failed with HTTP ${response.status} (§5.4: descriptor invalidated, re-pull to recover)`, true);
64
+ }
65
+ return new Uint8Array(await response.arrayBuffer());
66
+ };
67
+ return Object.assign(direct, { fetchUrl });
68
+ }
69
+ /**
70
+ * §5.9.3/§5.9.5 blob transport: host-authenticated `PUT`/`GET
71
+ * <mount>/blobs/{blobId}`. Both carry normal host auth (the blob id is not
72
+ * a capability — the server re-authorizes downloads against referencing
73
+ * rows). Content-address verification is the client core's job (§5.9.7).
74
+ */
75
+ export function httpBlobTransport(blobsBaseUrl, options) {
76
+ const doFetch = options?.fetch ?? fetch;
77
+ const blobUrl = (blobId) => `${blobsBaseUrl}/${encodeURIComponent(blobId)}`;
78
+ return {
79
+ upload: async (blobId, bytes, mediaType) => {
80
+ const response = await doFetch(blobUrl(blobId), {
81
+ method: 'PUT',
82
+ headers: {
83
+ 'Content-Type': mediaType ?? 'application/octet-stream',
84
+ ...options?.headers,
85
+ },
86
+ body: bytes.slice().buffer,
87
+ });
88
+ if (!response.ok)
89
+ await throwHttpError(response);
90
+ },
91
+ download: async (blobId) => {
92
+ const response = await doFetch(blobUrl(blobId), {
93
+ headers: { ...options?.headers },
94
+ });
95
+ if (!response.ok)
96
+ await throwHttpError(response);
97
+ // §5.9.5 always-issue: a JSON body with `url` means presigned delivery;
98
+ // an octet-stream body is inline bytes.
99
+ const contentType = response.headers.get('content-type') ?? '';
100
+ if (contentType.includes('application/json')) {
101
+ const body = (await response.json());
102
+ if (typeof body.url === 'string') {
103
+ return {
104
+ kind: 'url',
105
+ url: body.url,
106
+ ...(typeof body.urlExpiresAtMs === 'number'
107
+ ? { urlExpiresAtMs: body.urlExpiresAtMs }
108
+ : {}),
109
+ };
110
+ }
111
+ }
112
+ return {
113
+ kind: 'bytes',
114
+ bytes: new Uint8Array(await response.arrayBuffer()),
115
+ };
116
+ },
117
+ // §5.9.5: bare GET of the signed URL — no host auth (the URL is the grant).
118
+ fetchUrl: async (url) => {
119
+ const response = await doFetch(url);
120
+ if (!response.ok) {
121
+ throw new ClientSyncError('sync.transport_failed', `blob signed-URL fetch failed with HTTP ${response.status} (§5.9.5: re-request to recover)`, true);
122
+ }
123
+ return new Uint8Array(await response.arrayBuffer());
124
+ },
125
+ // §5.9.3: presigned-upload grant.
126
+ uploadGrant: async (blobId, byteLength, mediaType) => {
127
+ const response = await doFetch(`${blobUrl(blobId)}/upload-grant`, {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ ...options?.headers,
132
+ },
133
+ body: JSON.stringify({
134
+ byteLength,
135
+ ...(mediaType !== undefined ? { mediaType } : {}),
136
+ }),
137
+ });
138
+ if (!response.ok)
139
+ await throwHttpError(response);
140
+ const body = (await response.json());
141
+ if (typeof body.url === 'string') {
142
+ return {
143
+ kind: 'url',
144
+ url: body.url,
145
+ ...(typeof body.urlExpiresAtMs === 'number'
146
+ ? { urlExpiresAtMs: body.urlExpiresAtMs }
147
+ : {}),
148
+ };
149
+ }
150
+ if (body.present === true)
151
+ return { kind: 'present' };
152
+ return { kind: 'none' };
153
+ },
154
+ // §5.9.3: direct-to-storage PUT — no host auth (the URL is the grant).
155
+ uploadToUrl: async (url, bytes, mediaType) => {
156
+ const response = await doFetch(url, {
157
+ method: 'PUT',
158
+ headers: {
159
+ 'Content-Type': mediaType ?? 'application/octet-stream',
160
+ },
161
+ body: bytes.slice().buffer,
162
+ });
163
+ if (!response.ok) {
164
+ throw new ClientSyncError('sync.transport_failed', `blob presigned PUT failed with HTTP ${response.status} (§5.9.3: re-request a grant or stream direct)`, true);
165
+ }
166
+ },
167
+ };
168
+ }
169
+ /** WebSocket realtime connector (§8.1): text = control, binary = deltas. */
170
+ export function webSocketRealtimeConnector(realtimeUrl) {
171
+ return (handlers) => new Promise((resolve, reject) => {
172
+ const socket = new WebSocket(realtimeUrl);
173
+ socket.binaryType = 'arraybuffer';
174
+ socket.onopen = () => {
175
+ resolve({
176
+ send: (text) => socket.send(text),
177
+ sendBytes: (bytes) => {
178
+ socket.send(bytes.slice().buffer);
179
+ },
180
+ close: () => socket.close(),
181
+ });
182
+ };
183
+ socket.onmessage = (event) => {
184
+ if (typeof event.data === 'string')
185
+ handlers.onText(event.data);
186
+ else
187
+ handlers.onBinary(new Uint8Array(event.data));
188
+ };
189
+ socket.onerror = () => {
190
+ reject(new ClientSyncError('sync.transport_failed', 'realtime socket failed to connect', true));
191
+ };
192
+ socket.onclose = () => {
193
+ handlers.onClose?.();
194
+ };
195
+ });
196
+ }
package/dist/index.d.ts CHANGED
@@ -1,16 +1,28 @@
1
- export * from './blob-limits';
2
- export * from './bridge-client';
3
- export * from './client';
4
- export * from './client-config';
5
- export * from './command-history';
6
- export * from './console-diagnostics';
7
- export * from './database';
8
- export * from './errors';
9
- export * from './mutations';
10
- export * from './network';
11
- export * from './runtime-contract';
12
- export * from './types';
13
- export type { SyncularWasmArtifactVariant } from './wasm-runtime';
14
- export { getSyncularPackagedRuntimeArtifacts, getSyncularRuntimeArtifact, getSyncularRuntimeArtifactCatalogUrl, getSyncularWasmGlueUrl, getSyncularWasmUrl, resolveSyncularRuntimeArtifactCatalog, selectSyncularRuntimeArtifact, } from './wasm-runtime';
15
- export * from './worker-client';
16
- //# sourceMappingURL=index.d.ts.map
1
+ /**
2
+ * @syncular/client — the B3 TypeScript client protocol core
3
+ * (SPEC.md is normative; REVISE.md B3 is the architectural mandate).
4
+ *
5
+ * Browser-safe root: database backends live behind subpath exports
6
+ * (`./bun` for bun:sqlite tests, `./wasm` for sqlite-wasm + OPFS); the
7
+ * worker-side bootstrap lives behind `./worker`. The main-thread handle
8
+ * (`worker-host`) and the RPC protocol types are root exports — they
9
+ * import no SQLite.
10
+ */
11
+ export * from './apply.js';
12
+ export * from './blob.js';
13
+ export * from './client.js';
14
+ export * from './content-type.js';
15
+ export * from './database.js';
16
+ export * from './encryption.js';
17
+ export * from './errors.js';
18
+ export * from './http.js';
19
+ export * from './invalidation.js';
20
+ export * from './leader-lock.js';
21
+ export * from './multi-tab.js';
22
+ export * from './outbox.js';
23
+ export * from './schema.js';
24
+ export * from './state.js';
25
+ export * from './transport.js';
26
+ export * from './window.js';
27
+ export * from './worker-host.js';
28
+ export * from './worker-protocol.js';