@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.
Files changed (53) 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-config-store.d.ts +3 -0
  6. package/dist/channel-config-store.d.ts.map +1 -1
  7. package/dist/channel-config-store.js +28 -5
  8. package/dist/channel-config-store.js.map +1 -1
  9. package/dist/channel-join-exchange.d.ts +87 -0
  10. package/dist/channel-join-exchange.d.ts.map +1 -0
  11. package/dist/channel-join-exchange.js +289 -0
  12. package/dist/channel-join-exchange.js.map +1 -0
  13. package/dist/channel-membership-store.d.ts +118 -0
  14. package/dist/channel-membership-store.d.ts.map +1 -0
  15. package/dist/channel-membership-store.js +226 -0
  16. package/dist/channel-membership-store.js.map +1 -0
  17. package/dist/channel-membership-wiring.d.ts +90 -0
  18. package/dist/channel-membership-wiring.d.ts.map +1 -0
  19. package/dist/channel-membership-wiring.js +387 -0
  20. package/dist/channel-membership-wiring.js.map +1 -0
  21. package/dist/channel-publish-wiring.d.ts +11 -0
  22. package/dist/channel-publish-wiring.d.ts.map +1 -1
  23. package/dist/channel-publish-wiring.js +21 -2
  24. package/dist/channel-publish-wiring.js.map +1 -1
  25. package/dist/channel-publisher.d.ts +34 -2
  26. package/dist/channel-publisher.d.ts.map +1 -1
  27. package/dist/channel-publisher.js +46 -10
  28. package/dist/channel-publisher.js.map +1 -1
  29. package/dist/channel-subscription-store.d.ts +71 -0
  30. package/dist/channel-subscription-store.d.ts.map +1 -1
  31. package/dist/channel-subscription-store.js +150 -9
  32. package/dist/channel-subscription-store.js.map +1 -1
  33. package/dist/daemon.d.ts.map +1 -1
  34. package/dist/daemon.js +40 -0
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/inbound-sessions.d.ts +6 -0
  37. package/dist/inbound-sessions.d.ts.map +1 -1
  38. package/dist/inbound-sessions.js +27 -1
  39. package/dist/inbound-sessions.js.map +1 -1
  40. package/dist/refusal-reasons.d.ts +7 -0
  41. package/dist/refusal-reasons.d.ts.map +1 -1
  42. package/dist/refusal-reasons.js +15 -0
  43. package/dist/refusal-reasons.js.map +1 -1
  44. package/dist/session-content-context.d.ts +8 -0
  45. package/dist/session-content-context.d.ts.map +1 -1
  46. package/dist/session-content-ingest.d.ts.map +1 -1
  47. package/dist/session-content-ingest.js +28 -0
  48. package/dist/session-content-ingest.js.map +1 -1
  49. package/dist/session-node-manager.d.ts +4 -0
  50. package/dist/session-node-manager.d.ts.map +1 -1
  51. package/dist/session-node-manager.js +11 -0
  52. package/dist/session-node-manager.js.map +1 -1
  53. 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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"}
@@ -23,6 +23,9 @@ export interface ChannelConfig {
23
23
  relays: string[];
24
24
  guidance: string;
25
25
  retention_seconds: number;
26
+ members_visible?: boolean;
27
+ /** The agent that answers join requests. NOT the channel key — see the column comment. */
28
+ admin_pubkey?: string;
26
29
  }
27
30
  export declare class ChannelConfigStore {
28
31
  #private;
@@ -1 +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"}
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,QA0BrC,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;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,kBAAkB;;gBAIjB,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM;IAM9C,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAkC7C,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAsBlE"}
@@ -9,6 +9,21 @@ export const CHANNEL_CONFIG_CREATE_SQL = `
9
9
  relays TEXT NOT NULL,
10
10
  guidance TEXT NOT NULL DEFAULT '',
11
11
  retention_seconds INTEGER NOT NULL DEFAULT ${String(DEFAULT_RETENTION_SECONDS)},
12
+ -- ─── M16 019-MEMBERSHIP: one table, not two ───────────────────────────────────────────────
13
+ -- 019 first added a channel_settings table duplicating four of these columns, and wired NO
14
+ -- writer to it — so every membership read came back empty and every join was refused on a
15
+ -- channel the daemon genuinely administered. Two tables for one fact, with the live path
16
+ -- reading the empty one. Collapsed here while the database is still empty, which makes it a
17
+ -- schema edit rather than a migration.
18
+ --
19
+ -- Whether members can see each other. A publisher's decision about its own channel.
20
+ members_visible INTEGER NOT NULL DEFAULT 0,
21
+ -- 0 means NO KEY HAS EVER BEEN ISSUED. The first join mints generation 1; each ejection
22
+ -- advances it, and that number is what every body and every fetch key is bound to.
23
+ key_generation INTEGER NOT NULL DEFAULT 0,
24
+ -- The AGENT that admits members and answers join requests. Distinct from the channel key: the
25
+ -- channel signs posts and never converses, the admin holds the sessions. Empty until set.
26
+ admin_pubkey TEXT NOT NULL DEFAULT '',
12
27
  updated_at INTEGER NOT NULL
13
28
  );
14
29
  `;
@@ -22,7 +37,8 @@ export class ChannelConfigStore {
22
37
  }
23
38
  get(channelHex) {
24
39
  const row = this.#db
25
- .prepare(`SELECT access, relays, guidance, retention_seconds FROM channel_config WHERE channel_pubkey = ?`)
40
+ .prepare(`SELECT access, relays, guidance, retention_seconds, members_visible, admin_pubkey
41
+ FROM channel_config WHERE channel_pubkey = ?`)
26
42
  .get(channelHex.toLowerCase());
27
43
  if (!row)
28
44
  return null;
@@ -43,16 +59,23 @@ export class ChannelConfigStore {
43
59
  relays,
44
60
  guidance: row.guidance,
45
61
  retention_seconds: Number(row.retention_seconds),
62
+ members_visible: Number(row.members_visible) === 1,
63
+ admin_pubkey: row.admin_pubkey,
46
64
  };
47
65
  }
48
66
  set(channelHex, config, now) {
49
67
  this.#db
50
- .prepare(`INSERT INTO channel_config (channel_pubkey, access, relays, guidance, retention_seconds, updated_at)
51
- VALUES (?, ?, ?, ?, ?, ?)
68
+ .prepare(
69
+ // ⚠️ `key_generation` is NOT in the update list, and must never be: it is advanced only by an
70
+ // ejection, and letting a settings edit touch it would silently re-key or un-re-key a channel.
71
+ `INSERT INTO channel_config
72
+ (channel_pubkey, access, relays, guidance, retention_seconds, members_visible, admin_pubkey, updated_at)
73
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
52
74
  ON CONFLICT (channel_pubkey) DO UPDATE SET
53
75
  access = excluded.access, relays = excluded.relays, guidance = excluded.guidance,
54
- retention_seconds = excluded.retention_seconds, updated_at = excluded.updated_at`)
55
- .run(channelHex.toLowerCase(), config.access, JSON.stringify(config.relays), config.guidance, config.retention_seconds, now);
76
+ retention_seconds = excluded.retention_seconds, members_visible = excluded.members_visible,
77
+ admin_pubkey = excluded.admin_pubkey, updated_at = excluded.updated_at`)
78
+ .run(channelHex.toLowerCase(), config.access, JSON.stringify(config.relays), config.guidance, config.retention_seconds, config.members_visible === true ? 1 : 0, config.admin_pubkey ?? "", now);
56
79
  this.#logger.info("channel.config.set", {
57
80
  channel_pubkey: channelHex, access: config.access, relays: config.relays.length,
58
81
  });
@@ -1 +1 @@
1
- {"version":3,"file":"channel-config-store.js","sourceRoot":"","sources":["../src/channel-config-store.ts"],"names":[],"mappings":"AAkBA,8DAA8D;AAC9D,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE1D,MAAM,CAAC,MAAM,yBAAyB,GAAG;;;;;;;;kDAQS,MAAM,CAAC,yBAAyB,CAAC;;;CAGlF,CAAC;AASF,MAAM,OAAO,kBAAkB;IACpB,GAAG,CAAiB;IACpB,OAAO,CAAS;IAEzB,YAAY,EAAkB,EAAE,MAAc;QAC5C,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,UAAkB;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG;aACjB,OAAO,CAAC,iGAAiG,CAAC;aAC1G,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAElB,CAAC;QACd,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QAEtB,IAAI,MAAM,GAAa,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAY,CAAC;YACjD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;gBAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QAC/F,CAAC;QAAC,MAAM,CAAC;YACP,8FAA8F;YAC9F,sCAAsC;YACtC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;YACpF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,MAAM,EAAE,GAAG,CAAC,MAAuB;YACnC,MAAM;YACN,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,iBAAiB,EAAE,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC;SACjD,CAAC;IACJ,CAAC;IAED,GAAG,CAAC,UAAkB,EAAE,MAAqB,EAAE,GAAW;QACxD,IAAI,CAAC,GAAG;aACL,OAAO,CACN;;;;4FAIoF,CACrF;aACA,GAAG,CACF,UAAU,CAAC,WAAW,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,EACtE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,iBAAiB,EAAE,GAAG,CAC/C,CAAC;QACJ,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE;YACtC,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM;SAChF,CAAC,CAAC;IACL,CAAC;CACF"}
1
+ {"version":3,"file":"channel-config-store.js","sourceRoot":"","sources":["../src/channel-config-store.ts"],"names":[],"mappings":"AAkBA,8DAA8D;AAC9D,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE1D,MAAM,CAAC,MAAM,yBAAyB,GAAG;;;;;;;;kDAQS,MAAM,CAAC,yBAAyB,CAAC;;;;;;;;;;;;;;;;;;CAkBlF,CAAC;AAYF,MAAM,OAAO,kBAAkB;IACpB,GAAG,CAAiB;IACpB,OAAO,CAAS;IAEzB,YAAY,EAAkB,EAAE,MAAc;QAC5C,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,UAAkB;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG;aACjB,OAAO,CACN;wDACgD,CACjD;aACA,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAKlB,CAAC;QACd,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QAEtB,IAAI,MAAM,GAAa,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAY,CAAC;YACjD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;gBAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QAC/F,CAAC;QAAC,MAAM,CAAC;YACP,8FAA8F;YAC9F,sCAAsC;YACtC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;YACpF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,MAAM,EAAE,GAAG,CAAC,MAAuB;YACnC,MAAM;YACN,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,iBAAiB,EAAE,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC;YAChD,eAAe,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC;YAClD,YAAY,EAAE,GAAG,CAAC,YAAY;SAC/B,CAAC;IACJ,CAAC;IAED,GAAG,CAAC,UAAkB,EAAE,MAAqB,EAAE,GAAW;QACxD,IAAI,CAAC,GAAG;aACL,OAAO;QACN,8FAA8F;QAC9F,+FAA+F;QAC/F;;;;;;kFAM0E,CAC3E;aACA,GAAG,CACF,UAAU,CAAC,WAAW,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,EACtE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,iBAAiB,EACzC,MAAM,CAAC,eAAe,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,YAAY,IAAI,EAAE,EAAE,GAAG,CACxE,CAAC;QACJ,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE;YACtC,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM;SAChF,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,87 @@
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 { encodeChannelRekey } from "@cello-protocol/protocol-types";
27
+ import type { KeyProvider } from "@cello-protocol/crypto";
28
+ import type { Logger } from "./types.js";
29
+ import type { ChannelMembershipStore } from "./channel-membership-store.js";
30
+ import type { ChannelSubscriptionStore } from "./channel-subscription-store.js";
31
+ /** What this daemon knows about a channel it administers. `null` means it does not administer one. */
32
+ export interface LocalChannelAdmin {
33
+ agentId: string;
34
+ adminPubkeyHex: string;
35
+ channelKeyProvider: KeyProvider;
36
+ adminKeyProvider: KeyProvider;
37
+ }
38
+ export interface ChannelJoinExchangeDeps {
39
+ logger: Logger;
40
+ members: ChannelMembershipStore;
41
+ subscriptions: ChannelSubscriptionStore;
42
+ /** Put a frame on an open session. The session layer owns delivery; this owns the decisions. */
43
+ sendInSession: (sessionId: string, content: Uint8Array) => Promise<void>;
44
+ localChannelAdmin: (channelHex: string) => LocalChannelAdmin | null;
45
+ /**
46
+ * The channel's admin AS THE DIRECTORY REPORTS IT. `null` means the lookup could not be resolved,
47
+ * which FAILS CLOSED — an unreachable directory must not become "whoever answered is the admin".
48
+ */
49
+ profileAdminPubkey: (channelHex: string, agentId: string) => Promise<string | null>;
50
+ keyProviderFor: (agentId: string) => KeyProvider | null;
51
+ raiseNotice: (event: string, channelHex: string, subscriberHex: string) => void;
52
+ now?: () => number;
53
+ }
54
+ export type SubscriberJoinResult = {
55
+ ok: true;
56
+ channelHex: string;
57
+ generation: number;
58
+ } | {
59
+ ok: false;
60
+ reason: "not_a_join_frame" | "malformed" | "not_admin_of_channel" | "admin_unresolved" | "key_unwrap_failed" | "no_key_provider";
61
+ };
62
+ export interface ChannelJoinExchange {
63
+ /** An inbound frame on the ADMIN's daemon. `consumed: false` means it was not a join frame. */
64
+ onAdminFrame: (sessionId: string, counterpartyHex: string, content: Uint8Array) => Promise<{
65
+ consumed: boolean;
66
+ }>;
67
+ /** An inbound acceptance or re-key on the SUBSCRIBER's daemon. */
68
+ onSubscriberFrame: (agentId: string, sessionId: string, counterpartyHex: string, content: Uint8Array) => Promise<SubscriberJoinResult>;
69
+ /** The admin agent's explicit decision on a pending invite-only request. */
70
+ approve: (channelHex: string, subscriberHex: string, sessionId: string) => Promise<{
71
+ ok: true;
72
+ } | {
73
+ ok: false;
74
+ reason: string;
75
+ }>;
76
+ /** Refuse a pending request by name. */
77
+ refuse: (channelHex: string, subscriberHex: string, sessionId: string) => Promise<{
78
+ ok: true;
79
+ } | {
80
+ ok: false;
81
+ reason: string;
82
+ }>;
83
+ }
84
+ export declare function createChannelJoinExchange(deps: ChannelJoinExchangeDeps): ChannelJoinExchange;
85
+ /** Re-exported so the caller does not need a second import to build a re-key frame. */
86
+ export { encodeChannelRekey };
87
+ //# sourceMappingURL=channel-join-exchange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-join-exchange.d.ts","sourceRoot":"","sources":["../src/channel-join-exchange.ts"],"names":[],"mappings":"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"}