@underdogai/mesh-app-sdk 0.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.
package/dist/serve.js ADDED
@@ -0,0 +1,415 @@
1
+ /**
2
+ * 봇 계정 실행기(Phase 3, 킥오프 "이 작업의 심장") — 등록 파일 없이 **토큰 하나로** 앱을 상시 실행한다.
3
+ *
4
+ * await serve(app, { homeserver, accessToken }); // 봇 계정 상시 실행
5
+ * await publish(app, { homeserver, accessToken, roomId }); // 1회 발행(프로세스 없음)
6
+ *
7
+ * 하는 일(§3 Phase 3):
8
+ * - 초대 자동 수락 → 참여 → 서피스 1회 발행(중복 방지, host.ts 재사용) = "초대하면 활성화"(§2.4)
9
+ * - /sync 로 org.corp.ui.action 과 구독(on) 이벤트 수신 → dispatchAction/dispatchEvent 라우팅 → applyEffects 적용
10
+ * - **자기 것이 아니면 넘긴다** — 모르는 action_id/미구독 타입은 어떤 메시지도 발행하지 않는다(외부 공존).
11
+ * - 자기(봇 계정)가 보낸 이벤트는 무시한다 — emit→on 자기 루프 방지(dispatchEvent 의 호스트 책임 이행).
12
+ * - 과거를 재생하지 않는다 — 초기 sync 타임라인은 버리고, 세션 중 새로 참여한 방은 참여 시각 워터마크로
13
+ * join 백필(limited 타임라인이 실어오는 과거 이벤트)을 걸러낸다(지난주 클릭이 오늘 재실행되는 사고 방지).
14
+ *
15
+ * 의존 0 추가: Matrix 왕복은 런타임 내장 fetch 로 직접 한다(§1.3 이 curl 만으로 증명한 것과 같은 와이어).
16
+ * App Service 실행(여러 정체성·홈서버 push, §2.1)은 이 파일의 범위 밖 — 기존 appservice-agent 패턴이
17
+ * 같은 라우팅 코어(dispatchAction·dispatchEvent·applyEffects·host.ts)를 쓰므로, 필요해질 때 어댑터만 얹으면 된다.
18
+ *
19
+ * 보안(§6-3 결정): 기본 게이트는 "비파괴 허용, 파괴적은 발신자 방 PL≥50"(defaultBotGate — Matrix PL 재사용).
20
+ * opts.gate 주입으로 완전 대체 가능(정책은 앱 소유). invoke 는 발신 봇 *자신의* 권한으로 평가된다(위장 금지).
21
+ * 초대는 기본 전부 수락(§2.4)이되 opts.acceptInvite 로 좁힐 수 있다.
22
+ *
23
+ * 알려진 한계(v1): sync 타임라인 갭(limited)의 잘린 이벤트는 역채움하지 않고 경고만 남긴다(prev_batch 미소비).
24
+ */
25
+ import { EventType, loadPlugins } from "@underdogai/mesh-event-schemas";
26
+ import { applyEffects, dispatchAction, dispatchEvent, } from "./runtime.js";
27
+ import { publishSurface } from "./host.js";
28
+ function resolveFetch(injected) {
29
+ const f = injected ?? globalThis.fetch;
30
+ if (!f)
31
+ throw new Error("[app-sdk] fetch 를 찾을 수 없습니다 — Node 20+ 이거나 opts.fetch 를 주입하세요.");
32
+ // 내장 fetch 는 this 바인딩이 필요하다 — globalThis 에 묶어 호출.
33
+ return (url, init) => f.call(globalThis, url, init);
34
+ }
35
+ function sleep(ms) {
36
+ const g = globalThis;
37
+ return new Promise((r) => (g.setTimeout ? g.setTimeout(() => r(), ms) : r()));
38
+ }
39
+ /**
40
+ * Matrix API 오류(구조화) — 호출자가 상태/errcode 로 회복 정책을 분기할 수 있게 한다
41
+ * (401/403=재시도 무의미 fail-fast, 429=retry_after_ms 준수, 그 외=백오프 재시도).
42
+ */
43
+ export class MatrixApiError extends Error {
44
+ status;
45
+ errcode;
46
+ retryAfterMs;
47
+ constructor(message, status, errcode, retryAfterMs) {
48
+ super(message);
49
+ this.name = "MatrixApiError";
50
+ this.status = status;
51
+ if (errcode !== undefined)
52
+ this.errcode = errcode;
53
+ if (retryAfterMs !== undefined)
54
+ this.retryAfterMs = retryAfterMs;
55
+ }
56
+ }
57
+ /** 토큰 하나로 도는 최소 클라이언트를 만든다. 전송 규약 주의: 메시지는 PUT .../send/{type}/{txnId} (§7 — POST 는 404). */
58
+ export function createBotClient(opts) {
59
+ const doFetch = resolveFetch(opts.fetch);
60
+ const base = `${opts.homeserver.replace(/\/+$/, "")}/_matrix/client/v3`;
61
+ let txn = 0;
62
+ const request = async (method, path, body) => {
63
+ // 429 만 retry_after_ms 를 지켜 소폭(최대 2회) 자체 재시도 — 효과 전송이 rate limit 한 번에 유실되지 않게.
64
+ // 그 외 오류의 회복 정책은 호출자 소유(MatrixApiError 로 상태/errcode 를 구조화해 넘긴다).
65
+ for (let attempt = 0;; attempt++) {
66
+ const res = await doFetch(`${base}${path}`, {
67
+ method,
68
+ headers: {
69
+ Authorization: `Bearer ${opts.accessToken}`,
70
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
71
+ },
72
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
73
+ });
74
+ const text = await res.text();
75
+ if (res.ok)
76
+ return text ? JSON.parse(text) : {};
77
+ let errcode;
78
+ let retryAfterMs;
79
+ try {
80
+ const e = JSON.parse(text);
81
+ errcode = e.errcode;
82
+ retryAfterMs = e.retry_after_ms;
83
+ }
84
+ catch {
85
+ // 본문이 JSON 이 아니면(프록시 오류 등) 상태 코드만으로 분기한다.
86
+ }
87
+ if (res.status === 429 && attempt < 2) {
88
+ await sleep(retryAfterMs ?? 1000);
89
+ continue;
90
+ }
91
+ throw new MatrixApiError(`[app-sdk] ${method} ${path} → ${res.status}${errcode ? ` ${errcode}` : ""}: ${text.slice(0, 300)}`, res.status, errcode, retryAfterMs);
92
+ }
93
+ };
94
+ const seg = encodeURIComponent;
95
+ return {
96
+ async sendEvent(roomId, eventType, content) {
97
+ // §7 함정: 메시지 전송은 PUT + txnId (POST 는 404). txnId 는 프로세스 내 단조 증가면 충분.
98
+ const id = `sdk${Date.now()}.${txn++}`;
99
+ const r = (await request("PUT", `/rooms/${seg(roomId)}/send/${seg(eventType)}/${seg(id)}`, content));
100
+ return r.event_id;
101
+ },
102
+ async sendStateEvent(roomId, eventType, stateKey, content) {
103
+ const r = (await request("PUT", `/rooms/${seg(roomId)}/state/${seg(eventType)}/${seg(stateKey)}`, content));
104
+ return r.event_id;
105
+ },
106
+ async getRoomStateEvent(roomId, eventType, stateKey) {
107
+ // 상태 없음(404 M_NOT_FOUND)은 throw — publishSurface 가 "없으면 발행"으로 해석한다.
108
+ return request("GET", `/rooms/${seg(roomId)}/state/${seg(eventType)}/${seg(stateKey)}`);
109
+ },
110
+ async joinRoom(roomId) {
111
+ return request("POST", `/join/${seg(roomId)}`, {});
112
+ },
113
+ async whoami() {
114
+ const r = (await request("GET", "/account/whoami"));
115
+ if (!r.user_id)
116
+ throw new Error("[app-sdk] whoami 가 user_id 를 돌려주지 않았습니다 — 토큰을 확인하세요.");
117
+ return r.user_id;
118
+ },
119
+ async sync(since, timeoutMs, filter) {
120
+ // 봇은 presence 를 온라인으로 만들지 않는다(set_presence=offline) — 사람 목록을 어지럽히지 않게.
121
+ const parts = [`timeout=${timeoutMs}`, "set_presence=offline"];
122
+ if (since)
123
+ parts.push(`since=${seg(since)}`);
124
+ if (filter)
125
+ parts.push(`filter=${seg(filter)}`);
126
+ return (await request("GET", `/sync?${parts.join("&")}`));
127
+ },
128
+ };
129
+ }
130
+ /** 앱 서피스를 방에 1회 발행하고 끝낸다. 서피스는 상태 이벤트라 한 번 올리면 영속한다(§1.3). */
131
+ export async function publish(app, opts) {
132
+ const client = createBotClient(opts);
133
+ try {
134
+ await client.joinRoom(opts.roomId);
135
+ }
136
+ catch {
137
+ // 이미 참여했거나 초대가 없는 방 — 발행 시도는 계속한다(권한 없으면 발행에서 명확히 실패).
138
+ }
139
+ return publishSurface({ client, app, roomId: opts.roomId, stateKey: opts.stateKey ?? app.app });
140
+ }
141
+ /** 초기 sync 는 기준점(next_batch)·초대·참여 방 목록만 필요하다 — 타임라인/상태/프레즌스를 최소로 줄인다. */
142
+ const INITIAL_SYNC_FILTER = JSON.stringify({
143
+ room: { timeline: { limit: 1 }, state: { lazy_load_members: true }, account_data: { types: [] } },
144
+ presence: { types: [] },
145
+ account_data: { types: [] },
146
+ });
147
+ /** 봇 모드 파괴적 액션 PL 임계 — 서버 정책(DESTRUCTIVE_PL_THRESHOLD)과 같은 값. */
148
+ export const BOT_DESTRUCTIVE_PL_THRESHOLD = 50;
149
+ /** m.room.power_levels 에서 발신자 유효 PL(users → users_default → 0). */
150
+ function powerLevelOf(powerLevels, userId) {
151
+ const pls = (powerLevels ?? {});
152
+ const explicit = pls.users?.[userId];
153
+ if (typeof explicit === "number")
154
+ return explicit;
155
+ return typeof pls.users_default === "number" ? pls.users_default : 0;
156
+ }
157
+ /**
158
+ * 기본 봇 게이트(§6-3 결정) — 비파괴 액션은 허용, 파괴적(destructive) 액션은 발신자의 방 PL ≥ 50 요구.
159
+ * 방장이 PL 로 권한을 나눠주는 Matrix 네이티브 권위를 재사용한다(서피스 발행 권한과 같은 원리).
160
+ * opts.gate 를 주입하면 완전히 대체된다.
161
+ */
162
+ export const defaultBotGate = (i) => {
163
+ if (!i.destructive)
164
+ return { allowed: true, reason: "non-destructive(bot-default)" };
165
+ const pl = powerLevelOf(i.powerLevels, i.senderId);
166
+ return pl >= BOT_DESTRUCTIVE_PL_THRESHOLD
167
+ ? { allowed: true, reason: `destructive: pl ${pl} >= ${BOT_DESTRUCTIVE_PL_THRESHOLD}` }
168
+ : { allowed: false, reason: `파괴적 액션은 방 PL ${BOT_DESTRUCTIVE_PL_THRESHOLD} 이상이 필요합니다(현재 ${pl}).` };
169
+ };
170
+ /** 초대 방의 invite_state 에서 초대자(m.room.member invite 의 sender)를 찾는다(없으면 undefined). */
171
+ function inviterOf(invite, self) {
172
+ const ev = invite?.invite_state?.events?.find((e) => e.type === "m.room.member" && e.state_key === self && e.content?.membership === "invite");
173
+ return ev?.sender;
174
+ }
175
+ /**
176
+ * 앱을 봇 계정으로 상시 실행한다. 초대를 자동 수락해 서피스를 1회 발행하고(§2.4), sync 타임라인에서
177
+ * 자기 소유 액션·구독 이벤트만 처리한다(아니면 무발행 무시). signal.aborted 로 정상 종료할 수 있다.
178
+ * 토큰이 죽으면(401/403) 재시도가 무의미하므로 reject 로 즉시 알린다 — 그 외 sync 오류는 백오프 후 재시도.
179
+ */
180
+ export async function serve(app, opts) {
181
+ const client = createBotClient(opts);
182
+ const g = globalThis;
183
+ const log = opts.log ?? ((m) => g.console?.log(m));
184
+ const warn = (m) => (opts.log ?? ((x) => g.console?.warn(x)))(m);
185
+ const msg = (err) => (err instanceof Error ? err.message : String(err));
186
+ // 게이트 미주입 = 기본 게이트(§6-3 결정): 비파괴 허용, 파괴적은 발신자 방 PL≥50(방장이 봇/사람에게 준
187
+ // 권한이라는 Matrix 네이티브 권위). invoke 로 온 액션도 동일 — 발신 봇 자신의 PL 로 평가된다(위장 금지 원칙).
188
+ const gate = opts.gate ?? defaultBotGate;
189
+ const stateKey = opts.surfaceStateKey ?? app.app;
190
+ const timeoutMs = opts.syncTimeoutMs ?? 30_000;
191
+ // sync 오류 회복 정책 — 401/403(토큰 사망)은 즉시 throw(좀비 방지), 429 는 retry_after 준수,
192
+ // 그 외(5xx/네트워크)는 상한 있는 지수 백오프 + 지터(동시 재기동 몰림 방지).
193
+ const recoverSyncError = async (err, failures) => {
194
+ if (err instanceof MatrixApiError && (err.status === 401 || err.status === 403))
195
+ throw err;
196
+ const backoff = Math.min(30_000, 1000 * 2 ** Math.min(failures, 5)) + Math.floor(Math.random() * 250);
197
+ const wait = err instanceof MatrixApiError && err.status === 429 && err.retryAfterMs ? err.retryAfterMs : backoff;
198
+ warn(`[app-sdk] sync 실패 — ${wait}ms 후 재시도: ${msg(err)}`);
199
+ await sleep(wait);
200
+ };
201
+ // 스키마 등록(멱등) + 매니페스트 검증 — enabledPlugins 부트와 같은 경로(SEC-2)를 봇에서도 지난다.
202
+ loadPlugins([app]);
203
+ const self = await client.whoami();
204
+ log(`[app-sdk] serve '${app.app}' 시작 (${self})`);
205
+ // 초기 sync — since 기준점만 확보한다(최소 필터). 부팅 중 일시 오류는 루프와 같은 정책으로 재시도한다
206
+ // (부팅만 fail-fast 인 비대칭 금지). ⚠️ 초기 타임라인은 처리하지 않는다(재부팅 때 히스토리 재생 금지).
207
+ let first;
208
+ for (let failures = 0; first === undefined; failures++) {
209
+ if (opts.signal?.aborted)
210
+ return;
211
+ try {
212
+ first = await client.sync(undefined, 0, INITIAL_SYNC_FILTER);
213
+ }
214
+ catch (err) {
215
+ await recoverSyncError(err, failures);
216
+ }
217
+ }
218
+ let since = first.next_batch;
219
+ // 방 등장 추적 — 부팅 시 이미 참여한 방은 since 토큰이 과거를 막아준다. 세션 중 *새로 등장한* 방은
220
+ // 첫 목격 시각을 워터마크로 남겨, join 백필(limited 타임라인의 과거 이벤트)이 재생되지 않게 한다
221
+ // (지난주 처리된 승인 클릭이 재초대 순간 또 실행되는 사고 방지). 워터마크는 초대 목격 시점에 찍으므로
222
+ // 그 이후 도착한 진짜 새 이벤트는 통과한다.
223
+ const knownRooms = new Set(Object.keys(first.rooms?.join ?? {}));
224
+ const roomWatermark = new Map();
225
+ const noteRoomAppeared = (roomId) => {
226
+ if (knownRooms.has(roomId))
227
+ return;
228
+ knownRooms.add(roomId);
229
+ roomWatermark.set(roomId, Date.now());
230
+ };
231
+ // 초대는 incremental sync 에 한 번만 실린다 — join 이 일시 오류로 실패하면 여기 남겨 다음 루프에서 재시도한다.
232
+ const pendingInvites = new Set();
233
+ // 서피스 발행 실패도 재시도 대상 — 대표 사례: 봇 PL 부족(방 기본 state PL 50 vs 봇 0)으로 403.
234
+ // 방 관리자가 나중에 PL 을 주면 다음 sync 루프에서 자동으로 뜬다("초대하면 활성화" §2.4 를 지연 권한 부여까지 확장).
235
+ const pendingSurfaces = new Set();
236
+ const ensureSurface = async (roomId) => {
237
+ try {
238
+ const r = await publishSurface({ client, app, roomId, stateKey });
239
+ pendingSurfaces.delete(roomId);
240
+ if (r === "published")
241
+ log(`[app-sdk] 서피스 발행 → ${roomId} (state_key="${stateKey}")`);
242
+ }
243
+ catch (err) {
244
+ pendingSurfaces.add(roomId);
245
+ warn(`[app-sdk] 서피스 발행 실패 — 다음 sync 때 재시도 (${roomId}): ${msg(err)}`);
246
+ }
247
+ };
248
+ // 활성화 시퀀스: 초대 수락 → 참여 → 서피스 1회 발행(중복 방지). 부팅 시 기존 방도 보정한다(멱등).
249
+ const activate = async (roomId) => {
250
+ noteRoomAppeared(roomId);
251
+ try {
252
+ await client.joinRoom(roomId);
253
+ }
254
+ catch (err) {
255
+ if (err instanceof MatrixApiError && (err.status === 403 || err.status === 404)) {
256
+ pendingInvites.delete(roomId); // 밴/철회 등 — 재시도해도 못 들어간다.
257
+ warn(`[app-sdk] 초대 수락 불가 — 포기 (${roomId}): ${msg(err)}`);
258
+ }
259
+ else {
260
+ pendingInvites.add(roomId);
261
+ warn(`[app-sdk] 초대 수락 실패 — 다음 sync 때 재시도 (${roomId}): ${msg(err)}`);
262
+ }
263
+ return;
264
+ }
265
+ pendingInvites.delete(roomId);
266
+ await ensureSurface(roomId);
267
+ };
268
+ // 초대 관문 — acceptInvite(§2.4 좁힘 옵션)가 거절하면 join 하지 않고 조용히 넘긴다(콜백 오류는 fail-closed).
269
+ const onInvite = async (roomId, inviter) => {
270
+ if (opts.acceptInvite) {
271
+ let ok = false;
272
+ try {
273
+ ok = await opts.acceptInvite({ roomId, inviter });
274
+ }
275
+ catch (err) {
276
+ warn(`[app-sdk] acceptInvite 판정 오류 — 거절로 처리 (${roomId}): ${msg(err)}`);
277
+ }
278
+ if (!ok) {
279
+ pendingInvites.delete(roomId);
280
+ log(`[app-sdk] 초대 거절 (${roomId}${inviter ? `, inviter=${inviter}` : ""}) — acceptInvite 정책`);
281
+ return;
282
+ }
283
+ }
284
+ await activate(roomId);
285
+ };
286
+ for (const [roomId, inv] of Object.entries(first.rooms?.invite ?? {}))
287
+ await onInvite(roomId, inviterOf(inv, self));
288
+ for (const roomId of Object.keys(first.rooms?.join ?? {}))
289
+ await ensureSurface(roomId);
290
+ // 이벤트 중복 처리 방어(안전벨트) — since 는 성공 시에만 전진하므로 정상 경로의 재전달은 드물지만,
291
+ // 서버 비정상·경계 재전달을 한 번 더 막는다. join 백필(과거 재생)은 위 워터마크의 몫이다.
292
+ const seen = new Set();
293
+ const markSeen = (id) => {
294
+ if (seen.has(id))
295
+ return false;
296
+ seen.add(id);
297
+ if (seen.size > 2000) {
298
+ const it = seen.values();
299
+ for (let i = 0; i < 1000; i++) {
300
+ const n = it.next();
301
+ if (n.done)
302
+ break;
303
+ seen.delete(n.value);
304
+ }
305
+ }
306
+ return true;
307
+ };
308
+ const handleTimelineEvent = async (roomId, ev) => {
309
+ if (!ev.type || !ev.sender)
310
+ return;
311
+ if (ev.sender === self)
312
+ return; // 루프 방지 — 자기(emit 포함)가 보낸 이벤트에 자기가 반응하지 않는다.
313
+ // join 백필 차단 — 세션 중 새로 등장한 방에서 워터마크(첫 목격 시각)보다 오래된 이벤트는 과거다.
314
+ const wm = roomWatermark.get(roomId);
315
+ if (wm !== undefined && typeof ev.origin_server_ts === "number" && ev.origin_server_ts < wm)
316
+ return;
317
+ if (ev.event_id && !markSeen(ev.event_id))
318
+ return;
319
+ if (ev.type === EventType.UiAction) {
320
+ const content = ev.content ?? {};
321
+ const rel = content["m.relates_to"];
322
+ const cardEventId = rel?.event_id;
323
+ // 방 power_levels 는 게이트가 실제로 볼 때만 best-effort 조회 — 커스텀 게이트 주입 시, 또는
324
+ // 기본 게이트가 PL 을 요구하는 파괴적 액션일 때(자기 소유 액션 정의를 미리 보고 판단).
325
+ const targetDef = app.actions.get(typeof content.action_id === "string" ? content.action_id : "");
326
+ let powerLevels;
327
+ if (opts.gate || targetDef?.destructive) {
328
+ try {
329
+ powerLevels = await client.getRoomStateEvent(roomId, "m.room.power_levels", "");
330
+ }
331
+ catch {
332
+ powerLevels = undefined;
333
+ }
334
+ }
335
+ const entity = entityFromCardId(typeof content.card_id === "string" ? content.card_id : "");
336
+ const res = await dispatchAction({
337
+ app,
338
+ actionContent: content,
339
+ sender: ev.sender,
340
+ powerLevels,
341
+ cardEventId,
342
+ entity,
343
+ gate,
344
+ backend: opts.backend,
345
+ config: opts.config,
346
+ });
347
+ // 자기 것이 아니면(unknown_action 등 ignored) 아무것도 발행하지 않는다 — 외부 앱 공존의 반쪽(§1.5의 역).
348
+ if (res.status === "ran" && res.effects.length) {
349
+ await applyEffects({
350
+ client, app, roomId, cardEventId, entity,
351
+ sender: ev.sender, effects: res.effects, onDelegate: opts.onDelegate,
352
+ });
353
+ log(`[app-sdk] 액션 실행 (${res.actionId}, effects=${res.effects.length})`);
354
+ }
355
+ else if (res.status === "denied") {
356
+ log(`[app-sdk] 액션 거부 (${res.actionId}): ${res.reason}`); // 거부 카드 발행 여부는 앱 정책 — 기본은 로그만.
357
+ }
358
+ return;
359
+ }
360
+ if (app.on.has(ev.type)) {
361
+ const r = await dispatchEvent({ app, type: ev.type, content: ev.content ?? {}, sender: ev.sender, roomId, eventId: ev.event_id });
362
+ if (r.status === "ran" && r.effects.length) {
363
+ await applyEffects({ client, app, roomId, sender: ev.sender, effects: r.effects, onDelegate: opts.onDelegate });
364
+ log(`[app-sdk] 구독 반응 (${ev.type}, effects=${r.effects.length})`);
365
+ }
366
+ }
367
+ };
368
+ let failures = 0;
369
+ while (!opts.signal?.aborted) {
370
+ let res;
371
+ try {
372
+ res = await client.sync(since, timeoutMs);
373
+ failures = 0;
374
+ }
375
+ catch (err) {
376
+ failures++;
377
+ await recoverSyncError(err, failures);
378
+ continue;
379
+ }
380
+ since = res.next_batch;
381
+ for (const roomId of [...pendingInvites])
382
+ await activate(roomId); // 지난 초대의 join 재시도(이미 수락 판정 통과분)
383
+ for (const roomId of [...pendingSurfaces])
384
+ await ensureSurface(roomId); // 실패한 서피스 재시도(지연 PL 부여 등)
385
+ for (const [roomId, inv] of Object.entries(res.rooms?.invite ?? {}))
386
+ await onInvite(roomId, inviterOf(inv, self));
387
+ for (const [roomId, jr] of Object.entries(res.rooms?.join ?? {})) {
388
+ noteRoomAppeared(roomId); // 우리 activate 밖(다른 클라이언트로 join 등)으로 등장한 방도 백필 워터마크 대상
389
+ if (jr.timeline?.limited) {
390
+ // 갭 신호 — 잘린 이벤트는 처리되지 않는다(v1 은 prev_batch 역채움 미지원). 조용한 유실 금지: 경고를 남긴다.
391
+ warn(`[app-sdk] sync 타임라인 갭 (${roomId}) — 잘린 이벤트는 건너뛴다(prev_batch 역채움 미지원)`);
392
+ }
393
+ for (const ev of jr.timeline?.events ?? []) {
394
+ try {
395
+ await handleTimelineEvent(roomId, ev);
396
+ }
397
+ catch (err) {
398
+ // 효과가 일부만 적용된 채 실패했을 수 있다 — 부작용은 되돌리지 않는다(호스트 회복 정책은 앱 몫).
399
+ warn(`[app-sdk] 이벤트 처리 실패 (${roomId}, ${ev.type}): ${msg(err)}`);
400
+ }
401
+ }
402
+ }
403
+ }
404
+ log(`[app-sdk] serve '${app.app}' 종료(signal)`);
405
+ }
406
+ /**
407
+ * 카드 없는 환경의 엔티티 폴백 — card_id 가 "<소문자type>-<id>" 앵커드 패턴이면 id 를 복원한다
408
+ * (appservice-agent resolveCardEntity 의 폴백 절반과 같은 규칙 — 앵커드라 id 내부 '-' 보존).
409
+ * 원본 카드 조회(권위)는 봇 모드 v1 범위 밖 — 핸들러는 ctx.values/백엔드 재조회를 권위로 삼을 것.
410
+ */
411
+ function entityFromCardId(cardId) {
412
+ const m = cardId.match(/^([a-z][a-z0-9]*)-(.+)$/);
413
+ return m?.[2] ? { type: m[1], id: m[2] } : {};
414
+ }
415
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@underdogai/mesh-app-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Matrix 채팅에 붙는 앱을 선언형으로 만드는 SDK — 카드·서피스·액션·구독을 한 곳에서 정의하고, 봇 토큰 하나로 실행한다(App Service 불필요).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "zod": "^3.23.8",
24
+ "@underdogai/mesh-event-schemas": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.6.3"
28
+ },
29
+ "license": "MIT",
30
+ "author": "Underdog",
31
+ "keywords": [
32
+ "matrix",
33
+ "matrix-bot",
34
+ "chatops",
35
+ "chat-app",
36
+ "sdk",
37
+ "bot",
38
+ "matrix-client"
39
+ ],
40
+ "homepage": "https://chatops.theunderdog.ai/sdk-guide",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://forgejo.taildff74c.ts.net:3000/Underdog/ChatOps.git",
44
+ "directory": "packages/app-sdk"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit"
52
+ }
53
+ }
package/src/action.ts ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 액션 정의 — 카드 버튼(UI)과 서버 핸들러(동작)를 "하나의 id/scope/values"로 묶는 컨트랙트.
3
+ *
4
+ * 지금까지 흩어져 있던 것 — 카드 빌더의 `action_id` 문자열 ↔ 핸들러의 `if (action_id === ...)` ↔
5
+ * policy 의 `ACTION_SCOPE` 하드코딩 맵 — 을 defineAction 한 곳으로 모은다. 결과:
6
+ * - 버튼의 action_id 가 정의에서 파생 → 카드와 핸들러가 컴파일타임에 연결(오타/리네임 안전).
7
+ * - scope/destructive 는 데이터로 서버 게이트(evaluateAction)에 주입 → 보안은 서버가 소유, SDK 는 데이터만 공급.
8
+ */
9
+ import { z } from "zod";
10
+ import type { Block, ButtonStyle } from "@underdogai/mesh-event-schemas";
11
+
12
+ /** UiCardContent 의 actions 블록 element 타입을 스키마에서 직접 파생(닫힌 집합과 100% 일치 보장). */
13
+ type ActionsBlock = Extract<Block, { type: "actions" }>;
14
+ export type ButtonElement = ActionsBlock["elements"][number];
15
+
16
+ export interface ActionButtonOpts {
17
+ style?: ButtonStyle;
18
+ /** true → 기본 확인 다이얼로그, 객체 → 커스텀 확인 문구. UI 게이트일 뿐(권한은 서버). */
19
+ confirm?: boolean | { title: string; text: string };
20
+ /** 버튼 고유 값(폼 입력 values 와 별개). */
21
+ value?: string;
22
+ }
23
+
24
+ export interface AppAction<V extends z.ZodTypeAny = z.ZodTypeAny> {
25
+ /** 안정적 action_id (org.corp.ui.action.action_id 로 회신된다). */
26
+ id: string;
27
+ /** 이 액션을 실행하려면 에이전트 capability 에 있어야 하는 scope(서버 게이트가 검사). */
28
+ scope: string;
29
+ /** 파괴적(되돌릴 수 없는) 여부 — 참이면 서버가 발신자 PL 게이트도 적용(HITL). */
30
+ destructive: boolean;
31
+ /** 폼 입력(values) 검증 스키마 — 핸들러에서 .parse(ctx.values) 로 타입 확보. */
32
+ values: V;
33
+ /**
34
+ * 이 액션의 핸들러가 `ctx.backend.call(tool)` 로 부를 수 있는 MCP 도구 화이트리스트(declaration = 데이터).
35
+ * 미선언(undefined/빈 배열)이면 이 액션 핸들러는 backend 도구를 *전혀* 못 부른다(fail-closed).
36
+ * confused-deputy 방어: 핸들러가 선언 밖 도구(예: delete_invoice)를 부르면 런타임이 거부한다(SDK 가 기계적 강제,
37
+ * 도구 구현·권한은 호스트가 소유 = GateFn 과 같은 의존성 역전). scope 가 "발신자 자격"이라면 tools 는 "앱 리소스 접근면".
38
+ */
39
+ tools?: string[];
40
+ /** 이 액션을 부르는 버튼 element(action_id 자동 연결). card.actions([...]) 에 넣는다. */
41
+ button(text: string, opts?: ActionButtonOpts): ButtonElement;
42
+ }
43
+
44
+ /** 액션을 한 번 정의 → 카드 버튼과 서버 핸들러가 같은 id/scope/values 를 공유한다. */
45
+ export function defineAction<V extends z.ZodTypeAny = z.ZodTypeAny>(def: {
46
+ id: string;
47
+ scope: string;
48
+ destructive?: boolean;
49
+ values?: V;
50
+ /** 핸들러가 호출 가능한 MCP 도구 화이트리스트(미선언=backend 도구 호출 금지, fail-closed). */
51
+ tools?: string[];
52
+ }): AppAction<V> {
53
+ const values = (def.values ?? z.object({}).passthrough()) as unknown as V;
54
+ return {
55
+ id: def.id,
56
+ scope: def.scope,
57
+ destructive: def.destructive ?? false,
58
+ values,
59
+ ...(def.tools ? { tools: def.tools } : {}),
60
+ button(text, opts) {
61
+ const confirm =
62
+ opts?.confirm === true ? { title: text, text: "Are you sure?" } : opts?.confirm || undefined;
63
+ const el = {
64
+ type: "button" as const,
65
+ action_id: def.id,
66
+ text,
67
+ ...(opts?.style ? { style: opts.style } : {}),
68
+ ...(confirm ? { confirm } : {}),
69
+ ...(opts?.value ? { value: opts.value } : {}),
70
+ };
71
+ return el as ButtonElement;
72
+ },
73
+ };
74
+ }