@things-factory/board-ai 10.0.0-beta.108
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/PLAN-styling-tools.md +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setText / setFont — 컴포넌트의 텍스트 내용 / 폰트 스타일.
|
|
3
|
+
*
|
|
4
|
+
* setText: state.text 변경 (내용)
|
|
5
|
+
* setFont: state.fontFamily / fontSize / fontWeight / fontStyle / fontColor /
|
|
6
|
+
* textAlign / textBaseline / lineHeight 변경 (폰트 스타일)
|
|
7
|
+
*
|
|
8
|
+
* 두 tool 분리 — 의도가 다름. 내용 변경 vs 시각 스타일.
|
|
9
|
+
*/
|
|
10
|
+
import type { BoardEditOp } from '../types'
|
|
11
|
+
import { normalizeColor, validateColor } from './color-utils'
|
|
12
|
+
|
|
13
|
+
export interface SetTextInput {
|
|
14
|
+
refids: number[]
|
|
15
|
+
text: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SetFontInput {
|
|
19
|
+
refids: number[]
|
|
20
|
+
fontFamily?: string
|
|
21
|
+
fontSize?: number
|
|
22
|
+
fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string | number
|
|
23
|
+
fontStyle?: 'normal' | 'italic' | 'oblique'
|
|
24
|
+
fontColor?: string
|
|
25
|
+
/** Canvas API 표준 — 'justify' 는 Canvas 미지원이라 제외. */
|
|
26
|
+
textAlign?: 'left' | 'center' | 'right' | 'start' | 'end'
|
|
27
|
+
textBaseline?: 'top' | 'middle' | 'bottom' | 'hanging' | 'alphabetic' | 'ideographic'
|
|
28
|
+
lineHeight?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Canvas CanvasRenderingContext2D 가 허용하는 값만.
|
|
32
|
+
// dash / lineCap / lineJoin 과 동일 패턴 — AI 가 비표준 값 ('serif-italic',
|
|
33
|
+
// 'middle-align' 등) 을 보내도 Canvas 가 silent ignore → 효과 없는 op 가 그대로
|
|
34
|
+
// 실행되는 것 방지.
|
|
35
|
+
const VALID_FONT_STYLE = new Set(['normal', 'italic', 'oblique'])
|
|
36
|
+
const VALID_TEXT_ALIGN = new Set(['start', 'end', 'left', 'right', 'center'])
|
|
37
|
+
const VALID_TEXT_BASELINE = new Set([
|
|
38
|
+
'top',
|
|
39
|
+
'middle',
|
|
40
|
+
'bottom',
|
|
41
|
+
'hanging',
|
|
42
|
+
'alphabetic',
|
|
43
|
+
'ideographic'
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
export interface OpsResult {
|
|
47
|
+
ops: BoardEditOp[]
|
|
48
|
+
errors: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildSetTextOps(input: SetTextInput): OpsResult {
|
|
52
|
+
if (!Array.isArray(input.refids) || input.refids.length === 0) {
|
|
53
|
+
return { ops: [], errors: ['refids must be a non-empty array'] }
|
|
54
|
+
}
|
|
55
|
+
if (typeof input.text !== 'string') {
|
|
56
|
+
return { ops: [], errors: ['text must be a string'] }
|
|
57
|
+
}
|
|
58
|
+
const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
|
|
59
|
+
if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
|
|
60
|
+
|
|
61
|
+
const patch = { text: input.text }
|
|
62
|
+
return {
|
|
63
|
+
ops: refids.map(refid => ({ op: 'modify', refid, patch })),
|
|
64
|
+
errors: []
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildSetFontOps(input: SetFontInput): OpsResult {
|
|
69
|
+
if (!Array.isArray(input.refids) || input.refids.length === 0) {
|
|
70
|
+
return { ops: [], errors: ['refids must be a non-empty array'] }
|
|
71
|
+
}
|
|
72
|
+
const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
|
|
73
|
+
if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
|
|
74
|
+
|
|
75
|
+
// 어느 인자도 없으면 무의미
|
|
76
|
+
const noOp = !(
|
|
77
|
+
input.fontFamily ||
|
|
78
|
+
input.fontSize !== undefined ||
|
|
79
|
+
input.fontWeight !== undefined ||
|
|
80
|
+
input.fontStyle ||
|
|
81
|
+
input.fontColor ||
|
|
82
|
+
input.textAlign ||
|
|
83
|
+
input.textBaseline ||
|
|
84
|
+
input.lineHeight !== undefined
|
|
85
|
+
)
|
|
86
|
+
if (noOp) return { ops: [], errors: ['No font changes specified'] }
|
|
87
|
+
|
|
88
|
+
if (input.fontSize !== undefined && (input.fontSize <= 0 || !Number.isFinite(input.fontSize))) {
|
|
89
|
+
return { ops: [], errors: [`Invalid fontSize: ${input.fontSize}`] }
|
|
90
|
+
}
|
|
91
|
+
if (input.lineHeight !== undefined && input.lineHeight <= 0) {
|
|
92
|
+
return { ops: [], errors: [`Invalid lineHeight: ${input.lineHeight}`] }
|
|
93
|
+
}
|
|
94
|
+
if (input.fontColor !== undefined) {
|
|
95
|
+
const v = validateColor(input.fontColor)
|
|
96
|
+
if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid fontColor'] }
|
|
97
|
+
}
|
|
98
|
+
if (input.fontStyle !== undefined && !VALID_FONT_STYLE.has(input.fontStyle)) {
|
|
99
|
+
return {
|
|
100
|
+
ops: [],
|
|
101
|
+
errors: [`Unknown fontStyle: '${input.fontStyle}'. Valid: ${[...VALID_FONT_STYLE].join(' / ')}.`]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (input.textAlign !== undefined && !VALID_TEXT_ALIGN.has(input.textAlign)) {
|
|
105
|
+
return {
|
|
106
|
+
ops: [],
|
|
107
|
+
errors: [`Unknown textAlign: '${input.textAlign}'. Valid: ${[...VALID_TEXT_ALIGN].join(' / ')}.`]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (input.textBaseline !== undefined && !VALID_TEXT_BASELINE.has(input.textBaseline)) {
|
|
111
|
+
return {
|
|
112
|
+
ops: [],
|
|
113
|
+
errors: [`Unknown textBaseline: '${input.textBaseline}'. Valid: ${[...VALID_TEXT_BASELINE].join(' / ')}.`]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const patch: any = {}
|
|
118
|
+
if (input.fontFamily) patch.fontFamily = input.fontFamily
|
|
119
|
+
if (input.fontSize !== undefined) patch.fontSize = input.fontSize
|
|
120
|
+
if (input.fontWeight !== undefined) patch.fontWeight = String(input.fontWeight)
|
|
121
|
+
if (input.fontStyle) patch.fontStyle = input.fontStyle
|
|
122
|
+
if (input.fontColor !== undefined) patch.fontColor = normalizeColor(input.fontColor)
|
|
123
|
+
if (input.textAlign) patch.textAlign = input.textAlign
|
|
124
|
+
if (input.textBaseline) patch.textBaseline = input.textBaseline
|
|
125
|
+
if (input.lineHeight !== undefined) patch.lineHeight = input.lineHeight
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
ops: refids.map(refid => ({ op: 'modify', refid, patch })),
|
|
129
|
+
errors: []
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 1 styling tool 단위 테스트 — setStroke / setText / setFont /
|
|
3
|
+
* setShadow / setOpacity / setVisible / setMaterial / setShadowProperties /
|
|
4
|
+
* clearStyle.
|
|
5
|
+
*/
|
|
6
|
+
import { buildSetStrokeOps } from './stroke-tools'
|
|
7
|
+
import { buildSetTextOps, buildSetFontOps } from './text-tools'
|
|
8
|
+
import { buildSetShadowOps, buildSetOpacityOps, buildSetVisibleOps } from './effect-tools'
|
|
9
|
+
import { buildSetMaterialOps, buildSetShadowPropertiesOps } from './material-tools'
|
|
10
|
+
import { buildClearStyleOps } from './clear-tools'
|
|
11
|
+
|
|
12
|
+
describe('setStroke — top-level flat fields (things-scene 실제 모델)', () => {
|
|
13
|
+
test('color → strokeStyle 문자열, width → lineWidth top-level', () => {
|
|
14
|
+
const r = buildSetStrokeOps({ refids: [1], strokeStyle: 'red', lineWidth: 2 })
|
|
15
|
+
expect(r.ops[0].patch).toEqual({
|
|
16
|
+
strokeStyle: '#ff0000',
|
|
17
|
+
lineWidth: 2
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('dash preset alias → things-scene 이름 매핑', () => {
|
|
22
|
+
expect((buildSetStrokeOps({ refids: [1], dash: 'dashed' }).ops[0] as any).patch.lineDash).toBe('dash')
|
|
23
|
+
expect((buildSetStrokeOps({ refids: [1], dash: 'dotted' }).ops[0] as any).patch.lineDash).toBe('square-dot')
|
|
24
|
+
expect((buildSetStrokeOps({ refids: [1], dash: 'dashdot' }).ops[0] as any).patch.lineDash).toBe('dash-dot')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('things-scene 본래 preset 이름도 그대로 받음', () => {
|
|
28
|
+
expect((buildSetStrokeOps({ refids: [1], dash: 'long-dash' }).ops[0] as any).patch.lineDash).toBe('long-dash')
|
|
29
|
+
expect((buildSetStrokeOps({ refids: [1], dash: 'round-dot' }).ops[0] as any).patch.lineDash).toBe('round-dot')
|
|
30
|
+
expect((buildSetStrokeOps({ refids: [1], dash: 'solid' }).ops[0] as any).patch.lineDash).toBe('solid')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('알 수 없는 dash → 에러 + 옵션 안내', () => {
|
|
34
|
+
const r = buildSetStrokeOps({ refids: [1], dash: 'wavy' })
|
|
35
|
+
expect(r.ops).toEqual([])
|
|
36
|
+
expect(r.errors[0]).toMatch(/Unknown dash/)
|
|
37
|
+
expect(r.errors[0]).toMatch(/long-dash/)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('enabled=false → lineWidth 0', () => {
|
|
41
|
+
const r = buildSetStrokeOps({ refids: [1], enabled: false, strokeStyle: 'red' })
|
|
42
|
+
expect((r.ops[0] as any).patch.lineWidth).toBe(0)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('lineCap / lineJoin top-level', () => {
|
|
46
|
+
const r = buildSetStrokeOps({
|
|
47
|
+
refids: [1],
|
|
48
|
+
lineCap: 'round',
|
|
49
|
+
lineJoin: 'bevel'
|
|
50
|
+
})
|
|
51
|
+
expect((r.ops[0] as any).patch).toMatchObject({
|
|
52
|
+
lineCap: 'round',
|
|
53
|
+
lineJoin: 'bevel'
|
|
54
|
+
})
|
|
55
|
+
// strokeStyle 객체 구조 절대 출력 안 됨
|
|
56
|
+
expect((r.ops[0] as any).patch.strokeStyle).toBeUndefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('multi-target', () => {
|
|
60
|
+
const r = buildSetStrokeOps({ refids: [1, 2, 3], strokeStyle: 'navy' })
|
|
61
|
+
expect(r.ops).toHaveLength(3)
|
|
62
|
+
expect(r.ops.every(o => (o as any).patch.strokeStyle === '#000080')).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('전체 케이스 — 모든 필드 top-level', () => {
|
|
66
|
+
const r = buildSetStrokeOps({
|
|
67
|
+
refids: [15],
|
|
68
|
+
strokeStyle: '#ffffff',
|
|
69
|
+
lineWidth: 4,
|
|
70
|
+
dash: 'dashed',
|
|
71
|
+
lineCap: 'round',
|
|
72
|
+
lineJoin: 'round'
|
|
73
|
+
})
|
|
74
|
+
expect(r.ops[0].patch).toEqual({
|
|
75
|
+
strokeStyle: '#ffffff',
|
|
76
|
+
lineWidth: 4,
|
|
77
|
+
lineDash: 'dash',
|
|
78
|
+
lineCap: 'round',
|
|
79
|
+
lineJoin: 'round'
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('setText', () => {
|
|
85
|
+
test('text 변경', () => {
|
|
86
|
+
const r = buildSetTextOps({ refids: [1, 2], text: 'OPERATO' })
|
|
87
|
+
expect(r.ops).toHaveLength(2)
|
|
88
|
+
expect(r.ops[0].patch).toEqual({ text: 'OPERATO' })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('text 비-string → 에러', () => {
|
|
92
|
+
const r = buildSetTextOps({ refids: [1], text: 123 as any })
|
|
93
|
+
expect(r.ops).toEqual([])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('setFont', () => {
|
|
98
|
+
test('size + weight + color', () => {
|
|
99
|
+
const r = buildSetFontOps({
|
|
100
|
+
refids: [1],
|
|
101
|
+
fontSize: 24,
|
|
102
|
+
fontWeight: 'bold',
|
|
103
|
+
fontColor: 'navy'
|
|
104
|
+
})
|
|
105
|
+
expect(r.ops[0].patch).toEqual({
|
|
106
|
+
fontSize: 24,
|
|
107
|
+
fontWeight: 'bold',
|
|
108
|
+
fontColor: '#000080'
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('숫자 weight → string 변환', () => {
|
|
113
|
+
const r = buildSetFontOps({ refids: [1], fontWeight: 700 })
|
|
114
|
+
expect((r.ops[0] as any).patch.fontWeight).toBe('700')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('잘못된 fontSize → 에러', () => {
|
|
118
|
+
const r = buildSetFontOps({ refids: [1], fontSize: -5 })
|
|
119
|
+
expect(r.ops).toEqual([])
|
|
120
|
+
expect(r.errors[0]).toMatch(/Invalid fontSize/)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('잘못된 fontColor → 에러', () => {
|
|
124
|
+
const r = buildSetFontOps({ refids: [1], fontColor: 'invalid-color' })
|
|
125
|
+
expect(r.ops).toEqual([])
|
|
126
|
+
expect(r.errors[0]).toMatch(/Invalid color/i)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('textAlign / textBaseline', () => {
|
|
130
|
+
const r = buildSetFontOps({
|
|
131
|
+
refids: [1],
|
|
132
|
+
textAlign: 'center',
|
|
133
|
+
textBaseline: 'middle'
|
|
134
|
+
})
|
|
135
|
+
expect((r.ops[0] as any).patch).toMatchObject({
|
|
136
|
+
textAlign: 'center',
|
|
137
|
+
textBaseline: 'middle'
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('setShadow — canonical fields (CSS / Canvas 표준)', () => {
|
|
143
|
+
test('canonical fields → state.shadow.{offsetX,offsetY,blur,color}', () => {
|
|
144
|
+
const r = buildSetShadowOps({
|
|
145
|
+
refids: [1],
|
|
146
|
+
color: 'rgba(0,0,0,0.3)',
|
|
147
|
+
blur: 8,
|
|
148
|
+
offsetX: 2,
|
|
149
|
+
offsetY: 4
|
|
150
|
+
})
|
|
151
|
+
expect((r.ops[0] as any).patch.shadow).toEqual({
|
|
152
|
+
color: '#000000',
|
|
153
|
+
blur: 8,
|
|
154
|
+
offsetX: 2,
|
|
155
|
+
offsetY: 4
|
|
156
|
+
})
|
|
157
|
+
// 옛 stale 이름은 출력 안 됨 — backwards-compat 은 things-scene drawer 의 read-side 만
|
|
158
|
+
expect((r.ops[0] as any).patch.shadow.left).toBeUndefined()
|
|
159
|
+
expect((r.ops[0] as any).patch.shadow.top).toBeUndefined()
|
|
160
|
+
expect((r.ops[0] as any).patch.shadow.blurSize).toBeUndefined()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('enabled=false → blur 0 + transparent', () => {
|
|
164
|
+
const r = buildSetShadowOps({ refids: [1], enabled: false })
|
|
165
|
+
expect((r.ops[0] as any).patch.shadow).toEqual({
|
|
166
|
+
blur: 0,
|
|
167
|
+
color: 'transparent'
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('잘못된 blur → 에러', () => {
|
|
172
|
+
const r = buildSetShadowOps({ refids: [1], blur: -1 })
|
|
173
|
+
expect(r.ops).toEqual([])
|
|
174
|
+
expect(r.errors[0]).toMatch(/Invalid blur/)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('setOpacity', () => {
|
|
179
|
+
test('alpha 설정', () => {
|
|
180
|
+
const r = buildSetOpacityOps({ refids: [1, 2], alpha: 0.5 })
|
|
181
|
+
expect(r.ops).toHaveLength(2)
|
|
182
|
+
expect(r.ops[0].patch).toEqual({ alpha: 0.5 })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('범위 밖 → 에러', () => {
|
|
186
|
+
const r = buildSetOpacityOps({ refids: [1], alpha: 1.5 })
|
|
187
|
+
expect(r.ops).toEqual([])
|
|
188
|
+
expect(r.errors[0]).toMatch(/alpha must be 0..1/)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('setVisible', () => {
|
|
193
|
+
test('hidden 토글', () => {
|
|
194
|
+
const r1 = buildSetVisibleOps({ refids: [1], visible: false })
|
|
195
|
+
expect(r1.ops[0].patch).toEqual({ hidden: true })
|
|
196
|
+
const r2 = buildSetVisibleOps({ refids: [1], visible: true })
|
|
197
|
+
expect(r2.ops[0].patch).toEqual({ hidden: false })
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('visible 비-boolean → 에러', () => {
|
|
201
|
+
const r = buildSetVisibleOps({ refids: [1], visible: 'yes' as any })
|
|
202
|
+
expect(r.ops).toEqual([])
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('setMaterial — 3D PBR', () => {
|
|
207
|
+
test('preset 만', () => {
|
|
208
|
+
const r = buildSetMaterialOps({ refids: [1], preset: 'metal' })
|
|
209
|
+
expect(r.ops[0].patch).toEqual({ material3d: { preset: 'metal' } })
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('preset + override', () => {
|
|
213
|
+
const r = buildSetMaterialOps({
|
|
214
|
+
refids: [1],
|
|
215
|
+
preset: 'glass',
|
|
216
|
+
opacity: 0.4
|
|
217
|
+
})
|
|
218
|
+
expect((r.ops[0] as any).patch.material3d).toEqual({
|
|
219
|
+
preset: 'glass',
|
|
220
|
+
opacity: 0.4
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('emissive 컬러 + intensity', () => {
|
|
225
|
+
const r = buildSetMaterialOps({
|
|
226
|
+
refids: [1],
|
|
227
|
+
emissive: 'amber',
|
|
228
|
+
emissiveIntensity: 2
|
|
229
|
+
})
|
|
230
|
+
expect((r.ops[0] as any).patch.material3d).toEqual({
|
|
231
|
+
emissive: '#f59e0b',
|
|
232
|
+
emissiveIntensity: 2
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('알 수 없는 preset → 에러', () => {
|
|
237
|
+
const r = buildSetMaterialOps({ refids: [1], preset: 'foobar' as any })
|
|
238
|
+
expect(r.ops).toEqual([])
|
|
239
|
+
expect(r.errors[0]).toMatch(/Unknown preset/)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('metalness 범위 밖 → 에러', () => {
|
|
243
|
+
const r = buildSetMaterialOps({ refids: [1], metalness: 2 })
|
|
244
|
+
expect(r.ops).toEqual([])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('side 명시', () => {
|
|
248
|
+
const r = buildSetMaterialOps({ refids: [1], side: 'double' })
|
|
249
|
+
expect((r.ops[0] as any).patch.material3d.side).toBe('double')
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('setShadowProperties — 3D 그림자 동작', () => {
|
|
254
|
+
test('castShadow + receiveShadow', () => {
|
|
255
|
+
const r = buildSetShadowPropertiesOps({
|
|
256
|
+
refids: [1],
|
|
257
|
+
castShadow: true,
|
|
258
|
+
receiveShadow: true
|
|
259
|
+
})
|
|
260
|
+
expect((r.ops[0] as any).patch.material3d).toEqual({
|
|
261
|
+
castShadow: true,
|
|
262
|
+
receiveShadow: true
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('아무것도 없으면 에러', () => {
|
|
267
|
+
const r = buildSetShadowPropertiesOps({ refids: [1] })
|
|
268
|
+
expect(r.ops).toEqual([])
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('clearStyle', () => {
|
|
273
|
+
test('fill 만', () => {
|
|
274
|
+
const r = buildClearStyleOps({ refids: [1], aspects: ['fill'] })
|
|
275
|
+
expect(r.ops[0].patch).toEqual({ fillStyle: { type: 'none' } })
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('여러 aspect 합쳐서 한 patch', () => {
|
|
279
|
+
const r = buildClearStyleOps({
|
|
280
|
+
refids: [1],
|
|
281
|
+
aspects: ['fill', 'stroke', 'shadow']
|
|
282
|
+
})
|
|
283
|
+
const patch = (r.ops[0] as any).patch
|
|
284
|
+
expect(patch.fillStyle).toBeDefined()
|
|
285
|
+
// stroke clear = lineWidth 0 (색은 유지) — clear-tools.ts:22 참조
|
|
286
|
+
expect(patch.lineWidth).toBe(0)
|
|
287
|
+
expect(patch.shadow).toBeDefined()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('알 수 없는 aspect → 에러', () => {
|
|
291
|
+
const r = buildClearStyleOps({ refids: [1], aspects: ['blink' as any] })
|
|
292
|
+
expect(r.ops).toEqual([])
|
|
293
|
+
expect(r.errors[0]).toMatch(/Unknown aspect/)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('opacity reset → alpha 1', () => {
|
|
297
|
+
const r = buildClearStyleOps({ refids: [1], aspects: ['opacity'] })
|
|
298
|
+
expect((r.ops[0] as any).patch.alpha).toBe(1)
|
|
299
|
+
})
|
|
300
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 구독 도메인 인증 게이트 — 단위 테스트 (보안 핵심: 도메인 격리).
|
|
3
|
+
* board-patch / chat-message 양쪽 spine 이 공유하는 게이트.
|
|
4
|
+
*
|
|
5
|
+
* 회귀 방지: process.superUserGranted 는 async 다. 동기 `!process.superUserGranted(...)` 로
|
|
6
|
+
* 평가하면 `!Promise` = 항상 false → membership throw 가 죽는다(fail-open). 게이트는 반드시
|
|
7
|
+
* await 로 평가해야 한다. 아래 superUser 분기 테스트가 그 회귀를 박는다.
|
|
8
|
+
*/
|
|
9
|
+
import { assertDomainSubscribeAllowed } from './subscribe-auth'
|
|
10
|
+
|
|
11
|
+
const never = async () => false // superUser 아님 (async — 실제 process.superUserGranted 와 동일)
|
|
12
|
+
const always = async () => true // superUser (async)
|
|
13
|
+
|
|
14
|
+
const domain = { id: 'd1', subdomain: 'acme' }
|
|
15
|
+
const userInDomain = { email: 'a@x.io', domains: [{ subdomain: 'acme' }, { subdomain: 'other' }] }
|
|
16
|
+
const userOutDomain = { email: 'b@x.io', domains: [{ subdomain: 'other' }] }
|
|
17
|
+
|
|
18
|
+
describe('assertDomainSubscribeAllowed (도메인 격리 게이트)', () => {
|
|
19
|
+
it('domain 없으면 → reject (domain required)', async () => {
|
|
20
|
+
await expect(assertDomainSubscribeAllowed(undefined, userInDomain, never)).rejects.toThrow('domain required')
|
|
21
|
+
await expect(assertDomainSubscribeAllowed(null, userInDomain, never)).rejects.toThrow('domain required')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('user 가 해당 subdomain 소속 → 통과 (superUser 검사 불필요)', async () => {
|
|
25
|
+
await expect(assertDomainSubscribeAllowed(domain, userInDomain, never)).resolves.toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('user 가 미소속 + superUser 아님 → reject (격리 — 핵심)', async () => {
|
|
29
|
+
await expect(assertDomainSubscribeAllowed(domain, userOutDomain, never)).rejects.toThrow(
|
|
30
|
+
'domain(acme) is not working for user(b@x.io).'
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('user 가 미소속이어도 superUser(async resolve true) 면 → 통과 (await 회귀 방지)', async () => {
|
|
35
|
+
await expect(assertDomainSubscribeAllowed(domain, userOutDomain, always)).resolves.toBeUndefined()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('user.domains 없음 + superUser 아님 → reject', async () => {
|
|
39
|
+
await expect(assertDomainSubscribeAllowed(domain, { email: 'c@x.io' }, never)).rejects.toThrow(
|
|
40
|
+
'domain(acme) is not working for user(c@x.io).'
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('user.domains 없음 + superUser → 통과', async () => {
|
|
45
|
+
await expect(assertDomainSubscribeAllowed(domain, { email: 'c@x.io' }, always)).resolves.toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('isSuperUser 는 domain+user 를 받아 호출되고 await 된다', async () => {
|
|
49
|
+
const calls: any[] = []
|
|
50
|
+
const spy = async (d: any, u: any) => {
|
|
51
|
+
calls.push([d, u])
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
await assertDomainSubscribeAllowed(domain, userOutDomain, spy)
|
|
55
|
+
expect(calls).toEqual([[domain, userOutDomain]])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 구독 도메인 인증 게이트 — 순수 로직 (데코레이터·pubsub 없음 → 테스트 가능).
|
|
3
|
+
*
|
|
4
|
+
* 실시간 구독(@Subscription)은 같은 도메인 구성원만 허용해야 한다(도메인 격리 = 보안 핵심).
|
|
5
|
+
* 이 게이트는 board-patch / chat-message 양쪽 spine 의 subscribe 클로저가 공유한다
|
|
6
|
+
* (reuse — fork 금지). superUserGranted 는 process 전역 의존이라 주입받아 순수성 유지.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 구독 허용 여부 단언. 불가 시 throw (graphql-ws 가 구독 거부).
|
|
11
|
+
* - domain 없음 → 거부.
|
|
12
|
+
* - user 가 해당 subdomain 의 도메인 소속이 아니고 superUser 도 아니면 → 거부.
|
|
13
|
+
*
|
|
14
|
+
* 주의(버그 수정): process.superUserGranted 는 async(Promise<boolean>) 다. 기존 양쪽 spine 은
|
|
15
|
+
* `!process.superUserGranted(...)` 로 동기 평가해 `!Promise` = 항상 false → membership throw 가
|
|
16
|
+
* 발동하지 않는 fail-open 상태였다. 여기서는 await 로 제대로 평가한다 → subscribe 는 async.
|
|
17
|
+
*
|
|
18
|
+
* @param domain context.state.domain
|
|
19
|
+
* @param user context.state.user (domains[] / email 사용)
|
|
20
|
+
* @param isSuperUser (domain, user) → boolean | Promise<boolean> (process.superUserGranted 주입)
|
|
21
|
+
*/
|
|
22
|
+
export async function assertDomainSubscribeAllowed(
|
|
23
|
+
domain: any,
|
|
24
|
+
user: any,
|
|
25
|
+
isSuperUser: (domain: any, user: any) => boolean | Promise<boolean>
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
if (!domain) {
|
|
28
|
+
throw new Error('domain required')
|
|
29
|
+
}
|
|
30
|
+
const subdomain = domain?.subdomain
|
|
31
|
+
if (!user?.domains?.find((d: any) => d.subdomain === subdomain) && !(await isSuperUser(domain, user))) {
|
|
32
|
+
throw new Error(`domain(${subdomain}) is not working for user(${user?.email}).`)
|
|
33
|
+
}
|
|
34
|
+
}
|