@cello-protocol/daemon 0.0.230 → 0.0.232

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 (76) 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 +50 -4
  6. package/dist/channel-collect-tick.d.ts.map +1 -1
  7. package/dist/channel-collect-tick.js +103 -7
  8. package/dist/channel-collect-tick.js.map +1 -1
  9. package/dist/channel-config-store.d.ts +3 -0
  10. package/dist/channel-config-store.d.ts.map +1 -1
  11. package/dist/channel-config-store.js +28 -5
  12. package/dist/channel-config-store.js.map +1 -1
  13. package/dist/channel-join-exchange.d.ts +111 -0
  14. package/dist/channel-join-exchange.d.ts.map +1 -0
  15. package/dist/channel-join-exchange.js +296 -0
  16. package/dist/channel-join-exchange.js.map +1 -0
  17. package/dist/channel-membership-store.d.ts +118 -0
  18. package/dist/channel-membership-store.d.ts.map +1 -0
  19. package/dist/channel-membership-store.js +226 -0
  20. package/dist/channel-membership-store.js.map +1 -0
  21. package/dist/channel-membership-wiring.d.ts +98 -0
  22. package/dist/channel-membership-wiring.d.ts.map +1 -0
  23. package/dist/channel-membership-wiring.js +393 -0
  24. package/dist/channel-membership-wiring.js.map +1 -0
  25. package/dist/channel-publish-handlers.d.ts +8 -0
  26. package/dist/channel-publish-handlers.d.ts.map +1 -1
  27. package/dist/channel-publish-handlers.js +3 -0
  28. package/dist/channel-publish-handlers.js.map +1 -1
  29. package/dist/channel-publish-wiring.d.ts +21 -0
  30. package/dist/channel-publish-wiring.d.ts.map +1 -1
  31. package/dist/channel-publish-wiring.js +39 -3
  32. package/dist/channel-publish-wiring.js.map +1 -1
  33. package/dist/channel-publisher.d.ts +34 -2
  34. package/dist/channel-publisher.d.ts.map +1 -1
  35. package/dist/channel-publisher.js +46 -10
  36. package/dist/channel-publisher.js.map +1 -1
  37. package/dist/channel-subscription-store.d.ts +71 -0
  38. package/dist/channel-subscription-store.d.ts.map +1 -1
  39. package/dist/channel-subscription-store.js +150 -9
  40. package/dist/channel-subscription-store.js.map +1 -1
  41. package/dist/channel-wake-sender.d.ts +35 -0
  42. package/dist/channel-wake-sender.d.ts.map +1 -0
  43. package/dist/channel-wake-sender.js +61 -0
  44. package/dist/channel-wake-sender.js.map +1 -0
  45. package/dist/daemon.d.ts.map +1 -1
  46. package/dist/daemon.js +76 -1
  47. package/dist/daemon.js.map +1 -1
  48. package/dist/inbound-sessions.d.ts +6 -0
  49. package/dist/inbound-sessions.d.ts.map +1 -1
  50. package/dist/inbound-sessions.js +27 -1
  51. package/dist/inbound-sessions.js.map +1 -1
  52. package/dist/refusal-reasons.d.ts +7 -0
  53. package/dist/refusal-reasons.d.ts.map +1 -1
  54. package/dist/refusal-reasons.js +15 -0
  55. package/dist/refusal-reasons.js.map +1 -1
  56. package/dist/register-handler.d.ts.map +1 -1
  57. package/dist/register-handler.js +18 -1
  58. package/dist/register-handler.js.map +1 -1
  59. package/dist/registration-manager.d.ts +6 -0
  60. package/dist/registration-manager.d.ts.map +1 -1
  61. package/dist/registration-manager.js +9 -1
  62. package/dist/registration-manager.js.map +1 -1
  63. package/dist/session-content-context.d.ts +8 -0
  64. package/dist/session-content-context.d.ts.map +1 -1
  65. package/dist/session-content-ingest.d.ts.map +1 -1
  66. package/dist/session-content-ingest.js +28 -0
  67. package/dist/session-content-ingest.js.map +1 -1
  68. package/dist/session-node-manager.d.ts +4 -0
  69. package/dist/session-node-manager.d.ts.map +1 -1
  70. package/dist/session-node-manager.js +11 -0
  71. package/dist/session-node-manager.js.map +1 -1
  72. package/dist/signaling-wiring.d.ts +5 -0
  73. package/dist/signaling-wiring.d.ts.map +1 -1
  74. package/dist/signaling-wiring.js +14 -0
  75. package/dist/signaling-wiring.js.map +1 -1
  76. package/package.json +5 -5
@@ -0,0 +1,226 @@
1
+ /**
2
+ * ⚠️ **THERE IS NO `channel_settings` TABLE, AND THERE MUST NOT BE ONE.** This store first created
3
+ * its own, duplicating four columns of 018's `channel_config`, and nothing in production ever wrote
4
+ * a row to it. Every membership read came back empty, so every join was refused
5
+ * `not_admin_of_channel` on a channel the daemon demonstrably administered, every eject threw
6
+ * `channel_unknown`, and no group key was ever minted — which meant the fetch key was absent and the
7
+ * relay served the queue to anyone.
8
+ *
9
+ * Two tables holding one fact, with the live path reading the empty one. Collapsed into
10
+ * `channel_config` while the database was still empty. If you are about to add a second table for a
11
+ * publisher's decisions about its own channel: that is this bug.
12
+ */
13
+ import { CHANNEL_CONFIG_CREATE_SQL } from "./channel-config-store.js";
14
+ export { CHANNEL_CONFIG_CREATE_SQL };
15
+ export const CHANNEL_MEMBERS_CREATE_SQL = `
16
+ CREATE TABLE IF NOT EXISTS channel_members (
17
+ channel_pubkey TEXT NOT NULL,
18
+ subscriber_pubkey TEXT NOT NULL,
19
+ joined_at INTEGER NOT NULL,
20
+ -- active | pending | ejected | refused. An ejected row is NEVER deleted — see the header.
21
+ status TEXT NOT NULL,
22
+ PRIMARY KEY (channel_pubkey, subscriber_pubkey)
23
+ );
24
+ `;
25
+ export class ChannelMembershipError extends Error {
26
+ code;
27
+ constructor(code, message) {
28
+ super(`${code}: ${message}`);
29
+ this.name = "ChannelMembershipError";
30
+ this.code = code;
31
+ }
32
+ }
33
+ export class ChannelMembershipStore {
34
+ #db;
35
+ #logger;
36
+ constructor(db, logger) {
37
+ this.#db = db;
38
+ this.#logger = logger;
39
+ this.#db.exec(CHANNEL_CONFIG_CREATE_SQL);
40
+ this.#db.exec(CHANNEL_MEMBERS_CREATE_SQL);
41
+ }
42
+ /** Create or update a channel's settings. Never touches `key_generation` — only an eject does. */
43
+ putSettings(channelHex, s, now = Date.now()) {
44
+ this.#db
45
+ .prepare(`INSERT INTO channel_config
46
+ (channel_pubkey, access, members_visible, guidance, retention_seconds, relays, admin_pubkey, updated_at)
47
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
48
+ ON CONFLICT (channel_pubkey) DO UPDATE SET
49
+ access = excluded.access, members_visible = excluded.members_visible,
50
+ guidance = excluded.guidance, retention_seconds = excluded.retention_seconds,
51
+ relays = excluded.relays, admin_pubkey = excluded.admin_pubkey,
52
+ updated_at = excluded.updated_at`)
53
+ .run(channelHex.toLowerCase(), s.access, s.members_visible ? 1 : 0, s.guidance, s.retention_seconds, JSON.stringify(s.relays), s.admin_pubkey ?? "", now);
54
+ }
55
+ settings(channelHex) {
56
+ const row = this.#db
57
+ .prepare(`SELECT access, members_visible, guidance, retention_seconds, relays, key_generation, admin_pubkey
58
+ FROM channel_config WHERE channel_pubkey = ?`)
59
+ .get(channelHex.toLowerCase());
60
+ if (!row)
61
+ return null;
62
+ let relays = [];
63
+ try {
64
+ const parsed = JSON.parse(row.relays);
65
+ if (Array.isArray(parsed))
66
+ relays = parsed.filter((r) => typeof r === "string");
67
+ }
68
+ catch {
69
+ this.#logger.warn("channel.settings.relays_corrupt", { channel_pubkey: channelHex });
70
+ }
71
+ return {
72
+ access: row.access,
73
+ members_visible: Number(row.members_visible) === 1,
74
+ guidance: row.guidance,
75
+ retention_seconds: Number(row.retention_seconds),
76
+ relays,
77
+ key_generation: Number(row.key_generation),
78
+ admin_pubkey: row.admin_pubkey,
79
+ };
80
+ }
81
+ /**
82
+ * Record a member. `pending` is an invite-only request awaiting the admin agent's decision.
83
+ *
84
+ * ⚠️ A PENDING MEMBER RECEIVES NO KEY — `activeMembers` excludes them. Counting a request as a
85
+ * membership would auto-approve every stranger who asked, and invite-only admission is the admin's
86
+ * decision, never a heuristic.
87
+ */
88
+ admit(channelHex, subscriberHex, status, joinedAt) {
89
+ this.#db
90
+ .prepare(`INSERT INTO channel_members (channel_pubkey, subscriber_pubkey, joined_at, status)
91
+ VALUES (?, ?, ?, ?)
92
+ ON CONFLICT (channel_pubkey, subscriber_pubkey) DO UPDATE SET status = excluded.status`)
93
+ .run(channelHex.toLowerCase(), subscriberHex.toLowerCase(), joinedAt, status);
94
+ /**
95
+ * ⚠️ THE EVENT NAMES WHAT HAPPENED. This logged `channel.member.joined` for every status, so an
96
+ * operator grepping for who joined got pending requests and ejections back as joins — the log
97
+ * asserting something the row contradicts.
98
+ */
99
+ const event = status === "active" ? "channel.member.joined"
100
+ : status === "pending" ? "channel.join.pending"
101
+ : status === "ejected" ? "channel.member.ejected" : "channel.join.refused";
102
+ this.#logger.info(event, {
103
+ channel_pubkey: channelHex, subscriber_pubkey: subscriberHex, status,
104
+ });
105
+ }
106
+ /** Move a pending request to active. The admin agent's explicit decision, never automatic. */
107
+ approve(channelHex, subscriberHex) {
108
+ const changed = this.#db
109
+ .prepare(`UPDATE channel_members SET status = 'active'
110
+ WHERE channel_pubkey = ? AND subscriber_pubkey = ? AND status = 'pending'`)
111
+ .run(channelHex.toLowerCase(), subscriberHex.toLowerCase());
112
+ if (Number(changed.changes) === 0) {
113
+ throw new ChannelMembershipError("not_an_active_member", "no pending request for that subscriber");
114
+ }
115
+ this.#logger.info("channel.member.joined", {
116
+ channel_pubkey: channelHex, subscriber_pubkey: subscriberHex, status: "active",
117
+ });
118
+ }
119
+ /**
120
+ * Refuse a PENDING request. Conditional on `pending`, and that condition is the whole method.
121
+ *
122
+ * ⚠️ **REFUSING IS NOT EJECTING, AND IT MUST NOT BE ABLE TO ACT LIKE ONE.** An earlier version
123
+ * upserted `ejected` unconditionally, so an admin who typed a refusal against an existing MEMBER
124
+ * got `ok: true`, the row said ejected, and the member kept the current group key and fetch key
125
+ * and went on reading indefinitely. The table and reality disagreed with nothing to say so —
126
+ * against the order's own rule that an eject without a generation bump is not an eject.
127
+ *
128
+ * A refused request gets its own status, not `ejected`: they were never a member, and reusing the
129
+ * word would make the two indistinguishable in every later read.
130
+ */
131
+ refusePending(channelHex, subscriberHex) {
132
+ const changed = this.#db
133
+ .prepare(`UPDATE channel_members SET status = 'refused'
134
+ WHERE channel_pubkey = ? AND subscriber_pubkey = ? AND status = 'pending'`)
135
+ .run(channelHex.toLowerCase(), subscriberHex.toLowerCase());
136
+ if (Number(changed.changes) === 0) {
137
+ throw new ChannelMembershipError("not_a_pending_request", `${subscriberHex.slice(0, 16)} has no pending request on this channel. To remove an existing member, eject them — which re-keys the channel.`);
138
+ }
139
+ this.#logger.info("channel.join.refused", {
140
+ channel_pubkey: channelHex, subscriber_pubkey: subscriberHex, reason: "refused_by_admin",
141
+ });
142
+ }
143
+ statusOf(channelHex, subscriberHex) {
144
+ const row = this.#db
145
+ .prepare(`SELECT status FROM channel_members WHERE channel_pubkey = ? AND subscriber_pubkey = ?`)
146
+ .get(channelHex.toLowerCase(), subscriberHex.toLowerCase());
147
+ return row ? row.status : null;
148
+ }
149
+ /** Who a new key goes to. Pending and ejected are excluded, for different reasons — see above. */
150
+ activeMembers(channelHex) {
151
+ const rows = this.#db
152
+ .prepare(`SELECT subscriber_pubkey FROM channel_members
153
+ WHERE channel_pubkey = ? AND status = 'active' ORDER BY subscriber_pubkey ASC`)
154
+ .all(channelHex.toLowerCase());
155
+ return rows.map((r) => r.subscriber_pubkey);
156
+ }
157
+ /**
158
+ * Eject a member and advance the generation, atomically.
159
+ *
160
+ * Returns the new generation and the members it must be delivered to — the ejected one is simply
161
+ * not in that list, which is the entire mechanism.
162
+ *
163
+ * ⚠️ **AN OPEN CHANNEL CANNOT EJECT**, because the member would rejoin the moment they asked: the
164
+ * re-key would cost every other member a delivery and change nothing. A PUBLIC channel has no
165
+ * members at all. Both refuse BY NAME, because a generic failure leaves an admin retrying
166
+ * something that will never work.
167
+ *
168
+ * ⚠️ **EJECTING SOMEBODY WHO IS NOT AN ACTIVE MEMBER MUST NOT BUMP THE GENERATION.** Every bump
169
+ * re-keys every real member, and every re-key is a delivery that can fail — so a mistyped pubkey
170
+ * would cost the whole channel a key rotation for nothing.
171
+ */
172
+ eject(channelHex, subscriberHex) {
173
+ const channel = channelHex.toLowerCase();
174
+ const subscriber = subscriberHex.toLowerCase();
175
+ const settings = this.settings(channel);
176
+ if (!settings)
177
+ throw new ChannelMembershipError("channel_unknown", `no settings for ${channel.slice(0, 16)}`);
178
+ if (settings.access === "public") {
179
+ throw new ChannelMembershipError("eject_not_applicable_public_channel", "a public channel has no members: anyone may read it, so there is nobody to remove");
180
+ }
181
+ if (settings.access === "open") {
182
+ throw new ChannelMembershipError("eject_not_applicable_open_channel", "an open channel admits anyone who asks, so an ejected member would rejoin immediately");
183
+ }
184
+ this.#db.exec("BEGIN IMMEDIATE");
185
+ let generation;
186
+ let remaining;
187
+ try {
188
+ // Under the write lock, and conditional on `active`: the status check and the bump are one
189
+ // decision, so a concurrent eject of the same member cannot advance the generation twice.
190
+ const changed = this.#db
191
+ .prepare(`UPDATE channel_members SET status = 'ejected'
192
+ WHERE channel_pubkey = ? AND subscriber_pubkey = ? AND status = 'active'`)
193
+ .run(channel, subscriber);
194
+ if (Number(changed.changes) === 0) {
195
+ throw new ChannelMembershipError("not_an_active_member", `${subscriber.slice(0, 16)} is not an active member of this channel`);
196
+ }
197
+ this.#db
198
+ .prepare(`UPDATE channel_config SET key_generation = key_generation + 1 WHERE channel_pubkey = ?`)
199
+ .run(channel);
200
+ generation = Number(this.#db.prepare(`SELECT key_generation FROM channel_config WHERE channel_pubkey = ?`)
201
+ .get(channel).key_generation);
202
+ remaining = this.activeMembers(channel);
203
+ this.#db.exec("COMMIT");
204
+ }
205
+ catch (err) {
206
+ try {
207
+ this.#db.exec("ROLLBACK");
208
+ }
209
+ catch { /* the transaction is already gone */ }
210
+ throw err;
211
+ }
212
+ this.#logger.info("channel.member.ejected", {
213
+ channel_pubkey: channelHex, subscriber_pubkey: subscriberHex,
214
+ generation, member_count: remaining.length,
215
+ });
216
+ return { generation, remaining };
217
+ }
218
+ /** Mint the FIRST generation for a channel that has never issued a key. */
219
+ startGeneration(channelHex) {
220
+ this.#db
221
+ .prepare(`UPDATE channel_config SET key_generation = 1 WHERE channel_pubkey = ? AND key_generation = 0`)
222
+ .run(channelHex.toLowerCase());
223
+ return this.settings(channelHex)?.key_generation ?? 0;
224
+ }
225
+ }
226
+ //# sourceMappingURL=channel-membership-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-membership-store.js","sourceRoot":"","sources":["../src/channel-membership-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * M16 019-MEMBERSHIP — the join exchange, the eject re-key, and the operator's channel verbs.
3
+ *
4
+ * The publishing half is wired next door in `channel-publish-wiring.ts`; this is membership. Both
5
+ * exist because a module registered into nothing is a feature that does not exist — the mistake 017
6
+ * shipped and 018 repeated on its other half.
7
+ *
8
+ * ⚠️ **THE JOIN FRAME HOOK IS SYNCHRONOUS AND THE HANDLING IS NOT.** `setOnChannelJoinFrame` must
9
+ * answer "is this mine" immediately, because the ingest path is deciding whether to write the frame
10
+ * into a transcript as something a person said. The decision is made by CLASSIFYING (a cheap decode)
11
+ * and the work is then queued, exactly as the document layer does. A rejected promise here must not
12
+ * take down the content path, so it is caught and logged.
13
+ */
14
+ import type { Logger } from "./types.js";
15
+ import type { DaemonDatabase } from "./sqlcipher-db.js";
16
+ import type { KeyProvider } from "@cello-protocol/crypto";
17
+ import { ChannelMembershipStore } from "./channel-membership-store.js";
18
+ import { type AdminLookupOutcome } from "./channel-join-exchange.js";
19
+ import { type ChannelAdminOutcome, type SignalingLike } from "./channel-admin-lookup.js";
20
+ type Handler = (params: Record<string, unknown> | undefined, connectionId: string) => Promise<unknown>;
21
+ export interface ChannelMembershipWiringDeps {
22
+ handlers: Map<string, Handler>;
23
+ logger: Logger;
24
+ getDb: () => DaemonDatabase;
25
+ /** Route a frame into an open session. The session layer owns delivery. */
26
+ sendInSession: (agentName: string, sessionId: string, content: Uint8Array) => Promise<void>;
27
+ /** Register the inbound hook. Separate from the document one — see that field's note. */
28
+ setOnChannelJoinFrame: (cb: (agentName: string, sessionId: string, content: Uint8Array, senderPubkey: string, correlationId?: string) => {
29
+ consumed: boolean;
30
+ }) => void;
31
+ /** Every agent this daemon loaded. A channel IS one of them — looked up by pubkey, never by name. */
32
+ loadedAgents: ReadonlyArray<{
33
+ name: string;
34
+ pubkey: string;
35
+ keyProvider: KeyProvider;
36
+ }>;
37
+ keyProviders: Map<string, KeyProvider>;
38
+ resolveAgentId: (agentName: string) => string;
39
+ resolveCurrentAgent: (connectionId: string, explicitAgent?: string) => string | null;
40
+ /** Open sessions for an agent, so a re-key can ride one this daemon already holds. */
41
+ activeSessionsFor: (agentName: string) => Array<{
42
+ sessionId: string;
43
+ counterpartyPubkeyHex: string;
44
+ }>;
45
+ /**
46
+ * M16 020-CHANADMIN: an agent's directory connection, by NAME (the daemon's own key for it), or
47
+ * null when it has none. The subscriber asks on ITS OWN authenticated stream — the directory
48
+ * answers a client that has proved its agent key, which is the difference between this frame and
49
+ * the relay's copy of it.
50
+ */
51
+ signalingFor: (agentName: string) => SignalingLike | null;
52
+ }
53
+ /**
54
+ * M16 020-CHANADMIN — where the subscriber's admin key comes from.
55
+ *
56
+ * The join exchange compares the agent that answered against this. It is deliberately the ONLY
57
+ * source: the session proves who the counterparty is and says nothing about their authority over a
58
+ * channel, so "whoever answered" is exactly the hole the check exists to close.
59
+ *
60
+ * Two sources, in order:
61
+ * 1. **This daemon's own settings**, when we publish the channel. No round trip, and no directory
62
+ * outage in the path of a join to a channel running on this very machine.
63
+ * 2. **The directory**, for everything else. Before 020 there was no step 2 and this returned null
64
+ * for every channel not published here — so nobody could join anybody else's channel, or their
65
+ * own from a second device.
66
+ *
67
+ * ⚠️ **`null` MEANS REFUSE, AND EVERY UNKNOWN STILL ENDS HERE.** A directory that cannot be reached,
68
+ * one that faults, a pubkey that is not a channel, and a lookup that throws all answer `null`, which
69
+ * the exchange refuses as `admin_unresolved`. 020 removes a refusal that was firing on every
70
+ * channel; it must not weaken the one that remains.
71
+ */
72
+ export declare function createProfileAdminPubkey(deps: {
73
+ members: ChannelMembershipStore;
74
+ lookup: (agentId: string, channelHex: string) => Promise<ChannelAdminOutcome>;
75
+ logger: Logger;
76
+ }): (channelHex: string, agentId: string) => Promise<AdminLookupOutcome>;
77
+ export interface ChannelMembershipWiring {
78
+ /**
79
+ * The channel's CURRENT fetch key, signed, or undefined when this daemon holds no group key for
80
+ * it. Handed to the publishing half so a re-key reaches the relays on the very next post — which
81
+ * is what makes an ejection lock the member out at the relay and not only at the ciphertext.
82
+ */
83
+ currentFetchKey: (channelHex: string) => Promise<{
84
+ pubkey: Uint8Array;
85
+ time_ms: number;
86
+ signature: Uint8Array;
87
+ } | undefined>;
88
+ /**
89
+ * M16 021-WAKE: the channel's ACTIVE members, which is who a post's doorbell is rung for. Pending
90
+ * and ejected rows are excluded by the store — waking a pending request would tell somebody who
91
+ * has not been admitted that a post exists, and waking an ejected member is the thing the
92
+ * ejection undid.
93
+ */
94
+ activeMembers: (channelHex: string) => string[];
95
+ }
96
+ export declare function wireChannelMembership(deps: ChannelMembershipWiringDeps): ChannelMembershipWiring;
97
+ export {};
98
+ //# sourceMappingURL=channel-membership-wiring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-membership-wiring.d.ts","sourceRoot":"","sources":["../src/channel-membership-wiring.ts"],"names":[],"mappings":"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"}