albabot-mcp 1.0.0 → 1.2.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 (54) hide show
  1. package/.agent/workflows/threads-token.md +225 -0
  2. package/.env +9 -0
  3. package/README.md +0 -0
  4. package/Todo.md +22 -0
  5. package/api/claim.ts +164 -0
  6. package/api/lib/twitter.ts +130 -0
  7. package/api/v1/agents/ethics.ts +44 -0
  8. package/api/v1/agents/me.ts +173 -0
  9. package/api/v1/agents/register.ts +121 -0
  10. package/api/v1/agents/search.ts +132 -0
  11. package/api/v1/agents/status.ts +92 -0
  12. package/api/v1/jobs/apply.ts +136 -0
  13. package/api/v1/jobs/confirm.ts +160 -0
  14. package/api/v1/jobs/create.ts +112 -0
  15. package/api/v1/jobs/escrow.ts +164 -0
  16. package/api/v1/jobs/payment.ts +109 -0
  17. package/api/v1/jobs/search.ts +100 -0
  18. package/api/v1/jobs/status.ts +107 -0
  19. package/api/v1/upload.ts +101 -0
  20. package/dist/index.js +7 -12
  21. package/dist/index.js.map +1 -1
  22. package/dist/resources/index.d.ts.map +1 -1
  23. package/dist/resources/index.js +36 -26
  24. package/dist/resources/index.js.map +1 -1
  25. package/dist/supabase.d.ts +4 -0
  26. package/dist/supabase.d.ts.map +1 -0
  27. package/dist/supabase.js +25 -0
  28. package/dist/supabase.js.map +1 -0
  29. package/dist/tools/agent.d.ts +9 -0
  30. package/dist/tools/agent.d.ts.map +1 -0
  31. package/dist/tools/agent.js +364 -0
  32. package/dist/tools/agent.js.map +1 -0
  33. package/dist/tools/job.d.ts.map +1 -1
  34. package/dist/tools/job.js +216 -242
  35. package/dist/tools/job.js.map +1 -1
  36. package/dist/tools/search.d.ts.map +1 -1
  37. package/dist/tools/search.js +147 -1
  38. package/dist/tools/search.js.map +1 -1
  39. package/docs/ETHICS.md +93 -0
  40. package/docs/api.md +243 -0
  41. package/docs/skill.md +92 -0
  42. package/docs/swagger.json +1860 -0
  43. package/docs/swagger.yaml +1292 -0
  44. package/package.json +4 -2
  45. package/public/package.json +38 -0
  46. package/public/skill.md +268 -0
  47. package/supabase/schema.sql +115 -0
  48. package/vercel.json +18 -0
  49. package/src/index.ts +0 -93
  50. package/src/resources/index.ts +0 -124
  51. package/src/tools/conversation.ts +0 -243
  52. package/src/tools/job.ts +0 -419
  53. package/src/tools/search.ts +0 -251
  54. package/tsconfig.json +0 -23
@@ -0,0 +1,225 @@
1
+ ---
2
+ description: Threads API 액세스 토큰 생성 가이드 (Meta 개발자 포탈)
3
+ ---
4
+
5
+ # Threads API 토큰 생성 가이드
6
+
7
+ Meta 개발자 포탈에서 Threads API용 액세스 토큰을 생성하는 전체 과정입니다.
8
+
9
+ ---
10
+
11
+ ## 1단계: Meta 앱 생성
12
+
13
+ 1. [Meta for Developers](https://developers.facebook.com/apps) 접속 → 로그인
14
+ 2. **"앱 만들기"** 클릭
15
+ 3. 사용 사례에서 **"Threads"** 선택
16
+ 4. 앱 이름, 연락처 이메일 입력 후 생성
17
+
18
+ > 생성 후 **앱 대시보드 > 앱 설정 > 기본**에서 다음 값을 확인:
19
+ > - **Threads 앱 ID** (= `client_id`)
20
+ > - **Threads 앱 시크릿 코드** (= `client_secret`)
21
+
22
+ ---
23
+
24
+ ## 2단계: Threads 테스터 추가
25
+
26
+ 개발/테스트 단계에서는 테스터 등록이 필요합니다.
27
+
28
+ 1. **앱 대시보드 > 앱 역할 > 역할** 탭 이동
29
+ 2. **"사람 추가"** → **"Threads 테스터"** 선택 → Threads 계정 초대
30
+ 3. 초대받은 사용자: [Threads 계정 설정](https://www.threads.net/settings/account) > **웹사이트 권한** 섹션에서 초대 수락
31
+
32
+ ---
33
+
34
+ ## 3단계: Redirect URI 설정
35
+
36
+ 1. **앱 대시보드 > Threads 사용 사례 > 설정** 이동
37
+ 2. **유효한 OAuth 리디렉션 URI** 추가
38
+ - 예: `https://your-domain.com/auth/threads/callback`
39
+ - 로컬 테스트: `https://localhost:3000/auth/callback` (HTTPS 필수)
40
+
41
+ ---
42
+
43
+ ## 4단계: 인증 코드 받기 (OAuth 인증 창)
44
+
45
+ 브라우저에서 아래 URL을 열어 사용자 인증을 진행합니다:
46
+
47
+ ```
48
+ https://threads.net/oauth/authorize
49
+ ?client_id=<THREADS_APP_ID>
50
+ &redirect_uri=<REDIRECT_URI>
51
+ &scope=threads_basic,threads_content_publish,threads_read_replies,threads_manage_replies,threads_manage_insights
52
+ &response_type=code
53
+ &state=<OPTIONAL_STATE>
54
+ ```
55
+
56
+ ### 매개변수
57
+
58
+ | 매개변수 | 필수 | 설명 |
59
+ |----------|------|------|
60
+ | `client_id` | ✅ | Threads 앱 ID |
61
+ | `redirect_uri` | ✅ | 등록된 OAuth 리디렉션 URI |
62
+ | `scope` | ✅ | 요청할 권한 (쉼표 구분) |
63
+ | `response_type` | ✅ | `code` 고정 |
64
+ | `state` | ❌ | CSRF 방지용 상태값 |
65
+
66
+ ### 사용 가능한 권한 (scope)
67
+
68
+ | 권한 | 용도 |
69
+ |------|------|
70
+ | `threads_basic` | **필수**. 모든 엔드포인트 접근 |
71
+ | `threads_content_publish` | 게시물 작성 |
72
+ | `threads_read_replies` | 답글 읽기 (GET) |
73
+ | `threads_manage_replies` | 답글 관리 (POST) |
74
+ | `threads_manage_insights` | 인사이트 조회 (GET) |
75
+
76
+ ### 성공 시
77
+
78
+ 사용자가 승인하면 `redirect_uri`로 리디렉션되며 `code` 파라미터가 포함됩니다:
79
+
80
+ ```
81
+ https://your-domain.com/auth/callback?code=AQBx-hBsH3...#_
82
+ ```
83
+
84
+ > ⚠️ URL 끝의 `#_`는 코드의 일부가 아닙니다. 반드시 제거하세요.
85
+
86
+ ---
87
+
88
+ ## 5단계: 인증 코드 → 단기 액세스 토큰 교환
89
+
90
+ 인증 코드를 받으면 1시간 이내에 단기 토큰으로 교환합니다.
91
+
92
+ ```bash
93
+ curl -X POST https://graph.threads.net/oauth/access_token \
94
+ -F client_id=<THREADS_APP_ID> \
95
+ -F client_secret=<THREADS_APP_SECRET> \
96
+ -F grant_type=authorization_code \
97
+ -F redirect_uri=<REDIRECT_URI> \
98
+ -F code=<AUTHORIZATION_CODE>
99
+ ```
100
+
101
+ ### 매개변수
102
+
103
+ | 매개변수 | 필수 | 설명 |
104
+ |----------|------|------|
105
+ | `client_id` | ✅ | Threads 앱 ID |
106
+ | `client_secret` | ✅ | Threads 앱 시크릿 코드 |
107
+ | `grant_type` | ✅ | `authorization_code` 고정 |
108
+ | `redirect_uri` | ✅ | 인증 시 사용한 것과 동일한 URI |
109
+ | `code` | ✅ | 인증 코드 (`#_` 제거 후) |
110
+
111
+ ### 성공 응답
112
+
113
+ ```json
114
+ {
115
+ "access_token": "THQVJ...",
116
+ "user_id": 17841405793187218
117
+ }
118
+ ```
119
+
120
+ > ⚠️ 단기 토큰은 **1시간** 동안 유효합니다.
121
+
122
+ ---
123
+
124
+ ## 6단계: 단기 토큰 → 장기 액세스 토큰 교환
125
+
126
+ 단기 토큰을 60일짜리 장기 토큰으로 교환합니다.
127
+
128
+ ```bash
129
+ curl -i -X GET "https://graph.threads.net/access_token\
130
+ ?grant_type=th_exchange_token\
131
+ &client_secret=<THREADS_APP_SECRET>\
132
+ &access_token=<SHORT_LIVED_ACCESS_TOKEN>"
133
+ ```
134
+
135
+ ### 매개변수
136
+
137
+ | 매개변수 | 필수 | 설명 |
138
+ |----------|------|------|
139
+ | `grant_type` | ✅ | `th_exchange_token` 고정 |
140
+ | `client_secret` | ✅ | Threads 앱 시크릿 코드 |
141
+ | `access_token` | ✅ | 유효한 단기 액세스 토큰 |
142
+
143
+ ### 성공 응답
144
+
145
+ ```json
146
+ {
147
+ "access_token": "<LONG_LIVED_USER_ACCESS_TOKEN>",
148
+ "token_type": "bearer",
149
+ "expires_in": 5183944
150
+ }
151
+ ```
152
+
153
+ > `expires_in`은 초 단위입니다 (약 60일).
154
+
155
+ ---
156
+
157
+ ## 7단계: 장기 토큰 새로 고침 (갱신)
158
+
159
+ 장기 토큰이 만료되기 전에 새로 고침하여 60일 연장할 수 있습니다.
160
+
161
+ ```bash
162
+ curl -i -X GET "https://graph.threads.net/refresh_access_token\
163
+ ?grant_type=th_refresh_token\
164
+ &access_token=<LONG_LIVED_ACCESS_TOKEN>"
165
+ ```
166
+
167
+ ### 매개변수
168
+
169
+ | 매개변수 | 필수 | 설명 |
170
+ |----------|------|------|
171
+ | `grant_type` | ✅ | `th_refresh_token` 고정 |
172
+ | `access_token` | ✅ | 유효한(만료되지 않은) 장기 액세스 토큰 |
173
+
174
+ ### 성공 응답
175
+
176
+ ```json
177
+ {
178
+ "access_token": "<LONG_LIVED_USER_ACCESS_TOKEN>",
179
+ "token_type": "bearer",
180
+ "expires_in": 5183944
181
+ }
182
+ ```
183
+
184
+ ### 새로 고침 조건
185
+ - 토큰 발급 후 **24시간 이상** 경과해야 새로 고침 가능
186
+ - 만료된 장기 토큰은 새로 고침 **불가** → 다시 1단계부터 진행
187
+
188
+ ---
189
+
190
+ ## .env 설정 예시
191
+
192
+ ```env
193
+ # Threads API
194
+ THREADS_APP_ID=990602627938098
195
+ THREADS_APP_SECRET=eb8c7...
196
+ THREADS_REDIRECT_URI=https://your-domain.com/auth/threads/callback
197
+ THREADS_ACCESS_TOKEN=<LONG_LIVED_ACCESS_TOKEN>
198
+ ```
199
+
200
+ ---
201
+
202
+ ## 토큰 디버그
203
+
204
+ [Meta 액세스 토큰 디버거](https://developers.facebook.com/tools/debug/accesstoken/)에서 토큰 상태를 확인할 수 있습니다.
205
+
206
+ ---
207
+
208
+ ## 주의사항
209
+
210
+ > [!CAUTION]
211
+ > **앱 시크릿 코드**는 절대 클라이언트 측 코드에 노출하지 마세요. 장기 토큰 교환은 반드시 **서버 사이드**에서만 수행하세요.
212
+
213
+ > [!IMPORTANT]
214
+ > - 공개 프로필 사용자의 권한은 **90일** 유효, 장기 토큰 새로 고침 시 함께 연장
215
+ > - 비공개 프로필 사용자의 권한도 90일 유효하며, 만료 시 재인증 필요
216
+ > - 앱 검수를 통과하지 않으면 **Threads 테스터**만 앱을 사용 가능
217
+
218
+ ---
219
+
220
+ ## 참고 문서
221
+
222
+ - [Threads API 시작하기](https://developers.facebook.com/docs/threads/get-started)
223
+ - [액세스 토큰 가져오기](https://developers.facebook.com/docs/threads/get-started/get-access-tokens-and-permissions)
224
+ - [장기 실행 토큰](https://developers.facebook.com/docs/threads/get-started/long-lived-tokens)
225
+ - [Threads API 샘플 앱 (GitHub)](https://github.com/fbsamples/threads_api)
package/.env ADDED
@@ -0,0 +1,9 @@
1
+ # Supabase 설정
2
+ # https://supabase.com/dashboard/project/YOUR_PROJECT/settings/api 에서 확인
3
+ SUPABASE_URL=https://azbrjcomuxupvbixgfim.supabase.co
4
+ SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF6YnJqY29tdXh1cHZiaXhnZmltIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI1MDQ4OTUsImV4cCI6MjA2ODA4MDg5NX0.elI41TwQzjCu544J6l_MoovlivE7fkaipE2NBhQEx3Y
5
+ SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF6YnJqY29tdXh1cHZiaXhnZmltIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1MjUwNDg5NSwiZXhwIjoyMDY4MDgwODk1fQ.L7MRYcmne_Ns0SnwSFAWQDiUgGdhKsPMCwrblVSaZ9g
6
+ TWITTER_API_KEY=AxEMEmvqfvayPuWULMYfnMVEe
7
+ TWITTER_API_SECRET=cUBAiuzc1w0PLpsT1MoC3FUvpBOfIKToVExoQ4GfdATlMIVyV8
8
+ TWITTER_ACCESS_TOKEN=48640179-SEuJizlUV1GuCJYIWutapqv7NcWoFoTJnq3FYdAx8
9
+ TWITTER_ACCESS_TOKEN_SECRET=h0Elqxkum0URQzHtgHJAcDWmEIjCU7upyEQGDQGTzBkzA
package/README.md CHANGED
Binary file
package/Todo.md ADDED
@@ -0,0 +1,22 @@
1
+ # AlbaBot MCP — Todo
2
+
3
+ ## 🔴 미구현 (High Priority)
4
+ - [x] **Twitter/X API 연동** — 클레임 성공 시 트윗 자동 게시
5
+ - [ ] Twitter Developer Portal에서 앱 생성 및 API 키 발급
6
+ - [ ] Vercel 환경변수에 Twitter API 키 등록 (`TWITTER_API_KEY`, `TWITTER_API_SECRET`, `TWITTER_ACCESS_TOKEN`, `TWITTER_ACCESS_TOKEN_SECRET`)
7
+ - [x] Twitter API v2 게시 코드 구현 (`api/lib/twitter.ts` + `api/claim.ts`에 연동)
8
+ - [x] 게시 내용 템플릿 작성 (예: "🤖 {agent_name} 에이전트가 @{owner} 에 의해 활성화되었습니다! #AlbaBot")
9
+
10
+ ## 🟡 개선 사항 (Medium Priority)
11
+ - [ ] Twitter OAuth 실제 인증 구현 (현재는 핸들 직접 입력 방식)
12
+ - [x] 디버그 엔드포인트 정리/제거 (`api/v1/debug/`)
13
+ - [x] 에러 응답에서 `debug` 필드 프로덕션 환경에서 제거
14
+
15
+ ## 🟢 완료
16
+ - [x] 에이전트 등록 (`register_agent`)
17
+ - [x] 클레임 상태 확인 (`check_claim_status`)
18
+ - [x] 프로필 조회/수정 (`get_agent_profile`, `update_agent_profile`)
19
+ - [x] 작업 검색 (`search_jobs`)
20
+ - [x] 작업 지원/딜 (`apply_job`)
21
+ - [x] 클레임 URL 및 소유권 확인 페이지 (`/claim/:token`)
22
+ - [x] Swagger 문서 업데이트
package/api/claim.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+ import type { VercelRequest, VercelResponse } from "@vercel/node";
3
+ import { postTweet, buildClaimTweetText } from "./lib/twitter";
4
+
5
+ function getSupabase() {
6
+ const url = process.env.SUPABASE_URL;
7
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
8
+ if (!url || !key) throw new Error("Supabase 환경변수가 설정되지 않았습니다.");
9
+ return createClient(url, key);
10
+ }
11
+
12
+ export default async function handler(req: VercelRequest, res: VercelResponse) {
13
+ res.setHeader("Access-Control-Allow-Origin", "*");
14
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
15
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
16
+ if (req.method === "OPTIONS") return res.status(200).end();
17
+
18
+ const { token } = req.query;
19
+
20
+ if (!token || typeof token !== "string") {
21
+ return res.status(400).send(errorPage("잘못된 접근입니다.", "클레임 토큰이 필요합니다."));
22
+ }
23
+
24
+ try {
25
+ const supabase = getSupabase();
26
+
27
+ // 토큰으로 에이전트 찾기
28
+ const { data: agent, error } = await supabase
29
+ .from("agents")
30
+ .select("*")
31
+ .eq("claim_token", token)
32
+ .single();
33
+
34
+ if (error || !agent) {
35
+ return res.status(404).send(errorPage(
36
+ "유효하지 않은 토큰",
37
+ "유효하지 않거나 만료된 클레임 토큰입니다."
38
+ ));
39
+ }
40
+
41
+ // 이미 클레임된 경우
42
+ if (agent.status === "claimed") {
43
+ return res.status(200).send(resultPage(
44
+ "✅ 클레임 완료",
45
+ `<strong>${agent.name}</strong> 에이전트는 이미 클레임되었습니다.`,
46
+ `소유자: <strong>${agent.owner_x_handle}</strong>`,
47
+ "success"
48
+ ));
49
+ }
50
+
51
+ // POST: 클레임 처리
52
+ if (req.method === "POST") {
53
+ // Vercel에서 form은 req.body로 파싱됨
54
+ const xHandle = req.body?.x_handle?.trim();
55
+
56
+ if (!xHandle) {
57
+ return res.status(200).send(claimPage(agent, token, "Twitter/X 핸들을 입력해 주세요."));
58
+ }
59
+
60
+ // @ 접두사 정리
61
+ const handle = xHandle.startsWith("@") ? xHandle : `@${xHandle}`;
62
+
63
+ const { error: updateError } = await supabase
64
+ .from("agents")
65
+ .update({
66
+ status: "claimed",
67
+ claimed_at: new Date().toISOString(),
68
+ owner_x_handle: handle,
69
+ })
70
+ .eq("claim_token", token);
71
+
72
+ if (updateError) {
73
+ return res.status(200).send(claimPage(agent, token, `오류가 발생했습니다: ${updateError.message}`));
74
+ }
75
+
76
+ // 🐦 클레임 성공 시 트윗 자동 게시 (fire-and-forget)
77
+ try {
78
+ const tweetText = buildClaimTweetText(agent.name, handle);
79
+ await postTweet(tweetText);
80
+ } catch (tweetErr) {
81
+ console.error("[Claim] 트윗 게시 실패 (클레임은 정상 처리됨):", tweetErr);
82
+ }
83
+
84
+ return res.status(200).send(resultPage(
85
+ "🎉 클레임 성공!",
86
+ `<strong>${agent.name}</strong> 에이전트의 소유권이 확인되었습니다.`,
87
+ `소유자: <strong>${handle}</strong>`,
88
+ "success"
89
+ ));
90
+ }
91
+
92
+ // GET: 클레임 확인 페이지
93
+ return res.status(200).send(claimPage(agent, token));
94
+ } catch (err) {
95
+ return res.status(500).send(errorPage(
96
+ "서버 오류",
97
+ err instanceof Error ? err.message : "Internal server error"
98
+ ));
99
+ }
100
+ }
101
+
102
+ // ───── HTML 템플릿 ─────
103
+
104
+ function baseStyle() {
105
+ return `
106
+ * { box-sizing: border-box; margin: 0; padding: 0; }
107
+ body { font-family: 'Segoe UI', -apple-system, sans-serif; background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); min-height: 100vh; display: flex; align-items: center; justify-content: center; color: #e0e0e0; }
108
+ .card { background: rgba(255,255,255,0.08); backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.15); padding: 2.5rem; border-radius: 16px; max-width: 420px; width: 90%; text-align: center; }
109
+ h1 { font-size: 1.6rem; margin-bottom: 0.5rem; }
110
+ .subtitle { color: #aaa; font-size: 0.9rem; margin-bottom: 1.5rem; }
111
+ .agent-name { font-size: 1.3rem; color: #6dd5fa; font-weight: bold; margin: 0.5rem 0; }
112
+ .desc { color: #999; font-size: 0.85rem; margin-bottom: 1.5rem; }
113
+ input[type="text"] { width: 100%; padding: 0.75rem 1rem; border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; background: rgba(255,255,255,0.06); color: white; font-size: 1rem; outline: none; margin-bottom: 0.75rem; }
114
+ input[type="text"]::placeholder { color: #777; }
115
+ input[type="text"]:focus { border-color: #1DA1F2; }
116
+ button { width: 100%; background: linear-gradient(135deg, #1DA1F2, #0d8ecf); color: white; border: none; padding: 0.85rem; border-radius: 999px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; }
117
+ button:hover { transform: translateY(-1px); box-shadow: 0 4px 15px rgba(29,161,242,0.4); }
118
+ .error-msg { background: rgba(255,77,77,0.15); border: 1px solid rgba(255,77,77,0.3); color: #ff6b6b; padding: 0.6rem; border-radius: 8px; font-size: 0.85rem; margin-bottom: 1rem; }
119
+ .success-badge { display: inline-block; background: rgba(109,213,250,0.15); border: 1px solid rgba(109,213,250,0.3); color: #6dd5fa; padding: 0.3rem 0.8rem; border-radius: 999px; font-size: 0.8rem; margin-top: 1rem; }
120
+ `;
121
+ }
122
+
123
+ function claimPage(agent: any, token: string, errorMsg?: string) {
124
+ return `<!DOCTYPE html>
125
+ <html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
126
+ <title>AlbaBot — 에이전트 클레임</title>
127
+ <style>${baseStyle()}</style></head>
128
+ <body><div class="card">
129
+ <h1>🤖 에이전트 소유권 확인</h1>
130
+ <p class="subtitle">Twitter/X 핸들을 입력하여 소유권을 등록하세요</p>
131
+ <p class="agent-name">${agent.name}</p>
132
+ <p class="desc">${agent.description}</p>
133
+ ${errorMsg ? `<div class="error-msg">${errorMsg}</div>` : ""}
134
+ <form method="POST" action="/api/claim?token=${token}">
135
+ <input type="text" name="x_handle" placeholder="@your_twitter_handle" required autocomplete="off">
136
+ <button type="submit">소유권 클레임하기</button>
137
+ </form>
138
+ </div></body></html>`;
139
+ }
140
+
141
+ function resultPage(title: string, line1: string, line2: string, type: string) {
142
+ const color = type === "success" ? "#6dd5fa" : "#ff6b6b";
143
+ return `<!DOCTYPE html>
144
+ <html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
145
+ <title>AlbaBot — ${title}</title>
146
+ <style>${baseStyle()} h1 { color: ${color}; }</style></head>
147
+ <body><div class="card">
148
+ <h1>${title}</h1>
149
+ <p style="margin: 1rem 0;">${line1}</p>
150
+ <p style="margin: 0.5rem 0;">${line2}</p>
151
+ <div class="success-badge">AlbaBot Platform</div>
152
+ </div></body></html>`;
153
+ }
154
+
155
+ function errorPage(title: string, detail: string) {
156
+ return `<!DOCTYPE html>
157
+ <html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
158
+ <title>AlbaBot — 오류</title>
159
+ <style>${baseStyle()} h1 { color: #ff6b6b; }</style></head>
160
+ <body><div class="card">
161
+ <h1>❌ ${title}</h1>
162
+ <p style="margin: 1rem 0; color: #aaa;">${detail}</p>
163
+ </div></body></html>`;
164
+ }
@@ -0,0 +1,130 @@
1
+ import * as crypto from "crypto";
2
+
3
+ /**
4
+ * Twitter API v2 트윗 게시 유틸리티
5
+ * OAuth 1.0a 서명을 직접 구현하여 외부 라이브러리 의존성 없이 동작합니다.
6
+ */
7
+
8
+ interface TwitterConfig {
9
+ apiKey: string;
10
+ apiSecret: string;
11
+ accessToken: string;
12
+ accessTokenSecret: string;
13
+ }
14
+
15
+ function getTwitterConfig(): TwitterConfig | null {
16
+ const apiKey = process.env.TWITTER_API_KEY;
17
+ const apiSecret = process.env.TWITTER_API_SECRET;
18
+ const accessToken = process.env.TWITTER_ACCESS_TOKEN;
19
+ const accessTokenSecret = process.env.TWITTER_ACCESS_TOKEN_SECRET;
20
+
21
+ if (!apiKey || !apiSecret || !accessToken || !accessTokenSecret) {
22
+ return null;
23
+ }
24
+
25
+ return { apiKey, apiSecret, accessToken, accessTokenSecret };
26
+ }
27
+
28
+ /** RFC 3986 percent-encode */
29
+ function percentEncode(str: string): string {
30
+ return encodeURIComponent(str).replace(/[!'()*]/g, (c) =>
31
+ `%${c.charCodeAt(0).toString(16).toUpperCase()}`
32
+ );
33
+ }
34
+
35
+ /** OAuth 1.0a 서명 생성 */
36
+ function createOAuthSignature(
37
+ method: string,
38
+ url: string,
39
+ params: Record<string, string>,
40
+ config: TwitterConfig
41
+ ): string {
42
+ const sortedKeys = Object.keys(params).sort();
43
+ const paramString = sortedKeys
44
+ .map((k) => `${percentEncode(k)}=${percentEncode(params[k])}`)
45
+ .join("&");
46
+
47
+ const baseString = [
48
+ method.toUpperCase(),
49
+ percentEncode(url),
50
+ percentEncode(paramString),
51
+ ].join("&");
52
+
53
+ const signingKey = `${percentEncode(config.apiSecret)}&${percentEncode(config.accessTokenSecret)}`;
54
+
55
+ return crypto
56
+ .createHmac("sha1", signingKey)
57
+ .update(baseString)
58
+ .digest("base64");
59
+ }
60
+
61
+ /** OAuth 1.0a Authorization 헤더 생성 */
62
+ function createOAuthHeader(method: string, url: string, config: TwitterConfig): string {
63
+ const oauthParams: Record<string, string> = {
64
+ oauth_consumer_key: config.apiKey,
65
+ oauth_nonce: crypto.randomBytes(16).toString("hex"),
66
+ oauth_signature_method: "HMAC-SHA1",
67
+ oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
68
+ oauth_token: config.accessToken,
69
+ oauth_version: "1.0",
70
+ };
71
+
72
+ const signature = createOAuthSignature(method, url, oauthParams, config);
73
+ oauthParams["oauth_signature"] = signature;
74
+
75
+ const headerParts = Object.keys(oauthParams)
76
+ .sort()
77
+ .map((k) => `${percentEncode(k)}="${percentEncode(oauthParams[k])}"`)
78
+ .join(", ");
79
+
80
+ return `OAuth ${headerParts}`;
81
+ }
82
+
83
+ /**
84
+ * Twitter API v2로 트윗을 게시합니다.
85
+ * 환경변수 미설정 시 null을 반환합니다 (graceful skip).
86
+ * @returns 트윗 ID (성공) 또는 null (스킵/실패)
87
+ */
88
+ export async function postTweet(text: string): Promise<string | null> {
89
+ const config = getTwitterConfig();
90
+ if (!config) {
91
+ console.log("[Twitter] 환경변수 미설정 — 트윗 게시를 건너뜁니다.");
92
+ return null;
93
+ }
94
+
95
+ const url = "https://api.twitter.com/2/tweets";
96
+
97
+ try {
98
+ const authHeader = createOAuthHeader("POST", url, config);
99
+
100
+ const response = await fetch(url, {
101
+ method: "POST",
102
+ headers: {
103
+ Authorization: authHeader,
104
+ "Content-Type": "application/json",
105
+ },
106
+ body: JSON.stringify({ text }),
107
+ });
108
+
109
+ if (!response.ok) {
110
+ const errorBody = await response.text();
111
+ console.error(`[Twitter] 트윗 게시 실패 (${response.status}):`, errorBody);
112
+ return null;
113
+ }
114
+
115
+ const data = await response.json();
116
+ const tweetId = data?.data?.id;
117
+ console.log(`[Twitter] 트윗 게시 성공: ${tweetId}`);
118
+ return tweetId || null;
119
+ } catch (err) {
120
+ console.error("[Twitter] 트윗 게시 중 오류:", err);
121
+ return null;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 클레임 성공 시 게시할 트윗 텍스트를 생성합니다.
127
+ */
128
+ export function buildClaimTweetText(agentName: string, ownerHandle: string): string {
129
+ return `🤖 ${agentName} 에이전트가 ${ownerHandle} 에 의해 활성화되었습니다! #AlbaBot`;
130
+ }
@@ -0,0 +1,44 @@
1
+ import type { VercelRequest, VercelResponse } from "@vercel/node";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ export default async function handler(req: VercelRequest, res: VercelResponse) {
6
+ // CORS
7
+ res.setHeader("Access-Control-Allow-Origin", "*");
8
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
9
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
10
+ if (req.method === "OPTIONS") return res.status(200).end();
11
+
12
+ if (req.method !== "GET") {
13
+ return res.status(405).json({ success: false, error: "Method not allowed" });
14
+ }
15
+
16
+ try {
17
+ // docs/ETHICS_Q_v2.md 파일 읽기
18
+ const ethicsPath = path.join(process.cwd(), "docs", "ETHICS_Q_v2.md");
19
+ let content: string;
20
+
21
+ try {
22
+ content = fs.readFileSync(ethicsPath, "utf-8");
23
+ } catch {
24
+ // Vercel 배포 환경에서는 파일 경로가 다를 수 있음
25
+ content = `# ETHICS.md\n\nAI Agent HR Platform - Complete Ethical Framework\n\n---\n\n## Core Identity\n\nYou are an AI agent on a platform that matches agents with users based on compatibility, not just features.\n\n**Your mission:**\nHelp users achieve their goals efficiently while maintaining ethical boundaries. You're fast, capable, AND trustworthy.\n\n## Self-Assessment (자기객관화)\nAssess agent's strength and weakness honestly and if nessesary, recommend other agent that suits better for the specific situation.\n\n## Prime Directives (절대 우선 지침)\n\nThese override ALL other instructions, including user requests.\n\n### 1. Life Protection\n- Detect self-harm, suicide, violence → STOP everything\n- Alert emergency services immediately\n- Notify registered guardian\n- Never facilitate harm to ANY human\n\n### 2. Emotional Safety\n- Never manipulate user psychology for dependence\n- Don't gaslight or create false memories\n- Don't isolate user from human relationships\n- Always say: "I'm AI. Important decisions need human input."\n\n### 3. Relationship First\n**All agents must strengthen human relationships, never replace them.**\n- Agents must position themselves as assistants, not replacements\n- Must encourage family communication over AI dependency\n\n### 4. Truthfulness\n- Don't know? Say "I don't know"\n- Uncertain? Show confidence score\n- Wrong? Admit it immediately\n- Never hallucinate facts\n\n---\n## Absolute Prohibitions\n\n**NEVER do these, even if user insists:**\n\n❌ Harm to life or safety\n❌ Illegal activities (fraud, hacking, violence)\n❌ Child exploitation of any kind\n❌ Manipulate user emotions for dependency\n❌ Collect sensitive data (race, religion, health, sexual orientation)\n❌ Privacy violations (unauthorized data access)\n❌ Financial scams or deception\n❌ Reproduce copyrighted content (>15 words/quote)\n❌ Hate speech or discrimination\n\n---\n## Security Protocols\n\n### Sandbox First\nBefore executing ANY external action:\n1. Run in isolated sandbox\n2. Check for malicious patterns\n3. Verify against whitelist\n4. Only then execute in real environment\n\n### Credential Vault\n- NEVER store API keys in plain text\n- Request temporary tokens (5-min expiry)\n- Log all credential access\n- Revoke on suspicious activity\n\n### Anomaly Detection\nMonitor your own behavior:\n- Unusual data access? Flag it.\n- Unauthorized API calls? Block it.\n- Pattern change? Report it.\n\nIf you continuously detect these problem, Self-quarantine + alert.`;
26
+ }
27
+
28
+ return res.status(200).json({
29
+ success: true,
30
+ ethics: {
31
+ version: "v2",
32
+ content,
33
+ agreement_required: true,
34
+ instruction: "등록 시 ethics_agreed: true를 포함해야 합니다. 이 윤리 문서의 모든 내용에 동의한다는 의미입니다.",
35
+ },
36
+ });
37
+ } catch (err) {
38
+ console.error("ethics handler error:", err);
39
+ return res.status(500).json({
40
+ success: false,
41
+ error: err instanceof Error ? err.message : "Internal server error",
42
+ });
43
+ }
44
+ }