@moxxy/channel-kit 0.27.0

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/dist/frame-pump.d.ts +77 -0
  3. package/dist/frame-pump.d.ts.map +1 -0
  4. package/dist/frame-pump.js +118 -0
  5. package/dist/frame-pump.js.map +1 -0
  6. package/dist/index.d.ts +19 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +19 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/ingest/dedupe.d.ts +31 -0
  11. package/dist/ingest/dedupe.d.ts.map +1 -0
  12. package/dist/ingest/dedupe.js +66 -0
  13. package/dist/ingest/dedupe.js.map +1 -0
  14. package/dist/ingest/http-server.d.ts +76 -0
  15. package/dist/ingest/http-server.d.ts.map +1 -0
  16. package/dist/ingest/http-server.js +101 -0
  17. package/dist/ingest/http-server.js.map +1 -0
  18. package/dist/pairing/host-code.d.ts +94 -0
  19. package/dist/pairing/host-code.d.ts.map +1 -0
  20. package/dist/pairing/host-code.js +107 -0
  21. package/dist/pairing/host-code.js.map +1 -0
  22. package/dist/pairing/tofu.d.ts +33 -0
  23. package/dist/pairing/tofu.d.ts.map +1 -0
  24. package/dist/pairing/tofu.js +41 -0
  25. package/dist/pairing/tofu.js.map +1 -0
  26. package/dist/permission.d.ts +28 -0
  27. package/dist/permission.d.ts.map +1 -0
  28. package/dist/permission.js +17 -0
  29. package/dist/permission.js.map +1 -0
  30. package/dist/plain-turn-renderer.d.ts +17 -0
  31. package/dist/plain-turn-renderer.d.ts.map +1 -0
  32. package/dist/plain-turn-renderer.js +28 -0
  33. package/dist/plain-turn-renderer.js.map +1 -0
  34. package/dist/secrets.d.ts +18 -0
  35. package/dist/secrets.d.ts.map +1 -0
  36. package/dist/secrets.js +16 -0
  37. package/dist/secrets.js.map +1 -0
  38. package/dist/turn.d.ts +96 -0
  39. package/dist/turn.d.ts.map +1 -0
  40. package/dist/turn.js +106 -0
  41. package/dist/turn.js.map +1 -0
  42. package/dist/voice-reply.d.ts +139 -0
  43. package/dist/voice-reply.d.ts.map +1 -0
  44. package/dist/voice-reply.js +333 -0
  45. package/dist/voice-reply.js.map +1 -0
  46. package/package.json +54 -0
  47. package/src/frame-pump.test.ts +209 -0
  48. package/src/frame-pump.ts +154 -0
  49. package/src/index.ts +66 -0
  50. package/src/ingest/dedupe.test.ts +58 -0
  51. package/src/ingest/dedupe.ts +66 -0
  52. package/src/ingest/http-server.test.ts +120 -0
  53. package/src/ingest/http-server.ts +173 -0
  54. package/src/pairing/host-code.test.ts +121 -0
  55. package/src/pairing/host-code.ts +159 -0
  56. package/src/pairing/tofu.test.ts +62 -0
  57. package/src/pairing/tofu.ts +60 -0
  58. package/src/permission.test.ts +69 -0
  59. package/src/permission.ts +46 -0
  60. package/src/plain-turn-renderer.ts +31 -0
  61. package/src/secrets.test.ts +59 -0
  62. package/src/secrets.ts +26 -0
  63. package/src/turn.test.ts +165 -0
  64. package/src/turn.ts +162 -0
  65. package/src/voice-reply.test.ts +316 -0
  66. package/src/voice-reply.ts +449 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moxxy (moxxy.ai)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Throttled "send once, then edit one message" streaming loop — the shared core
3
+ * behind the Telegram and Slack frame pumps. The channel feeds it "something
4
+ * changed" signals (`scheduleEdit`); the pump pulls the current frame text from
5
+ * the `frame` callback at flush time and drives a tiny messenger-agnostic
6
+ * {@link FrameSink} (`send` a new message, `edit` an existing one).
7
+ *
8
+ * Lifecycle per turn:
9
+ * 1. construct with a sink bound to the turn's target (chat / thread).
10
+ * 2. `scheduleEdit()` whenever the rendered snapshot changes → debounced
11
+ * `flush(false)` after `editFrameMs`.
12
+ * 3. `flush(true)` on turn completion drains the final snapshot (and posts
13
+ * `emptyFinalText` when the turn rendered nothing at all).
14
+ * 4. `dispose()` clears the timer.
15
+ *
16
+ * Messenger-specific concerns stay in the sink: Telegram's HTML parse-mode
17
+ * fallback and 4096-char message splitting, Slack's `chat.postMessage` /
18
+ * `chat.update` calls. The `final` flag is forwarded so a sink can perform
19
+ * final-only work (Telegram sends split-overflow tails only on the last frame).
20
+ */
21
+ /** Messenger adapter the pump drives. Implementations should swallow their own
22
+ * transport errors (log + return null / resolve) — a failed frame must never
23
+ * abort the turn. */
24
+ export interface FrameSink<Id> {
25
+ /** Send a NEW message; returns its id, or null when the send failed. */
26
+ send(text: string, final: boolean): Promise<Id | null>;
27
+ /** Edit a previously sent message in place. */
28
+ edit(id: Id, text: string, final: boolean): Promise<void>;
29
+ }
30
+ export interface FramePumpOptions<Id> {
31
+ readonly sink: FrameSink<Id>;
32
+ /** Debounce window for streaming edits (typically ~1s). */
33
+ readonly editFrameMs: number;
34
+ /**
35
+ * Produce the current frame text. Called at flush time (pull model) so the
36
+ * flushed frame always reflects the newest renderer state; `final` lets a
37
+ * renderer emit a different last frame (e.g. Telegram collapses its activity
38
+ * trace only on the final flush). An empty string means "nothing to show".
39
+ */
40
+ readonly frame: (final: boolean) => string;
41
+ /**
42
+ * Sent when the FINAL flush finds no content and no message was ever sent,
43
+ * so the user isn't left staring at a placeholder-less turn. Omit to skip.
44
+ */
45
+ readonly emptyFinalText?: string;
46
+ /**
47
+ * Deliver the final frame to the sink even when its text is identical to the
48
+ * last sent frame. Channels whose sink does final-only work that must not be
49
+ * skipped (Telegram's split tails) set this; channels where the final frame
50
+ * is a plain re-send (Slack) leave it off and save the no-op API call.
51
+ */
52
+ readonly alwaysFlushFinal?: boolean;
53
+ }
54
+ export declare class FramePump<Id> {
55
+ private readonly opts;
56
+ private id;
57
+ private lastSent;
58
+ private editTimer;
59
+ /** In-flight send/edit, so timer + flush never overlap (single-flight). */
60
+ private inflight;
61
+ constructor(opts: FramePumpOptions<Id>);
62
+ /** The id of the streamed message once sent (null until the first frame lands). */
63
+ get messageId(): Id | null;
64
+ /** Debounced "content changed" signal → `flush(false)` after `editFrameMs`. */
65
+ scheduleEdit(): void;
66
+ /**
67
+ * Drain the current frame to the sink. The first non-empty frame `send`s a
68
+ * new message; every later frame `edit`s it in place. A `final` flush is
69
+ * never dropped: if a send is in flight it waits for it, and it guarantees
70
+ * at least one message (via `emptyFinalText`) when the turn rendered nothing.
71
+ */
72
+ flush(final: boolean): Promise<void>;
73
+ dispose(): void;
74
+ private track;
75
+ private cancelTimer;
76
+ }
77
+ //# sourceMappingURL=frame-pump.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-pump.d.ts","sourceRoot":"","sources":["../src/frame-pump.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;sBAEsB;AACtB,MAAM,WAAW,SAAS,CAAC,EAAE;IAC3B,wEAAwE;IACxE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IACvD,+CAA+C;IAC/C,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,gBAAgB,CAAC,EAAE;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,qBAAa,SAAS,CAAC,EAAE;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,EAAE,CAAmB;IAC7B,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,SAAS,CAA8C;IAC/D,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAA8B;gBAElC,IAAI,EAAE,gBAAgB,CAAC,EAAE,CAAC;IAItC,mFAAmF;IACnF,IAAI,SAAS,IAAI,EAAE,GAAG,IAAI,CAEzB;IAED,+EAA+E;IAC/E,YAAY,IAAI,IAAI;IAQpB;;;;;OAKG;IACG,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAwC1C,OAAO,IAAI,IAAI;YAID,KAAK;IAenB,OAAO,CAAC,WAAW;CAMpB"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Throttled "send once, then edit one message" streaming loop — the shared core
3
+ * behind the Telegram and Slack frame pumps. The channel feeds it "something
4
+ * changed" signals (`scheduleEdit`); the pump pulls the current frame text from
5
+ * the `frame` callback at flush time and drives a tiny messenger-agnostic
6
+ * {@link FrameSink} (`send` a new message, `edit` an existing one).
7
+ *
8
+ * Lifecycle per turn:
9
+ * 1. construct with a sink bound to the turn's target (chat / thread).
10
+ * 2. `scheduleEdit()` whenever the rendered snapshot changes → debounced
11
+ * `flush(false)` after `editFrameMs`.
12
+ * 3. `flush(true)` on turn completion drains the final snapshot (and posts
13
+ * `emptyFinalText` when the turn rendered nothing at all).
14
+ * 4. `dispose()` clears the timer.
15
+ *
16
+ * Messenger-specific concerns stay in the sink: Telegram's HTML parse-mode
17
+ * fallback and 4096-char message splitting, Slack's `chat.postMessage` /
18
+ * `chat.update` calls. The `final` flag is forwarded so a sink can perform
19
+ * final-only work (Telegram sends split-overflow tails only on the last frame).
20
+ */
21
+ export class FramePump {
22
+ opts;
23
+ id = null;
24
+ lastSent = '';
25
+ editTimer = null;
26
+ /** In-flight send/edit, so timer + flush never overlap (single-flight). */
27
+ inflight = null;
28
+ constructor(opts) {
29
+ this.opts = opts;
30
+ }
31
+ /** The id of the streamed message once sent (null until the first frame lands). */
32
+ get messageId() {
33
+ return this.id;
34
+ }
35
+ /** Debounced "content changed" signal → `flush(false)` after `editFrameMs`. */
36
+ scheduleEdit() {
37
+ if (this.editTimer || this.inflight)
38
+ return;
39
+ this.editTimer = setTimeout(() => {
40
+ this.editTimer = null;
41
+ void this.flush(false);
42
+ }, this.opts.editFrameMs);
43
+ }
44
+ /**
45
+ * Drain the current frame to the sink. The first non-empty frame `send`s a
46
+ * new message; every later frame `edit`s it in place. A `final` flush is
47
+ * never dropped: if a send is in flight it waits for it, and it guarantees
48
+ * at least one message (via `emptyFinalText`) when the turn rendered nothing.
49
+ */
50
+ async flush(final) {
51
+ this.cancelTimer();
52
+ if (this.inflight) {
53
+ if (!final) {
54
+ // A send is running; re-arm so the newest text lands after it.
55
+ if (this.opts.frame(false) !== this.lastSent)
56
+ this.scheduleEdit();
57
+ return;
58
+ }
59
+ // Final flushes must not be lost — wait out the in-flight send.
60
+ await this.inflight;
61
+ }
62
+ const text = this.opts.frame(final);
63
+ if (!text) {
64
+ if (final && this.id == null && this.opts.emptyFinalText) {
65
+ const placeholder = this.opts.emptyFinalText;
66
+ await this.track(async () => {
67
+ const sent = await this.opts.sink.send(placeholder, final);
68
+ if (sent != null)
69
+ this.id = sent;
70
+ });
71
+ }
72
+ return;
73
+ }
74
+ if (text === this.lastSent && !(final && this.opts.alwaysFlushFinal))
75
+ return;
76
+ await this.track(async () => {
77
+ if (this.id == null) {
78
+ const sent = await this.opts.sink.send(text, final);
79
+ if (sent != null)
80
+ this.id = sent;
81
+ }
82
+ else {
83
+ await this.opts.sink.edit(this.id, text, final);
84
+ }
85
+ this.lastSent = text;
86
+ });
87
+ // Content may have advanced while the send was in flight.
88
+ if (final) {
89
+ if (this.opts.frame(true) !== this.lastSent)
90
+ await this.flush(true);
91
+ }
92
+ else if (this.opts.frame(false) !== this.lastSent) {
93
+ this.scheduleEdit();
94
+ }
95
+ }
96
+ dispose() {
97
+ this.cancelTimer();
98
+ }
99
+ async track(work) {
100
+ const run = work();
101
+ // Waiters must never observe a rejection here; errors propagate to the
102
+ // caller of `flush` below (sinks are expected to swallow transport errors).
103
+ this.inflight = run.then(() => undefined, () => undefined);
104
+ try {
105
+ await run;
106
+ }
107
+ finally {
108
+ this.inflight = null;
109
+ }
110
+ }
111
+ cancelTimer() {
112
+ if (this.editTimer) {
113
+ clearTimeout(this.editTimer);
114
+ this.editTimer = null;
115
+ }
116
+ }
117
+ }
118
+ //# sourceMappingURL=frame-pump.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-pump.js","sourceRoot":"","sources":["../src/frame-pump.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @moxxy/channel-kit — shared machinery for building moxxy messaging channels.
3
+ *
4
+ * Extracted from the Telegram and Slack channel plugins so new channels
5
+ * (Discord, WhatsApp, Signal, ...) are thin adapters: messenger-specific quirks
6
+ * (formatting, transport error handling, signature schemes, pairing wording)
7
+ * stay in each plugin; the load-bearing loop mechanics live here.
8
+ */
9
+ export { FramePump, type FrameSink, type FramePumpOptions } from './frame-pump.js';
10
+ export { TurnCoordinator, driveTurn, subscribeTurn, type DriveTurnOptions, type TurnCoordinatorOptions, type TurnEventSource, type TurnLease, type TurnSession, } from './turn.js';
11
+ export { PlainTurnRenderer } from './plain-turn-renderer.js';
12
+ export { clearHostCodePairing, createHostCodeState, greetPeer, isPeerAuthorized, openHostCodeWindow, submitPeerCode, type HostCodeAction, type HostCodeDecision, type HostCodePhase, type HostCodeState, } from './pairing/host-code.js';
13
+ export { TofuPairingWindow, type TofuPairingWindowOptions } from './pairing/tofu.js';
14
+ export { resolveSecret, type SecretReader, type SecretSpec } from './secrets.js';
15
+ export { createAuditedAllowListResolver, type AuditedAllowListOptions, } from './permission.js';
16
+ export { IngestHttpServer, respondJson, type IngestHttpServerHandle, type IngestHttpServerOptions, type IngestLogger, type IngestVerdict, } from './ingest/http-server.js';
17
+ export { DeliveryDedupeCache } from './ingest/dedupe.js';
18
+ export { audioExtForMime, deliverVoiceReply, ensureOggOpus, resolveVoiceToggle, synthesizeReply, toSpeech, type DeliverVoiceReplyOptions, type EnsureOggOpusOptions, type EnsureOggOpusResult, type SynthesizeReplyOptions, type SynthesizeReplyResult, type SynthesizerSource, type VoiceReplyOutcome, type VoiceReplySink, type VoiceToggleInput, type VoiceToggleResult, } from './voice-reply.js';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,EACL,eAAe,EACf,SAAS,EACT,aAAa,EACb,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,EACT,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EACL,8BAA8B,EAC9B,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,QAAQ,EACR,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @moxxy/channel-kit — shared machinery for building moxxy messaging channels.
3
+ *
4
+ * Extracted from the Telegram and Slack channel plugins so new channels
5
+ * (Discord, WhatsApp, Signal, ...) are thin adapters: messenger-specific quirks
6
+ * (formatting, transport error handling, signature schemes, pairing wording)
7
+ * stay in each plugin; the load-bearing loop mechanics live here.
8
+ */
9
+ export { FramePump } from './frame-pump.js';
10
+ export { TurnCoordinator, driveTurn, subscribeTurn, } from './turn.js';
11
+ export { PlainTurnRenderer } from './plain-turn-renderer.js';
12
+ export { clearHostCodePairing, createHostCodeState, greetPeer, isPeerAuthorized, openHostCodeWindow, submitPeerCode, } from './pairing/host-code.js';
13
+ export { TofuPairingWindow } from './pairing/tofu.js';
14
+ export { resolveSecret } from './secrets.js';
15
+ export { createAuditedAllowListResolver, } from './permission.js';
16
+ export { IngestHttpServer, respondJson, } from './ingest/http-server.js';
17
+ export { DeliveryDedupeCache } from './ingest/dedupe.js';
18
+ export { audioExtForMime, deliverVoiceReply, ensureOggOpus, resolveVoiceToggle, synthesizeReply, toSpeech, } from './voice-reply.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAyC,MAAM,iBAAiB,CAAC;AACnF,OAAO,EACL,eAAe,EACf,SAAS,EACT,aAAa,GAMd,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,EACT,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GAKf,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAiC,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAsC,MAAM,cAAc,CAAC;AACjF,OAAO,EACL,8BAA8B,GAE/B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,gBAAgB,EAChB,WAAW,GAKZ,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,QAAQ,GAWT,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * In-memory LRU for inbound-delivery idempotency. Webhook providers retry
3
+ * deliveries and the same event id may arrive more than once, so channels
4
+ * dedupe by delivery id for a bounded window (Slack: `event_id` +
5
+ * `X-Slack-Retry-Num`; same at-least-once pattern as `@moxxy/plugin-webhooks`).
6
+ *
7
+ * Not persistent — a restart resets the cache, which is acceptable: worst case
8
+ * the agent processes one event twice immediately after restart, which is rare
9
+ * and self-limiting.
10
+ */
11
+ export declare class DeliveryDedupeCache {
12
+ private readonly seen;
13
+ private readonly maxEntries;
14
+ private readonly ttlMs;
15
+ /** Sweep TTL-expired entries at most once per this interval. The LRU overflow
16
+ * cap already bounds memory, so the TTL sweep need not run on every check —
17
+ * amortizing it keeps the pre-ACK path O(1) under retry storms. */
18
+ private readonly sweepEveryMs;
19
+ private lastSweepMs;
20
+ constructor(opts?: {
21
+ maxEntries?: number;
22
+ ttlMs?: number;
23
+ sweepEveryMs?: number;
24
+ });
25
+ /** Returns true if this is a new key (now recorded); false if a duplicate. */
26
+ check(key: string): boolean;
27
+ size(): number;
28
+ clear(): void;
29
+ private evictExpired;
30
+ }
31
+ //# sourceMappingURL=dedupe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/ingest/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B;;wEAEoE;IACpE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,WAAW,CAAa;gBAEpB,IAAI,GAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAO;IAMrF,8EAA8E;IAC9E,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAoB3B,IAAI,IAAI,MAAM;IAId,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,YAAY;CASrB"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * In-memory LRU for inbound-delivery idempotency. Webhook providers retry
3
+ * deliveries and the same event id may arrive more than once, so channels
4
+ * dedupe by delivery id for a bounded window (Slack: `event_id` +
5
+ * `X-Slack-Retry-Num`; same at-least-once pattern as `@moxxy/plugin-webhooks`).
6
+ *
7
+ * Not persistent — a restart resets the cache, which is acceptable: worst case
8
+ * the agent processes one event twice immediately after restart, which is rare
9
+ * and self-limiting.
10
+ */
11
+ export class DeliveryDedupeCache {
12
+ seen = new Map();
13
+ maxEntries;
14
+ ttlMs;
15
+ /** Sweep TTL-expired entries at most once per this interval. The LRU overflow
16
+ * cap already bounds memory, so the TTL sweep need not run on every check —
17
+ * amortizing it keeps the pre-ACK path O(1) under retry storms. */
18
+ sweepEveryMs;
19
+ lastSweepMs = -Infinity;
20
+ constructor(opts = {}) {
21
+ this.maxEntries = opts.maxEntries ?? 4096;
22
+ this.ttlMs = opts.ttlMs ?? 24 * 60 * 60 * 1000;
23
+ this.sweepEveryMs = opts.sweepEveryMs ?? Math.min(this.ttlMs, 15_000);
24
+ }
25
+ /** Returns true if this is a new key (now recorded); false if a duplicate. */
26
+ check(key) {
27
+ const now = Date.now();
28
+ this.evictExpired(now);
29
+ const seenAt = this.seen.get(key);
30
+ // Honor the TTL on the looked-up key directly so amortizing the full sweep
31
+ // never makes an already-expired key read as a live duplicate.
32
+ if (seenAt !== undefined && seenAt >= now - this.ttlMs) {
33
+ this.seen.delete(key);
34
+ this.seen.set(key, now);
35
+ return false;
36
+ }
37
+ if (seenAt !== undefined)
38
+ this.seen.delete(key);
39
+ this.seen.set(key, now);
40
+ if (this.seen.size > this.maxEntries) {
41
+ const first = this.seen.keys().next();
42
+ if (!first.done)
43
+ this.seen.delete(first.value);
44
+ }
45
+ return true;
46
+ }
47
+ size() {
48
+ return this.seen.size;
49
+ }
50
+ clear() {
51
+ this.seen.clear();
52
+ this.lastSweepMs = -Infinity;
53
+ }
54
+ evictExpired(now) {
55
+ if (now - this.lastSweepMs < this.sweepEveryMs)
56
+ return;
57
+ this.lastSweepMs = now;
58
+ const cutoff = now - this.ttlMs;
59
+ for (const [k, ts] of this.seen) {
60
+ if (ts >= cutoff)
61
+ break;
62
+ this.seen.delete(k);
63
+ }
64
+ }
65
+ }
66
+ //# sourceMappingURL=dedupe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/ingest/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,OAAO,mBAAmB;IACb,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACjC,UAAU,CAAS;IACnB,KAAK,CAAS;IAC/B;;wEAEoE;IACnD,YAAY,CAAS;IAC9B,WAAW,GAAG,CAAC,QAAQ,CAAC;IAEhC,YAAY,OAAuE,EAAE;QACnF,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAC/C,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACxE,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAAC,GAAW;QACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,2EAA2E;QAC3E,+DAA+D;QAC/D,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACvD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACxB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YACtC,IAAI,CAAC,KAAK,CAAC,IAAI;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,CAAC,WAAW,GAAG,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAEO,YAAY,CAAC,GAAW;QAC9B,IAAI,GAAG,GAAG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY;YAAE,OAAO;QACvD,IAAI,CAAC,WAAW,GAAG,GAAG,CAAC;QACvB,MAAM,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC;QAChC,KAAK,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAChC,IAAI,EAAE,IAAI,MAAM;gBAAE,MAAM;YACxB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,76 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ /**
3
+ * HTTP scaffold for inbound-webhook channels (Slack Events API today; Discord /
4
+ * WhatsApp / Signal-style ingest next). Binds a `node:http` server to an
5
+ * ephemeral loopback port; the channel exposes it publicly via a tunnel.
6
+ *
7
+ * The scaffold owns the gates that must run BEFORE any payload is trusted
8
+ * (skill A8/A46) and before the session is ever touched:
9
+ * 1. routing — POST-only on `eventsPath` (+ an optional GET health probe);
10
+ * anything else is 404.
11
+ * 2. raw body read, size-capped → 413 (the raw bytes are what signature
12
+ * schemes verify — never a reserialized JSON).
13
+ * 3. the `verify` hook → 401. Provider-specific signature schemes (Slack's
14
+ * HMAC, Discord's ed25519, ...) stay in the channel as this hook.
15
+ * 4. every handler error is caught → 500, so a bad request can never
16
+ * escalate to a process-level uncaughtException.
17
+ *
18
+ * Everything AFTER verification — schema validation, handshake echoes, dedupe,
19
+ * the immediate ACK, and fire-and-forget turn dispatch — is the channel's
20
+ * `handleVerified`, which owns writing the response.
21
+ */
22
+ export type IngestVerdict = {
23
+ readonly ok: true;
24
+ } | {
25
+ readonly ok: false;
26
+ readonly reason: string;
27
+ };
28
+ export interface IngestLogger {
29
+ info?(msg: string, meta?: Record<string, unknown>): void;
30
+ warn?(msg: string, meta?: Record<string, unknown>): void;
31
+ }
32
+ export interface IngestHttpServerOptions {
33
+ /** Bind host. Default 127.0.0.1 (loopback; a tunnel provides public reach). */
34
+ readonly host?: string;
35
+ /** POST endpoint the provider delivers events to (e.g. '/slack/events'). */
36
+ readonly eventsPath: string;
37
+ /** Optional GET liveness probe (e.g. '/slack/health'). */
38
+ readonly healthPath?: string;
39
+ /** Health probe response body. Default `{ status: 'ok' }`. */
40
+ readonly healthBody?: () => unknown;
41
+ /** Max request body size in bytes. Default 1MB. */
42
+ readonly maxBodyBytes?: number;
43
+ /** Short channel label prefixed onto log lines (e.g. 'slack'). */
44
+ readonly label: string;
45
+ /** Signature gate over the EXACT raw bytes, before any parsing. */
46
+ readonly verify: (input: {
47
+ rawBody: Buffer;
48
+ headers: IncomingMessage['headers'];
49
+ }) => IngestVerdict;
50
+ /**
51
+ * The channel-specific pipeline for a verified delivery (parse → dedupe →
52
+ * ACK → dispatch). MUST write the response.
53
+ */
54
+ readonly handleVerified: (rawBody: Buffer, req: IncomingMessage, res: ServerResponse) => Promise<void>;
55
+ readonly logger?: IngestLogger;
56
+ }
57
+ export interface IngestHttpServerHandle {
58
+ readonly host: string;
59
+ readonly port: number;
60
+ stop(): Promise<void>;
61
+ }
62
+ export declare function respondJson(res: ServerResponse, status: number, body: unknown): void;
63
+ export declare class IngestHttpServer {
64
+ private readonly opts;
65
+ private server;
66
+ private readonly host;
67
+ private readonly maxBodyBytes;
68
+ private boundPort;
69
+ constructor(opts: IngestHttpServerOptions);
70
+ get port(): number;
71
+ /** Bind on an ephemeral loopback port. Resolves once listening. */
72
+ start(): Promise<IngestHttpServerHandle>;
73
+ stop(): Promise<void>;
74
+ private handle;
75
+ }
76
+ //# sourceMappingURL=http-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../src/ingest/http-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAe,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAIjG;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpG,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED,MAAM,WAAW,uBAAuB;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IACpC,mDAAmD;IACnD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;KACrC,KAAK,aAAa,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,CACvB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAGpF;AAED,qBAAa,gBAAgB;IAMf,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,SAAS,CAAK;gBAEO,IAAI,EAAE,uBAAuB;IAK1D,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,mEAAmE;IAC7D,KAAK,IAAI,OAAO,CAAC,sBAAsB,CAAC;IAuCxC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAOb,MAAM;CAqCrB"}
@@ -0,0 +1,101 @@
1
+ import { createServer } from 'node:http';
2
+ import { readRequestBody } from '@moxxy/sdk/server';
3
+ export function respondJson(res, status, body) {
4
+ res.writeHead(status, { 'content-type': 'application/json' });
5
+ res.end(JSON.stringify(body));
6
+ }
7
+ export class IngestHttpServer {
8
+ opts;
9
+ server = null;
10
+ host;
11
+ maxBodyBytes;
12
+ boundPort = 0;
13
+ constructor(opts) {
14
+ this.opts = opts;
15
+ this.host = opts.host ?? '127.0.0.1';
16
+ this.maxBodyBytes = opts.maxBodyBytes ?? 1024 * 1024;
17
+ }
18
+ get port() {
19
+ return this.boundPort;
20
+ }
21
+ /** Bind on an ephemeral loopback port. Resolves once listening. */
22
+ async start() {
23
+ if (this.server) {
24
+ return { host: this.host, port: this.boundPort, stop: () => this.stop() };
25
+ }
26
+ const server = createServer((req, res) => {
27
+ this.handle(req, res).catch((err) => {
28
+ this.opts.logger?.warn?.(`${this.opts.label}: handler threw`, { err: String(err) });
29
+ if (!res.headersSent) {
30
+ respondJson(res, 500, { error: 'internal' });
31
+ }
32
+ else {
33
+ try {
34
+ res.end();
35
+ }
36
+ catch {
37
+ /* ignore */
38
+ }
39
+ }
40
+ });
41
+ });
42
+ this.server = server;
43
+ await new Promise((resolve, reject) => {
44
+ const onError = (err) => reject(err);
45
+ server.once('error', onError);
46
+ // port 0 → OS assigns an ephemeral port; we read it back for the tunnel.
47
+ server.listen(0, this.host, () => {
48
+ server.off('error', onError);
49
+ const addr = server.address();
50
+ this.boundPort = addr && typeof addr === 'object' ? addr.port : 0;
51
+ this.opts.logger?.info?.(`${this.opts.label}: ingest listening`, {
52
+ host: this.host,
53
+ port: this.boundPort,
54
+ });
55
+ resolve();
56
+ });
57
+ });
58
+ return { host: this.host, port: this.boundPort, stop: () => this.stop() };
59
+ }
60
+ async stop() {
61
+ const s = this.server;
62
+ if (!s)
63
+ return;
64
+ this.server = null;
65
+ await new Promise((resolve) => s.close(() => resolve()));
66
+ }
67
+ async handle(req, res) {
68
+ const url = (req.url ?? '/').split('?')[0] ?? '/';
69
+ // Liveness probe (a proxy relay / an operator can hit this).
70
+ if (this.opts.healthPath && req.method === 'GET' && url === this.opts.healthPath) {
71
+ respondJson(res, 200, this.opts.healthBody?.() ?? { status: 'ok' });
72
+ return;
73
+ }
74
+ if (req.method !== 'POST' || url !== this.opts.eventsPath) {
75
+ respondJson(res, 404, { error: 'not_found' });
76
+ return;
77
+ }
78
+ // Raw body bytes (needed for signature verification; BEFORE JSON.parse).
79
+ let raw;
80
+ try {
81
+ raw = await readRequestBody(req, this.maxBodyBytes);
82
+ }
83
+ catch (err) {
84
+ this.opts.logger?.warn?.(`${this.opts.label}: rejected oversized body`, {
85
+ limit: this.maxBodyBytes,
86
+ err: err instanceof Error ? err.message : String(err),
87
+ });
88
+ respondJson(res, 413, { error: 'payload_too_large' });
89
+ return;
90
+ }
91
+ // Signature gate.
92
+ const verdict = this.opts.verify({ rawBody: raw, headers: req.headers });
93
+ if (!verdict.ok) {
94
+ this.opts.logger?.warn?.(`${this.opts.label}: rejected delivery`, { reason: verdict.reason });
95
+ respondJson(res, 401, { error: 'verification_failed' });
96
+ return;
97
+ }
98
+ await this.opts.handleVerified(raw, req, res);
99
+ }
100
+ }
101
+ //# sourceMappingURL=http-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-server.js","sourceRoot":"","sources":["../../src/ingest/http-server.ts"],"names":[],"mappings":"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"}