openkbs 0.0.67 → 0.0.70
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/README.md +1 -0
- package/elastic/README.md +1 -1
- package/elastic/functions.md +5 -5
- package/elastic/pulse.md +2 -2
- package/package.json +2 -2
- package/scripts/deploy.js +68 -0
- package/src/actions.js +7 -0
- package/templates/.claude/skills/openkbs/SKILL.md +37 -8
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/README.md +55 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/app/instructions.txt +40 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/app/settings.json +41 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/openkbs.json +3 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/actions.js +141 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/handler.js +32 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/memoryHelpers.js +91 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onCronjob.js +105 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onPublicAPIRequest.js +165 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onRequest.js +2 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onResponse.js +2 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.js +74 -0
- package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.json +3 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/index.mjs +228 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/package.json +7 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/index.mjs +287 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/package.json +10 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/settings.json +4 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/openkbs.json +16 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/site/index.html +658 -0
- package/templates/.claude/skills/openkbs/examples/nodejs-demo/site/settings.json +4 -0
- package/templates/.claude/skills/openkbs/patterns/cronjob-batch-processing.md +278 -0
- package/templates/.claude/skills/openkbs/patterns/cronjob-monitoring.md +341 -0
- package/templates/.claude/skills/openkbs/patterns/file-upload.md +205 -0
- package/templates/.claude/skills/openkbs/patterns/image-generation.md +139 -0
- package/templates/.claude/skills/openkbs/patterns/memory-system.md +264 -0
- package/templates/.claude/skills/openkbs/patterns/public-api-item-proxy.md +254 -0
- package/templates/.claude/skills/openkbs/patterns/scheduled-tasks.md +157 -0
- package/templates/.claude/skills/openkbs/patterns/telegram-webhook.md +424 -0
- package/templates/.claude/skills/openkbs/patterns/telegram.md +222 -0
- package/templates/.claude/skills/openkbs/patterns/vectordb-archive.md +231 -0
- package/templates/.claude/skills/openkbs/patterns/video-generation.md +145 -0
- package/templates/.claude/skills/openkbs/patterns/web-publishing.md +257 -0
- package/templates/.claude/skills/openkbs/reference/backend-sdk.md +13 -2
- package/templates/.claude/skills/openkbs/reference/elastic-services.md +61 -29
- package/templates/platform/README.md +15 -50
- package/templates/platform/agents/assistant/app/icon.png +0 -0
- package/templates/platform/agents/assistant/app/instructions.txt +9 -21
- package/templates/platform/agents/assistant/app/settings.json +11 -15
- package/templates/platform/agents/assistant/src/Events/actions.js +31 -62
- package/templates/platform/agents/assistant/src/Events/handler.js +54 -0
- package/templates/platform/agents/assistant/src/Events/onRequest.js +3 -0
- package/templates/platform/agents/assistant/src/Events/onRequest.json +5 -0
- package/templates/platform/agents/assistant/src/Events/onResponse.js +2 -40
- package/templates/platform/agents/assistant/src/Events/onResponse.json +4 -2
- package/templates/platform/agents/assistant/src/Frontend/contentRender.js +17 -16
- package/templates/platform/agents/assistant/src/Frontend/contentRender.json +1 -1
- package/templates/platform/functions/api/index.mjs +17 -23
- package/templates/platform/functions/api/package.json +4 -0
- package/templates/platform/openkbs.json +7 -2
- package/templates/platform/site/index.html +18 -19
- package/version.json +3 -3
- package/templates/.claude/skills/openkbs/examples/ai-copywriter-agent/scripts/run_job.js +0 -26
- package/templates/.claude/skills/openkbs/examples/ai-copywriter-agent/scripts/utils/agent_client.js +0 -265
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getAgentSetting, setMemoryValue, cleanupExpiredMemory } from './memoryHelpers.js';
|
|
2
|
+
|
|
3
|
+
async function shouldExecute() {
|
|
4
|
+
try {
|
|
5
|
+
// Check sleep mode
|
|
6
|
+
const sleepUntil = await getAgentSetting('agent_sleepUntil');
|
|
7
|
+
if (sleepUntil) {
|
|
8
|
+
if (new Date(sleepUntil) > new Date()) {
|
|
9
|
+
return { execute: false, reason: 'sleeping' };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Check pulse interval
|
|
14
|
+
const pulseInterval = (await getAgentSetting('agent_pulseInterval')) || 1;
|
|
15
|
+
const currentMinute = new Date().getMinutes();
|
|
16
|
+
|
|
17
|
+
if (currentMinute % pulseInterval !== 0) {
|
|
18
|
+
return { execute: false, reason: 'pulse_interval' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { execute: true, interval: pulseInterval };
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return { execute: true, interval: 1 };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const handler = async (event) => {
|
|
28
|
+
try {
|
|
29
|
+
// Cleanup expired memory
|
|
30
|
+
await cleanupExpiredMemory();
|
|
31
|
+
|
|
32
|
+
// Check if should execute
|
|
33
|
+
const status = await shouldExecute();
|
|
34
|
+
if (!status.execute) {
|
|
35
|
+
return { success: true, skipped: true, reason: status.reason };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build monitoring message
|
|
39
|
+
const message = [];
|
|
40
|
+
|
|
41
|
+
message.push({
|
|
42
|
+
type: "text",
|
|
43
|
+
text: "PROCESS_MONITORING_CHECK"
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// === ADD YOUR DATA SOURCES HERE ===
|
|
47
|
+
// Example: Fetch from API
|
|
48
|
+
// const data = await axios.get('https://api.example.com/status');
|
|
49
|
+
// message.push({ type: "text", text: `API Status: ${data.status}` });
|
|
50
|
+
|
|
51
|
+
// Example: Add images
|
|
52
|
+
// message.push({ type: "image_url", image_url: { url: "https://..." } });
|
|
53
|
+
|
|
54
|
+
// Inject reference images from memory
|
|
55
|
+
try {
|
|
56
|
+
const imageMemories = await openkbs.fetchItems({
|
|
57
|
+
beginsWith: 'memory_with_image_',
|
|
58
|
+
limit: 20,
|
|
59
|
+
field: 'itemId'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (imageMemories?.items?.length > 0) {
|
|
63
|
+
message.push({
|
|
64
|
+
type: "text",
|
|
65
|
+
text: `\n📸 REFERENCE IMAGES (${imageMemories.items.length})`
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for (const item of imageMemories.items) {
|
|
69
|
+
const value = item.item?.body?.value;
|
|
70
|
+
if (value?.imageUrl) {
|
|
71
|
+
message.push({
|
|
72
|
+
type: "text",
|
|
73
|
+
text: `\n🏷️ ${value.description || 'Reference'}`
|
|
74
|
+
});
|
|
75
|
+
message.push({
|
|
76
|
+
type: "image_url",
|
|
77
|
+
image_url: { url: value.imageUrl }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error('Failed to fetch reference images:', e.message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create chat
|
|
87
|
+
const now = new Date();
|
|
88
|
+
const timeStr = now.toLocaleTimeString('en-GB', {
|
|
89
|
+
hour: '2-digit',
|
|
90
|
+
minute: '2-digit'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await openkbs.chats({
|
|
94
|
+
chatTitle: `Monitoring - ${timeStr}`,
|
|
95
|
+
message: JSON.stringify(message)
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return { success: true, timestamp: now.toISOString() };
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { success: false, error: error.message };
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
handler.CRON_SCHEDULE = "* * * * *";
|
package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onPublicAPIRequest.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setAgentSetting,
|
|
3
|
+
getAgentSetting,
|
|
4
|
+
storeTelegramMessage,
|
|
5
|
+
getTelegramMessage,
|
|
6
|
+
deleteTelegramMessage
|
|
7
|
+
} from './memoryHelpers.js';
|
|
8
|
+
|
|
9
|
+
const AUTONOMOUS_CHAT_RULES = `**Autonomous Chat Rules:**
|
|
10
|
+
This chat has NO USER listening. ONLY output commands.
|
|
11
|
+
To communicate: Use sendToTelegramChannel command.
|
|
12
|
+
Plain text "END" to finish.`;
|
|
13
|
+
|
|
14
|
+
const BOT_TOKEN = '{{secrets.telegramBotToken}}';
|
|
15
|
+
|
|
16
|
+
async function getChannelId() {
|
|
17
|
+
const secretsId = '{{secrets.telegramChannelID}}';
|
|
18
|
+
if (secretsId && !secretsId.includes('{{')) return secretsId;
|
|
19
|
+
return await getAgentSetting('agent_telegramChannelID');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const handler = async ({ payload, queryStringParameters, headers }) => {
|
|
23
|
+
try {
|
|
24
|
+
let CHANNEL_ID = await getChannelId();
|
|
25
|
+
|
|
26
|
+
// Webhook setup
|
|
27
|
+
if (queryStringParameters?.setupTelegramWebhook === 'true') {
|
|
28
|
+
const existing = await getAgentSetting('agent_telegramWebhookSetup');
|
|
29
|
+
if (existing) {
|
|
30
|
+
return { ok: false, error: 'Already configured', setupDate: existing };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SECRET_TOKEN = crypto.createHash('sha256')
|
|
34
|
+
.update(BOT_TOKEN).digest('hex').substring(0, 32);
|
|
35
|
+
|
|
36
|
+
const WEBHOOK_URL = `https://chat.openkbs.com/publicAPIRequest?kbId=${openkbs.kbId}&source=telegram`;
|
|
37
|
+
|
|
38
|
+
await axios.post(`https://api.telegram.org/bot${BOT_TOKEN}/deleteWebhook`);
|
|
39
|
+
const response = await axios.post(`https://api.telegram.org/bot${BOT_TOKEN}/setWebhook`, {
|
|
40
|
+
url: WEBHOOK_URL,
|
|
41
|
+
allowed_updates: ['message', 'channel_post', 'edited_message', 'edited_channel_post'],
|
|
42
|
+
drop_pending_updates: true,
|
|
43
|
+
secret_token: SECRET_TOKEN
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.data.ok) throw new Error(response.data.description);
|
|
47
|
+
|
|
48
|
+
const setupDate = new Date().toISOString();
|
|
49
|
+
await setAgentSetting('agent_telegramWebhookSetup', setupDate);
|
|
50
|
+
|
|
51
|
+
return { ok: true, webhookUrl: WEBHOOK_URL, setupDate };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Webhook removal
|
|
55
|
+
if (queryStringParameters?.removeTelegramWebhook === 'true') {
|
|
56
|
+
const existing = await getAgentSetting('agent_telegramWebhookSetup');
|
|
57
|
+
if (existing) {
|
|
58
|
+
return { ok: false, error: 'Remove internally first' };
|
|
59
|
+
}
|
|
60
|
+
await axios.post(`https://api.telegram.org/bot${BOT_TOKEN}/deleteWebhook`);
|
|
61
|
+
return { ok: true, message: 'Removed' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate secret token
|
|
65
|
+
const expectedToken = crypto.createHash('sha256')
|
|
66
|
+
.update(BOT_TOKEN).digest('hex').substring(0, 32);
|
|
67
|
+
const receivedToken = headers?.[
|
|
68
|
+
Object.keys(headers || {}).find(k => k.toLowerCase() === 'x-telegram-bot-api-secret-token')
|
|
69
|
+
];
|
|
70
|
+
if (receivedToken && receivedToken !== expectedToken) {
|
|
71
|
+
return { ok: false, error: 'Invalid token' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { message, channel_post, edited_message, edited_channel_post } = payload;
|
|
75
|
+
|
|
76
|
+
// Handle channel post
|
|
77
|
+
if (channel_post) {
|
|
78
|
+
const text = channel_post.text || channel_post.caption || '';
|
|
79
|
+
const messageId = channel_post.message_id;
|
|
80
|
+
const chatId = channel_post.chat.id;
|
|
81
|
+
|
|
82
|
+
if (!CHANNEL_ID) {
|
|
83
|
+
await setAgentSetting('agent_telegramChannelID', chatId.toString());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const existing = await getTelegramMessage(messageId);
|
|
87
|
+
if (existing) return { ok: true, duplicate: true };
|
|
88
|
+
|
|
89
|
+
const senderName = channel_post.from?.username ||
|
|
90
|
+
channel_post.author_signature ||
|
|
91
|
+
'Unknown';
|
|
92
|
+
|
|
93
|
+
await storeTelegramMessage(messageId, {
|
|
94
|
+
text: text.substring(0, 50000),
|
|
95
|
+
date: channel_post.date,
|
|
96
|
+
from: senderName,
|
|
97
|
+
chatId
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const timeStr = new Date(channel_post.date * 1000)
|
|
101
|
+
.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
102
|
+
|
|
103
|
+
await openkbs.chats({
|
|
104
|
+
chatTitle: `TG: ${senderName} - ${timeStr}`,
|
|
105
|
+
message: `PROCESS_TELEGRAM_MESSAGE from ${senderName}\n\n"${text}"\n\n${AUTONOMOUS_CHAT_RULES}`
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return { ok: true, processed: 'channel_post' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle direct message
|
|
112
|
+
if (message) {
|
|
113
|
+
const text = message.text || '';
|
|
114
|
+
const messageId = message.message_id;
|
|
115
|
+
const userName = message.from.username || message.from.first_name;
|
|
116
|
+
|
|
117
|
+
const existing = await getTelegramMessage(messageId);
|
|
118
|
+
if (existing) return { ok: true, duplicate: true };
|
|
119
|
+
|
|
120
|
+
await storeTelegramMessage(messageId, {
|
|
121
|
+
text: text.substring(0, 20000),
|
|
122
|
+
date: message.date,
|
|
123
|
+
from: userName,
|
|
124
|
+
chatId: message.chat.id
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await openkbs.chats({
|
|
128
|
+
chatTitle: `TG DM: ${userName}`,
|
|
129
|
+
message: `TELEGRAM DM from ${userName}\n\n"${text}"\n\n${AUTONOMOUS_CHAT_RULES}`
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { ok: true, processed: 'message' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle edits
|
|
136
|
+
if (edited_channel_post || edited_message) {
|
|
137
|
+
const edited = edited_channel_post || edited_message;
|
|
138
|
+
const messageId = edited.message_id;
|
|
139
|
+
const newText = edited.text;
|
|
140
|
+
|
|
141
|
+
const existing = await getTelegramMessage(messageId);
|
|
142
|
+
if (!existing) return { ok: true, note: 'Not found' };
|
|
143
|
+
|
|
144
|
+
const itemId = `telegram_${messageId.toString().padStart(12, '0')}`;
|
|
145
|
+
|
|
146
|
+
if (newText?.trim().toLowerCase().startsWith('_delete')) {
|
|
147
|
+
await deleteTelegramMessage(itemId);
|
|
148
|
+
return { ok: true, action: 'deleted' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await storeTelegramMessage(messageId, {
|
|
152
|
+
...existing,
|
|
153
|
+
text: newText,
|
|
154
|
+
edited: true
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return { ok: true, action: 'edited' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { ok: true, message: 'Not handled' };
|
|
161
|
+
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { ok: true, error: error.message };
|
|
164
|
+
}
|
|
165
|
+
};
|
package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const Header = () => {
|
|
2
|
+
const [status, setStatus] = React.useState(null);
|
|
3
|
+
|
|
4
|
+
React.useEffect(() => {
|
|
5
|
+
const fetchStatus = async () => {
|
|
6
|
+
try {
|
|
7
|
+
const [sleepUntil, pulseInterval] = await Promise.all([
|
|
8
|
+
openkbs.items({ action: 'getItem', itemId: 'agent_sleepUntil' }),
|
|
9
|
+
openkbs.items({ action: 'getItem', itemId: 'agent_pulseInterval' })
|
|
10
|
+
]);
|
|
11
|
+
setStatus({
|
|
12
|
+
sleeping: sleepUntil?.item?.body?.value,
|
|
13
|
+
interval: pulseInterval?.item?.body?.value || 1
|
|
14
|
+
});
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
};
|
|
17
|
+
fetchStatus();
|
|
18
|
+
const interval = setInterval(fetchStatus, 30000);
|
|
19
|
+
return () => clearInterval(interval);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
if (!status) return null;
|
|
23
|
+
|
|
24
|
+
const isSleeping = status.sleeping && new Date(status.sleeping) > new Date();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div style={{
|
|
28
|
+
padding: '8px 16px',
|
|
29
|
+
background: isSleeping ? '#ff9800' : '#4caf50',
|
|
30
|
+
color: 'white',
|
|
31
|
+
fontSize: '12px',
|
|
32
|
+
display: 'flex',
|
|
33
|
+
justifyContent: 'space-between'
|
|
34
|
+
}}>
|
|
35
|
+
<span>{isSleeping ? '😴 SLEEPING' : '🟢 ACTIVE'}</span>
|
|
36
|
+
<span>Pulse: every {status.interval} min</span>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const onRenderChatMessage = (params) => {
|
|
42
|
+
const { content } = params;
|
|
43
|
+
|
|
44
|
+
// Try to parse as JSON
|
|
45
|
+
let data;
|
|
46
|
+
try {
|
|
47
|
+
data = typeof content === 'string' ? JSON.parse(content) : content;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return null; // Use default rendering
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle command results
|
|
53
|
+
if (data?.type) {
|
|
54
|
+
const isError = data.type.includes('ERROR');
|
|
55
|
+
return (
|
|
56
|
+
<div style={{
|
|
57
|
+
padding: '12px',
|
|
58
|
+
margin: '8px 0',
|
|
59
|
+
borderRadius: '8px',
|
|
60
|
+
background: isError ? '#ffebee' : '#e8f5e9',
|
|
61
|
+
border: `1px solid ${isError ? '#ef5350' : '#66bb6a'}`
|
|
62
|
+
}}>
|
|
63
|
+
<strong>{data.type}</strong>
|
|
64
|
+
{data.error && <div style={{ color: '#c62828' }}>{data.error}</div>}
|
|
65
|
+
{data.itemId && <div>Item: {data.itemId}</div>}
|
|
66
|
+
{data.messageId && <div>Message ID: {data.messageId}</div>}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
window.contentRender = { Header, onRenderChatMessage };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
const { Client } = pg;
|
|
4
|
+
|
|
5
|
+
const db = new Client({ connectionString: process.env.DATABASE_URL });
|
|
6
|
+
let dbConnected = false;
|
|
7
|
+
|
|
8
|
+
async function connectDB() {
|
|
9
|
+
if (!dbConnected) {
|
|
10
|
+
await db.connect();
|
|
11
|
+
dbConnected = true;
|
|
12
|
+
|
|
13
|
+
// Create users table with private_channel for secure messaging
|
|
14
|
+
await db.query(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
16
|
+
id SERIAL PRIMARY KEY,
|
|
17
|
+
name VARCHAR(255) NOT NULL,
|
|
18
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
19
|
+
password VARCHAR(255) NOT NULL,
|
|
20
|
+
private_channel VARCHAR(64) UNIQUE NOT NULL,
|
|
21
|
+
avatar_color VARCHAR(7) DEFAULT '#007bff',
|
|
22
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
23
|
+
)
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
// Add private_channel column if missing (migration)
|
|
27
|
+
await db.query(`
|
|
28
|
+
ALTER TABLE users ADD COLUMN IF NOT EXISTS private_channel VARCHAR(64) UNIQUE
|
|
29
|
+
`);
|
|
30
|
+
await db.query(`
|
|
31
|
+
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_color VARCHAR(7) DEFAULT '#007bff'
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate unpredictable channel ID for secure private messaging
|
|
37
|
+
function generatePrivateChannel() {
|
|
38
|
+
return crypto.randomBytes(32).toString('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Generate random avatar color
|
|
42
|
+
function generateAvatarColor() {
|
|
43
|
+
const colors = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#00bcd4', '#009688', '#4caf50', '#ff9800', '#ff5722'];
|
|
44
|
+
return colors[Math.floor(Math.random() * colors.length)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a Pulse token for WebSocket authentication
|
|
49
|
+
* Calls lambda-kb to get a signed JWT token
|
|
50
|
+
* Uses OPENKBS_API_KEY for authentication
|
|
51
|
+
*/
|
|
52
|
+
async function getPulseToken(userId) {
|
|
53
|
+
const kbId = process.env.OPENKBS_KB_ID || process.env.PULSE_KB_ID;
|
|
54
|
+
const apiKey = process.env.OPENKBS_API_KEY;
|
|
55
|
+
|
|
56
|
+
if (!kbId) {
|
|
57
|
+
console.log('OPENKBS_KB_ID not set, skipping pulse token');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!apiKey) {
|
|
62
|
+
console.log('OPENKBS_API_KEY not set, skipping pulse token');
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch('https://kb.openkbs.com', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
action: 'createPulseToken',
|
|
72
|
+
kbId,
|
|
73
|
+
apiKey,
|
|
74
|
+
userId: String(userId)
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
if (data.error) {
|
|
80
|
+
console.error('Pulse token error:', data.error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return data;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error('Failed to get pulse token:', e);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function handler(event) {
|
|
91
|
+
const headers = {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
'Access-Control-Allow-Origin': '*',
|
|
94
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Handle OPTIONS preflight
|
|
98
|
+
if (event.requestContext?.http?.method === 'OPTIONS') {
|
|
99
|
+
return { statusCode: 200, headers, body: '' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await connectDB();
|
|
104
|
+
|
|
105
|
+
const body = JSON.parse(event.body || '{}');
|
|
106
|
+
const { action, email, password, name } = body;
|
|
107
|
+
|
|
108
|
+
if (action === 'register') {
|
|
109
|
+
// Check if user exists
|
|
110
|
+
const existing = await db.query('SELECT id FROM users WHERE email = $1', [email]);
|
|
111
|
+
if (existing.rows.length > 0) {
|
|
112
|
+
return {
|
|
113
|
+
statusCode: 400,
|
|
114
|
+
headers,
|
|
115
|
+
body: JSON.stringify({ error: 'Email already registered' })
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Generate unique private channel for secure messaging
|
|
120
|
+
const privateChannel = generatePrivateChannel();
|
|
121
|
+
const avatarColor = generateAvatarColor();
|
|
122
|
+
|
|
123
|
+
// Create user with private channel
|
|
124
|
+
const result = await db.query(
|
|
125
|
+
'INSERT INTO users (name, email, password, private_channel, avatar_color) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, email, private_channel, avatar_color, created_at',
|
|
126
|
+
[name, email, password, privateChannel, avatarColor]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const user = result.rows[0];
|
|
130
|
+
|
|
131
|
+
// Get Pulse token for real-time features
|
|
132
|
+
const pulseData = await getPulseToken(user.id);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
statusCode: 200,
|
|
136
|
+
headers,
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
user: {
|
|
139
|
+
id: user.id,
|
|
140
|
+
name: user.name,
|
|
141
|
+
email: user.email,
|
|
142
|
+
avatarColor: user.avatar_color,
|
|
143
|
+
// Private channel is SECRET - only this user knows it
|
|
144
|
+
privateChannel: user.private_channel,
|
|
145
|
+
pulseToken: pulseData?.token || null,
|
|
146
|
+
pulseEndpoint: pulseData?.endpoint || null
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action === 'login') {
|
|
153
|
+
const result = await db.query(
|
|
154
|
+
'SELECT id, name, email, private_channel, avatar_color FROM users WHERE email = $1 AND password = $2',
|
|
155
|
+
[email, password]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (result.rows.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
statusCode: 401,
|
|
161
|
+
headers,
|
|
162
|
+
body: JSON.stringify({ error: 'Invalid email or password' })
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const user = result.rows[0];
|
|
167
|
+
|
|
168
|
+
// Generate private channel if missing (for existing users)
|
|
169
|
+
let privateChannel = user.private_channel;
|
|
170
|
+
if (!privateChannel) {
|
|
171
|
+
privateChannel = generatePrivateChannel();
|
|
172
|
+
await db.query('UPDATE users SET private_channel = $1 WHERE id = $2', [privateChannel, user.id]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get Pulse token for real-time features
|
|
176
|
+
const pulseData = await getPulseToken(user.id);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
statusCode: 200,
|
|
180
|
+
headers,
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
user: {
|
|
183
|
+
id: user.id,
|
|
184
|
+
name: user.name,
|
|
185
|
+
email: user.email,
|
|
186
|
+
avatarColor: user.avatar_color || '#007bff',
|
|
187
|
+
privateChannel: privateChannel,
|
|
188
|
+
pulseToken: pulseData?.token || null,
|
|
189
|
+
pulseEndpoint: pulseData?.endpoint || null
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get list of users (for chat) - WITHOUT exposing private channels
|
|
196
|
+
if (action === 'users') {
|
|
197
|
+
const result = await db.query(
|
|
198
|
+
'SELECT id, name, avatar_color FROM users ORDER BY name'
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
statusCode: 200,
|
|
203
|
+
headers,
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
users: result.rows.map(u => ({
|
|
206
|
+
id: u.id,
|
|
207
|
+
name: u.name,
|
|
208
|
+
avatarColor: u.avatar_color || '#007bff'
|
|
209
|
+
}))
|
|
210
|
+
})
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
statusCode: 400,
|
|
216
|
+
headers,
|
|
217
|
+
body: JSON.stringify({ error: 'Invalid action' })
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('Auth error:', error);
|
|
222
|
+
return {
|
|
223
|
+
statusCode: 500,
|
|
224
|
+
headers,
|
|
225
|
+
body: JSON.stringify({ error: error.message })
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|