@a5c-ai/channels-adapter 5.1.1-staging.5ee7f5f98b91

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +661 -0
  3. package/dist/backend.d.ts +7 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +22 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/backends/github.d.ts +16 -0
  8. package/dist/backends/github.d.ts.map +1 -0
  9. package/dist/backends/github.js +357 -0
  10. package/dist/backends/github.js.map +1 -0
  11. package/dist/backends/jira.d.ts +18 -0
  12. package/dist/backends/jira.d.ts.map +1 -0
  13. package/dist/backends/jira.js +256 -0
  14. package/dist/backends/jira.js.map +1 -0
  15. package/dist/backends/webhook.d.ts +25 -0
  16. package/dist/backends/webhook.d.ts.map +1 -0
  17. package/dist/backends/webhook.js +206 -0
  18. package/dist/backends/webhook.js.map +1 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +28 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config.d.ts +10 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +323 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/dedup.d.ts +26 -0
  28. package/dist/dedup.d.ts.map +1 -0
  29. package/dist/dedup.js +68 -0
  30. package/dist/dedup.js.map +1 -0
  31. package/dist/filter.d.ts +9 -0
  32. package/dist/filter.d.ts.map +1 -0
  33. package/dist/filter.js +115 -0
  34. package/dist/filter.js.map +1 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +21 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/poller.d.ts +63 -0
  40. package/dist/poller.d.ts.map +1 -0
  41. package/dist/poller.js +195 -0
  42. package/dist/poller.js.map +1 -0
  43. package/dist/registry.d.ts +22 -0
  44. package/dist/registry.d.ts.map +1 -0
  45. package/dist/registry.js +59 -0
  46. package/dist/registry.js.map +1 -0
  47. package/dist/relay.d.ts +58 -0
  48. package/dist/relay.d.ts.map +1 -0
  49. package/dist/relay.js +172 -0
  50. package/dist/relay.js.map +1 -0
  51. package/dist/runtime.d.ts +27 -0
  52. package/dist/runtime.d.ts.map +1 -0
  53. package/dist/runtime.js +198 -0
  54. package/dist/runtime.js.map +1 -0
  55. package/dist/server.d.ts +64 -0
  56. package/dist/server.d.ts.map +1 -0
  57. package/dist/server.js +217 -0
  58. package/dist/server.js.map +1 -0
  59. package/dist/spawner.d.ts +96 -0
  60. package/dist/spawner.d.ts.map +1 -0
  61. package/dist/spawner.js +368 -0
  62. package/dist/spawner.js.map +1 -0
  63. package/dist/state.d.ts +43 -0
  64. package/dist/state.d.ts.map +1 -0
  65. package/dist/state.js +92 -0
  66. package/dist/state.js.map +1 -0
  67. package/dist/types.d.ts +140 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/types.js +10 -0
  70. package/dist/types.js.map +1 -0
  71. package/package.json +78 -0
@@ -0,0 +1,63 @@
1
+ import type { Backend, ChannelEvent, ReplyToken } from './types.js';
2
+ import type { StateStoreLike } from './state.js';
3
+ type AnySource = Record<string, any>;
4
+ /** A spawner-like seam the poller dispatches surviving events to. */
5
+ export interface SpawnerLike {
6
+ spawn(source: AnySource, event: ChannelEvent): unknown;
7
+ }
8
+ /** A server-like seam the poller emits surviving events to. */
9
+ export interface EmitTargetLike {
10
+ emit(event: {
11
+ content: string;
12
+ meta: Record<string, unknown>;
13
+ }): void | Promise<void>;
14
+ }
15
+ export interface PollerDeps {
16
+ sources: AnySource[];
17
+ resolveBackend: (source: AnySource) => Backend | undefined | Promise<Backend | undefined>;
18
+ stateStore: StateStoreLike;
19
+ server: EmitTargetLike;
20
+ spawner?: SpawnerLike | null;
21
+ encodeReplyTo?: (record: ReplyToken) => string;
22
+ http: (url: string | URL, opts?: Record<string, unknown>) => Promise<unknown>;
23
+ now?: () => Date;
24
+ log?: (...args: unknown[]) => void;
25
+ }
26
+ export declare class Poller {
27
+ sources: AnySource[];
28
+ resolveBackend: PollerDeps['resolveBackend'];
29
+ stateStore: StateStoreLike;
30
+ server: EmitTargetLike;
31
+ spawner: SpawnerLike | null;
32
+ encodeReplyTo: (record: ReplyToken) => string;
33
+ http: PollerDeps['http'];
34
+ now: () => Date;
35
+ log: (...args: unknown[]) => void;
36
+ _byId: Map<string, AnySource>;
37
+ _timers: Map<string, ReturnType<typeof setInterval>>;
38
+ _initialized: Set<string>;
39
+ _active: Map<string, Promise<void>>;
40
+ _pending: Map<string, Promise<void>>;
41
+ constructor({ sources, resolveBackend, stateStore, server, spawner, encodeReplyTo, http, now, log }: PollerDeps);
42
+ /**
43
+ * Run a single poll for one source. Ticks for the SAME source are SERIALIZED
44
+ * AND COALESCED:
45
+ * - if no tick is in flight, this one runs immediately;
46
+ * - if a tick is in flight and NONE is pending, exactly ONE follow-up is
47
+ * scheduled to run after the active one settles;
48
+ * - if a tick is in flight and a follow-up is ALREADY pending, this call is
49
+ * COALESCED onto that single pending tick.
50
+ * Ticks for DIFFERENT sources run concurrently. Errors are isolated so one
51
+ * failing tick doesn't poison the next.
52
+ */
53
+ tick(sourceId: string): Promise<void>;
54
+ /**
55
+ * The actual single-poll body (no serialization). Use `tick()` externally.
56
+ */
57
+ _runTick(sourceId: string): Promise<void>;
58
+ /** Start interval polling for every source. */
59
+ start(): void;
60
+ /** Stop all interval polling. */
61
+ stop(): void;
62
+ }
63
+ export {};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poller.d.ts","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAErC,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;CACxD;AAED,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvF;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,cAAc,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC1F,UAAU,EAAE,cAAc,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC/C,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED,qBAAa,MAAM;IACjB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,cAAc,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC7C,UAAU,EAAE,cAAc,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC9C,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IACzB,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC9B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC;IACrD,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACpC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;gBAEzB,EACV,OAAO,EACP,cAAc,EACd,UAAU,EACV,MAAM,EACN,OAAO,EACP,aAAa,EACb,IAAI,EACJ,GAAG,EACH,GAAG,EACJ,EAAE,UAAU;IAqBb;;;;;;;;;;OAUG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCrC;;OAEG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsF/C,+CAA+C;IAC/C,KAAK,IAAI,IAAI;IAcb,iCAAiC;IACjC,IAAI,IAAI,IAAI;CAMb"}
package/dist/poller.js ADDED
@@ -0,0 +1,195 @@
1
+ // Poller — per-source scheduling + the inbound pipeline (SPEC §3/§6 R5,
2
+ // DESIGN §1/§3.1).
3
+ //
4
+ // For each source, on its scheduled tick (or a test's explicit tick(id)):
5
+ // 1. load state StateStore.get(id) -> { cursor, seen }
6
+ // 2. poll the backend backend.poll({ source, state, http, log, now })
7
+ // 3. FILTER (core) keep events whose payload satisfies source.filter
8
+ // 4. DEDUP (core) drop events whose id is already in seen
9
+ // 5. mint reply_to attach the opaque routing token to event.meta
10
+ // 6. persist state StateStore.set(id, { cursor', seen ∪ survivor ids })
11
+ // 7. EMIT server.emit({ content, meta }) per survivor
12
+ //
13
+ // Filtering happens BEFORE dedup so a non-matching event is simply ignored that
14
+ // tick (not recorded as seen). Dedup is the authoritative at-most-once gate.
15
+ import { compileFilter } from './filter.js';
16
+ import { deriveNew } from './dedup.js';
17
+ import { encodeReplyTo as defaultEncodeReplyTo } from './relay.js';
18
+ export class Poller {
19
+ sources;
20
+ resolveBackend;
21
+ stateStore;
22
+ server;
23
+ spawner;
24
+ encodeReplyTo;
25
+ http;
26
+ now;
27
+ log;
28
+ _byId;
29
+ _timers;
30
+ _initialized;
31
+ _active;
32
+ _pending;
33
+ constructor({ sources, resolveBackend, stateStore, server, spawner, encodeReplyTo, http, now, log }) {
34
+ this.sources = sources || [];
35
+ this.resolveBackend = resolveBackend;
36
+ this.stateStore = stateStore;
37
+ this.server = server;
38
+ this.spawner = spawner || null;
39
+ // The reply_to minter. Injected (secret-bound) by the runtime when a shared
40
+ // replySecret is configured; otherwise the module's per-process function.
41
+ this.encodeReplyTo = typeof encodeReplyTo === 'function' ? encodeReplyTo : defaultEncodeReplyTo;
42
+ this.http = http;
43
+ this.now = now || (() => new Date());
44
+ this.log = log || (() => { });
45
+ this._byId = new Map(this.sources.map((s) => [s.id, s]));
46
+ this._timers = new Map();
47
+ this._initialized = new Set();
48
+ /** the in-flight tick per source. */
49
+ this._active = new Map();
50
+ /** the single COALESCED follow-up per source. */
51
+ this._pending = new Map();
52
+ }
53
+ /**
54
+ * Run a single poll for one source. Ticks for the SAME source are SERIALIZED
55
+ * AND COALESCED:
56
+ * - if no tick is in flight, this one runs immediately;
57
+ * - if a tick is in flight and NONE is pending, exactly ONE follow-up is
58
+ * scheduled to run after the active one settles;
59
+ * - if a tick is in flight and a follow-up is ALREADY pending, this call is
60
+ * COALESCED onto that single pending tick.
61
+ * Ticks for DIFFERENT sources run concurrently. Errors are isolated so one
62
+ * failing tick doesn't poison the next.
63
+ */
64
+ tick(sourceId) {
65
+ // No tick in flight → run now and record it as the active one.
66
+ if (!this._active.has(sourceId)) {
67
+ const run = this._runTick(sourceId)
68
+ .catch(() => { })
69
+ .finally(() => {
70
+ if (this._active.get(sourceId) === run)
71
+ this._active.delete(sourceId);
72
+ });
73
+ this._active.set(sourceId, run);
74
+ return run;
75
+ }
76
+ // A tick is in flight. If one is already pending, coalesce onto it.
77
+ const pending = this._pending.get(sourceId);
78
+ if (pending)
79
+ return pending;
80
+ // Otherwise schedule EXACTLY ONE follow-up after the active tick settles.
81
+ const active = this._active.get(sourceId);
82
+ const next = active
83
+ .then(() => { }, () => { })
84
+ .then(() => {
85
+ // Promote the pending tick to active before running it.
86
+ this._pending.delete(sourceId);
87
+ const run = this._runTick(sourceId)
88
+ .catch(() => { })
89
+ .finally(() => {
90
+ if (this._active.get(sourceId) === run)
91
+ this._active.delete(sourceId);
92
+ });
93
+ this._active.set(sourceId, run);
94
+ return run;
95
+ });
96
+ this._pending.set(sourceId, next);
97
+ return next;
98
+ }
99
+ /**
100
+ * The actual single-poll body (no serialization). Use `tick()` externally.
101
+ */
102
+ async _runTick(sourceId) {
103
+ const source = this._byId.get(sourceId);
104
+ if (!source)
105
+ return;
106
+ const backend = await this.resolveBackend(source);
107
+ if (!backend) {
108
+ this.log(`poller: no backend for source "${sourceId}"`);
109
+ return;
110
+ }
111
+ // One-time init before the first poll of this source.
112
+ if (!this._initialized.has(sourceId)) {
113
+ this._initialized.add(sourceId);
114
+ if (typeof backend.init === 'function') {
115
+ await backend.init(source);
116
+ }
117
+ }
118
+ const state = this.stateStore.get(sourceId);
119
+ const polled = await backend.poll({
120
+ source,
121
+ state,
122
+ http: this.http,
123
+ log: this.log,
124
+ now: this.now()
125
+ });
126
+ const events = Array.isArray(polled?.events) ? polled.events : [];
127
+ // (3) FILTER — core is the authoritative gate.
128
+ const predicate = compileFilter(source.filter);
129
+ const matched = events.filter((e) => predicate(e?.payload));
130
+ // (4) DEDUP — at-most-once across overlapping windows.
131
+ const { fresh, seen } = deriveNew(matched, {
132
+ idOf: (e) => e.id,
133
+ seen: state.seen || []
134
+ });
135
+ // (6) persist: cursor advances; seen is the union (store enforces the bound).
136
+ // Protect this tick's window ids from FIFO eviction so a boundary id still
137
+ // inside the cursor window can't be pruned and then re-emitted next poll
138
+ // (finding §3). The boundary bucket is a subset of the current window, so
139
+ // passing every matched id is a safe superset to keep.
140
+ const nextCursor = polled?.state?.cursor ?? null;
141
+ const keepSeen = matched.map((e) => e.id);
142
+ await this.stateStore.set(sourceId, { cursor: nextCursor, seen, keepSeen });
143
+ // (5) mint reply_to + (7) DISPATCH each survivor per the source's effective
144
+ // onEvent (DESIGN §7.6): emit -> server.emit, spawn -> spawner.spawn, both ->
145
+ // both. The SAME minted reply_to is shared between the emitted meta and the
146
+ // spawned event. Default is 'emit' (today's behavior) so existing sources
147
+ // never spawn.
148
+ const action = source.onEvent || 'emit';
149
+ const doEmit = action === 'emit' || action === 'both';
150
+ const doSpawn = (action === 'spawn' || action === 'both') && this.spawner;
151
+ for (const ev of fresh) {
152
+ const reply_to = this.encodeReplyTo({
153
+ sourceId,
154
+ backendType: backend.type || source.backend,
155
+ routing: ev.routing || {}
156
+ });
157
+ // The event carrying the minted reply_to in its meta (shared by both paths).
158
+ const meta = { ...(ev.meta || {}), reply_to };
159
+ if (doEmit) {
160
+ await this.server.emit({ content: ev.content, meta });
161
+ }
162
+ if (doSpawn) {
163
+ // Spawn is dispatched but NOT awaited to completion (the spawner bounds
164
+ // concurrency + isolates errors internally), so a slow/failing launch
165
+ // never stalls the tick or blocks sibling events.
166
+ const spawnEvent = { ...ev, meta };
167
+ const p = this.spawner.spawn(source, spawnEvent);
168
+ if (p && typeof p.catch === 'function') {
169
+ p.catch((err) => this.log(`poller: spawn dispatch failed for "${sourceId}": ${err?.message || err}`));
170
+ }
171
+ }
172
+ }
173
+ }
174
+ /** Start interval polling for every source. */
175
+ start() {
176
+ for (const source of this.sources) {
177
+ const ms = Math.max(1, Number(source.pollIntervalSeconds) || 60) * 1000;
178
+ const timer = setInterval(() => {
179
+ this.tick(source.id).catch((err) => this.log(`poller: tick("${source.id}") failed: ${err?.message || err}`));
180
+ }, ms);
181
+ // Don't keep the process alive solely for polling.
182
+ if (typeof timer.unref === 'function')
183
+ timer.unref();
184
+ this._timers.set(source.id, timer);
185
+ }
186
+ }
187
+ /** Stop all interval polling. */
188
+ stop() {
189
+ for (const timer of this._timers.values()) {
190
+ clearInterval(timer);
191
+ }
192
+ this._timers.clear();
193
+ }
194
+ }
195
+ //# sourceMappingURL=poller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poller.js","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { Backend } from './types.js';
2
+ declare class Registry {
3
+ _map: Map<string, Backend>;
4
+ constructor();
5
+ /**
6
+ * Register a backend programmatically.
7
+ */
8
+ register(type: string, backend: Backend): Backend;
9
+ /**
10
+ * Look up a backend by type. Returns `undefined` for an unknown type (never
11
+ * throws).
12
+ */
13
+ get(type: string): Backend | undefined;
14
+ /**
15
+ * Dynamically import a custom backend referenced by (relative) path, resolving
16
+ * it against `baseDir`. Validates the module exposes the hook interface.
17
+ */
18
+ load(path: string, baseDir: string): Promise<Backend>;
19
+ }
20
+ /** The shared registry instance (built-ins pre-registered). */
21
+ export declare const registry: Registry;
22
+ export { Registry };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAS1C,cAAM,QAAQ;IACZ,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;;IAS3B;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO;IAKjD;;;OAGG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAItC;;;OAGG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAO5D;AAED,+DAA+D;AAC/D,eAAO,MAAM,QAAQ,UAAiB,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,59 @@
1
+ // Backend registry (SPEC §3/§6 R2, DESIGN §1).
2
+ //
3
+ // Maps a backend `type` -> backend module. Built-ins `github` and `jira` are
4
+ // pre-registered as the REAL backend modules (statically imported), so a single
5
+ // module instance is shared everywhere: `registry.get('github')` returns the same
6
+ // object that `import github from './backends/github.js'` yields.
7
+ //
8
+ // Custom backends referenced from YAML by relative path are loaded on demand via
9
+ // `registry.load(path, baseDir)` (dynamic import, resolved against the config dir).
10
+ import { pathToFileURL } from 'node:url';
11
+ import { resolve, isAbsolute } from 'node:path';
12
+ import { defineBackend } from './backend.js';
13
+ import githubBackend from './backends/github.js';
14
+ import jiraBackend from './backends/jira.js';
15
+ import webhookBackend from './backends/webhook.js';
16
+ /** Built-in backend type -> backend module. */
17
+ const BUILTINS = {
18
+ github: githubBackend,
19
+ jira: jiraBackend,
20
+ webhook: webhookBackend
21
+ };
22
+ class Registry {
23
+ _map;
24
+ constructor() {
25
+ this._map = new Map();
26
+ for (const [type, backend] of Object.entries(BUILTINS)) {
27
+ this._map.set(type, backend);
28
+ }
29
+ }
30
+ /**
31
+ * Register a backend programmatically.
32
+ */
33
+ register(type, backend) {
34
+ this._map.set(type, backend);
35
+ return backend;
36
+ }
37
+ /**
38
+ * Look up a backend by type. Returns `undefined` for an unknown type (never
39
+ * throws).
40
+ */
41
+ get(type) {
42
+ return this._map.get(type);
43
+ }
44
+ /**
45
+ * Dynamically import a custom backend referenced by (relative) path, resolving
46
+ * it against `baseDir`. Validates the module exposes the hook interface.
47
+ */
48
+ async load(path, baseDir) {
49
+ const abs = isAbsolute(path) ? path : resolve(baseDir || process.cwd(), path);
50
+ const mod = (await import(pathToFileURL(abs).href));
51
+ const backend = (mod.default ?? mod);
52
+ // Validate it really is a backend (throws a clear error if not).
53
+ return defineBackend(backend);
54
+ }
55
+ }
56
+ /** The shared registry instance (built-ins pre-registered). */
57
+ export const registry = new Registry();
58
+ export { Registry };
59
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,kFAAkF;AAClF,kEAAkE;AAClE,EAAE;AACF,iFAAiF;AACjF,oFAAoF;AAEpF,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,aAAa,MAAM,sBAAsB,CAAC;AACjD,OAAO,WAAW,MAAM,oBAAoB,CAAC;AAC7C,OAAO,cAAc,MAAM,uBAAuB,CAAC;AAGnD,+CAA+C;AAC/C,MAAM,QAAQ,GAA4B;IACxC,MAAM,EAAE,aAAa;IACrB,IAAI,EAAE,WAAW;IACjB,OAAO,EAAE,cAAc;CACxB,CAAC;AAEF,MAAM,QAAQ;IACZ,IAAI,CAAuB;IAE3B;QACE,IAAI,CAAC,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACvD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED;;OAEG;IACH,QAAQ,CAAC,IAAY,EAAE,OAAgB;QACrC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC7B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,OAAe;QACtC,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;QAC9E,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAA6C,CAAC;QAChG,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAqB,CAAC;QACzD,iEAAiE;QACjE,OAAO,aAAa,CAAC,OAAO,CAAY,CAAC;IAC3C,CAAC;CACF;AAED,+DAA+D;AAC/D,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { ReplyToken, ReplyResult } from './types.js';
2
+ /**
3
+ * Encode a routing record into an opaque, URL-safe, tamper-evident token of the
4
+ * form `<payload>.<sig>` where `payload = base64url(JSON(record))` and
5
+ * `sig = base64url(HMAC-SHA256(payload))`.
6
+ *
7
+ * The optional trailing `secret` (DESIGN §7.4) derives the signing key so a
8
+ * second process with the SAME secret can verify the token. Omitted ⇒ the
9
+ * per-process random key (existing single-arg behavior is unchanged).
10
+ */
11
+ export declare function encodeReplyTo(record: ReplyToken | unknown, secret?: string): string;
12
+ /**
13
+ * Decode an opaque token back into the routing record, VERIFYING the HMAC first.
14
+ * Returns `null` for any garbage (missing/invalid signature, bad base64, non-JSON,
15
+ * empty/nullish, or a tampered payload) — NEVER throws (AC-16).
16
+ *
17
+ * The optional trailing `secret` must match the one used to encode (DESIGN §7.4):
18
+ * a token minted under secret A does not verify under secret B (or the default),
19
+ * yielding `null`. Omitted ⇒ the per-process random key (unchanged behavior).
20
+ */
21
+ export declare function decodeReplyTo(token: unknown, secret?: string): ReplyToken | null;
22
+ /** Arguments to `dispatchReply`. */
23
+ export interface DispatchArgs {
24
+ reply_to: string;
25
+ text: string;
26
+ resolveSource: (id: string) => Record<string, unknown> | undefined;
27
+ resolveBackend: (type: string, sourceId?: string) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;
28
+ http: (url: string | URL, opts?: Record<string, unknown>) => Promise<unknown>;
29
+ /** Shared secret to decode under (DESIGN §7.4). */
30
+ secret?: string;
31
+ }
32
+ /** A relay bound to an optional shared secret. */
33
+ export interface BoundRelay {
34
+ encodeReplyTo: (record: ReplyToken) => string;
35
+ decodeReplyTo: (token: unknown) => ReplyToken | null;
36
+ dispatchReply: (args: Omit<DispatchArgs, 'secret'>) => Promise<ReplyResult>;
37
+ }
38
+ /**
39
+ * Bind a shared `secret` to a relay instance (DESIGN §7.4). Returns
40
+ * `{ encodeReplyTo, decodeReplyTo, dispatchReply }` whose encode/decode use the
41
+ * derived key (and dispatchReply decodes under it), so the running instance signs
42
+ * AND verifies with the shared key. With no secret, this is exactly the
43
+ * per-process behavior of the module-level functions.
44
+ */
45
+ export declare function createRelay(secret?: string): BoundRelay;
46
+ /**
47
+ * Decode a reply token, look up the owning source + backend, and dispatch the
48
+ * reply to that backend. The single source of truth for the reply path; the
49
+ * runtime's reply handler delegates here. Returns the backend's `{ ok, ref? }`;
50
+ * an unknown/garbled/forged token or a missing source/backend yields
51
+ * `{ ok: false }` WITHOUT throwing (AC-16).
52
+ *
53
+ * The token's `sourceId` is passed as the second argument to `resolveBackend` so
54
+ * the caller can route to the EXACT owning source's backend (rather than the
55
+ * first backend matching `type`), which keeps two custom backends that share a
56
+ * `type` from cross-dispatching.
57
+ */
58
+ export declare function dispatchReply({ reply_to, text, resolveSource, resolveBackend, http, secret }: DispatchArgs): Promise<ReplyResult>;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA8C1D;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAInF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6BhF;AAED,oCAAoC;AACpC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACnE,cAAc,EAAE,CACd,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,KAEf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACvB,SAAS,GACT,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IACjD,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC9C,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,UAAU,GAAG,IAAI,CAAC;IACrD,aAAa,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAC7E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,EAClC,QAAQ,EACR,IAAI,EACJ,aAAa,EACb,cAAc,EACd,IAAI,EACJ,MAAM,EACP,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BrC"}
package/dist/relay.js ADDED
@@ -0,0 +1,172 @@
1
+ // The opaque, TAMPER-EVIDENT `reply_to` routing token + reply dispatch
2
+ // (SPEC §4, DESIGN §4).
3
+ //
4
+ // A reply must reach the EXACT origin (this repo + issue number, or this Jira
5
+ // key). Claude forwards only one small attribute back to us and inbound channel
6
+ // text is an untrusted prompt-injection surface, so we must not let model-authored
7
+ // text dictate where a write goes. The framework mints a single opaque token per
8
+ // event (`meta.reply_to`); Claude treats it as a black box and echoes it back
9
+ // when calling the `reply` tool. Only the framework interprets it.
10
+ //
11
+ // The token is `<base64url(JSON)>.<base64url(HMAC-SHA256)>` — URL-safe, a valid
12
+ // meta identifier value (so it survives the meta-key sanitizer), and OPAQUE
13
+ // (Claude can't read or mutate the routing target). The HMAC is computed over the
14
+ // encoded payload with a PER-PROCESS random secret generated at startup, so a
15
+ // forged/tampered token (e.g. a flipped char, or a hand-rolled base64url(JSON))
16
+ // fails verification and is rejected. It is a *routing* token, not a bearer
17
+ // secret: it carries no credentials (auth lives only in the loaded config, keyed
18
+ // by source id) — the HMAC exists purely so the runtime never POSTs under real
19
+ // credentials to a routing target it didn't itself mint.
20
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
21
+ // Per-process signing secret, generated once at module load. It never leaves the
22
+ // process and is not derivable from a token, so tokens minted by this process
23
+ // cannot be forged by anyone who hasn't seen the secret. A fresh secret each run
24
+ // is fine for the single-process model: reply_to tokens are only meaningful
25
+ // within the lifetime of the running server (the in-memory source/backend tables
26
+ // they reference are process-local).
27
+ //
28
+ // SPEC §10 AC-21 / DESIGN §7.4: a SHARED secret makes a token portable across
29
+ // processes (a spawned child must verify a token its parent minted). When a
30
+ // non-empty secret is supplied, the HMAC key is DERIVED from it deterministically
31
+ // (sha256(secret) -> 32 bytes) so two processes with the same secret produce
32
+ // identical signatures. When no secret is supplied, this per-process random key
33
+ // is used and behavior is identical to before (backward compatible).
34
+ const REPLY_SECRET = randomBytes(32);
35
+ /** Cache of derived 32-byte keys per shared secret string (avoid re-hashing). */
36
+ const KEY_CACHE = new Map();
37
+ /**
38
+ * Resolve the HMAC key: the derived key from a non-empty `secret`, else the
39
+ * per-process random key. A derived key is `sha256(secret)` (32 bytes), cached.
40
+ */
41
+ function keyFor(secret) {
42
+ if (typeof secret === 'string' && secret.length > 0) {
43
+ let key = KEY_CACHE.get(secret);
44
+ if (!key) {
45
+ key = createHash('sha256').update(secret, 'utf8').digest();
46
+ KEY_CACHE.set(secret, key);
47
+ }
48
+ return key;
49
+ }
50
+ return REPLY_SECRET;
51
+ }
52
+ /** base64url-encode a Buffer/string without padding ambiguity. */
53
+ function b64url(buf) {
54
+ return Buffer.from(buf).toString('base64url');
55
+ }
56
+ /** Compute the HMAC-SHA256 of `encodedPayload` (under `key`) as base64url. */
57
+ function sign(encodedPayload, key) {
58
+ return createHmac('sha256', key).update(encodedPayload).digest('base64url');
59
+ }
60
+ /**
61
+ * Encode a routing record into an opaque, URL-safe, tamper-evident token of the
62
+ * form `<payload>.<sig>` where `payload = base64url(JSON(record))` and
63
+ * `sig = base64url(HMAC-SHA256(payload))`.
64
+ *
65
+ * The optional trailing `secret` (DESIGN §7.4) derives the signing key so a
66
+ * second process with the SAME secret can verify the token. Omitted ⇒ the
67
+ * per-process random key (existing single-arg behavior is unchanged).
68
+ */
69
+ export function encodeReplyTo(record, secret) {
70
+ const json = JSON.stringify(record);
71
+ const payload = b64url(Buffer.from(json, 'utf8'));
72
+ return `${payload}.${sign(payload, keyFor(secret))}`;
73
+ }
74
+ /**
75
+ * Decode an opaque token back into the routing record, VERIFYING the HMAC first.
76
+ * Returns `null` for any garbage (missing/invalid signature, bad base64, non-JSON,
77
+ * empty/nullish, or a tampered payload) — NEVER throws (AC-16).
78
+ *
79
+ * The optional trailing `secret` must match the one used to encode (DESIGN §7.4):
80
+ * a token minted under secret A does not verify under secret B (or the default),
81
+ * yielding `null`. Omitted ⇒ the per-process random key (unchanged behavior).
82
+ */
83
+ export function decodeReplyTo(token, secret) {
84
+ if (typeof token !== 'string' || token.length === 0)
85
+ return null;
86
+ const dot = token.lastIndexOf('.');
87
+ if (dot <= 0 || dot === token.length - 1)
88
+ return null;
89
+ const payload = token.slice(0, dot);
90
+ const provided = token.slice(dot + 1);
91
+ // Verify the signature in constant time. A flipped char in EITHER half makes
92
+ // the buffers differ (or differ in length) and the token is rejected.
93
+ let expectedBuf;
94
+ let providedBuf;
95
+ try {
96
+ expectedBuf = Buffer.from(sign(payload, keyFor(secret)), 'utf8');
97
+ providedBuf = Buffer.from(provided, 'utf8');
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ if (expectedBuf.length !== providedBuf.length)
103
+ return null;
104
+ if (!timingSafeEqual(expectedBuf, providedBuf))
105
+ return null;
106
+ try {
107
+ const json = Buffer.from(payload, 'base64url').toString('utf8');
108
+ const obj = JSON.parse(json);
109
+ if (obj == null || typeof obj !== 'object')
110
+ return null;
111
+ return obj;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * Bind a shared `secret` to a relay instance (DESIGN §7.4). Returns
119
+ * `{ encodeReplyTo, decodeReplyTo, dispatchReply }` whose encode/decode use the
120
+ * derived key (and dispatchReply decodes under it), so the running instance signs
121
+ * AND verifies with the shared key. With no secret, this is exactly the
122
+ * per-process behavior of the module-level functions.
123
+ */
124
+ export function createRelay(secret) {
125
+ return {
126
+ encodeReplyTo: (record) => encodeReplyTo(record, secret),
127
+ decodeReplyTo: (token) => decodeReplyTo(token, secret),
128
+ dispatchReply: (args) => dispatchReply({ ...args, secret })
129
+ };
130
+ }
131
+ /**
132
+ * Decode a reply token, look up the owning source + backend, and dispatch the
133
+ * reply to that backend. The single source of truth for the reply path; the
134
+ * runtime's reply handler delegates here. Returns the backend's `{ ok, ref? }`;
135
+ * an unknown/garbled/forged token or a missing source/backend yields
136
+ * `{ ok: false }` WITHOUT throwing (AC-16).
137
+ *
138
+ * The token's `sourceId` is passed as the second argument to `resolveBackend` so
139
+ * the caller can route to the EXACT owning source's backend (rather than the
140
+ * first backend matching `type`), which keeps two custom backends that share a
141
+ * `type` from cross-dispatching.
142
+ */
143
+ export async function dispatchReply({ reply_to, text, resolveSource, resolveBackend, http, secret }) {
144
+ const decoded = decodeReplyTo(reply_to, secret);
145
+ if (!decoded || !decoded.backendType)
146
+ return { ok: false };
147
+ const source = resolveSource(decoded.sourceId);
148
+ if (!source)
149
+ return { ok: false };
150
+ let backend;
151
+ try {
152
+ backend = await resolveBackend(decoded.backendType, decoded.sourceId);
153
+ }
154
+ catch {
155
+ return { ok: false };
156
+ }
157
+ if (!backend || typeof backend.reply !== 'function')
158
+ return { ok: false };
159
+ try {
160
+ const result = await backend.reply({
161
+ routing: decoded.routing,
162
+ text,
163
+ source,
164
+ http
165
+ });
166
+ return result || { ok: false };
167
+ }
168
+ catch {
169
+ return { ok: false };
170
+ }
171
+ }
172
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { ChannelServer } from './server.js';
2
+ import { Poller } from './poller.js';
3
+ import { type AdaptersClientLike } from './spawner.js';
4
+ import type { ChannelsConfig } from './types.js';
5
+ import type { StateStoreLike } from './state.js';
6
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
7
+ export interface RuntimeDeps {
8
+ http?: (url: string | URL, opts?: Record<string, unknown>) => Promise<unknown>;
9
+ stateStore?: StateStoreLike;
10
+ now?: () => Date;
11
+ transport?: Transport;
12
+ env?: Record<string, string>;
13
+ client?: AdaptersClientLike;
14
+ loadClient?: () => Promise<AdaptersClientLike> | AdaptersClientLike;
15
+ resolveCliPath?: () => string;
16
+ permissionHandler?: (req: unknown) => 'allow' | 'deny' | Promise<'allow' | 'deny'>;
17
+ log?: (...args: unknown[]) => void;
18
+ }
19
+ export interface Runtime {
20
+ server: ChannelServer;
21
+ poller: Poller;
22
+ config: ChannelsConfig;
23
+ errors: string[];
24
+ start(): Promise<void>;
25
+ stop(): Promise<void>;
26
+ }
27
+ export declare function createRuntime(configPathOrYaml: string, deps?: RuntimeDeps): Promise<Runtime>;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EAAW,cAAc,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAI/E,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/E,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAC;IACpE,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC;IACnF,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAcD,wBAAsB,aAAa,CACjC,gBAAgB,EAAE,MAAM,EACxB,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,OAAO,CAAC,CA2JlB"}