@cello-protocol/daemon 0.0.228 → 0.0.230

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 (48) hide show
  1. package/dist/channel-collect-tick.d.ts +36 -0
  2. package/dist/channel-collect-tick.d.ts.map +1 -0
  3. package/dist/channel-collect-tick.js +133 -0
  4. package/dist/channel-collect-tick.js.map +1 -0
  5. package/dist/channel-collector.d.ts +99 -0
  6. package/dist/channel-collector.d.ts.map +1 -0
  7. package/dist/channel-collector.js +249 -0
  8. package/dist/channel-collector.js.map +1 -0
  9. package/dist/channel-config-store.d.ts +33 -0
  10. package/dist/channel-config-store.d.ts.map +1 -0
  11. package/dist/channel-config-store.js +61 -0
  12. package/dist/channel-config-store.js.map +1 -0
  13. package/dist/channel-inbox-store.d.ts +46 -0
  14. package/dist/channel-inbox-store.d.ts.map +1 -0
  15. package/dist/channel-inbox-store.js +88 -0
  16. package/dist/channel-inbox-store.js.map +1 -0
  17. package/dist/channel-log-store.d.ts +14 -0
  18. package/dist/channel-log-store.d.ts.map +1 -1
  19. package/dist/channel-log-store.js +58 -0
  20. package/dist/channel-log-store.js.map +1 -1
  21. package/dist/channel-publish-handlers.d.ts +42 -0
  22. package/dist/channel-publish-handlers.d.ts.map +1 -0
  23. package/dist/channel-publish-handlers.js +212 -0
  24. package/dist/channel-publish-handlers.js.map +1 -0
  25. package/dist/channel-publish-wiring.d.ts +41 -0
  26. package/dist/channel-publish-wiring.d.ts.map +1 -0
  27. package/dist/channel-publish-wiring.js +124 -0
  28. package/dist/channel-publish-wiring.js.map +1 -0
  29. package/dist/channel-publisher.d.ts +189 -0
  30. package/dist/channel-publisher.d.ts.map +1 -0
  31. package/dist/channel-publisher.js +394 -0
  32. package/dist/channel-publisher.js.map +1 -0
  33. package/dist/channel-relay-client.d.ts +88 -0
  34. package/dist/channel-relay-client.d.ts.map +1 -0
  35. package/dist/channel-relay-client.js +180 -0
  36. package/dist/channel-relay-client.js.map +1 -0
  37. package/dist/channel-subscription-store.d.ts +66 -0
  38. package/dist/channel-subscription-store.d.ts.map +1 -0
  39. package/dist/channel-subscription-store.js +109 -0
  40. package/dist/channel-subscription-store.js.map +1 -0
  41. package/dist/daemon.d.ts.map +1 -1
  42. package/dist/daemon.js +14 -0
  43. package/dist/daemon.js.map +1 -1
  44. package/dist/gateway-config-handlers.d.ts +2 -2
  45. package/dist/gateway-config-handlers.d.ts.map +1 -1
  46. package/dist/gateway-config-handlers.js +37 -12
  47. package/dist/gateway-config-handlers.js.map +1 -1
  48. package/package.json +5 -5
@@ -0,0 +1,36 @@
1
+ import type { Logger } from "./types.js";
2
+ import type { ChannelCollector } from "./channel-collector.js";
3
+ import type { ChannelSubscriptionStore } from "./channel-subscription-store.js";
4
+ /** See the header: bounded by the shortest retention any channel may configure. */
5
+ export declare const COLLECT_TICK_INTERVAL_MS: number;
6
+ /** The widest the pubkey-derived offset can push a channel's tick. */
7
+ export declare const COLLECT_JITTER_MS = 60000;
8
+ /** A failing channel is backed off to at most this, then stays there until it succeeds. */
9
+ export declare const COLLECT_MAX_BACKOFF_MS: number;
10
+ export interface ChannelCollectTickDeps {
11
+ logger: Logger;
12
+ collector: ChannelCollector;
13
+ subscriptions: ChannelSubscriptionStore;
14
+ /**
15
+ * Whether this agent is ONLINE. Collecting for an agent the operator switched off is the kill
16
+ * switch failing to switch something off — the same rule the trust-signal tick follows.
17
+ */
18
+ isAgentOnline: (agentId: string) => boolean;
19
+ intervalMs?: number;
20
+ jitterMs?: number;
21
+ maxBackoffMs?: number;
22
+ }
23
+ export interface ChannelCollectTicker {
24
+ /** Begin collecting. Idempotent: a second call does not stack a second timer. */
25
+ start: () => void;
26
+ /** One pass over every active subscription. Exposed so a test drives it without a clock. */
27
+ collectAllDue: (now: number) => Promise<void>;
28
+ stop: () => void;
29
+ }
30
+ /**
31
+ * A stable offset in [0, jitterMs) for a channel, from its public key. Same channel, same
32
+ * subscriber, same offset every time — see the header for why that beats a fresh random each tick.
33
+ */
34
+ export declare function jitterForChannel(channelHex: string, agentId: string, jitterMs: number): number;
35
+ export declare function createChannelCollectTicker(deps: ChannelCollectTickDeps): ChannelCollectTicker;
36
+ //# sourceMappingURL=channel-collect-tick.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-collect-tick.d.ts","sourceRoot":"","sources":["../src/channel-collect-tick.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,QAAa,CAAC;AACnD,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,QAAS,CAAC;AACxC,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,QAAc,CAAC;AAElD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,gBAAgB,CAAC;IAC5B,aAAa,EAAE,wBAAwB,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,4FAA4F;IAC5F,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI9F;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,sBAAsB,GAAG,oBAAoB,CAgF7F"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * M16 018-PUBCOLLECT — what actually RUNS the collector.
3
+ *
4
+ * ⚠️ **WITHOUT THIS, SUBSCRIBING DOES NOTHING.** `ChannelCollector` fetches when it is called, and
5
+ * a subscriber has nobody to call it: there is no inbound event for a channel post — the relay holds
6
+ * a queue and waits to be asked. A collector nobody schedules is a subscriber who never receives
7
+ * anything, with every component reporting healthy. The publishing half shipped wired and this half
8
+ * did not, which is the same hole twice in one milestone.
9
+ *
10
+ * ─── The interval, and why it is bounded by retention ────────────────────────────────────────
11
+ *
12
+ * A relay drops posts on its own schedule. If the gap between collections is longer than a channel's
13
+ * retention, a subscriber can miss a post that was delivered, held for its full life and swept —
14
+ * never seeing it, and never recording a gap either, because the number is below the floor by the
15
+ * time anyone looks. The interval therefore has to stay well under the shortest retention any
16
+ * channel uses, and the default retention is seven days. Five minutes is three orders of magnitude
17
+ * inside that, which is the margin this needs to be safe against a channel configured far shorter.
18
+ *
19
+ * ─── Jitter is derived from the channel key, not random ──────────────────────────────────────
20
+ *
21
+ * Every subscriber to a popular channel would otherwise poll its two relays at the same moment —
22
+ * the relays see the entire audience arrive at once, every interval, for ever. The offset is
23
+ * derived from the channel's public key so it is STABLE for a given subscriber: a random offset
24
+ * re-rolled each tick spreads the load just as well but makes the cadence unpredictable to the
25
+ * operator watching it, and makes a slow channel impossible to distinguish from a jittery one.
26
+ *
27
+ * ─── Backoff is per channel, and only on FAILURE ─────────────────────────────────────────────
28
+ *
29
+ * A channel whose relays are both down is asked less and less often, up to a ceiling, so a dead
30
+ * channel costs little. Any success resets it immediately: the point is to stop hammering something
31
+ * broken, not to punish a channel for a bad afternoon.
32
+ */
33
+ import { createHash } from "node:crypto";
34
+ import { extractErrorMessage } from "./error-message.js";
35
+ /** See the header: bounded by the shortest retention any channel may configure. */
36
+ export const COLLECT_TICK_INTERVAL_MS = 5 * 60_000;
37
+ /** The widest the pubkey-derived offset can push a channel's tick. */
38
+ export const COLLECT_JITTER_MS = 60_000;
39
+ /** A failing channel is backed off to at most this, then stays there until it succeeds. */
40
+ export const COLLECT_MAX_BACKOFF_MS = 30 * 60_000;
41
+ /**
42
+ * A stable offset in [0, jitterMs) for a channel, from its public key. Same channel, same
43
+ * subscriber, same offset every time — see the header for why that beats a fresh random each tick.
44
+ */
45
+ export function jitterForChannel(channelHex, agentId, jitterMs) {
46
+ if (jitterMs <= 0)
47
+ return 0;
48
+ const digest = createHash("sha256").update(`${agentId}:${channelHex}`, "utf8").digest();
49
+ return digest.readUInt32BE(0) % jitterMs;
50
+ }
51
+ export function createChannelCollectTicker(deps) {
52
+ const { logger, collector, subscriptions, isAgentOnline } = deps;
53
+ const intervalMs = deps.intervalMs ?? COLLECT_TICK_INTERVAL_MS;
54
+ const jitterMs = deps.jitterMs ?? COLLECT_JITTER_MS;
55
+ const maxBackoffMs = deps.maxBackoffMs ?? COLLECT_MAX_BACKOFF_MS;
56
+ /** When each subscription is next due, and how far it has been backed off. */
57
+ const nextDue = new Map();
58
+ const backoff = new Map();
59
+ /**
60
+ * Subscriptions with a pass still running. A tick that lands on one is SKIPPED rather than
61
+ * queued: a slow relay would otherwise stack passes that all fetch the same posts, and each one
62
+ * holds a database write.
63
+ */
64
+ const inFlight = new Set();
65
+ let timer = null;
66
+ const keyOf = (agentId, channelHex) => `${agentId}:${channelHex}`;
67
+ async function collectAllDue(now) {
68
+ for (const sub of subscriptions.active()) {
69
+ const key = keyOf(sub.agent_id, sub.channel_pubkey);
70
+ if (inFlight.has(key))
71
+ continue;
72
+ // Checked PER SUBSCRIPTION, not once for the pass: one agent being offline must not stop
73
+ // another agent's channels from collecting.
74
+ if (!isAgentOnline(sub.agent_id))
75
+ continue;
76
+ const due = nextDue.get(key);
77
+ if (due === undefined) {
78
+ // First sight of this subscription: collect now, then settle into the jittered cadence. A
79
+ // new subscriber waiting a full interval for its first post looks broken and is.
80
+ nextDue.set(key, now);
81
+ }
82
+ else if (now < due) {
83
+ continue;
84
+ }
85
+ inFlight.add(key);
86
+ try {
87
+ await collector.collectOnce(sub.agent_id, sub.channel_pubkey);
88
+ await collector.repairGaps(sub.agent_id, sub.channel_pubkey);
89
+ backoff.delete(key);
90
+ nextDue.set(key, now + intervalMs + jitterForChannel(sub.channel_pubkey, sub.agent_id, jitterMs));
91
+ }
92
+ catch (err) {
93
+ // ⚠️ LOGGED, never swallowed and never fatal to the loop. A background collection that fails
94
+ // in silence is a subscriber who simply stops receiving, with nothing anywhere saying why —
95
+ // and one channel's bad relay must not stop every other channel on this daemon.
96
+ const next = Math.min(maxBackoffMs, (backoff.get(key) ?? intervalMs) * 2);
97
+ backoff.set(key, next);
98
+ nextDue.set(key, now + next + jitterForChannel(sub.channel_pubkey, sub.agent_id, jitterMs));
99
+ logger.warn("channel.collect.tick_failed", {
100
+ channel_pubkey: sub.channel_pubkey,
101
+ reason: extractErrorMessage(err),
102
+ next_attempt_in_ms: next,
103
+ });
104
+ }
105
+ finally {
106
+ inFlight.delete(key);
107
+ }
108
+ }
109
+ }
110
+ return {
111
+ start() {
112
+ if (timer !== null)
113
+ return;
114
+ // The pass runs every interval and decides per subscription what is due; the jitter lives in
115
+ // the due times rather than in the timer, so one slow channel cannot drag the others.
116
+ timer = setInterval(() => {
117
+ void collectAllDue(Date.now()).catch((err) => {
118
+ logger.warn("channel.collect.pass_failed", { reason: extractErrorMessage(err) });
119
+ });
120
+ }, intervalMs);
121
+ // Never hold the process open for a background poll.
122
+ timer.unref?.();
123
+ logger.info("channel.collect.started", { interval_ms: intervalMs, jitter_ms: jitterMs });
124
+ },
125
+ collectAllDue,
126
+ stop() {
127
+ if (timer !== null)
128
+ clearInterval(timer);
129
+ timer = null;
130
+ },
131
+ };
132
+ }
133
+ //# sourceMappingURL=channel-collect-tick.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-collect-tick.js","sourceRoot":"","sources":["../src/channel-collect-tick.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * ChannelCollector — M16 018-PUBCOLLECT, the subscriber half.
3
+ *
4
+ * A channel's two relays are **not coordinated**. Each holds its own queue, they disagree by design
5
+ * — different `first_held_seq`, different `last_seq`, different subsets — and none of that is an
6
+ * error. The subscriber fetches from both and takes the union.
7
+ *
8
+ * ⚠️ **EVERYTHING IS VERIFIED HERE.** Both signatures, the title rules, the size, and that the
9
+ * publishing agent is the admin THIS SUBSCRIBER knows for the channel. A relay's word that a post is
10
+ * good is worth nothing: the relay is a witness, not an authority, and the whole design assumes it
11
+ * may be hostile.
12
+ *
13
+ * ─── Two positions, and why a fetch may only move one ────────────────────────────────────────
14
+ *
15
+ * delivered_through how far the daemon has fetched and verified — moved here
16
+ * processed_through how far the AGENT has read — never touched here
17
+ *
18
+ * ─── The contiguous run ──────────────────────────────────────────────────────────────────────
19
+ *
20
+ * `delivered_through` advances only across an unbroken run. Holding 1, 2 and 4 leaves it at 2 —
21
+ * because advancing to 4 would mean post 3 is never delivered when it finally arrives, the position
22
+ * having already passed it. The gap is reported instead.
23
+ *
24
+ * ─── Forks ───────────────────────────────────────────────────────────────────────────────────
25
+ *
26
+ * Two different posts at one number means the channel signed both. Both are kept, the fork is
27
+ * reported with both hashes, and the position stops below it. Nothing is resolved automatically:
28
+ * picking one would make whichever relay answered first the arbiter of what the channel said.
29
+ */
30
+ import type { Logger } from "./types.js";
31
+ import { type ChannelAccess } from "@cello-protocol/protocol-types";
32
+ import { ChannelInboxStore } from "./channel-inbox-store.js";
33
+ import { ChannelSubscriptionStore } from "./channel-subscription-store.js";
34
+ export type RelayFetchSeam = (relay: string, req: {
35
+ channel_pubkey: Uint8Array;
36
+ since_seq: number;
37
+ max_bytes: number;
38
+ auth?: {
39
+ signature: Uint8Array;
40
+ time_ms: number;
41
+ };
42
+ agent_pubkeys?: Uint8Array[];
43
+ }) => Promise<{
44
+ ok: true;
45
+ posts: Array<{
46
+ seq: number;
47
+ post_cbor: Uint8Array;
48
+ receipt_cbor?: Uint8Array;
49
+ }>;
50
+ first_held_seq: number | null;
51
+ last_seq: number | null;
52
+ } | {
53
+ ok: false;
54
+ reason: string;
55
+ }>;
56
+ export interface ChannelCollectorOptions {
57
+ logger: Logger;
58
+ subscriptions: ChannelSubscriptionStore;
59
+ inbox: ChannelInboxStore;
60
+ fetch: RelayFetchSeam;
61
+ /** The fetch-key signature for a non-public channel. 019 owns the key; `undefined` means public. */
62
+ fetchAuth: (access: ChannelAccess, channelHex: string, sinceSeq: number) => Promise<{
63
+ signature: Uint8Array;
64
+ time_ms: number;
65
+ } | undefined>;
66
+ /** Local agent keys to declare for the reader count. Best-effort, and never affects delivery. */
67
+ localAgentKeys: (channelHex: string) => Uint8Array[];
68
+ /** Ask the PUBLISHER to re-deposit a range. It answers by re-depositing, never by sending posts. */
69
+ requestRepair: (agentId: string, channelHex: string, from: number, to: number) => Promise<void>;
70
+ now?: () => number;
71
+ maxBytesPerFetch?: number;
72
+ }
73
+ export interface GapReport {
74
+ missing: number[];
75
+ first_held_seq: number;
76
+ }
77
+ export declare class ChannelCollector {
78
+ #private;
79
+ constructor(opts: ChannelCollectorOptions);
80
+ /** One pass over one channel: fetch both relays, verify, store, advance. */
81
+ collectOnce(agentId: string, channelHex: string, correlationId?: string): Promise<void>;
82
+ /**
83
+ * What is missing between the relays' floor and the highest post seen.
84
+ *
85
+ * ⚠️ **BELOW `first_held_seq` IS NEVER A GAP.** Those posts were pruned or aged out on purpose.
86
+ * Reporting them would have every subscriber demand content that no longer exists, for ever.
87
+ */
88
+ gapsFor(agentId: string, channelHex: string): GapReport;
89
+ /**
90
+ * Repair what is still missing: the OTHER relay first, then the publisher.
91
+ *
92
+ * The other relay is asked first because it costs nothing and usually has it — the two queues
93
+ * disagree by design. The publisher is a last resort and is asked ONCE per gap: a gap that
94
+ * survives both is reported, not re-requested every tick, or a permanently pruned post becomes a
95
+ * permanent request loop.
96
+ */
97
+ repairGaps(agentId: string, channelHex: string, correlationId?: string): Promise<void>;
98
+ }
99
+ //# sourceMappingURL=channel-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-collector.d.ts","sourceRoot":"","sources":["../src/channel-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAK3E,MAAM,MAAM,cAAc,GAAG,CAC3B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE;IACH,cAAc,EAAE,UAAU,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QAAE,SAAS,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,aAAa,CAAC,EAAE,UAAU,EAAE,CAAC;CAC9B,KACE,OAAO,CACR;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,UAAU,CAAC;QAAC,YAAY,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACrJ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAChC,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,wBAAwB,CAAC;IACxC,KAAK,EAAE,iBAAiB,CAAC;IACzB,KAAK,EAAE,cAAc,CAAC;IACtB,oGAAoG;IACpG,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,SAAS,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IAC5I,iGAAiG;IACjG,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,UAAU,EAAE,CAAC;IACrD,oGAAoG;IACpG,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChG,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,gBAAgB;;gBAmBf,IAAI,EAAE,uBAAuB;IAKzC,4EAA4E;IACtE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyI7F;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IAyCvD;;;;;;;OAOG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA6B7F"}
@@ -0,0 +1,249 @@
1
+ import { broadcastPostHash, decodeBroadcastArtifact, validateBroadcastTitle, verifyBroadcastArtifact, } from "@cello-protocol/protocol-types";
2
+ /** At most this many agent keys ride a fetch, per the relay's own cap (017). */
3
+ const MAX_AGENT_KEYS_PER_FETCH = 32;
4
+ export class ChannelCollector {
5
+ #opts;
6
+ #now;
7
+ /** Relay-reported `first_held_seq` per channel — the floor below which absence is not a gap. */
8
+ #firstHeld = new Map();
9
+ /**
10
+ * Gaps already reported to the publisher, so a survivor is not re-requested every tick.
11
+ *
12
+ * ⚠️ KEYED ON `agentId:channel`, NOT on the channel. Two agents on this daemon can subscribe to
13
+ * the same channel, and one key per channel means the second agent's genuine gap is silently never
14
+ * requested once the first has asked — its posts simply never arrive, with nothing logged.
15
+ */
16
+ #repairRequested = new Map();
17
+ /** The one place the repair key is built, so the two halves cannot drift apart. */
18
+ static #repairKey(agentId, channelHex) {
19
+ return `${agentId}:${channelHex}`;
20
+ }
21
+ constructor(opts) {
22
+ this.#opts = opts;
23
+ this.#now = opts.now ?? (() => Date.now());
24
+ }
25
+ /** One pass over one channel: fetch both relays, verify, store, advance. */
26
+ async collectOnce(agentId, channelHex, correlationId) {
27
+ const { logger, subscriptions, inbox } = this.#opts;
28
+ const sub = subscriptions.get(agentId, channelHex);
29
+ if (!sub || sub.status !== "active")
30
+ return;
31
+ const since = sub.delivered_through + 1;
32
+ const auth = await this.#opts.fetchAuth(sub.access, channelHex, since);
33
+ const agentKeys = this.#opts.localAgentKeys(channelHex).slice(0, MAX_AGENT_KEYS_PER_FETCH);
34
+ const channelPubkey = new Uint8Array(Buffer.from(channelHex, "hex"));
35
+ let lowestFirstHeld = null;
36
+ for (const relay of sub.relays) {
37
+ let answer;
38
+ try {
39
+ answer = await this.#opts.fetch(relay, {
40
+ channel_pubkey: channelPubkey,
41
+ since_seq: since,
42
+ max_bytes: this.#opts.maxBytesPerFetch ?? 4 * 1024 * 1024,
43
+ ...(auth ? { auth } : {}),
44
+ ...(agentKeys.length > 0 ? { agent_pubkeys: agentKeys } : {}),
45
+ });
46
+ }
47
+ catch (err) {
48
+ // ⚠️ ONE RELAY DOWN IS NOT A FAILED COLLECTION. The other still has the posts, which is the
49
+ // entire reason a channel publishes to two.
50
+ logger.warn("channel.fetch.failed", {
51
+ ...(correlationId !== undefined ? { correlationId } : {}),
52
+ channel_pubkey: channelHex, relay, reason: extract(err),
53
+ });
54
+ continue;
55
+ }
56
+ if (!answer.ok) {
57
+ logger.warn("channel.fetch.failed", {
58
+ ...(correlationId !== undefined ? { correlationId } : {}),
59
+ channel_pubkey: channelHex, relay, reason: answer.reason,
60
+ });
61
+ continue;
62
+ }
63
+ if (answer.first_held_seq !== null) {
64
+ lowestFirstHeld = lowestFirstHeld === null ? answer.first_held_seq : Math.min(lowestFirstHeld, answer.first_held_seq);
65
+ }
66
+ let stored = 0;
67
+ for (const entry of answer.posts) {
68
+ const post = this.#verify(agentId, channelHex, sub.admin_pubkey, entry.post_cbor, relay, correlationId);
69
+ if (!post)
70
+ continue;
71
+ const result = inbox.store(agentId, channelHex, post, {
72
+ ...(entry.receipt_cbor ? { receiptCbor: entry.receipt_cbor } : {}),
73
+ fromRelay: relay,
74
+ collectedAt: this.#now(),
75
+ });
76
+ if (result.stored)
77
+ stored += 1;
78
+ }
79
+ logger.info("channel.fetch.completed", {
80
+ ...(correlationId !== undefined ? { correlationId } : {}),
81
+ // `sub` was read BEFORE the fetch, so its position is the one we started from. Naming it
82
+ // `delivered_through` made the log say the position had not moved on every successful
83
+ // collection — the advance happens below, after every relay has been read.
84
+ channel_pubkey: channelHex, relay, count: stored, delivered_through_before: sub.delivered_through,
85
+ });
86
+ }
87
+ if (lowestFirstHeld !== null)
88
+ this.#firstHeld.set(channelHex, lowestFirstHeld);
89
+ this.#advance(agentId, channelHex, correlationId);
90
+ this.#reportGaps(agentId, channelHex, correlationId);
91
+ }
92
+ /**
93
+ * Verify a post before it is stored, and name the check that failed.
94
+ *
95
+ * The admin check is against the admin THIS SUBSCRIBER knows from its subscription — not against
96
+ * anything the relay said. A relay that swapped a post for one signed by another agent is exactly
97
+ * what this refuses.
98
+ */
99
+ #verify(agentId, channelHex, adminPubkeyHex, cbor, relay, correlationId) {
100
+ const drop = (check, detail) => {
101
+ this.#opts.logger.warn("channel.post.invalid", {
102
+ ...(correlationId !== undefined ? { correlationId } : {}),
103
+ channel_pubkey: channelHex, relay, check, detail,
104
+ });
105
+ return null;
106
+ };
107
+ const decoded = decodeBroadcastArtifact(cbor);
108
+ if (!decoded.ok)
109
+ return drop("decode", decoded.reason);
110
+ const post = decoded.artifact;
111
+ if (Buffer.from(post.channel_pubkey).toString("hex") !== channelHex.toLowerCase()) {
112
+ return drop("channel_pubkey", "the post belongs to a different channel");
113
+ }
114
+ const verdict = verifyBroadcastArtifact(post);
115
+ if (!verdict.ok)
116
+ return drop("signature", verdict.reason);
117
+ const title = validateBroadcastTitle(post.title);
118
+ if (!title.ok)
119
+ return drop("title", title.detail);
120
+ if (Buffer.from(post.agent_pubkey).toString("hex") !== adminPubkeyHex.toLowerCase()) {
121
+ return drop("admin", "the publishing agent is not the admin this subscriber knows for the channel");
122
+ }
123
+ return post;
124
+ }
125
+ /** Advance over the contiguous run, stopping below any gap and below any fork. */
126
+ #advance(agentId, channelHex, correlationId) {
127
+ const { subscriptions, inbox, logger } = this.#opts;
128
+ const sub = subscriptions.get(agentId, channelHex);
129
+ if (!sub)
130
+ return;
131
+ const held = new Set(inbox.heldSeqs(agentId, channelHex));
132
+ const forked = new Set(inbox.forkedSeqs(agentId, channelHex));
133
+ for (const seq of forked) {
134
+ const versions = inbox.forksFor(agentId, channelHex, seq);
135
+ logger.warn("channel.fork.detected", {
136
+ ...(correlationId !== undefined ? { correlationId } : {}),
137
+ channel_pubkey: channelHex,
138
+ seq,
139
+ // Both hashes, so the publisher can be confronted with exactly what it signed.
140
+ hashes: versions.map((v) => Buffer.from(broadcastPostHash(v)).toString("hex")),
141
+ });
142
+ }
143
+ // Start from the first post this subscriber could possibly hold: either the next one it expects,
144
+ // or the relay's floor when everything below has been pruned away.
145
+ const floor = this.#firstHeld.get(channelHex);
146
+ let next = sub.delivered_through + 1;
147
+ if (floor !== undefined && floor > next && !held.has(next))
148
+ next = floor;
149
+ let advanced = sub.delivered_through;
150
+ while (held.has(next) && !forked.has(next)) {
151
+ advanced = next;
152
+ next += 1;
153
+ }
154
+ if (advanced > sub.delivered_through) {
155
+ subscriptions.setDeliveredThrough(agentId, channelHex, advanced);
156
+ }
157
+ }
158
+ /**
159
+ * What is missing between the relays' floor and the highest post seen.
160
+ *
161
+ * ⚠️ **BELOW `first_held_seq` IS NEVER A GAP.** Those posts were pruned or aged out on purpose.
162
+ * Reporting them would have every subscriber demand content that no longer exists, for ever.
163
+ */
164
+ gapsFor(agentId, channelHex) {
165
+ const { subscriptions, inbox } = this.#opts;
166
+ const sub = subscriptions.get(agentId, channelHex);
167
+ const held = inbox.heldSeqs(agentId, channelHex);
168
+ /**
169
+ * ⚠️ **THE RELAY'S REPORTED FLOOR WINS OUTRIGHT, and is never lowered by anything local.**
170
+ *
171
+ * The floor used to be the minimum of the relay's floor, the lowest post held, and
172
+ * `delivered_through + 1`. Take a relay that pruned 1–4 and serves 5, 6, 7, where post 5 fails
173
+ * verification: nothing is held below 6, `delivered_through` stays 0, and the minimum is 1 — so
174
+ * the subscriber demands posts 1 to 5 from a publisher that pruned four of them months ago, and
175
+ * keeps demanding them for ever. That is precisely what "below the floor is never a gap" forbids.
176
+ *
177
+ * Only when NO relay has reported a floor do the local positions stand in for one.
178
+ */
179
+ const reported = this.#firstHeld.get(channelHex);
180
+ const localCandidates = [held[0], (sub?.delivered_through ?? 0) + 1]
181
+ .filter((n) => typeof n === "number" && n > 0);
182
+ const first_held_seq = reported !== undefined && reported > 0
183
+ ? reported
184
+ : (localCandidates.length > 0 ? Math.min(...localCandidates) : 1);
185
+ if (held.length === 0)
186
+ return { missing: [], first_held_seq };
187
+ const highest = held[held.length - 1];
188
+ const have = new Set(held);
189
+ const missing = [];
190
+ for (let seq = first_held_seq; seq < highest; seq++) {
191
+ if (!have.has(seq))
192
+ missing.push(seq);
193
+ }
194
+ return { missing, first_held_seq };
195
+ }
196
+ #reportGaps(agentId, channelHex, correlationId) {
197
+ const { missing, first_held_seq } = this.gapsFor(agentId, channelHex);
198
+ if (missing.length === 0)
199
+ return;
200
+ this.#opts.logger.warn("channel.gap.detected", {
201
+ ...(correlationId !== undefined ? { correlationId } : {}),
202
+ channel_pubkey: channelHex, missing, first_held_seq,
203
+ });
204
+ }
205
+ /**
206
+ * Repair what is still missing: the OTHER relay first, then the publisher.
207
+ *
208
+ * The other relay is asked first because it costs nothing and usually has it — the two queues
209
+ * disagree by design. The publisher is a last resort and is asked ONCE per gap: a gap that
210
+ * survives both is reported, not re-requested every tick, or a permanently pruned post becomes a
211
+ * permanent request loop.
212
+ */
213
+ async repairGaps(agentId, channelHex, correlationId) {
214
+ const sub = this.#opts.subscriptions.get(agentId, channelHex);
215
+ if (!sub || sub.status !== "active")
216
+ return;
217
+ // A fresh pass over both relays is the "ask the other relay" step: whatever either holds now is
218
+ // collected and the gap closes without troubling the publisher.
219
+ await this.collectOnce(agentId, channelHex, correlationId);
220
+ const { missing } = this.gapsFor(agentId, channelHex);
221
+ if (missing.length === 0)
222
+ return;
223
+ const repairKey = ChannelCollector.#repairKey(agentId, channelHex);
224
+ let asked = this.#repairRequested.get(repairKey);
225
+ if (!asked) {
226
+ asked = new Set();
227
+ this.#repairRequested.set(repairKey, asked);
228
+ }
229
+ const fresh = missing.filter((seq) => !asked.has(seq));
230
+ if (fresh.length === 0)
231
+ return;
232
+ for (const seq of fresh)
233
+ asked.add(seq);
234
+ const from = Math.min(...fresh);
235
+ const to = Math.max(...fresh);
236
+ this.#opts.logger.info("channel.repair.requested", {
237
+ ...(correlationId !== undefined ? { correlationId } : {}),
238
+ channel_pubkey: channelHex, from, to,
239
+ });
240
+ await this.#opts.requestRepair(agentId, channelHex, from, to);
241
+ }
242
+ }
243
+ function extract(err) {
244
+ if (err instanceof Error)
245
+ return err.message;
246
+ const m = err?.message;
247
+ return typeof m === "string" ? m : JSON.stringify(err);
248
+ }
249
+ //# sourceMappingURL=channel-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-collector.js","sourceRoot":"","sources":["../src/channel-collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ChannelConfigStore — what a PUBLISHER has decided about its own channel (M16 018-PUBCOLLECT).
3
+ *
4
+ * The mirror image of `channel-subscription-store.ts`: that one is what a subscriber knows about
5
+ * somebody else's channel, this is what a publisher has chosen for its own — which two relays it
6
+ * publishes to, whether the channel is public, what it is for, how long posts are kept.
7
+ *
8
+ * ⚠️ **THE RELAY PAIR LIVES HERE, NOT IN THE INFO RECORD.** The record is the SIGNED STATEMENT of
9
+ * these facts, deposited for subscribers to read; this is the source it is signed from. Deriving the
10
+ * pair from the last record instead would mean a publisher that lost its record could no longer
11
+ * publish, and a relay serving a stale record could quietly redirect one.
12
+ *
13
+ * Keyed on the channel's own pubkey.
14
+ */
15
+ import type { DaemonDatabase } from "./sqlcipher-db.js";
16
+ import type { Logger } from "./types.js";
17
+ import type { ChannelAccess } from "@cello-protocol/protocol-types";
18
+ /** Seven days, matching the relay's own default retention. */
19
+ export declare const DEFAULT_RETENTION_SECONDS: number;
20
+ export declare const CHANNEL_CONFIG_CREATE_SQL: string;
21
+ export interface ChannelConfig {
22
+ access: ChannelAccess;
23
+ relays: string[];
24
+ guidance: string;
25
+ retention_seconds: number;
26
+ }
27
+ export declare class ChannelConfigStore {
28
+ #private;
29
+ constructor(db: DaemonDatabase, logger: Logger);
30
+ get(channelHex: string): ChannelConfig | null;
31
+ set(channelHex: string, config: ChannelConfig, now: number): void;
32
+ }
33
+ //# sourceMappingURL=channel-config-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-config-store.d.ts","sourceRoot":"","sources":["../src/channel-config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEpE,8DAA8D;AAC9D,eAAO,MAAM,yBAAyB,QAAmB,CAAC;AAE1D,eAAO,MAAM,yBAAyB,QAWrC,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,kBAAkB;;gBAIjB,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM;IAM9C,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IA0B7C,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAiBlE"}