cursedbelt 2.6.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/file-tree/fileTreeModel.d.ts +18 -0
- package/dist/core/file-tree/fileTreeModel.d.ts.map +1 -1
- package/dist/core/file-tree/fileTreeModel.js +24 -0
- package/dist/core/file-tree/fileTreeModel.js.map +1 -1
- package/dist/react/components/TagsInput.d.ts +8 -1
- package/dist/react/components/TagsInput.d.ts.map +1 -1
- package/dist/react/components/TagsInput.js +10 -4
- package/dist/react/components/TagsInput.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts +14 -2
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +3 -9
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/index.d.ts +1 -1
- package/dist/react/file-tree/index.d.ts.map +1 -1
- package/dist/react/file-tree/index.js +1 -1
- package/dist/react/file-tree/index.js.map +1 -1
- package/dist/server/sync/alarm.d.ts +35 -0
- package/dist/server/sync/alarm.d.ts.map +1 -0
- package/dist/server/sync/alarm.js +92 -0
- package/dist/server/sync/alarm.js.map +1 -0
- package/dist/server/sync/commands.d.ts +88 -0
- package/dist/server/sync/commands.d.ts.map +1 -0
- package/dist/server/sync/commands.js +242 -0
- package/dist/server/sync/commands.js.map +1 -0
- package/dist/server/sync/engine.d.ts +63 -0
- package/dist/server/sync/engine.d.ts.map +1 -0
- package/dist/server/sync/engine.js +185 -0
- package/dist/server/sync/engine.js.map +1 -0
- package/dist/server/sync/http.d.ts +107 -0
- package/dist/server/sync/http.d.ts.map +1 -0
- package/dist/server/sync/http.js +244 -0
- package/dist/server/sync/http.js.map +1 -0
- package/dist/server/sync/index.d.ts +42 -0
- package/dist/server/sync/index.d.ts.map +1 -0
- package/dist/server/sync/index.js +42 -0
- package/dist/server/sync/index.js.map +1 -0
- package/dist/server/sync/opLog.d.ts +62 -0
- package/dist/server/sync/opLog.d.ts.map +1 -0
- package/dist/server/sync/opLog.js +97 -0
- package/dist/server/sync/opLog.js.map +1 -0
- package/dist/server/sync/planner.d.ts +32 -0
- package/dist/server/sync/planner.d.ts.map +1 -0
- package/dist/server/sync/planner.js +31 -0
- package/dist/server/sync/planner.js.map +1 -0
- package/dist/server/sync/status.d.ts +64 -0
- package/dist/server/sync/status.d.ts.map +1 -0
- package/dist/server/sync/status.js +50 -0
- package/dist/server/sync/status.js.map +1 -0
- package/dist/server/sync/timer.d.ts +53 -0
- package/dist/server/sync/timer.d.ts.map +1 -0
- package/dist/server/sync/timer.js +171 -0
- package/dist/server/sync/timer.js.map +1 -0
- package/dist/server/sync/tokens.d.ts +26 -0
- package/dist/server/sync/tokens.d.ts.map +1 -0
- package/dist/server/sync/tokens.js +52 -0
- package/dist/server/sync/tokens.js.map +1 -0
- package/dist/server/sync/types.d.ts +102 -0
- package/dist/server/sync/types.d.ts.map +1 -0
- package/dist/server/sync/types.js +30 -0
- package/dist/server/sync/types.js.map +1 -0
- package/package.json +25 -15
- package/scripts/verify.ts +292 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +23 -0
- package/src/core/file-tree/fileTreeModel.ts +33 -0
- package/src/demoFixture.spec.ts +11 -1
- package/src/react/components/TagsInput.spec.tsx +34 -0
- package/src/react/components/TagsInput.tsx +23 -1
- package/src/react/file-tree/FileTree.tsx +19 -11
- package/src/react/file-tree/index.ts +2 -0
- package/src/server/sync/alarm.spec.ts +149 -0
- package/src/server/sync/alarm.ts +137 -0
- package/src/server/sync/commands.spec.ts +145 -0
- package/src/server/sync/commands.ts +361 -0
- package/src/server/sync/engine.spec.ts +496 -0
- package/src/server/sync/engine.ts +255 -0
- package/src/server/sync/http.ts +316 -0
- package/src/server/sync/httpRemote.spec.ts +158 -0
- package/src/server/sync/index.ts +90 -0
- package/src/server/sync/opLog.spec.ts +110 -0
- package/src/server/sync/opLog.ts +189 -0
- package/src/server/sync/planner.spec.ts +53 -0
- package/src/server/sync/planner.ts +62 -0
- package/src/server/sync/status.spec.ts +92 -0
- package/src/server/sync/status.ts +108 -0
- package/src/server/sync/timer.spec.ts +150 -0
- package/src/server/sync/timer.ts +207 -0
- package/src/server/sync/tokens.spec.ts +38 -0
- package/src/server/sync/tokens.ts +94 -0
- package/src/server/sync/types.ts +108 -0
- package/src/verifyGraph.spec.ts +181 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure loop planner — vault's `planNextSync`, unchanged in spirit: back off after
|
|
3
|
+
* a failure, else honor a debounced dirty flag, else the steady interval. Pure so
|
|
4
|
+
* the timer is a thin wrapper and this is fake-clock testable.
|
|
5
|
+
*/
|
|
6
|
+
export interface SyncLoopConfig {
|
|
7
|
+
/** Steady poll cadence when reachable and idle. */
|
|
8
|
+
intervalMs: number;
|
|
9
|
+
/** How long after a local write to wait before syncing, so a burst coalesces. */
|
|
10
|
+
debounceMs: number;
|
|
11
|
+
/** First backoff step after a failure. */
|
|
12
|
+
backoffBaseMs: number;
|
|
13
|
+
/** Backoff ceiling — an unreachable peer (the nightly window) settles here. */
|
|
14
|
+
backoffMaxMs: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const DEFAULT_LOOP: SyncLoopConfig;
|
|
17
|
+
export interface SyncLoopState {
|
|
18
|
+
/** When the last sync attempt STARTED (0 if never). */
|
|
19
|
+
lastAttempt: number;
|
|
20
|
+
/** Consecutive failed attempts — drives exponential backoff. */
|
|
21
|
+
consecutiveFailures: number;
|
|
22
|
+
/** When the oldest un-synced local write happened, or null if clean. */
|
|
23
|
+
dirtySince: number | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function planNextSync(state: SyncLoopState, now: number, cfg?: SyncLoopConfig): {
|
|
26
|
+
runNow: boolean;
|
|
27
|
+
waitMs: number;
|
|
28
|
+
};
|
|
29
|
+
/** A peer that is asleep (the nightly EC2 window) or an internet-less Mac is NORMAL,
|
|
30
|
+
* not an error. It still feeds the backoff. */
|
|
31
|
+
export declare function isUnreachable(err: unknown): boolean;
|
|
32
|
+
//# sourceMappingURL=planner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../../src/server/sync/planner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,cAAc;IAC9B,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,YAAY,EAAE,cAK1B,CAAC;AAEF,MAAM,WAAW,aAAa;IAC7B,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,YAAY,CAC3B,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,cAA6B,GAChC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAerC;AAED;gDACgD;AAChD,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAKnD"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure loop planner — vault's `planNextSync`, unchanged in spirit: back off after
|
|
3
|
+
* a failure, else honor a debounced dirty flag, else the steady interval. Pure so
|
|
4
|
+
* the timer is a thin wrapper and this is fake-clock testable.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_LOOP = {
|
|
7
|
+
intervalMs: 5 * 60_000,
|
|
8
|
+
debounceMs: 3_000,
|
|
9
|
+
backoffBaseMs: 30_000,
|
|
10
|
+
backoffMaxMs: 30 * 60_000,
|
|
11
|
+
};
|
|
12
|
+
export function planNextSync(state, now, cfg = DEFAULT_LOOP) {
|
|
13
|
+
if (state.consecutiveFailures > 0) {
|
|
14
|
+
const step = Math.min(cfg.backoffMaxMs, cfg.backoffBaseMs * 2 ** (state.consecutiveFailures - 1));
|
|
15
|
+
const due = state.lastAttempt + step;
|
|
16
|
+
return now >= due ? { runNow: true, waitMs: 0 } : { runNow: false, waitMs: due - now };
|
|
17
|
+
}
|
|
18
|
+
if (state.dirtySince !== null) {
|
|
19
|
+
const due = state.dirtySince + cfg.debounceMs;
|
|
20
|
+
return now >= due ? { runNow: true, waitMs: 0 } : { runNow: false, waitMs: due - now };
|
|
21
|
+
}
|
|
22
|
+
const due = state.lastAttempt + cfg.intervalMs;
|
|
23
|
+
return now >= due ? { runNow: true, waitMs: 0 } : { runNow: false, waitMs: due - now };
|
|
24
|
+
}
|
|
25
|
+
/** A peer that is asleep (the nightly EC2 window) or an internet-less Mac is NORMAL,
|
|
26
|
+
* not an error. It still feeds the backoff. */
|
|
27
|
+
export function isUnreachable(err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return /ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|fetch failed|timed out|The operation was aborted|network/i.test(msg);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=planner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.js","sourceRoot":"","sources":["../../../src/server/sync/planner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,MAAM,CAAC,MAAM,YAAY,GAAmB;IAC3C,UAAU,EAAE,CAAC,GAAG,MAAM;IACtB,UAAU,EAAE,KAAK;IACjB,aAAa,EAAE,MAAM;IACrB,YAAY,EAAE,EAAE,GAAG,MAAM;CACzB,CAAC;AAWF,MAAM,UAAU,YAAY,CAC3B,KAAoB,EACpB,GAAW,EACX,MAAsB,YAAY;IAElC,IAAI,KAAK,CAAC,mBAAmB,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CACpB,GAAG,CAAC,YAAY,EAChB,GAAG,CAAC,aAAa,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mBAAmB,GAAG,CAAC,CAAC,CACxD,CAAC;QACF,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;QACrC,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;IACxF,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC;QAC9C,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;IACxF,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,GAAG,GAAG,CAAC,UAAU,CAAC;IAC/C,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;AACxF,CAAC;AAED;gDACgD;AAChD,MAAM,UAAU,aAAa,CAAC,GAAY;IACzC,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,sGAAsG,CAAC,IAAI,CACjH,GAAG,CACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status surface — vault's `SyncStatus` generalized verbatim, because its shape
|
|
3
|
+
* is the product of a real defect: `enabled: false` alone was read as "not syncing"
|
|
4
|
+
* on a receiving half that was in step the whole time. `role` is what tells the
|
|
5
|
+
* receiving half apart from a scratch instance, and anything reasoning about "is
|
|
6
|
+
* this instance syncing" must read `role`, not `enabled`.
|
|
7
|
+
*/
|
|
8
|
+
export interface SyncStatus {
|
|
9
|
+
/** Does THIS instance dial out at all? */
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
/** dialer — runs the loop · receiver — answers the sync surface for a dialer ·
|
|
12
|
+
* none — neither (a dev shell on its own scratch database). */
|
|
13
|
+
role: "dialer" | "receiver" | "none";
|
|
14
|
+
/** The peer's ORIGIN — never the bearer token. Null when unlinked. */
|
|
15
|
+
peer: string | null;
|
|
16
|
+
lastAttemptAt: number | null;
|
|
17
|
+
lastSuccessAt: number | null;
|
|
18
|
+
lastOutcome: "ok" | "unreachable" | "paused" | "error" | null;
|
|
19
|
+
/** Set while the push is version-skew paused (`blockedBy`) — names the op kind the
|
|
20
|
+
* peer cannot take yet, i.e. "the peer needs deploying", the true statement. */
|
|
21
|
+
pausedOn: string | null;
|
|
22
|
+
consecutiveFailures: number;
|
|
23
|
+
/** Local writes the peer has not acknowledged yet — 0 means "fully in step". */
|
|
24
|
+
pending: number;
|
|
25
|
+
/** RECEIVER: when a dialing peer last authenticated against the sync surface. */
|
|
26
|
+
lastPeerContactAt: number | null;
|
|
27
|
+
/** RECEIVER: when someone last pressed "Sync now" here (the dialer polls it). */
|
|
28
|
+
syncRequestedAt: number | null;
|
|
29
|
+
}
|
|
30
|
+
export declare const OFFLINE_STATUS: SyncStatus;
|
|
31
|
+
export interface SyncStatusReporter {
|
|
32
|
+
read(): SyncStatus;
|
|
33
|
+
/** The timer's / receiver hooks' writer half — nothing else should call it. */
|
|
34
|
+
update(patch: Partial<SyncStatus>): void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Somewhere to keep the ONE field that is a fact about the DATA rather than about
|
|
38
|
+
* this process.
|
|
39
|
+
*
|
|
40
|
+
* 🔴 Why only `lastPeerContactAt`. Every other field describes the live process and
|
|
41
|
+
* SHOULD reset with it: a dialer that has not attempted anything since boot has
|
|
42
|
+
* genuinely not attempted anything, and a restored `consecutiveFailures: 3` would be
|
|
43
|
+
* a claim about failures that did not happen. But "a peer authenticated here" is
|
|
44
|
+
* something that happened to the database, and forgetting it produced the owner's
|
|
45
|
+
* 2026-08-16 report — prod saying *"Awaiting first sync"* over a vault he had used
|
|
46
|
+
* for weeks. The badge's own words are "your Mac has not dialled since this server
|
|
47
|
+
* last restarted", which is true and which nobody reads as anything but "this has
|
|
48
|
+
* never synced". A receiver restarts on every deploy, and the dialer runs every 5
|
|
49
|
+
* minutes only while the Mac is awake — so the window in which a perfectly converged
|
|
50
|
+
* pair reads as never-synced is wide. It was wider still when the receiver was the
|
|
51
|
+
* `prod-ec2` box (decommissioned since), which also slept 12am–10am CT: most of a
|
|
52
|
+
* morning, every morning. In-memory was the wrong home for it.
|
|
53
|
+
*/
|
|
54
|
+
export interface SyncStatusStore {
|
|
55
|
+
/** The stored contact stamp, or null if a peer has genuinely never called. */
|
|
56
|
+
loadPeerContactAt(): number | null;
|
|
57
|
+
/** Persist a newer stamp. Called at most once per {@link PEER_CONTACT_WRITE_MS}. */
|
|
58
|
+
savePeerContactAt(at: number): void;
|
|
59
|
+
}
|
|
60
|
+
/** A dialer makes several requests per run; one durable write per this window is
|
|
61
|
+
* enough to answer "when did a peer last call" to the nearest half-minute. */
|
|
62
|
+
export declare const PEER_CONTACT_WRITE_MS = 30000;
|
|
63
|
+
export declare function createSyncStatusReporter(initial?: Partial<SyncStatus>, store?: SyncStatusStore): SyncStatusReporter;
|
|
64
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/server/sync/status.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,UAAU;IAC1B,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB;oEACgE;IAChE,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;IACrC,sEAAsE;IACtE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,IAAI,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAC9D;qFACiF;IACjF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,iFAAiF;IACjF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,eAAO,MAAM,cAAc,EAAE,UAY5B,CAAC;AAEF,MAAM,WAAW,kBAAkB;IAClC,IAAI,IAAI,UAAU,CAAC;IACnB,+EAA+E;IAC/E,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAe;IAC/B,8EAA8E;IAC9E,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAAC;IACnC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;+EAC+E;AAC/E,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,wBAAgB,wBAAwB,CACvC,OAAO,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAC7B,KAAK,CAAC,EAAE,eAAe,GACrB,kBAAkB,CAwBpB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status surface — vault's `SyncStatus` generalized verbatim, because its shape
|
|
3
|
+
* is the product of a real defect: `enabled: false` alone was read as "not syncing"
|
|
4
|
+
* on a receiving half that was in step the whole time. `role` is what tells the
|
|
5
|
+
* receiving half apart from a scratch instance, and anything reasoning about "is
|
|
6
|
+
* this instance syncing" must read `role`, not `enabled`.
|
|
7
|
+
*/
|
|
8
|
+
export const OFFLINE_STATUS = {
|
|
9
|
+
enabled: false,
|
|
10
|
+
role: "none",
|
|
11
|
+
peer: null,
|
|
12
|
+
lastAttemptAt: null,
|
|
13
|
+
lastSuccessAt: null,
|
|
14
|
+
lastOutcome: null,
|
|
15
|
+
pausedOn: null,
|
|
16
|
+
consecutiveFailures: 0,
|
|
17
|
+
pending: 0,
|
|
18
|
+
lastPeerContactAt: null,
|
|
19
|
+
syncRequestedAt: null,
|
|
20
|
+
};
|
|
21
|
+
/** A dialer makes several requests per run; one durable write per this window is
|
|
22
|
+
* enough to answer "when did a peer last call" to the nearest half-minute. */
|
|
23
|
+
export const PEER_CONTACT_WRITE_MS = 30_000;
|
|
24
|
+
export function createSyncStatusReporter(initial, store) {
|
|
25
|
+
const restored = store?.loadPeerContactAt() ?? null;
|
|
26
|
+
let current = {
|
|
27
|
+
...OFFLINE_STATUS,
|
|
28
|
+
...(restored === null ? {} : { lastPeerContactAt: restored }),
|
|
29
|
+
...initial,
|
|
30
|
+
};
|
|
31
|
+
let lastWrittenAt = restored ?? 0;
|
|
32
|
+
return {
|
|
33
|
+
read: () => ({ ...current }),
|
|
34
|
+
update: (patch) => {
|
|
35
|
+
current = { ...current, ...patch };
|
|
36
|
+
const at = patch.lastPeerContactAt;
|
|
37
|
+
if (store && typeof at === "number" && at - lastWrittenAt >= PEER_CONTACT_WRITE_MS) {
|
|
38
|
+
lastWrittenAt = at;
|
|
39
|
+
// A status write must never be able to fail the sync request it rode in on.
|
|
40
|
+
try {
|
|
41
|
+
store.savePeerContactAt(at);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* the in-memory value still answers; the next contact retries the write */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/server/sync/status.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyBH,MAAM,CAAC,MAAM,cAAc,GAAe;IACzC,OAAO,EAAE,KAAK;IACd,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,IAAI;IACV,aAAa,EAAE,IAAI;IACnB,aAAa,EAAE,IAAI;IACnB,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,IAAI;IACd,mBAAmB,EAAE,CAAC;IACtB,OAAO,EAAE,CAAC;IACV,iBAAiB,EAAE,IAAI;IACvB,eAAe,EAAE,IAAI;CACrB,CAAC;AAiCF;+EAC+E;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAE5C,MAAM,UAAU,wBAAwB,CACvC,OAA6B,EAC7B,KAAuB;IAEvB,MAAM,QAAQ,GAAG,KAAK,EAAE,iBAAiB,EAAE,IAAI,IAAI,CAAC;IACpD,IAAI,OAAO,GAAe;QACzB,GAAG,cAAc;QACjB,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC;QAC7D,GAAG,OAAO;KACV,CAAC;IACF,IAAI,aAAa,GAAG,QAAQ,IAAI,CAAC,CAAC;IAClC,OAAO;QACN,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC;QAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YACjB,OAAO,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;YACnC,MAAM,EAAE,GAAG,KAAK,CAAC,iBAAiB,CAAC;YACnC,IAAI,KAAK,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,GAAG,aAAa,IAAI,qBAAqB,EAAE,CAAC;gBACpF,aAAa,GAAG,EAAE,CAAC;gBACnB,4EAA4E;gBAC5E,IAAI,CAAC;oBACJ,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;gBAC7B,CAAC;gBAAC,MAAM,CAAC;oBACR,2EAA2E;gBAC5E,CAAC;YACF,CAAC;QACF,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-process dialer loop — vault's `syncTimer` generalized. Runs on the dialing
|
|
3
|
+
* half only; a failure NEVER takes the host daemon down (an unsynced instance still
|
|
4
|
+
* serves and still accepts writes — that is the point of two independently-writable
|
|
5
|
+
* instances). The timer is `unref`'d so it never holds the process open.
|
|
6
|
+
*
|
|
7
|
+
* The peer is re-resolved on EVERY tick, so linking (or revoking) a peer takes
|
|
8
|
+
* effect without a restart. The "Sync now" fast lane polls the receiver's request
|
|
9
|
+
* stamp every ~20s while healthy, so a button press on the far side means seconds,
|
|
10
|
+
* not "sometime in the next interval".
|
|
11
|
+
*/
|
|
12
|
+
import { type SyncLoopConfig } from "./planner";
|
|
13
|
+
import type { SyncStatusReporter } from "./status";
|
|
14
|
+
import type { SyncSummary } from "./types";
|
|
15
|
+
export declare const REQUEST_POLL_MS = 20000;
|
|
16
|
+
export interface SyncTimerDeps {
|
|
17
|
+
/** Re-read every tick: null = not linked (quiet no-op, re-checked next tick). */
|
|
18
|
+
resolvePeer: () => {
|
|
19
|
+
basePath: string;
|
|
20
|
+
token: string;
|
|
21
|
+
} | null;
|
|
22
|
+
/** Run one full reconciliation against the resolved peer. */
|
|
23
|
+
sync: (peer: {
|
|
24
|
+
basePath: string;
|
|
25
|
+
token: string;
|
|
26
|
+
}) => Promise<SyncSummary>;
|
|
27
|
+
/** The local op-log head — a head beyond the last synced one marks dirty. */
|
|
28
|
+
head: () => number;
|
|
29
|
+
/** Ask the peer whether its "Sync now" was pressed; null on any failure. */
|
|
30
|
+
checkRequest?: (peer: {
|
|
31
|
+
basePath: string;
|
|
32
|
+
token: string;
|
|
33
|
+
}) => Promise<number | null>;
|
|
34
|
+
status?: SyncStatusReporter;
|
|
35
|
+
loop?: SyncLoopConfig;
|
|
36
|
+
requestPollMs?: number;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
39
|
+
clearTimer?: (handle: unknown) => void;
|
|
40
|
+
log?: (msg: string) => void;
|
|
41
|
+
warn?: (msg: string) => void;
|
|
42
|
+
/** Display origin for the status line; defaults to URL origin parsing. */
|
|
43
|
+
peerLabel?: (basePath: string) => string | null;
|
|
44
|
+
}
|
|
45
|
+
export interface SyncTimerHandle {
|
|
46
|
+
stop(): void;
|
|
47
|
+
/** Run the next tick immediately (the "Sync now" button on the dialing half). */
|
|
48
|
+
syncNow(): void;
|
|
49
|
+
/** Mark local writes pending (call after any local op) — debounces a burst. */
|
|
50
|
+
markDirty(): void;
|
|
51
|
+
}
|
|
52
|
+
export declare function startSyncTimer(deps: SyncTimerDeps): SyncTimerHandle;
|
|
53
|
+
//# sourceMappingURL=timer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timer.d.ts","sourceRoot":"","sources":["../../../src/server/sync/timer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAIN,KAAK,cAAc,EAEnB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,eAAO,MAAM,eAAe,QAAS,CAAC;AAGtC,MAAM,WAAW,aAAa;IAC7B,iFAAiF;IACjF,WAAW,EAAE,MAAM;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9D,6DAA6D;IAC7D,IAAI,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1E,6EAA6E;IAC7E,IAAI,EAAE,MAAM,MAAM,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrF,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAChD;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,IAAI,IAAI,CAAC;IACb,iFAAiF;IACjF,OAAO,IAAI,IAAI,CAAC;IAChB,+EAA+E;IAC/E,SAAS,IAAI,IAAI,CAAC;CAClB;AAUD,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,CAiJnE"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-process dialer loop — vault's `syncTimer` generalized. Runs on the dialing
|
|
3
|
+
* half only; a failure NEVER takes the host daemon down (an unsynced instance still
|
|
4
|
+
* serves and still accepts writes — that is the point of two independently-writable
|
|
5
|
+
* instances). The timer is `unref`'d so it never holds the process open.
|
|
6
|
+
*
|
|
7
|
+
* The peer is re-resolved on EVERY tick, so linking (or revoking) a peer takes
|
|
8
|
+
* effect without a restart. The "Sync now" fast lane polls the receiver's request
|
|
9
|
+
* stamp every ~20s while healthy, so a button press on the far side means seconds,
|
|
10
|
+
* not "sometime in the next interval".
|
|
11
|
+
*/
|
|
12
|
+
import { isUnreachable, DEFAULT_LOOP, planNextSync, } from "./planner";
|
|
13
|
+
export const REQUEST_POLL_MS = 20_000;
|
|
14
|
+
const KICKOFF_MS = 5_000;
|
|
15
|
+
function defaultPeerLabel(basePath) {
|
|
16
|
+
try {
|
|
17
|
+
return new URL(basePath).origin;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function startSyncTimer(deps) {
|
|
24
|
+
const now = deps.now ?? Date.now;
|
|
25
|
+
const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
26
|
+
const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
|
|
27
|
+
const loop = deps.loop ?? DEFAULT_LOOP;
|
|
28
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
29
|
+
const warn = deps.warn ?? ((m) => console.error(m));
|
|
30
|
+
const status = deps.status;
|
|
31
|
+
const peerLabel = deps.peerLabel ?? defaultPeerLabel;
|
|
32
|
+
const requestPollMs = deps.requestPollMs ?? REQUEST_POLL_MS;
|
|
33
|
+
status?.update({ enabled: true, role: "dialer" });
|
|
34
|
+
const state = { lastAttempt: 0, consecutiveFailures: 0, dirtySince: null };
|
|
35
|
+
let syncedHead = deps.head();
|
|
36
|
+
let stopped = false;
|
|
37
|
+
let handle = null;
|
|
38
|
+
let reportedUnreachable = false;
|
|
39
|
+
let forceRun = false;
|
|
40
|
+
let handledRequestAt = 0;
|
|
41
|
+
let probing = false;
|
|
42
|
+
let inFlight = false;
|
|
43
|
+
const schedule = (ms) => {
|
|
44
|
+
if (stopped)
|
|
45
|
+
return;
|
|
46
|
+
if (handle !== null)
|
|
47
|
+
clearTimer(handle);
|
|
48
|
+
handle = setTimer(tick, ms);
|
|
49
|
+
handle?.unref?.();
|
|
50
|
+
};
|
|
51
|
+
const sleepFor = (waitMs) => {
|
|
52
|
+
const healthy = state.consecutiveFailures === 0 && requestPollMs > 0;
|
|
53
|
+
return Math.max(1, healthy ? Math.min(waitMs, requestPollMs) : waitMs);
|
|
54
|
+
};
|
|
55
|
+
const probeRequest = (peer) => {
|
|
56
|
+
if (probing || requestPollMs === 0 || !deps.checkRequest)
|
|
57
|
+
return;
|
|
58
|
+
probing = true;
|
|
59
|
+
void deps
|
|
60
|
+
.checkRequest(peer)
|
|
61
|
+
.then((requestedAt) => {
|
|
62
|
+
if (stopped || requestedAt === null || requestedAt <= handledRequestAt)
|
|
63
|
+
return;
|
|
64
|
+
handledRequestAt = requestedAt;
|
|
65
|
+
log("[sync] sync requested from the peer — running now.");
|
|
66
|
+
forceRun = true;
|
|
67
|
+
schedule(1);
|
|
68
|
+
})
|
|
69
|
+
.finally(() => {
|
|
70
|
+
probing = false;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const tick = () => {
|
|
74
|
+
if (stopped)
|
|
75
|
+
return;
|
|
76
|
+
if (inFlight)
|
|
77
|
+
return; // two concurrent runs share cursors and would double-push
|
|
78
|
+
const head = deps.head();
|
|
79
|
+
if (head > syncedHead && state.dirtySince === null)
|
|
80
|
+
state.dirtySince = now();
|
|
81
|
+
const peer = deps.resolvePeer();
|
|
82
|
+
status?.update({
|
|
83
|
+
peer: peer === null ? null : peerLabel(peer.basePath),
|
|
84
|
+
pending: Math.max(0, head - syncedHead),
|
|
85
|
+
});
|
|
86
|
+
if (peer === null) {
|
|
87
|
+
schedule(loop.intervalMs);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const plan = planNextSync(state, now(), loop);
|
|
91
|
+
if (!plan.runNow && !forceRun) {
|
|
92
|
+
const linkedAndHealthy = state.consecutiveFailures === 0 && requestPollMs > 0;
|
|
93
|
+
schedule(sleepFor(plan.waitMs));
|
|
94
|
+
if (linkedAndHealthy)
|
|
95
|
+
probeRequest(peer);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
forceRun = false;
|
|
99
|
+
state.lastAttempt = now();
|
|
100
|
+
status?.update({ lastAttemptAt: state.lastAttempt });
|
|
101
|
+
const headAtAttempt = head;
|
|
102
|
+
inFlight = true;
|
|
103
|
+
deps
|
|
104
|
+
.sync(peer)
|
|
105
|
+
.then((s) => {
|
|
106
|
+
state.consecutiveFailures = 0;
|
|
107
|
+
state.dirtySince = null;
|
|
108
|
+
syncedHead = headAtAttempt;
|
|
109
|
+
reportedUnreachable = false;
|
|
110
|
+
status?.update({
|
|
111
|
+
lastSuccessAt: now(),
|
|
112
|
+
lastOutcome: s.blockedBy ? "paused" : "ok",
|
|
113
|
+
pausedOn: s.blockedBy ?? null,
|
|
114
|
+
consecutiveFailures: 0,
|
|
115
|
+
pending: Math.max(0, deps.head() - syncedHead),
|
|
116
|
+
});
|
|
117
|
+
if (s.pushed > 0 || s.pulled > 0 || s.conflicts > 0) {
|
|
118
|
+
log(`[sync] ↑${s.pushed} ↓${s.pulled}` +
|
|
119
|
+
(s.conflicts > 0 ? ` (${s.conflicts} conflicts)` : ""));
|
|
120
|
+
}
|
|
121
|
+
if (s.blockedBy) {
|
|
122
|
+
log(`[sync] push paused before "${s.blockedBy}" — the peer cannot take it yet (deploy pending?).`);
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.catch((err) => {
|
|
126
|
+
state.consecutiveFailures += 1;
|
|
127
|
+
status?.update({
|
|
128
|
+
lastOutcome: isUnreachable(err) ? "unreachable" : "error",
|
|
129
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
130
|
+
});
|
|
131
|
+
if (isUnreachable(err)) {
|
|
132
|
+
if (!reportedUnreachable) {
|
|
133
|
+
log(`[sync] peer unreachable — will retry. This is normal (nightly window / offline).`);
|
|
134
|
+
reportedUnreachable = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
warn(`[sync] failed (ignored): ${err instanceof Error ? err.message : err}`);
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.finally(() => {
|
|
142
|
+
inFlight = false;
|
|
143
|
+
if (forceRun)
|
|
144
|
+
schedule(1);
|
|
145
|
+
else
|
|
146
|
+
schedule(sleepFor(Math.max(1_000, planNextSync(state, now(), loop).waitMs)));
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
schedule(KICKOFF_MS);
|
|
150
|
+
return {
|
|
151
|
+
stop: () => {
|
|
152
|
+
stopped = true;
|
|
153
|
+
if (handle !== null)
|
|
154
|
+
clearTimer(handle);
|
|
155
|
+
},
|
|
156
|
+
syncNow: () => {
|
|
157
|
+
if (stopped)
|
|
158
|
+
return;
|
|
159
|
+
forceRun = true;
|
|
160
|
+
schedule(1);
|
|
161
|
+
},
|
|
162
|
+
markDirty: () => {
|
|
163
|
+
if (stopped)
|
|
164
|
+
return;
|
|
165
|
+
if (state.dirtySince === null)
|
|
166
|
+
state.dirtySince = now();
|
|
167
|
+
schedule(1);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=timer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timer.js","sourceRoot":"","sources":["../../../src/server/sync/timer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped device tokens — notes' mint/verify/revoke generalized (vault runs a single
|
|
3
|
+
* static env secret; the token TABLE is the better half: revocable per device, scoped
|
|
4
|
+
* per capability, sha256 at rest so a database read never yields a usable credential).
|
|
5
|
+
*
|
|
6
|
+
* The plaintext token is returned exactly once, at mint. Verification compares sha256
|
|
7
|
+
* digests via `timingSafeEqual` — length-independent, no oracle.
|
|
8
|
+
*/
|
|
9
|
+
import type { Database } from "bun:sqlite";
|
|
10
|
+
export interface DeviceToken {
|
|
11
|
+
name: string;
|
|
12
|
+
scope: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
revokedAt: number | null;
|
|
15
|
+
}
|
|
16
|
+
export interface TokenStore {
|
|
17
|
+
/** Mint a token for `name`+`scope`; returns the plaintext ONCE. Re-minting a name
|
|
18
|
+
* revokes the previous token of that name (one live credential per device name). */
|
|
19
|
+
mint(name: string, scope: string): string;
|
|
20
|
+
/** Null when unknown, revoked, or out of scope. */
|
|
21
|
+
verify(presented: string, scope: string): DeviceToken | null;
|
|
22
|
+
revoke(name: string): boolean;
|
|
23
|
+
list(): DeviceToken[];
|
|
24
|
+
}
|
|
25
|
+
export declare function createTokenStore(db: Database): TokenStore;
|
|
26
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../../src/server/sync/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IAC1B;yFACqF;IACrF,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1C,mDAAmD;IACnD,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IAC7D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,IAAI,IAAI,WAAW,EAAE,CAAC;CACtB;AAID,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,UAAU,CA+DzD"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const sha256 = (value) => createHash("sha256").update(value).digest();
|
|
3
|
+
export function createTokenStore(db) {
|
|
4
|
+
db.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS sync_tokens (
|
|
6
|
+
name TEXT PRIMARY KEY,
|
|
7
|
+
scope TEXT NOT NULL,
|
|
8
|
+
token_sha256 BLOB NOT NULL,
|
|
9
|
+
created_at INTEGER NOT NULL,
|
|
10
|
+
revoked_at INTEGER
|
|
11
|
+
);
|
|
12
|
+
`);
|
|
13
|
+
const upsert = db.prepare("INSERT INTO sync_tokens (name, scope, token_sha256, created_at, revoked_at) VALUES (?, ?, ?, ?, NULL) ON CONFLICT(name) DO UPDATE SET scope = excluded.scope, token_sha256 = excluded.token_sha256, created_at = excluded.created_at, revoked_at = NULL");
|
|
14
|
+
const allLive = db.prepare("SELECT name, scope, token_sha256, created_at, revoked_at FROM sync_tokens WHERE revoked_at IS NULL");
|
|
15
|
+
const all = db.prepare("SELECT name, scope, token_sha256, created_at, revoked_at FROM sync_tokens ORDER BY created_at DESC");
|
|
16
|
+
const revokeStmt = db.prepare("UPDATE sync_tokens SET revoked_at = ? WHERE name = ? AND revoked_at IS NULL");
|
|
17
|
+
const toToken = (r) => ({
|
|
18
|
+
name: r.name,
|
|
19
|
+
scope: r.scope,
|
|
20
|
+
createdAt: r.created_at,
|
|
21
|
+
revokedAt: r.revoked_at,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
mint(name, scope) {
|
|
25
|
+
const token = randomBytes(32).toString("base64url");
|
|
26
|
+
upsert.run(name, scope, sha256(token), Date.now());
|
|
27
|
+
return token;
|
|
28
|
+
},
|
|
29
|
+
verify(presented, scope) {
|
|
30
|
+
if (!presented)
|
|
31
|
+
return null;
|
|
32
|
+
const got = sha256(presented);
|
|
33
|
+
// Scan live rows and compare digests constant-time each. The fleet holds a
|
|
34
|
+
// handful of devices; a table scan is simpler than an index that would make
|
|
35
|
+
// the lookup timing depend on the presented value.
|
|
36
|
+
for (const raw of allLive.all()) {
|
|
37
|
+
const want = Buffer.from(raw.token_sha256);
|
|
38
|
+
if (want.length === got.length && timingSafeEqual(want, got)) {
|
|
39
|
+
if (raw.scope !== scope)
|
|
40
|
+
return null;
|
|
41
|
+
return toToken(raw);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
},
|
|
46
|
+
revoke(name) {
|
|
47
|
+
return revokeStmt.run(Date.now(), name).changes > 0;
|
|
48
|
+
},
|
|
49
|
+
list: () => all.all().map(toToken),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../../src/server/sync/tokens.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAmBvE,MAAM,MAAM,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;AAEtF,MAAM,UAAU,gBAAgB,CAAC,EAAY;IAC5C,EAAE,CAAC,IAAI,CAAC;;;;;;;;EAQP,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACxB,yPAAyP,CACzP,CAAC;IACF,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CACzB,oGAAoG,CACpG,CAAC;IACF,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CACrB,oGAAoG,CACpG,CAAC;IACF,MAAM,UAAU,GAAG,EAAE,CAAC,OAAO,CAC5B,6EAA6E,CAC7E,CAAC;IASF,MAAM,OAAO,GAAG,CAAC,CAAM,EAAe,EAAE,CAAC,CAAC;QACzC,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,SAAS,EAAE,CAAC,CAAC,UAAU;QACvB,SAAS,EAAE,CAAC,CAAC,UAAU;KACvB,CAAC,CAAC;IAEH,OAAO;QACN,IAAI,CAAC,IAAI,EAAE,KAAK;YACf,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACpD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YACnD,OAAO,KAAK,CAAC;QACd,CAAC;QACD,MAAM,CAAC,SAAS,EAAE,KAAK;YACtB,IAAI,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAC;YAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;YAC9B,2EAA2E;YAC3E,4EAA4E;YAC5E,mDAAmD;YACnD,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,GAAG,EAAW,EAAE,CAAC;gBAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBAC3C,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;oBAC9D,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK;wBAAE,OAAO,IAAI,CAAC;oBACrC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;gBACrB,CAAC;YACF,CAAC;YACD,OAAO,IAAI,CAAC;QACb,CAAC;QACD,MAAM,CAAC,IAAI;YACV,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAE,GAAG,CAAC,GAAG,EAAY,CAAC,GAAG,CAAC,OAAO,CAAC;KAC7C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire and store types every consumer of this sync engine shares. The op
|
|
3
|
+
* envelope is the notes shape — `(origin, originSeq)` gives exactly-once application
|
|
4
|
+
* with no coordination — and the report is vault's cursor discipline (the reader may
|
|
5
|
+
* advance exactly to `cursor`, never further) merged with notes' `seen` (a duplicate
|
|
6
|
+
* is not an error, it is proof the dedupe ledger works).
|
|
7
|
+
*
|
|
8
|
+
* ── 🔴 "sync-kit" is a PAST NAME. The engine is `cursedbelt/sync` ────────────
|
|
9
|
+
* The retired generation published this as `@satellites/sync-kit`. It came across
|
|
10
|
+
* whole into `apps/vault/src/kit/sync/` when that app graduated, and came here —
|
|
11
|
+
* `cursedbelt/sync` — on 2026-09-15, before `apps/station` could graduate and fork
|
|
12
|
+
* it a second time. That is the move this header used to call an open question in
|
|
13
|
+
* `tasks/`; it is done, and the app-local copies are what is temporary now.
|
|
14
|
+
*
|
|
15
|
+
* `@satellites/sync-kit` is NOT installable and never was — it was never published
|
|
16
|
+
* to npm — and its design plan (`plans/01-sync-kit-adoption-design.md`) belonged to
|
|
17
|
+
* the retired monorepo and did not come across. Comments in consuming apps still say
|
|
18
|
+
* "the sync-kit adoption": that is the NAME OF AN EVENT, the 2026-08 change that
|
|
19
|
+
* moved vault from its own `oplog` table onto this engine's `sync_ops`, and it is
|
|
20
|
+
* worth keeping because the dual-compat code only makes sense in its light.
|
|
21
|
+
*/
|
|
22
|
+
/** One durable mutation in the shared op log. `payload` is app-defined per `kind`. */
|
|
23
|
+
export interface SyncOp {
|
|
24
|
+
/** This instance's monotonic sequence for the op (assigned by the local log). */
|
|
25
|
+
seq: number;
|
|
26
|
+
/** The instance id that BORN the op (stable per instance, never per device). */
|
|
27
|
+
origin: string;
|
|
28
|
+
/** The origin's own monotonic counter — `(origin, originSeq)` is the identity. */
|
|
29
|
+
originSeq: number;
|
|
30
|
+
/** Wall-clock ms when the op was born. LWW inputs, display, nothing structural. */
|
|
31
|
+
ts: number;
|
|
32
|
+
/** App-defined kind, e.g. `doc.set`. The version-negotiation unit. */
|
|
33
|
+
kind: string;
|
|
34
|
+
payload: unknown;
|
|
35
|
+
}
|
|
36
|
+
/** What applying one batch did. `cursor` is the last seq FULLY applied — a stopped
|
|
37
|
+
* batch leaves it just before the failure so the next run resumes there. */
|
|
38
|
+
export interface ApplyReport {
|
|
39
|
+
/** Ops that changed local state. */
|
|
40
|
+
applied: number;
|
|
41
|
+
/** Ops already held (dedupe hit) — counted, never re-applied. */
|
|
42
|
+
seen: number;
|
|
43
|
+
/** Concurrent edits reconciled (however the app's applier chose to). */
|
|
44
|
+
conflicts: number;
|
|
45
|
+
cursor: number;
|
|
46
|
+
/** Non-null when the batch stopped early (version skew, deferred ordering…). */
|
|
47
|
+
error: string | null;
|
|
48
|
+
}
|
|
49
|
+
/** The peer info handshake. `opKinds` is the compatibility advertisement — a dialer
|
|
50
|
+
* stops BEFORE the first op a peer cannot take (notes' version-skew lesson: sending
|
|
51
|
+
* it wedges the push with an error that reads like corruption). */
|
|
52
|
+
export interface PeerInfo {
|
|
53
|
+
instanceId: string;
|
|
54
|
+
head: number;
|
|
55
|
+
opKinds: string[];
|
|
56
|
+
}
|
|
57
|
+
/** The transport seam. Pure interface so tests drive the engine against a REAL second
|
|
58
|
+
* store mounted in a Hono app — no HTTP mocks (the notes/vault discipline). */
|
|
59
|
+
export interface RemoteApi {
|
|
60
|
+
info(): Promise<PeerInfo>;
|
|
61
|
+
pull(after: number, excludeOrigin: string): Promise<{
|
|
62
|
+
ops: SyncOp[];
|
|
63
|
+
cursor: number;
|
|
64
|
+
head: number;
|
|
65
|
+
}>;
|
|
66
|
+
push(payload: {
|
|
67
|
+
ops: SyncOp[];
|
|
68
|
+
peerHas: number;
|
|
69
|
+
}): Promise<ApplyReport>;
|
|
70
|
+
/** The receiver's "someone pressed Sync now here" stamp; null when unsupported. */
|
|
71
|
+
requestedAt?(): Promise<number | null>;
|
|
72
|
+
}
|
|
73
|
+
/** Context the applier gets per batch — vault's concurrency bound. */
|
|
74
|
+
export interface ApplyContext {
|
|
75
|
+
/** The peer's stable instance id. */
|
|
76
|
+
peerId: string;
|
|
77
|
+
/** Max LOCAL-born seq the peer already holds of OURS — an entity whose newest
|
|
78
|
+
* local op is beyond this AND diverges is a true concurrent edit. */
|
|
79
|
+
peerReceivedUpTo: number;
|
|
80
|
+
}
|
|
81
|
+
/** The app's merge logic for ONE op that passed the dedupe ledger. Return value:
|
|
82
|
+
* - `"applied"` — state changed
|
|
83
|
+
* - `"ignored"` — stale/no-op (still recorded in the ledger, cursor advances)
|
|
84
|
+
* - `"conflict"` — applied with a materialized conflict artifact
|
|
85
|
+
* Throw {@link StopBatch} to stop BEFORE this op (cursor stays short of it). */
|
|
86
|
+
export type ApplyOne = (op: SyncOp, ctx: ApplyContext) => "applied" | "ignored" | "conflict";
|
|
87
|
+
/** Stop the current batch before the offending op — deferred ordering, a payload this
|
|
88
|
+
* build cannot hold yet, anything where "retry after the world changes" is right. */
|
|
89
|
+
export declare class StopBatch extends Error {
|
|
90
|
+
constructor(reason: string);
|
|
91
|
+
}
|
|
92
|
+
export interface SyncSummary {
|
|
93
|
+
remoteId: string;
|
|
94
|
+
pushed: number;
|
|
95
|
+
pulled: number;
|
|
96
|
+
conflicts: number;
|
|
97
|
+
/** Ops the export policy held back (counted, never sent). */
|
|
98
|
+
held: number;
|
|
99
|
+
/** Set when the remote runs an older schema and cannot take what comes next. */
|
|
100
|
+
blockedBy?: string;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/server/sync/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,sFAAsF;AACtF,MAAM,WAAW,MAAM;IACtB,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CACjB;AAED;6EAC6E;AAC7E,MAAM,WAAW,WAAW;IAC3B,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED;;oEAEoE;AACpE,MAAM,WAAW,QAAQ;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;gFACgF;AAChF,MAAM,WAAW,SAAS;IACzB,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,IAAI,CACH,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACnB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,IAAI,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACxE,mFAAmF;IACnF,WAAW,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACvC;AAED,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC5B,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf;0EACsE;IACtE,gBAAgB,EAAE,MAAM,CAAC;CACzB;AAED;;;;iFAIiF;AACjF,MAAM,MAAM,QAAQ,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,KAAK,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAE7F;sFACsF;AACtF,qBAAa,SAAU,SAAQ,KAAK;gBACvB,MAAM,EAAE,MAAM;CAI1B;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB"}
|