@sena-ai/platform-core 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/app.d.ts +9 -0
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +147 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/auth/handler.d.ts +19 -0
  6. package/dist/auth/handler.d.ts.map +1 -0
  7. package/dist/auth/handler.js +213 -0
  8. package/dist/auth/handler.js.map +1 -0
  9. package/dist/auth/session.d.ts +16 -0
  10. package/dist/auth/session.d.ts.map +1 -0
  11. package/dist/auth/session.js +54 -0
  12. package/dist/auth/session.js.map +1 -0
  13. package/dist/db/d1/index.d.ts +14 -0
  14. package/dist/db/d1/index.d.ts.map +1 -0
  15. package/dist/db/d1/index.js +252 -0
  16. package/dist/db/d1/index.js.map +1 -0
  17. package/dist/db/d1/schema.d.ts +610 -0
  18. package/dist/db/d1/schema.d.ts.map +1 -0
  19. package/dist/db/d1/schema.js +58 -0
  20. package/dist/db/d1/schema.js.map +1 -0
  21. package/dist/db/mysql/index.d.ts +14 -0
  22. package/dist/db/mysql/index.d.ts.map +1 -0
  23. package/dist/db/mysql/index.js +248 -0
  24. package/dist/db/mysql/index.js.map +1 -0
  25. package/dist/db/mysql/schema.d.ts +562 -0
  26. package/dist/db/mysql/schema.d.ts.map +1 -0
  27. package/dist/db/mysql/schema.js +61 -0
  28. package/dist/db/mysql/schema.js.map +1 -0
  29. package/dist/db/postgresql/index.d.ts +14 -0
  30. package/dist/db/postgresql/index.d.ts.map +1 -0
  31. package/dist/db/postgresql/index.js +246 -0
  32. package/dist/db/postgresql/index.js.map +1 -0
  33. package/dist/db/postgresql/schema.d.ts +591 -0
  34. package/dist/db/postgresql/schema.d.ts.map +1 -0
  35. package/dist/db/postgresql/schema.js +64 -0
  36. package/dist/db/postgresql/schema.js.map +1 -0
  37. package/dist/index.d.ts +6 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +3 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/relay/api-proxy.d.ts +10 -0
  42. package/dist/relay/api-proxy.d.ts.map +1 -0
  43. package/dist/relay/api-proxy.js +40 -0
  44. package/dist/relay/api-proxy.js.map +1 -0
  45. package/dist/runtime/cf/crypto.d.ts +7 -0
  46. package/dist/runtime/cf/crypto.d.ts.map +1 -0
  47. package/dist/runtime/cf/crypto.js +48 -0
  48. package/dist/runtime/cf/crypto.js.map +1 -0
  49. package/dist/runtime/cf/index.d.ts +20 -0
  50. package/dist/runtime/cf/index.d.ts.map +1 -0
  51. package/dist/runtime/cf/index.js +14 -0
  52. package/dist/runtime/cf/index.js.map +1 -0
  53. package/dist/runtime/cf/relay.d.ts +11 -0
  54. package/dist/runtime/cf/relay.d.ts.map +1 -0
  55. package/dist/runtime/cf/relay.js +57 -0
  56. package/dist/runtime/cf/relay.js.map +1 -0
  57. package/dist/runtime/cf/vault.d.ts +7 -0
  58. package/dist/runtime/cf/vault.d.ts.map +1 -0
  59. package/dist/runtime/cf/vault.js +68 -0
  60. package/dist/runtime/cf/vault.js.map +1 -0
  61. package/dist/runtime/node/crypto.d.ts +6 -0
  62. package/dist/runtime/node/crypto.d.ts.map +1 -0
  63. package/dist/runtime/node/crypto.js +26 -0
  64. package/dist/runtime/node/crypto.js.map +1 -0
  65. package/dist/runtime/node/index.d.ts +17 -0
  66. package/dist/runtime/node/index.d.ts.map +1 -0
  67. package/dist/runtime/node/index.js +14 -0
  68. package/dist/runtime/node/index.js.map +1 -0
  69. package/dist/runtime/node/relay.d.ts +6 -0
  70. package/dist/runtime/node/relay.d.ts.map +1 -0
  71. package/dist/runtime/node/relay.js +73 -0
  72. package/dist/runtime/node/relay.js.map +1 -0
  73. package/dist/runtime/node/vault.d.ts +7 -0
  74. package/dist/runtime/node/vault.d.ts.map +1 -0
  75. package/dist/runtime/node/vault.js +41 -0
  76. package/dist/runtime/node/vault.js.map +1 -0
  77. package/dist/slack/events.d.ts +15 -0
  78. package/dist/slack/events.d.ts.map +1 -0
  79. package/dist/slack/events.js +63 -0
  80. package/dist/slack/events.js.map +1 -0
  81. package/dist/slack/oauth.d.ts +13 -0
  82. package/dist/slack/oauth.d.ts.map +1 -0
  83. package/dist/slack/oauth.js +90 -0
  84. package/dist/slack/oauth.js.map +1 -0
  85. package/dist/slack/provisioner.d.ts +60 -0
  86. package/dist/slack/provisioner.d.ts.map +1 -0
  87. package/dist/slack/provisioner.js +156 -0
  88. package/dist/slack/provisioner.js.map +1 -0
  89. package/dist/types/crypto.d.ts +15 -0
  90. package/dist/types/crypto.d.ts.map +1 -0
  91. package/dist/types/crypto.js +2 -0
  92. package/dist/types/crypto.js.map +1 -0
  93. package/dist/types/index.d.ts +6 -0
  94. package/dist/types/index.d.ts.map +1 -0
  95. package/dist/types/index.js +2 -0
  96. package/dist/types/index.js.map +1 -0
  97. package/dist/types/platform.d.ts +25 -0
  98. package/dist/types/platform.d.ts.map +1 -0
  99. package/dist/types/platform.js +2 -0
  100. package/dist/types/platform.js.map +1 -0
  101. package/dist/types/relay.d.ts +16 -0
  102. package/dist/types/relay.d.ts.map +1 -0
  103. package/dist/types/relay.js +2 -0
  104. package/dist/types/relay.js.map +1 -0
  105. package/dist/types/repository.d.ts +78 -0
  106. package/dist/types/repository.d.ts.map +1 -0
  107. package/dist/types/repository.js +6 -0
  108. package/dist/types/repository.js.map +1 -0
  109. package/dist/types/vault.d.ts +9 -0
  110. package/dist/types/vault.d.ts.map +1 -0
  111. package/dist/types/vault.js +2 -0
  112. package/dist/types/vault.js.map +1 -0
  113. package/dist/web/api.d.ts +9 -0
  114. package/dist/web/api.d.ts.map +1 -0
  115. package/dist/web/api.js +144 -0
  116. package/dist/web/api.js.map +1 -0
  117. package/dist/web/pages.d.ts +4 -0
  118. package/dist/web/pages.d.ts.map +1 -0
  119. package/dist/web/pages.js +401 -0
  120. package/dist/web/pages.js.map +1 -0
  121. package/dist/web/setup.d.ts +5 -0
  122. package/dist/web/setup.d.ts.map +1 -0
  123. package/dist/web/setup.js +208 -0
  124. package/dist/web/setup.js.map +1 -0
  125. package/package.json +46 -0
  126. package/src/app.ts +221 -0
  127. package/src/auth/handler.ts +343 -0
  128. package/src/auth/session.ts +89 -0
  129. package/src/db/d1/index.ts +304 -0
  130. package/src/db/d1/schema.ts +62 -0
  131. package/src/db/mysql/index.ts +301 -0
  132. package/src/db/mysql/schema.ts +78 -0
  133. package/src/db/postgresql/index.ts +311 -0
  134. package/src/db/postgresql/schema.ts +82 -0
  135. package/src/index.ts +21 -0
  136. package/src/relay/api-proxy.ts +61 -0
  137. package/src/runtime/cf/crypto.ts +74 -0
  138. package/src/runtime/cf/index.ts +31 -0
  139. package/src/runtime/cf/relay.ts +74 -0
  140. package/src/runtime/cf/vault.ts +99 -0
  141. package/src/runtime/node/crypto.ts +33 -0
  142. package/src/runtime/node/index.ts +28 -0
  143. package/src/runtime/node/relay.ts +98 -0
  144. package/src/runtime/node/vault.ts +50 -0
  145. package/src/slack/events.ts +92 -0
  146. package/src/slack/oauth.ts +127 -0
  147. package/src/slack/provisioner.ts +256 -0
  148. package/src/types/crypto.ts +14 -0
  149. package/src/types/index.ts +14 -0
  150. package/src/types/platform.ts +31 -0
  151. package/src/types/relay.ts +16 -0
  152. package/src/types/repository.ts +93 -0
  153. package/src/types/vault.ts +8 -0
  154. package/src/web/api.ts +204 -0
  155. package/src/web/pages.ts +458 -0
  156. package/src/web/setup.ts +270 -0
  157. package/tsconfig.json +19 -0
  158. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,270 @@
1
+ import { Hono } from 'hono'
2
+ import { getCookie } from 'hono/cookie'
3
+ import type { WorkspaceAdminConfigRepository } from '../types/repository.js'
4
+ import type { Vault } from '../types/vault.js'
5
+ import { parseSessionCookieValue } from '../auth/session.js'
6
+
7
+ const DEFAULT_WORKSPACE_ADMIN_CONFIG_ID = 'default'
8
+
9
+ export function createSetupPage(
10
+ workspaceAdminConfig: WorkspaceAdminConfigRepository,
11
+ vault: Vault,
12
+ ) {
13
+ const app = new Hono()
14
+
15
+ app.get('/setup', async (c) => {
16
+ const access = await getSetupAccess(
17
+ workspaceAdminConfig,
18
+ vault,
19
+ getCookie(c, 'sena_session') ?? null,
20
+ )
21
+ if (!access.allowed) {
22
+ return c.redirect('/auth/login')
23
+ }
24
+
25
+ const origin = new URL(c.req.url).origin
26
+ const redirectUrl = `${origin}/auth/callback`
27
+ const currentClientId = access.currentConfig?.slackClientId ?? ''
28
+
29
+ return c.html(`<!DOCTYPE html>
30
+ <html lang="ko">
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>초기 설정 - Sena Platform</title>
35
+ <script src="https://cdn.tailwindcss.com"></script>
36
+ <style>
37
+ body { font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; }
38
+ </style>
39
+ </head>
40
+ <body class="bg-gray-50 min-h-screen flex items-center justify-center">
41
+ <main class="w-full max-w-lg px-4">
42
+ <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
43
+ <div class="text-center mb-6">
44
+ <h1 class="text-2xl font-bold text-gray-900">Sena Platform 초기 설정</h1>
45
+ <p class="text-gray-500 text-sm mt-2">플랫폼 접근을 Slack 로그인으로 보호하려면 로그인 앱 정보를 먼저 넣어야 해요.</p>
46
+ </div>
47
+
48
+ <div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
49
+ <h3 class="text-sm font-semibold text-blue-800 mb-2">Slack 로그인 앱 준비</h3>
50
+ <ol class="text-sm text-blue-700 space-y-1 list-decimal list-inside">
51
+ <li><a href="https://api.slack.com/apps" target="_blank" rel="noopener" class="underline">api.slack.com/apps</a>에서 로그인용 Slack 앱을 만드세요.</li>
52
+ <li>OAuth &amp; Permissions → Redirect URL에 아래 값을 추가하세요.<br>
53
+ <code class="bg-blue-100 px-1 py-0.5 rounded text-xs">${redirectUrl}</code>
54
+ </li>
55
+ <li>OpenID Connect scopes로 <code class="bg-blue-100 px-1 py-0.5 rounded text-xs">openid, profile, email</code> 을 추가하세요.</li>
56
+ <li>Basic Information에서 Client ID / Client Secret을 복사하세요.</li>
57
+ </ol>
58
+ </div>
59
+
60
+ <form id="setup-form" class="space-y-5">
61
+ <div>
62
+ <label for="slack-client-id" class="block text-sm font-medium text-gray-700 mb-1">Slack Login App Client ID <span class="text-red-500">*</span></label>
63
+ <input type="text" id="slack-client-id" name="slackClientId" required
64
+ placeholder="1234567890.1234567890"
65
+ value="${escapeHtml(currentClientId)}"
66
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
67
+ </div>
68
+
69
+ <div>
70
+ <label for="slack-client-secret" class="block text-sm font-medium text-gray-700 mb-1">Slack Login App Client Secret <span class="text-red-500">*</span></label>
71
+ <input type="password" id="slack-client-secret" name="slackClientSecret" required
72
+ placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
73
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
74
+ </div>
75
+
76
+ <div id="setup-msg" class="hidden p-3 rounded-lg text-sm"></div>
77
+
78
+ <button type="submit" id="setup-btn"
79
+ class="w-full py-2.5 bg-indigo-600 text-white font-medium rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
80
+ 저장하고 Slack 로그인 연결하기
81
+ </button>
82
+ </form>
83
+ </div>
84
+ </main>
85
+
86
+ <script>
87
+ document.getElementById('setup-form').addEventListener('submit', async function (event) {
88
+ event.preventDefault();
89
+ const button = document.getElementById('setup-btn');
90
+ const message = document.getElementById('setup-msg');
91
+ button.disabled = true;
92
+ button.textContent = '저장 중...';
93
+ message.classList.add('hidden');
94
+
95
+ try {
96
+ const payload = {
97
+ slackClientId: document.getElementById('slack-client-id').value.trim(),
98
+ slackClientSecret: document.getElementById('slack-client-secret').value.trim(),
99
+ };
100
+
101
+ if (!payload.slackClientId || !payload.slackClientSecret) {
102
+ throw new Error('Client ID와 Client Secret을 모두 입력해주세요.');
103
+ }
104
+
105
+ const response = await fetch('/api/setup', {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify(payload),
109
+ });
110
+ const data = await response.json();
111
+ if (!response.ok || !data.ok) {
112
+ throw new Error(data.error || '설정 저장에 실패했습니다.');
113
+ }
114
+
115
+ window.location.href = data.redirectTo || '/auth/login';
116
+ } catch (error) {
117
+ message.className = 'p-3 rounded-lg text-sm bg-red-50 border border-red-200 text-red-700';
118
+ message.textContent = error.message;
119
+ message.classList.remove('hidden');
120
+ button.disabled = false;
121
+ button.textContent = '저장하고 Slack 로그인 연결하기';
122
+ }
123
+ });
124
+ </script>
125
+ </body>
126
+ </html>`)
127
+ })
128
+
129
+ app.post('/api/setup', async (c) => {
130
+ const access = await getSetupAccess(
131
+ workspaceAdminConfig,
132
+ vault,
133
+ getCookie(c, 'sena_session') ?? null,
134
+ )
135
+ if (!access.allowed) {
136
+ return c.json(
137
+ { error: '이미 설정된 워크스페이스라 Slack 로그인 후에만 수정할 수 있어요.' },
138
+ 401,
139
+ )
140
+ }
141
+
142
+ const body = await c.req.json<{
143
+ slackClientId?: string
144
+ slackClientSecret?: string
145
+ }>()
146
+
147
+ const slackClientId = (body.slackClientId ?? '').trim()
148
+ const slackClientSecret = (body.slackClientSecret ?? '').trim()
149
+ if (!slackClientId || !slackClientSecret) {
150
+ return c.json(
151
+ { error: 'slackClientId와 slackClientSecret은 필수예요.' },
152
+ 400,
153
+ )
154
+ }
155
+
156
+ const targetWorkspaceId =
157
+ access.session?.user.slackTeamId ?? DEFAULT_WORKSPACE_ADMIN_CONFIG_ID
158
+ const existing =
159
+ (await workspaceAdminConfig.findByWorkspaceId(targetWorkspaceId)) ??
160
+ (targetWorkspaceId !== DEFAULT_WORKSPACE_ADMIN_CONFIG_ID
161
+ ? await workspaceAdminConfig.findByWorkspaceId(
162
+ DEFAULT_WORKSPACE_ADMIN_CONFIG_ID,
163
+ )
164
+ : null)
165
+
166
+ await workspaceAdminConfig.upsert({
167
+ workspaceId: targetWorkspaceId,
168
+ slackClientId,
169
+ slackClientSecretEnc: await vault.encrypt(slackClientSecret),
170
+ dCookieEnc: existing?.dCookieEnc ?? null,
171
+ xoxcTokenEnc: existing?.xoxcTokenEnc ?? null,
172
+ workspaceDomain: existing?.workspaceDomain ?? null,
173
+ updatedByUserId: access.session?.user.slackUserId ?? existing?.updatedByUserId ?? null,
174
+ })
175
+
176
+ return c.json({
177
+ ok: true,
178
+ redirectTo: access.session ? '/' : '/auth/login',
179
+ })
180
+ })
181
+
182
+ return app
183
+ }
184
+
185
+ async function getSetupAccess(
186
+ workspaceAdminConfig: WorkspaceAdminConfigRepository,
187
+ vault: Vault,
188
+ rawSession: string | null,
189
+ ) {
190
+ const configuredConfigs = await listConfiguredSlackLoginConfigs(
191
+ workspaceAdminConfig,
192
+ )
193
+ const configuredTeamIds = configuredConfigs
194
+ .map((config) => config.workspaceId)
195
+ .filter((workspaceId) => workspaceId !== DEFAULT_WORKSPACE_ADMIN_CONFIG_ID)
196
+
197
+ if (configuredConfigs.length === 0) {
198
+ return {
199
+ allowed: true,
200
+ session: null,
201
+ currentConfig: null,
202
+ }
203
+ }
204
+
205
+ const session = rawSession
206
+ ? await parseSessionCookieValue(vault, rawSession)
207
+ : null
208
+
209
+ if (!session) {
210
+ return {
211
+ allowed: false,
212
+ session: null,
213
+ currentConfig: null,
214
+ }
215
+ }
216
+
217
+ if (
218
+ configuredTeamIds.length > 0 &&
219
+ !configuredTeamIds.includes(session.user.slackTeamId)
220
+ ) {
221
+ return {
222
+ allowed: false,
223
+ session,
224
+ currentConfig: null,
225
+ }
226
+ }
227
+
228
+ const currentConfig =
229
+ configuredConfigs.find(
230
+ (config) => config.workspaceId === session.user.slackTeamId,
231
+ ) ?? configuredConfigs[0]
232
+
233
+ return {
234
+ allowed: true,
235
+ session,
236
+ currentConfig,
237
+ }
238
+ }
239
+
240
+ async function listConfiguredSlackLoginConfigs(
241
+ workspaceAdminConfig: WorkspaceAdminConfigRepository,
242
+ ) {
243
+ const configs = await workspaceAdminConfig.findAll()
244
+ return configs.filter(
245
+ (config) =>
246
+ typeof config.slackClientId === 'string' &&
247
+ config.slackClientId.trim().length > 0 &&
248
+ typeof config.slackClientSecretEnc === 'string' &&
249
+ config.slackClientSecretEnc.length > 0,
250
+ )
251
+ }
252
+
253
+ function escapeHtml(value: string): string {
254
+ return value.replace(/[&<>"']/g, (char) => {
255
+ switch (char) {
256
+ case '&':
257
+ return '&amp;'
258
+ case '<':
259
+ return '&lt;'
260
+ case '>':
261
+ return '&gt;'
262
+ case '"':
263
+ return '&quot;'
264
+ case "'":
265
+ return '&#39;'
266
+ default:
267
+ return char
268
+ }
269
+ })
270
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "lib": ["ES2022"],
16
+ "types": ["node", "@cloudflare/workers-types"]
17
+ },
18
+ "include": ["src"]
19
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/app.ts","./src/index.ts","./src/auth/handler.ts","./src/auth/session.ts","./src/db/d1/index.ts","./src/db/d1/schema.ts","./src/db/mysql/index.ts","./src/db/mysql/schema.ts","./src/db/postgresql/index.ts","./src/db/postgresql/schema.ts","./src/relay/api-proxy.ts","./src/runtime/cf/crypto.ts","./src/runtime/cf/index.ts","./src/runtime/cf/relay.ts","./src/runtime/cf/vault.ts","./src/runtime/node/crypto.ts","./src/runtime/node/index.ts","./src/runtime/node/relay.ts","./src/runtime/node/vault.ts","./src/slack/events.ts","./src/slack/oauth.ts","./src/slack/provisioner.ts","./src/types/crypto.ts","./src/types/index.ts","./src/types/platform.ts","./src/types/relay.ts","./src/types/repository.ts","./src/types/vault.ts","./src/web/api.ts","./src/web/pages.ts","./src/web/setup.ts"],"version":"5.9.3"}