@things-factory/ai-assistant 10.1.37 → 10.1.38

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 (28) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +15 -0
  3. package/dist-server/service/chat-message/chat-message-broadcast.js +23 -0
  4. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -1
  5. package/dist-server/service/chat-message/chat-message-room.d.ts +22 -0
  6. package/dist-server/service/chat-message/chat-message-room.js +40 -0
  7. package/dist-server/service/chat-message/chat-message-room.js.map +1 -0
  8. package/dist-server/service/chat-message/chat-message-subscription.js +16 -4
  9. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
  10. package/dist-server/service/chat-session/session-activity-subscription.js +1 -2
  11. package/dist-server/service/chat-session/session-activity-subscription.js.map +1 -1
  12. package/dist-server/service/index.d.ts +0 -1
  13. package/dist-server/service/index.js +0 -1
  14. package/dist-server/service/index.js.map +1 -1
  15. package/dist-server/tsconfig.tsbuildinfo +1 -1
  16. package/package.json +4 -4
  17. package/server/service/chat-message/chat-message-broadcast.ts +41 -0
  18. package/server/service/chat-message/chat-message-room.test.ts +140 -0
  19. package/server/service/chat-message/chat-message-room.ts +45 -0
  20. package/server/service/chat-message/chat-message-subscription.ts +20 -5
  21. package/server/service/chat-session/session-activity-subscription.ts +1 -2
  22. package/server/service/index.ts +0 -1
  23. package/server/tool-registry-boot-report.test.ts +13 -5
  24. package/dist-server/service/subscribe-auth.d.ts +0 -14
  25. package/dist-server/service/subscribe-auth.js +0 -33
  26. package/dist-server/service/subscribe-auth.js.map +0 -1
  27. package/server/service/subscribe-auth.test.ts +0 -57
  28. package/server/service/subscribe-auth.ts +0 -34
@@ -11,3 +11,18 @@ export interface ChatMessagePayload {
11
11
  * 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).
12
12
  */
13
13
  export declare function chatMessageFilter(domainId: string, sessionId: string): (payload: ChatMessagePayload) => boolean;
14
+ /** 방에 대한 두 물음. 저장소는 부르는 쪽이 준다 — 이 파일은 저장소를 모른다. */
15
+ export interface ChatRoomLookup {
16
+ /** 그 세션의 지금 공개 범위. 이 도메인에 그런 세션이 없으면 `undefined`. */
17
+ visibility(): Promise<string | null | undefined>;
18
+ /** 부르는 사람이 지금 그 세션의 참여자인가. */
19
+ isParticipant(): Promise<boolean>;
20
+ }
21
+ export declare function mayHearRoom(room: ChatRoomLookup): Promise<boolean>;
22
+ /**
23
+ * 같은 도메인 + 세션이고 **그 방이 지금 이 사람에게 열려 있을 때만** 통과한다.
24
+ *
25
+ * 방은 메시지마다 다시 묻는다 — 누가 듣고 있는 동안 세션이 비공개로 바뀔 수 있다. 사람의 대화라
26
+ * 메시지가 드물고, 공개 방은 조회 하나로 끝난다.
27
+ */
28
+ export declare function chatMessageRoomFilter(domainId: string, sessionId: string, room: ChatRoomLookup): (payload: ChatMessagePayload) => Promise<boolean>;
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CHAT_MESSAGE_TOPIC = void 0;
4
4
  exports.chatMessageFilter = chatMessageFilter;
5
+ exports.mayHearRoom = mayHearRoom;
6
+ exports.chatMessageRoomFilter = chatMessageRoomFilter;
7
+ const session_visibility_js_1 = require("../chat-session/session-visibility.js");
5
8
  /** chat-message 브로드캐스트 토픽. */
6
9
  exports.CHAT_MESSAGE_TOPIC = 'board-ai-chat-message';
7
10
  /**
@@ -10,4 +13,24 @@ exports.CHAT_MESSAGE_TOPIC = 'board-ai-chat-message';
10
13
  function chatMessageFilter(domainId, sessionId) {
11
14
  return (payload) => !!payload && payload.domainId === domainId && payload.sessionId === sessionId;
12
15
  }
16
+ async function mayHearRoom(room) {
17
+ const visibility = await room.visibility();
18
+ /* 이 도메인에 없는 세션 — 열림으로 읽지 않는다. */
19
+ if (visibility === undefined)
20
+ return false;
21
+ /* 공개 방이면 참여 여부를 묻지 않는다. 방송마다 조회가 하나 준다. */
22
+ if ((0, session_visibility_js_1.mayReadSession)(visibility, false))
23
+ return true;
24
+ return (0, session_visibility_js_1.mayReadSession)(visibility, await room.isParticipant());
25
+ }
26
+ /**
27
+ * 같은 도메인 + 세션이고 **그 방이 지금 이 사람에게 열려 있을 때만** 통과한다.
28
+ *
29
+ * 방은 메시지마다 다시 묻는다 — 누가 듣고 있는 동안 세션이 비공개로 바뀔 수 있다. 사람의 대화라
30
+ * 메시지가 드물고, 공개 방은 조회 하나로 끝난다.
31
+ */
32
+ function chatMessageRoomFilter(domainId, sessionId, room) {
33
+ const sameSession = chatMessageFilter(domainId, sessionId);
34
+ return async (payload) => sameSession(payload) && (await mayHearRoom(room));
35
+ }
13
36
  //# sourceMappingURL=chat-message-broadcast.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"chat-message-broadcast.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-broadcast.ts"],"names":[],"mappings":";;;AAuBA,8CAGC;AAhBD,8BAA8B;AACjB,QAAA,kBAAkB,GAAG,uBAAuB,CAAA;AASzD;;GAEG;AACH,SAAgB,iBAAiB,CAAC,QAAgB,EAAE,SAAiB;IACnE,OAAO,CAAC,OAA2B,EAAW,EAAE,CAC9C,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,CAAA;AACjF,CAAC","sourcesContent":["/*\n * chat-message 브로드캐스트 — 순수 로직 (토픽/페이로드/필터). 데코레이터·pubsub import 없음\n * → 단위테스트 가능. 데코레이터 resolver(chat-message-subscription.ts)와 pubsub 의존\n * publishChatMessage 는 별도.\n *\n * patch spine(board-patch-broadcast.ts)과 동일 컨벤션. 다만 채팅 메시지는 board 가 아니라\n * **ChatSession 단위**로 묶이므로 필터 키가 sessionId (+ domain 격리).\n */\nimport type { ChatMessage } from './chat-message.js' // type-only — 런타임 import 아님\n\n/** chat-message 브로드캐스트 토픽. */\nexport const CHAT_MESSAGE_TOPIC = 'board-ai-chat-message'\n\n/** 방송 페이로드 — 구독 필터(domain+session)용 메타 + ChatMessage 본체. */\nexport interface ChatMessagePayload {\n domainId: string\n sessionId?: string\n message: ChatMessage\n}\n\n/**\n * 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).\n */\nexport function chatMessageFilter(domainId: string, sessionId: string) {\n return (payload: ChatMessagePayload): boolean =>\n !!payload && payload.domainId === domainId && payload.sessionId === sessionId\n}\n"]}
1
+ {"version":3,"file":"chat-message-broadcast.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-broadcast.ts"],"names":[],"mappings":";;;AAwBA,8CAGC;AAmBD,kCAUC;AAQD,sDAGC;AA1DD,iFAAsE;AAEtE,8BAA8B;AACjB,QAAA,kBAAkB,GAAG,uBAAuB,CAAA;AASzD;;GAEG;AACH,SAAgB,iBAAiB,CAAC,QAAgB,EAAE,SAAiB;IACnE,OAAO,CAAC,OAA2B,EAAW,EAAE,CAC9C,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,CAAA;AACjF,CAAC;AAmBM,KAAK,UAAU,WAAW,CAAC,IAAoB;IACpD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAA;IAE1C,iCAAiC;IACjC,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAE1C,2CAA2C;IAC3C,IAAI,IAAA,sCAAc,EAAC,UAAU,EAAE,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAElD,OAAO,IAAA,sCAAc,EAAC,UAAU,EAAE,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC,CAAA;AAC/D,CAAC;AAED;;;;;GAKG;AACH,SAAgB,qBAAqB,CAAC,QAAgB,EAAE,SAAiB,EAAE,IAAoB;IAC7F,MAAM,WAAW,GAAG,iBAAiB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IAC1D,OAAO,KAAK,EAAE,OAA2B,EAAoB,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC,CAAA;AACnH,CAAC","sourcesContent":["/*\n * chat-message 브로드캐스트 — 순수 로직 (토픽/페이로드/필터). 데코레이터·pubsub import 없음\n * → 단위테스트 가능. 데코레이터 resolver(chat-message-subscription.ts)와 pubsub 의존\n * publishChatMessage 는 별도.\n *\n * patch spine(board-patch-broadcast.ts)과 동일 컨벤션. 다만 채팅 메시지는 board 가 아니라\n * **ChatSession 단위**로 묶이므로 필터 키가 sessionId (+ domain 격리).\n */\nimport type { ChatMessage } from './chat-message.js' // type-only — 런타임 import 아님\nimport { mayReadSession } from '../chat-session/session-visibility.js'\n\n/** chat-message 브로드캐스트 토픽. */\nexport const CHAT_MESSAGE_TOPIC = 'board-ai-chat-message'\n\n/** 방송 페이로드 — 구독 필터(domain+session)용 메타 + ChatMessage 본체. */\nexport interface ChatMessagePayload {\n domainId: string\n sessionId?: string\n message: ChatMessage\n}\n\n/**\n * 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).\n */\nexport function chatMessageFilter(domainId: string, sessionId: string) {\n return (payload: ChatMessagePayload): boolean =>\n !!payload && payload.domainId === domainId && payload.sessionId === sessionId\n}\n\n/*\n * ── 방이 이 사람에게 열려 있나 — ADR-0073 결정 2 ────────────────────────────\n *\n * 공개 대화는 도메인 안 누구나, 비공개 대화는 참여자만. 저장된 길(`chatMessages`)은 그것을 묻고\n * 있었고 방송 길은 묻지 않았다 — 구독 때 도메인 소속만 보고, 필터는 도메인 + 세션만 맞췄다. 그래서\n * 비공개 세션의 id 를 가진 도메인 안 누구나 그 대화를 실시간으로 받았다.\n *\n * 규칙은 한 줄(`mayReadSession`)이고 두 길이 같은 줄을 읽는다. 여기서 다시 적으면 그날부터 두 벌이다.\n */\n/** 방에 대한 두 물음. 저장소는 부르는 쪽이 준다 — 이 파일은 저장소를 모른다. */\nexport interface ChatRoomLookup {\n /** 그 세션의 지금 공개 범위. 이 도메인에 그런 세션이 없으면 `undefined`. */\n visibility(): Promise<string | null | undefined>\n /** 부르는 사람이 지금 그 세션의 참여자인가. */\n isParticipant(): Promise<boolean>\n}\n\nexport async function mayHearRoom(room: ChatRoomLookup): Promise<boolean> {\n const visibility = await room.visibility()\n\n /* 이 도메인에 없는 세션 — 열림으로 읽지 않는다. */\n if (visibility === undefined) return false\n\n /* 공개 방이면 참여 여부를 묻지 않는다. 방송마다 조회가 하나 준다. */\n if (mayReadSession(visibility, false)) return true\n\n return mayReadSession(visibility, await room.isParticipant())\n}\n\n/**\n * 같은 도메인 + 세션이고 **그 방이 지금 이 사람에게 열려 있을 때만** 통과한다.\n *\n * 방은 메시지마다 다시 묻는다 — 누가 듣고 있는 동안 세션이 비공개로 바뀔 수 있다. 사람의 대화라\n * 메시지가 드물고, 공개 방은 조회 하나로 끝난다.\n */\nexport function chatMessageRoomFilter(domainId: string, sessionId: string, room: ChatRoomLookup) {\n const sameSession = chatMessageFilter(domainId, sessionId)\n return async (payload: ChatMessagePayload): Promise<boolean> => sameSession(payload) && (await mayHearRoom(room))\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { type ChatRoomLookup } from './chat-message-broadcast.js';
2
+ interface FindOne {
3
+ findOne(options: {
4
+ where: any;
5
+ select?: any;
6
+ }): Promise<any>;
7
+ }
8
+ /**
9
+ * 방을 묻는 두 조회. **둘 다 부르는 사람의 도메인 안에서** 찾는다 — 저장된 길(`chatMessages`)의 질의와
10
+ * 같은 조건이다.
11
+ */
12
+ export declare function chatRoomLookup(repos: {
13
+ sessions: FindOne;
14
+ participants: FindOne;
15
+ }, who: {
16
+ domainId: string;
17
+ sessionId: string;
18
+ userId?: string;
19
+ }): ChatRoomLookup;
20
+ /** 구독이 돌려주는 스트림 — 같은 도메인 + 세션이고, 그 방이 메시지마다 이 사람에게 열려 있을 때만. */
21
+ export declare function chatMessageStream(domainId: string, sessionId: string, room: ChatRoomLookup): import("graphql-yoga").Repeater<import("./chat-message-broadcast.js").ChatMessagePayload, void, unknown>;
22
+ export {};
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.chatRoomLookup = chatRoomLookup;
4
+ exports.chatMessageStream = chatMessageStream;
5
+ /*
6
+ * 방송 길의 방 — 구독이 돌려주는 스트림과, 방을 묻는 두 조회.
7
+ *
8
+ * 구독 리졸버는 파라미터 데코레이터(@Root · @Arg) 때문에 jest 가 못 읽는다. 그래서 스트림을 짜는 일을
9
+ * 여기로 뺐다 — 시험이 리졸버가 돌려주는 것과 **같은 것**을 돌린다(`chat-message-room.test.ts`).
10
+ *
11
+ * 저장소는 부르는 쪽이 넘긴다. 엔티티를 여기서 import 하면 이 파일도 시험에서 못 읽는다.
12
+ */
13
+ const graphql_yoga_1 = require("graphql-yoga");
14
+ const shell_1 = require("@things-factory/shell");
15
+ const chat_message_broadcast_js_1 = require("./chat-message-broadcast.js");
16
+ /**
17
+ * 방을 묻는 두 조회. **둘 다 부르는 사람의 도메인 안에서** 찾는다 — 저장된 길(`chatMessages`)의 질의와
18
+ * 같은 조건이다.
19
+ */
20
+ function chatRoomLookup(repos, who) {
21
+ return {
22
+ async visibility() {
23
+ const session = await repos.sessions.findOne({ where: { id: who.sessionId, domain: { id: who.domainId } } });
24
+ return session ? (session.visibility ?? null) : undefined;
25
+ },
26
+ async isParticipant() {
27
+ if (!who.userId)
28
+ return false;
29
+ const seat = await repos.participants.findOne({
30
+ where: { session: { id: who.sessionId }, user: { id: who.userId }, domain: { id: who.domainId } }
31
+ });
32
+ return !!seat;
33
+ }
34
+ };
35
+ }
36
+ /** 구독이 돌려주는 스트림 — 같은 도메인 + 세션이고, 그 방이 메시지마다 이 사람에게 열려 있을 때만. */
37
+ function chatMessageStream(domainId, sessionId, room) {
38
+ return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(chat_message_broadcast_js_1.CHAT_MESSAGE_TOPIC), (0, graphql_yoga_1.filter)((0, chat_message_broadcast_js_1.chatMessageRoomFilter)(domainId, sessionId, room)));
39
+ }
40
+ //# sourceMappingURL=chat-message-room.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-message-room.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-room.ts"],"names":[],"mappings":";;AAqBA,wCAkBC;AAGD,8CAEC;AA5CD;;;;;;;GAOG;AACH,+CAA2C;AAC3C,iDAA8C;AAE9C,2EAA4G;AAM5G;;;GAGG;AACH,SAAgB,cAAc,CAC5B,KAAmD,EACnD,GAA6D;IAE7D,OAAO;QACL,KAAK,CAAC,UAAU;YACd,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAA;YAC5G,OAAO,OAAO,CAAC,CAAC,CAAC,CAAE,OAAe,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACpE,CAAC;QAED,KAAK,CAAC,aAAa;YACjB,IAAI,CAAC,GAAG,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAA;YAC7B,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC;gBAC5C,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE;aAClG,CAAC,CAAA;YACF,OAAO,CAAC,CAAC,IAAI,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC;AAED,gEAAgE;AAChE,SAAgB,iBAAiB,CAAC,QAAgB,EAAE,SAAiB,EAAE,IAAoB;IACzF,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,8CAAkB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,iDAAqB,EAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;AAC7G,CAAC","sourcesContent":["/*\n * 방송 길의 방 — 구독이 돌려주는 스트림과, 방을 묻는 두 조회.\n *\n * 구독 리졸버는 파라미터 데코레이터(@Root · @Arg) 때문에 jest 가 못 읽는다. 그래서 스트림을 짜는 일을\n * 여기로 뺐다 — 시험이 리졸버가 돌려주는 것과 **같은 것**을 돌린다(`chat-message-room.test.ts`).\n *\n * 저장소는 부르는 쪽이 넘긴다. 엔티티를 여기서 import 하면 이 파일도 시험에서 못 읽는다.\n */\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\n\nimport { CHAT_MESSAGE_TOPIC, chatMessageRoomFilter, type ChatRoomLookup } from './chat-message-broadcast.js'\n\ninterface FindOne {\n findOne(options: { where: any; select?: any }): Promise<any>\n}\n\n/**\n * 방을 묻는 두 조회. **둘 다 부르는 사람의 도메인 안에서** 찾는다 — 저장된 길(`chatMessages`)의 질의와\n * 같은 조건이다.\n */\nexport function chatRoomLookup(\n repos: { sessions: FindOne; participants: FindOne },\n who: { domainId: string; sessionId: string; userId?: string }\n): ChatRoomLookup {\n return {\n async visibility() {\n const session = await repos.sessions.findOne({ where: { id: who.sessionId, domain: { id: who.domainId } } })\n return session ? ((session as any).visibility ?? null) : undefined\n },\n\n async isParticipant() {\n if (!who.userId) return false\n const seat = await repos.participants.findOne({\n where: { session: { id: who.sessionId }, user: { id: who.userId }, domain: { id: who.domainId } }\n })\n return !!seat\n }\n }\n}\n\n/** 구독이 돌려주는 스트림 — 같은 도메인 + 세션이고, 그 방이 메시지마다 이 사람에게 열려 있을 때만. */\nexport function chatMessageStream(domainId: string, sessionId: string, room: ChatRoomLookup) {\n return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageRoomFilter(domainId, sessionId, room)))\n}\n"]}
@@ -18,11 +18,12 @@ const tslib_1 = require("tslib");
18
18
  * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.
19
19
  */
20
20
  const type_graphql_1 = require("type-graphql");
21
- const graphql_yoga_1 = require("graphql-yoga");
22
21
  const shell_1 = require("@things-factory/shell");
23
22
  const chat_message_js_1 = require("./chat-message.js");
23
+ const chat_session_js_1 = require("../chat-session/chat-session.js");
24
+ const chat_session_participant_js_1 = require("../chat-session-participant/chat-session-participant.js");
24
25
  const chat_message_broadcast_js_1 = require("./chat-message-broadcast.js");
25
- const subscribe_auth_js_1 = require("../subscribe-auth.js");
26
+ const chat_message_room_js_1 = require("./chat-message-room.js");
26
27
  const reader_gate_js_1 = require("./reader-gate.js");
27
28
  // publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).
28
29
  var chat_message_publish_js_1 = require("./chat-message-publish.js");
@@ -53,8 +54,19 @@ tslib_1.__decorate([
53
54
  subscribe: async ({ args, context }) => {
54
55
  const { domain, user } = context.state;
55
56
  const { sessionId } = args;
56
- await (0, subscribe_auth_js_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
57
- return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(chat_message_broadcast_js_1.CHAT_MESSAGE_TOPIC), (0, graphql_yoga_1.filter)((0, chat_message_broadcast_js_1.chatMessageFilter)(domain.id, sessionId)));
57
+ await (0, shell_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
58
+ /*
59
+ * **방을 묻는다** — ADR-0073 결정 2. 공개 방은 도메인 안 누구나, 비공개 방은 참여자만. 저장된 길
60
+ * (`chatMessages`)은 이것을 물었고 방송 길은 묻지 않아, 비공개 세션의 id 를 가진 도메인 안 누구나
61
+ * 그 대화를 실시간으로 받았다.
62
+ *
63
+ * 못 들어오는 방은 빈 스트림을 주지 않고 거절한다. 없는 세션과 같은 말이다 — 비공개 방이 있다는
64
+ * 사실도 알려 주지 않는다. 구독이 선 뒤에도 방은 메시지마다 다시 묻는다(`chatMessageRoomFilter`).
65
+ */
66
+ const room = (0, chat_message_room_js_1.chatRoomLookup)({ sessions: (0, shell_1.getRepository)(chat_session_js_1.ChatSession), participants: (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant) }, { domainId: domain.id, sessionId, userId: user?.id });
67
+ if (!(await (0, chat_message_broadcast_js_1.mayHearRoom)(room)))
68
+ throw new Error('chat session not found');
69
+ return (0, chat_message_room_js_1.chatMessageStream)(domain.id, sessionId, room);
58
70
  }
59
71
  })
60
72
  /*
@@ -1 +1 @@
1
- {"version":3,"file":"chat-message-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,+CAAqE;AACrE,+CAA2C;AAC3C,iDAA8C;AAC9C,uDAA+C;AAC/C,2EAA4G;AAC5G,4DAAmE;AACnE,qDAA6C;AAE7C,8EAA8E;AAC9E,qEAA8D;AAArD,6HAAA,kBAAkB,OAAA;AAC3B,yEAAmF;AAA1E,+HAAA,kBAAkB,OAAA;AAAE,8HAAA,iBAAiB,OAAA;AAIvC,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAuB5B,AAJN;;;OAGG;IACH,KAAK,CAAC,mBAAmB,CACf,OAA2B,EACjB,SAAiB,EAC5B,OAAY;QAEnB,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAA,2BAAU,EAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;QAC9D,OAAO,OAAsB,CAAA;IAC/B,CAAC;CACF,CAAA;AA/BY,0DAAuB;AAuB5B;IAtBL,IAAA,2BAAY,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE;QAC/B;;;;;WAKG;QACH,WAAW,EACT,gGAAgG;QAClG,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;YAE1B,MAAM,IAAA,gDAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,8CAAkB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,6CAAiB,EAAC,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;QACpG,CAAC;KACF,CAAC;IACF;;;OAGG;;IAEA,mBAAA,IAAA,mBAAI,GAAE,CAAA;IACN,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;IAChB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;kEAIP;kCA9BU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,6BAAW,CAAC;GACT,uBAAuB,CA+BnC","sourcesContent":["/*\n * ChatMessageSubscription — 그룹챗 메시지 실시간 방송.\n *\n * 클래스 이름이 `BoardChatMessageSubscription` 이었다. 전선 이름(`chatMessageActivity`)은 처음부터\n * 중립이었고 클래스 이름만 보드였다 — 중립 패키지로 옮기면서 고쳤다(2026-09-10). 전선이 안 바뀌므로\n * 소비처는 그대로다.\n *\n * ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →\n * 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.\n * 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).\n *\n * patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.\n * 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).\n * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.\n */\nimport { Resolver, Subscription, Root, Arg, Ctx } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\nimport { ChatMessage } from './chat-message.js'\nimport { CHAT_MESSAGE_TOPIC, chatMessageFilter, type ChatMessagePayload } from './chat-message-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '../subscribe-auth.js'\nimport { readerGate } from './reader-gate.js'\n\n// publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).\nexport { publishChatMessage } from './chat-message-publish.js'\nexport { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js'\nexport type { ChatMessagePayload } from './chat-message-broadcast.js'\n\n@Resolver(ChatMessage)\nexport class ChatMessageSubscription {\n @Subscription(() => ChatMessage, {\n /*\n * 돌려주는 형을 **명시한다**. 이 핸들러가 async 가 되면서(받는 사람마다 가리기 — ADR-0073 판정 1)\n * 반환형이 `Promise<ChatMessage>` 가 되었고, type-graphql 의 reflection 은 Promise 안을 못 본다.\n * 빠지면 스키마를 세울 때 NoExplicitTypeError 로 **패키지 적재 자체가 실패한다** — 시험이 아니라\n * 부팅이 죽는다. plant 레인이 설치본에서 이 오류를 잡아 알려 주었다(2026-09-18).\n */\n description:\n 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { sessionId } = args\n\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domain.id, sessionId)))\n }\n })\n /*\n * 방송도 **받는 사람 기준으로** 가린다 — ADR-0073 판정 1. 저장해 놓고 읽을 때만 가리면, 그 자리에\n * 앉아 있던 사람은 방송으로 원문을 받는다. 같은 값이 두 길로 나가고 한쪽만 문을 본다.\n */\n async chatMessageActivity(\n @Root() payload: ChatMessagePayload,\n @Arg('sessionId') sessionId: string,\n @Ctx() context: any\n ): Promise<ChatMessage> {\n const [message] = await readerGate(context)([payload.message])\n return message as ChatMessage\n }\n}\n"]}
1
+ {"version":3,"file":"chat-message-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,+CAAqE;AACrE,iDAAmF;AACnF,uDAA+C;AAC/C,qEAA6D;AAC7D,yGAAgG;AAChG,2EAAkF;AAClF,iEAA0E;AAC1E,qDAA6C;AAE7C,8EAA8E;AAC9E,qEAA8D;AAArD,6HAAA,kBAAkB,OAAA;AAC3B,yEAAmF;AAA1E,+HAAA,kBAAkB,OAAA;AAAE,8HAAA,iBAAiB,OAAA;AAIvC,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAqC5B,AAJN;;;OAGG;IACH,KAAK,CAAC,mBAAmB,CACf,OAA2B,EACjB,SAAiB,EAC5B,OAAY;QAEnB,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAA,2BAAU,EAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;QAC9D,OAAO,OAAsB,CAAA;IAC/B,CAAC;CACF,CAAA;AA7CY,0DAAuB;AAqC5B;IApCL,IAAA,2BAAY,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE;QAC/B;;;;;WAKG;QACH,WAAW,EACT,gGAAgG;QAClG,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;YAE1B,MAAM,IAAA,oCAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F;;;;;;;eAOG;YACH,MAAM,IAAI,GAAG,IAAA,qCAAc,EACzB,EAAE,QAAQ,EAAE,IAAA,qBAAa,EAAC,6BAAW,CAAC,EAAE,YAAY,EAAE,IAAA,qBAAa,EAAC,oDAAsB,CAAC,EAAE,EAC7F,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CACrD,CAAA;YACD,IAAI,CAAC,CAAC,MAAM,IAAA,uCAAW,EAAC,IAAI,CAAC,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;YAEzE,OAAO,IAAA,wCAAiB,EAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;QACtD,CAAC;KACF,CAAC;IACF;;;OAGG;;IAEA,mBAAA,IAAA,mBAAI,GAAE,CAAA;IACN,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;IAChB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;kEAIP;kCA5CU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,6BAAW,CAAC;GACT,uBAAuB,CA6CnC","sourcesContent":["/*\n * ChatMessageSubscription — 그룹챗 메시지 실시간 방송.\n *\n * 클래스 이름이 `BoardChatMessageSubscription` 이었다. 전선 이름(`chatMessageActivity`)은 처음부터\n * 중립이었고 클래스 이름만 보드였다 — 중립 패키지로 옮기면서 고쳤다(2026-09-10). 전선이 안 바뀌므로\n * 소비처는 그대로다.\n *\n * ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →\n * 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.\n * 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).\n *\n * patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.\n * 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).\n * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.\n */\nimport { Resolver, Subscription, Root, Arg, Ctx } from 'type-graphql'\nimport { assertDomainSubscribeAllowed, getRepository } from '@things-factory/shell'\nimport { ChatMessage } from './chat-message.js'\nimport { ChatSession } from '../chat-session/chat-session.js'\nimport { ChatSessionParticipant } from '../chat-session-participant/chat-session-participant.js'\nimport { mayHearRoom, type ChatMessagePayload } from './chat-message-broadcast.js'\nimport { chatMessageStream, chatRoomLookup } from './chat-message-room.js'\nimport { readerGate } from './reader-gate.js'\n\n// publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).\nexport { publishChatMessage } from './chat-message-publish.js'\nexport { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js'\nexport type { ChatMessagePayload } from './chat-message-broadcast.js'\n\n@Resolver(ChatMessage)\nexport class ChatMessageSubscription {\n @Subscription(() => ChatMessage, {\n /*\n * 돌려주는 형을 **명시한다**. 이 핸들러가 async 가 되면서(받는 사람마다 가리기 — ADR-0073 판정 1)\n * 반환형이 `Promise<ChatMessage>` 가 되었고, type-graphql 의 reflection 은 Promise 안을 못 본다.\n * 빠지면 스키마를 세울 때 NoExplicitTypeError 로 **패키지 적재 자체가 실패한다** — 시험이 아니라\n * 부팅이 죽는다. plant 레인이 설치본에서 이 오류를 잡아 알려 주었다(2026-09-18).\n */\n description:\n 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { sessionId } = args\n\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n /*\n * **방을 묻는다** — ADR-0073 결정 2. 공개 방은 도메인 안 누구나, 비공개 방은 참여자만. 저장된 길\n * (`chatMessages`)은 이것을 물었고 방송 길은 묻지 않아, 비공개 세션의 id 를 가진 도메인 안 누구나\n * 그 대화를 실시간으로 받았다.\n *\n * 못 들어오는 방은 빈 스트림을 주지 않고 거절한다. 없는 세션과 같은 말이다 — 비공개 방이 있다는\n * 사실도 알려 주지 않는다. 구독이 선 뒤에도 방은 메시지마다 다시 묻는다(`chatMessageRoomFilter`).\n */\n const room = chatRoomLookup(\n { sessions: getRepository(ChatSession), participants: getRepository(ChatSessionParticipant) },\n { domainId: domain.id, sessionId, userId: user?.id }\n )\n if (!(await mayHearRoom(room))) throw new Error('chat session not found')\n\n return chatMessageStream(domain.id, sessionId, room)\n }\n })\n /*\n * 방송도 **받는 사람 기준으로** 가린다 — ADR-0073 판정 1. 저장해 놓고 읽을 때만 가리면, 그 자리에\n * 앉아 있던 사람은 방송으로 원문을 받는다. 같은 값이 두 길로 나가고 한쪽만 문을 본다.\n */\n async chatMessageActivity(\n @Root() payload: ChatMessagePayload,\n @Arg('sessionId') sessionId: string,\n @Ctx() context: any\n ): Promise<ChatMessage> {\n const [message] = await readerGate(context)([payload.message])\n return message as ChatMessage\n }\n}\n"]}
@@ -14,7 +14,6 @@ const type_graphql_1 = require("type-graphql");
14
14
  const graphql_yoga_1 = require("graphql-yoga");
15
15
  const shell_1 = require("@things-factory/shell");
16
16
  const session_activity_broadcast_js_1 = require("./session-activity-broadcast.js");
17
- const subscribe_auth_js_1 = require("../subscribe-auth.js");
18
17
  var session_activity_publish_js_1 = require("./session-activity-publish.js");
19
18
  Object.defineProperty(exports, "publishSessionActivity", { enumerable: true, get: function () { return session_activity_publish_js_1.publishSessionActivity; } });
20
19
  let SessionActivity = class SessionActivity {
@@ -72,7 +71,7 @@ tslib_1.__decorate([
72
71
  description: 'Subscribes to activity in every session the current user participates in, so a closed chat surface can still show unread badges and refresh its list.',
73
72
  subscribe: async ({ context }) => {
74
73
  const { domain, user } = context.state;
75
- await (0, subscribe_auth_js_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
74
+ await (0, shell_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
76
75
  return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(session_activity_broadcast_js_1.SESSION_ACTIVITY_TOPIC), (0, graphql_yoga_1.filter)((0, session_activity_broadcast_js_1.sessionActivityFilter)(domain.id, user.id)));
77
76
  }
78
77
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"session-activity-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;GAOG;AACH,+CAA8E;AAC9E,+CAA2C;AAC3C,iDAA8C;AAE9C,mFAA4H;AAC5H,4DAAmE;AAEnE,6EAAsE;AAA7D,qIAAA,sBAAsB,OAAA;AAGxB,IAAM,eAAe,GAArB,MAAM,eAAe;CAwB3B,CAAA;AAxBY,0CAAe;AAE1B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;kDACnC;AAMlB;IAJC,IAAA,oBAAK,EAAC;QACL,WAAW,EACT,0JAA0J;KAC7J,CAAC;;6CACW;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;mDACjG;AAGnB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;iDACpD;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;2CACzC;AAGX;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;gDAChE;AAGhB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;gDACzF;0BAvBL,eAAe;IAD3B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,4GAA4G,EAAE,CAAC;GAC7H,eAAe,CAwB3B;AAGM,IAAM,6BAA6B,GAAnC,MAAM,6BAA6B;IAUxC,iBAAiB,CAAS,OAA+B;QACvD,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,SAAS;YAC/B,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAA;IACH,CAAC;CACF,CAAA;AArBY,sEAA6B;AAUxC;IATC,IAAA,2BAAY,EAAC,GAAG,EAAE,CAAC,eAAe,EAAE;QACnC,WAAW,EACT,uJAAuJ;QACzJ,SAAS,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,IAAA,gDAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAC1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,sDAAsB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,qDAAqB,EAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAC1G,CAAC;KACF,CAAC;IACiB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;;4CAAmC,eAAe;sEAU1E;wCApBU,6BAA6B;IADzC,IAAA,uBAAQ,EAAC,eAAe,CAAC;GACb,6BAA6B,CAqBzC","sourcesContent":["/*\n * MySessionActivitySubscription — **내가 참여한 모든 세션**의 활동 알림.\n *\n * 기존 chatMessageActivity 는 그 세션을 열어 둔 구독자에게 본문을 보낸다 → 창을 닫아 두면 새 메시지를\n * 알 수 없었다. 이 구독은 배지·목록 갱신용으로 최소 정보만 보낸다(본문 없음).\n *\n * 도메인 격리는 필터가, \"내 것만\" 은 참여자 검사가 성립시킨다(session-activity-broadcast.ts).\n */\nimport { Resolver, Subscription, Root, ObjectType, Field } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\n\nimport { SESSION_ACTIVITY_TOPIC, sessionActivityFilter, type SessionActivityPayload } from './session-activity-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '../subscribe-auth.js'\n\nexport { publishSessionActivity } from './session-activity-publish.js'\n\n@ObjectType({ description: 'Activity notice for a session the current user participates in — badge/list refresh only, no message body.' })\nexport class SessionActivity {\n @Field({ description: 'Session that had activity.' })\n sessionId!: string\n\n @Field({\n description:\n \"Kind of activity: 'message' (new message — raise the unread badge) or 'meta' (session itself changed, e.g. renamed — refresh the list but do not badge).\"\n })\n kind!: string\n\n @Field({ nullable: true, description: \"What the session is anchored to: 'board' | 'space' | 'instance' | 'none'.\" })\n anchorType?: string\n\n @Field({ nullable: true, description: 'Id of the anchored target.' })\n anchorId?: string\n\n @Field({ description: 'Activity timestamp (ISO).' })\n at!: string\n\n @Field({ nullable: true, description: 'One-line preview (not the full body).' })\n preview?: string\n\n @Field({ nullable: true, description: 'User who caused the activity — clients skip badging their own.' })\n actorId?: string\n}\n\n@Resolver(SessionActivity)\nexport class MySessionActivitySubscription {\n @Subscription(() => SessionActivity, {\n description:\n 'Subscribes to activity in every session the current user participates in, so a closed chat surface can still show unread badges and refresh its list.',\n subscribe: async ({ context }) => {\n const { domain, user } = context.state\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n return pipe(pubsub.subscribe(SESSION_ACTIVITY_TOPIC), filter(sessionActivityFilter(domain.id, user.id)))\n }\n })\n mySessionActivity(@Root() payload: SessionActivityPayload): SessionActivity {\n return {\n sessionId: payload.sessionId,\n kind: payload.kind ?? 'message',\n anchorType: payload.anchorType,\n anchorId: payload.anchorId,\n at: payload.at,\n preview: payload.preview,\n actorId: payload.actorId\n }\n }\n}\n"]}
1
+ {"version":3,"file":"session-activity-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;GAOG;AACH,+CAA8E;AAC9E,+CAA2C;AAC3C,iDAA4E;AAE5E,mFAA4H;AAE5H,6EAAsE;AAA7D,qIAAA,sBAAsB,OAAA;AAGxB,IAAM,eAAe,GAArB,MAAM,eAAe;CAwB3B,CAAA;AAxBY,0CAAe;AAE1B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;kDACnC;AAMlB;IAJC,IAAA,oBAAK,EAAC;QACL,WAAW,EACT,0JAA0J;KAC7J,CAAC;;6CACW;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;mDACjG;AAGnB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;iDACpD;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;2CACzC;AAGX;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;gDAChE;AAGhB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;gDACzF;0BAvBL,eAAe;IAD3B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,4GAA4G,EAAE,CAAC;GAC7H,eAAe,CAwB3B;AAGM,IAAM,6BAA6B,GAAnC,MAAM,6BAA6B;IAUxC,iBAAiB,CAAS,OAA+B;QACvD,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,SAAS;YAC/B,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAA;IACH,CAAC;CACF,CAAA;AArBY,sEAA6B;AAUxC;IATC,IAAA,2BAAY,EAAC,GAAG,EAAE,CAAC,eAAe,EAAE;QACnC,WAAW,EACT,uJAAuJ;QACzJ,SAAS,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,IAAA,oCAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAC1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,sDAAsB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,qDAAqB,EAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAC1G,CAAC;KACF,CAAC;IACiB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;;4CAAmC,eAAe;sEAU1E;wCApBU,6BAA6B;IADzC,IAAA,uBAAQ,EAAC,eAAe,CAAC;GACb,6BAA6B,CAqBzC","sourcesContent":["/*\n * MySessionActivitySubscription — **내가 참여한 모든 세션**의 활동 알림.\n *\n * 기존 chatMessageActivity 는 그 세션을 열어 둔 구독자에게 본문을 보낸다 → 창을 닫아 두면 새 메시지를\n * 알 수 없었다. 이 구독은 배지·목록 갱신용으로 최소 정보만 보낸다(본문 없음).\n *\n * 도메인 격리는 필터가, \"내 것만\" 은 참여자 검사가 성립시킨다(session-activity-broadcast.ts).\n */\nimport { Resolver, Subscription, Root, ObjectType, Field } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { assertDomainSubscribeAllowed, pubsub } from '@things-factory/shell'\n\nimport { SESSION_ACTIVITY_TOPIC, sessionActivityFilter, type SessionActivityPayload } from './session-activity-broadcast.js'\n\nexport { publishSessionActivity } from './session-activity-publish.js'\n\n@ObjectType({ description: 'Activity notice for a session the current user participates in — badge/list refresh only, no message body.' })\nexport class SessionActivity {\n @Field({ description: 'Session that had activity.' })\n sessionId!: string\n\n @Field({\n description:\n \"Kind of activity: 'message' (new message — raise the unread badge) or 'meta' (session itself changed, e.g. renamed — refresh the list but do not badge).\"\n })\n kind!: string\n\n @Field({ nullable: true, description: \"What the session is anchored to: 'board' | 'space' | 'instance' | 'none'.\" })\n anchorType?: string\n\n @Field({ nullable: true, description: 'Id of the anchored target.' })\n anchorId?: string\n\n @Field({ description: 'Activity timestamp (ISO).' })\n at!: string\n\n @Field({ nullable: true, description: 'One-line preview (not the full body).' })\n preview?: string\n\n @Field({ nullable: true, description: 'User who caused the activity — clients skip badging their own.' })\n actorId?: string\n}\n\n@Resolver(SessionActivity)\nexport class MySessionActivitySubscription {\n @Subscription(() => SessionActivity, {\n description:\n 'Subscribes to activity in every session the current user participates in, so a closed chat surface can still show unread badges and refresh its list.',\n subscribe: async ({ context }) => {\n const { domain, user } = context.state\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n return pipe(pubsub.subscribe(SESSION_ACTIVITY_TOPIC), filter(sessionActivityFilter(domain.id, user.id)))\n }\n })\n mySessionActivity(@Root() payload: SessionActivityPayload): SessionActivity {\n return {\n sessionId: payload.sessionId,\n kind: payload.kind ?? 'message',\n anchorType: payload.anchorType,\n anchorId: payload.anchorId,\n at: payload.at,\n preview: payload.preview,\n actorId: payload.actorId\n }\n }\n}\n"]}
@@ -11,7 +11,6 @@ export * from './assistant-mode/index.js';
11
11
  export * from './assistant-capability-resolver.js';
12
12
  export * from './tool-access.js';
13
13
  export * from './grounding-sources.js';
14
- export * from './subscribe-auth.js';
15
14
  export * from './chat-session/session-anchor.js';
16
15
  export * from './chat-session/session-visibility.js';
17
16
  export * from './chat-session/index.js';
@@ -24,7 +24,6 @@ tslib_1.__exportStar(require("./assistant-capability-resolver.js"), exports);
24
24
  tslib_1.__exportStar(require("./tool-access.js"), exports);
25
25
  /* 무엇이 근거인가 — 도구 결과 밖의 셋(프롬프트 · 대화 · 화면 스냅숏). */
26
26
  tslib_1.__exportStar(require("./grounding-sources.js"), exports);
27
- tslib_1.__exportStar(require("./subscribe-auth.js"), exports);
28
27
  /* 앵커 순수 로직 — 호스트가 앵커 종류를 참조한다(리졸버는 데코레이터 때문에 무겁다). */
29
28
  tslib_1.__exportStar(require("./chat-session/session-anchor.js"), exports);
30
29
  /* Who may read a conversation — one rule, also asked by doors outside this package (board-ai's revertPatch). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yEAAgE;AAChE,6EAAoE;AACpE,4FAAmF;AACnF,yFAAgF;AAChF,sGAA+F;AAC/F,8FAAqF;AACrF,sDAAyE;AACzE,sDAAyE;AACzE,kEAAgG;AAEhG,qDAA0B;AAC1B,qEAA0C;AAC1C,uEAA4C;AAC5C,oGAAoG;AACpG,oEAAyC;AACzC,6EAAkD;AAClD;;;GAGG;AACH,2DAAgC;AAChC,gDAAgD;AAChD,iEAAsC;AACtC,8DAAmC;AACnC,sDAAsD;AACtD,2EAAgD;AAChD,gHAAgH;AAChH,+EAAoD;AACpD,kEAAuC;AACvC,8DAA8D;AAC9D,0EAA+C;AAC/C,qFAA0D;AAC1D,uFAA4D;AAC5D,wEAA6C;AAC7C,4EAAiD;AACjD,yEAA8C;AAC9C,wDAAwD;AACxD,iFAAsD;AACtD,iFAAsD;AACtD,mFAAwD;AACxD,4FAAiE;AACjE,2DAA2D;AAC3D,uFAA4D;AAC5D,iFAAsD;AACtD,kEAAuC;AACvC,8EAAmD;AAEtC,QAAA,QAAQ,GAAG,CAAC,GAAG,mBAAmB,EAAE,GAAG,mBAAmB,EAAE,GAAG,mBAA8B,CAAC,CAAA;AAE9F,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,8CAAmB;QACnB,sDAAsD;QACtD,kDAAqB;QACrB,uDAAuD;QACvD,kDAAqB;QACrB,wDAAwD;QACxD,8DAA2B;QAC3B,sDAAuB;QACvB,qDAAqD;QACrD,gEAA6B;KAC9B;CACF,CAAA","sourcesContent":["import { ChatSessionResolver } from './chat-session-resolver.js'\nimport { AssistantChatResolver } from './assistant-chat-resolver.js'\nimport { AssistantModeResolver } from './assistant-mode/assistant-mode-resolver.js'\nimport { AssistantCapabilityResolver } from './assistant-capability-resolver.js'\nimport { MySessionActivitySubscription } from './chat-session/session-activity-subscription.js'\nimport { ChatMessageSubscription } from './chat-message/chat-message-subscription.js'\nimport { entities as ChatSessionEntities } from './chat-session/index.js'\nimport { entities as ChatMessageEntities } from './chat-message/index.js'\nimport { entities as ChatSessionParticipantEntities } from './chat-session-participant/index.js'\n\nexport * from './types.js'\nexport * from './chat-session-resolver.js'\nexport * from './assistant-chat-resolver.js'\n/* The mode registry — each app server declares what its conversations are (ADR-0061 decision 7). */\nexport * from './assistant-mode/index.js'\nexport * from './assistant-capability-resolver.js'\n/*\n * 대화 각본 하네스는 **런타임 export 에서 뺀다**(ADR-0070 A15 결정 ④) — 시험 도구가 서버 번들에\n * 실린다. 시험은 경로로 가져온다: `…/service/testing/conversation-script.js`.\n */\nexport * from './tool-access.js'\n/* 무엇이 근거인가 — 도구 결과 밖의 셋(프롬프트 · 대화 · 화면 스냅숏). */\nexport * from './grounding-sources.js'\nexport * from './subscribe-auth.js'\n/* 앵커 순수 로직 — 호스트가 앵커 종류를 참조한다(리졸버는 데코레이터 때문에 무겁다). */\nexport * from './chat-session/session-anchor.js'\n/* Who may read a conversation — one rule, also asked by doors outside this package (board-ai's revertPatch). */\nexport * from './chat-session/session-visibility.js'\nexport * from './chat-session/index.js'\n/* 순수 로직들 — 호스트(board-ai 등)가 같은 규칙을 쓰라고 공개한다. 베끼면 한쪽만 고쳐진다. */\nexport * from './chat-session/session-inbox.js'\nexport * from './chat-session/session-activity-publish.js'\nexport * from './chat-session/session-activity-broadcast.js'\nexport * from './chat-message/llm-history.js'\nexport * from './chat-message/history-summary.js'\nexport * from './chat-message/fold-history.js'\n/* 상한 밖으로 밀려난 앞부분을 요약으로 접는 orchestration — 중립 문이 부른다. */\nexport * from './chat-message/fold-session-history.js'\nexport * from './chat-message/chat-message-publish.js'\nexport * from './chat-message/chat-message-broadcast.js'\nexport * from './chat-session-participant/invite-notification.js'\n/* 멘션이 사람을 불러들이는 자리 — 사람의 말을 저장하는 문만 부른다(ADR-0071 결정 1). */\nexport * from './chat-session-participant/mention-invite.js'\nexport * from './chat-session-participant/presence.js'\nexport * from './chat-message/index.js'\nexport * from './chat-session-participant/index.js'\n\nexport const entities = [...ChatSessionEntities, ...ChatMessageEntities, ...ChatSessionParticipantEntities]\n\nexport const schema = {\n resolverClasses: [\n ChatSessionResolver,\n /* 문서를 갖지 않은 표면의 대화 — board 를 안 싣는 제품이 물어볼 수 있는 자리. */\n AssistantChatResolver,\n /* 화면이 자기 대화가 무엇을 주는지 묻는 자리 — 문구는 서버가 요청 로케일로 옮겨 낸다. */\n AssistantModeResolver,\n /* 「이 배포에 AI 가 있나 · 무엇이 선언 없이 등록됐나」 — 창구 하나(표준 A7-a). */\n AssistantCapabilityResolver,\n ChatMessageSubscription,\n /* 내가 참여한 모든 세션의 활동 — 창을 닫아 둔 표면이 배지·목록을 갱신할 수 있게. */\n MySessionActivitySubscription\n ]\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yEAAgE;AAChE,6EAAoE;AACpE,4FAAmF;AACnF,yFAAgF;AAChF,sGAA+F;AAC/F,8FAAqF;AACrF,sDAAyE;AACzE,sDAAyE;AACzE,kEAAgG;AAEhG,qDAA0B;AAC1B,qEAA0C;AAC1C,uEAA4C;AAC5C,oGAAoG;AACpG,oEAAyC;AACzC,6EAAkD;AAClD;;;GAGG;AACH,2DAAgC;AAChC,gDAAgD;AAChD,iEAAsC;AACtC,sDAAsD;AACtD,2EAAgD;AAChD,gHAAgH;AAChH,+EAAoD;AACpD,kEAAuC;AACvC,8DAA8D;AAC9D,0EAA+C;AAC/C,qFAA0D;AAC1D,uFAA4D;AAC5D,wEAA6C;AAC7C,4EAAiD;AACjD,yEAA8C;AAC9C,wDAAwD;AACxD,iFAAsD;AACtD,iFAAsD;AACtD,mFAAwD;AACxD,4FAAiE;AACjE,2DAA2D;AAC3D,uFAA4D;AAC5D,iFAAsD;AACtD,kEAAuC;AACvC,8EAAmD;AAEtC,QAAA,QAAQ,GAAG,CAAC,GAAG,mBAAmB,EAAE,GAAG,mBAAmB,EAAE,GAAG,mBAA8B,CAAC,CAAA;AAE9F,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,8CAAmB;QACnB,sDAAsD;QACtD,kDAAqB;QACrB,uDAAuD;QACvD,kDAAqB;QACrB,wDAAwD;QACxD,8DAA2B;QAC3B,sDAAuB;QACvB,qDAAqD;QACrD,gEAA6B;KAC9B;CACF,CAAA","sourcesContent":["import { ChatSessionResolver } from './chat-session-resolver.js'\nimport { AssistantChatResolver } from './assistant-chat-resolver.js'\nimport { AssistantModeResolver } from './assistant-mode/assistant-mode-resolver.js'\nimport { AssistantCapabilityResolver } from './assistant-capability-resolver.js'\nimport { MySessionActivitySubscription } from './chat-session/session-activity-subscription.js'\nimport { ChatMessageSubscription } from './chat-message/chat-message-subscription.js'\nimport { entities as ChatSessionEntities } from './chat-session/index.js'\nimport { entities as ChatMessageEntities } from './chat-message/index.js'\nimport { entities as ChatSessionParticipantEntities } from './chat-session-participant/index.js'\n\nexport * from './types.js'\nexport * from './chat-session-resolver.js'\nexport * from './assistant-chat-resolver.js'\n/* The mode registry — each app server declares what its conversations are (ADR-0061 decision 7). */\nexport * from './assistant-mode/index.js'\nexport * from './assistant-capability-resolver.js'\n/*\n * 대화 각본 하네스는 **런타임 export 에서 뺀다**(ADR-0070 A15 결정 ④) — 시험 도구가 서버 번들에\n * 실린다. 시험은 경로로 가져온다: `…/service/testing/conversation-script.js`.\n */\nexport * from './tool-access.js'\n/* 무엇이 근거인가 — 도구 결과 밖의 셋(프롬프트 · 대화 · 화면 스냅숏). */\nexport * from './grounding-sources.js'\n/* 앵커 순수 로직 — 호스트가 앵커 종류를 참조한다(리졸버는 데코레이터 때문에 무겁다). */\nexport * from './chat-session/session-anchor.js'\n/* Who may read a conversation — one rule, also asked by doors outside this package (board-ai's revertPatch). */\nexport * from './chat-session/session-visibility.js'\nexport * from './chat-session/index.js'\n/* 순수 로직들 — 호스트(board-ai 등)가 같은 규칙을 쓰라고 공개한다. 베끼면 한쪽만 고쳐진다. */\nexport * from './chat-session/session-inbox.js'\nexport * from './chat-session/session-activity-publish.js'\nexport * from './chat-session/session-activity-broadcast.js'\nexport * from './chat-message/llm-history.js'\nexport * from './chat-message/history-summary.js'\nexport * from './chat-message/fold-history.js'\n/* 상한 밖으로 밀려난 앞부분을 요약으로 접는 orchestration — 중립 문이 부른다. */\nexport * from './chat-message/fold-session-history.js'\nexport * from './chat-message/chat-message-publish.js'\nexport * from './chat-message/chat-message-broadcast.js'\nexport * from './chat-session-participant/invite-notification.js'\n/* 멘션이 사람을 불러들이는 자리 — 사람의 말을 저장하는 문만 부른다(ADR-0071 결정 1). */\nexport * from './chat-session-participant/mention-invite.js'\nexport * from './chat-session-participant/presence.js'\nexport * from './chat-message/index.js'\nexport * from './chat-session-participant/index.js'\n\nexport const entities = [...ChatSessionEntities, ...ChatMessageEntities, ...ChatSessionParticipantEntities]\n\nexport const schema = {\n resolverClasses: [\n ChatSessionResolver,\n /* 문서를 갖지 않은 표면의 대화 — board 를 안 싣는 제품이 물어볼 수 있는 자리. */\n AssistantChatResolver,\n /* 화면이 자기 대화가 무엇을 주는지 묻는 자리 — 문구는 서버가 요청 로케일로 옮겨 낸다. */\n AssistantModeResolver,\n /* 「이 배포에 AI 가 있나 · 무엇이 선언 없이 등록됐나」 — 창구 하나(표준 A7-a). */\n AssistantCapabilityResolver,\n ChatMessageSubscription,\n /* 내가 참여한 모든 세션의 활동 — 창을 닫아 둔 표면이 배지·목록을 갱신할 수 있게. */\n MySessionActivitySubscription\n ]\n}\n"]}