@cello-protocol/daemon 0.0.229 → 0.0.231

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 (77) hide show
  1. package/dist/channel-admin-lookup.d.ts +54 -0
  2. package/dist/channel-admin-lookup.d.ts.map +1 -0
  3. package/dist/channel-admin-lookup.js +122 -0
  4. package/dist/channel-admin-lookup.js.map +1 -0
  5. package/dist/channel-collect-tick.d.ts +36 -0
  6. package/dist/channel-collect-tick.d.ts.map +1 -0
  7. package/dist/channel-collect-tick.js +133 -0
  8. package/dist/channel-collect-tick.js.map +1 -0
  9. package/dist/channel-collector.d.ts +99 -0
  10. package/dist/channel-collector.d.ts.map +1 -0
  11. package/dist/channel-collector.js +249 -0
  12. package/dist/channel-collector.js.map +1 -0
  13. package/dist/channel-config-store.d.ts +36 -0
  14. package/dist/channel-config-store.d.ts.map +1 -0
  15. package/dist/channel-config-store.js +84 -0
  16. package/dist/channel-config-store.js.map +1 -0
  17. package/dist/channel-inbox-store.d.ts +46 -0
  18. package/dist/channel-inbox-store.d.ts.map +1 -0
  19. package/dist/channel-inbox-store.js +88 -0
  20. package/dist/channel-inbox-store.js.map +1 -0
  21. package/dist/channel-join-exchange.d.ts +87 -0
  22. package/dist/channel-join-exchange.d.ts.map +1 -0
  23. package/dist/channel-join-exchange.js +289 -0
  24. package/dist/channel-join-exchange.js.map +1 -0
  25. package/dist/channel-log-store.d.ts +14 -0
  26. package/dist/channel-log-store.d.ts.map +1 -1
  27. package/dist/channel-log-store.js +58 -0
  28. package/dist/channel-log-store.js.map +1 -1
  29. package/dist/channel-membership-store.d.ts +118 -0
  30. package/dist/channel-membership-store.d.ts.map +1 -0
  31. package/dist/channel-membership-store.js +226 -0
  32. package/dist/channel-membership-store.js.map +1 -0
  33. package/dist/channel-membership-wiring.d.ts +90 -0
  34. package/dist/channel-membership-wiring.d.ts.map +1 -0
  35. package/dist/channel-membership-wiring.js +387 -0
  36. package/dist/channel-membership-wiring.js.map +1 -0
  37. package/dist/channel-publish-handlers.d.ts +42 -0
  38. package/dist/channel-publish-handlers.d.ts.map +1 -0
  39. package/dist/channel-publish-handlers.js +212 -0
  40. package/dist/channel-publish-handlers.js.map +1 -0
  41. package/dist/channel-publish-wiring.d.ts +52 -0
  42. package/dist/channel-publish-wiring.d.ts.map +1 -0
  43. package/dist/channel-publish-wiring.js +143 -0
  44. package/dist/channel-publish-wiring.js.map +1 -0
  45. package/dist/channel-publisher.d.ts +221 -0
  46. package/dist/channel-publisher.d.ts.map +1 -0
  47. package/dist/channel-publisher.js +430 -0
  48. package/dist/channel-publisher.js.map +1 -0
  49. package/dist/channel-relay-client.d.ts +88 -0
  50. package/dist/channel-relay-client.d.ts.map +1 -0
  51. package/dist/channel-relay-client.js +180 -0
  52. package/dist/channel-relay-client.js.map +1 -0
  53. package/dist/channel-subscription-store.d.ts +137 -0
  54. package/dist/channel-subscription-store.d.ts.map +1 -0
  55. package/dist/channel-subscription-store.js +250 -0
  56. package/dist/channel-subscription-store.js.map +1 -0
  57. package/dist/daemon.d.ts.map +1 -1
  58. package/dist/daemon.js +54 -0
  59. package/dist/daemon.js.map +1 -1
  60. package/dist/inbound-sessions.d.ts +6 -0
  61. package/dist/inbound-sessions.d.ts.map +1 -1
  62. package/dist/inbound-sessions.js +27 -1
  63. package/dist/inbound-sessions.js.map +1 -1
  64. package/dist/refusal-reasons.d.ts +7 -0
  65. package/dist/refusal-reasons.d.ts.map +1 -1
  66. package/dist/refusal-reasons.js +15 -0
  67. package/dist/refusal-reasons.js.map +1 -1
  68. package/dist/session-content-context.d.ts +8 -0
  69. package/dist/session-content-context.d.ts.map +1 -1
  70. package/dist/session-content-ingest.d.ts.map +1 -1
  71. package/dist/session-content-ingest.js +28 -0
  72. package/dist/session-content-ingest.js.map +1 -1
  73. package/dist/session-node-manager.d.ts +4 -0
  74. package/dist/session-node-manager.d.ts.map +1 -1
  75. package/dist/session-node-manager.js +11 -0
  76. package/dist/session-node-manager.js.map +1 -1
  77. package/package.json +5 -5
@@ -0,0 +1,54 @@
1
+ /**
2
+ * M16 020-CHANADMIN — asking a directory who administers a channel.
3
+ *
4
+ * A subscriber that has been handed a group key must check the agent that handed it over against
5
+ * what the directory says about the channel. The session proves who the counterparty IS; it says
6
+ * nothing about whether they have any authority over this channel. Without the check, any agent that
7
+ * can open a session with you becomes your channel.
8
+ *
9
+ * 019 built the check and it could not run: the answer lives in the directory's profile and no
10
+ * client-facing frame carried it. `channel_admin_query` (020, directory side) does, on the
11
+ * authenticated signaling stream. This is the client half.
12
+ *
13
+ * ⚠️ **"NOT A CHANNEL" AND "I COULD NOT FIND OUT" ARE DIFFERENT ANSWERS, and the whole value of this
14
+ * module is keeping them apart.** A negative is authoritative — the subscriber acts on it. A
15
+ * timeout, a down stream, a malformed reply or a directory-side `lookup_failed` are all the absence
16
+ * of an answer, and they leave the join refused. Collapsed into the negative, a directory outage
17
+ * would read as a verdict about the channel, and the step after that is accepting a key from
18
+ * whoever happened to answer.
19
+ */
20
+ import type { Logger } from "./types.js";
21
+ /**
22
+ * The part of a `SignalingManager` this needs. Structural rather than the class, so the lookup can
23
+ * be exercised without standing up a directory connection.
24
+ */
25
+ export interface SignalingLike {
26
+ registerInboundHandler(handler: (frame: Record<string, unknown>) => void): () => void;
27
+ sendRaw(frame: unknown): Promise<{
28
+ ok: boolean;
29
+ reason?: string;
30
+ }>;
31
+ }
32
+ export type ChannelAdminOutcome =
33
+ /** A registered channel, and the agent the directory says administers it. */
34
+ {
35
+ kind: "admin";
36
+ adminPubkeyHex: string;
37
+ }
38
+ /** The directory answered, and this pubkey is not a channel — unregistered, or an ordinary agent. */
39
+ | {
40
+ kind: "not_a_channel";
41
+ }
42
+ /** No answer. NOT a verdict about the channel. */
43
+ | {
44
+ kind: "unavailable";
45
+ reason: string;
46
+ };
47
+ export interface ChannelAdminLookupDeps {
48
+ /** The asking agent's directory connection, or null when it has none. */
49
+ signalingFor: (agentId: string) => SignalingLike | null;
50
+ logger: Logger;
51
+ timeoutMs?: number;
52
+ }
53
+ export declare function createChannelAdminLookup(deps: ChannelAdminLookupDeps): (agentId: string, channelHex: string) => Promise<ChannelAdminOutcome>;
54
+ //# sourceMappingURL=channel-admin-lookup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-admin-lookup.d.ts","sourceRoot":"","sources":["../src/channel-admin-lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACtF,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpE;AAED,MAAM,MAAM,mBAAmB;AAC7B,6EAA6E;AAC3E;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE;AAC3C,qGAAqG;GACnG;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE;AAC3B,kDAAkD;GAChD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,aAAa,GAAG,IAAI,CAAC;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAmBD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,sBAAsB,GAC3B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CA0GvE"}
@@ -0,0 +1,122 @@
1
+ import { extractErrorMessage } from "./error-message.js";
2
+ const DEFAULT_TIMEOUT_MS = 10_000;
3
+ /** 64 lowercase hex characters, or nothing. A partial admin key must not reach a comparison. */
4
+ function adminPubkeyOf(frame) {
5
+ const raw = frame["admin_pubkey"];
6
+ if (typeof raw !== "string" || !/^[0-9a-fA-F]{64}$/.test(raw))
7
+ return null;
8
+ return raw.toLowerCase();
9
+ }
10
+ function echoedChannelHex(frame) {
11
+ const raw = frame["channel_pubkey"];
12
+ if (raw instanceof Uint8Array)
13
+ return Buffer.from(raw).toString("hex");
14
+ if (Buffer.isBuffer(raw))
15
+ return raw.toString("hex");
16
+ if (typeof raw === "string" && /^[0-9a-fA-F]{64}$/.test(raw))
17
+ return raw.toLowerCase();
18
+ return null;
19
+ }
20
+ export function createChannelAdminLookup(deps) {
21
+ const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
22
+ return async function lookup(agentId, channelHexRaw) {
23
+ const channelHex = channelHexRaw.toLowerCase();
24
+ const signaling = deps.signalingFor(agentId);
25
+ if (!signaling) {
26
+ // Answer now rather than waiting out the timeout for a reply that was never coming — this runs
27
+ // on the inbound join path, once per frame.
28
+ return { kind: "unavailable", reason: "signaling_unavailable" };
29
+ }
30
+ let resolveFrame;
31
+ const pending = new Promise((r) => { resolveFrame = r; });
32
+ /**
33
+ * ⚠️ **MATCHED ON THE ECHOED PUBKEY, not on the frame type alone.** The signaling stream is one
34
+ * multiplexed stream shared by every lookup this agent has in flight. Taking the first
35
+ * `channel_admin_result` that arrives would let two concurrent joins each accept the other's
36
+ * answer — and then the admin check runs against a different channel's administrator, where a
37
+ * match proves nothing.
38
+ */
39
+ const unregister = signaling.registerInboundHandler((frame) => {
40
+ const t = frame["type"];
41
+ if (t !== "channel_admin_result" && t !== "channel_admin_error")
42
+ return;
43
+ /**
44
+ * ⚠️ **THE ERROR IS MATCHED THE SAME WAY, and it was not at first.** An unmatched error
45
+ * resolved whichever lookup happened to be waiting: two joins in flight, a database fault on
46
+ * the first channel, and the second was refused although the directory had answered it
47
+ * perfectly. It fails closed, so nothing unsafe — it just turns one channel's outage into
48
+ * every concurrent join's refusal, which is a bad trade for a field.
49
+ */
50
+ if (echoedChannelHex(frame) !== channelHex)
51
+ return;
52
+ resolveFrame(frame);
53
+ });
54
+ let timer;
55
+ try {
56
+ const sent = await signaling.sendRaw({
57
+ type: "channel_admin_query",
58
+ channel_pubkey: new Uint8Array(Buffer.from(channelHex, "hex")),
59
+ });
60
+ if (!sent.ok) {
61
+ // The transport's own reason, never a guess about the directory or the channel.
62
+ return { kind: "unavailable", reason: sent.reason ?? "signaling_unavailable" };
63
+ }
64
+ const timeoutP = new Promise((r) => {
65
+ timer = setTimeout(() => r({ type: "__timeout__" }), timeoutMs);
66
+ });
67
+ const frame = await Promise.race([pending, timeoutP]);
68
+ if (frame["type"] === "__timeout__") {
69
+ deps.logger.warn("directory.channel.admin.lookup.failed", { channel: channelHex.slice(0, 16), reason: "timeout" });
70
+ return { kind: "unavailable", reason: "timeout" };
71
+ }
72
+ if (frame["type"] === "channel_admin_error") {
73
+ const reason = typeof frame["reason"] === "string" ? frame["reason"] : "lookup_failed";
74
+ deps.logger.warn("directory.channel.admin.lookup.failed", { channel: channelHex.slice(0, 16), reason });
75
+ return { kind: "unavailable", reason };
76
+ }
77
+ /**
78
+ * ⚠️ **SHAPE FIRST, AND THIS WAS A DEFECT THE TEST CAUGHT.** `registered !== true` is true for
79
+ * a field that is ABSENT as well as one that is `false` — so a reply missing the field
80
+ * entirely read as an authoritative "not a channel", which is precisely the collapse this
81
+ * module exists to prevent. A frame that does not carry both booleans is a directory that
82
+ * replied with something we cannot read, and that is not knowing.
83
+ */
84
+ if (typeof frame["registered"] !== "boolean" || typeof frame["channel"] !== "boolean") {
85
+ deps.logger.warn("directory.channel.admin.lookup.failed", { channel: channelHex.slice(0, 16), reason: "malformed_reply" });
86
+ return { kind: "unavailable", reason: "malformed_reply" };
87
+ }
88
+ if (frame["registered"] !== true || frame["channel"] !== true) {
89
+ // An answer, and a settled one: the subscriber is looking at something that is not a channel.
90
+ deps.logger.info("directory.channel.admin.lookup", { channel: channelHex.slice(0, 16), answered: "not_a_channel" });
91
+ return { kind: "not_a_channel" };
92
+ }
93
+ const adminPubkeyHex = adminPubkeyOf(frame);
94
+ if (adminPubkeyHex === null) {
95
+ /**
96
+ * A registered channel whose answer carries no usable admin key. Reading a missing or
97
+ * malformed field as `""` would put an empty key into the comparison against whoever
98
+ * answered — which either matches nothing for ever or, worse, matches an empty counterparty.
99
+ * So it is `unavailable`: there is no admin to check against, and that is not knowing.
100
+ *
101
+ * ⚠️ **NAMED FOR THE REGISTRATION, NOT THE WIRE.** Calling this `malformed_reply` sent the
102
+ * operator to debug the protocol, when the directory answered exactly as designed about a
103
+ * channel that was registered without an administrator. The reason has to point at the
104
+ * thing that is actually wrong.
105
+ */
106
+ deps.logger.warn("directory.channel.admin.lookup.failed", { channel: channelHex.slice(0, 16), reason: "channel_without_admin" });
107
+ return { kind: "unavailable", reason: "channel_without_admin" };
108
+ }
109
+ deps.logger.info("directory.channel.admin.lookup", { channel: channelHex.slice(0, 16), answered: "admin" });
110
+ return { kind: "admin", adminPubkeyHex };
111
+ }
112
+ catch (err) {
113
+ return { kind: "unavailable", reason: extractErrorMessage(err) };
114
+ }
115
+ finally {
116
+ if (timer !== undefined)
117
+ clearTimeout(timer);
118
+ unregister();
119
+ }
120
+ };
121
+ }
122
+ //# sourceMappingURL=channel-admin-lookup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-admin-lookup.js","sourceRoot":"","sources":["../src/channel-admin-lookup.ts"],"names":[],"mappings":"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"}
@@ -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"}