@lix-js/sdk 0.16.1 → 0.17.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 (71) hide show
  1. package/README.md +162 -44
  2. package/dist/binding-types.d.ts +15 -5
  3. package/dist/binding.browser.d.ts +2 -0
  4. package/dist/binding.browser.js +14 -4
  5. package/dist/binding.node-wasm.d.ts +1 -1
  6. package/dist/binding.node-wasm.js +5 -3
  7. package/dist/binding.node.d.ts +2 -0
  8. package/dist/binding.node.js +34 -8
  9. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  10. package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
  11. package/dist/compatibility.d.ts +6 -0
  12. package/dist/compatibility.js +6 -0
  13. package/dist/component-host/dispatch.d.ts +12 -0
  14. package/dist/component-host/dispatch.js +364 -0
  15. package/dist/component-host/index.d.ts +15 -0
  16. package/dist/component-host/index.js +84 -0
  17. package/dist/component-host/instrument.d.ts +6 -0
  18. package/dist/component-host/instrument.js +260 -0
  19. package/dist/conversion-provider.d.ts +4 -0
  20. package/dist/conversion-provider.js +20 -0
  21. package/dist/hosted-lix.js +1 -1
  22. package/dist/http-transport.d.ts +25 -0
  23. package/dist/http-transport.js +162 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +1 -0
  26. package/dist/lix.d.ts +14 -9
  27. package/dist/lix.js +44 -9
  28. package/dist/migration-binding.browser.d.ts +33 -0
  29. package/dist/migration-binding.browser.js +46 -0
  30. package/dist/migration-binding.node.d.ts +6 -0
  31. package/dist/migration-binding.node.js +5 -0
  32. package/dist/migration-wasm/lix_js_sdk.d.ts +271 -0
  33. package/dist/migration-wasm/lix_js_sdk.js +1852 -0
  34. package/dist/migration-wasm/lix_js_sdk_bg.wasm +4 -0
  35. package/dist/migration-wasm/lix_js_sdk_bg.wasm.d.ts +101 -0
  36. package/dist/migration.d.ts +23 -0
  37. package/dist/migration.js +55 -0
  38. package/dist/open-lix.js +29 -18
  39. package/dist/open-progress.d.ts +10 -0
  40. package/dist/open-progress.js +59 -0
  41. package/dist/remote/client.d.ts +2 -1
  42. package/dist/remote/client.js +11 -1
  43. package/dist/result.d.ts +4 -3
  44. package/dist/result.js +3 -4
  45. package/dist/storage-adapter.d.ts +7 -1
  46. package/dist/storage-ownership.d.ts +5 -0
  47. package/dist/storage-ownership.js +4 -0
  48. package/dist/types.d.ts +65 -31
  49. package/dist/wasm/lix_js_sdk.d.ts +45 -16
  50. package/dist/wasm/lix_js_sdk.js +214 -60
  51. package/dist/wasm/lix_js_sdk_bg.wasm +2 -2
  52. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +17 -9
  53. package/dist/worker/client.d.ts +12 -3
  54. package/dist/worker/client.js +95 -81
  55. package/dist/worker/durable-local-admission.d.ts +20 -0
  56. package/dist/worker/durable-local-admission.js +87 -0
  57. package/dist/worker/entry.shared.browser.d.ts +1 -0
  58. package/dist/worker/entry.shared.browser.js +211 -0
  59. package/dist/worker/factory.browser.d.ts +2 -0
  60. package/dist/worker/factory.browser.js +65 -1
  61. package/dist/worker/factory.node.d.ts +1 -0
  62. package/dist/worker/factory.node.js +3 -0
  63. package/dist/worker/host.d.ts +4 -2
  64. package/dist/worker/host.js +86 -32
  65. package/dist/worker/protocol.d.ts +25 -8
  66. package/dist/worker/protocol.js +25 -7
  67. package/dist/worker/shared-admission.d.ts +26 -0
  68. package/dist/worker/shared-admission.js +116 -0
  69. package/dist/worker/shared-engine.d.ts +34 -0
  70. package/dist/worker/shared-engine.js +194 -0
  71. package/package.json +21 -9
@@ -1,4 +1,5 @@
1
1
  /// <reference lib="webworker" />
2
+ import { deserializeWorkerError } from "./protocol.js";
2
3
  // Browser/Wasm execution stays off the main thread.
3
4
  export const openDirectLixBinding = undefined;
4
5
  export function createWorkerConnection() {
@@ -14,7 +15,7 @@ export function createWorkerConnection() {
14
15
  worker.onmessage = (event) => listener(event.data);
15
16
  },
16
17
  onFatal(listener) {
17
- worker.onerror = (event) => listener(event.error ?? new Error(event.message ?? "Lix worker failed"));
18
+ worker.onerror = (event) => listener(workerFailure(event));
18
19
  },
19
20
  ref() { },
20
21
  unref() { },
@@ -23,3 +24,66 @@ export function createWorkerConnection() {
23
24
  },
24
25
  };
25
26
  }
27
+ /** Package-private provider identity selects one engine, not one engine per tab. */
28
+ export function createSharedWorkerConnection(key) {
29
+ if (typeof SharedWorker === "undefined" || !navigator.locks)
30
+ throw new Error("Shared partial replicas require SharedWorker and Web Locks");
31
+ const worker = new SharedWorker(new URL("./entry.shared.browser.js", import.meta.url), { type: "module", name: key });
32
+ const port = worker.port;
33
+ const leaseName = `lix:shared-client:${crypto.randomUUID()}`;
34
+ let release;
35
+ const lifetime = new Promise(resolve => { release = resolve; });
36
+ let ready;
37
+ const acquired = new Promise(resolve => { ready = resolve; });
38
+ let failure;
39
+ void navigator.locks.request(leaseName, async () => { ready(); await lifetime; }).catch(error => failure?.(error));
40
+ void acquired.then(() => port.postMessage({ kind: "shared.clientLease", name: leaseName }));
41
+ port.start();
42
+ let closed = false;
43
+ let termination;
44
+ let disconnected;
45
+ const detached = new Promise((resolve, reject) => { disconnected = error => error ? reject(error) : resolve(); });
46
+ return {
47
+ postMessage(message) { if (closed)
48
+ throw new Error("Shared engine connection closed"); port.postMessage(message); },
49
+ onMessage(listener) {
50
+ port.onmessage = event => {
51
+ if (event.data?.kind === "shared.disconnected") {
52
+ const error = event.data.error;
53
+ disconnected(error ? deserializeWorkerError(error) : undefined);
54
+ }
55
+ else
56
+ listener(event.data);
57
+ };
58
+ },
59
+ onFatal(listener) { failure = listener; worker.onerror = event => listener(workerFailure(event)); },
60
+ ref() { }, unref() { },
61
+ terminate() {
62
+ if (termination)
63
+ return termination;
64
+ termination = (async () => {
65
+ closed = true;
66
+ port.postMessage({ kind: "shared.disconnect" });
67
+ release();
68
+ let timeout;
69
+ try {
70
+ await Promise.race([detached, new Promise((_, reject) => {
71
+ timeout = setTimeout(() => reject(Object.assign(new Error("Shared engine close was not acknowledged"), { code: "LIX_SHARED_ENGINE_CLOSE_UNCONFIRMED" })), 10000);
72
+ })]);
73
+ }
74
+ finally {
75
+ if (timeout !== undefined)
76
+ clearTimeout(timeout);
77
+ port.close();
78
+ }
79
+ })();
80
+ return termination;
81
+ },
82
+ };
83
+ }
84
+ function workerFailure(event) {
85
+ if (event.error instanceof Error)
86
+ return event.error;
87
+ const location = event.filename ? ` (${event.filename}:${event.lineno ?? 0}:${event.colno ?? 0})` : "";
88
+ return Object.assign(new Error(`${event.message || "Lix worker failed to load or execute"}${location}`), { code: "LIX_WORKER_FAILED" });
89
+ }
@@ -3,3 +3,4 @@ import type { WorkerConnection } from "./protocol.js";
3
3
  export declare function createWorkerConnection(): WorkerConnection;
4
4
  export declare function workerExecArgv(execArgv: readonly string[]): string[];
5
5
  export declare const openDirectLixBinding: (storage: LixStorageConfig, telemetry?: TelemetryDispatch, telemetryParent?: TelemetryParentContext, server?: SyncServerBindingOptions, openProgress?: OpenProgressDispatch, snapshot?: ReadableStream<Uint8Array>) => Promise<LixBinding | undefined>;
6
+ export declare function createSharedWorkerConnection(_key: string): WorkerConnection | undefined;
@@ -55,3 +55,6 @@ export function workerExecArgv(execArgv) {
55
55
  export const openDirectLixBinding = async (storage, telemetry, telemetryParent, server, openProgress, snapshot) => {
56
56
  return openLixBinding(storage, telemetry, telemetryParent, server, openProgress, snapshot);
57
57
  };
58
+ export function createSharedWorkerConnection(_key) {
59
+ return undefined;
60
+ }
@@ -1,4 +1,6 @@
1
- import { openLixBinding } from "#binding";
1
+ import { openLixBinding, convertReplicaBinding } from "#binding";
2
2
  import { type WorkerHostEndpoint, type WorkerSyncFetchResponse } from "./protocol.js";
3
- export declare function startWorkerHost(endpoint: WorkerHostEndpoint, openBinding?: typeof openLixBinding): void;
3
+ export declare function startWorkerHost(endpoint: WorkerHostEndpoint, openBinding?: typeof openLixBinding, convertBinding?: typeof convertReplicaBinding): {
4
+ close(): Promise<void>;
5
+ };
4
6
  export declare function responseFromSyncFetch(resolved: WorkerSyncFetchResponse): Response;
@@ -1,6 +1,8 @@
1
- import { openLixBinding, createHostedBinding, deleteHostedBinding, } from "#binding";
1
+ import { validateHttpRequest } from "../http-transport.js";
2
+ import { openLixBinding, convertReplicaBinding, retryReplicaMigrationCleanupBinding, createHostedBinding, deleteHostedBinding, } from "#binding";
2
3
  import { deserializeWorkerError, serializeWorkerError, } from "./protocol.js";
3
- export function startWorkerHost(endpoint, openBinding = openLixBinding) {
4
+ export function startWorkerHost(endpoint, openBinding = openLixBinding, convertBinding = convertReplicaBinding) {
5
+ let closed = false;
4
6
  const sessions = new Map();
5
7
  let nextSessionId = 1;
6
8
  let nextTransactionId = 1;
@@ -16,7 +18,10 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
16
18
  const pendingSyncStreamPulls = new Map();
17
19
  const syncStreamCleanup = new Map();
18
20
  let finiteQueue = Promise.resolve();
21
+ const registrations = new Set();
19
22
  endpoint.onMessage((message) => {
23
+ if (closed && "id" in message)
24
+ return;
20
25
  if (!("id" in message)) {
21
26
  handleNotification(message);
22
27
  return;
@@ -54,15 +59,21 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
54
59
  // finite-operation queue lets a long-running operation block a newly
55
60
  // mounted query, including one that needs lazy history hydration.
56
61
  // The live `next()` lane is already independent for the same reason.
57
- void respond(message, () => handleObserveRegistration(message.sessionId, operation.sql, operation.params));
62
+ const registration = respond(message, () => handleObserveRegistration(message.sessionId, operation.sql, operation.params));
63
+ registrations.add(registration);
64
+ void registration.finally(() => registrations.delete(registration));
58
65
  return;
59
66
  }
60
67
  finiteQueue = finiteQueue.then(async () => {
61
68
  try {
62
69
  await respond(message, async () => {
70
+ if (closed)
71
+ throw workerStateError("Worker client disconnected");
63
72
  if (message.operation.kind !== "open" &&
64
73
  message.operation.kind !== "hosted.create" &&
65
- message.operation.kind !== "hosted.delete") {
74
+ message.operation.kind !== "hosted.delete" &&
75
+ message.operation.kind !== "replica.convert" &&
76
+ message.operation.kind !== "replica.cleanup") {
66
77
  requiredLix(message.sessionId).setTelemetryParent(message.telemetryParent);
67
78
  }
68
79
  return handleFiniteOperation(message.sessionId, message.operation, message.telemetryParent);
@@ -165,6 +176,14 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
165
176
  }
166
177
  async function handleFiniteOperation(sessionId, operation, telemetryParent) {
167
178
  switch (operation.kind) {
179
+ case "replica.cleanup":
180
+ if (sessions.size > 0)
181
+ throw workerStateError("Migration cleanup requires closed storage");
182
+ return retryReplicaMigrationCleanupBinding(operation.storage, createSyncServerBridge(operation.server));
183
+ case "replica.convert":
184
+ if (sessions.size > 0)
185
+ throw workerStateError("Conversion requires closed storage");
186
+ return convertBinding(operation.storage, createSyncServerBridge(operation.server), operation.branchId);
168
187
  case "hosted.create":
169
188
  return createHostedBinding(operation.server);
170
189
  case "hosted.delete":
@@ -221,8 +240,7 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
221
240
  case "transaction.commit": {
222
241
  const transaction = requiredTransaction(operation.transactionId);
223
242
  transactions.delete(operation.transactionId);
224
- await transaction.commit();
225
- return undefined;
243
+ return await transaction.commit();
226
244
  }
227
245
  case "transaction.rollback": {
228
246
  const transaction = requiredTransaction(operation.transactionId);
@@ -236,6 +254,10 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
236
254
  return requiredLix(sessionId).exportReplicaRecovery(operation.id);
237
255
  case "recoverReplica":
238
256
  return requiredLix(sessionId).recoverReplica(operation.id);
257
+ case "recoverReplicaWithServer":
258
+ return requiredLix(sessionId).recoverReplicaWithServer(operation.id, createSyncServerBridge(operation.server, operation.transportScope));
259
+ case "syncHealth":
260
+ return requiredLix(sessionId).syncHealth();
239
261
  case "activeBranchId":
240
262
  return requiredLix(sessionId).activeBranchId();
241
263
  case "activeAccountId":
@@ -328,7 +350,46 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
328
350
  }
329
351
  await input.writer.close();
330
352
  }
331
- function createSyncServerBridge(server) {
353
+ return { async close() {
354
+ if (closed)
355
+ return;
356
+ closed = true;
357
+ const failure = workerStateError("Worker client disconnected");
358
+ for (const pending of pendingSyncHeaders.values())
359
+ pending.reject(failure);
360
+ pendingSyncHeaders.clear();
361
+ for (const pending of pendingSyncFetch.values())
362
+ pending.reject(failure);
363
+ pendingSyncFetch.clear();
364
+ for (const cleanup of syncStreamCleanup.values())
365
+ cleanup();
366
+ syncStreamCleanup.clear();
367
+ for (const pending of pendingSyncStreamPulls.values()) {
368
+ pending.controller.error(failure);
369
+ pending.reject(failure);
370
+ }
371
+ pendingSyncStreamPulls.clear();
372
+ for (const observation of observations.values())
373
+ observation.close();
374
+ observations.clear();
375
+ for (const snapshot of snapshotExports.values())
376
+ await Promise.resolve(snapshot.cancel()).catch(() => undefined);
377
+ snapshotExports.clear();
378
+ await finiteQueue.catch(() => undefined);
379
+ await Promise.allSettled(registrations);
380
+ // The active finite operation has finished; never roll back a handle
381
+ // concurrently with its execute/commit operation.
382
+ for (const transaction of transactions.values())
383
+ await transaction.rollback().catch(() => undefined);
384
+ transactions.clear();
385
+ for (const snapshot of snapshotExports.values())
386
+ await Promise.resolve(snapshot.cancel()).catch(() => undefined);
387
+ snapshotExports.clear();
388
+ for (const session of sessions.values())
389
+ await session.close();
390
+ sessions.clear();
391
+ } };
392
+ function createSyncServerBridge(server, transportScope) {
332
393
  if (!server)
333
394
  return undefined;
334
395
  return {
@@ -336,48 +397,37 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
336
397
  headers: server.headers ?? [],
337
398
  headerProvider: server.dynamicHeaders
338
399
  ? () => {
400
+ if (closed)
401
+ throw workerStateError("Worker client disconnected");
339
402
  const requestId = nextSyncRequestId++;
340
403
  return new Promise((resolve, reject) => {
341
404
  pendingSyncHeaders.set(requestId, { resolve, reject });
342
- endpoint.postMessage({ kind: "sync.headers", requestId });
405
+ endpoint.postMessage({ kind: "sync.headers", requestId, transportScope });
343
406
  });
344
407
  }
345
408
  : undefined,
346
- fetch: server.customFetch ? bridgeFetch : undefined,
409
+ transport: (request) => bridgeFetch(request, transportScope),
347
410
  };
348
411
  }
349
- async function bridgeFetch(input, init) {
350
- const extension = init;
351
- const streaming = extension?.lixResponseStream === true;
352
- const responseLimit = extension?.lixResponseLimit;
353
- if (!streaming &&
354
- (typeof responseLimit !== "number" ||
355
- !Number.isSafeInteger(responseLimit) ||
356
- responseLimit <= 0)) {
357
- throw new TypeError("Browser sync fetch has no valid response limit");
358
- }
412
+ async function bridgeFetch(httpRequest, transportScope) {
413
+ validateHttpRequest(httpRequest);
414
+ const { url: input, init, response: policy } = httpRequest;
415
+ const streaming = policy.mode === "streaming";
416
+ if (closed)
417
+ throw workerStateError("Worker client disconnected");
359
418
  const requestId = nextSyncRequestId++;
360
419
  const requestBase = {
361
- url: typeof input === "string"
362
- ? input
363
- : input instanceof URL
364
- ? input.toString()
365
- : input.url,
420
+ url: input,
366
421
  method: init?.method ?? "GET",
367
422
  headers: headerEntries(init?.headers),
368
423
  body: serializableBody(init?.body),
369
424
  credentials: init?.credentials,
425
+ cache: init?.cache, redirect: init?.redirect,
370
426
  };
371
- const request = streaming
372
- ? { ...requestBase, responseMode: "stream" }
373
- : {
374
- ...requestBase,
375
- responseMode: "buffered",
376
- responseLimit: responseLimit,
377
- };
427
+ const request = { ...requestBase, response: policy };
378
428
  const response = new Promise((resolve, reject) => {
379
429
  pendingSyncFetch.set(requestId, { resolve, reject });
380
- endpoint.postMessage({ kind: "sync.fetch", requestId, request });
430
+ endpoint.postMessage({ kind: "sync.fetch", requestId, request, transportScope });
381
431
  });
382
432
  const abort = () => {
383
433
  const pending = pendingSyncFetch.get(requestId);
@@ -469,6 +519,10 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
469
519
  // serialized finite lane. Each `observe.next` supplies telemetry directly
470
520
  // to its observation binding.
471
521
  const events = await requiredLix(sessionId).observe(sql, params);
522
+ if (closed) {
523
+ events.close();
524
+ throw workerStateError("Worker client disconnected");
525
+ }
472
526
  const observeId = nextObserveId++;
473
527
  observations.set(observeId, events);
474
528
  return observeId;
@@ -1,10 +1,10 @@
1
+ import type { HttpResponsePolicy } from "../http-transport.js";
1
2
  import type { BindingBatchStatement, BindingParam, LixStorageConfig } from "../binding-types.js";
2
3
  import type { CreateBranchOptions, ExecuteOptions, LixBatchOptions, MergeBranchOptions, SwitchBranchOptions, LixTelemetrySpan, LixTelemetryParentContext, LixOpenProgress, OpenAnotherSessionOptions } from "../types.js";
3
4
  export type WorkerSyncServerOptions = {
4
5
  url: string;
5
6
  headers?: [string, string][];
6
7
  dynamicHeaders: boolean;
7
- customFetch: boolean;
8
8
  };
9
9
  export type WorkerSyncFetchRequest = {
10
10
  url: string;
@@ -12,12 +12,10 @@ export type WorkerSyncFetchRequest = {
12
12
  headers: [string, string][];
13
13
  body?: string | Uint8Array;
14
14
  credentials?: RequestCredentials;
15
- } & ({
16
- responseMode: "buffered";
17
- responseLimit: number;
18
- } | {
19
- responseMode: "stream";
20
- });
15
+ cache?: RequestCache;
16
+ redirect?: RequestRedirect;
17
+ response: HttpResponsePolicy;
18
+ };
21
19
  type WorkerSyncFetchResponseHead = {
22
20
  status: number;
23
21
  statusText: string;
@@ -36,6 +34,15 @@ export type WorkerRequest = {
36
34
  operation: WorkerOperation;
37
35
  };
38
36
  export type WorkerOperation = {
37
+ kind: "replica.cleanup";
38
+ storage: LixStorageConfig;
39
+ server: WorkerSyncServerOptions;
40
+ } | {
41
+ kind: "replica.convert";
42
+ storage: LixStorageConfig;
43
+ server: WorkerSyncServerOptions;
44
+ branchId?: string;
45
+ } | {
39
46
  kind: "hosted.create";
40
47
  server: import("../binding-types.js").HostedServerBindingOptions;
41
48
  } | {
@@ -92,6 +99,13 @@ export type WorkerOperation = {
92
99
  } | {
93
100
  kind: "recoverReplica";
94
101
  id: string;
102
+ } | {
103
+ kind: "recoverReplicaWithServer";
104
+ id: string;
105
+ server: WorkerSyncServerOptions;
106
+ transportScope: number;
107
+ } | {
108
+ kind: "syncHealth";
95
109
  } | {
96
110
  kind: "activeBranchId";
97
111
  } | {
@@ -193,6 +207,7 @@ export type WorkerHostEndpoint = {
193
207
  onMessage(listener: (message: WorkerInput) => void): void;
194
208
  };
195
209
  export type SerializedWorkerError = {
210
+ cause?: SerializedWorkerError;
196
211
  name: string;
197
212
  message: string;
198
213
  stack?: string;
@@ -217,10 +232,12 @@ export type WorkerResponse = {
217
232
  } | {
218
233
  kind: "sync.headers";
219
234
  requestId: number;
235
+ transportScope?: number;
220
236
  } | {
221
237
  kind: "sync.fetch";
222
238
  requestId: number;
223
239
  request: WorkerSyncFetchRequest;
240
+ transportScope?: number;
224
241
  } | {
225
242
  kind: "sync.fetch.stream.pull";
226
243
  requestId: number;
@@ -228,6 +245,6 @@ export type WorkerResponse = {
228
245
  kind: "sync.fetch.cancel";
229
246
  requestId: number;
230
247
  };
231
- export declare function serializeWorkerError(error: unknown): SerializedWorkerError;
248
+ export declare function serializeWorkerError(error: unknown, depth?: number): SerializedWorkerError;
232
249
  export declare function deserializeWorkerError(error: SerializedWorkerError): Error;
233
250
  export {};
@@ -1,19 +1,20 @@
1
- export function serializeWorkerError(error) {
1
+ export function serializeWorkerError(error, depth = 0) {
2
2
  if (!(error instanceof Error)) {
3
- return { name: "Error", message: String(error) };
3
+ return { name: "Error", message: "Non-error failure" };
4
4
  }
5
5
  const lixError = error;
6
6
  return {
7
7
  name: error.name,
8
- message: error.message,
9
- stack: error.stack,
8
+ message: redactDiagnostic(error.message),
9
+ stack: error.stack ? redactDiagnostic(error.stack) : undefined,
10
10
  code: typeof lixError.code === "string" ? lixError.code : undefined,
11
- hint: typeof lixError.hint === "string" ? lixError.hint : undefined,
12
- details: lixError.details,
11
+ hint: typeof lixError.hint === "string" ? redactDiagnostic(lixError.hint) : undefined,
12
+ details: redactDetails(lixError.details),
13
+ cause: depth < 3 && error.cause !== undefined ? serializeWorkerError(error.cause, depth + 1) : undefined,
13
14
  };
14
15
  }
15
16
  export function deserializeWorkerError(error) {
16
- const restored = new Error(error.message);
17
+ const restored = new Error(error.message, error.cause ? { cause: deserializeWorkerError(error.cause) } : undefined);
17
18
  restored.name = error.name;
18
19
  restored.stack = error.stack;
19
20
  restored.code = error.code;
@@ -21,3 +22,20 @@ export function deserializeWorkerError(error) {
21
22
  restored.details = error.details;
22
23
  return restored;
23
24
  }
25
+ function redactDiagnostic(value) {
26
+ return value.slice(0, 4096).replace(/Bearer\s+[^\s,;"']+/gi, "Bearer [redacted]")
27
+ .replace(/((?:authorization|cookie|token|password|secret)\s*[:=]\s*)[^\n]+/gi, "$1[redacted]");
28
+ }
29
+ function redactDetails(value, depth = 0) {
30
+ if (depth > 3)
31
+ return "[truncated]";
32
+ if (typeof value === "string")
33
+ return redactDiagnostic(value);
34
+ if (value === null || typeof value === "number" || typeof value === "boolean" || value === undefined)
35
+ return value;
36
+ if (Array.isArray(value))
37
+ return value.slice(0, 32).map(item => redactDetails(item, depth + 1));
38
+ if (typeof value === "object")
39
+ return Object.fromEntries(Object.entries(value).slice(0, 32).map(([key, item]) => [key, /authorization|cookie|token|password|secret|headers/i.test(key) ? "[redacted]" : redactDetails(item, depth + 1)]));
40
+ return "[unsupported]";
41
+ }
@@ -0,0 +1,26 @@
1
+ import { type HttpTransport } from "../http-transport.js";
2
+ export declare const ADMISSION_PROTOCOL_EPOCH = 17;
3
+ export declare const ADMISSION_STORAGE_EPOCH = 81;
4
+ export type AdmissionIdentity = {
5
+ repositoryId: string;
6
+ principalId: string;
7
+ protocolEpoch: number;
8
+ storageEpoch: number;
9
+ };
10
+ export declare function sharedCredentialKey(url: string, headers: [string, string][]): string;
11
+ export declare function sameAdmission(a: AdmissionIdentity, b: AdmissionIdentity): boolean;
12
+ /** Authorize attachment, awaiting any server-owned repository upgrade. */
13
+ export declare function requestAdmission(url: string, credentials: [string, string][], transport: HttpTransport): Promise<AdmissionIdentity>;
14
+ /** Memory-only proofs grant cached local attachment, never remote authorization. */
15
+ export declare class SharedAdmissionCache {
16
+ private readonly revocations;
17
+ private readonly proofs;
18
+ record(url: string, headers: [string, string][], identity: AdmissionIdentity): void;
19
+ generation(url: string, headers: [string, string][]): number;
20
+ remove(url: string, headers: [string, string][]): void;
21
+ persistLocal(url: string, headers: [string, string][], generation: number, write: () => Promise<void>, remove: () => Promise<void>): Promise<void>;
22
+ verify(url: string, headers: [string, string][], expected: AdmissionIdentity | undefined, probe: () => Promise<AdmissionIdentity>, allowOffline?: boolean): Promise<{
23
+ identity: AdmissionIdentity;
24
+ online: boolean;
25
+ }>;
26
+ }
@@ -0,0 +1,116 @@
1
+ import { HttpTransportError } from "../http-transport.js";
2
+ export const ADMISSION_PROTOCOL_EPOCH = 17;
3
+ export const ADMISSION_STORAGE_EPOCH = 81;
4
+ const ANONYMOUS_ACCOUNT_ID = "00000000-0000-7000-8000-000000000002";
5
+ export function sharedCredentialKey(url, headers) {
6
+ const entries = [];
7
+ new Headers(headers).forEach((value, key) => entries.push([key, value]));
8
+ return JSON.stringify([url, entries]);
9
+ }
10
+ export function sameAdmission(a, b) {
11
+ return a.repositoryId === b.repositoryId && a.principalId === b.principalId &&
12
+ a.protocolEpoch === b.protocolEpoch && a.storageEpoch === b.storageEpoch;
13
+ }
14
+ /** Authorize attachment, awaiting any server-owned repository upgrade. */
15
+ export async function requestAdmission(url, credentials, transport) {
16
+ const locator = new URL(url);
17
+ const repositoryId = locator.pathname.match(/\/lix\/([0-9a-f-]{36})\/?$/i)?.[1];
18
+ if (!repositoryId || locator.search || locator.hash || locator.username || locator.password) {
19
+ throw new HttpTransportError("LIX_TRANSPORT_CONTRACT", "Admission requires a repository protocol URL");
20
+ }
21
+ const headers = new Headers(credentials);
22
+ headers.set("lix-sync-protocol-version", String(ADMISSION_PROTOCOL_EPOCH));
23
+ locator.pathname = `/lix/v1/${repositoryId}/admission`;
24
+ let response;
25
+ for (;;) {
26
+ response = await transport({ url: locator.toString(),
27
+ init: { method: "GET", headers, signal: AbortSignal.timeout(10_000), cache: "no-store", redirect: "error", credentials: "omit" },
28
+ response: { mode: "buffered", maxBytes: 16 * 1024 } });
29
+ if (response.status !== 503)
30
+ break;
31
+ const body = await response.clone().json().catch(() => null);
32
+ if (body?.error?.code !== "LIX_REPOSITORY_MIGRATING")
33
+ break;
34
+ // The authority owns the migration independently of this request. Poll only
35
+ // its explicit in-progress response; other failures retain their semantics.
36
+ await new Promise(resolve => setTimeout(resolve, 1_000));
37
+ }
38
+ if (response.status === 401 || response.status === 403) {
39
+ throw new HttpTransportError("LIX_ADMISSION_AUTH_REJECTED", "Authority rejected repository admission");
40
+ }
41
+ if (response.status === 409 || response.status === 426) {
42
+ throw new HttpTransportError("LIX_ADMISSION_EPOCH", "Repository is incompatible with this client version");
43
+ }
44
+ if (!response.ok)
45
+ throw new HttpTransportError("LIX_ADMISSION_HTTP", `Authority admission returned HTTP ${response.status}`);
46
+ let result;
47
+ try {
48
+ result = await response.json();
49
+ }
50
+ catch {
51
+ throw new HttpTransportError("LIX_ADMISSION_PROTOCOL", "Authority returned invalid admission metadata");
52
+ }
53
+ if (!result || result.repositoryId !== repositoryId ||
54
+ typeof result.principalId !== "string" || !/^[\x21-\x7e]{1,255}$/.test(result.principalId)) {
55
+ throw new HttpTransportError("LIX_ADMISSION_PROTOCOL", "Authority admission identity does not match the repository");
56
+ }
57
+ if (result.protocolEpoch !== ADMISSION_PROTOCOL_EPOCH || result.storageEpoch !== ADMISSION_STORAGE_EPOCH) {
58
+ throw new HttpTransportError("LIX_ADMISSION_EPOCH", "Repository is incompatible with this client version");
59
+ }
60
+ return result;
61
+ }
62
+ /** Memory-only proofs grant cached local attachment, never remote authorization. */
63
+ export class SharedAdmissionCache {
64
+ revocations = new Map();
65
+ proofs = new Map();
66
+ record(url, headers, identity) {
67
+ // Invisible cookies/custom-fetch identity cannot be a credential proof.
68
+ if (!new Headers(headers).get("authorization") && identity.principalId !== ANONYMOUS_ACCOUNT_ID)
69
+ return;
70
+ if (this.proofs.size >= 64)
71
+ this.proofs.delete(this.proofs.keys().next().value);
72
+ this.proofs.set(sharedCredentialKey(url, headers), { ...identity });
73
+ }
74
+ generation(url, headers) {
75
+ return this.revocations.get(sharedCredentialKey(url, headers)) ?? 0;
76
+ }
77
+ remove(url, headers) {
78
+ const key = sharedCredentialKey(url, headers);
79
+ this.revocations.set(key, this.generation(url, headers) + 1);
80
+ this.proofs.delete(key);
81
+ }
82
+ async persistLocal(url, headers, generation, write, remove) {
83
+ if (this.generation(url, headers) !== generation)
84
+ return;
85
+ await write();
86
+ if (this.generation(url, headers) !== generation)
87
+ await remove();
88
+ }
89
+ async verify(url, headers, expected, probe, allowOffline = true) {
90
+ const generation = this.generation(url, headers);
91
+ let identity;
92
+ let online = true;
93
+ try {
94
+ identity = await probe();
95
+ }
96
+ catch (error) {
97
+ if (!(error instanceof Error) || error.code !== "LIX_TRANSPORT_NETWORK")
98
+ throw error;
99
+ const known = this.proofs.get(sharedCredentialKey(url, headers));
100
+ if (!allowOffline || !known || !expected || !sameAdmission(known, expected)) {
101
+ throw new HttpTransportError("LIX_IDENTITY_UNVERIFIED_OFFLINE", "Repository identity cannot be verified while offline", { cause: error });
102
+ }
103
+ identity = known;
104
+ online = false;
105
+ }
106
+ if (this.generation(url, headers) !== generation) {
107
+ throw new HttpTransportError("LIX_ADMISSION_AUTH_REJECTED", "Credentials were rejected while admission was in flight");
108
+ }
109
+ if (expected && !sameAdmission(identity, expected)) {
110
+ throw new HttpTransportError("LIX_SHARED_ENGINE_IDENTITY_MISMATCH", "Shared engine repository/account does not match this client");
111
+ }
112
+ if (online)
113
+ this.record(url, headers, identity);
114
+ return { identity, online };
115
+ }
116
+ }
@@ -0,0 +1,34 @@
1
+ import type { LixBinding, SyncServerBindingOptions, TelemetryDispatch, TelemetryParentContext, OpenProgressDispatch } from "../binding-types.js";
2
+ export type SharedEngineClient = {
3
+ server: SyncServerBindingOptions;
4
+ isDisconnected?(): boolean;
5
+ telemetry?: TelemetryDispatch;
6
+ parent?: TelemetryParentContext;
7
+ progress?: OpenProgressDispatch;
8
+ commitIdentity?(): void | Promise<void>;
9
+ rejectCredentials?(headers: [string, string][]): void | Promise<void>;
10
+ verifyIdentity(): Promise<{
11
+ authorityUrl: string;
12
+ accountId: string;
13
+ headers: [string, string][];
14
+ online?: boolean;
15
+ }>;
16
+ };
17
+ /** One physical owner; ports receive independent sessions, never the root. */
18
+ export declare class SharedEngineOwner {
19
+ private readonly open;
20
+ private root;
21
+ private principalId;
22
+ private state;
23
+ get lifecycleState(): "closed" | "ready" | "closing" | "opening" | "migration-exclusive";
24
+ private readonly clients;
25
+ private queue;
26
+ constructor(open: (server: SyncServerBindingOptions, telemetry: TelemetryDispatch, client: SharedEngineClient) => Promise<LixBinding>);
27
+ private readonly backgroundTelemetry;
28
+ attach(client: SharedEngineClient): Promise<LixBinding>;
29
+ /** Serialize closed-storage conversion with root admission across all ports. */
30
+ convert(client: SharedEngineClient, conversion: (server: SyncServerBindingOptions) => Promise<void>, branchId?: string): Promise<void>;
31
+ deactivate(client: SharedEngineClient): void;
32
+ detach(client: SharedEngineClient): Promise<void>;
33
+ private transport;
34
+ }