@n2world/orchestrator 1.1.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 (154) hide show
  1. package/dist/agent-os-rd.d.ts +100 -0
  2. package/dist/agent-os-rd.js +258 -0
  3. package/dist/audit-store.d.ts +14 -0
  4. package/dist/audit-store.js +107 -0
  5. package/dist/beta-runner.d.ts +95 -0
  6. package/dist/beta-runner.js +251 -0
  7. package/dist/beta.d.ts +102 -0
  8. package/dist/beta.js +180 -0
  9. package/dist/browser-agent.d.ts +90 -0
  10. package/dist/browser-agent.js +223 -0
  11. package/dist/channel-gateway.d.ts +74 -0
  12. package/dist/channel-gateway.js +270 -0
  13. package/dist/channels.d.ts +120 -0
  14. package/dist/channels.js +432 -0
  15. package/dist/chat-store.d.ts +29 -0
  16. package/dist/chat-store.js +120 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +607 -0
  19. package/dist/command-screen.d.ts +12 -0
  20. package/dist/command-screen.js +44 -0
  21. package/dist/commit-gate.d.ts +98 -0
  22. package/dist/commit-gate.js +258 -0
  23. package/dist/companion-api.d.ts +37 -0
  24. package/dist/companion-api.js +101 -0
  25. package/dist/conversation-graph.d.ts +39 -0
  26. package/dist/conversation-graph.js +92 -0
  27. package/dist/cost-estimator.d.ts +27 -0
  28. package/dist/cost-estimator.js +42 -0
  29. package/dist/cron-runner.d.ts +31 -0
  30. package/dist/cron-runner.js +46 -0
  31. package/dist/dashboard/chat.html +326 -0
  32. package/dist/dashboard/dental.html +58 -0
  33. package/dist/dashboard/freebie.png +0 -0
  34. package/dist/dashboard/icon-192.png +0 -0
  35. package/dist/dashboard/index.html +892 -0
  36. package/dist/dashboard/manifest.json +15 -0
  37. package/dist/dashboard/service-worker.js +28 -0
  38. package/dist/dashboard-server.d.ts +37 -0
  39. package/dist/dashboard-server.js +457 -0
  40. package/dist/dental-intake-service.d.ts +37 -0
  41. package/dist/dental-intake-service.js +61 -0
  42. package/dist/dental-metrics.d.ts +25 -0
  43. package/dist/dental-metrics.js +37 -0
  44. package/dist/docking.d.ts +36 -0
  45. package/dist/docking.js +73 -0
  46. package/dist/finance-mcts-candidate.d.ts +37 -0
  47. package/dist/finance-mcts-candidate.js +106 -0
  48. package/dist/finance-regulation-kr.d.ts +33 -0
  49. package/dist/finance-regulation-kr.js +104 -0
  50. package/dist/finance-workflow.d.ts +135 -0
  51. package/dist/finance-workflow.js +242 -0
  52. package/dist/gateway.d.ts +18 -0
  53. package/dist/gateway.js +123 -0
  54. package/dist/governance.d.ts +39 -0
  55. package/dist/governance.js +48 -0
  56. package/dist/governed-executor.d.ts +31 -0
  57. package/dist/governed-executor.js +63 -0
  58. package/dist/governed-llm.d.ts +41 -0
  59. package/dist/governed-llm.js +83 -0
  60. package/dist/gpu-bridge.d.ts +16 -0
  61. package/dist/gpu-bridge.js +53 -0
  62. package/dist/health.d.ts +47 -0
  63. package/dist/health.js +66 -0
  64. package/dist/identity-link.d.ts +32 -0
  65. package/dist/identity-link.js +98 -0
  66. package/dist/index.d.ts +184 -0
  67. package/dist/index.js +417 -0
  68. package/dist/integrations/emr-adapter.d.ts +41 -0
  69. package/dist/integrations/emr-adapter.js +63 -0
  70. package/dist/kakao-oauth.d.ts +16 -0
  71. package/dist/kakao-oauth.js +87 -0
  72. package/dist/knowledge-graph.d.ts +53 -0
  73. package/dist/knowledge-graph.js +156 -0
  74. package/dist/llm.d.ts +65 -0
  75. package/dist/llm.js +357 -0
  76. package/dist/mcp-client-guard.d.ts +32 -0
  77. package/dist/mcp-client-guard.js +179 -0
  78. package/dist/mcp-macaroon.d.ts +75 -0
  79. package/dist/mcp-macaroon.js +161 -0
  80. package/dist/mcts-kernel-bridge.d.ts +36 -0
  81. package/dist/mcts-kernel-bridge.js +99 -0
  82. package/dist/mcts-prior.d.ts +79 -0
  83. package/dist/mcts-prior.js +170 -0
  84. package/dist/model-router.d.ts +51 -0
  85. package/dist/model-router.js +75 -0
  86. package/dist/multi-axis-lift.d.ts +43 -0
  87. package/dist/multi-axis-lift.js +141 -0
  88. package/dist/net-guard.d.ts +39 -0
  89. package/dist/net-guard.js +141 -0
  90. package/dist/onboarding.d.ts +38 -0
  91. package/dist/onboarding.js +94 -0
  92. package/dist/oracle-anchored-search.d.ts +25 -0
  93. package/dist/oracle-anchored-search.js +50 -0
  94. package/dist/oracle.d.ts +22 -0
  95. package/dist/oracle.js +116 -0
  96. package/dist/p6-governance.d.ts +150 -0
  97. package/dist/p6-governance.js +252 -0
  98. package/dist/pairing.d.ts +22 -0
  99. package/dist/pairing.js +81 -0
  100. package/dist/personalization.d.ts +35 -0
  101. package/dist/personalization.js +73 -0
  102. package/dist/pglite-hnsw-bridge.d.ts +118 -0
  103. package/dist/pglite-hnsw-bridge.js +311 -0
  104. package/dist/pglite-store.d.ts +59 -0
  105. package/dist/pglite-store.js +180 -0
  106. package/dist/playbook.d.ts +79 -0
  107. package/dist/playbook.js +83 -0
  108. package/dist/playbooks/dental-intake.d.ts +20 -0
  109. package/dist/playbooks/dental-intake.js +112 -0
  110. package/dist/predictive-agent.d.ts +157 -0
  111. package/dist/predictive-agent.js +535 -0
  112. package/dist/prompt-optimizer.d.ts +18 -0
  113. package/dist/prompt-optimizer.js +104 -0
  114. package/dist/rate-limiter.d.ts +25 -0
  115. package/dist/rate-limiter.js +75 -0
  116. package/dist/safety-anneal.d.ts +83 -0
  117. package/dist/safety-anneal.js +153 -0
  118. package/dist/sandbox-controller.d.ts +12 -0
  119. package/dist/sandbox-controller.js +95 -0
  120. package/dist/satisfaction-metrics.d.ts +26 -0
  121. package/dist/satisfaction-metrics.js +61 -0
  122. package/dist/sensor-bridge.d.ts +53 -0
  123. package/dist/sensor-bridge.js +133 -0
  124. package/dist/session-repair.d.ts +27 -0
  125. package/dist/session-repair.js +66 -0
  126. package/dist/slack-finance-intake.d.ts +42 -0
  127. package/dist/slack-finance-intake.js +122 -0
  128. package/dist/symbolic-dynamics.d.ts +113 -0
  129. package/dist/symbolic-dynamics.js +420 -0
  130. package/dist/telemetry.d.ts +19 -0
  131. package/dist/telemetry.js +68 -0
  132. package/dist/text-embedding.d.ts +6 -0
  133. package/dist/text-embedding.js +42 -0
  134. package/dist/tier-classifier.d.ts +20 -0
  135. package/dist/tier-classifier.js +58 -0
  136. package/dist/tier-guard.d.ts +36 -0
  137. package/dist/tier-guard.js +56 -0
  138. package/dist/tui.d.ts +9 -0
  139. package/dist/tui.js +214 -0
  140. package/dist/update-security.d.ts +31 -0
  141. package/dist/update-security.js +112 -0
  142. package/dist/v-calibration.d.ts +16 -0
  143. package/dist/v-calibration.js +42 -0
  144. package/dist/value-calibration.d.ts +41 -0
  145. package/dist/value-calibration.js +133 -0
  146. package/dist/value-head.d.ts +20 -0
  147. package/dist/value-head.js +91 -0
  148. package/dist/wal-buffer.d.ts +23 -0
  149. package/dist/wal-buffer.js +144 -0
  150. package/dist/wiki-synthesizer.d.ts +80 -0
  151. package/dist/wiki-synthesizer.js +0 -0
  152. package/dist/worker-agent.d.ts +10 -0
  153. package/dist/worker-agent.js +19 -0
  154. package/package.json +65 -0
@@ -0,0 +1,120 @@
1
+ export type ChannelName = 'telegram' | 'slack' | 'discord' | 'kakao';
2
+ /** 인바운드 메시지(채널 수신 → 에이전트로 라우팅). */
3
+ export interface InboundMessage {
4
+ channel: ChannelName;
5
+ /** 답장 대상 식별자(telegram chat_id, slack channel, discord channel id, kakao user id). */
6
+ from: string;
7
+ text: string;
8
+ }
9
+ export interface SendResult {
10
+ ok: boolean;
11
+ reason?: string;
12
+ /** 플랫폼 응답(디버그/검증용). */
13
+ raw?: unknown;
14
+ }
15
+ /** 인바운드 능력 분류(정직). socket=WS 푸시(공개서버 불요). */
16
+ export type InboundMode = 'poll' | 'webhook' | 'socket' | 'none';
17
+ /** WS 인바운드 연결 해제 함수. */
18
+ export type Disconnect = () => void;
19
+ export type FetchFn = (url: string, init?: any) => Promise<{
20
+ ok: boolean;
21
+ status: number;
22
+ json: () => Promise<any>;
23
+ text: () => Promise<string>;
24
+ }>;
25
+ export interface IChannel {
26
+ readonly name: ChannelName;
27
+ /** 인바운드 방식(정직 — 구현 가능 범위). */
28
+ readonly inbound: InboundMode;
29
+ /** 자격증명이 설정돼 실제 동작 가능한가. */
30
+ isConfigured(): boolean;
31
+ /** 아웃바운드 전송(실 HTTP). 미설정이면 ok:false('미설정'). */
32
+ send(to: string, text: string): Promise<SendResult>;
33
+ }
34
+ export declare class TelegramChannel implements IChannel {
35
+ private token;
36
+ private fetchFn?;
37
+ readonly name: "telegram";
38
+ readonly inbound: InboundMode;
39
+ private offset;
40
+ constructor(token?: string, fetchFn?: FetchFn | undefined);
41
+ isConfigured(): boolean;
42
+ send(chatId: string, text: string): Promise<SendResult>;
43
+ /** 롱폴링 1회 — 새 메시지들을 반환하고 offset 을 갱신한다(공개서버 불필요). */
44
+ poll(timeoutSec?: number): Promise<InboundMessage[]>;
45
+ }
46
+ export declare class SlackChannel implements IChannel {
47
+ private token;
48
+ private fetchFn?;
49
+ readonly name: "slack";
50
+ readonly inbound: InboundMode;
51
+ constructor(token?: string, fetchFn?: FetchFn | undefined);
52
+ isConfigured(): boolean;
53
+ send(channel: string, text: string): Promise<SendResult>;
54
+ /** Events API 페이로드 파싱. url_verification(challenge)·봇 자신/비-메시지는 null. */
55
+ static parseInbound(payload: any): InboundMessage | null;
56
+ /**
57
+ * Slack 웹훅 서명 검증(v0 HMAC-SHA256). 위조·재전송 차단.
58
+ * sig = 'v0=' + HMAC(signingSecret, `v0:${ts}:${rawBody}`), |now-ts| ≤ 300s, 타이밍 안전 비교.
59
+ */
60
+ static verifySignature(rawBody: string, signature: string | undefined, timestamp: string | undefined, signingSecret: string): boolean;
61
+ /** Socket Mode 봉투 파싱 → InboundMessage|null. events_api·slash_commands 지원. */
62
+ static parseSocketEnvelope(env: any): InboundMessage | null;
63
+ /**
64
+ * Slack Socket Mode 연결(공개 URL 불요). app-level token(xapp-, connections:write)으로
65
+ * apps.connections.open → WSS 접속, 이벤트 봉투를 ack 하고 메시지를 onMessage 로 넘긴다.
66
+ * 정직: 실제 수신은 app/bot 토큰·스코프가 있어야 검증됨(그 전엔 미측정).
67
+ */
68
+ connectSocketMode(appToken: string, onMessage: (m: InboundMessage) => void): Promise<Disconnect>;
69
+ }
70
+ /** Discord Gateway intents: GUILDS|GUILD_MESSAGES|DIRECT_MESSAGES|MESSAGE_CONTENT. */
71
+ export declare const DISCORD_INTENTS: number;
72
+ export declare class DiscordChannel implements IChannel {
73
+ private token;
74
+ private fetchFn?;
75
+ readonly name: "discord";
76
+ readonly inbound: InboundMode;
77
+ constructor(token?: string, fetchFn?: FetchFn | undefined);
78
+ isConfigured(): boolean;
79
+ send(channelId: string, text: string): Promise<SendResult>;
80
+ /** 인터랙션(슬래시 커맨드) 웹훅 파싱. PING(type 1)·비-메시지는 null. */
81
+ static parseInbound(payload: any): InboundMessage | null;
82
+ /**
83
+ * Discord 인터랙션 웹훅 서명 검증(Ed25519, 명세상 필수). 위조 차단.
84
+ * message = timestamp + rawBody, X-Signature-Ed25519 를 앱 공개키로 검증.
85
+ */
86
+ static verifySignature(rawBody: string, signatureHex: string | undefined, timestamp: string | undefined, publicKeyHex: string): boolean;
87
+ /** IDENTIFY(op 2) 페이로드. */
88
+ static identifyPayload(token: string, intents?: number): any;
89
+ /** Gateway MESSAGE_CREATE 데이터(d) → InboundMessage|null. 봇/빈 메시지는 null. */
90
+ static gatewayMessageToInbound(d: any): InboundMessage | null;
91
+ /**
92
+ * Discord Gateway WebSocket 연결(공개 URL 불요). HELLO(op10)→하트비트+IDENTIFY,
93
+ * MESSAGE_CREATE(op0) 수신 시 onMessage. 끊기면 재연결.
94
+ * 정직: MESSAGE_CONTENT 는 특권 인텐트 — 개발자 포털에서 활성화해야 본문이 온다.
95
+ * 실제 수신은 토큰·인텐트가 있어야 검증됨(그 전엔 미측정).
96
+ */
97
+ connectGateway(onMessage: (m: InboundMessage) => void): Disconnect;
98
+ }
99
+ export declare class KakaoTalkChannel implements IChannel {
100
+ private accessToken;
101
+ private fetchFn?;
102
+ readonly name: "kakao";
103
+ readonly inbound: InboundMode;
104
+ constructor(accessToken?: string, fetchFn?: FetchFn | undefined);
105
+ isConfigured(): boolean;
106
+ /** '나에게 보내기'(memo/default) — 텍스트 템플릿. to 는 무시(항상 인증 사용자 본인). */
107
+ send(_to: string, text: string): Promise<SendResult>;
108
+ /**
109
+ * Kakao 오픈빌더 웹훅 검증. 카카오는 표준 서명이 없으므로 **공유 비밀 헤더**로 보호한다.
110
+ * configuredSecret 설정 시 헤더 값과 타이밍 안전 비교, 미설정 시 false(보안 기본 — 호출측에서
111
+ * 로컬바인딩 등 별도 정책 결정). 즉 명시적 비밀 없이는 웹훅 수용을 보장하지 않는다.
112
+ */
113
+ static verifyWebhook(headerSecret: string | undefined, configuredSecret: string): boolean;
114
+ /** 카카오 i 오픈빌더 스킬 웹훅 파싱: { userRequest: { utterance, user:{id} } }. */
115
+ static parseInbound(payload: any): InboundMessage | null;
116
+ /** 오픈빌더 스킬 응답 포맷(simpleText). 웹훅 응답으로 그대로 반환한다. */
117
+ static formatSkillResponse(text: string): any;
118
+ }
119
+ /** 환경변수 기준으로 4채널 인스턴스를 만든다(미설정 채널도 포함 — isConfigured 로 판별). */
120
+ export declare function createDefaultChannels(fetchFn?: FetchFn): IChannel[];
@@ -0,0 +1,432 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // 채널 어댑터 — Telegram / Slack / Discord / KakaoTalk (메시징 게이트웨이)
4
+ // ----------------------------------------------------------------------------
5
+ // 정직 고지(제1계명): 이 모듈은 각 플랫폼 HTTP API 로 **실제 전송**을 시도한다(SDK 미사용, fetch).
6
+ // 그러나 자격증명(토큰) 없이는 비활성이며, 실제 전송 성공은 토큰이 있어야만 검증된다
7
+ // → 토큰 부재 시 send 는 ok:false('미설정')를 정직히 반환한다. mock 으로 성공을 연기하지 않는다.
8
+ //
9
+ // 인바운드(수신) 능력은 플랫폼마다 다르다(정직):
10
+ // - Telegram: getUpdates 롱폴링 → 공개서버 없이 로컬에서 양방향 가능.
11
+ // - Slack: Events API 웹훅(공개 URL 필요). 본 모듈은 페이로드 파싱·서명검증 제공.
12
+ // - Discord: 일반 메시지 수신은 Gateway WebSocket 필요(미구현). 아웃바운드/인터랙션 웹훅만.
13
+ // - KakaoTalk: 개방형 양방향 봇 API 없음. 아웃바운드='나에게 보내기'(자기 자신).
14
+ // 인바운드는 카카오 i 오픈빌더 '스킬 웹훅'(사업자 채널) 페이로드 파싱 제공.
15
+ // ============================================================================
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.KakaoTalkChannel = exports.DiscordChannel = exports.DISCORD_INTENTS = exports.SlackChannel = exports.TelegramChannel = void 0;
51
+ exports.createDefaultChannels = createDefaultChannels;
52
+ const crypto = __importStar(require("crypto"));
53
+ /** 길이 안전 + 타이밍 안전 문자열 비교(서명 검증용). */
54
+ function timingSafeEqual(a, b) {
55
+ const ba = Buffer.from(a, 'utf8');
56
+ const bb = Buffer.from(b, 'utf8');
57
+ if (ba.length !== bb.length)
58
+ return false;
59
+ return crypto.timingSafeEqual(ba, bb);
60
+ }
61
+ function getWebSocket() {
62
+ const W = globalThis.WebSocket;
63
+ if (!W)
64
+ throw new Error('global WebSocket 이 없습니다(Node 21+ 필요).');
65
+ return W;
66
+ }
67
+ function getFetch(injected) {
68
+ if (injected)
69
+ return injected;
70
+ const f = globalThis.fetch;
71
+ if (!f)
72
+ throw new Error('global fetch 가 없습니다(Node 18+ 필요).');
73
+ return f;
74
+ }
75
+ // ─────────────────────────── Telegram ───────────────────────────
76
+ class TelegramChannel {
77
+ token;
78
+ fetchFn;
79
+ name = 'telegram';
80
+ inbound = 'poll';
81
+ offset = 0;
82
+ constructor(token = process.env.TELEGRAM_BOT_TOKEN || '', fetchFn) {
83
+ this.token = token;
84
+ this.fetchFn = fetchFn;
85
+ }
86
+ isConfigured() {
87
+ return !!this.token.trim();
88
+ }
89
+ async send(chatId, text) {
90
+ if (!this.isConfigured())
91
+ return { ok: false, reason: 'TELEGRAM_BOT_TOKEN 미설정' };
92
+ const fetch = getFetch(this.fetchFn);
93
+ const res = await fetch(`https://api.telegram.org/bot${this.token}/sendMessage`, {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ chat_id: chatId, text }),
97
+ });
98
+ const body = await res.json().catch(() => ({}));
99
+ return { ok: res.ok && body?.ok !== false, reason: res.ok ? undefined : `HTTP ${res.status}`, raw: body };
100
+ }
101
+ /** 롱폴링 1회 — 새 메시지들을 반환하고 offset 을 갱신한다(공개서버 불필요). */
102
+ async poll(timeoutSec = 30) {
103
+ if (!this.isConfigured())
104
+ return [];
105
+ const fetch = getFetch(this.fetchFn);
106
+ const res = await fetch(`https://api.telegram.org/bot${this.token}/getUpdates?timeout=${timeoutSec}&offset=${this.offset}`, { method: 'GET' });
107
+ const body = await res.json().catch(() => ({}));
108
+ const out = [];
109
+ for (const u of body?.result ?? []) {
110
+ this.offset = Math.max(this.offset, (u.update_id ?? 0) + 1);
111
+ const msg = u.message ?? u.edited_message;
112
+ if (msg?.text && msg.chat?.id != null)
113
+ out.push({ channel: 'telegram', from: String(msg.chat.id), text: msg.text });
114
+ }
115
+ return out;
116
+ }
117
+ }
118
+ exports.TelegramChannel = TelegramChannel;
119
+ // ─────────────────────────── Slack ───────────────────────────
120
+ class SlackChannel {
121
+ token;
122
+ fetchFn;
123
+ name = 'slack';
124
+ // Socket Mode(WS) 로 공개서버 없이 수신. 웹훅(Events API) 파싱도 지원.
125
+ inbound = 'socket';
126
+ constructor(token = process.env.SLACK_BOT_TOKEN || '', fetchFn) {
127
+ this.token = token;
128
+ this.fetchFn = fetchFn;
129
+ }
130
+ isConfigured() {
131
+ return !!this.token.trim();
132
+ }
133
+ async send(channel, text) {
134
+ if (!this.isConfigured())
135
+ return { ok: false, reason: 'SLACK_BOT_TOKEN 미설정' };
136
+ const fetch = getFetch(this.fetchFn);
137
+ const res = await fetch('https://slack.com/api/chat.postMessage', {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Bearer ${this.token}` },
140
+ body: JSON.stringify({ channel, text }),
141
+ });
142
+ const body = await res.json().catch(() => ({}));
143
+ return { ok: !!body?.ok, reason: body?.ok ? undefined : body?.error || `HTTP ${res.status}`, raw: body };
144
+ }
145
+ /** Events API 페이로드 파싱. url_verification(challenge)·봇 자신/비-메시지는 null. */
146
+ static parseInbound(payload) {
147
+ if (!payload || typeof payload !== 'object')
148
+ return null;
149
+ if (payload.type === 'url_verification')
150
+ return null; // challenge 는 별도 처리
151
+ const e = payload.event;
152
+ if (payload.type === 'event_callback' && e?.type === 'message' && e.text && !e.bot_id && !e.subtype) {
153
+ return { channel: 'slack', from: String(e.channel), text: String(e.text) };
154
+ }
155
+ return null;
156
+ }
157
+ /**
158
+ * Slack 웹훅 서명 검증(v0 HMAC-SHA256). 위조·재전송 차단.
159
+ * sig = 'v0=' + HMAC(signingSecret, `v0:${ts}:${rawBody}`), |now-ts| ≤ 300s, 타이밍 안전 비교.
160
+ */
161
+ static verifySignature(rawBody, signature, timestamp, signingSecret) {
162
+ if (!signingSecret || !signature || !timestamp)
163
+ return false;
164
+ const ts = parseInt(timestamp, 10);
165
+ if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300)
166
+ return false; // 재전송 방지
167
+ const base = `v0:${timestamp}:${rawBody}`;
168
+ const expected = 'v0=' + crypto.createHmac('sha256', signingSecret).update(base).digest('hex');
169
+ return timingSafeEqual(expected, signature);
170
+ }
171
+ /** Socket Mode 봉투 파싱 → InboundMessage|null. events_api·slash_commands 지원. */
172
+ static parseSocketEnvelope(env) {
173
+ if (!env || typeof env !== 'object')
174
+ return null;
175
+ if (env.type === 'events_api')
176
+ return SlackChannel.parseInbound(env.payload);
177
+ if (env.type === 'slash_commands' && env.payload?.text) {
178
+ return { channel: 'slack', from: String(env.payload.channel_id), text: String(env.payload.text) };
179
+ }
180
+ return null;
181
+ }
182
+ /**
183
+ * Slack Socket Mode 연결(공개 URL 불요). app-level token(xapp-, connections:write)으로
184
+ * apps.connections.open → WSS 접속, 이벤트 봉투를 ack 하고 메시지를 onMessage 로 넘긴다.
185
+ * 정직: 실제 수신은 app/bot 토큰·스코프가 있어야 검증됨(그 전엔 미측정).
186
+ */
187
+ async connectSocketMode(appToken, onMessage) {
188
+ if (!appToken?.trim())
189
+ return () => { };
190
+ const fetch = getFetch(this.fetchFn);
191
+ const WS = getWebSocket();
192
+ let ws = null;
193
+ let closed = false;
194
+ const open = async () => {
195
+ const res = await fetch('https://slack.com/api/apps.connections.open', {
196
+ method: 'POST',
197
+ headers: { Authorization: `Bearer ${appToken}`, 'Content-Type': 'application/x-www-form-urlencoded' },
198
+ });
199
+ const j = await res.json().catch(() => ({}));
200
+ if (!j?.ok || !j.url)
201
+ throw new Error(`apps.connections.open 실패: ${j?.error || `HTTP ${res.status}`}`);
202
+ ws = new WS(j.url);
203
+ ws.onmessage = (ev) => {
204
+ let env;
205
+ try {
206
+ env = JSON.parse(ev.data);
207
+ }
208
+ catch {
209
+ return;
210
+ }
211
+ if (env.type === 'hello' || env.type === 'disconnect') {
212
+ if (env.type === 'disconnect' && !closed)
213
+ void open().catch(() => { });
214
+ return;
215
+ }
216
+ if (env.envelope_id) {
217
+ try {
218
+ ws.send(JSON.stringify({ envelope_id: env.envelope_id }));
219
+ }
220
+ catch { /* ack 실패 무시 */ }
221
+ }
222
+ const m = SlackChannel.parseSocketEnvelope(env);
223
+ if (m)
224
+ onMessage(m);
225
+ };
226
+ ws.onclose = () => { if (!closed)
227
+ setTimeout(() => void open().catch(() => { }), 3000); };
228
+ ws.onerror = () => { };
229
+ };
230
+ await open();
231
+ return () => { closed = true; try {
232
+ ws?.close();
233
+ }
234
+ catch { /* noop */ } };
235
+ }
236
+ }
237
+ exports.SlackChannel = SlackChannel;
238
+ // ─────────────────────────── Discord ───────────────────────────
239
+ /** Discord Gateway intents: GUILDS|GUILD_MESSAGES|DIRECT_MESSAGES|MESSAGE_CONTENT. */
240
+ exports.DISCORD_INTENTS = (1 << 0) | (1 << 9) | (1 << 12) | (1 << 15);
241
+ class DiscordChannel {
242
+ token;
243
+ fetchFn;
244
+ name = 'discord';
245
+ // Gateway WebSocket 으로 일반 메시지 수신(공개서버 불요). 인터랙션 웹훅 파싱도 지원.
246
+ inbound = 'socket';
247
+ constructor(token = process.env.DISCORD_BOT_TOKEN || '', fetchFn) {
248
+ this.token = token;
249
+ this.fetchFn = fetchFn;
250
+ }
251
+ isConfigured() {
252
+ return !!this.token.trim();
253
+ }
254
+ async send(channelId, text) {
255
+ if (!this.isConfigured())
256
+ return { ok: false, reason: 'DISCORD_BOT_TOKEN 미설정' };
257
+ const fetch = getFetch(this.fetchFn);
258
+ const res = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
259
+ method: 'POST',
260
+ headers: { 'Content-Type': 'application/json', Authorization: `Bot ${this.token}` },
261
+ body: JSON.stringify({ content: text }),
262
+ });
263
+ const body = await res.json().catch(() => ({}));
264
+ return { ok: res.ok, reason: res.ok ? undefined : body?.message || `HTTP ${res.status}`, raw: body };
265
+ }
266
+ /** 인터랙션(슬래시 커맨드) 웹훅 파싱. PING(type 1)·비-메시지는 null. */
267
+ static parseInbound(payload) {
268
+ if (!payload || typeof payload !== 'object')
269
+ return null;
270
+ if (payload.type === 1)
271
+ return null; // PING
272
+ if (payload.type === 2 && payload.data) {
273
+ const opt = (payload.data.options ?? []).find((o) => o?.value);
274
+ const text = opt?.value ?? payload.data.name;
275
+ const from = payload.channel_id ?? payload.channel?.id;
276
+ if (text && from)
277
+ return { channel: 'discord', from: String(from), text: String(text) };
278
+ }
279
+ return null;
280
+ }
281
+ /**
282
+ * Discord 인터랙션 웹훅 서명 검증(Ed25519, 명세상 필수). 위조 차단.
283
+ * message = timestamp + rawBody, X-Signature-Ed25519 를 앱 공개키로 검증.
284
+ */
285
+ static verifySignature(rawBody, signatureHex, timestamp, publicKeyHex) {
286
+ if (!publicKeyHex || !signatureHex || !timestamp)
287
+ return false;
288
+ try {
289
+ // raw 32바이트 Ed25519 공개키를 SPKI DER 로 감싼다(고정 프리픽스).
290
+ const der = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(publicKeyHex, 'hex')]);
291
+ const pub = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
292
+ return crypto.verify(null, Buffer.from(timestamp + rawBody), pub, Buffer.from(signatureHex, 'hex'));
293
+ }
294
+ catch {
295
+ return false;
296
+ }
297
+ }
298
+ /** IDENTIFY(op 2) 페이로드. */
299
+ static identifyPayload(token, intents = exports.DISCORD_INTENTS) {
300
+ return { op: 2, d: { token, intents, properties: { os: 'linux', browser: 'n2world', device: 'n2world' } } };
301
+ }
302
+ /** Gateway MESSAGE_CREATE 데이터(d) → InboundMessage|null. 봇/빈 메시지는 null. */
303
+ static gatewayMessageToInbound(d) {
304
+ if (!d || d.author?.bot)
305
+ return null;
306
+ if (!d.content || !d.channel_id)
307
+ return null;
308
+ return { channel: 'discord', from: String(d.channel_id), text: String(d.content) };
309
+ }
310
+ /**
311
+ * Discord Gateway WebSocket 연결(공개 URL 불요). HELLO(op10)→하트비트+IDENTIFY,
312
+ * MESSAGE_CREATE(op0) 수신 시 onMessage. 끊기면 재연결.
313
+ * 정직: MESSAGE_CONTENT 는 특권 인텐트 — 개발자 포털에서 활성화해야 본문이 온다.
314
+ * 실제 수신은 토큰·인텐트가 있어야 검증됨(그 전엔 미측정).
315
+ */
316
+ connectGateway(onMessage) {
317
+ if (!this.isConfigured())
318
+ return () => { };
319
+ const WS = getWebSocket();
320
+ let ws = null;
321
+ let hb = null;
322
+ let seq = null;
323
+ let closed = false;
324
+ const connect = () => {
325
+ ws = new WS('wss://gateway.discord.gg/?v=10&encoding=json');
326
+ ws.onmessage = (ev) => {
327
+ let m;
328
+ try {
329
+ m = JSON.parse(ev.data);
330
+ }
331
+ catch {
332
+ return;
333
+ }
334
+ if (m.s != null)
335
+ seq = m.s;
336
+ if (m.op === 10) {
337
+ const interval = m.d?.heartbeat_interval ?? 41250;
338
+ hb = setInterval(() => { try {
339
+ ws.send(JSON.stringify({ op: 1, d: seq }));
340
+ }
341
+ catch { /* noop */ } }, interval);
342
+ ws.send(JSON.stringify(DiscordChannel.identifyPayload(this.token)));
343
+ }
344
+ else if (m.op === 0 && m.t === 'MESSAGE_CREATE') {
345
+ const im = DiscordChannel.gatewayMessageToInbound(m.d);
346
+ if (im)
347
+ onMessage(im);
348
+ }
349
+ };
350
+ ws.onclose = () => { if (hb)
351
+ clearInterval(hb); if (!closed)
352
+ setTimeout(connect, 3000); };
353
+ ws.onerror = () => { };
354
+ };
355
+ connect();
356
+ return () => { closed = true; if (hb)
357
+ clearInterval(hb); try {
358
+ ws?.close();
359
+ }
360
+ catch { /* noop */ } };
361
+ }
362
+ }
363
+ exports.DiscordChannel = DiscordChannel;
364
+ // ─────────────────────────── KakaoTalk ───────────────────────────
365
+ // 정직: 개방형 양방향 봇 API 없음. 아웃바운드='나에게 보내기'(자기 자신, Kakao OAuth access token).
366
+ // 인바운드=카카오 i 오픈빌더 스킬 웹훅 페이로드 파싱(사업자 채널 필요).
367
+ class KakaoTalkChannel {
368
+ accessToken;
369
+ fetchFn;
370
+ name = 'kakao';
371
+ inbound = 'webhook';
372
+ constructor(accessToken = process.env.KAKAO_ACCESS_TOKEN || '', fetchFn) {
373
+ this.accessToken = accessToken;
374
+ this.fetchFn = fetchFn;
375
+ }
376
+ isConfigured() {
377
+ return !!this.accessToken.trim();
378
+ }
379
+ /** '나에게 보내기'(memo/default) — 텍스트 템플릿. to 는 무시(항상 인증 사용자 본인). */
380
+ async send(_to, text) {
381
+ if (!this.isConfigured())
382
+ return { ok: false, reason: 'KAKAO_ACCESS_TOKEN 미설정' };
383
+ const fetch = getFetch(this.fetchFn);
384
+ const template = {
385
+ object_type: 'text',
386
+ text: text.slice(0, 200), // 카카오 텍스트 템플릿 길이 제한(보수적)
387
+ link: { web_url: 'https://localhost', mobile_web_url: 'https://localhost' },
388
+ };
389
+ const form = `template_object=${encodeURIComponent(JSON.stringify(template))}`;
390
+ const res = await fetch('https://kapi.kakao.com/v2/api/talk/memo/default/send', {
391
+ method: 'POST',
392
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', Authorization: `Bearer ${this.accessToken}` },
393
+ body: form,
394
+ });
395
+ const body = await res.json().catch(() => ({}));
396
+ // 카카오 성공 시 {result_code:0}.
397
+ return { ok: res.ok && body?.result_code === 0, reason: res.ok ? undefined : body?.msg || `HTTP ${res.status}`, raw: body };
398
+ }
399
+ /**
400
+ * Kakao 오픈빌더 웹훅 검증. 카카오는 표준 서명이 없으므로 **공유 비밀 헤더**로 보호한다.
401
+ * configuredSecret 설정 시 헤더 값과 타이밍 안전 비교, 미설정 시 false(보안 기본 — 호출측에서
402
+ * 로컬바인딩 등 별도 정책 결정). 즉 명시적 비밀 없이는 웹훅 수용을 보장하지 않는다.
403
+ */
404
+ static verifyWebhook(headerSecret, configuredSecret) {
405
+ if (!configuredSecret)
406
+ return false;
407
+ if (!headerSecret)
408
+ return false;
409
+ return timingSafeEqual(configuredSecret, headerSecret);
410
+ }
411
+ /** 카카오 i 오픈빌더 스킬 웹훅 파싱: { userRequest: { utterance, user:{id} } }. */
412
+ static parseInbound(payload) {
413
+ const ur = payload?.userRequest;
414
+ if (ur?.utterance)
415
+ return { channel: 'kakao', from: String(ur.user?.id ?? 'self'), text: String(ur.utterance) };
416
+ return null;
417
+ }
418
+ /** 오픈빌더 스킬 응답 포맷(simpleText). 웹훅 응답으로 그대로 반환한다. */
419
+ static formatSkillResponse(text) {
420
+ return { version: '2.0', template: { outputs: [{ simpleText: { text } }] } };
421
+ }
422
+ }
423
+ exports.KakaoTalkChannel = KakaoTalkChannel;
424
+ /** 환경변수 기준으로 4채널 인스턴스를 만든다(미설정 채널도 포함 — isConfigured 로 판별). */
425
+ function createDefaultChannels(fetchFn) {
426
+ return [
427
+ new TelegramChannel(undefined, fetchFn),
428
+ new SlackChannel(undefined, fetchFn),
429
+ new DiscordChannel(undefined, fetchFn),
430
+ new KakaoTalkChannel(undefined, fetchFn),
431
+ ];
432
+ }
@@ -0,0 +1,29 @@
1
+ import { ChatTurn } from './llm';
2
+ /** 기본 대화 보존 경로(로컬). beta-data 와 분리. */
3
+ export declare const DEFAULT_CHAT_HISTORY_PATH: string;
4
+ export declare class ChatStore {
5
+ private readonly filePath;
6
+ /** 보존 상한(턴 수). 초과분은 앞에서 절단 — 메모리·파일·토큰 통제. */
7
+ private readonly maxTurns;
8
+ private turns;
9
+ constructor(filePath?: string,
10
+ /** 보존 상한(턴 수). 초과분은 앞에서 절단 — 메모리·파일·토큰 통제. */
11
+ maxTurns?: number);
12
+ /**
13
+ * 디스크에서 복원한다(파일 없음/손상이면 빈 히스토리 — 정직, 가짜 복원 금지).
14
+ * 반환은 복원된 턴 배열(내부 상태와 동일 참조 아님 — 복사본).
15
+ */
16
+ load(): ChatTurn[];
17
+ /** 현재 히스토리(복사본). */
18
+ history(): ChatTurn[];
19
+ count(): number;
20
+ /** 한 턴 추가 + 즉시 영속(다운돼도 직전 턴까지 보존). 상한 초과분은 앞에서 절단. */
21
+ append(turn: ChatTurn): void;
22
+ /** 원자적 쓰기(임시파일 → rename) — 쓰는 도중 죽어도 기존 파일이 깨지지 않는다. */
23
+ private persist;
24
+ /** 히스토리 비우기(메모리 + 디스크 파일 제거). */
25
+ clear(): void;
26
+ /** 사람이 읽는 마크다운으로 내보내기(복구·공유용). */
27
+ exportMarkdown(): string;
28
+ path_(): string;
29
+ }