@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.
- 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-collect-tick.d.ts +50 -4
- package/dist/channel-collect-tick.d.ts.map +1 -1
- package/dist/channel-collect-tick.js +103 -7
- package/dist/channel-collect-tick.js.map +1 -1
- 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 +111 -0
- package/dist/channel-join-exchange.d.ts.map +1 -0
- package/dist/channel-join-exchange.js +296 -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 +98 -0
- package/dist/channel-membership-wiring.d.ts.map +1 -0
- package/dist/channel-membership-wiring.js +393 -0
- package/dist/channel-membership-wiring.js.map +1 -0
- package/dist/channel-publish-handlers.d.ts +8 -0
- package/dist/channel-publish-handlers.d.ts.map +1 -1
- package/dist/channel-publish-handlers.js +3 -0
- package/dist/channel-publish-handlers.js.map +1 -1
- package/dist/channel-publish-wiring.d.ts +21 -0
- package/dist/channel-publish-wiring.d.ts.map +1 -1
- package/dist/channel-publish-wiring.js +39 -3
- 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/channel-wake-sender.d.ts +35 -0
- package/dist/channel-wake-sender.d.ts.map +1 -0
- package/dist/channel-wake-sender.js +61 -0
- package/dist/channel-wake-sender.js.map +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +76 -1
- 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/register-handler.d.ts.map +1 -1
- package/dist/register-handler.js +18 -1
- package/dist/register-handler.js.map +1 -1
- package/dist/registration-manager.d.ts +6 -0
- package/dist/registration-manager.d.ts.map +1 -1
- package/dist/registration-manager.js +9 -1
- package/dist/registration-manager.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/dist/signaling-wiring.d.ts +5 -0
- package/dist/signaling-wiring.d.ts.map +1 -1
- package/dist/signaling-wiring.js +14 -0
- package/dist/signaling-wiring.js.map +1 -1
- 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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"}
|
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
import type { Logger } from "./types.js";
|
|
2
2
|
import type { ChannelCollector } from "./channel-collector.js";
|
|
3
3
|
import type { ChannelSubscriptionStore } from "./channel-subscription-store.js";
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* ⚠️ **THIS IS A BACKSTOP, NOT THE DELIVERY MECHANISM — 021-WAKE changed what it is FOR.**
|
|
6
|
+
*
|
|
7
|
+
* At five minutes this timer WAS how a post reached anyone, and the feature was email with a
|
|
8
|
+
* five-minute inbox check. Delivery is now the wake (021): the publisher asks a directory to poke
|
|
9
|
+
* the members and they fetch at once.
|
|
10
|
+
*
|
|
11
|
+
* The timer keeps a job because push is best-effort, and it is the only thing that covers a post
|
|
12
|
+
* sitting on a relay with nobody told: the publisher's daemon never sent the wake, the frame was
|
|
13
|
+
* lost while the signaling stream rotated, or the peer forward never reached this subscriber's
|
|
14
|
+
* node. It does not go to zero, because the case it cannot otherwise repair is a channel that goes
|
|
15
|
+
* QUIET right after the message you missed — which is exactly the "relays going down now" post.
|
|
16
|
+
*/
|
|
5
17
|
export declare const COLLECT_TICK_INTERVAL_MS: number;
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The widest the pubkey-derived offset can push a channel's tick — **a proportion of the interval,
|
|
20
|
+
* which is why it is written as one.** A fixed 60 seconds was right for a 5-minute tick and is a
|
|
21
|
+
* narrow band inside an hour: the fleet would clump into the same minute every hour, which is the
|
|
22
|
+
* thundering herd this exists to prevent, arriving less often and so harder to notice.
|
|
23
|
+
*/
|
|
24
|
+
export declare const COLLECT_JITTER_MS: number;
|
|
25
|
+
/**
|
|
26
|
+
* A failing channel is backed off to at most this, then stays there until it succeeds.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ **IT HAS TO EXCEED THE INTERVAL OR IT IS NOT A BACKOFF.** At 30 minutes against an hourly
|
|
29
|
+
* poll, a FAILING channel became due sooner than a healthy one — the first failure computed
|
|
30
|
+
* `min(30min, 60min×2)` = 30 minutes, and every subsequent one stayed there. This number was sized
|
|
31
|
+
* for a five-minute interval and not re-derived when the interval moved, which is the whole
|
|
32
|
+
* mistake: a constant that only makes sense relative to another constant should be written
|
|
33
|
+
* relative to it.
|
|
34
|
+
*/
|
|
9
35
|
export declare const COLLECT_MAX_BACKOFF_MS: number;
|
|
36
|
+
/**
|
|
37
|
+
* Randomness added to a RETRY, on top of the stable per-channel jitter.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ **THE STEADY-STATE TIMER IS NOT THE STAMPEDE RISK — RECOVERY IS.** When a relay returns after
|
|
40
|
+
* an outage, every daemon that has been failing is on the retry path, not the timer, and
|
|
41
|
+
* `jitterForChannel` is stable per (agent, channel) so it does not stagger the returning WAVE: all
|
|
42
|
+
* of them back off to the same ceiling and come due on the same pass. This is the one place a
|
|
43
|
+
* re-rolled random belongs, because the thing being spread is a moment rather than a schedule.
|
|
44
|
+
*/
|
|
45
|
+
export declare const COLLECT_RETRY_SPREAD_MS: number;
|
|
10
46
|
export interface ChannelCollectTickDeps {
|
|
11
47
|
logger: Logger;
|
|
12
48
|
collector: ChannelCollector;
|
|
@@ -19,12 +55,22 @@ export interface ChannelCollectTickDeps {
|
|
|
19
55
|
intervalMs?: number;
|
|
20
56
|
jitterMs?: number;
|
|
21
57
|
maxBackoffMs?: number;
|
|
58
|
+
/** M16 021-WAKE: random spread on the RETRY path only. Set to 0 in a test that needs determinism. */
|
|
59
|
+
retrySpreadMs?: number;
|
|
22
60
|
}
|
|
23
61
|
export interface ChannelCollectTicker {
|
|
24
62
|
/** Begin collecting. Idempotent: a second call does not stack a second timer. */
|
|
25
63
|
start: () => void;
|
|
26
64
|
/** One pass over every active subscription. Exposed so a test drives it without a clock. */
|
|
27
65
|
collectAllDue: (now: number) => Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* M16 021-WAKE: the doorbell rang — collect this agent's channels NOW.
|
|
68
|
+
*
|
|
69
|
+
* ⚠️ **EVERY channel this agent follows, because the wake names none.** The frame is deliberately
|
|
70
|
+
* empty so that channel→member never travels a second wire; the daemon already knows what it
|
|
71
|
+
* subscribes to, and fetching all of them is what makes the empty frame sufficient.
|
|
72
|
+
*/
|
|
73
|
+
collectNow: (agentId: string) => Promise<void>;
|
|
28
74
|
stop: () => void;
|
|
29
75
|
}
|
|
30
76
|
/**
|
|
@@ -1 +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
|
|
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;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,QAAc,CAAC;AACpD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAA+B,CAAC;AAC9D;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,QAA+B,CAAC;AAEnE;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD,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;IACtB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;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;;;;;;OAMG;IACH,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,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,CAyI7F"}
|
|
@@ -32,12 +32,48 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { createHash } from "node:crypto";
|
|
34
34
|
import { extractErrorMessage } from "./error-message.js";
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
/**
|
|
36
|
+
* ⚠️ **THIS IS A BACKSTOP, NOT THE DELIVERY MECHANISM — 021-WAKE changed what it is FOR.**
|
|
37
|
+
*
|
|
38
|
+
* At five minutes this timer WAS how a post reached anyone, and the feature was email with a
|
|
39
|
+
* five-minute inbox check. Delivery is now the wake (021): the publisher asks a directory to poke
|
|
40
|
+
* the members and they fetch at once.
|
|
41
|
+
*
|
|
42
|
+
* The timer keeps a job because push is best-effort, and it is the only thing that covers a post
|
|
43
|
+
* sitting on a relay with nobody told: the publisher's daemon never sent the wake, the frame was
|
|
44
|
+
* lost while the signaling stream rotated, or the peer forward never reached this subscriber's
|
|
45
|
+
* node. It does not go to zero, because the case it cannot otherwise repair is a channel that goes
|
|
46
|
+
* QUIET right after the message you missed — which is exactly the "relays going down now" post.
|
|
47
|
+
*/
|
|
48
|
+
export const COLLECT_TICK_INTERVAL_MS = 60 * 60_000;
|
|
49
|
+
/**
|
|
50
|
+
* The widest the pubkey-derived offset can push a channel's tick — **a proportion of the interval,
|
|
51
|
+
* which is why it is written as one.** A fixed 60 seconds was right for a 5-minute tick and is a
|
|
52
|
+
* narrow band inside an hour: the fleet would clump into the same minute every hour, which is the
|
|
53
|
+
* thundering herd this exists to prevent, arriving less often and so harder to notice.
|
|
54
|
+
*/
|
|
55
|
+
export const COLLECT_JITTER_MS = COLLECT_TICK_INTERVAL_MS / 2;
|
|
56
|
+
/**
|
|
57
|
+
* A failing channel is backed off to at most this, then stays there until it succeeds.
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ **IT HAS TO EXCEED THE INTERVAL OR IT IS NOT A BACKOFF.** At 30 minutes against an hourly
|
|
60
|
+
* poll, a FAILING channel became due sooner than a healthy one — the first failure computed
|
|
61
|
+
* `min(30min, 60min×2)` = 30 minutes, and every subsequent one stayed there. This number was sized
|
|
62
|
+
* for a five-minute interval and not re-derived when the interval moved, which is the whole
|
|
63
|
+
* mistake: a constant that only makes sense relative to another constant should be written
|
|
64
|
+
* relative to it.
|
|
65
|
+
*/
|
|
66
|
+
export const COLLECT_MAX_BACKOFF_MS = 4 * COLLECT_TICK_INTERVAL_MS;
|
|
67
|
+
/**
|
|
68
|
+
* Randomness added to a RETRY, on top of the stable per-channel jitter.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ **THE STEADY-STATE TIMER IS NOT THE STAMPEDE RISK — RECOVERY IS.** When a relay returns after
|
|
71
|
+
* an outage, every daemon that has been failing is on the retry path, not the timer, and
|
|
72
|
+
* `jitterForChannel` is stable per (agent, channel) so it does not stagger the returning WAVE: all
|
|
73
|
+
* of them back off to the same ceiling and come due on the same pass. This is the one place a
|
|
74
|
+
* re-rolled random belongs, because the thing being spread is a moment rather than a schedule.
|
|
75
|
+
*/
|
|
76
|
+
export const COLLECT_RETRY_SPREAD_MS = 10 * 60_000;
|
|
41
77
|
/**
|
|
42
78
|
* A stable offset in [0, jitterMs) for a channel, from its public key. Same channel, same
|
|
43
79
|
* subscriber, same offset every time — see the header for why that beats a fresh random each tick.
|
|
@@ -53,6 +89,7 @@ export function createChannelCollectTicker(deps) {
|
|
|
53
89
|
const intervalMs = deps.intervalMs ?? COLLECT_TICK_INTERVAL_MS;
|
|
54
90
|
const jitterMs = deps.jitterMs ?? COLLECT_JITTER_MS;
|
|
55
91
|
const maxBackoffMs = deps.maxBackoffMs ?? COLLECT_MAX_BACKOFF_MS;
|
|
92
|
+
const retrySpreadMs = deps.retrySpreadMs ?? COLLECT_RETRY_SPREAD_MS;
|
|
56
93
|
/** When each subscription is next due, and how far it has been backed off. */
|
|
57
94
|
const nextDue = new Map();
|
|
58
95
|
const backoff = new Map();
|
|
@@ -95,7 +132,12 @@ export function createChannelCollectTicker(deps) {
|
|
|
95
132
|
// and one channel's bad relay must not stop every other channel on this daemon.
|
|
96
133
|
const next = Math.min(maxBackoffMs, (backoff.get(key) ?? intervalMs) * 2);
|
|
97
134
|
backoff.set(key, next);
|
|
98
|
-
|
|
135
|
+
// Stable jitter spreads channels across subscribers; the RANDOM part spreads the moment a
|
|
136
|
+
// whole fleet returns after an outage, which stable jitter cannot do because every failing
|
|
137
|
+
// channel lands on the same ceiling.
|
|
138
|
+
nextDue.set(key, now + next
|
|
139
|
+
+ jitterForChannel(sub.channel_pubkey, sub.agent_id, jitterMs)
|
|
140
|
+
+ Math.floor(Math.random() * retrySpreadMs));
|
|
99
141
|
logger.warn("channel.collect.tick_failed", {
|
|
100
142
|
channel_pubkey: sub.channel_pubkey,
|
|
101
143
|
reason: extractErrorMessage(err),
|
|
@@ -107,10 +149,64 @@ export function createChannelCollectTicker(deps) {
|
|
|
107
149
|
}
|
|
108
150
|
}
|
|
109
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* M16 021-WAKE — a doorbell arrived for this agent. Fetch its channels at once.
|
|
154
|
+
*
|
|
155
|
+
* ⚠️ **IT DOES NOT TOUCH `nextDue`, and that is deliberate.** A wake that reset the schedule would
|
|
156
|
+
* let a chatty channel keep pushing the backstop out for every OTHER channel on this agent — and
|
|
157
|
+
* the backstop is the only thing that repairs a wake that never arrived.
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ The online check is repeated here. The kill switch has to hold on this path too: collecting
|
|
160
|
+
* for an agent the operator switched off is the switch failing to switch something off, and a
|
|
161
|
+
* doorbell must not become the way around it.
|
|
162
|
+
*/
|
|
163
|
+
async function collectNow(agentId) {
|
|
164
|
+
if (!isAgentOnline(agentId)) {
|
|
165
|
+
logger.debug("channel.collect.wake_ignored", { reason: "agent_offline" });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const sub of subscriptions.active()) {
|
|
169
|
+
if (sub.agent_id !== agentId)
|
|
170
|
+
continue;
|
|
171
|
+
const key = keyOf(sub.agent_id, sub.channel_pubkey);
|
|
172
|
+
if (inFlight.has(key))
|
|
173
|
+
continue;
|
|
174
|
+
inFlight.add(key);
|
|
175
|
+
try {
|
|
176
|
+
await collector.collectOnce(sub.agent_id, sub.channel_pubkey);
|
|
177
|
+
await collector.repairGaps(sub.agent_id, sub.channel_pubkey);
|
|
178
|
+
backoff.delete(key);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
// One channel's bad relay must not stop the rest of this agent's channels collecting. The
|
|
182
|
+
// timer's own backoff is untouched: this pass is extra, not a replacement for it.
|
|
183
|
+
logger.warn("channel.collect.wake_failed", {
|
|
184
|
+
channel_pubkey: sub.channel_pubkey, reason: extractErrorMessage(err),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
inFlight.delete(key);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
110
192
|
return {
|
|
193
|
+
collectNow,
|
|
111
194
|
start() {
|
|
112
195
|
if (timer !== null)
|
|
113
196
|
return;
|
|
197
|
+
/**
|
|
198
|
+
* ⚠️ **ONE PASS NOW, BEFORE THE TIMER — and leaving this out made the order's own worst case
|
|
199
|
+
* twelve times worse.** At a five-minute interval a daemon that had just started waited five
|
|
200
|
+
* minutes for its first collection; at sixty it waits an hour. Your laptop was shut
|
|
201
|
+
* overnight, you open it, the daemon reconnects — and nothing is fetched until the next hour.
|
|
202
|
+
* The offline subscriber is exactly the case the backstop exists for.
|
|
203
|
+
*
|
|
204
|
+
* It is also what makes a NEW subscription arrive promptly: `collectAllDue` treats first
|
|
205
|
+
* sight as due immediately, but only a pass can notice it.
|
|
206
|
+
*/
|
|
207
|
+
void collectAllDue(Date.now()).catch((err) => {
|
|
208
|
+
logger.warn("channel.collect.pass_failed", { reason: extractErrorMessage(err) });
|
|
209
|
+
});
|
|
114
210
|
// The pass runs every interval and decides per subscription what is due; the jitter lives in
|
|
115
211
|
// the due times rather than in the timer, so one slow channel cannot drag the others.
|
|
116
212
|
timer = setInterval(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-collect-tick.js","sourceRoot":"","sources":["../src/channel-collect-tick.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD
|
|
1
|
+
{"version":3,"file":"channel-collect-tick.js","sourceRoot":"","sources":["../src/channel-collect-tick.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,
|
|
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
|
|
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(
|
|
51
|
-
|
|
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,
|
|
55
|
-
|
|
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
|
|
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"}
|