@interocitor/core 0.0.0-beta.10
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/LICENSE +21 -0
- package/README.md +706 -0
- package/dist/adapters/cloudflare.d.ts +78 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +325 -0
- package/dist/adapters/google-drive.d.ts +64 -0
- package/dist/adapters/google-drive.d.ts.map +1 -0
- package/dist/adapters/google-drive.js +339 -0
- package/dist/adapters/memory.d.ts +53 -0
- package/dist/adapters/memory.d.ts.map +1 -0
- package/dist/adapters/memory.js +182 -0
- package/dist/adapters/webdav.d.ts +70 -0
- package/dist/adapters/webdav.d.ts.map +1 -0
- package/dist/adapters/webdav.js +323 -0
- package/dist/core/codec.d.ts +20 -0
- package/dist/core/codec.d.ts.map +1 -0
- package/dist/core/codec.js +102 -0
- package/dist/core/compaction.d.ts +45 -0
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/core/compaction.js +190 -0
- package/dist/core/connected-stores.d.ts +77 -0
- package/dist/core/connected-stores.d.ts.map +1 -0
- package/dist/core/connected-stores.js +76 -0
- package/dist/core/crdt.d.ts +36 -0
- package/dist/core/crdt.d.ts.map +1 -0
- package/dist/core/crdt.js +174 -0
- package/dist/core/errors.d.ts +47 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +61 -0
- package/dist/core/flush.d.ts +9 -0
- package/dist/core/flush.d.ts.map +1 -0
- package/dist/core/flush.js +98 -0
- package/dist/core/hlc.d.ts +25 -0
- package/dist/core/hlc.d.ts.map +1 -0
- package/dist/core/hlc.js +75 -0
- package/dist/core/ids.d.ts +49 -0
- package/dist/core/ids.d.ts.map +1 -0
- package/dist/core/ids.js +132 -0
- package/dist/core/internals.d.ts +33 -0
- package/dist/core/internals.d.ts.map +1 -0
- package/dist/core/internals.js +72 -0
- package/dist/core/manifest.d.ts +56 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +203 -0
- package/dist/core/pull.d.ts +26 -0
- package/dist/core/pull.d.ts.map +1 -0
- package/dist/core/pull.js +113 -0
- package/dist/core/row-id.d.ts +12 -0
- package/dist/core/row-id.d.ts.map +1 -0
- package/dist/core/row-id.js +11 -0
- package/dist/core/schema-types.d.ts +26 -0
- package/dist/core/schema-types.d.ts.map +1 -0
- package/dist/core/schema-types.js +31 -0
- package/dist/core/schema-types.type-test.d.ts +2 -0
- package/dist/core/schema-types.type-test.d.ts.map +1 -0
- package/dist/core/schema-types.type-test.js +224 -0
- package/dist/core/sync-engine.d.ts +364 -0
- package/dist/core/sync-engine.d.ts.map +1 -0
- package/dist/core/sync-engine.js +2475 -0
- package/dist/core/table.d.ts +260 -0
- package/dist/core/table.d.ts.map +1 -0
- package/dist/core/table.js +461 -0
- package/dist/core/types.d.ts +952 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +6 -0
- package/dist/crypto/encryption.d.ts +61 -0
- package/dist/crypto/encryption.d.ts.map +1 -0
- package/dist/crypto/encryption.js +216 -0
- package/dist/crypto/keys.d.ts +48 -0
- package/dist/crypto/keys.d.ts.map +1 -0
- package/dist/crypto/keys.js +54 -0
- package/dist/handshake/channel.d.ts +117 -0
- package/dist/handshake/channel.d.ts.map +1 -0
- package/dist/handshake/channel.js +245 -0
- package/dist/handshake/index.d.ts +216 -0
- package/dist/handshake/index.d.ts.map +1 -0
- package/dist/handshake/index.js +199 -0
- package/dist/handshake/qr-public.d.ts +3 -0
- package/dist/handshake/qr-public.d.ts.map +1 -0
- package/dist/handshake/qr-public.js +1 -0
- package/dist/handshake/qr.d.ts +100 -0
- package/dist/handshake/qr.d.ts.map +1 -0
- package/dist/handshake/qr.js +102 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/storage/credential-store.d.ts +122 -0
- package/dist/storage/credential-store.d.ts.map +1 -0
- package/dist/storage/credential-store.js +356 -0
- package/dist/storage/local-store.d.ts +64 -0
- package/dist/storage/local-store.d.ts.map +1 -0
- package/dist/storage/local-store.js +490 -0
- package/dist/storage/reset.d.ts +10 -0
- package/dist/storage/reset.d.ts.map +1 -0
- package/dist/storage/reset.js +18 -0
- package/package.json +76 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flush — push queued local outbox entries to remote cloud.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from Interocitor. Not part of the public API.
|
|
5
|
+
*/
|
|
6
|
+
import type { StorageAdapter, ChangeEntry, SyncEvent } from './types.ts';
|
|
7
|
+
import type { CodecState } from './codec.ts';
|
|
8
|
+
export declare function flushToAdapter(adapter: StorageAdapter, remotePath: string, entries: ChangeEntry[], isPrimary: boolean, codecState: CodecState, deviceId: string, emit?: (event: SyncEvent) => void): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=flush.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flush.d.ts","sourceRoot":"","sources":["../../src/core/flush.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EAEX,SAAS,EACV,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAe,GAC1C,OAAO,CAAC,IAAI,CAAC,CAqGf"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flush — push queued local outbox entries to remote cloud.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from Interocitor. Not part of the public API.
|
|
5
|
+
*/
|
|
6
|
+
import { hlcCompareStr } from "./hlc.js";
|
|
7
|
+
import { paths, textEncoder, textDecoder } from "./internals.js";
|
|
8
|
+
import { encodeChangePayload } from "./codec.js";
|
|
9
|
+
import { upsertDeviceMetadata } from "./manifest.js";
|
|
10
|
+
export async function flushToAdapter(adapter, remotePath, entries, isPrimary, codecState, deviceId, emit = () => { }) {
|
|
11
|
+
const p = paths(remotePath);
|
|
12
|
+
await adapter.ensureFolder(p.changesFolder);
|
|
13
|
+
let lastWrittenHlc = '';
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const fileName = `${entry.hlc}-${entry.id}.json`;
|
|
16
|
+
const payload = await encodeChangePayload(codecState, entry);
|
|
17
|
+
console.log('[interocitor:write] flush.changeFile', { path: p.changeFile(fileName), deviceId, isPrimary, entryId: entry.id, hlc: entry.hlc });
|
|
18
|
+
await adapter.writeFile(p.changeFile(fileName), textEncoder.encode(payload));
|
|
19
|
+
if (!lastWrittenHlc || hlcCompareStr(entry.hlc, lastWrittenHlc) > 0) {
|
|
20
|
+
lastWrittenHlc = entry.hlc;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Update global head — monotonic HLC hint for fast poll skipping.
|
|
24
|
+
// Tracing rules (see SyncEvent `trace:head`):
|
|
25
|
+
// - read → emit { op: 'read', reason: 'flush', priorHlc }
|
|
26
|
+
// - write strictly forward → emit { op: 'write', priorHlc, nextHlc }
|
|
27
|
+
// - write where nextHlc <= priorHlc → emit
|
|
28
|
+
// { op: 'write', regressed: true } AND skip the writeFile
|
|
29
|
+
// (regression = bug signal; never let head go backwards on disk).
|
|
30
|
+
// - no entries to flush → emit { op: 'skip-no-change' } and don't touch head.
|
|
31
|
+
if (!lastWrittenHlc) {
|
|
32
|
+
emit({
|
|
33
|
+
type: 'trace:head',
|
|
34
|
+
op: 'skip-no-change',
|
|
35
|
+
reason: 'flush',
|
|
36
|
+
path: p.changesHead,
|
|
37
|
+
nextHlc: null,
|
|
38
|
+
});
|
|
39
|
+
if (isPrimary) {
|
|
40
|
+
await upsertDeviceMetadata(adapter, remotePath, deviceId);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const readHeadIfExists = async () => {
|
|
45
|
+
try {
|
|
46
|
+
const data = await adapter.readFile(p.changesHead);
|
|
47
|
+
return JSON.parse(textDecoder.decode(data));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const priorHead = await readHeadIfExists();
|
|
54
|
+
const priorHlc = priorHead?.latestHlc ?? null;
|
|
55
|
+
emit({
|
|
56
|
+
type: 'trace:head',
|
|
57
|
+
op: 'read',
|
|
58
|
+
reason: 'flush',
|
|
59
|
+
path: p.changesHead,
|
|
60
|
+
priorHlc,
|
|
61
|
+
});
|
|
62
|
+
// Already-current short-circuit. Nothing to write — head already at or
|
|
63
|
+
// beyond what we just wrote (replica catching up, retried flush, etc.).
|
|
64
|
+
if (priorHlc && hlcCompareStr(priorHlc, lastWrittenHlc) >= 0) {
|
|
65
|
+
emit({
|
|
66
|
+
type: 'trace:head',
|
|
67
|
+
op: 'skip-no-change',
|
|
68
|
+
reason: 'flush',
|
|
69
|
+
path: p.changesHead,
|
|
70
|
+
priorHlc,
|
|
71
|
+
nextHlc: lastWrittenHlc,
|
|
72
|
+
});
|
|
73
|
+
if (isPrimary) {
|
|
74
|
+
await upsertDeviceMetadata(adapter, remotePath, deviceId);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Defensive: if priorHlc somehow > our lastWritten (shouldn't happen
|
|
79
|
+
// after the >= short-circuit) flag as regression and don't write.
|
|
80
|
+
const regressed = priorHlc !== null && hlcCompareStr(priorHlc, lastWrittenHlc) > 0;
|
|
81
|
+
const bestHlc = regressed ? priorHlc : lastWrittenHlc;
|
|
82
|
+
emit({
|
|
83
|
+
type: 'trace:head',
|
|
84
|
+
op: 'write',
|
|
85
|
+
reason: 'flush',
|
|
86
|
+
path: p.changesHead,
|
|
87
|
+
priorHlc,
|
|
88
|
+
nextHlc: bestHlc,
|
|
89
|
+
regressed,
|
|
90
|
+
});
|
|
91
|
+
if (!regressed) {
|
|
92
|
+
console.log('[interocitor:write] flush.head', { path: p.changesHead, deviceId, isPrimary, priorHlc, nextHlc: bestHlc });
|
|
93
|
+
await adapter.writeFile(p.changesHead, textEncoder.encode(JSON.stringify({ latestHlc: bestHlc }, null, 2)));
|
|
94
|
+
}
|
|
95
|
+
if (isPrimary) {
|
|
96
|
+
await upsertDeviceMetadata(adapter, remotePath, deviceId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid Logical Clock
|
|
3
|
+
*
|
|
4
|
+
* Combines wall-clock time with a logical counter to produce
|
|
5
|
+
* a total order across distributed devices without coordination.
|
|
6
|
+
*
|
|
7
|
+
* Format when serialized: "{ts}-{counter:04x}-{nodeId}"
|
|
8
|
+
* Example: "1711785600000-0000-dev_x1"
|
|
9
|
+
*/
|
|
10
|
+
import type { HLC } from './types.ts';
|
|
11
|
+
export declare const HLC_MAX_FUTURE_SKEW_MS: number;
|
|
12
|
+
export declare function hlcInit(nodeId: string): HLC;
|
|
13
|
+
/** Tick the local clock forward for a new local event. */
|
|
14
|
+
export declare function hlcNow(local: HLC): HLC;
|
|
15
|
+
/** Merge a remote HLC into the local clock (called on receive). */
|
|
16
|
+
export declare function hlcReceive(local: HLC, remote: HLC): HLC;
|
|
17
|
+
/** Total ordering: -1 if a < b, 0 if equal, 1 if a > b */
|
|
18
|
+
export declare function hlcCompare(a: HLC, b: HLC): number;
|
|
19
|
+
/** Compare two serialized HLC strings without parsing (fast path). */
|
|
20
|
+
export declare function hlcCompareStr(a: string, b: string): number;
|
|
21
|
+
/** Serialize HLC to a string that sorts lexicographically. */
|
|
22
|
+
export declare function hlcSerialize(hlc: HLC): string;
|
|
23
|
+
/** Parse a serialized HLC string back to an HLC object. */
|
|
24
|
+
export declare function hlcParse(s: string): HLC;
|
|
25
|
+
//# sourceMappingURL=hlc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hlc.d.ts","sourceRoot":"","sources":["../../src/core/hlc.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAGtC,eAAO,MAAM,sBAAsB,QAAgB,CAAC;AAEpD,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAE3C;AAED,0DAA0D;AAC1D,wBAAgB,MAAM,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAMtC;AAED,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAiBvD;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,MAAM,CAMjD;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAK7C;AAED,2DAA2D;AAC3D,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAQvC"}
|
package/dist/core/hlc.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid Logical Clock
|
|
3
|
+
*
|
|
4
|
+
* Combines wall-clock time with a logical counter to produce
|
|
5
|
+
* a total order across distributed devices without coordination.
|
|
6
|
+
*
|
|
7
|
+
* Format when serialized: "{ts}-{counter:04x}-{nodeId}"
|
|
8
|
+
* Example: "1711785600000-0000-dev_x1"
|
|
9
|
+
*/
|
|
10
|
+
// Guard against poisoned/misconfigured peers that report far-future clocks.
|
|
11
|
+
export const HLC_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
12
|
+
export function hlcInit(nodeId) {
|
|
13
|
+
return { ts: Date.now(), counter: 0, nodeId };
|
|
14
|
+
}
|
|
15
|
+
/** Tick the local clock forward for a new local event. */
|
|
16
|
+
export function hlcNow(local) {
|
|
17
|
+
const wall = Date.now();
|
|
18
|
+
if (wall > local.ts) {
|
|
19
|
+
return { ts: wall, counter: 0, nodeId: local.nodeId };
|
|
20
|
+
}
|
|
21
|
+
return { ts: local.ts, counter: local.counter + 1, nodeId: local.nodeId };
|
|
22
|
+
}
|
|
23
|
+
/** Merge a remote HLC into the local clock (called on receive). */
|
|
24
|
+
export function hlcReceive(local, remote) {
|
|
25
|
+
const wall = Date.now();
|
|
26
|
+
const safeRemoteTs = Math.min(remote.ts, wall + HLC_MAX_FUTURE_SKEW_MS);
|
|
27
|
+
const maxTs = Math.max(wall, local.ts, safeRemoteTs);
|
|
28
|
+
let counter;
|
|
29
|
+
if (maxTs === local.ts && maxTs === remote.ts) {
|
|
30
|
+
counter = Math.max(local.counter, remote.counter) + 1;
|
|
31
|
+
}
|
|
32
|
+
else if (maxTs === local.ts) {
|
|
33
|
+
counter = local.counter + 1;
|
|
34
|
+
}
|
|
35
|
+
else if (maxTs === safeRemoteTs) {
|
|
36
|
+
counter = remote.counter + 1;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
counter = 0;
|
|
40
|
+
}
|
|
41
|
+
return { ts: maxTs, counter, nodeId: local.nodeId };
|
|
42
|
+
}
|
|
43
|
+
/** Total ordering: -1 if a < b, 0 if equal, 1 if a > b */
|
|
44
|
+
export function hlcCompare(a, b) {
|
|
45
|
+
if (a.ts !== b.ts)
|
|
46
|
+
return a.ts - b.ts;
|
|
47
|
+
if (a.counter !== b.counter)
|
|
48
|
+
return a.counter - b.counter;
|
|
49
|
+
if (a.nodeId < b.nodeId)
|
|
50
|
+
return -1;
|
|
51
|
+
if (a.nodeId > b.nodeId)
|
|
52
|
+
return 1;
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
/** Compare two serialized HLC strings without parsing (fast path). */
|
|
56
|
+
export function hlcCompareStr(a, b) {
|
|
57
|
+
return hlcCompare(hlcParse(a), hlcParse(b));
|
|
58
|
+
}
|
|
59
|
+
/** Serialize HLC to a string that sorts lexicographically. */
|
|
60
|
+
export function hlcSerialize(hlc) {
|
|
61
|
+
// Zero-pad ts to 15 digits (covers until year 2286)
|
|
62
|
+
const ts = hlc.ts.toString().padStart(15, '0');
|
|
63
|
+
const counter = hlc.counter.toString(16).padStart(4, '0');
|
|
64
|
+
return `${ts}-${counter}-${hlc.nodeId}`;
|
|
65
|
+
}
|
|
66
|
+
/** Parse a serialized HLC string back to an HLC object. */
|
|
67
|
+
export function hlcParse(s) {
|
|
68
|
+
const firstDash = s.indexOf('-');
|
|
69
|
+
const secondDash = s.indexOf('-', firstDash + 1);
|
|
70
|
+
return {
|
|
71
|
+
ts: parseInt(s.slice(0, firstDash), 10),
|
|
72
|
+
counter: parseInt(s.slice(firstDash + 1, secondDash), 16),
|
|
73
|
+
nodeId: s.slice(secondDash + 1),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured ID generation and validation.
|
|
3
|
+
*
|
|
4
|
+
* Device IDs: UUIDv7 — timestamp + random, client-generated.
|
|
5
|
+
* Mesh IDs: UUIDv7 + HMAC tag — timestamp + random + MAC, worker-issued.
|
|
6
|
+
* Row IDs: UUID v4/v7 with optional prefix — client-generated.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate a UUIDv7: 48-bit ms timestamp + 74 bits random, RFC 9562.
|
|
10
|
+
* Sortable by creation time. Globally unique without coordination.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uuidv7(): string;
|
|
13
|
+
/** Generate a device ID. UUIDv7 — sortable, globally unique. */
|
|
14
|
+
export declare function createDeviceId(): string;
|
|
15
|
+
/** Validate a device ID (must be UUIDv7). */
|
|
16
|
+
export declare function isValidDeviceId(id: unknown): id is string;
|
|
17
|
+
/**
|
|
18
|
+
* Issue a mesh/team ID with embedded HMAC tag.
|
|
19
|
+
*
|
|
20
|
+
* Layout (opaque string):
|
|
21
|
+
* <uuidv7>.<tag>
|
|
22
|
+
*
|
|
23
|
+
* Where tag = base64url(HMAC-SHA256(secret, uuidv7))[0..10]
|
|
24
|
+
*
|
|
25
|
+
* Only workers with the secret can mint valid mesh IDs.
|
|
26
|
+
* Clients and workers validate with `isValidMeshId(id, secret)`.
|
|
27
|
+
*
|
|
28
|
+
* @param secret — HMAC key (CryptoKey or raw bytes). Workers hold this.
|
|
29
|
+
*/
|
|
30
|
+
export declare function issueMeshId(secret: CryptoKey): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Validate a mesh ID: format check + HMAC tag verification.
|
|
33
|
+
*
|
|
34
|
+
* @param id — the full mesh ID string (uuid.tag)
|
|
35
|
+
* @param secret — same HMAC key used to issue
|
|
36
|
+
*/
|
|
37
|
+
export declare function isValidMeshId(id: unknown, secret: CryptoKey): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a mesh ID into its parts. Does NOT verify tag.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseMeshId(id: string): {
|
|
42
|
+
uuid: string;
|
|
43
|
+
tag: string;
|
|
44
|
+
} | null;
|
|
45
|
+
/**
|
|
46
|
+
* Create a mesh HMAC secret key for use with issueMeshId / isValidMeshId.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createMeshSecret(): Promise<CryptoKey>;
|
|
49
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/core/ids.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;GAGG;AACH,wBAAgB,MAAM,IAAI,MAAM,CAiC/B;AAID,gEAAgE;AAChE,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAID,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,EAAE,IAAI,MAAM,CAEzD;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CASpF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO5E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,SAAS,CAAC,CAM3D"}
|
package/dist/core/ids.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured ID generation and validation.
|
|
3
|
+
*
|
|
4
|
+
* Device IDs: UUIDv7 — timestamp + random, client-generated.
|
|
5
|
+
* Mesh IDs: UUIDv7 + HMAC tag — timestamp + random + MAC, worker-issued.
|
|
6
|
+
* Row IDs: UUID v4/v7 with optional prefix — client-generated.
|
|
7
|
+
*/
|
|
8
|
+
// ─── UUIDv7 ──────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Generate a UUIDv7: 48-bit ms timestamp + 74 bits random, RFC 9562.
|
|
11
|
+
* Sortable by creation time. Globally unique without coordination.
|
|
12
|
+
*/
|
|
13
|
+
export function uuidv7() {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
// 6 bytes timestamp (48-bit ms)
|
|
16
|
+
const tsBytes = new Uint8Array(6);
|
|
17
|
+
let ts = now;
|
|
18
|
+
for (let i = 5; i >= 0; i--) {
|
|
19
|
+
tsBytes[i] = ts & 0xff;
|
|
20
|
+
ts = Math.floor(ts / 256);
|
|
21
|
+
}
|
|
22
|
+
// 10 bytes random
|
|
23
|
+
const randBytes = crypto.getRandomValues(new Uint8Array(10));
|
|
24
|
+
// Assemble 16 bytes
|
|
25
|
+
const bytes = new Uint8Array(16);
|
|
26
|
+
bytes.set(tsBytes, 0);
|
|
27
|
+
bytes.set(randBytes, 6);
|
|
28
|
+
// Set version 7 (bits 48-51)
|
|
29
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
30
|
+
// Set variant 10xx (bits 64-65)
|
|
31
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
32
|
+
// Format as UUID string
|
|
33
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
34
|
+
return [
|
|
35
|
+
hex.slice(0, 8),
|
|
36
|
+
hex.slice(8, 12),
|
|
37
|
+
hex.slice(12, 16),
|
|
38
|
+
hex.slice(16, 20),
|
|
39
|
+
hex.slice(20, 32),
|
|
40
|
+
].join('-');
|
|
41
|
+
}
|
|
42
|
+
// ─── Device IDs ──────────────────────────────────────────────────────
|
|
43
|
+
/** Generate a device ID. UUIDv7 — sortable, globally unique. */
|
|
44
|
+
export function createDeviceId() {
|
|
45
|
+
return uuidv7();
|
|
46
|
+
}
|
|
47
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
48
|
+
/** Validate a device ID (must be UUIDv7). */
|
|
49
|
+
export function isValidDeviceId(id) {
|
|
50
|
+
return typeof id === 'string' && UUID_RE.test(id);
|
|
51
|
+
}
|
|
52
|
+
// ─── Mesh / Team IDs ─────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Issue a mesh/team ID with embedded HMAC tag.
|
|
55
|
+
*
|
|
56
|
+
* Layout (opaque string):
|
|
57
|
+
* <uuidv7>.<tag>
|
|
58
|
+
*
|
|
59
|
+
* Where tag = base64url(HMAC-SHA256(secret, uuidv7))[0..10]
|
|
60
|
+
*
|
|
61
|
+
* Only workers with the secret can mint valid mesh IDs.
|
|
62
|
+
* Clients and workers validate with `isValidMeshId(id, secret)`.
|
|
63
|
+
*
|
|
64
|
+
* @param secret — HMAC key (CryptoKey or raw bytes). Workers hold this.
|
|
65
|
+
*/
|
|
66
|
+
export async function issueMeshId(secret) {
|
|
67
|
+
const id = uuidv7();
|
|
68
|
+
const tag = await computeTag(id, secret);
|
|
69
|
+
return `${id}.${tag}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate a mesh ID: format check + HMAC tag verification.
|
|
73
|
+
*
|
|
74
|
+
* @param id — the full mesh ID string (uuid.tag)
|
|
75
|
+
* @param secret — same HMAC key used to issue
|
|
76
|
+
*/
|
|
77
|
+
export async function isValidMeshId(id, secret) {
|
|
78
|
+
if (typeof id !== 'string')
|
|
79
|
+
return false;
|
|
80
|
+
const dot = id.lastIndexOf('.');
|
|
81
|
+
if (dot === -1)
|
|
82
|
+
return false;
|
|
83
|
+
const uuid = id.slice(0, dot);
|
|
84
|
+
const tag = id.slice(dot + 1);
|
|
85
|
+
if (!UUID_RE.test(uuid) || !tag)
|
|
86
|
+
return false;
|
|
87
|
+
const expected = await computeTag(uuid, secret);
|
|
88
|
+
return timingSafeEqual(tag, expected);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse a mesh ID into its parts. Does NOT verify tag.
|
|
92
|
+
*/
|
|
93
|
+
export function parseMeshId(id) {
|
|
94
|
+
const dot = id.lastIndexOf('.');
|
|
95
|
+
if (dot === -1)
|
|
96
|
+
return null;
|
|
97
|
+
const uuid = id.slice(0, dot);
|
|
98
|
+
const tag = id.slice(dot + 1);
|
|
99
|
+
if (!UUID_RE.test(uuid) || !tag)
|
|
100
|
+
return null;
|
|
101
|
+
return { uuid, tag };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Create a mesh HMAC secret key for use with issueMeshId / isValidMeshId.
|
|
105
|
+
*/
|
|
106
|
+
export async function createMeshSecret() {
|
|
107
|
+
return crypto.subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify']);
|
|
108
|
+
}
|
|
109
|
+
// ─── Internal helpers ────────────────────────────────────────────────
|
|
110
|
+
const encoder = new TextEncoder();
|
|
111
|
+
async function computeTag(data, secret) {
|
|
112
|
+
const sig = await crypto.subtle.sign('HMAC', secret, encoder.encode(data));
|
|
113
|
+
// Take first 8 bytes (64 bits) → base64url (11 chars)
|
|
114
|
+
const bytes = new Uint8Array(sig, 0, 8);
|
|
115
|
+
return base64url(bytes);
|
|
116
|
+
}
|
|
117
|
+
function base64url(bytes) {
|
|
118
|
+
let binary = '';
|
|
119
|
+
for (let i = 0; i < bytes.length; i++)
|
|
120
|
+
binary += String.fromCodePoint(bytes[i]);
|
|
121
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
122
|
+
}
|
|
123
|
+
/** Constant-time string comparison to prevent timing attacks on tag. */
|
|
124
|
+
function timingSafeEqual(a, b) {
|
|
125
|
+
if (a.length !== b.length)
|
|
126
|
+
return false;
|
|
127
|
+
let result = 0;
|
|
128
|
+
for (let i = 0; i < a.length; i++) {
|
|
129
|
+
result |= a.codePointAt(i) ^ b.codePointAt(i);
|
|
130
|
+
}
|
|
131
|
+
return result === 0;
|
|
132
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared across sync-engine modules.
|
|
3
|
+
*
|
|
4
|
+
* Not part of the public API.
|
|
5
|
+
*/
|
|
6
|
+
export interface CloudPaths {
|
|
7
|
+
manifestPointer: string;
|
|
8
|
+
manifestFile: (generation: number) => string;
|
|
9
|
+
devicesFolder: string;
|
|
10
|
+
deviceFile: (deviceId: string) => string;
|
|
11
|
+
mainlineFolder: string;
|
|
12
|
+
changesFolder: string;
|
|
13
|
+
changesHead: string;
|
|
14
|
+
changeFile: (fileName: string) => string;
|
|
15
|
+
}
|
|
16
|
+
export declare function paths(root: string): CloudPaths;
|
|
17
|
+
declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
|
|
18
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
19
|
+
export declare function logAtLevel(currentLevel: LogLevel, level: LogLevel, ...args: unknown[]): void;
|
|
20
|
+
export declare function log(level: LogLevel, ...args: unknown[]): void;
|
|
21
|
+
export declare function normalizeLogLevel(level: string | null | undefined): LogLevel;
|
|
22
|
+
export { LOG_LEVELS };
|
|
23
|
+
/**
|
|
24
|
+
* Generate a prefixed ID for internal use (change entries, snapshots, etc).
|
|
25
|
+
* Uses UUIDv7 for sortability.
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateId(prefix: string): string;
|
|
28
|
+
export declare function getDeviceId(override?: string): string;
|
|
29
|
+
export declare const textEncoder: TextEncoder;
|
|
30
|
+
export declare const textDecoder: TextDecoder;
|
|
31
|
+
export declare function hexFromBytes(bytes: Uint8Array): string;
|
|
32
|
+
export declare function computeContentHash(payload: unknown): Promise<string>;
|
|
33
|
+
//# sourceMappingURL=internals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internals.d.ts","sourceRoot":"","sources":["../../src/core/internals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAY9C;AAKD,QAAA,MAAM,UAAU,6CAA8C,CAAC;AAE/D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAMnD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAI5F;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAE7D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,QAAQ,CAE5E;AAED,OAAO,EAAE,UAAU,EAAE,CAAC;AAOtB;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAUrD;AAID,eAAO,MAAM,WAAW,aAAoB,CAAC;AAC7C,eAAO,MAAM,WAAW,aAAoB,CAAC;AAE7C,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEtD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared across sync-engine modules.
|
|
3
|
+
*
|
|
4
|
+
* Not part of the public API.
|
|
5
|
+
*/
|
|
6
|
+
export function paths(root) {
|
|
7
|
+
const changesFolder = `${root}/changes`;
|
|
8
|
+
return {
|
|
9
|
+
manifestPointer: `${root}/manifest.json`,
|
|
10
|
+
manifestFile: (generation) => `${root}/manifest-${generation}.json`,
|
|
11
|
+
devicesFolder: `${root}/devices`,
|
|
12
|
+
deviceFile: (deviceId) => `${root}/devices/${deviceId}.json`,
|
|
13
|
+
mainlineFolder: `${root}/mainline`,
|
|
14
|
+
changesFolder,
|
|
15
|
+
changesHead: `${changesFolder}/head.json`,
|
|
16
|
+
changeFile: (fileName) => `${changesFolder}/${fileName}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// ─── Logger ──────────────────────────────────────────────────────────
|
|
20
|
+
const LOG_PREFIX = '[interocitor]';
|
|
21
|
+
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
22
|
+
function shouldLog(currentLevel, messageLevel) {
|
|
23
|
+
return LOG_LEVELS.indexOf(messageLevel) >= LOG_LEVELS.indexOf(currentLevel);
|
|
24
|
+
}
|
|
25
|
+
export function logAtLevel(currentLevel, level, ...args) {
|
|
26
|
+
if (!shouldLog(currentLevel, level))
|
|
27
|
+
return;
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console[level](LOG_PREFIX, ...args);
|
|
30
|
+
}
|
|
31
|
+
export function log(level, ...args) {
|
|
32
|
+
logAtLevel('debug', level, ...args);
|
|
33
|
+
}
|
|
34
|
+
export function normalizeLogLevel(level) {
|
|
35
|
+
return LOG_LEVELS.includes(level ?? '') ? level : 'info';
|
|
36
|
+
}
|
|
37
|
+
export { LOG_LEVELS };
|
|
38
|
+
// ─── ID generation ───────────────────────────────────────────────────
|
|
39
|
+
import { uuidv7, createDeviceId } from "./ids.js";
|
|
40
|
+
/**
|
|
41
|
+
* Generate a prefixed ID for internal use (change entries, snapshots, etc).
|
|
42
|
+
* Uses UUIDv7 for sortability.
|
|
43
|
+
*/
|
|
44
|
+
export function generateId(prefix) {
|
|
45
|
+
return `${prefix}_${uuidv7()}`;
|
|
46
|
+
}
|
|
47
|
+
export function getDeviceId(override) {
|
|
48
|
+
if (override)
|
|
49
|
+
return override;
|
|
50
|
+
const KEY = 'interocitor-device-id';
|
|
51
|
+
const storage = typeof localStorage === 'undefined' ? null : localStorage;
|
|
52
|
+
let id = storage?.getItem(KEY) ?? null;
|
|
53
|
+
if (!id) {
|
|
54
|
+
id = createDeviceId();
|
|
55
|
+
try {
|
|
56
|
+
storage?.setItem(KEY, id);
|
|
57
|
+
}
|
|
58
|
+
catch { /* ok */ }
|
|
59
|
+
}
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
// ─── Encoding / Hashing ─────────────────────────────────────────────
|
|
63
|
+
export const textEncoder = new TextEncoder();
|
|
64
|
+
export const textDecoder = new TextDecoder();
|
|
65
|
+
export function hexFromBytes(bytes) {
|
|
66
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
67
|
+
}
|
|
68
|
+
export async function computeContentHash(payload) {
|
|
69
|
+
const json = JSON.stringify(payload);
|
|
70
|
+
const digest = await crypto.subtle.digest('SHA-256', textEncoder.encode(json));
|
|
71
|
+
return `sha256:${hexFromBytes(new Uint8Array(digest))}`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest — reading, writing, creating, and validating cloud manifests.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from Interocitor. Not part of the public API.
|
|
5
|
+
*/
|
|
6
|
+
import type { StorageAdapter, Manifest, ManifestPointer, DatabaseSchemaDefinition, SyncEvent } from './types.ts';
|
|
7
|
+
import type { CodecState } from './codec.ts';
|
|
8
|
+
export interface ManifestContext {
|
|
9
|
+
adapter: StorageAdapter;
|
|
10
|
+
remotePath: string;
|
|
11
|
+
serverId: string;
|
|
12
|
+
serverManaged: boolean;
|
|
13
|
+
deviceId: string;
|
|
14
|
+
encrypted: boolean;
|
|
15
|
+
schema?: DatabaseSchemaDefinition;
|
|
16
|
+
emit: (event: SyncEvent) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function readJson<T>(adapter: StorageAdapter, path: string): Promise<T>;
|
|
19
|
+
export declare function readJsonIfExists<T>(adapter: StorageAdapter, path: string): Promise<T | null>;
|
|
20
|
+
export declare function assertServerAuth(manifest: {
|
|
21
|
+
writtenBy: string;
|
|
22
|
+
}, serverId: string): void;
|
|
23
|
+
export declare function validateManifestHash(manifest: {
|
|
24
|
+
contentHash: string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
export declare function writeJson(adapter: StorageAdapter, path: string, value: unknown): Promise<void>;
|
|
28
|
+
export declare function createBootstrapManifest(ctx: ManifestContext, meshId?: string): Promise<{
|
|
29
|
+
pointer: ManifestPointer;
|
|
30
|
+
manifest: Manifest;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function loadOrCreateManifest(ctx: ManifestContext, codecState: CodecState, local: import('./types.ts').LocalStoreAdapter, poisonRemote: (error: unknown, path?: string) => Promise<Error>, reason?: string): Promise<{
|
|
33
|
+
manifest: Manifest;
|
|
34
|
+
bootstrapped: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function upsertDeviceMetadata(adapter: StorageAdapter, remotePath: string, deviceId: string, opts?: {
|
|
37
|
+
displayName?: string;
|
|
38
|
+
deviceType?: import('./types.ts').DeviceType;
|
|
39
|
+
observedManifestGeneration?: number;
|
|
40
|
+
observedEpoch?: number;
|
|
41
|
+
observedWatermarkHlc?: string;
|
|
42
|
+
observedGcFloorHlc?: string;
|
|
43
|
+
/**
|
|
44
|
+
* When true, skip the read-merge step. Use only when the caller knows
|
|
45
|
+
* no prior device record exists (e.g. immediately after bootstrap of
|
|
46
|
+
* a fresh mesh). Saves one round-trip per connect on first run.
|
|
47
|
+
*/
|
|
48
|
+
bootstrap?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* When true, preserve existing timestamps and skip the write entirely if
|
|
51
|
+
* the merged metadata would be identical. Use for reconnect acknowledgements
|
|
52
|
+
* where observability matters more than heartbeats.
|
|
53
|
+
*/
|
|
54
|
+
skipTouchIfUnchanged?: boolean;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/core/manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EACR,eAAe,EAEf,wBAAwB,EACxB,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAClC;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGnF;AAED,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAMlG;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxF;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAGpG;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,eAAe,EACpB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,CAsD3D;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,OAAO,YAAY,EAAE,iBAAiB,EAC7C,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,EAC/D,MAAM,GAAE,MAAkB,GACzB,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC,CA2DxD;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,YAAY,EAAE,UAAU,CAAC;IAC7C,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GACA,OAAO,CAAC,IAAI,CAAC,CAyDf"}
|