@cello-protocol/daemon 0.0.230 → 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.
- package/dist/channel-admin-lookup.d.ts +54 -0
- package/dist/channel-admin-lookup.d.ts.map +1 -0
- package/dist/channel-admin-lookup.js +122 -0
- package/dist/channel-admin-lookup.js.map +1 -0
- package/dist/channel-config-store.d.ts +3 -0
- package/dist/channel-config-store.d.ts.map +1 -1
- package/dist/channel-config-store.js +28 -5
- package/dist/channel-config-store.js.map +1 -1
- package/dist/channel-join-exchange.d.ts +87 -0
- package/dist/channel-join-exchange.d.ts.map +1 -0
- package/dist/channel-join-exchange.js +289 -0
- package/dist/channel-join-exchange.js.map +1 -0
- package/dist/channel-membership-store.d.ts +118 -0
- package/dist/channel-membership-store.d.ts.map +1 -0
- package/dist/channel-membership-store.js +226 -0
- package/dist/channel-membership-store.js.map +1 -0
- package/dist/channel-membership-wiring.d.ts +90 -0
- package/dist/channel-membership-wiring.d.ts.map +1 -0
- package/dist/channel-membership-wiring.js +387 -0
- package/dist/channel-membership-wiring.js.map +1 -0
- package/dist/channel-publish-wiring.d.ts +11 -0
- package/dist/channel-publish-wiring.d.ts.map +1 -1
- package/dist/channel-publish-wiring.js +21 -2
- package/dist/channel-publish-wiring.js.map +1 -1
- package/dist/channel-publisher.d.ts +34 -2
- package/dist/channel-publisher.d.ts.map +1 -1
- package/dist/channel-publisher.js +46 -10
- package/dist/channel-publisher.js.map +1 -1
- package/dist/channel-subscription-store.d.ts +71 -0
- package/dist/channel-subscription-store.d.ts.map +1 -1
- package/dist/channel-subscription-store.js +150 -9
- package/dist/channel-subscription-store.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +40 -0
- package/dist/daemon.js.map +1 -1
- package/dist/inbound-sessions.d.ts +6 -0
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +27 -1
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/refusal-reasons.d.ts +7 -0
- package/dist/refusal-reasons.d.ts.map +1 -1
- package/dist/refusal-reasons.js +15 -0
- package/dist/refusal-reasons.js.map +1 -1
- package/dist/session-content-context.d.ts +8 -0
- package/dist/session-content-context.d.ts.map +1 -1
- package/dist/session-content-ingest.d.ts.map +1 -1
- package/dist/session-content-ingest.js +28 -0
- package/dist/session-content-ingest.js.map +1 -1
- package/dist/session-node-manager.d.ts +4 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +11 -0
- package/dist/session-node-manager.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChannelJoinExchange — M16 019-MEMBERSHIP Part B, both sides of joining a channel.
|
|
3
|
+
*
|
|
4
|
+
* A channel never converses. Its ADMIN is an ordinary agent, so a join is a typed exchange inside a
|
|
5
|
+
* normal sealed session with that admin — no new transport, no relay frames, no special case in the
|
|
6
|
+
* session layer.
|
|
7
|
+
*
|
|
8
|
+
* ─── The two identity checks, and why each is load-bearing ───────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* **ADMIN side: the session counterparty must BE the subscriber the frame names.** The frame says
|
|
11
|
+
* who is joining and the session says who is talking; if they may differ, anyone can enrol a third
|
|
12
|
+
* party — and, worse, receive that third party's key bundle themselves, because the bundle is
|
|
13
|
+
* wrapped for the pubkey in the frame and sent down the session they are holding.
|
|
14
|
+
*
|
|
15
|
+
* **SUBSCRIBER side: the answering agent must be the admin in the channel's DIRECTORY PROFILE.**
|
|
16
|
+
* The session proves who the counterparty is. It does not prove they are this channel's admin. Drop
|
|
17
|
+
* this check and any agent that can open a session with you hands you a key bundle and a relay pair
|
|
18
|
+
* and becomes your channel: you read their posts believing them to be somebody else's, and the
|
|
19
|
+
* immutable `admin_pubkey` the design rests on protects nobody.
|
|
20
|
+
*
|
|
21
|
+
* ─── What is NOT decided here ────────────────────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* Invite-only admission is the admin AGENT's decision. A request lands as `pending` and raises a
|
|
24
|
+
* notice; nothing in this file approves one. There is no heuristic, no allowlist, no auto-approve.
|
|
25
|
+
*/
|
|
26
|
+
import { decodeChannelJoinRequest, decodeChannelJoinAccepted, decodeChannelRekey, encodeChannelJoinAccepted, encodeChannelJoinRefused, encodeChannelRekey, isChannelJoinFrame, } from "@cello-protocol/protocol-types";
|
|
27
|
+
import { generateGroupKey, wrapGroupKeyFor, unwrapGroupKey, } from "@cello-protocol/crypto";
|
|
28
|
+
import { extractErrorMessage } from "./error-message.js";
|
|
29
|
+
export function createChannelJoinExchange(deps) {
|
|
30
|
+
const { logger, members, subscriptions } = deps;
|
|
31
|
+
const now = deps.now ?? (() => Date.now());
|
|
32
|
+
async function refuseTo(sessionId, channelPubkey, reason) {
|
|
33
|
+
logger.info("channel.join.refused", {
|
|
34
|
+
channel_pubkey: Buffer.from(channelPubkey).toString("hex"), reason,
|
|
35
|
+
});
|
|
36
|
+
await deps.sendInSession(sessionId, encodeChannelJoinRefused({ channel_pubkey: channelPubkey, reason }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mint or reuse this channel's current group key, wrap it for one member, and send the acceptance.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ **THE ADMIN STORES ITS OWN CHANNEL'S GROUP KEY IN THE SAME TABLE ITS SUBSCRIBERS USE**, under
|
|
42
|
+
* its own agent id. A key held only in this process is lost on restart — and then the second
|
|
43
|
+
* member admitted after a restart gets a DIFFERENT key at the same generation, so the two decrypt
|
|
44
|
+
* different halves of the channel and neither can tell why. The admin is a reader of its own
|
|
45
|
+
* channel; storing the key where readers keep keys is the honest place for it.
|
|
46
|
+
*/
|
|
47
|
+
function currentGroupKey(adminAgentId, channelHex, generation) {
|
|
48
|
+
const held = subscriptions.keysFor(adminAgentId, channelHex).find((k) => k.generation === generation);
|
|
49
|
+
if (held)
|
|
50
|
+
return held;
|
|
51
|
+
const minted = generateGroupKey(generation);
|
|
52
|
+
subscriptions.addKey(adminAgentId, channelHex, minted, now());
|
|
53
|
+
return minted;
|
|
54
|
+
}
|
|
55
|
+
async function acceptInto(sessionId, channelHex, subscriberHex, admin) {
|
|
56
|
+
const settings = members.settings(channelHex);
|
|
57
|
+
/**
|
|
58
|
+
* ⚠️ RETURNS A REASON RATHER THAN NOTHING. This used to return silently, and `approve` reported
|
|
59
|
+
* `ok: true` on top of it — so an admin believed they had admitted somebody who received no key
|
|
60
|
+
* and no relays, and the member's daemon heard nothing at all.
|
|
61
|
+
*/
|
|
62
|
+
if (!settings)
|
|
63
|
+
return { ok: false, reason: "channel_not_configured_for_membership" };
|
|
64
|
+
let generation = settings.key_generation;
|
|
65
|
+
if (generation === 0)
|
|
66
|
+
generation = members.startGeneration(channelHex);
|
|
67
|
+
const gk = currentGroupKey(admin.agentId, channelHex, generation);
|
|
68
|
+
const channelPubkey = await admin.channelKeyProvider.getPublicKey();
|
|
69
|
+
const bundle = await wrapGroupKeyFor(gk, channelPubkey, new Uint8Array(Buffer.from(subscriberHex, "hex")), admin.adminKeyProvider);
|
|
70
|
+
await deps.sendInSession(sessionId, encodeChannelJoinAccepted({
|
|
71
|
+
channel_pubkey: channelPubkey,
|
|
72
|
+
key_bundle: bundle,
|
|
73
|
+
guidance: settings.guidance,
|
|
74
|
+
retention_seconds: settings.retention_seconds,
|
|
75
|
+
// `public` cannot reach here — the caller refuses it — and the frame refuses it too.
|
|
76
|
+
access: settings.access === "invite_only" ? "invite_only" : "open",
|
|
77
|
+
relays: settings.relays,
|
|
78
|
+
members_visible: settings.members_visible,
|
|
79
|
+
}));
|
|
80
|
+
logger.info("channel.member.joined", {
|
|
81
|
+
channel_pubkey: channelHex, subscriber_pubkey: subscriberHex, generation,
|
|
82
|
+
});
|
|
83
|
+
return { ok: true };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
async onAdminFrame(sessionId, counterpartyHex, content) {
|
|
87
|
+
// Not a join frame means somebody is TALKING. Consuming it would make a person's message
|
|
88
|
+
// vanish instead of reaching the operator — the same rule the document router follows.
|
|
89
|
+
if (!isChannelJoinFrame(content))
|
|
90
|
+
return { consumed: false };
|
|
91
|
+
const decoded = decodeChannelJoinRequest(content);
|
|
92
|
+
if (!decoded.ok) {
|
|
93
|
+
/**
|
|
94
|
+
* ⚠️ LOGGED, because this frame is now GONE. It is consumed — structured traffic, not
|
|
95
|
+
* conversation — so it never reaches the operator's transcript. Returning silently meant any
|
|
96
|
+
* counterparty could send a CBOR array whose first element was one of the four type strings
|
|
97
|
+
* and have it disappear with nothing recorded anywhere. The decoder already produces a
|
|
98
|
+
* reason and a detail; there is no excuse for discarding them.
|
|
99
|
+
*/
|
|
100
|
+
logger.warn("channel.join.frame_undecodable", {
|
|
101
|
+
reason: decoded.reason, detail: decoded.detail, sender: counterpartyHex,
|
|
102
|
+
});
|
|
103
|
+
return { consumed: true };
|
|
104
|
+
}
|
|
105
|
+
const channelPubkey = decoded.frame.channel_pubkey;
|
|
106
|
+
const channelHex = Buffer.from(channelPubkey).toString("hex");
|
|
107
|
+
const namedSubscriber = Buffer.from(decoded.frame.subscriber_pubkey).toString("hex");
|
|
108
|
+
/**
|
|
109
|
+
* ⚠️ THE COUNTERPARTY, NOT THE FRAME'S CLAIM. `namedSubscriber` is whatever the caller wrote;
|
|
110
|
+
* logging it before the check below let a prober put an arbitrary pubkey in an operator's log
|
|
111
|
+
* and make it look like that party had asked to join. The session's counterparty is the only
|
|
112
|
+
* identity here that anything has proven.
|
|
113
|
+
*/
|
|
114
|
+
logger.info("channel.join.requested", {
|
|
115
|
+
channel_pubkey: channelHex, counterparty_pubkey: counterpartyHex,
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* ⚠️ **THE COUNTERPARTY MUST BE THE SUBSCRIBER THE FRAME NAMES.** Otherwise anyone can enrol a
|
|
119
|
+
* third party — and receive that party's key bundle down the session they are holding.
|
|
120
|
+
* Refused as `not_admin_of_channel` rather than a more specific reason: to a caller who is not
|
|
121
|
+
* who they claim, "this is not your join" and "this is not my channel" are the same answer,
|
|
122
|
+
* and a distinct one would tell a prober which channels this daemon administers.
|
|
123
|
+
*/
|
|
124
|
+
if (namedSubscriber !== counterpartyHex.toLowerCase()) {
|
|
125
|
+
await refuseTo(sessionId, channelPubkey, "not_admin_of_channel");
|
|
126
|
+
return { consumed: true };
|
|
127
|
+
}
|
|
128
|
+
const admin = deps.localChannelAdmin(channelHex);
|
|
129
|
+
if (!admin) {
|
|
130
|
+
await refuseTo(sessionId, channelPubkey, "not_admin_of_channel");
|
|
131
|
+
return { consumed: true };
|
|
132
|
+
}
|
|
133
|
+
const settings = members.settings(channelHex);
|
|
134
|
+
if (!settings) {
|
|
135
|
+
await refuseTo(sessionId, channelPubkey, "not_admin_of_channel");
|
|
136
|
+
return { consumed: true };
|
|
137
|
+
}
|
|
138
|
+
// A public channel has no keys and no membership: anyone may read it. Admitting somebody would
|
|
139
|
+
// record a membership that means nothing and promise a key that does not exist.
|
|
140
|
+
if (settings.access === "public") {
|
|
141
|
+
await refuseTo(sessionId, channelPubkey, "channel_is_public");
|
|
142
|
+
return { consumed: true };
|
|
143
|
+
}
|
|
144
|
+
const status = members.statusOf(channelHex, namedSubscriber);
|
|
145
|
+
if (status === "active") {
|
|
146
|
+
await refuseTo(sessionId, channelPubkey, "already_member");
|
|
147
|
+
return { consumed: true };
|
|
148
|
+
}
|
|
149
|
+
// ⚠️ AN EJECTED MEMBER CANNOT SIMPLY ASK AGAIN. Re-admitting on request would undo the
|
|
150
|
+
// ejection the moment they retried — which is exactly why the ejected row is never deleted.
|
|
151
|
+
if (status === "ejected") {
|
|
152
|
+
await refuseTo(sessionId, channelPubkey, "ejected");
|
|
153
|
+
return { consumed: true };
|
|
154
|
+
}
|
|
155
|
+
if (status === "pending") {
|
|
156
|
+
await refuseTo(sessionId, channelPubkey, "pending_approval");
|
|
157
|
+
return { consumed: true };
|
|
158
|
+
}
|
|
159
|
+
if (settings.access === "open") {
|
|
160
|
+
members.admit(channelHex, namedSubscriber, "active", now());
|
|
161
|
+
await acceptInto(sessionId, channelHex, namedSubscriber, admin);
|
|
162
|
+
return { consumed: true };
|
|
163
|
+
}
|
|
164
|
+
// invite_only: recorded as PENDING and handed to the admin agent. Nothing here approves it.
|
|
165
|
+
members.admit(channelHex, namedSubscriber, "pending", now());
|
|
166
|
+
logger.info("channel.join.pending", { channel_pubkey: channelHex, subscriber_pubkey: namedSubscriber });
|
|
167
|
+
deps.raiseNotice("channel.join.pending", channelHex, namedSubscriber);
|
|
168
|
+
await refuseTo(sessionId, channelPubkey, "pending_approval");
|
|
169
|
+
return { consumed: true };
|
|
170
|
+
},
|
|
171
|
+
async onSubscriberFrame(agentId, sessionId, counterpartyHex, content) {
|
|
172
|
+
if (!isChannelJoinFrame(content))
|
|
173
|
+
return { ok: false, reason: "not_a_join_frame" };
|
|
174
|
+
/**
|
|
175
|
+
* An acceptance OR a re-key. Narrowed into two locals rather than kept as a pair of results,
|
|
176
|
+
* so the compiler knows which one carries a frame — a non-null assertion here would be the
|
|
177
|
+
* kind of "I know better" that survives a later edit changing which branch can be reached.
|
|
178
|
+
*/
|
|
179
|
+
const acceptedResult = decodeChannelJoinAccepted(content);
|
|
180
|
+
const acceptedFrame = acceptedResult.ok ? acceptedResult.frame : null;
|
|
181
|
+
let rekeyFrame = null;
|
|
182
|
+
if (!acceptedFrame) {
|
|
183
|
+
const rekeyResult = decodeChannelRekey(content);
|
|
184
|
+
if (rekeyResult.ok)
|
|
185
|
+
rekeyFrame = rekeyResult.frame;
|
|
186
|
+
}
|
|
187
|
+
if (!acceptedFrame && !rekeyFrame)
|
|
188
|
+
return { ok: false, reason: "malformed" };
|
|
189
|
+
const channelPubkey = acceptedFrame ? acceptedFrame.channel_pubkey : rekeyFrame.channel_pubkey;
|
|
190
|
+
const channelHex = Buffer.from(channelPubkey).toString("hex");
|
|
191
|
+
/**
|
|
192
|
+
* ⚠️ **THE ADMIN CHECK, AGAINST THE DIRECTORY PROFILE.** Not against whoever answered: the
|
|
193
|
+
* session proves identity, not authority. Without this any agent that can open a session with
|
|
194
|
+
* you becomes your channel, and the immutable admin key protects nobody.
|
|
195
|
+
*
|
|
196
|
+
* A lookup that cannot be RESOLVED fails closed. An unreachable directory must never mean
|
|
197
|
+
* "accept whoever this is" — that would make a network problem into an admission.
|
|
198
|
+
*/
|
|
199
|
+
/**
|
|
200
|
+
* ⚠️ **A RE-KEY ASKS NOBODY: the admin was checked when the subscription was made, and is
|
|
201
|
+
* stored on it.** Going back to the directory for one would put a network round trip, and a
|
|
202
|
+
* directory outage, in the path of every re-key — and a re-key is how an EJECTION reaches the
|
|
203
|
+
* remaining members, so losing one is the thing with a cost. It is also what closed the
|
|
204
|
+
* exposure this check opened: an unsolicited re-key naming any pubkey would otherwise have
|
|
205
|
+
* bought a stranger a directory lookup, on a handler the session layer is waiting for.
|
|
206
|
+
*
|
|
207
|
+
* The asking agent goes with the acceptance case, because the lookup rides THAT agent's own
|
|
208
|
+
* authenticated directory stream rather than any connection that happens to be open.
|
|
209
|
+
*/
|
|
210
|
+
const storedAdmin = rekeyFrame ? (subscriptions.get(agentId, channelHex)?.admin_pubkey ?? null) : null;
|
|
211
|
+
const profileAdmin = storedAdmin ?? await deps.profileAdminPubkey(channelHex, agentId);
|
|
212
|
+
if (profileAdmin === null) {
|
|
213
|
+
logger.warn("channel.join.refused", { channel_pubkey: channelHex, reason: "admin_unresolved" });
|
|
214
|
+
return { ok: false, reason: "admin_unresolved" };
|
|
215
|
+
}
|
|
216
|
+
if (profileAdmin.toLowerCase() !== counterpartyHex.toLowerCase()) {
|
|
217
|
+
logger.warn("channel.join.refused", {
|
|
218
|
+
channel_pubkey: channelHex, reason: "not_admin_of_channel",
|
|
219
|
+
answered_by: counterpartyHex, profile_admin: profileAdmin,
|
|
220
|
+
});
|
|
221
|
+
return { ok: false, reason: "not_admin_of_channel" };
|
|
222
|
+
}
|
|
223
|
+
const myKeys = deps.keyProviderFor(agentId);
|
|
224
|
+
if (!myKeys)
|
|
225
|
+
return { ok: false, reason: "no_key_provider" };
|
|
226
|
+
const bundle = acceptedFrame ? acceptedFrame.key_bundle : rekeyFrame.key_bundle;
|
|
227
|
+
const unwrapped = await unwrapGroupKey(bundle, channelPubkey, myKeys);
|
|
228
|
+
if (!unwrapped.ok) {
|
|
229
|
+
logger.warn("channel.join.refused", { channel_pubkey: channelHex, reason: unwrapped.reason });
|
|
230
|
+
return { ok: false, reason: "key_unwrap_failed" };
|
|
231
|
+
}
|
|
232
|
+
// An ACCEPTANCE brings the subscription with it; a RE-KEY only adds a key to one that exists.
|
|
233
|
+
if (acceptedFrame) {
|
|
234
|
+
subscriptions.upsert({
|
|
235
|
+
agent_id: agentId,
|
|
236
|
+
channel_pubkey: channelHex,
|
|
237
|
+
admin_pubkey: profileAdmin,
|
|
238
|
+
access: acceptedFrame.access,
|
|
239
|
+
relays: acceptedFrame.relays,
|
|
240
|
+
// STORED, not just decoded. What the channel is for and how long its posts last are the
|
|
241
|
+
// two things a subscriber has no other way to learn.
|
|
242
|
+
guidance: acceptedFrame.guidance,
|
|
243
|
+
retention_seconds: acceptedFrame.retention_seconds,
|
|
244
|
+
joined_at: now(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
subscriptions.addKey(agentId, channelHex, unwrapped.gk, now());
|
|
248
|
+
return { ok: true, channelHex, generation: unwrapped.gk.generation };
|
|
249
|
+
},
|
|
250
|
+
async approve(channelHex, subscriberHex, sessionId) {
|
|
251
|
+
const admin = deps.localChannelAdmin(channelHex);
|
|
252
|
+
if (!admin)
|
|
253
|
+
return { ok: false, reason: "channel_not_local" };
|
|
254
|
+
try {
|
|
255
|
+
members.approve(channelHex, subscriberHex);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
return { ok: false, reason: extractErrorMessage(err) };
|
|
259
|
+
}
|
|
260
|
+
// The delivery's verdict is the ANSWER. Reporting `ok` regardless told an admin they had
|
|
261
|
+
// admitted somebody who in fact received nothing.
|
|
262
|
+
return acceptInto(sessionId, channelHex, subscriberHex, admin);
|
|
263
|
+
},
|
|
264
|
+
async refuse(channelHex, subscriberHex, sessionId) {
|
|
265
|
+
const admin = deps.localChannelAdmin(channelHex);
|
|
266
|
+
if (!admin)
|
|
267
|
+
return { ok: false, reason: "channel_not_local" };
|
|
268
|
+
const channelPubkey = await admin.channelKeyProvider.getPublicKey();
|
|
269
|
+
/**
|
|
270
|
+
* ⚠️ **CONDITIONAL ON `pending`, AND THAT CONDITION IS THE POINT.** This used to upsert
|
|
271
|
+
* `ejected` unconditionally, so a refusal typed against an existing MEMBER answered `ok`,
|
|
272
|
+
* marked them ejected, and left them holding the current group key and fetch key — reading
|
|
273
|
+
* indefinitely while the table said otherwise. Refusing is not ejecting: an eject re-keys the
|
|
274
|
+
* channel, and a refusal has nothing to re-key because they were never in.
|
|
275
|
+
*/
|
|
276
|
+
try {
|
|
277
|
+
members.refusePending(channelHex, subscriberHex);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
return { ok: false, reason: extractErrorMessage(err) };
|
|
281
|
+
}
|
|
282
|
+
await refuseTo(sessionId, channelPubkey, "refused_by_admin");
|
|
283
|
+
return { ok: true };
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Re-exported so the caller does not need a second import to build a re-key frame. */
|
|
288
|
+
export { encodeChannelRekey };
|
|
289
|
+
//# sourceMappingURL=channel-join-exchange.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-join-exchange.js","sourceRoot":"","sources":["../src/channel-join-exchange.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChannelMembershipStore — the PUBLISHER's record of who is in a channel (M16 019-MEMBERSHIP).
|
|
3
|
+
*
|
|
4
|
+
* The mirror of `channel-subscription-store.ts`: that one is what a subscriber knows about somebody
|
|
5
|
+
* else's channel, this is what an admin knows about its own — the access rule, the members, and the
|
|
6
|
+
* key generation counter that ejection advances.
|
|
7
|
+
*
|
|
8
|
+
* ─── Why an ejected row is never deleted ─────────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* Deleting it would make an ejected member indistinguishable from a stranger, so their very next
|
|
11
|
+
* join request would be admitted as a new one — undoing the ejection silently, which is the one
|
|
12
|
+
* thing the whole mechanism exists to prevent. The row stays, with `ejected` on it, for ever.
|
|
13
|
+
*
|
|
14
|
+
* ─── Why the generation moves in the SAME transaction as the status ──────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* An eject that flipped the row and failed to bump the generation is not an eject: every member
|
|
17
|
+
* including the ejected one keeps reading, and nothing anywhere says the ejection did not take. The
|
|
18
|
+
* two facts are one fact, so they commit together or neither does.
|
|
19
|
+
*
|
|
20
|
+
* Keyed on the channel's pubkey and the member's pubkey — identities, never display names.
|
|
21
|
+
*/
|
|
22
|
+
import type { DaemonDatabase } from "./sqlcipher-db.js";
|
|
23
|
+
import type { Logger } from "./types.js";
|
|
24
|
+
import type { ChannelAccess } from "@cello-protocol/protocol-types";
|
|
25
|
+
/**
|
|
26
|
+
* ⚠️ **THERE IS NO `channel_settings` TABLE, AND THERE MUST NOT BE ONE.** This store first created
|
|
27
|
+
* its own, duplicating four columns of 018's `channel_config`, and nothing in production ever wrote
|
|
28
|
+
* a row to it. Every membership read came back empty, so every join was refused
|
|
29
|
+
* `not_admin_of_channel` on a channel the daemon demonstrably administered, every eject threw
|
|
30
|
+
* `channel_unknown`, and no group key was ever minted — which meant the fetch key was absent and the
|
|
31
|
+
* relay served the queue to anyone.
|
|
32
|
+
*
|
|
33
|
+
* Two tables holding one fact, with the live path reading the empty one. Collapsed into
|
|
34
|
+
* `channel_config` while the database was still empty. If you are about to add a second table for a
|
|
35
|
+
* publisher's decisions about its own channel: that is this bug.
|
|
36
|
+
*/
|
|
37
|
+
import { CHANNEL_CONFIG_CREATE_SQL } from "./channel-config-store.js";
|
|
38
|
+
export { CHANNEL_CONFIG_CREATE_SQL };
|
|
39
|
+
export declare const CHANNEL_MEMBERS_CREATE_SQL = "\n CREATE TABLE IF NOT EXISTS channel_members (\n channel_pubkey TEXT NOT NULL,\n subscriber_pubkey TEXT NOT NULL,\n joined_at INTEGER NOT NULL,\n -- active | pending | ejected | refused. An ejected row is NEVER deleted \u2014 see the header.\n status TEXT NOT NULL,\n PRIMARY KEY (channel_pubkey, subscriber_pubkey)\n );\n";
|
|
40
|
+
/**
|
|
41
|
+
* `refused` is distinct from `ejected`: one never became a member, the other was removed and cost
|
|
42
|
+
* the channel a re-key. Collapsing them would make the two indistinguishable in every later read.
|
|
43
|
+
*/
|
|
44
|
+
export type MemberStatus = "active" | "pending" | "ejected" | "refused";
|
|
45
|
+
export interface ChannelSettings {
|
|
46
|
+
access: ChannelAccess;
|
|
47
|
+
members_visible: boolean;
|
|
48
|
+
guidance: string;
|
|
49
|
+
retention_seconds: number;
|
|
50
|
+
relays: string[];
|
|
51
|
+
key_generation: number;
|
|
52
|
+
/**
|
|
53
|
+
* The AGENT that admits members and answers join requests. NOT the channel key: the channel signs
|
|
54
|
+
* posts and never converses; the admin holds the sessions. A subscriber checks the agent that
|
|
55
|
+
* answered against THIS, so conflating the two makes the check compare a key with itself.
|
|
56
|
+
*/
|
|
57
|
+
admin_pubkey: string;
|
|
58
|
+
}
|
|
59
|
+
export type ChannelMembershipErrorCode = "channel_unknown" | "not_an_active_member" | "not_a_pending_request" | "eject_not_applicable_open_channel" | "eject_not_applicable_public_channel";
|
|
60
|
+
export declare class ChannelMembershipError extends Error {
|
|
61
|
+
readonly code: ChannelMembershipErrorCode;
|
|
62
|
+
constructor(code: ChannelMembershipErrorCode, message: string);
|
|
63
|
+
}
|
|
64
|
+
export declare class ChannelMembershipStore {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(db: DaemonDatabase, logger: Logger);
|
|
67
|
+
/** Create or update a channel's settings. Never touches `key_generation` — only an eject does. */
|
|
68
|
+
putSettings(channelHex: string, s: Omit<ChannelSettings, "key_generation">, now?: number): void;
|
|
69
|
+
settings(channelHex: string): ChannelSettings | null;
|
|
70
|
+
/**
|
|
71
|
+
* Record a member. `pending` is an invite-only request awaiting the admin agent's decision.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ A PENDING MEMBER RECEIVES NO KEY — `activeMembers` excludes them. Counting a request as a
|
|
74
|
+
* membership would auto-approve every stranger who asked, and invite-only admission is the admin's
|
|
75
|
+
* decision, never a heuristic.
|
|
76
|
+
*/
|
|
77
|
+
admit(channelHex: string, subscriberHex: string, status: MemberStatus, joinedAt: number): void;
|
|
78
|
+
/** Move a pending request to active. The admin agent's explicit decision, never automatic. */
|
|
79
|
+
approve(channelHex: string, subscriberHex: string): void;
|
|
80
|
+
/**
|
|
81
|
+
* Refuse a PENDING request. Conditional on `pending`, and that condition is the whole method.
|
|
82
|
+
*
|
|
83
|
+
* ⚠️ **REFUSING IS NOT EJECTING, AND IT MUST NOT BE ABLE TO ACT LIKE ONE.** An earlier version
|
|
84
|
+
* upserted `ejected` unconditionally, so an admin who typed a refusal against an existing MEMBER
|
|
85
|
+
* got `ok: true`, the row said ejected, and the member kept the current group key and fetch key
|
|
86
|
+
* and went on reading indefinitely. The table and reality disagreed with nothing to say so —
|
|
87
|
+
* against the order's own rule that an eject without a generation bump is not an eject.
|
|
88
|
+
*
|
|
89
|
+
* A refused request gets its own status, not `ejected`: they were never a member, and reusing the
|
|
90
|
+
* word would make the two indistinguishable in every later read.
|
|
91
|
+
*/
|
|
92
|
+
refusePending(channelHex: string, subscriberHex: string): void;
|
|
93
|
+
statusOf(channelHex: string, subscriberHex: string): MemberStatus | null;
|
|
94
|
+
/** Who a new key goes to. Pending and ejected are excluded, for different reasons — see above. */
|
|
95
|
+
activeMembers(channelHex: string): string[];
|
|
96
|
+
/**
|
|
97
|
+
* Eject a member and advance the generation, atomically.
|
|
98
|
+
*
|
|
99
|
+
* Returns the new generation and the members it must be delivered to — the ejected one is simply
|
|
100
|
+
* not in that list, which is the entire mechanism.
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ **AN OPEN CHANNEL CANNOT EJECT**, because the member would rejoin the moment they asked: the
|
|
103
|
+
* re-key would cost every other member a delivery and change nothing. A PUBLIC channel has no
|
|
104
|
+
* members at all. Both refuse BY NAME, because a generic failure leaves an admin retrying
|
|
105
|
+
* something that will never work.
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ **EJECTING SOMEBODY WHO IS NOT AN ACTIVE MEMBER MUST NOT BUMP THE GENERATION.** Every bump
|
|
108
|
+
* re-keys every real member, and every re-key is a delivery that can fail — so a mistyped pubkey
|
|
109
|
+
* would cost the whole channel a key rotation for nothing.
|
|
110
|
+
*/
|
|
111
|
+
eject(channelHex: string, subscriberHex: string): {
|
|
112
|
+
generation: number;
|
|
113
|
+
remaining: string[];
|
|
114
|
+
};
|
|
115
|
+
/** Mint the FIRST generation for a channel that has never issued a key. */
|
|
116
|
+
startGeneration(channelHex: string): number;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=channel-membership-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-membership-store.d.ts","sourceRoot":"","sources":["../src/channel-membership-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;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;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,eAAO,MAAM,0BAA0B,+XAStC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAExE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,aAAa,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,0BAA0B,GAClC,iBAAiB,GACjB,sBAAsB,GACtB,uBAAuB,GACvB,mCAAmC,GACnC,qCAAqC,CAAC;AAE1C,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;gBAC9B,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,MAAM;CAK9D;AAYD,qBAAa,sBAAsB;;gBAIrB,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM;IAO9C,kGAAkG;IAClG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,gBAAgB,CAAC,EAAE,GAAG,SAAa,GAAG,IAAI;IAkBnG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IA2BpD;;;;;;OAMG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAqB9F,8FAA8F;IAC9F,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAexD;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAkB9D,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAOxE,kGAAkG;IAClG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAU3C;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE;IA0D7F,2EAA2E;IAC3E,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAM5C"}
|