cursedbelt 2.6.0 → 2.7.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 (66) hide show
  1. package/dist/server/sync/alarm.d.ts +35 -0
  2. package/dist/server/sync/alarm.d.ts.map +1 -0
  3. package/dist/server/sync/alarm.js +92 -0
  4. package/dist/server/sync/alarm.js.map +1 -0
  5. package/dist/server/sync/commands.d.ts +88 -0
  6. package/dist/server/sync/commands.d.ts.map +1 -0
  7. package/dist/server/sync/commands.js +242 -0
  8. package/dist/server/sync/commands.js.map +1 -0
  9. package/dist/server/sync/engine.d.ts +63 -0
  10. package/dist/server/sync/engine.d.ts.map +1 -0
  11. package/dist/server/sync/engine.js +185 -0
  12. package/dist/server/sync/engine.js.map +1 -0
  13. package/dist/server/sync/http.d.ts +107 -0
  14. package/dist/server/sync/http.d.ts.map +1 -0
  15. package/dist/server/sync/http.js +244 -0
  16. package/dist/server/sync/http.js.map +1 -0
  17. package/dist/server/sync/index.d.ts +42 -0
  18. package/dist/server/sync/index.d.ts.map +1 -0
  19. package/dist/server/sync/index.js +42 -0
  20. package/dist/server/sync/index.js.map +1 -0
  21. package/dist/server/sync/opLog.d.ts +62 -0
  22. package/dist/server/sync/opLog.d.ts.map +1 -0
  23. package/dist/server/sync/opLog.js +97 -0
  24. package/dist/server/sync/opLog.js.map +1 -0
  25. package/dist/server/sync/planner.d.ts +32 -0
  26. package/dist/server/sync/planner.d.ts.map +1 -0
  27. package/dist/server/sync/planner.js +31 -0
  28. package/dist/server/sync/planner.js.map +1 -0
  29. package/dist/server/sync/status.d.ts +64 -0
  30. package/dist/server/sync/status.d.ts.map +1 -0
  31. package/dist/server/sync/status.js +50 -0
  32. package/dist/server/sync/status.js.map +1 -0
  33. package/dist/server/sync/timer.d.ts +53 -0
  34. package/dist/server/sync/timer.d.ts.map +1 -0
  35. package/dist/server/sync/timer.js +171 -0
  36. package/dist/server/sync/timer.js.map +1 -0
  37. package/dist/server/sync/tokens.d.ts +26 -0
  38. package/dist/server/sync/tokens.d.ts.map +1 -0
  39. package/dist/server/sync/tokens.js +52 -0
  40. package/dist/server/sync/tokens.js.map +1 -0
  41. package/dist/server/sync/types.d.ts +102 -0
  42. package/dist/server/sync/types.d.ts.map +1 -0
  43. package/dist/server/sync/types.js +30 -0
  44. package/dist/server/sync/types.js.map +1 -0
  45. package/package.json +19 -7
  46. package/src/leafSubpathsImportNothing.spec.ts +59 -3
  47. package/src/server/sync/alarm.spec.ts +149 -0
  48. package/src/server/sync/alarm.ts +137 -0
  49. package/src/server/sync/commands.spec.ts +145 -0
  50. package/src/server/sync/commands.ts +361 -0
  51. package/src/server/sync/engine.spec.ts +496 -0
  52. package/src/server/sync/engine.ts +255 -0
  53. package/src/server/sync/http.ts +316 -0
  54. package/src/server/sync/httpRemote.spec.ts +158 -0
  55. package/src/server/sync/index.ts +90 -0
  56. package/src/server/sync/opLog.spec.ts +110 -0
  57. package/src/server/sync/opLog.ts +189 -0
  58. package/src/server/sync/planner.spec.ts +53 -0
  59. package/src/server/sync/planner.ts +62 -0
  60. package/src/server/sync/status.spec.ts +92 -0
  61. package/src/server/sync/status.ts +108 -0
  62. package/src/server/sync/timer.spec.ts +150 -0
  63. package/src/server/sync/timer.ts +207 -0
  64. package/src/server/sync/tokens.spec.ts +38 -0
  65. package/src/server/sync/tokens.ts +94 -0
  66. package/src/server/sync/types.ts +108 -0
@@ -0,0 +1,185 @@
1
+ import { StopBatch, } from "./types";
2
+ const BATCH = 500;
3
+ /** Kit-owned dedupe + ledger insert around the app's per-op merge logic. Everything
4
+ * for one op happens in one transaction: state change AND ledger insert, so a crash
5
+ * can never apply without recording (double-apply) or record without applying
6
+ * (silent drop). */
7
+ export function createSyncApplier(log, applyOne) {
8
+ return (ops, ctx) => {
9
+ let applied = 0;
10
+ let seen = 0;
11
+ let conflicts = 0;
12
+ let cursor = ops.length > 0 ? ops[0].seq - 1 : 0;
13
+ let error = null;
14
+ for (const op of ops) {
15
+ if (log.has(op.origin, op.originSeq)) {
16
+ seen++;
17
+ cursor = op.seq;
18
+ continue;
19
+ }
20
+ try {
21
+ const outcome = log.transaction(() => {
22
+ const result = applyOne(op, ctx);
23
+ log.recordRemote(op);
24
+ return result;
25
+ });
26
+ if (outcome === "applied")
27
+ applied++;
28
+ else if (outcome === "conflict") {
29
+ applied++;
30
+ conflicts++;
31
+ }
32
+ cursor = op.seq;
33
+ }
34
+ catch (err) {
35
+ if (err instanceof StopBatch) {
36
+ error = err.message;
37
+ break;
38
+ }
39
+ throw err;
40
+ }
41
+ }
42
+ return { applied, seen, conflicts, cursor, error };
43
+ };
44
+ }
45
+ const DEFAULT_MAX_PUSH_BYTES = 1_000_000;
46
+ /** Split ops into request-sized chunks by serialized payload weight. */
47
+ export function chunkByBytes(ops, maxBytes) {
48
+ const chunks = [];
49
+ let current = [];
50
+ let weight = 0;
51
+ for (const op of ops) {
52
+ const size = JSON.stringify(op).length + 1;
53
+ if (current.length > 0 && weight + size > maxBytes) {
54
+ chunks.push(current);
55
+ current = [];
56
+ weight = 0;
57
+ }
58
+ current.push(op);
59
+ weight += size;
60
+ }
61
+ if (current.length > 0)
62
+ chunks.push(current);
63
+ return chunks;
64
+ }
65
+ export async function pushToRemote(opts, remote) {
66
+ const info = await remote.info();
67
+ const remoteId = info.instanceId;
68
+ const remoteKinds = new Set(info.opKinds);
69
+ // One predicate for both compatibility layers, so the "stop before it, leave the
70
+ // cursor there" path is shared and a caller cannot accidentally give the finer
71
+ // check the weaker (drop-it) semantics.
72
+ const refusedBecause = (op) => {
73
+ if (!remoteKinds.has(op.kind))
74
+ return op.kind;
75
+ return opts.peerRefusal?.(op, info) ?? null;
76
+ };
77
+ const { log } = opts;
78
+ const peerHas = log.cursor(remoteId, "pull");
79
+ let cursor = log.cursor(remoteId, "push");
80
+ let pushed = 0;
81
+ let held = 0;
82
+ // Conflicts the RECEIVER detected while applying our push — push-before-pull means
83
+ // exactly one side detects any given concurrency, and when that side is the
84
+ // receiver the count would otherwise vanish from the dialer's summary.
85
+ let conflicts = 0;
86
+ let blockedBy;
87
+ for (;;) {
88
+ const page = log.listAfter(cursor, remoteId, BATCH);
89
+ if (page.ops.length === 0) {
90
+ // Nothing sendable in the window — advance past the peer's own reflected ops.
91
+ if (page.cursor > cursor) {
92
+ cursor = page.cursor;
93
+ log.setCursor(remoteId, "push", cursor);
94
+ }
95
+ break;
96
+ }
97
+ const blockedAt = page.ops.findIndex((op) => refusedBecause(op) !== null);
98
+ const sendableRaw = blockedAt === -1 ? page.ops : page.ops.slice(0, blockedAt);
99
+ if (blockedAt !== -1) {
100
+ const op = page.ops[blockedAt];
101
+ blockedBy = refusedBecause(op) ?? op.kind;
102
+ }
103
+ const outbound = [];
104
+ for (const op of sendableRaw) {
105
+ const filtered = opts.policy ? opts.policy(op) : op;
106
+ if (filtered === null)
107
+ held++;
108
+ else
109
+ outbound.push(filtered);
110
+ }
111
+ if (outbound.length > 0) {
112
+ // Chunked by bytes so a large backlog cannot trip the proxy's body cap;
113
+ // the cursor advances per DELIVERED chunk, so an interrupted push resumes
114
+ // at the failure with nothing lost or re-sent.
115
+ for (const chunk of chunkByBytes(outbound, opts.maxPushBytes ?? DEFAULT_MAX_PUSH_BYTES)) {
116
+ const report = await remote.push({ ops: chunk, peerHas });
117
+ pushed += report.applied;
118
+ conflicts += report.conflicts;
119
+ if (report.error !== null) {
120
+ // The remote stopped inside the chunk (its own ordering guard). Advance
121
+ // to what it durably took and stop; the next run retries from there.
122
+ const processed = report.applied + report.seen;
123
+ if (processed > 0) {
124
+ cursor = chunk[processed - 1].seq;
125
+ log.setCursor(remoteId, "push", cursor);
126
+ }
127
+ return { remoteId, pushed, held, conflicts, blockedBy: report.error };
128
+ }
129
+ cursor = chunk[chunk.length - 1].seq;
130
+ log.setCursor(remoteId, "push", cursor);
131
+ }
132
+ }
133
+ if (sendableRaw.length === 0)
134
+ break; // the very next op is blocked — nothing to advance past
135
+ cursor = sendableRaw[sendableRaw.length - 1].seq;
136
+ log.setCursor(remoteId, "push", cursor);
137
+ if (blockedBy)
138
+ break;
139
+ if (page.cursor >= page.head && blockedAt === -1) {
140
+ // Window served through head with nothing blocked — take the page cursor so
141
+ // trailing excluded ops don't re-page forever.
142
+ cursor = page.cursor;
143
+ log.setCursor(remoteId, "push", cursor);
144
+ break;
145
+ }
146
+ }
147
+ return { remoteId, pushed, held, conflicts, blockedBy };
148
+ }
149
+ export async function pullFromRemote(opts, remote, applyBatch) {
150
+ const info = await remote.info();
151
+ const remoteId = info.instanceId;
152
+ const { log } = opts;
153
+ const self = log.instanceId();
154
+ const peerReceivedUpTo = log.cursor(remoteId, "push");
155
+ let cursor = log.cursor(remoteId, "pull");
156
+ let pulled = 0;
157
+ let conflicts = 0;
158
+ for (;;) {
159
+ const page = await remote.pull(cursor, self);
160
+ if (page.ops.length === 0 && page.cursor <= cursor)
161
+ break;
162
+ const report = applyBatch(page.ops, { peerId: remoteId, peerReceivedUpTo });
163
+ pulled += report.applied;
164
+ conflicts += report.conflicts;
165
+ if (report.error !== null) {
166
+ if (report.cursor > cursor) {
167
+ cursor = report.cursor;
168
+ log.setCursor(remoteId, "pull", cursor);
169
+ }
170
+ break;
171
+ }
172
+ cursor = page.ops.length === 0 ? page.cursor : Math.max(report.cursor, page.cursor);
173
+ log.setCursor(remoteId, "pull", cursor);
174
+ if (page.cursor >= page.head)
175
+ break;
176
+ }
177
+ return { remoteId, pulled, conflicts };
178
+ }
179
+ /** One full reconciliation: push, then pull. */
180
+ export async function runSync(opts, remote, applyBatch) {
181
+ const push = await pushToRemote(opts, remote);
182
+ const pull = await pullFromRemote(opts, remote, applyBatch);
183
+ return { ...push, ...pull, conflicts: push.conflicts + pull.conflicts, remoteId: push.remoteId };
184
+ }
185
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/server/sync/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The HTTP transport: the receiver sub-app the internet-facing instance mounts, and
3
+ * the fetch-backed {@link RemoteApi} the dialer binds to.
4
+ *
5
+ * Mount the receiver OUTSIDE any browser-session gate (a machine peer has no session;
6
+ * notes' 401 trap: a token route registered after the session gate answers 401 with a
7
+ * valid token) but behind the token gate built here. An unconfigured receiver simply
8
+ * does not exist — the orch-companion idiom: no token verifier, no routes, nothing to
9
+ * probe.
10
+ */
11
+ import { Hono } from "hono";
12
+ import type { OpLog } from "./opLog";
13
+ import type { ApplyOne, ApplyReport, RemoteApi } from "./types";
14
+ /**
15
+ * Trim a page to the byte budget, keeping it a PREFIX so `cursor` stays the seq
16
+ * of the last op actually returned. Anything else silently drops ops: the caller
17
+ * advances past rows it never received and no later pull will offer them again.
18
+ */
19
+ export declare function capPageBytes<T extends {
20
+ seq: number;
21
+ }>(ops: T[], maxBytes: number): {
22
+ ops: T[];
23
+ truncated: boolean;
24
+ };
25
+ export interface ReceiverHooks {
26
+ /** Every authenticated hit — the only evidence a non-dialing half has that it is
27
+ * in step (vault's `lastPeerContactAt` lesson). */
28
+ onPeerContact?: (peerId: string) => void;
29
+ /** The "Sync now was pressed HERE" stamp the dialer polls. */
30
+ readRequest?: () => number | null;
31
+ }
32
+ export interface ReceiverOptions {
33
+ log: OpLog;
34
+ opKinds: readonly string[];
35
+ /** Verify a presented bearer token for the sync scope. Return false to refuse.
36
+ * Bind this to a TokenStore (`(t) => tokens.verify(t, "sync") !== null`) or a
37
+ * constant-time static compare — the kit does not care which. */
38
+ verifyToken: (presented: string) => boolean;
39
+ /** The app's merge logic, when it is stateless across a batch (vault, station).
40
+ * Give this OR {@link beginBatch}, never both. */
41
+ applyOne?: ApplyOne;
42
+ /**
43
+ * The app's merge logic when the batch needs state of its own — called once per
44
+ * `POST /push`, so what it closes over belongs to that batch alone.
45
+ *
46
+ * 🔴 A single shared `ApplyOne` closing over a mutable accumulator is the bug this
47
+ * exists to make unrepresentable: two pushes in flight would pour into one counter,
48
+ * and the second response would carry the first's leftovers. notes needs it — its
49
+ * applier collects the attachment blobs a purge orphaned and the items whose search
50
+ * index must be rebuilt, neither of which can happen inside the op's transaction.
51
+ */
52
+ beginBatch?: () => ApplyOne;
53
+ /**
54
+ * Post-commit work, awaited BEFORE the response goes out — an index rebuild, a blob
55
+ * delete that is a network call. Anything that must not run inside the applier's
56
+ * transaction but must be done by the time the caller sees a 200, or the caller will
57
+ * advance its cursor past work that never happened.
58
+ */
59
+ afterBatch?: (report: ApplyReport) => void | Promise<void>;
60
+ hooks?: ReceiverHooks;
61
+ }
62
+ /** Build the receiver: GET /info, GET /pull, POST /push, GET /requested. */
63
+ export declare function createSyncReceiver(options: ReceiverOptions): Hono;
64
+ /**
65
+ * The fetch-backed remote the dialer binds to. `basePath` is where the peer
66
+ * mounted its receiver (e.g. `https://station.cursedalchemy.com/api/sync`).
67
+ *
68
+ * 🔴 **Redirects are REFUSED, never followed** (2026-08-21). `fetch` follows by
69
+ * default, and that turned a moved hostname into eight days of silence: the
70
+ * console's subdomain cutover made `station.cursedalchemy.com` a **301** to
71
+ * `station.cursedalchemy.com`, the dialer followed it, Cloudflare Access answered
72
+ * the un-credentialed redirect with its **login page — HTTP 200, text/html**,
73
+ * and `r.json()` threw `Failed to parse JSON`. The dialer logs that as
74
+ * `[sync] failed (ignored)` and retries forever, so:
75
+ *
76
+ * · every mirrored report on prod froze at the moment of the cutover, while
77
+ * each one still rendered a confident `capturedAt` age;
78
+ * · nothing was red anywhere — not a gate, not a smoke, not `/healthz`;
79
+ * · the error named JSON, which is three layers away from the cause.
80
+ *
81
+ * A 3xx here can only ever mean the peer address is wrong, so it is reported as
82
+ * exactly that, with the destination in the message. Following a redirect
83
+ * silently is how a sync survives its own misconfiguration.
84
+ *
85
+ * The same reasoning covers a 2xx that is not JSON: an Access login page, an
86
+ * nginx error page or a captive portal all arrive as `200 text/html`, and
87
+ * "Failed to parse JSON" is the least useful sentence available about any of
88
+ * them. {@link readJson} names the status, the content type and a snippet.
89
+ */
90
+ export declare function createHttpRemote(options: {
91
+ basePath: string;
92
+ token: string;
93
+ selfId: string;
94
+ fetchImpl?: typeof fetch;
95
+ timeoutMs?: number;
96
+ /**
97
+ * Extra headers for THIS peer's host — the Cloudflare Access service token,
98
+ * where the receiver sits behind an Access application. Host-scoped BY THE
99
+ * CALLER — never a blanket wrapper: a credential sent where it is not needed is a
100
+ * credential leaked. The retired generation had a `satellite-kit/cloudflare-access`
101
+ * helper that did the host-scoping; no caller in this app passes this today
102
+ * (`syncClient.ts` handles Access by refusing to follow its 302 instead), so a
103
+ * future one owns the scoping itself.
104
+ */
105
+ extraHeaders?: Record<string, string>;
106
+ }): RemoteApi;
107
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/server/sync/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAgB,QAAQ,EAAE,WAAW,EAAY,SAAS,EAAU,MAAM,SAAS,CAAC;AA2BhG;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACrD,GAAG,EAAE,CAAC,EAAE,EACR,QAAQ,EAAE,MAAM,GACd;IAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAOlC;AASD,MAAM,WAAW,aAAa;IAC7B;wDACoD;IACpD,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC/B,GAAG,EAAE,KAAK,CAAC;IACX,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;sEAEkE;IAClE,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C;uDACmD;IACnD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,QAAQ,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAmFjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC,GAAG,SAAS,CAqFZ"}
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The HTTP transport: the receiver sub-app the internet-facing instance mounts, and
3
+ * the fetch-backed {@link RemoteApi} the dialer binds to.
4
+ *
5
+ * Mount the receiver OUTSIDE any browser-session gate (a machine peer has no session;
6
+ * notes' 401 trap: a token route registered after the session gate answers 401 with a
7
+ * valid token) but behind the token gate built here. An unconfigured receiver simply
8
+ * does not exist — the orch-companion idiom: no token verifier, no routes, nothing to
9
+ * probe.
10
+ */
11
+ import { Hono } from "hono";
12
+ import { createSyncApplier } from "./engine";
13
+ const noStore = { "cache-control": "no-store" };
14
+ const SYNC_PAGE = 500;
15
+ /**
16
+ * Byte budget for ONE pull page — the mirror of `maxPushBytes` on the push side,
17
+ * and it was missing.
18
+ *
19
+ * 🔴 Measured against live prod on 2026-08-01: `GET /pull?after=0` against
20
+ * station's 709-op log served **37 MB at 309 ops** and then OOM-killed the unit
21
+ * (`memoryMax=300M` on a t3.micro). `SYNC_PAGE = 500` is a COUNT cap, and these
22
+ * ops are not small — a 130 KB overview snapshot, a 68 KB fleet CSV — so 500 of
23
+ * them is tens of megabytes serialized into one response buffer.
24
+ *
25
+ * What makes it worse than a slow request is that it does not converge: the unit
26
+ * dies, systemd restarts it, the dialer retries the identical oversized pull, and
27
+ * the pair never advances. Push already learned this exact lesson (a 413 wedges
28
+ * because the batch never shrinks); the receiving half kept only the count cap
29
+ * because a fully-caught-up peer never asks for enough rows to notice.
30
+ *
31
+ * A single op larger than the budget is still served alone — refusing it would
32
+ * wedge the log permanently on that op, which is worse than one big response.
33
+ */
34
+ const SYNC_PAGE_BYTES = 4_000_000;
35
+ /**
36
+ * Trim a page to the byte budget, keeping it a PREFIX so `cursor` stays the seq
37
+ * of the last op actually returned. Anything else silently drops ops: the caller
38
+ * advances past rows it never received and no later pull will offer them again.
39
+ */
40
+ export function capPageBytes(ops, maxBytes) {
41
+ let weight = 0;
42
+ for (let i = 0; i < ops.length; i++) {
43
+ weight += JSON.stringify(ops[i]).length + 1;
44
+ if (weight > maxBytes && i > 0)
45
+ return { ops: ops.slice(0, i), truncated: true };
46
+ }
47
+ return { ops, truncated: false };
48
+ }
49
+ /** Coerce and clamp the caller's peer id — a cursor key and a label, never trusted
50
+ * further than that. */
51
+ function peerIdOf(header, query) {
52
+ const raw = query ?? header ?? "";
53
+ return /^[0-9A-Za-z_-]{1,64}$/.test(raw) ? raw : "peer";
54
+ }
55
+ /** Build the receiver: GET /info, GET /pull, POST /push, GET /requested. */
56
+ export function createSyncReceiver(options) {
57
+ const { log, verifyToken } = options;
58
+ if ((options.applyOne === undefined) === (options.beginBatch === undefined)) {
59
+ // Loud at construction, because both mistakes are silent at runtime: neither
60
+ // given means every op vanishes as "ignored"; both given means one of the two
61
+ // is quietly never called.
62
+ throw new Error("[vault sync] receiver: give exactly one of `applyOne` or `beginBatch`");
63
+ }
64
+ const beginBatch = options.beginBatch ?? (() => options.applyOne);
65
+ const api = new Hono();
66
+ api.use("*", async (c, next) => {
67
+ const header = c.req.header("authorization") ?? "";
68
+ const presented = header.startsWith("Bearer ") ? header.slice(7) : "";
69
+ if (presented.length === 0 || !verifyToken(presented)) {
70
+ return c.json({ error: "unauthorized" }, 401, noStore);
71
+ }
72
+ await next();
73
+ });
74
+ // AFTER the gate — an unauthenticated caller must never look like peer contact.
75
+ api.use("*", async (c, next) => {
76
+ options.hooks?.onPeerContact?.(peerIdOf(c.req.header("x-sync-peer"), c.req.query("peer")));
77
+ await next();
78
+ });
79
+ api.get("/info", (c) => {
80
+ const body = {
81
+ instanceId: log.instanceId(),
82
+ head: log.head(),
83
+ opKinds: [...options.opKinds],
84
+ };
85
+ return c.json(body, 200, noStore);
86
+ });
87
+ api.get("/requested", (c) => c.json({ requestedAt: options.hooks?.readRequest?.() ?? null }, 200, noStore));
88
+ api.get("/pull", (c) => {
89
+ const afterRaw = c.req.query("after");
90
+ const after = afterRaw !== undefined && /^\d+$/.test(afterRaw) ? Number(afterRaw) : 0;
91
+ const excludeRaw = c.req.query("exclude") ?? "";
92
+ const exclude = /^[0-9A-Za-z_-]{1,64}$/.test(excludeRaw) ? excludeRaw : "";
93
+ const page = log.listAfter(after, exclude, SYNC_PAGE);
94
+ // Count cap first (cheap, bounds the serialize below), then the byte cap.
95
+ const capped = capPageBytes(page.ops, SYNC_PAGE_BYTES);
96
+ // The cursor must name the last op ACTUALLY served, or the peer skips the
97
+ // remainder forever. `head` is untouched — it is how the caller knows to
98
+ // come back for another page.
99
+ const cursor = capped.truncated
100
+ ? (capped.ops.at(-1)?.seq ?? after)
101
+ : page.cursor;
102
+ log.setCursor(peerIdOf(c.req.header("x-sync-peer"), c.req.query("peer")), "served", cursor);
103
+ return c.json({ ops: capped.ops, cursor, head: page.head }, 200, noStore);
104
+ });
105
+ api.post("/push", async (c) => {
106
+ let body;
107
+ try {
108
+ body = await c.req.json();
109
+ }
110
+ catch {
111
+ return c.json({ error: "bad_request" }, 400, noStore);
112
+ }
113
+ if (body === null || typeof body !== "object")
114
+ return c.json({ error: "bad_request" }, 400, noStore);
115
+ const { ops, peerHas } = body;
116
+ if (!Array.isArray(ops))
117
+ return c.json({ error: "bad_request" }, 400, noStore);
118
+ const peerId = peerIdOf(c.req.header("x-sync-peer"), c.req.query("peer"));
119
+ // The client's DURABLE pull cursor for us is the crash-safe concurrency bound;
120
+ // fall back to our served cursor if an older caller omits it (vault's rule).
121
+ const bound = typeof peerHas === "number" && Number.isInteger(peerHas) && peerHas >= 0
122
+ ? peerHas
123
+ : log.cursor(peerId, "served");
124
+ const ctx = { peerId, peerReceivedUpTo: bound };
125
+ const report = createSyncApplier(log, beginBatch())(ops, ctx);
126
+ // Awaited, not fire-and-forget: the caller advances its push cursor on this
127
+ // response, so work still outstanding when it lands is work nobody will redo.
128
+ await options.afterBatch?.(report);
129
+ return c.json(report, report.error === null ? 200 : 409, noStore);
130
+ });
131
+ return api;
132
+ }
133
+ /**
134
+ * The fetch-backed remote the dialer binds to. `basePath` is where the peer
135
+ * mounted its receiver (e.g. `https://station.cursedalchemy.com/api/sync`).
136
+ *
137
+ * 🔴 **Redirects are REFUSED, never followed** (2026-08-21). `fetch` follows by
138
+ * default, and that turned a moved hostname into eight days of silence: the
139
+ * console's subdomain cutover made `station.cursedalchemy.com` a **301** to
140
+ * `station.cursedalchemy.com`, the dialer followed it, Cloudflare Access answered
141
+ * the un-credentialed redirect with its **login page — HTTP 200, text/html**,
142
+ * and `r.json()` threw `Failed to parse JSON`. The dialer logs that as
143
+ * `[sync] failed (ignored)` and retries forever, so:
144
+ *
145
+ * · every mirrored report on prod froze at the moment of the cutover, while
146
+ * each one still rendered a confident `capturedAt` age;
147
+ * · nothing was red anywhere — not a gate, not a smoke, not `/healthz`;
148
+ * · the error named JSON, which is three layers away from the cause.
149
+ *
150
+ * A 3xx here can only ever mean the peer address is wrong, so it is reported as
151
+ * exactly that, with the destination in the message. Following a redirect
152
+ * silently is how a sync survives its own misconfiguration.
153
+ *
154
+ * The same reasoning covers a 2xx that is not JSON: an Access login page, an
155
+ * nginx error page or a captive portal all arrive as `200 text/html`, and
156
+ * "Failed to parse JSON" is the least useful sentence available about any of
157
+ * them. {@link readJson} names the status, the content type and a snippet.
158
+ */
159
+ export function createHttpRemote(options) {
160
+ const base = options.basePath.replace(/\/$/, "");
161
+ const doFetch = options.fetchImpl ?? fetch;
162
+ const timeout = options.timeoutMs ?? 60_000;
163
+ const peer = encodeURIComponent(options.selfId);
164
+ const request = async (path, init) => {
165
+ const response = await doFetch(`${base}${path}`, {
166
+ ...init,
167
+ // 🔴 See the header note: a followed redirect is how this sync spent
168
+ // eight days reporting a JSON parse error about an Access login page.
169
+ redirect: "manual",
170
+ headers: {
171
+ authorization: `Bearer ${options.token}`,
172
+ "x-sync-peer": options.selfId,
173
+ ...(options.extraHeaders ?? {}),
174
+ ...(init?.headers ?? {}),
175
+ },
176
+ signal: AbortSignal.timeout(timeout),
177
+ });
178
+ if (response.status >= 300 && response.status < 400) {
179
+ throw new Error(`${path}: ${response.status} — the peer address redirects to ` +
180
+ `${response.headers.get("location") ?? "an unnamed destination"}. ` +
181
+ `The sync URL points at a host that has moved; point it at the real one.`);
182
+ }
183
+ if (response.status === 401) {
184
+ throw new Error(`${path}: 401 — the sync token was refused.`);
185
+ }
186
+ return response;
187
+ };
188
+ /**
189
+ * Read a JSON body, or say what actually arrived.
190
+ *
191
+ * A 200 carrying `text/html` is a login page or an error page, and it is the
192
+ * shape every "the address is wrong" failure takes once a redirect has been
193
+ * followed. Naming the content type and the first line turns an unactionable
194
+ * `Failed to parse JSON` into the cause.
195
+ */
196
+ const readJson = async (r, path) => {
197
+ const text = await r.text();
198
+ try {
199
+ return JSON.parse(text);
200
+ }
201
+ catch {
202
+ const type = r.headers.get("content-type") ?? "no content-type";
203
+ throw new Error(`${path}: ${r.status} answered ${type}, not JSON — ` +
204
+ `${text.trim().slice(0, 120).replace(/\s+/g, " ") || "(empty body)"}`);
205
+ }
206
+ };
207
+ return {
208
+ async info() {
209
+ const r = await request("/info");
210
+ if (!r.ok)
211
+ throw new Error(`sync/info: ${r.status}`);
212
+ return await readJson(r, "sync/info");
213
+ },
214
+ async pull(after, excludeOrigin) {
215
+ const r = await request(`/pull?after=${after}&exclude=${encodeURIComponent(excludeOrigin)}&peer=${peer}`);
216
+ if (!r.ok)
217
+ throw new Error(`sync/pull: ${r.status}`);
218
+ return await readJson(r, "sync/pull");
219
+ },
220
+ async push(payload) {
221
+ const r = await request(`/push?peer=${peer}`, {
222
+ method: "POST",
223
+ headers: { "content-type": "application/json" },
224
+ body: JSON.stringify(payload),
225
+ });
226
+ if (!r.ok && r.status !== 409)
227
+ throw new Error(`sync/push: ${r.status}`);
228
+ return await readJson(r, "sync/push");
229
+ },
230
+ async requestedAt() {
231
+ try {
232
+ const r = await request("/requested");
233
+ if (!r.ok)
234
+ return null;
235
+ const body = await readJson(r, "sync/requested");
236
+ return typeof body.requestedAt === "number" ? body.requestedAt : null;
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ },
242
+ };
243
+ }
244
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/server/sync/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `cursedbelt/sync` — the op-log replication engine, shared rather than forked.
3
+ *
4
+ * ## Why it is here and not in the app
5
+ *
6
+ * The retired tree had FOUR consumers of this engine: `apps/vault` (8 files),
7
+ * `apps/station` (6), `apps/notes` (4) and `packages/feedback-kit` (5). `vault`
8
+ * graduated first and carried all 3,245 lines in whole, as
9
+ * `apps/vault/src/kit/sync/`, because a Phase A that also publishes a library is a
10
+ * Phase A that does not land. This is the other half of that trade, and it had a
11
+ * DEADLINE rather than a backlog slot: the moment `apps/station` graduates with the
12
+ * copy still in `vault`, the generation owns two divergent forks of a replication
13
+ * engine — the exact shape the owner ruled against on 2026-09-13, *"if we don't
14
+ * adapt to this at the right time then it will be hard to remember to keep updating
15
+ * multiple copies as the apps move and grow."*
16
+ *
17
+ * ## 🔴 A LEAF export, and it must stay one
18
+ *
19
+ * `cursedbelt/sync` resolves straight to this directory. It is deliberately NOT
20
+ * reachable through the `cursedbelt/server` barrel, which drags `kysely` and
21
+ * `otplib` behind it — an app that wants an op-log should not install two
22
+ * databases to get one. The only runtime dependency beneath this subpath is
23
+ * `hono` (for the receiver routes in `http.ts` and `commands.ts`); everything else
24
+ * is `bun:sqlite`, `node:crypto` and `node:fs`.
25
+ *
26
+ * It lives under `src/server/` because that is where this package's two typecheck
27
+ * projects put Bun's ambient types: the root `tsconfig.json` runs `types: []` to
28
+ * keep the browser/React surface pristine and EXCLUDES `src/server`, so
29
+ * `bun:sqlite` and `node:crypto` only resolve on this side of the split. The
30
+ * directory decides the types; the `exports` key decides the spelling.
31
+ */
32
+ export { StopBatch, type ApplyContext, type ApplyOne, type ApplyReport, type PeerInfo, type RemoteApi, type SyncOp, type SyncSummary, } from "./types";
33
+ export { createOpLog, type OpLog } from "./opLog";
34
+ export { createTokenStore, type DeviceToken, type TokenStore } from "./tokens";
35
+ export { createSyncApplier, pushToRemote, pullFromRemote, runSync, type EngineOptions, type ExportPolicy, type PeerRefusal, } from "./engine";
36
+ export { capPageBytes, createHttpRemote, createSyncReceiver, type ReceiverHooks, type ReceiverOptions, } from "./http";
37
+ export { DEFAULT_LOOP, isUnreachable, planNextSync, type SyncLoopConfig, type SyncLoopState, } from "./planner";
38
+ export { OFFLINE_STATUS, PEER_CONTACT_WRITE_MS, createSyncStatusReporter, type SyncStatus, type SyncStatusReporter, type SyncStatusStore, } from "./status";
39
+ export { REQUEST_POLL_MS, startSyncTimer, type SyncTimerDeps, type SyncTimerHandle } from "./timer";
40
+ export { createSyncAlarm, type AlarmOutcome, type SyncAlarm, type SyncAlarmOptions } from "./alarm";
41
+ export { DEFAULT_HOLD_MS, createCommandQueue, createCommandReceiver, createCommandRemote, startCommandWorker, type CommandQueue, type CommandRemote, type CommandRoutesOptions, type CommandStatus, type CommandWorkerDeps, type CommandWorkerHandle, type SyncCommand, } from "./commands";
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/sync/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EACN,SAAS,EACT,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,WAAW,GAChB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAC/E,OAAO,EACN,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,GAChB,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,eAAe,GACpB,MAAM,QAAQ,CAAC;AAChB,OAAO,EACN,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,aAAa,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACN,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EACN,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GAChB,MAAM,YAAY,CAAC"}