@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.
- package/dist/app.d.ts +9 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +147 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/handler.d.ts +19 -0
- package/dist/auth/handler.d.ts.map +1 -0
- package/dist/auth/handler.js +213 -0
- package/dist/auth/handler.js.map +1 -0
- package/dist/auth/session.d.ts +16 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/db/d1/index.d.ts +14 -0
- package/dist/db/d1/index.d.ts.map +1 -0
- package/dist/db/d1/index.js +252 -0
- package/dist/db/d1/index.js.map +1 -0
- package/dist/db/d1/schema.d.ts +610 -0
- package/dist/db/d1/schema.d.ts.map +1 -0
- package/dist/db/d1/schema.js +58 -0
- package/dist/db/d1/schema.js.map +1 -0
- package/dist/db/mysql/index.d.ts +14 -0
- package/dist/db/mysql/index.d.ts.map +1 -0
- package/dist/db/mysql/index.js +248 -0
- package/dist/db/mysql/index.js.map +1 -0
- package/dist/db/mysql/schema.d.ts +562 -0
- package/dist/db/mysql/schema.d.ts.map +1 -0
- package/dist/db/mysql/schema.js +61 -0
- package/dist/db/mysql/schema.js.map +1 -0
- package/dist/db/postgresql/index.d.ts +14 -0
- package/dist/db/postgresql/index.d.ts.map +1 -0
- package/dist/db/postgresql/index.js +246 -0
- package/dist/db/postgresql/index.js.map +1 -0
- package/dist/db/postgresql/schema.d.ts +591 -0
- package/dist/db/postgresql/schema.d.ts.map +1 -0
- package/dist/db/postgresql/schema.js +64 -0
- package/dist/db/postgresql/schema.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/relay/api-proxy.d.ts +10 -0
- package/dist/relay/api-proxy.d.ts.map +1 -0
- package/dist/relay/api-proxy.js +40 -0
- package/dist/relay/api-proxy.js.map +1 -0
- package/dist/runtime/cf/crypto.d.ts +7 -0
- package/dist/runtime/cf/crypto.d.ts.map +1 -0
- package/dist/runtime/cf/crypto.js +48 -0
- package/dist/runtime/cf/crypto.js.map +1 -0
- package/dist/runtime/cf/index.d.ts +20 -0
- package/dist/runtime/cf/index.d.ts.map +1 -0
- package/dist/runtime/cf/index.js +14 -0
- package/dist/runtime/cf/index.js.map +1 -0
- package/dist/runtime/cf/relay.d.ts +11 -0
- package/dist/runtime/cf/relay.d.ts.map +1 -0
- package/dist/runtime/cf/relay.js +57 -0
- package/dist/runtime/cf/relay.js.map +1 -0
- package/dist/runtime/cf/vault.d.ts +7 -0
- package/dist/runtime/cf/vault.d.ts.map +1 -0
- package/dist/runtime/cf/vault.js +68 -0
- package/dist/runtime/cf/vault.js.map +1 -0
- package/dist/runtime/node/crypto.d.ts +6 -0
- package/dist/runtime/node/crypto.d.ts.map +1 -0
- package/dist/runtime/node/crypto.js +26 -0
- package/dist/runtime/node/crypto.js.map +1 -0
- package/dist/runtime/node/index.d.ts +17 -0
- package/dist/runtime/node/index.d.ts.map +1 -0
- package/dist/runtime/node/index.js +14 -0
- package/dist/runtime/node/index.js.map +1 -0
- package/dist/runtime/node/relay.d.ts +6 -0
- package/dist/runtime/node/relay.d.ts.map +1 -0
- package/dist/runtime/node/relay.js +73 -0
- package/dist/runtime/node/relay.js.map +1 -0
- package/dist/runtime/node/vault.d.ts +7 -0
- package/dist/runtime/node/vault.d.ts.map +1 -0
- package/dist/runtime/node/vault.js +41 -0
- package/dist/runtime/node/vault.js.map +1 -0
- package/dist/slack/events.d.ts +15 -0
- package/dist/slack/events.d.ts.map +1 -0
- package/dist/slack/events.js +63 -0
- package/dist/slack/events.js.map +1 -0
- package/dist/slack/oauth.d.ts +13 -0
- package/dist/slack/oauth.d.ts.map +1 -0
- package/dist/slack/oauth.js +90 -0
- package/dist/slack/oauth.js.map +1 -0
- package/dist/slack/provisioner.d.ts +60 -0
- package/dist/slack/provisioner.d.ts.map +1 -0
- package/dist/slack/provisioner.js +156 -0
- package/dist/slack/provisioner.js.map +1 -0
- package/dist/types/crypto.d.ts +15 -0
- package/dist/types/crypto.d.ts.map +1 -0
- package/dist/types/crypto.js +2 -0
- package/dist/types/crypto.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/platform.d.ts +25 -0
- package/dist/types/platform.d.ts.map +1 -0
- package/dist/types/platform.js +2 -0
- package/dist/types/platform.js.map +1 -0
- package/dist/types/relay.d.ts +16 -0
- package/dist/types/relay.d.ts.map +1 -0
- package/dist/types/relay.js +2 -0
- package/dist/types/relay.js.map +1 -0
- package/dist/types/repository.d.ts +78 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/repository.js +6 -0
- package/dist/types/repository.js.map +1 -0
- package/dist/types/vault.d.ts +9 -0
- package/dist/types/vault.d.ts.map +1 -0
- package/dist/types/vault.js +2 -0
- package/dist/types/vault.js.map +1 -0
- package/dist/web/api.d.ts +9 -0
- package/dist/web/api.d.ts.map +1 -0
- package/dist/web/api.js +144 -0
- package/dist/web/api.js.map +1 -0
- package/dist/web/pages.d.ts +4 -0
- package/dist/web/pages.d.ts.map +1 -0
- package/dist/web/pages.js +401 -0
- package/dist/web/pages.js.map +1 -0
- package/dist/web/setup.d.ts +5 -0
- package/dist/web/setup.d.ts.map +1 -0
- package/dist/web/setup.js +208 -0
- package/dist/web/setup.js.map +1 -0
- package/package.json +46 -0
- package/src/app.ts +221 -0
- package/src/auth/handler.ts +343 -0
- package/src/auth/session.ts +89 -0
- package/src/db/d1/index.ts +304 -0
- package/src/db/d1/schema.ts +62 -0
- package/src/db/mysql/index.ts +301 -0
- package/src/db/mysql/schema.ts +78 -0
- package/src/db/postgresql/index.ts +311 -0
- package/src/db/postgresql/schema.ts +82 -0
- package/src/index.ts +21 -0
- package/src/relay/api-proxy.ts +61 -0
- package/src/runtime/cf/crypto.ts +74 -0
- package/src/runtime/cf/index.ts +31 -0
- package/src/runtime/cf/relay.ts +74 -0
- package/src/runtime/cf/vault.ts +99 -0
- package/src/runtime/node/crypto.ts +33 -0
- package/src/runtime/node/index.ts +28 -0
- package/src/runtime/node/relay.ts +98 -0
- package/src/runtime/node/vault.ts +50 -0
- package/src/slack/events.ts +92 -0
- package/src/slack/oauth.ts +127 -0
- package/src/slack/provisioner.ts +256 -0
- package/src/types/crypto.ts +14 -0
- package/src/types/index.ts +14 -0
- package/src/types/platform.ts +31 -0
- package/src/types/relay.ts +16 -0
- package/src/types/repository.ts +93 -0
- package/src/types/vault.ts +8 -0
- package/src/web/api.ts +204 -0
- package/src/web/pages.ts +458 -0
- package/src/web/setup.ts +270 -0
- package/tsconfig.json +19 -0
- 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 & 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 '&';
|
|
195
|
+
case '<':
|
|
196
|
+
return '<';
|
|
197
|
+
case '>':
|
|
198
|
+
return '>';
|
|
199
|
+
case '"':
|
|
200
|
+
return '"';
|
|
201
|
+
case "'":
|
|
202
|
+
return ''';
|
|
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":"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"}
|
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
|
+
}
|