@things-factory/ai-assistant 10.1.46 → 10.1.50

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 (174) hide show
  1. package/client/components/assistant-chat.ts +100 -20
  2. package/client/components/assistant-dock.ts +617 -0
  3. package/client/components/assistant-mode.ts +29 -0
  4. package/client/components/assistant-session-manager.ts +607 -0
  5. package/client/components/chat-echo-dedup.ts +4 -0
  6. package/client/components/chat-input-builder.ts +12 -2
  7. package/client/components/panel-aria.test.ts +13 -8
  8. package/client/index.ts +10 -0
  9. package/client/setup-assistant-dock.ts +109 -0
  10. package/client/utils/assistant-activity.ts +122 -0
  11. package/client/utils/assistant-ask.ts +44 -0
  12. package/client/utils/assistant-capability-read.ts +24 -0
  13. package/client/utils/assistant-capability.test.ts +17 -0
  14. package/client/utils/assistant-capability.ts +19 -0
  15. package/client/utils/assistant-proposals.ts +50 -0
  16. package/client/utils/assistant-session-actions.ts +18 -0
  17. package/client/utils/assistant-session-admin.ts +167 -0
  18. package/client/utils/assistant-session-controller.ts +2 -0
  19. package/client/utils/assistant-session-transport.ts +6 -2
  20. package/client/utils/assistant-surface.ts +157 -0
  21. package/client/utils/attachment-policy.ts +71 -0
  22. package/dist-client/components/assistant-chat.d.ts +6 -0
  23. package/dist-client/components/assistant-chat.js +101 -21
  24. package/dist-client/components/assistant-chat.js.map +1 -1
  25. package/dist-client/components/assistant-dock.d.ts +74 -0
  26. package/dist-client/components/assistant-dock.js +641 -0
  27. package/dist-client/components/assistant-dock.js.map +1 -0
  28. package/dist-client/components/assistant-mode.d.ts +6 -0
  29. package/dist-client/components/assistant-mode.js +24 -0
  30. package/dist-client/components/assistant-mode.js.map +1 -1
  31. package/dist-client/components/assistant-session-manager.d.ts +77 -0
  32. package/dist-client/components/assistant-session-manager.js +652 -0
  33. package/dist-client/components/assistant-session-manager.js.map +1 -0
  34. package/dist-client/components/chat-echo-dedup.js +4 -0
  35. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  36. package/dist-client/components/chat-input-builder.d.ts +11 -0
  37. package/dist-client/components/chat-input-builder.js +4 -2
  38. package/dist-client/components/chat-input-builder.js.map +1 -1
  39. package/dist-client/components/panel-aria.test.js +12 -8
  40. package/dist-client/components/panel-aria.test.js.map +1 -1
  41. package/dist-client/index.d.ts +10 -0
  42. package/dist-client/index.js +10 -0
  43. package/dist-client/index.js.map +1 -1
  44. package/dist-client/setup-assistant-dock.d.ts +15 -0
  45. package/dist-client/setup-assistant-dock.js +110 -0
  46. package/dist-client/setup-assistant-dock.js.map +1 -0
  47. package/dist-client/tsconfig.tsbuildinfo +1 -1
  48. package/dist-client/utils/assistant-activity.d.ts +22 -0
  49. package/dist-client/utils/assistant-activity.js +104 -0
  50. package/dist-client/utils/assistant-activity.js.map +1 -0
  51. package/dist-client/utils/assistant-ask.d.ts +23 -0
  52. package/dist-client/utils/assistant-ask.js +18 -0
  53. package/dist-client/utils/assistant-ask.js.map +1 -0
  54. package/dist-client/utils/assistant-capability-read.d.ts +6 -0
  55. package/dist-client/utils/assistant-capability-read.js +23 -0
  56. package/dist-client/utils/assistant-capability-read.js.map +1 -0
  57. package/dist-client/utils/assistant-capability.d.ts +4 -0
  58. package/dist-client/utils/assistant-capability.js +17 -0
  59. package/dist-client/utils/assistant-capability.js.map +1 -0
  60. package/dist-client/utils/assistant-capability.test.d.ts +1 -0
  61. package/dist-client/utils/assistant-capability.test.js +16 -0
  62. package/dist-client/utils/assistant-capability.test.js.map +1 -0
  63. package/dist-client/utils/assistant-proposals.d.ts +29 -0
  64. package/dist-client/utils/assistant-proposals.js +22 -0
  65. package/dist-client/utils/assistant-proposals.js.map +1 -0
  66. package/dist-client/utils/assistant-session-actions.d.ts +12 -0
  67. package/dist-client/utils/assistant-session-actions.js +19 -0
  68. package/dist-client/utils/assistant-session-actions.js.map +1 -0
  69. package/dist-client/utils/assistant-session-admin.d.ts +72 -0
  70. package/dist-client/utils/assistant-session-admin.js +81 -0
  71. package/dist-client/utils/assistant-session-admin.js.map +1 -0
  72. package/dist-client/utils/assistant-session-controller.d.ts +2 -0
  73. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  74. package/dist-client/utils/assistant-session-transport.js +6 -2
  75. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  76. package/dist-client/utils/assistant-surface.d.ts +72 -0
  77. package/dist-client/utils/assistant-surface.js +99 -0
  78. package/dist-client/utils/assistant-surface.js.map +1 -0
  79. package/dist-client/utils/attachment-policy.d.ts +35 -0
  80. package/dist-client/utils/attachment-policy.js +56 -0
  81. package/dist-client/utils/attachment-policy.js.map +1 -0
  82. package/dist-server/service/assistant-chat-resolver.d.ts +1 -0
  83. package/dist-server/service/assistant-chat-resolver.js +72 -4
  84. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  85. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +1 -0
  86. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +7 -0
  87. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
  88. package/dist-server/service/assistant-mode/resolve.d.ts +3 -0
  89. package/dist-server/service/assistant-mode/resolve.js +6 -2
  90. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  91. package/dist-server/service/assistant-mode/types.d.ts +6 -0
  92. package/dist-server/service/assistant-mode/types.js +0 -9
  93. package/dist-server/service/assistant-mode/types.js.map +1 -1
  94. package/dist-server/service/chat-message/chat-message.d.ts +6 -0
  95. package/dist-server/service/chat-message/chat-message.js +19 -0
  96. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  97. package/dist-server/service/chat-session/attachment-policy.d.ts +44 -0
  98. package/dist-server/service/chat-session/attachment-policy.js +56 -0
  99. package/dist-server/service/chat-session/attachment-policy.js.map +1 -0
  100. package/dist-server/service/chat-session/attachment-route.d.ts +3 -0
  101. package/dist-server/service/chat-session/attachment-route.js +44 -0
  102. package/dist-server/service/chat-session/attachment-route.js.map +1 -0
  103. package/dist-server/service/chat-session/attachment-tools.d.ts +3 -0
  104. package/dist-server/service/chat-session/attachment-tools.js +59 -0
  105. package/dist-server/service/chat-session/attachment-tools.js.map +1 -0
  106. package/dist-server/service/chat-session/session-attachments.d.ts +36 -0
  107. package/dist-server/service/chat-session/session-attachments.js +84 -0
  108. package/dist-server/service/chat-session/session-attachments.js.map +1 -0
  109. package/dist-server/service/chat-session/session-inbox.d.ts +29 -0
  110. package/dist-server/service/chat-session/session-inbox.js +32 -8
  111. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  112. package/dist-server/service/chat-session/session-purge.d.ts +14 -0
  113. package/dist-server/service/chat-session/session-purge.js +77 -0
  114. package/dist-server/service/chat-session/session-purge.js.map +1 -0
  115. package/dist-server/service/chat-session/session-retention.d.ts +6 -0
  116. package/dist-server/service/chat-session/session-retention.js +20 -0
  117. package/dist-server/service/chat-session/session-retention.js.map +1 -0
  118. package/dist-server/service/chat-session/session-visibility.d.ts +8 -0
  119. package/dist-server/service/chat-session/session-visibility.js +11 -0
  120. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  121. package/dist-server/service/chat-session/session-write-gate.d.ts +11 -0
  122. package/dist-server/service/chat-session/session-write-gate.js +24 -0
  123. package/dist-server/service/chat-session/session-write-gate.js.map +1 -0
  124. package/dist-server/service/chat-session-participant/invite-notification.d.ts +1 -1
  125. package/dist-server/service/chat-session-participant/invite-notification.js +21 -6
  126. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
  127. package/dist-server/service/chat-session-participant/participant-lookup-roles.d.ts +3 -0
  128. package/dist-server/service/chat-session-participant/participant-lookup-roles.js +10 -0
  129. package/dist-server/service/chat-session-participant/participant-lookup-roles.js.map +1 -0
  130. package/dist-server/service/chat-session-participant/participant-lookup.d.ts +2 -0
  131. package/dist-server/service/chat-session-participant/participant-lookup.js +26 -0
  132. package/dist-server/service/chat-session-participant/participant-lookup.js.map +1 -0
  133. package/dist-server/service/chat-session-resolver.d.ts +17 -2
  134. package/dist-server/service/chat-session-resolver.js +135 -15
  135. package/dist-server/service/chat-session-resolver.js.map +1 -1
  136. package/dist-server/service/index.d.ts +6 -0
  137. package/dist-server/service/index.js +8 -1
  138. package/dist-server/service/index.js.map +1 -1
  139. package/dist-server/tsconfig.tsbuildinfo +1 -1
  140. package/package.json +11 -4
  141. package/server/service/assistant-chat-resolver.ts +72 -5
  142. package/server/service/assistant-mode/assistant-mode-resolver.ts +6 -0
  143. package/server/service/assistant-mode/resolve.ts +9 -2
  144. package/server/service/assistant-mode/types.ts +6 -0
  145. package/server/service/chat-message/chat-message.ts +17 -0
  146. package/server/service/chat-message/fold-session-history.test.ts +18 -0
  147. package/server/service/chat-session/attachment-policy.test.ts +42 -0
  148. package/server/service/chat-session/attachment-policy.ts +68 -0
  149. package/server/service/chat-session/attachment-route.ts +45 -0
  150. package/server/service/chat-session/attachment-tools.ts +63 -0
  151. package/server/service/chat-session/session-attachments.ts +89 -0
  152. package/server/service/chat-session/session-inbox.test.ts +71 -1
  153. package/server/service/chat-session/session-inbox.ts +61 -11
  154. package/server/service/chat-session/session-purge.test.ts +24 -0
  155. package/server/service/chat-session/session-purge.ts +81 -0
  156. package/server/service/chat-session/session-retention.ts +17 -0
  157. package/server/service/chat-session/session-visibility.ts +11 -0
  158. package/server/service/chat-session/session-write-gate.test.ts +53 -0
  159. package/server/service/chat-session/session-write-gate.ts +31 -0
  160. package/server/service/chat-session-participant/invite-notification.test.ts +15 -0
  161. package/server/service/chat-session-participant/invite-notification.ts +24 -6
  162. package/server/service/chat-session-participant/participant-lookup-roles.ts +6 -0
  163. package/server/service/chat-session-participant/participant-lookup.ts +22 -0
  164. package/server/service/chat-session-resolver.ts +130 -13
  165. package/server/service/index.ts +6 -0
  166. package/test/assistant-dock.test.ts +118 -0
  167. package/test/assistant-session-manager.test.ts +126 -0
  168. package/test/attachment-policy-agree.test.ts +32 -0
  169. package/test/open-doors.test.ts +5 -2
  170. package/translations/en.json +61 -1
  171. package/translations/ja.json +61 -1
  172. package/translations/ko.json +61 -1
  173. package/translations/ms.json +61 -1
  174. package/translations/zh.json +61 -1
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PURGE_BATCH = exports.retentionDaysOf = exports.purgeCutoff = exports.DEFAULT_RETENTION_DAYS = void 0;
4
+ exports.purgeDeletedSessions = purgeDeletedSessions;
5
+ exports.schedulePurge = schedulePurge;
6
+ /*
7
+ * Emptying deleted conversations after they have been kept long enough - design/ai/shared-dock.md §9-5.
8
+ *
9
+ * `deleteAISession` is a soft delete: from that moment nobody can open the conversation or its files
10
+ * (every door checks the session), and within the retention period it can still be brought back whole.
11
+ * Nothing ever emptied them afterwards, so a deleted conversation's messages - and now its files - stayed
12
+ * forever (2026-09-25). This job removes a conversation, its messages, its participants and its attached
13
+ * files together once the retention period has passed.
14
+ *
15
+ * Retention: config `ai-assistant.sessionRetentionDays`, default 30. `0` or `false` turns the job off.
16
+ */
17
+ const env_1 = require("@things-factory/env");
18
+ const shell_1 = require("@things-factory/shell");
19
+ const typeorm_1 = require("typeorm");
20
+ const chat_message_js_1 = require("../chat-message/chat-message.js");
21
+ const chat_session_participant_js_1 = require("../chat-session-participant/chat-session-participant.js");
22
+ const chat_session_js_1 = require("./chat-session.js");
23
+ const session_attachments_js_1 = require("./session-attachments.js");
24
+ const session_retention_js_1 = require("./session-retention.js");
25
+ var session_retention_js_2 = require("./session-retention.js");
26
+ Object.defineProperty(exports, "DEFAULT_RETENTION_DAYS", { enumerable: true, get: function () { return session_retention_js_2.DEFAULT_RETENTION_DAYS; } });
27
+ Object.defineProperty(exports, "purgeCutoff", { enumerable: true, get: function () { return session_retention_js_2.purgeCutoff; } });
28
+ Object.defineProperty(exports, "retentionDaysOf", { enumerable: true, get: function () { return session_retention_js_2.retentionDaysOf; } });
29
+ /** How many conversations one run empties - a large backlog is worked off over several runs. */
30
+ exports.PURGE_BATCH = 100;
31
+ /** Empties the conversations whose deletion is older than the retention period. */
32
+ async function purgeDeletedSessions(now = new Date(), retentionDays = (0, session_retention_js_1.retentionDaysOf)(env_1.config.get('ai-assistant', {})?.sessionRetentionDays)) {
33
+ const result = { purged: 0, failed: [] };
34
+ if (retentionDays === null)
35
+ return result;
36
+ const sessions = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).find({
37
+ where: { deletedAt: (0, typeorm_1.LessThan)((0, session_retention_js_1.purgeCutoff)(now, retentionDays)) },
38
+ withDeleted: true,
39
+ relations: ['domain'],
40
+ take: exports.PURGE_BATCH
41
+ });
42
+ for (const session of sessions) {
43
+ try {
44
+ await (0, session_attachments_js_1.removeSessionFiles)(session);
45
+ /* Messages point at each other (parentMessage) - drop the links before the rows. */
46
+ await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage).update({ session: { id: session.id }, parentMessage: (0, typeorm_1.Not)((0, typeorm_1.IsNull)()) }, { parentMessage: null });
47
+ await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage).delete({ session: { id: session.id } });
48
+ await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).delete({ session: { id: session.id } });
49
+ /* Board patches and anything else hanging on the session go with it (ON DELETE CASCADE). */
50
+ await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).delete({ id: (0, typeorm_1.In)([session.id]) });
51
+ result.purged++;
52
+ }
53
+ catch (error) {
54
+ /* One conversation that cannot be emptied must not stop the rest; it is tried again next run. */
55
+ result.failed.push({ sessionId: session.id, message: error.message });
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+ let timer;
61
+ /** Runs the job once a little after boot, then daily. The timer never keeps the process alive. */
62
+ function schedulePurge() {
63
+ if (timer)
64
+ return;
65
+ const run = () => purgeDeletedSessions()
66
+ .then(({ purged, failed }) => {
67
+ if (purged || failed.length)
68
+ console.info(`[ai-assistant] purged ${purged} deleted conversation(s)`, failed.length ? { failed } : '');
69
+ })
70
+ .catch(error => console.warn('[ai-assistant] purging deleted conversations failed:', error.message));
71
+ const first = setTimeout(run, 60 * 1000);
72
+ first.unref?.();
73
+ timer = setInterval(run, session_retention_js_1.DAY_MS);
74
+ timer.unref?.();
75
+ }
76
+ process.on('bootstrap-module-start', () => schedulePurge());
77
+ //# sourceMappingURL=session-purge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-purge.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-purge.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Emptying deleted conversations after they have been kept long enough - design/ai/shared-dock.md §9-5.\n *\n * `deleteAISession` is a soft delete: from that moment nobody can open the conversation or its files\n * (every door checks the session), and within the retention period it can still be brought back whole.\n * Nothing ever emptied them afterwards, so a deleted conversation's messages - and now its files - stayed\n * forever (2026-09-25). This job removes a conversation, its messages, its participants and its attached\n * files together once the retention period has passed.\n *\n * Retention: config `ai-assistant.sessionRetentionDays`, default 30. `0` or `false` turns the job off.\n */\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { In, IsNull, LessThan, Not } from 'typeorm'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\nimport { ChatSessionParticipant } from '../chat-session-participant/chat-session-participant.js'\nimport { ChatSession } from './chat-session.js'\nimport { removeSessionFiles } from './session-attachments.js'\n\nimport { DAY_MS, purgeCutoff, retentionDaysOf } from './session-retention.js'\n\nexport { DEFAULT_RETENTION_DAYS, purgeCutoff, retentionDaysOf } from './session-retention.js'\n/** How many conversations one run empties - a large backlog is worked off over several runs. */\nexport const PURGE_BATCH = 100\n\nexport interface PurgeResult {\n purged: number\n failed: { sessionId: string; message: string }[]\n}\n\n/** Empties the conversations whose deletion is older than the retention period. */\nexport async function purgeDeletedSessions(now = new Date(), retentionDays = retentionDaysOf(config.get('ai-assistant', {})?.sessionRetentionDays)): Promise<PurgeResult> {\n const result: PurgeResult = { purged: 0, failed: [] }\n if (retentionDays === null) return result\n\n const sessions = await getRepository(ChatSession).find({\n where: { deletedAt: LessThan(purgeCutoff(now, retentionDays)) } as any,\n withDeleted: true,\n relations: ['domain'],\n take: PURGE_BATCH\n })\n\n for (const session of sessions) {\n try {\n await removeSessionFiles(session as any)\n /* Messages point at each other (parentMessage) - drop the links before the rows. */\n await getRepository(ChatMessage).update({ session: { id: session.id }, parentMessage: Not(IsNull()) } as any, { parentMessage: null } as any)\n await getRepository(ChatMessage).delete({ session: { id: session.id } } as any)\n await getRepository(ChatSessionParticipant).delete({ session: { id: session.id } } as any)\n /* Board patches and anything else hanging on the session go with it (ON DELETE CASCADE). */\n await getRepository(ChatSession).delete({ id: In([session.id]) } as any)\n result.purged++\n } catch (error) {\n /* One conversation that cannot be emptied must not stop the rest; it is tried again next run. */\n result.failed.push({ sessionId: session.id!, message: (error as Error).message })\n }\n }\n\n return result\n}\n\nlet timer: ReturnType<typeof setInterval> | undefined\n\n/** Runs the job once a little after boot, then daily. The timer never keeps the process alive. */\nexport function schedulePurge(): void {\n if (timer) return\n const run = () =>\n purgeDeletedSessions()\n .then(({ purged, failed }) => {\n if (purged || failed.length) console.info(`[ai-assistant] purged ${purged} deleted conversation(s)`, failed.length ? { failed } : '')\n })\n .catch(error => console.warn('[ai-assistant] purging deleted conversations failed:', (error as Error).message))\n\n const first = setTimeout(run, 60 * 1000)\n first.unref?.()\n timer = setInterval(run, DAY_MS)\n timer.unref?.()\n}\n\nprocess.on('bootstrap-module-start' as any, () => schedulePurge())\n"]}
@@ -0,0 +1,6 @@
1
+ export declare const DAY_MS: number;
2
+ export declare const DEFAULT_RETENTION_DAYS = 30;
3
+ /** The retention the config asks for: a positive number of days, `null` when the job is off, the default when unreadable. */
4
+ export declare function retentionDaysOf(value: unknown): number | null;
5
+ /** Conversations deleted before this moment are due. */
6
+ export declare function purgeCutoff(now: Date, retentionDays: number): Date;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /* How long a deleted conversation is kept before it is emptied - pure, tested alone (shared-dock.md §9-5). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.DEFAULT_RETENTION_DAYS = exports.DAY_MS = void 0;
5
+ exports.retentionDaysOf = retentionDaysOf;
6
+ exports.purgeCutoff = purgeCutoff;
7
+ exports.DAY_MS = 24 * 60 * 60 * 1000;
8
+ exports.DEFAULT_RETENTION_DAYS = 30;
9
+ /** The retention the config asks for: a positive number of days, `null` when the job is off, the default when unreadable. */
10
+ function retentionDaysOf(value) {
11
+ if (value === false || value === 0 || value === '0')
12
+ return null;
13
+ const days = Number(value);
14
+ return Number.isFinite(days) && days > 0 ? days : exports.DEFAULT_RETENTION_DAYS;
15
+ }
16
+ /** Conversations deleted before this moment are due. */
17
+ function purgeCutoff(now, retentionDays) {
18
+ return new Date(now.getTime() - retentionDays * exports.DAY_MS);
19
+ }
20
+ //# sourceMappingURL=session-retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-retention.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-retention.ts"],"names":[],"mappings":";AAAA,8GAA8G;;;AAM9G,0CAIC;AAGD,kCAEC;AAbY,QAAA,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAC5B,QAAA,sBAAsB,GAAG,EAAE,CAAA;AAExC,6HAA6H;AAC7H,SAAgB,eAAe,CAAC,KAAc;IAC5C,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAChE,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC1B,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,8BAAsB,CAAA;AAC1E,CAAC;AAED,wDAAwD;AACxD,SAAgB,WAAW,CAAC,GAAS,EAAE,aAAqB;IAC1D,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,aAAa,GAAG,cAAM,CAAC,CAAA;AACzD,CAAC","sourcesContent":["/* How long a deleted conversation is kept before it is emptied - pure, tested alone (shared-dock.md §9-5). */\n\nexport const DAY_MS = 24 * 60 * 60 * 1000\nexport const DEFAULT_RETENTION_DAYS = 30\n\n/** The retention the config asks for: a positive number of days, `null` when the job is off, the default when unreadable. */\nexport function retentionDaysOf(value: unknown): number | null {\n if (value === false || value === 0 || value === '0') return null\n const days = Number(value)\n return Number.isFinite(days) && days > 0 ? days : DEFAULT_RETENTION_DAYS\n}\n\n/** Conversations deleted before this moment are due. */\nexport function purgeCutoff(now: Date, retentionDays: number): Date {\n return new Date(now.getTime() - retentionDays * DAY_MS)\n}\n\n"]}
@@ -13,3 +13,11 @@ export declare function initialVisibility(session: {
13
13
  }): SessionVisibility;
14
14
  /** 이 사람이 이 대화를 읽을 수 있나. open 이면 도메인 안 누구나, private 이면 참여자만. */
15
15
  export declare function mayReadSession(visibility: string | null | undefined, isParticipant: boolean): boolean;
16
+ /**
17
+ * 이 사람이 이 대화에 **쓸** 수 있나 — 읽을 수 있는 사람만 쓴다.
18
+ *
19
+ * 읽는 문(`chatMessages`)과 목록만 막고 쓰는 문을 열어 두면, 비공개 대화의 id 를 가진 사람이 그 안에 말을
20
+ * 넣을 수 있었다. `assistantChat` 에서는 더 나쁘다 — 답이 그 대화의 이력 위에서 만들어지므로, 읽을 수 없는
21
+ * 내용이 답으로 새어 나간다(2026-09-24, operato-ecs 두 사람 시험 7번). 쓰는 문도 같은 관문을 지난다.
22
+ */
23
+ export declare function mayWriteSession(visibility: string | null | undefined, isParticipant: boolean): boolean;
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.SESSION_VISIBILITIES = void 0;
9
9
  exports.initialVisibility = initialVisibility;
10
10
  exports.mayReadSession = mayReadSession;
11
+ exports.mayWriteSession = mayWriteSession;
11
12
  exports.SESSION_VISIBILITIES = ['open', 'private'];
12
13
  /**
13
14
  * 이렇게 만들어진 세션은 무엇으로 시작하나.
@@ -32,4 +33,14 @@ function initialVisibility(session) {
32
33
  function mayReadSession(visibility, isParticipant) {
33
34
  return (visibility ?? 'open') === 'open' || isParticipant;
34
35
  }
36
+ /**
37
+ * 이 사람이 이 대화에 **쓸** 수 있나 — 읽을 수 있는 사람만 쓴다.
38
+ *
39
+ * 읽는 문(`chatMessages`)과 목록만 막고 쓰는 문을 열어 두면, 비공개 대화의 id 를 가진 사람이 그 안에 말을
40
+ * 넣을 수 있었다. `assistantChat` 에서는 더 나쁘다 — 답이 그 대화의 이력 위에서 만들어지므로, 읽을 수 없는
41
+ * 내용이 답으로 새어 나간다(2026-09-24, operato-ecs 두 사람 시험 7번). 쓰는 문도 같은 관문을 지난다.
42
+ */
43
+ function mayWriteSession(visibility, isParticipant) {
44
+ return mayReadSession(visibility, isParticipant);
45
+ }
35
46
  //# sourceMappingURL=session-visibility.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-visibility.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-visibility.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAaH,8CAWC;AAGD,wCAEC;AAzBY,QAAA,oBAAoB,GAAiC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAErF;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,OAA+D;IAC/F,IAAI,OAAO,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAEpC;;;;;OAKG;IACH,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,KAAK,MAAM,CAAA;IACtE,OAAO,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;AACtC,CAAC;AAED,+DAA+D;AAC/D,SAAgB,cAAc,CAAC,UAAqC,EAAE,aAAsB;IAC1F,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,KAAK,MAAM,IAAI,aAAa,CAAA;AAC3D,CAAC","sourcesContent":["/*\n * 새 대화가 공개로 태어나는가 — ADR-0073 판정 2.\n *\n * 판정을 세션을 만드는 자리마다 적으면 한 곳이 빠진 날 그 세션만 다른 규칙으로 산다. 한 줄로 모은다.\n */\n\nexport type SessionVisibility = 'open' | 'private'\n\nexport const SESSION_VISIBILITIES: readonly SessionVisibility[] = ['open', 'private']\n\n/**\n * 이렇게 만들어진 세션은 무엇으로 시작하나.\n *\n * 앵커가 있으면 공유된 대상을 놓고 하는 이야기다 — 공개. 앵커가 없거나 앞선 대화를 옮겨 적어 만든\n * 것이면 비공개. `copyOf` 가 있다는 것은 다른 대화의 원문이 이 안에 들어왔다는 뜻이고, 그 원문은\n * 자기가 있던 자리의 규칙으로 쓰였다.\n */\nexport function initialVisibility(session: { anchorType?: string | null; copyOf?: string | null }): SessionVisibility {\n if (session.copyOf) return 'private'\n\n /*\n * `'none'` 은 **앵커가 아니다.** 저장되는 앵커 종류 목록에 그 글자가 들어 있어서(`session-anchor.ts`\n * — 대상이 없다는 것도 행에 적어야 하므로), 「값이 있으면 앵커가 있다」로 읽으면 대상 없는 개인 질문이\n * 공개 방으로 태어난다. ADR-0073 결정 2 가 그것을 `private` 로 못박았다 — 「앵커 없는 세션(`twin.none`\n * 같은 개인 질문)」.\n */\n const anchored = !!session.anchorType && session.anchorType !== 'none'\n return anchored ? 'open' : 'private'\n}\n\n/** 이 사람이 이 대화를 읽을 수 있나. open 이면 도메인 안 누구나, private 이면 참여자만. */\nexport function mayReadSession(visibility: string | null | undefined, isParticipant: boolean): boolean {\n return (visibility ?? 'open') === 'open' || isParticipant\n}\n"]}
1
+ {"version":3,"file":"session-visibility.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-visibility.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAaH,8CAWC;AAGD,wCAEC;AASD,0CAEC;AApCY,QAAA,oBAAoB,GAAiC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAErF;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,OAA+D;IAC/F,IAAI,OAAO,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAEpC;;;;;OAKG;IACH,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,KAAK,MAAM,CAAA;IACtE,OAAO,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;AACtC,CAAC;AAED,+DAA+D;AAC/D,SAAgB,cAAc,CAAC,UAAqC,EAAE,aAAsB;IAC1F,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,KAAK,MAAM,IAAI,aAAa,CAAA;AAC3D,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,UAAqC,EAAE,aAAsB;IAC3F,OAAO,cAAc,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;AAClD,CAAC","sourcesContent":["/*\n * 새 대화가 공개로 태어나는가 — ADR-0073 판정 2.\n *\n * 판정을 세션을 만드는 자리마다 적으면 한 곳이 빠진 날 그 세션만 다른 규칙으로 산다. 한 줄로 모은다.\n */\n\nexport type SessionVisibility = 'open' | 'private'\n\nexport const SESSION_VISIBILITIES: readonly SessionVisibility[] = ['open', 'private']\n\n/**\n * 이렇게 만들어진 세션은 무엇으로 시작하나.\n *\n * 앵커가 있으면 공유된 대상을 놓고 하는 이야기다 — 공개. 앵커가 없거나 앞선 대화를 옮겨 적어 만든\n * 것이면 비공개. `copyOf` 가 있다는 것은 다른 대화의 원문이 이 안에 들어왔다는 뜻이고, 그 원문은\n * 자기가 있던 자리의 규칙으로 쓰였다.\n */\nexport function initialVisibility(session: { anchorType?: string | null; copyOf?: string | null }): SessionVisibility {\n if (session.copyOf) return 'private'\n\n /*\n * `'none'` 은 **앵커가 아니다.** 저장되는 앵커 종류 목록에 그 글자가 들어 있어서(`session-anchor.ts`\n * — 대상이 없다는 것도 행에 적어야 하므로), 「값이 있으면 앵커가 있다」로 읽으면 대상 없는 개인 질문이\n * 공개 방으로 태어난다. ADR-0073 결정 2 가 그것을 `private` 로 못박았다 — 「앵커 없는 세션(`twin.none`\n * 같은 개인 질문)」.\n */\n const anchored = !!session.anchorType && session.anchorType !== 'none'\n return anchored ? 'open' : 'private'\n}\n\n/** 이 사람이 이 대화를 읽을 수 있나. open 이면 도메인 안 누구나, private 이면 참여자만. */\nexport function mayReadSession(visibility: string | null | undefined, isParticipant: boolean): boolean {\n return (visibility ?? 'open') === 'open' || isParticipant\n}\n\n/**\n * 이 사람이 이 대화에 **쓸** 수 있나 — 읽을 수 있는 사람만 쓴다.\n *\n * 읽는 문(`chatMessages`)과 목록만 막고 쓰는 문을 열어 두면, 비공개 대화의 id 를 가진 사람이 그 안에 말을\n * 넣을 수 있었다. `assistantChat` 에서는 더 나쁘다 — 답이 그 대화의 이력 위에서 만들어지므로, 읽을 수 없는\n * 내용이 답으로 새어 나간다(2026-09-24, operato-ecs 두 사람 시험 7번). 쓰는 문도 같은 관문을 지난다.\n */\nexport function mayWriteSession(visibility: string | null | undefined, isParticipant: boolean): boolean {\n return mayReadSession(visibility, isParticipant)\n}\n"]}
@@ -0,0 +1,11 @@
1
+ export interface WriteGateInput {
2
+ session: {
3
+ id?: string;
4
+ visibility?: string | null;
5
+ };
6
+ userId?: string;
7
+ /** Is this user a participant of the session (owner or member)? Asked only for a private one. */
8
+ isParticipant: () => Promise<boolean>;
9
+ t: (key: string, options?: Record<string, unknown>) => string;
10
+ }
11
+ export declare function assertMayWriteSession({ session, userId, isParticipant, t }: WriteGateInput): Promise<void>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertMayWriteSession = assertMayWriteSession;
4
+ /*
5
+ * The gate every door that writes into a conversation passes - ADR-0073 decision 1, applied to writing.
6
+ *
7
+ * `mayWriteSession` is the rule; this looks up only what the rule needs (whether the writer is in the
8
+ * room, and only for a private conversation) and refuses by a named message. Kept apart from the resolvers
9
+ * so the five write doors call one line and cannot drift.
10
+ */
11
+ const session_visibility_js_1 = require("./session-visibility.js");
12
+ async function assertMayWriteSession({ session, userId, isParticipant, t }) {
13
+ const visibility = session.visibility ?? 'open';
14
+ /* A public conversation is open to everyone in the domain - no lookup. */
15
+ if (visibility === 'open')
16
+ return;
17
+ const participant = !!userId && (await isParticipant());
18
+ if (!(0, session_visibility_js_1.mayWriteSession)(visibility, participant)) {
19
+ throw new Error(t('ai-assistant.text.private-session-refused', {
20
+ defaultValue: 'this conversation is private - only the people in it can write here. ask one of them to bring you in.'
21
+ }));
22
+ }
23
+ }
24
+ //# sourceMappingURL=session-write-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-write-gate.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-write-gate.ts"],"names":[],"mappings":";;AAiBA,sDAaC;AA9BD;;;;;;GAMG;AACH,mEAAyD;AAUlD,KAAK,UAAU,qBAAqB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,EAAkB;IAC/F,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,MAAM,CAAA;IAC/C,0EAA0E;IAC1E,IAAI,UAAU,KAAK,MAAM;QAAE,OAAM;IAEjC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,aAAa,EAAE,CAAC,CAAA;IACvD,IAAI,CAAC,IAAA,uCAAe,EAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,CAAC,CAAC,2CAA2C,EAAE;YAC7C,YAAY,EAAE,uGAAuG;SACtH,CAAC,CACH,CAAA;IACH,CAAC;AACH,CAAC","sourcesContent":["/*\n * The gate every door that writes into a conversation passes - ADR-0073 decision 1, applied to writing.\n *\n * `mayWriteSession` is the rule; this looks up only what the rule needs (whether the writer is in the\n * room, and only for a private conversation) and refuses by a named message. Kept apart from the resolvers\n * so the five write doors call one line and cannot drift.\n */\nimport { mayWriteSession } from './session-visibility.js'\n\nexport interface WriteGateInput {\n session: { id?: string; visibility?: string | null }\n userId?: string\n /** Is this user a participant of the session (owner or member)? Asked only for a private one. */\n isParticipant: () => Promise<boolean>\n t: (key: string, options?: Record<string, unknown>) => string\n}\n\nexport async function assertMayWriteSession({ session, userId, isParticipant, t }: WriteGateInput): Promise<void> {\n const visibility = session.visibility ?? 'open'\n /* A public conversation is open to everyone in the domain - no lookup. */\n if (visibility === 'open') return\n\n const participant = !!userId && (await isParticipant())\n if (!mayWriteSession(visibility, participant)) {\n throw new Error(\n t('ai-assistant.text.private-session-refused', {\n defaultValue: 'this conversation is private - only the people in it can write here. ask one of them to bring you in.'\n })\n )\n }\n}\n"]}
@@ -24,4 +24,4 @@ export interface Inviter {
24
24
  * 초대된 사용자들에게 보낼 협업 초대 알림 payload 를 만든다.
25
25
  * 받는 사람이 없으면 null (= 보낼 것 없음).
26
26
  */
27
- export declare function buildInviteNotification(userIds: string[], boardId: string | undefined, inviter: Inviter | undefined | null): InviteNotification | null;
27
+ export declare function buildInviteNotification(userIds: string[], boardId: string | undefined, inviter: Inviter | undefined | null, sessionName?: string | null): InviteNotification | null;
@@ -12,18 +12,33 @@ exports.buildInviteNotification = buildInviteNotification;
12
12
  * 초대된 사용자들에게 보낼 협업 초대 알림 payload 를 만든다.
13
13
  * 받는 사람이 없으면 null (= 보낼 것 없음).
14
14
  */
15
- function buildInviteNotification(userIds, boardId, inviter) {
15
+ function buildInviteNotification(userIds, boardId, inviter, sessionName) {
16
16
  if (!Array.isArray(userIds) || userIds.length === 0)
17
17
  return null;
18
18
  const who = inviter?.name || inviter?.email || 'someone';
19
+ /* A board conversation keeps its board wording and the link to the modeller. */
20
+ if (boardId) {
21
+ return {
22
+ receivers: userIds,
23
+ type: 'board-collab-invite',
24
+ subject: 'info',
25
+ title: 'Board collaboration invite',
26
+ body: `${who} invited you to collaborate on a board.`,
27
+ url: `board-modeller/${boardId}`,
28
+ mode: 'background'
29
+ };
30
+ }
31
+ /*
32
+ * Every other conversation is not about a board. Both chat doors called this without one and every
33
+ * invite read "Board collaboration invite" - in operato-ecs, in plant, anywhere (2026-09-24).
34
+ */
35
+ const name = sessionName?.trim();
19
36
  return {
20
37
  receivers: userIds,
21
- type: 'board-collab-invite',
38
+ type: 'chat-invite',
22
39
  subject: 'info',
23
- title: 'Board collaboration invite',
24
- body: `${who} invited you to collaborate on a board.`,
25
- // board 가 연결돼 있으면 모델러로 바로 가는 링크. 없으면 링크 생략.
26
- url: boardId ? `board-modeller/${boardId}` : undefined,
40
+ title: 'Conversation invite',
41
+ body: name ? `${who} brought you into the conversation "${name}".` : `${who} brought you into a conversation.`,
27
42
  mode: 'background'
28
43
  };
29
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"invite-notification.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/invite-notification.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAuBH,0DAkBC;AAtBD;;;GAGG;AACH,SAAgB,uBAAuB,CACrC,OAAiB,EACjB,OAA2B,EAC3B,OAAmC;IAEnC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEhE,MAAM,GAAG,GAAG,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,KAAK,IAAI,SAAS,CAAA;IACxD,OAAO;QACL,SAAS,EAAE,OAAO;QAClB,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,4BAA4B;QACnC,IAAI,EAAE,GAAG,GAAG,yCAAyC;QACrD,4CAA4C;QAC5C,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS;QACtD,IAAI,EAAE,YAAY;KACnB,CAAA;AACH,CAAC","sourcesContent":["/**\n * 협업 초대 알림 — 순수 payload 빌더.\n *\n * 부수효과(실제 발송)는 resolver 의 thin caller 가 `context.state.notify` 로 수행.\n * 여기엔 \"무엇을 보낼지\"(payload) 결정 로직만 둔다 — DB·pubsub·notify 의존 없이 테스트 가능.\n * (patch spine 의 board-patch-broadcast.ts 와 동일한 순수/부수효과 분리 컨벤션.)\n */\n\n/** notification-middleware 의 notifier 가 받는 인자 형태 (필요한 키만). */\nexport interface InviteNotification {\n receivers: string[]\n type: string\n subject: string\n title: string\n body: string\n url?: string\n mode: 'background' | 'foreground'\n}\n\n/** 초대자 식별용 — User 엔티티에서 쓰는 필드만 (duck-typed). */\nexport interface Inviter {\n name?: string | null\n email?: string | null\n}\n\n/**\n * 초대된 사용자들에게 보낼 협업 초대 알림 payload 를 만든다.\n * 받는 사람이 없으면 null (= 보낼 것 없음).\n */\nexport function buildInviteNotification(\n userIds: string[],\n boardId: string | undefined,\n inviter: Inviter | undefined | null\n): InviteNotification | null {\n if (!Array.isArray(userIds) || userIds.length === 0) return null\n\n const who = inviter?.name || inviter?.email || 'someone'\n return {\n receivers: userIds,\n type: 'board-collab-invite',\n subject: 'info',\n title: 'Board collaboration invite',\n body: `${who} invited you to collaborate on a board.`,\n // board 가 연결돼 있으면 모델러로 바로 가는 링크. 없으면 링크 생략.\n url: boardId ? `board-modeller/${boardId}` : undefined,\n mode: 'background'\n }\n}\n"]}
1
+ {"version":3,"file":"invite-notification.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/invite-notification.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAuBH,0DAoCC;AAxCD;;;GAGG;AACH,SAAgB,uBAAuB,CACrC,OAAiB,EACjB,OAA2B,EAC3B,OAAmC,EACnC,WAA2B;IAE3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEhE,MAAM,GAAG,GAAG,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,KAAK,IAAI,SAAS,CAAA;IAExD,gFAAgF;IAChF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO;YACL,SAAS,EAAE,OAAO;YAClB,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,MAAM;YACf,KAAK,EAAE,4BAA4B;YACnC,IAAI,EAAE,GAAG,GAAG,yCAAyC;YACrD,GAAG,EAAE,kBAAkB,OAAO,EAAE;YAChC,IAAI,EAAE,YAAY;SACnB,CAAA;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,IAAI,GAAG,WAAW,EAAE,IAAI,EAAE,CAAA;IAChC,OAAO;QACL,SAAS,EAAE,OAAO;QAClB,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,qBAAqB;QAC5B,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,uCAAuC,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,mCAAmC;QAC9G,IAAI,EAAE,YAAY;KACnB,CAAA;AACH,CAAC","sourcesContent":["/**\n * 협업 초대 알림 — 순수 payload 빌더.\n *\n * 부수효과(실제 발송)는 resolver 의 thin caller 가 `context.state.notify` 로 수행.\n * 여기엔 \"무엇을 보낼지\"(payload) 결정 로직만 둔다 — DB·pubsub·notify 의존 없이 테스트 가능.\n * (patch spine 의 board-patch-broadcast.ts 와 동일한 순수/부수효과 분리 컨벤션.)\n */\n\n/** notification-middleware 의 notifier 가 받는 인자 형태 (필요한 키만). */\nexport interface InviteNotification {\n receivers: string[]\n type: string\n subject: string\n title: string\n body: string\n url?: string\n mode: 'background' | 'foreground'\n}\n\n/** 초대자 식별용 — User 엔티티에서 쓰는 필드만 (duck-typed). */\nexport interface Inviter {\n name?: string | null\n email?: string | null\n}\n\n/**\n * 초대된 사용자들에게 보낼 협업 초대 알림 payload 를 만든다.\n * 받는 사람이 없으면 null (= 보낼 것 없음).\n */\nexport function buildInviteNotification(\n userIds: string[],\n boardId: string | undefined,\n inviter: Inviter | undefined | null,\n sessionName?: string | null\n): InviteNotification | null {\n if (!Array.isArray(userIds) || userIds.length === 0) return null\n\n const who = inviter?.name || inviter?.email || 'someone'\n\n /* A board conversation keeps its board wording and the link to the modeller. */\n if (boardId) {\n return {\n receivers: userIds,\n type: 'board-collab-invite',\n subject: 'info',\n title: 'Board collaboration invite',\n body: `${who} invited you to collaborate on a board.`,\n url: `board-modeller/${boardId}`,\n mode: 'background'\n }\n }\n\n /*\n * Every other conversation is not about a board. Both chat doors called this without one and every\n * invite read \"Board collaboration invite\" - in operato-ecs, in plant, anywhere (2026-09-24).\n */\n const name = sessionName?.trim()\n return {\n receivers: userIds,\n type: 'chat-invite',\n subject: 'info',\n title: 'Conversation invite',\n body: name ? `${who} brought you into the conversation \"${name}\".` : `${who} brought you into a conversation.`,\n mode: 'background'\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /** Being in the room - owner or member. A `viewer` row only remembers a hide. Pure, so it is tested alone. */
2
+ export declare const ROOM_ROLES: readonly ["owner", "member"];
3
+ export declare function isRoomRole(role: string | null | undefined): boolean;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ROOM_ROLES = void 0;
4
+ exports.isRoomRole = isRoomRole;
5
+ /** Being in the room - owner or member. A `viewer` row only remembers a hide. Pure, so it is tested alone. */
6
+ exports.ROOM_ROLES = ['owner', 'member'];
7
+ function isRoomRole(role) {
8
+ return exports.ROOM_ROLES.includes(role ?? '');
9
+ }
10
+ //# sourceMappingURL=participant-lookup-roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"participant-lookup-roles.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/participant-lookup-roles.ts"],"names":[],"mappings":";;;AAGA,gCAEC;AALD,8GAA8G;AACjG,QAAA,UAAU,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAU,CAAA;AAEtD,SAAgB,UAAU,CAAC,IAA+B;IACxD,OAAQ,kBAAgC,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["/** Being in the room - owner or member. A `viewer` row only remembers a hide. Pure, so it is tested alone. */\nexport const ROOM_ROLES = ['owner', 'member'] as const\n\nexport function isRoomRole(role: string | null | undefined): boolean {\n return (ROOM_ROLES as readonly string[]).includes(role ?? '')\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export { ROOM_ROLES, isRoomRole } from './participant-lookup-roles.js';
2
+ export declare function isSessionParticipant(domainId: string, sessionId: string, userId: string | undefined, tx?: any): Promise<boolean>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRoomRole = exports.ROOM_ROLES = void 0;
4
+ exports.isSessionParticipant = isSessionParticipant;
5
+ /*
6
+ * Is this user in the room - owner or member?
7
+ *
8
+ * A `viewer` row is not being in the room: `hideAISession` writes one to remember a hide for someone who
9
+ * never took part. Counting it let a person who hid a private conversation by its id read it afterwards -
10
+ * the hide became the key. Every gate that asks "is this a participant" asks here.
11
+ */
12
+ const shell_1 = require("@things-factory/shell");
13
+ const chat_session_participant_js_1 = require("./chat-session-participant.js");
14
+ const participant_lookup_roles_js_1 = require("./participant-lookup-roles.js");
15
+ var participant_lookup_roles_js_2 = require("./participant-lookup-roles.js");
16
+ Object.defineProperty(exports, "ROOM_ROLES", { enumerable: true, get: function () { return participant_lookup_roles_js_2.ROOM_ROLES; } });
17
+ Object.defineProperty(exports, "isRoomRole", { enumerable: true, get: function () { return participant_lookup_roles_js_2.isRoomRole; } });
18
+ async function isSessionParticipant(domainId, sessionId, userId, tx) {
19
+ if (!userId)
20
+ return false;
21
+ const row = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant, tx).findOne({
22
+ where: { domain: { id: domainId }, session: { id: sessionId }, user: { id: userId } }
23
+ });
24
+ return (0, participant_lookup_roles_js_1.isRoomRole)(row?.role);
25
+ }
26
+ //# sourceMappingURL=participant-lookup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"participant-lookup.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/participant-lookup.ts"],"names":[],"mappings":";;;AAeA,oDAMC;AArBD;;;;;;GAMG;AACH,iDAAqD;AAErD,+EAAsE;AAEtE,+EAA0D;AAE1D,6EAAsE;AAA7D,yHAAA,UAAU,OAAA;AAAE,yHAAA,UAAU,OAAA;AAExB,KAAK,UAAU,oBAAoB,CAAC,QAAgB,EAAE,SAAiB,EAAE,MAA0B,EAAE,EAAQ;IAClH,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,MAAM,GAAG,GAAG,MAAM,IAAA,qBAAa,EAAC,oDAAsB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;QAClE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAS,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,SAAS,EAAS,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAS,EAAE;KAC3G,CAAC,CAAA;IACF,OAAO,IAAA,wCAAU,EAAC,GAAG,EAAE,IAAI,CAAC,CAAA;AAC9B,CAAC","sourcesContent":["/*\n * Is this user in the room - owner or member?\n *\n * A `viewer` row is not being in the room: `hideAISession` writes one to remember a hide for someone who\n * never took part. Counting it let a person who hid a private conversation by its id read it afterwards -\n * the hide became the key. Every gate that asks \"is this a participant\" asks here.\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSessionParticipant } from './chat-session-participant.js'\n\nimport { isRoomRole } from './participant-lookup-roles.js'\n\nexport { ROOM_ROLES, isRoomRole } from './participant-lookup-roles.js'\n\nexport async function isSessionParticipant(domainId: string, sessionId: string, userId: string | undefined, tx?: any): Promise<boolean> {\n if (!userId) return false\n const row = await getRepository(ChatSessionParticipant, tx).findOne({\n where: { domain: { id: domainId } as any, session: { id: sessionId } as any, user: { id: userId } as any }\n })\n return isRoomRole(row?.role)\n}\n"]}
@@ -3,6 +3,7 @@ import { ChatSession } from './chat-session/chat-session.js';
3
3
  import { type ProposalOutcome } from './chat-message/proposal-outcome.js';
4
4
  import { ChatMessage } from './chat-message/chat-message.js';
5
5
  import { ChatSessionParticipant } from './chat-session-participant/chat-session-participant.js';
6
+ import type { FileUpload } from 'graphql-upload/GraphQLUpload.js';
6
7
  declare class ChatSessionInboxEntry {
7
8
  id?: string;
8
9
  name?: string;
@@ -20,6 +21,14 @@ declare class ChatSessionInboxEntry {
20
21
  participant: boolean;
21
22
  mine: boolean;
22
23
  hidden?: boolean;
24
+ visibility: string;
25
+ owner: boolean;
26
+ }
27
+ export declare class ChatAttachmentInfo {
28
+ id: string;
29
+ name: string;
30
+ mimetype: string;
31
+ size: number;
23
32
  }
24
33
  export declare class ChatSessionResolver {
25
34
  chatSession(id: string, context: ResolverContext): Promise<ChatSession | null>;
@@ -29,14 +38,14 @@ export declare class ChatSessionResolver {
29
38
  * 집계 쿼리를 쓰지 않는다: 세션의 비정규화된 lastMessageAt 과 내 참여자 lastReadAt 을 **값 비교**한다
30
39
  * (5개 DB 드라이버 지원 때문에 group by/MAX 를 목록 경로에 두지 않는다).
31
40
  */
32
- chatSessionInbox(anchorType: string, anchorId: string, context: ResolverContext, showHidden?: boolean, search?: string, mineOnly?: boolean, limit?: number, offset?: number): Promise<ChatSessionInboxEntry[]>;
41
+ chatSessionInbox(anchorType: string, anchorId: string, context: ResolverContext, showHidden?: boolean, search?: string, mineOnly?: boolean, limit?: number, offset?: number, kind?: string, participantOnly?: boolean, visibility?: string, unreadOnly?: boolean): Promise<ChatSessionInboxEntry[]>;
33
42
  /**
34
43
  * 필터를 통과한 **전체 개수** — "더 보기" 가 남았는지, 검색 결과가 몇 건인지.
35
44
  *
36
45
  * 목록과 같은 요청에 담아 가져가므로 왕복은 늘지 않는다. 개수를 모르면 사용자는 목록의 끝이
37
46
  * 페이지의 끝인지 자료의 끝인지 구별할 수 없다.
38
47
  */
39
- chatSessionInboxTotal(anchorType: string, anchorId: string, context: ResolverContext, showHidden?: boolean, search?: string, mineOnly?: boolean): Promise<number>;
48
+ chatSessionInboxTotal(anchorType: string, anchorId: string, context: ResolverContext, showHidden?: boolean, search?: string, mineOnly?: boolean, kind?: string, participantOnly?: boolean, visibility?: string, unreadOnly?: boolean): Promise<number>;
40
49
  /** 필터 적용(페이지 전) — 목록·개수가 **같은 규칙**을 쓰도록 한 곳에 둔다(어긋나면 "더 보기" 가 거짓말이 된다). */
41
50
  private _inboxFiltered;
42
51
  /**
@@ -100,6 +109,12 @@ export declare class ChatSessionResolver {
100
109
  createAISession(anchorType: string, anchorId: string, name: string | undefined, context: ResolverContext): Promise<ChatSession>;
101
110
  renameChatSession(sessionId: string, name: string, context: ResolverContext): Promise<boolean>;
102
111
  reportProposalOutcome(sessionId: string, proposalKey: string, outcome: ProposalOutcome, context: ResolverContext): Promise<boolean>;
112
+ /**
113
+ * Stores files with a conversation before the turn that carries them (design/ai/shared-dock.md §9).
114
+ * Passes the write gate - a private conversation takes files from its people only - and the mode's
115
+ * attachment policy. The size is known only once stored, so a file over the limit is removed and refused.
116
+ */
117
+ attachToConversation(sessionId: string, files: FileUpload[], context: ResolverContext, kind?: string): Promise<ChatAttachmentInfo[]>;
103
118
  sayToRoom(sessionId: string, content: string, context: ResolverContext): Promise<string | null>;
104
119
  appendSessionNote(sessionId: string, content: string, context: ResolverContext): Promise<boolean>;
105
120
  appendSessionTurns(sessionId: string, turns: {