@things-factory/board-ai 10.1.28 → 10.1.29
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-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/board-session-resolver.js +20 -2
- package/dist-server/service/board-session-resolver.js.map +1 -1
- package/dist-server/service/patch-entry/revert-permission.d.ts +5 -0
- package/dist-server/service/patch-entry/revert-permission.js +9 -0
- package/dist-server/service/patch-entry/revert-permission.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/server/service/board-session-resolver.ts +24 -2
- package/server/service/patch-entry/revert-permission.ts +28 -0
- package/test/revert-permission.test.ts +41 -0
- package/test/fold-history-window.test.ts +0 -158
|
@@ -32,7 +32,10 @@ const ai_assistant_3 = require("@things-factory/ai-assistant");
|
|
|
32
32
|
const ai_assistant_4 = require("@things-factory/ai-assistant");
|
|
33
33
|
const ai_assistant_5 = require("@things-factory/ai-assistant");
|
|
34
34
|
const ai_assistant_6 = require("@things-factory/ai-assistant");
|
|
35
|
+
const ai_assistant_7 = require("@things-factory/ai-assistant");
|
|
36
|
+
const ai_assistant_8 = require("@things-factory/ai-assistant");
|
|
35
37
|
const patch_entry_js_1 = require("./patch-entry/patch-entry.js");
|
|
38
|
+
const revert_permission_js_1 = require("./patch-entry/revert-permission.js");
|
|
36
39
|
const board_patch_subscription_js_1 = require("./patch-entry/board-patch-subscription.js");
|
|
37
40
|
/** 중립 op 로의 위임 대상. 리졸버는 상태가 없으므로 하나면 된다. */
|
|
38
41
|
const sessions = new ai_assistant_6.ChatSessionResolver();
|
|
@@ -116,8 +119,23 @@ let BoardSessionResolver = class BoardSessionResolver {
|
|
|
116
119
|
return entry;
|
|
117
120
|
}
|
|
118
121
|
async revertPatch(patchId, context) {
|
|
119
|
-
const { tx } = context.state;
|
|
120
|
-
|
|
122
|
+
const { domain, user, tx } = context.state;
|
|
123
|
+
/* A patch id alone is not permission — see `patch-entry/revert-permission.ts`. */
|
|
124
|
+
const patch = await (0, shell_1.getRepository)(patch_entry_js_1.PatchEntry, tx).findOne({
|
|
125
|
+
where: { id: patchId, session: { domain: { id: domain.id } } },
|
|
126
|
+
relations: ['session']
|
|
127
|
+
});
|
|
128
|
+
const session = patch?.session;
|
|
129
|
+
const isParticipant = !!session && !!user?.id
|
|
130
|
+
? !!(await (0, shell_1.getRepository)(ai_assistant_7.ChatSessionParticipant, tx).findOneBy({
|
|
131
|
+
session: { id: session.id },
|
|
132
|
+
user: { id: user.id }
|
|
133
|
+
}))
|
|
134
|
+
: false;
|
|
135
|
+
if (!(0, revert_permission_js_1.mayRevertPatch)(patch ? { sessionVisibility: session?.visibility } : null, isParticipant, ai_assistant_8.mayReadSession)) {
|
|
136
|
+
throw new Error(`PatchEntry ${patchId} not found`);
|
|
137
|
+
}
|
|
138
|
+
await (0, shell_1.getRepository)(patch_entry_js_1.PatchEntry, tx).update(patch.id, { reverted: true });
|
|
121
139
|
return true;
|
|
122
140
|
}
|
|
123
141
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"board-session-resolver.js","sourceRoot":"","sources":["../../server/service/board-session-resolver.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,+CAAkF;AAClF,qDAA6C;AAE7C,iDAAqD;AAErD,+DAA0D;AAC1D,+DAA4D;AAC5D,+DAAqE;AACrE,+DAA0D;AAC1D,+DAAiE;AACjE,+DAAkE;AAClE,iEAAyD;AACzD,2FAA6E;AAE7E,4CAA4C;AAC5C,MAAM,QAAQ,GAAG,IAAI,kCAAmB,EAAE,CAAA;AAGnC,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAGzB,AAAN,KAAK,CAAC,kBAAkB,CACN,OAAe,EACxB,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAChC,OAAO,CACL,CAAC,MAAM,IAAA,qBAAa,EAAC,0BAAW,CAAC,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAC7F,CAAA;IACH,CAAC;IAED,4CAA4C;IAKtC,AAAN,KAAK,CAAC,mBAAmB,CACP,OAAe,EACxB,OAAwB;QAE/B,OAAO,MAAM,QAAQ,CAAC,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACvE,CAAC;IAaK,AAAN,KAAK,CAAC,WAAW,CACG,SAAiB,EAC6B,KAAa,EACtE,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAChC,MAAM,OAAO,GAAG,MAAM,IAAA,qBAAa,EAAC,0BAAW,CAAC,CAAC,SAAS,CAAC;YACzD,EAAE,EAAE,SAAS;YACb,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;SAC1B,CAAC,CAAA;QACF,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAA;QACvB,OAAO,MAAM,IAAA,qBAAa,EAAC,2BAAU,CAAC,CAAC,IAAI,CAAC;YAC1C,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,SAAS,EAAS,EAAE;YAC5C,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;YAC5B,IAAI,EAAE,KAAK;SACZ,CAAC,CAAA;IACJ,CAAC;IAED,4CAA4C;IAKtC,AAAN,KAAK,CAAC,mBAAmB,CACP,OAAe,EACxB,OAAwB;QAE/B,OAAO,MAAM,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACjE,CAAC;IAOK,AAAN,KAAK,CAAC,iBAAiB,CACH,SAAiB,EACJ,GAAU,EACL,OAA2B,EACxD,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAC1C,MAAM,OAAO,GAAG,MAAM,IAAA,qBAAa,EAAC,0BAAW,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC;YAC7D,EAAE,EAAE,SAAS;YACb,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;SAC1B,CAAC,CAAA;QACF,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,eAAe,SAAS,YAAY,CAAC,CAAA;QAEnE,MAAM,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,2BAAU,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC;YACrD,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAS;YAClC,MAAM,EAAE,aAAa;YACrB,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,CAAC;YAC9B,OAAO;YACP,QAAQ,EAAE,KAAK;SACT,CAAC,CAAA;QAET,uDAAuD;QACvD,6EAA6E;QAC7E,MAAM,YAAY,GAAG,MAAM,IAAA,qBAAa,EAAC,0BAAW,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;YAChE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAS,EAAE;YAC7C,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;SAC7B,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,IAAA,qBAAa,EAAC,0BAAW,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC;YACvD,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAS;YAClC,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,mCAAmC,OAAO,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,MAAM,QAAQ,EAAE;YACtF,cAAc,EAAE,KAAK,CAAC,EAAE;YACxB,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,IAAI;YACb,aAAa,EAAE,YAAY,CAAC,CAAC,CAAE,EAAE,EAAE,EAAE,YAAY,CAAC,EAAE,EAAU,CAAC,CAAC,CAAC,SAAS;SACpE,CAAC,CAAA;QAET,wDAAwD;QACxD,IAAI,CAAC;YACH,MAAM,IAAA,qBAAa,EAAC,0BAAW,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAG,EAAE,IAAA,4BAAa,EAAC,MAAM,CAAC,OAAO,CAAQ,CAAC,CAAA;QAChG,CAAC;QAAC,MAAM,CAAC;YACP,iCAAiC;QACnC,CAAC;QACD,IAAA,qCAAsB,EAAC;YACrB,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,SAAS,EAAE,OAAO,CAAC,EAAG;YACtB,IAAI,EAAE,SAAS;YACf,UAAU,EAAG,OAAe,CAAC,UAAU;YACvC,QAAQ,EAAG,OAAe,CAAC,QAAQ;YACnC,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,OAAO,EAAE,IAAI,EAAE,EAAE;SAClB,CAAC,CAAA;QAEF,uEAAuE;QACvE,IAAA,+CAAiB,EAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QAClF,yEAAyE;QACzE,IAAA,iCAAkB,EAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;QAEnF,OAAO,KAAK,CAAA;IACd,CAAC;IAKK,AAAN,KAAK,CAAC,WAAW,CACC,OAAe,EACxB,OAAwB;QAE/B,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAC5B,MAAM,IAAA,qBAAa,EAAC,2BAAU,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;QACvE,OAAO,IAAI,CAAA;IACb,CAAC;CACF,CAAA;AA/IY,oDAAoB;AAGzB;IAFL,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,0BAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gGAAgG,EAAE,CAAC;IAC3J,IAAA,wBAAS,EAAC,0DAA0D,CAAC;IAEnE,mBAAA,IAAA,kBAAG,EAAC,SAAS,CAAC,CAAA;IACd,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;8DAMP;AAOK;IAJL,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,CAAC,0BAAW,CAAC,EAAE;QAC1B,WAAW,EAAE,sGAAsG;KACpH,CAAC;IACD,IAAA,wBAAS,EAAC,0DAA0D,CAAC;IAEnE,mBAAA,IAAA,kBAAG,EAAC,SAAS,CAAC,CAAA;IACd,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;+DAGP;AAaK;IAXL,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,CAAC,2BAAU,CAAC,EAAE,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;IAC7F;;;;;;;;OAQG;;IACF,IAAA,wBAAS,EAAC,sDAAsD,CAAC;IAE/D,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;IAChB,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAA;IAC9D,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;uDAaP;AAOK;IAJL,IAAA,uBAAQ,EAAC,GAAG,EAAE,CAAC,0BAAW,EAAE;QAC3B,WAAW,EAAE,+FAA+F;KAC7G,CAAC;IACD,IAAA,wBAAS,EAAC,6DAA6D,CAAC;IAEtE,mBAAA,IAAA,kBAAG,EAAC,SAAS,CAAC,CAAA;IACd,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;+DAGP;AAOK;IALL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,uBAAQ,EAAC,GAAG,EAAE,CAAC,2BAAU,EAAE;QAC1B,WAAW,EAAE,8FAA8F;KAC5G,CAAC;IACD,IAAA,wBAAS,EAAC,yDAAyD,CAAC;IAElE,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;IAChB,mBAAA,IAAA,kBAAG,EAAC,KAAK,EAAE,GAAG,EAAE,CAAC,6BAAW,CAAC,CAAA;IAC7B,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAClC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;6DAuDP;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,uBAAQ,EAAC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,uDAAuD,EAAE,CAAC;IACjG,IAAA,wBAAS,EAAC,yDAAyD,CAAC;IAElE,mBAAA,IAAA,kBAAG,EAAC,SAAS,CAAC,CAAA;IACd,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;uDAKP;+BA9IU,oBAAoB;IADhC,IAAA,uBAAQ,GAAE;GACE,oBAAoB,CA+IhC","sourcesContent":["/*\n * 보드에 매인 대화 op — **보드 편집기의 것이라 여기 남는다.**\n *\n * ── 왜 갈랐나 (2026-09-10) ──────────────────────────────────────────────────\n * `chat-session-resolver.ts` 에 스물넷이 함께 있었다. 그 중 열여덟은 대화 자체의 일(세션·참가자·\n * 인박스·메시지)이라 보드가 없는 제품에도 뜻이 있고, 여섯은 보드의 일이다. 한 파일에 있는 동안은\n * 앞엣것을 중립 패키지로 옮길 수 없다 — 옮기면 `ai-assistant → board-ai` 의존이 생긴다.\n *\n * 그래서 **여섯을 이쪽으로 뗐다.** 남은 열여덟이 옮겨질 수 있는 상태가 되는 것이 이 갈이의 목적이다.\n *\n * ── 왜 여섯인가 ─────────────────────────────────────────────────────────────\n * chatSessionByBoard · chatSessionsByBoard 보드 id 로 찾는다 — 보드를 아는 질의\n * startBoardAISession 보드 앵커로 시작하는 얇은 래퍼\n * chatPatches · recordDirectPatch · revertPatch PatchEntry — 보드 편집 이력\n *\n * ── 위임을 `this` 로 하지 않는다 ────────────────────────────────────────────\n * 앞의 셋은 원래 `this.startAISession(...)` 처럼 같은 클래스의 중립 메서드를 불렀다. 이제 클래스가\n * 둘이므로 중립 리졸버 인스턴스에 위임한다. 리졸버는 상태가 없다(전부 `context.state` 에서 읽는다).\n *\n * **로직을 베끼지 않는다** — 같은 글이 두 곳에 있으면 한쪽만 고쳐진다.\n */\nimport { Resolver, Query, Mutation, Arg, Ctx, Directive, Int } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from '@things-factory/ai-assistant'\nimport { activityPatch } from '@things-factory/ai-assistant'\nimport { publishSessionActivity } from '@things-factory/ai-assistant'\nimport { ChatMessage } from '@things-factory/ai-assistant'\nimport { publishChatMessage } from '@things-factory/ai-assistant'\nimport { ChatSessionResolver } from '@things-factory/ai-assistant'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\n\n/** 중립 op 로의 위임 대상. 리졸버는 상태가 없으므로 하나면 된다. */\nconst sessions = new ChatSessionResolver()\n\n@Resolver()\nexport class BoardSessionResolver {\n @Query(() => ChatSession, { nullable: true, description: 'Get AI chat session by board id (returns first match — backward compat single-session lookup).' })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession | null> {\n const { domain } = context.state\n return (\n (await getRepository(ChatSession).findOneBy({ boardId, domain: { id: domain.id } })) ?? null\n )\n }\n\n /** 보드 전용 진입점 — 앵커 조회의 얇은 래퍼(기존 소비자 무변경). */\n @Query(() => [ChatSession], {\n description: 'List all AI chat sessions for a board (oldest first). Thin wrapper over chatSessionsByAnchor(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionsByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession[]> {\n return await sessions.chatSessionsByAnchor('board', boardId, context)\n }\n\n @Query(() => [PatchEntry], { description: 'List patch entries of a session, newest first.' })\n /*\n * ⚠ 이 셋은 **보드 편집기의 것**이라 `board-ai` 에 남는다 (2026-09-10).\n *\n * 나머지 스물둘을 `ai-assistant` 로 옮길 때 이것들도 함께 갔다가, 도는 스키마를 확인하면서\n * 되돌렸다 — `revertPatch` 가 `ai-assistant:mutation` 으로 떠 있었다. 즉 **대화 권한만 가진\n * 사람이 보드 패치를 되돌릴 수 있었다.** 좁히려던 변경이 넓히는 변경이 된 자리다.\n *\n * 가르는 물음: **이것이 보드를 바꾸나.** 패치는 바꾼다. 세션 조회는 안 바꾼다.\n */\n @Directive('@privilege(category: \"board-ai\", privilege: \"query\")')\n async chatPatches(\n @Arg('sessionId') sessionId: string,\n @Arg('limit', () => Int, { nullable: true, defaultValue: 100 }) limit: number,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry[]> {\n const { domain } = context.state\n const session = await getRepository(ChatSession).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) return []\n return await getRepository(PatchEntry).find({\n where: { session: { id: sessionId } as any },\n order: { createdAt: 'DESC' },\n take: limit\n })\n }\n\n /** 보드 전용 진입점 — 앵커 시작의 얇은 래퍼(기존 소비자 무변경). */\n @Mutation(() => ChatSession, {\n description: 'Start (or get existing) AI chat session for a board. Thin wrapper over startAISession(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\")')\n async startBoardAISession(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession> {\n return await sessions.startAISession('board', boardId, context)\n }\n\n @Directive('@transaction')\n @Mutation(() => PatchEntry, {\n description: 'Record a patch from user direct edit. Adds a system message so AI sees the change next turn.'\n })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async recordDirectPatch(\n @Arg('sessionId') sessionId: string,\n @Arg('ops', () => GraphQLJSON) ops: any[],\n @Arg('summary', { nullable: true }) summary: string | undefined,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry> {\n const { domain, tx, user } = context.state\n const session = await getRepository(ChatSession, tx).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) throw new Error(`ChatSession ${sessionId} not found`)\n\n const entry = await getRepository(PatchEntry, tx).save({\n session: { id: session.id } as any,\n source: 'user-direct',\n ops: JSON.stringify(ops || []),\n summary,\n reverted: false\n } as any)\n\n // AI 컨텍스트 흐름 — system 메시지 자동 추가. creator = 직접 편집한 사용자.\n // parentMessage = 직전 in-session 메시지 (timeline 연속성 유지 — system 도 chain 에 합류).\n const previousLast = await getRepository(ChatMessage, tx).findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const sysMsg = await getRepository(ChatMessage, tx).save({\n session: { id: session.id } as any,\n role: 'system',\n content: `User directly edited the board: ${summary ?? `${(ops || []).length} op(s)`}`,\n relatedPatchId: entry.id,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n } as any)\n\n /* 세션 활동 갱신 — 직접 편집도 다른 참여자에게는 새 소식이다(목록 정렬·안 읽음 판정). */\n try {\n await getRepository(ChatSession, tx).update(session.id!, activityPatch(sysMsg.content) as any)\n } catch {\n /* best-effort — 편집 기록을 막지 않는다 */\n }\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: sysMsg.content,\n actorId: user?.id\n })\n\n // C1: user-direct 편집도 같은 board 구독자에게 방송 (AI·타참여자 실시간 반영). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: entry })\n // C4: 직접편집 system 메시지를 같은 세션 구독자에게 방송 (transcript 실시간 일관성). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: sysMsg })\n\n return entry\n }\n\n @Directive('@transaction')\n @Mutation(() => Boolean, { description: 'Mark a patch as reverted (does not undo, only flags).' })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async revertPatch(\n @Arg('patchId') patchId: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n const { tx } = context.state\n await getRepository(PatchEntry, tx).update(patchId, { reverted: true })\n return true\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"board-session-resolver.js","sourceRoot":"","sources":["../../server/service/board-session-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 보드에 매인 대화 op — **보드 편집기의 것이라 여기 남는다.**\n *\n * ── 왜 갈랐나 (2026-09-10) ──────────────────────────────────────────────────\n * `chat-session-resolver.ts` 에 스물넷이 함께 있었다. 그 중 열여덟은 대화 자체의 일(세션·참가자·\n * 인박스·메시지)이라 보드가 없는 제품에도 뜻이 있고, 여섯은 보드의 일이다. 한 파일에 있는 동안은\n * 앞엣것을 중립 패키지로 옮길 수 없다 — 옮기면 `ai-assistant → board-ai` 의존이 생긴다.\n *\n * 그래서 **여섯을 이쪽으로 뗐다.** 남은 열여덟이 옮겨질 수 있는 상태가 되는 것이 이 갈이의 목적이다.\n *\n * ── 왜 여섯인가 ─────────────────────────────────────────────────────────────\n * chatSessionByBoard · chatSessionsByBoard 보드 id 로 찾는다 — 보드를 아는 질의\n * startBoardAISession 보드 앵커로 시작하는 얇은 래퍼\n * chatPatches · recordDirectPatch · revertPatch PatchEntry — 보드 편집 이력\n *\n * ── 위임을 `this` 로 하지 않는다 ────────────────────────────────────────────\n * 앞의 셋은 원래 `this.startAISession(...)` 처럼 같은 클래스의 중립 메서드를 불렀다. 이제 클래스가\n * 둘이므로 중립 리졸버 인스턴스에 위임한다. 리졸버는 상태가 없다(전부 `context.state` 에서 읽는다).\n *\n * **로직을 베끼지 않는다** — 같은 글이 두 곳에 있으면 한쪽만 고쳐진다.\n */\nimport { Resolver, Query, Mutation, Arg, Ctx, Directive, Int } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from '@things-factory/ai-assistant'\nimport { activityPatch } from '@things-factory/ai-assistant'\nimport { publishSessionActivity } from '@things-factory/ai-assistant'\nimport { ChatMessage } from '@things-factory/ai-assistant'\nimport { publishChatMessage } from '@things-factory/ai-assistant'\nimport { ChatSessionResolver } from '@things-factory/ai-assistant'\nimport { ChatSessionParticipant } from '@things-factory/ai-assistant'\nimport { mayReadSession } from '@things-factory/ai-assistant'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { mayRevertPatch } from './patch-entry/revert-permission.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\n\n/** 중립 op 로의 위임 대상. 리졸버는 상태가 없으므로 하나면 된다. */\nconst sessions = new ChatSessionResolver()\n\n@Resolver()\nexport class BoardSessionResolver {\n @Query(() => ChatSession, { nullable: true, description: 'Get AI chat session by board id (returns first match — backward compat single-session lookup).' })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession | null> {\n const { domain } = context.state\n return (\n (await getRepository(ChatSession).findOneBy({ boardId, domain: { id: domain.id } })) ?? null\n )\n }\n\n /** 보드 전용 진입점 — 앵커 조회의 얇은 래퍼(기존 소비자 무변경). */\n @Query(() => [ChatSession], {\n description: 'List all AI chat sessions for a board (oldest first). Thin wrapper over chatSessionsByAnchor(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionsByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession[]> {\n return await sessions.chatSessionsByAnchor('board', boardId, context)\n }\n\n @Query(() => [PatchEntry], { description: 'List patch entries of a session, newest first.' })\n /*\n * ⚠ 이 셋은 **보드 편집기의 것**이라 `board-ai` 에 남는다 (2026-09-10).\n *\n * 나머지 스물둘을 `ai-assistant` 로 옮길 때 이것들도 함께 갔다가, 도는 스키마를 확인하면서\n * 되돌렸다 — `revertPatch` 가 `ai-assistant:mutation` 으로 떠 있었다. 즉 **대화 권한만 가진\n * 사람이 보드 패치를 되돌릴 수 있었다.** 좁히려던 변경이 넓히는 변경이 된 자리다.\n *\n * 가르는 물음: **이것이 보드를 바꾸나.** 패치는 바꾼다. 세션 조회는 안 바꾼다.\n */\n @Directive('@privilege(category: \"board-ai\", privilege: \"query\")')\n async chatPatches(\n @Arg('sessionId') sessionId: string,\n @Arg('limit', () => Int, { nullable: true, defaultValue: 100 }) limit: number,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry[]> {\n const { domain } = context.state\n const session = await getRepository(ChatSession).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) return []\n return await getRepository(PatchEntry).find({\n where: { session: { id: sessionId } as any },\n order: { createdAt: 'DESC' },\n take: limit\n })\n }\n\n /** 보드 전용 진입점 — 앵커 시작의 얇은 래퍼(기존 소비자 무변경). */\n @Mutation(() => ChatSession, {\n description: 'Start (or get existing) AI chat session for a board. Thin wrapper over startAISession(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\")')\n async startBoardAISession(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession> {\n return await sessions.startAISession('board', boardId, context)\n }\n\n @Directive('@transaction')\n @Mutation(() => PatchEntry, {\n description: 'Record a patch from user direct edit. Adds a system message so AI sees the change next turn.'\n })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async recordDirectPatch(\n @Arg('sessionId') sessionId: string,\n @Arg('ops', () => GraphQLJSON) ops: any[],\n @Arg('summary', { nullable: true }) summary: string | undefined,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry> {\n const { domain, tx, user } = context.state\n const session = await getRepository(ChatSession, tx).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) throw new Error(`ChatSession ${sessionId} not found`)\n\n const entry = await getRepository(PatchEntry, tx).save({\n session: { id: session.id } as any,\n source: 'user-direct',\n ops: JSON.stringify(ops || []),\n summary,\n reverted: false\n } as any)\n\n // AI 컨텍스트 흐름 — system 메시지 자동 추가. creator = 직접 편집한 사용자.\n // parentMessage = 직전 in-session 메시지 (timeline 연속성 유지 — system 도 chain 에 합류).\n const previousLast = await getRepository(ChatMessage, tx).findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const sysMsg = await getRepository(ChatMessage, tx).save({\n session: { id: session.id } as any,\n role: 'system',\n content: `User directly edited the board: ${summary ?? `${(ops || []).length} op(s)`}`,\n relatedPatchId: entry.id,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n } as any)\n\n /* 세션 활동 갱신 — 직접 편집도 다른 참여자에게는 새 소식이다(목록 정렬·안 읽음 판정). */\n try {\n await getRepository(ChatSession, tx).update(session.id!, activityPatch(sysMsg.content) as any)\n } catch {\n /* best-effort — 편집 기록을 막지 않는다 */\n }\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: sysMsg.content,\n actorId: user?.id\n })\n\n // C1: user-direct 편집도 같은 board 구독자에게 방송 (AI·타참여자 실시간 반영). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: entry })\n // C4: 직접편집 system 메시지를 같은 세션 구독자에게 방송 (transcript 실시간 일관성). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: sysMsg })\n\n return entry\n }\n\n @Directive('@transaction')\n @Mutation(() => Boolean, { description: 'Mark a patch as reverted (does not undo, only flags).' })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async revertPatch(\n @Arg('patchId') patchId: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n const { domain, user, tx } = context.state\n\n /* A patch id alone is not permission — see `patch-entry/revert-permission.ts`. */\n const patch = await getRepository(PatchEntry, tx).findOne({\n where: { id: patchId, session: { domain: { id: domain.id } } } as any,\n relations: ['session']\n })\n const session: any = patch?.session\n const isParticipant =\n !!session && !!user?.id\n ? !!(await getRepository(ChatSessionParticipant, tx).findOneBy({\n session: { id: session.id },\n user: { id: user.id }\n } as any))\n : false\n\n if (!mayRevertPatch(patch ? { sessionVisibility: session?.visibility } : null, isParticipant, mayReadSession)) {\n throw new Error(`PatchEntry ${patchId} not found`)\n }\n\n await getRepository(PatchEntry, tx).update(patch!.id!, { reverted: true })\n return true\n }\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface FoundPatch {
|
|
2
|
+
/** `ChatSession.visibility` of the session the patch belongs to. */
|
|
3
|
+
sessionVisibility?: string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function mayRevertPatch(found: FoundPatch | null | undefined, isParticipant: boolean, mayReadSession: (visibility: string | null | undefined, isParticipant: boolean) => boolean): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mayRevertPatch = mayRevertPatch;
|
|
4
|
+
function mayRevertPatch(found, isParticipant, mayReadSession) {
|
|
5
|
+
if (!found)
|
|
6
|
+
return false;
|
|
7
|
+
return mayReadSession(found.sessionVisibility, isParticipant);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=revert-permission.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revert-permission.js","sourceRoot":"","sources":["../../../server/service/patch-entry/revert-permission.ts"],"names":[],"mappings":";;AAoBA,wCAOC;AAPD,SAAgB,cAAc,CAC5B,KAAoC,EACpC,aAAsB,EACtB,cAA0F;IAE1F,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxB,OAAO,cAAc,CAAC,KAAK,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["/*\n * Who may flag a patch as reverted.\n *\n * `revertPatch` used to take a patch id and write — the privilege gate was the only check, so anyone\n * holding `board-ai:mutation` in ANY tenant could flag a patch in another tenant's conversation by\n * knowing (or guessing) its id, and a person could reach into a private conversation they are not\n * part of (operato-twin ADR-0061 \"what was left\", found again by the 2026-09-18 ADR audit).\n *\n * The rule is the conversation's own: a patch belongs to a session, and a person may act on it only\n * where they may read that session — the caller's tenant, and for a private session, a participant\n * (ADR-0073 decision 2). The lookup is scoped by tenant, so a patch elsewhere simply is not found.\n *\n * One answer for every refusal. Telling \"exists but not yours\" apart from \"does not exist\" would\n * confirm an id to someone who should not learn it.\n */\nexport interface FoundPatch {\n /** `ChatSession.visibility` of the session the patch belongs to. */\n sessionVisibility?: string | null\n}\n\nexport function mayRevertPatch(\n found: FoundPatch | null | undefined,\n isParticipant: boolean,\n mayReadSession: (visibility: string | null | undefined, isParticipant: boolean) => boolean\n): boolean {\n if (!found) return false\n return mayReadSession(found.sessionVisibility, isParticipant)\n}\n"]}
|