@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,208 @@
1
+ import { Hono } from 'hono';
2
+ import { getCookie } from 'hono/cookie';
3
+ import { parseSessionCookieValue } from '../auth/session.js';
4
+ const DEFAULT_WORKSPACE_ADMIN_CONFIG_ID = 'default';
5
+ export function createSetupPage(workspaceAdminConfig, vault) {
6
+ const app = new Hono();
7
+ app.get('/setup', async (c) => {
8
+ const access = await getSetupAccess(workspaceAdminConfig, vault, getCookie(c, 'sena_session') ?? null);
9
+ if (!access.allowed) {
10
+ return c.redirect('/auth/login');
11
+ }
12
+ const origin = new URL(c.req.url).origin;
13
+ const redirectUrl = `${origin}/auth/callback`;
14
+ const currentClientId = access.currentConfig?.slackClientId ?? '';
15
+ return c.html(`<!DOCTYPE html>
16
+ <html lang="ko">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>초기 설정 - Sena Platform</title>
21
+ <script src="https://cdn.tailwindcss.com"></script>
22
+ <style>
23
+ body { font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; }
24
+ </style>
25
+ </head>
26
+ <body class="bg-gray-50 min-h-screen flex items-center justify-center">
27
+ <main class="w-full max-w-lg px-4">
28
+ <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
29
+ <div class="text-center mb-6">
30
+ <h1 class="text-2xl font-bold text-gray-900">Sena Platform 초기 설정</h1>
31
+ <p class="text-gray-500 text-sm mt-2">플랫폼 접근을 Slack 로그인으로 보호하려면 로그인 앱 정보를 먼저 넣어야 해요.</p>
32
+ </div>
33
+
34
+ <div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
35
+ <h3 class="text-sm font-semibold text-blue-800 mb-2">Slack 로그인 앱 준비</h3>
36
+ <ol class="text-sm text-blue-700 space-y-1 list-decimal list-inside">
37
+ <li><a href="https://api.slack.com/apps" target="_blank" rel="noopener" class="underline">api.slack.com/apps</a>에서 로그인용 Slack 앱을 만드세요.</li>
38
+ <li>OAuth &amp; Permissions → Redirect URL에 아래 값을 추가하세요.<br>
39
+ <code class="bg-blue-100 px-1 py-0.5 rounded text-xs">${redirectUrl}</code>
40
+ </li>
41
+ <li>OpenID Connect scopes로 <code class="bg-blue-100 px-1 py-0.5 rounded text-xs">openid, profile, email</code> 을 추가하세요.</li>
42
+ <li>Basic Information에서 Client ID / Client Secret을 복사하세요.</li>
43
+ </ol>
44
+ </div>
45
+
46
+ <form id="setup-form" class="space-y-5">
47
+ <div>
48
+ <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>
49
+ <input type="text" id="slack-client-id" name="slackClientId" required
50
+ placeholder="1234567890.1234567890"
51
+ value="${escapeHtml(currentClientId)}"
52
+ 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">
53
+ </div>
54
+
55
+ <div>
56
+ <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>
57
+ <input type="password" id="slack-client-secret" name="slackClientSecret" required
58
+ placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
59
+ 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">
60
+ </div>
61
+
62
+ <div id="setup-msg" class="hidden p-3 rounded-lg text-sm"></div>
63
+
64
+ <button type="submit" id="setup-btn"
65
+ 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">
66
+ 저장하고 Slack 로그인 연결하기
67
+ </button>
68
+ </form>
69
+ </div>
70
+ </main>
71
+
72
+ <script>
73
+ document.getElementById('setup-form').addEventListener('submit', async function (event) {
74
+ event.preventDefault();
75
+ const button = document.getElementById('setup-btn');
76
+ const message = document.getElementById('setup-msg');
77
+ button.disabled = true;
78
+ button.textContent = '저장 중...';
79
+ message.classList.add('hidden');
80
+
81
+ try {
82
+ const payload = {
83
+ slackClientId: document.getElementById('slack-client-id').value.trim(),
84
+ slackClientSecret: document.getElementById('slack-client-secret').value.trim(),
85
+ };
86
+
87
+ if (!payload.slackClientId || !payload.slackClientSecret) {
88
+ throw new Error('Client ID와 Client Secret을 모두 입력해주세요.');
89
+ }
90
+
91
+ const response = await fetch('/api/setup', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify(payload),
95
+ });
96
+ const data = await response.json();
97
+ if (!response.ok || !data.ok) {
98
+ throw new Error(data.error || '설정 저장에 실패했습니다.');
99
+ }
100
+
101
+ window.location.href = data.redirectTo || '/auth/login';
102
+ } catch (error) {
103
+ message.className = 'p-3 rounded-lg text-sm bg-red-50 border border-red-200 text-red-700';
104
+ message.textContent = error.message;
105
+ message.classList.remove('hidden');
106
+ button.disabled = false;
107
+ button.textContent = '저장하고 Slack 로그인 연결하기';
108
+ }
109
+ });
110
+ </script>
111
+ </body>
112
+ </html>`);
113
+ });
114
+ app.post('/api/setup', async (c) => {
115
+ const access = await getSetupAccess(workspaceAdminConfig, vault, getCookie(c, 'sena_session') ?? null);
116
+ if (!access.allowed) {
117
+ return c.json({ error: '이미 설정된 워크스페이스라 Slack 로그인 후에만 수정할 수 있어요.' }, 401);
118
+ }
119
+ const body = await c.req.json();
120
+ const slackClientId = (body.slackClientId ?? '').trim();
121
+ const slackClientSecret = (body.slackClientSecret ?? '').trim();
122
+ if (!slackClientId || !slackClientSecret) {
123
+ return c.json({ error: 'slackClientId와 slackClientSecret은 필수예요.' }, 400);
124
+ }
125
+ const targetWorkspaceId = access.session?.user.slackTeamId ?? DEFAULT_WORKSPACE_ADMIN_CONFIG_ID;
126
+ const existing = (await workspaceAdminConfig.findByWorkspaceId(targetWorkspaceId)) ??
127
+ (targetWorkspaceId !== DEFAULT_WORKSPACE_ADMIN_CONFIG_ID
128
+ ? await workspaceAdminConfig.findByWorkspaceId(DEFAULT_WORKSPACE_ADMIN_CONFIG_ID)
129
+ : null);
130
+ await workspaceAdminConfig.upsert({
131
+ workspaceId: targetWorkspaceId,
132
+ slackClientId,
133
+ slackClientSecretEnc: await vault.encrypt(slackClientSecret),
134
+ dCookieEnc: existing?.dCookieEnc ?? null,
135
+ xoxcTokenEnc: existing?.xoxcTokenEnc ?? null,
136
+ workspaceDomain: existing?.workspaceDomain ?? null,
137
+ updatedByUserId: access.session?.user.slackUserId ?? existing?.updatedByUserId ?? null,
138
+ });
139
+ return c.json({
140
+ ok: true,
141
+ redirectTo: access.session ? '/' : '/auth/login',
142
+ });
143
+ });
144
+ return app;
145
+ }
146
+ async function getSetupAccess(workspaceAdminConfig, vault, rawSession) {
147
+ const configuredConfigs = await listConfiguredSlackLoginConfigs(workspaceAdminConfig);
148
+ const configuredTeamIds = configuredConfigs
149
+ .map((config) => config.workspaceId)
150
+ .filter((workspaceId) => workspaceId !== DEFAULT_WORKSPACE_ADMIN_CONFIG_ID);
151
+ if (configuredConfigs.length === 0) {
152
+ return {
153
+ allowed: true,
154
+ session: null,
155
+ currentConfig: null,
156
+ };
157
+ }
158
+ const session = rawSession
159
+ ? await parseSessionCookieValue(vault, rawSession)
160
+ : null;
161
+ if (!session) {
162
+ return {
163
+ allowed: false,
164
+ session: null,
165
+ currentConfig: null,
166
+ };
167
+ }
168
+ if (configuredTeamIds.length > 0 &&
169
+ !configuredTeamIds.includes(session.user.slackTeamId)) {
170
+ return {
171
+ allowed: false,
172
+ session,
173
+ currentConfig: null,
174
+ };
175
+ }
176
+ const currentConfig = configuredConfigs.find((config) => config.workspaceId === session.user.slackTeamId) ?? configuredConfigs[0];
177
+ return {
178
+ allowed: true,
179
+ session,
180
+ currentConfig,
181
+ };
182
+ }
183
+ async function listConfiguredSlackLoginConfigs(workspaceAdminConfig) {
184
+ const configs = await workspaceAdminConfig.findAll();
185
+ return configs.filter((config) => typeof config.slackClientId === 'string' &&
186
+ config.slackClientId.trim().length > 0 &&
187
+ typeof config.slackClientSecretEnc === 'string' &&
188
+ config.slackClientSecretEnc.length > 0);
189
+ }
190
+ function escapeHtml(value) {
191
+ return value.replace(/[&<>"']/g, (char) => {
192
+ switch (char) {
193
+ case '&':
194
+ return '&amp;';
195
+ case '<':
196
+ return '&lt;';
197
+ case '>':
198
+ return '&gt;';
199
+ case '"':
200
+ return '&quot;';
201
+ case "'":
202
+ return '&#39;';
203
+ default:
204
+ return char;
205
+ }
206
+ });
207
+ }
208
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/web/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAGvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,MAAM,iCAAiC,GAAG,SAAS,CAAA;AAEnD,MAAM,UAAU,eAAe,CAC7B,oBAAoD,EACpD,KAAY;IAEZ,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IAEtB,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC5B,MAAM,MAAM,GAAG,MAAM,cAAc,CACjC,oBAAoB,EACpB,KAAK,EACL,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,IAAI,IAAI,CACrC,CAAA;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAA;QAClC,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAA;QACxC,MAAM,WAAW,GAAG,GAAG,MAAM,gBAAgB,CAAA;QAC7C,MAAM,eAAe,GAAG,MAAM,CAAC,aAAa,EAAE,aAAa,IAAI,EAAE,CAAA;QAEjE,OAAO,CAAC,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;oEAwBkD,WAAW;;;;;;;;;;;;0BAYrD,UAAU,CAAC,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6D7C,CAAC,CAAA;IACP,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACjC,MAAM,MAAM,GAAG,MAAM,cAAc,CACjC,oBAAoB,EACpB,KAAK,EACL,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,IAAI,IAAI,CACrC,CAAA;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,CAAC,IAAI,CACX,EAAE,KAAK,EAAE,yCAAyC,EAAE,EACpD,GAAG,CACJ,CAAA;QACH,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAGzB,CAAA;QAEJ,MAAM,aAAa,GAAG,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACvD,MAAM,iBAAiB,GAAG,CAAC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC/D,IAAI,CAAC,aAAa,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzC,OAAO,CAAC,CAAC,IAAI,CACX,EAAE,KAAK,EAAE,yCAAyC,EAAE,EACpD,GAAG,CACJ,CAAA;QACH,CAAC;QAED,MAAM,iBAAiB,GACrB,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,IAAI,iCAAiC,CAAA;QACvE,MAAM,QAAQ,GACZ,CAAC,MAAM,oBAAoB,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;YACjE,CAAC,iBAAiB,KAAK,iCAAiC;gBACtD,CAAC,CAAC,MAAM,oBAAoB,CAAC,iBAAiB,CAC1C,iCAAiC,CAClC;gBACH,CAAC,CAAC,IAAI,CAAC,CAAA;QAEX,MAAM,oBAAoB,CAAC,MAAM,CAAC;YAChC,WAAW,EAAE,iBAAiB;YAC9B,aAAa;YACb,oBAAoB,EAAE,MAAM,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC;YAC5D,UAAU,EAAE,QAAQ,EAAE,UAAU,IAAI,IAAI;YACxC,YAAY,EAAE,QAAQ,EAAE,YAAY,IAAI,IAAI;YAC5C,eAAe,EAAE,QAAQ,EAAE,eAAe,IAAI,IAAI;YAClD,eAAe,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,IAAI,QAAQ,EAAE,eAAe,IAAI,IAAI;SACvF,CAAC,CAAA;QAEF,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,IAAI;YACR,UAAU,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa;SACjD,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,KAAK,UAAU,cAAc,CAC3B,oBAAoD,EACpD,KAAY,EACZ,UAAyB;IAEzB,MAAM,iBAAiB,GAAG,MAAM,+BAA+B,CAC7D,oBAAoB,CACrB,CAAA;IACD,MAAM,iBAAiB,GAAG,iBAAiB;SACxC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC;SACnC,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,WAAW,KAAK,iCAAiC,CAAC,CAAA;IAE7E,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnC,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,IAAI;YACb,aAAa,EAAE,IAAI;SACpB,CAAA;IACH,CAAC;IAED,MAAM,OAAO,GAAG,UAAU;QACxB,CAAC,CAAC,MAAM,uBAAuB,CAAC,KAAK,EAAE,UAAU,CAAC;QAClD,CAAC,CAAC,IAAI,CAAA;IAER,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,IAAI;YACb,aAAa,EAAE,IAAI;SACpB,CAAA;IACH,CAAC;IAED,IACE,iBAAiB,CAAC,MAAM,GAAG,CAAC;QAC5B,CAAC,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,EACrD,CAAC;QACD,OAAO;YACL,OAAO,EAAE,KAAK;YACd,OAAO;YACP,aAAa,EAAE,IAAI;SACpB,CAAA;IACH,CAAC;IAED,MAAM,aAAa,GACjB,iBAAiB,CAAC,IAAI,CACpB,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,WAAW,CAC5D,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAA;IAE3B,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO;QACP,aAAa;KACd,CAAA;AACH,CAAC;AAED,KAAK,UAAU,+BAA+B,CAC5C,oBAAoD;IAEpD,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,OAAO,EAAE,CAAA;IACpD,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,MAAM,EAAE,EAAE,CACT,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ;QACxC,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QACtC,OAAO,MAAM,CAAC,oBAAoB,KAAK,QAAQ;QAC/C,MAAM,CAAC,oBAAoB,CAAC,MAAM,GAAG,CAAC,CACzC,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE;QACxC,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,GAAG;gBACN,OAAO,OAAO,CAAA;YAChB,KAAK,GAAG;gBACN,OAAO,MAAM,CAAA;YACf,KAAK,GAAG;gBACN,OAAO,MAAM,CAAA;YACf,KAAK,GAAG;gBACN,OAAO,QAAQ,CAAA;YACjB,KAAK,GAAG;gBACN,OAAO,OAAO,CAAA;YAChB;gBACE,OAAO,IAAI,CAAA;QACf,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@sena-ai/platform-core",
3
+ "version": "1.4.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./db/mysql": "./src/db/mysql/index.ts",
8
+ "./db/postgresql": "./src/db/postgresql/index.ts",
9
+ "./db/d1": "./src/db/d1/index.ts",
10
+ "./node": "./src/runtime/node/index.ts",
11
+ "./cf": "./src/runtime/cf/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "hono": "^4.7.4"
15
+ },
16
+ "peerDependencies": {
17
+ "drizzle-orm": ">=0.38.0",
18
+ "mysql2": ">=3.0.0",
19
+ "postgres": ">=3.0.0"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "drizzle-orm": {
23
+ "optional": true
24
+ },
25
+ "mysql2": {
26
+ "optional": true
27
+ },
28
+ "postgres": {
29
+ "optional": true
30
+ }
31
+ },
32
+ "devDependencies": {
33
+ "@cloudflare/workers-types": "^4.20250321.0",
34
+ "@types/node": "^22.12.0",
35
+ "@types/uuid": "^10.0.0",
36
+ "drizzle-orm": "^0.38.4",
37
+ "mysql2": "^3.12.0",
38
+ "postgres": "^3.4.5",
39
+ "typescript": "^5.8.0",
40
+ "uuid": "^11.1.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -b",
44
+ "typecheck": "tsc --noEmit"
45
+ }
46
+ }
package/src/app.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { Hono } from 'hono'
2
+ import { logger } from 'hono/logger'
3
+ import type { Platform, AppConfig } from './types/platform.js'
4
+ import { createApiProxy } from './relay/api-proxy.js'
5
+ import { createSlackEventsHandler } from './slack/events.js'
6
+ import { createOAuthHandler } from './slack/oauth.js'
7
+ import { createProvisioner, type Provisioner } from './slack/provisioner.js'
8
+ import { createAuthHandler, createAuthMiddleware } from './auth/handler.js'
9
+ import { createWebApi } from './web/api.js'
10
+ import { createPages } from './web/pages.js'
11
+ import { createSetupPage } from './web/setup.js'
12
+
13
+ export interface CreateAppResult {
14
+ app: Hono
15
+ provisioner: Provisioner
16
+ }
17
+
18
+ /**
19
+ * Create the main Hono application with all routes wired up.
20
+ * Works in both Node.js and CF Workers environments.
21
+ */
22
+ function generateInstallScript(): string {
23
+ return `#!/bin/sh
24
+ set -e
25
+
26
+ # sena-ai bot bootstrap script
27
+ # Usage: curl -fsSL <platform-url>/install.sh | sh -s -- --name "봇이름" --bot-username "lily-bot" --connect-key "cpk_..." --platform-url "https://..."
28
+
29
+ BOT_NAME=""
30
+ BOT_USERNAME=""
31
+ CONNECT_KEY=""
32
+ PLATFORM_URL=""
33
+
34
+ while [ \$# -gt 0 ]; do
35
+ case "\$1" in
36
+ --name) BOT_NAME="\$2"; shift 2;;
37
+ --bot-username) BOT_USERNAME="\$2"; shift 2;;
38
+ --connect-key) CONNECT_KEY="\$2"; shift 2;;
39
+ --platform-url) PLATFORM_URL="\$2"; shift 2;;
40
+ *) echo "Unknown option: \$1"; exit 1;;
41
+ esac
42
+ done
43
+
44
+ if [ -z "\$BOT_NAME" ] || [ -z "\$BOT_USERNAME" ] || [ -z "\$CONNECT_KEY" ] || [ -z "\$PLATFORM_URL" ]; then
45
+ echo "Error: --name, --bot-username, --connect-key, --platform-url are all required."
46
+ exit 1
47
+ fi
48
+
49
+ DIR_NAME="\$BOT_USERNAME"
50
+
51
+ echo "🤖 Setting up bot: \$BOT_NAME"
52
+ echo " Directory: ./\$DIR_NAME"
53
+ echo ""
54
+
55
+ # Download template from GitHub
56
+ echo "📦 Downloading bot template..."
57
+ TMPDIR_DL=\$(mktemp -d)
58
+ curl -fsSL https://github.com/unlimiting-studio/sena-ai/archive/refs/heads/main.tar.gz -o "\$TMPDIR_DL/repo.tar.gz"
59
+ tar xzf "\$TMPDIR_DL/repo.tar.gz" -C "\$TMPDIR_DL"
60
+
61
+ # Copy template directory
62
+ cp -r "\$TMPDIR_DL/sena-ai-main/templates/bot-starter" "\$DIR_NAME"
63
+ rm -rf "\$TMPDIR_DL"
64
+
65
+ cd "\$DIR_NAME"
66
+
67
+ # Escape special characters for sed replacement
68
+ escape_sed() {
69
+ printf '%s' "\$1" | sed 's/[&/\\]/\\&/g'
70
+ }
71
+
72
+ # Customize package.json name
73
+ ESCAPED_DIR=\$(escape_sed "\$DIR_NAME")
74
+ sed -i.bak "s/\\"sena-bot\\"/\\"\$ESCAPED_DIR\\"/" package.json && rm -f package.json.bak
75
+
76
+ # Replace bot name placeholder in sena.config.ts
77
+ ESCAPED_NAME=\$(escape_sed "\$BOT_NAME")
78
+ sed -i.bak "s/%%BOT_NAME%%/\$ESCAPED_NAME/" sena.config.ts && rm -f sena.config.ts.bak
79
+
80
+ # Create .env from template
81
+ ESCAPED_KEY=\$(escape_sed "\$CONNECT_KEY")
82
+ sed -e "s/%%CONNECT_KEY%%/\$ESCAPED_KEY/" -e "s|%%PLATFORM_URL%%|\$PLATFORM_URL|" .env.template > .env
83
+ rm -f .env.template
84
+
85
+ echo ""
86
+ echo "✅ Bot scaffolding complete!"
87
+ echo ""
88
+ echo "Next steps:"
89
+ echo " cd \$DIR_NAME"
90
+ echo " pnpm install"
91
+ echo " npx sena start"
92
+ echo ""
93
+ `
94
+ }
95
+
96
+ export function createApp(
97
+ platform: Platform,
98
+ config: AppConfig,
99
+ ): CreateAppResult {
100
+ const app = new Hono()
101
+ app.use('*', logger())
102
+
103
+ const provisioner = createProvisioner(
104
+ platform.bots,
105
+ platform.configTokens,
106
+ platform.vault,
107
+ config.platformBaseUrl,
108
+ )
109
+
110
+ const authMiddleware = createAuthMiddleware(
111
+ platform.workspaceAdminConfig,
112
+ platform.vault,
113
+ )
114
+
115
+ // Serve bootstrap script at /install.sh
116
+ app.get('/install.sh', (c) => {
117
+ c.header('Content-Type', 'text/plain; charset=utf-8')
118
+ return c.body(generateInstallScript())
119
+ })
120
+
121
+ // Health check
122
+ app.get('/health', (c) =>
123
+ c.json({
124
+ ok: true,
125
+ connectedBots: platform.relay.connectedBots().length,
126
+ ts: new Date().toISOString(),
127
+ }),
128
+ )
129
+
130
+ // SSE/WebSocket relay stream endpoint
131
+ app.get('/relay/stream', async (c) => {
132
+ const connectKey =
133
+ c.req.header('x-connect-key') || c.req.query('connect_key')
134
+ if (!connectKey) {
135
+ return c.json({ error: 'missing connect_key' }, 401)
136
+ }
137
+
138
+ const bot = await platform.bots.findByConnectKeyAndStatus(
139
+ connectKey,
140
+ 'active',
141
+ )
142
+ if (!bot) {
143
+ return c.json({ error: 'invalid connect_key or bot not active' }, 401)
144
+ }
145
+
146
+ return platform.relay.handleStream(c, bot.id, connectKey)
147
+ })
148
+
149
+ // Slack API proxy
150
+ app.route('/', createApiProxy(platform.bots, platform.vault))
151
+
152
+ // Slack events
153
+ app.route(
154
+ '/',
155
+ createSlackEventsHandler(
156
+ platform.bots,
157
+ platform.vault,
158
+ platform.relay,
159
+ platform.crypto,
160
+ ),
161
+ )
162
+
163
+ // Bot installation OAuth
164
+ app.route(
165
+ '/',
166
+ createOAuthHandler(
167
+ platform.bots,
168
+ platform.vault,
169
+ platform.crypto,
170
+ platform.oauthStates,
171
+ ),
172
+ )
173
+
174
+ // Slack login setup + auth
175
+ app.route('/', createSetupPage(platform.workspaceAdminConfig, platform.vault))
176
+ app.route(
177
+ '/',
178
+ createAuthHandler(
179
+ platform.workspaceAdminConfig,
180
+ platform.oauthStates,
181
+ platform.crypto,
182
+ platform.vault,
183
+ { platformBaseUrl: config.platformBaseUrl },
184
+ ),
185
+ )
186
+
187
+ // Protect all subsequent web/API/admin routes.
188
+ app.use('/', authMiddleware.requireAuth)
189
+ app.use('/bots/*', authMiddleware.requireAuth)
190
+ app.use('/api/*', authMiddleware.requireAuth)
191
+ app.use('/admin/*', authMiddleware.requireAuth)
192
+
193
+ // Web API
194
+ app.route(
195
+ '/',
196
+ createWebApi(
197
+ platform.bots,
198
+ provisioner,
199
+ platform.crypto,
200
+ config.workspaceId,
201
+ ),
202
+ )
203
+
204
+ // Web UI pages
205
+ app.route('/', createPages(platform.bots, config.platformBaseUrl))
206
+
207
+ // Admin endpoints
208
+ app.get('/admin/bots', async (c) => {
209
+ const allBots = await platform.bots.findAllSummary()
210
+ return c.json({ bots: allBots })
211
+ })
212
+
213
+ app.get('/admin/connections', (c) => {
214
+ return c.json({
215
+ connected: platform.relay.connectedBots(),
216
+ count: platform.relay.connectedBots().length,
217
+ })
218
+ })
219
+
220
+ return { app, provisioner }
221
+ }