@harbinger-ai/harbinger 0.1.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 (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +406 -0
  3. package/agents/README.md +76 -0
  4. package/agents/_template/CONFIG.yaml +7 -0
  5. package/agents/_template/HEARTBEAT.md +59 -0
  6. package/agents/_template/IDENTITY.md +4 -0
  7. package/agents/_template/SKILLS.md +1 -0
  8. package/agents/_template/SOUL.md +25 -0
  9. package/agents/_template/TOOLS.md +3 -0
  10. package/agents/binary-reverser/CONFIG.yaml +21 -0
  11. package/agents/binary-reverser/HEARTBEAT.md +65 -0
  12. package/agents/binary-reverser/IDENTITY.md +1 -0
  13. package/agents/binary-reverser/SKILLS.md +1 -0
  14. package/agents/binary-reverser/SOUL.md +23 -0
  15. package/agents/binary-reverser/TOOLS.md +99 -0
  16. package/agents/browser-agent/CONFIG.yaml +20 -0
  17. package/agents/browser-agent/HEARTBEAT.md +79 -0
  18. package/agents/browser-agent/IDENTITY.md +5 -0
  19. package/agents/browser-agent/SKILLS.md +86 -0
  20. package/agents/browser-agent/SOUL.md +23 -0
  21. package/agents/browser-agent/TOOLS.md +186 -0
  22. package/agents/cloud-infiltrator/CONFIG.yaml +22 -0
  23. package/agents/cloud-infiltrator/HEARTBEAT.md +78 -0
  24. package/agents/cloud-infiltrator/IDENTITY.md +1 -0
  25. package/agents/cloud-infiltrator/SKILLS.md +1 -0
  26. package/agents/cloud-infiltrator/SOUL.md +23 -0
  27. package/agents/cloud-infiltrator/TOOLS.md +68 -0
  28. package/agents/coding-assistant/CONFIG.yaml +22 -0
  29. package/agents/coding-assistant/HEARTBEAT.md +57 -0
  30. package/agents/coding-assistant/IDENTITY.md +5 -0
  31. package/agents/coding-assistant/SKILLS.md +69 -0
  32. package/agents/coding-assistant/SOUL.md +60 -0
  33. package/agents/coding-assistant/TOOLS.md +168 -0
  34. package/agents/learning-agent/CONFIG.yaml +21 -0
  35. package/agents/learning-agent/HEARTBEAT.md +63 -0
  36. package/agents/learning-agent/IDENTITY.md +5 -0
  37. package/agents/learning-agent/SKILLS.md +86 -0
  38. package/agents/learning-agent/SOUL.md +77 -0
  39. package/agents/learning-agent/TOOLS.md +145 -0
  40. package/agents/maintainer/CONFIG.yaml +31 -0
  41. package/agents/maintainer/HEARTBEAT.md +28 -0
  42. package/agents/maintainer/IDENTITY.md +33 -0
  43. package/agents/maintainer/SKILLS.md +24 -0
  44. package/agents/maintainer/SOUL.md +61 -0
  45. package/agents/maintainer/TOOLS.md +29 -0
  46. package/agents/maintainer/lib/engine.js +279 -0
  47. package/agents/maintainer/lib/safe-fixer.js +183 -0
  48. package/agents/morning-brief/CONFIG.yaml +22 -0
  49. package/agents/morning-brief/HEARTBEAT.md +60 -0
  50. package/agents/morning-brief/IDENTITY.md +5 -0
  51. package/agents/morning-brief/SKILLS.md +56 -0
  52. package/agents/morning-brief/SOUL.md +64 -0
  53. package/agents/morning-brief/TOOLS.md +112 -0
  54. package/agents/osint-detective/CONFIG.yaml +24 -0
  55. package/agents/osint-detective/HEARTBEAT.md +66 -0
  56. package/agents/osint-detective/IDENTITY.md +1 -0
  57. package/agents/osint-detective/SKILLS.md +1 -0
  58. package/agents/osint-detective/SOUL.md +23 -0
  59. package/agents/osint-detective/TOOLS.md +81 -0
  60. package/agents/recon-scout/CONFIG.yaml +22 -0
  61. package/agents/recon-scout/HEARTBEAT.md +79 -0
  62. package/agents/recon-scout/IDENTITY.md +1 -0
  63. package/agents/recon-scout/SKILLS.md +1 -0
  64. package/agents/recon-scout/SOUL.md +23 -0
  65. package/agents/recon-scout/TOOLS.md +93 -0
  66. package/agents/report-writer/CONFIG.yaml +21 -0
  67. package/agents/report-writer/HEARTBEAT.md +63 -0
  68. package/agents/report-writer/IDENTITY.md +1 -0
  69. package/agents/report-writer/SKILLS.md +1 -0
  70. package/agents/report-writer/SOUL.md +23 -0
  71. package/agents/report-writer/TOOLS.md +69 -0
  72. package/agents/shared/README.md +13 -0
  73. package/agents/web-hacker/CONFIG.yaml +24 -0
  74. package/agents/web-hacker/HEARTBEAT.md +78 -0
  75. package/agents/web-hacker/IDENTITY.md +1 -0
  76. package/agents/web-hacker/SKILLS.md +1 -0
  77. package/agents/web-hacker/SOUL.md +23 -0
  78. package/agents/web-hacker/TOOLS.md +86 -0
  79. package/api/CLAUDE.md +19 -0
  80. package/api/index.js +274 -0
  81. package/bin/cli.js +620 -0
  82. package/bin/local.sh +31 -0
  83. package/bin/postinstall.js +63 -0
  84. package/config/index.js +24 -0
  85. package/config/instrumentation.js +93 -0
  86. package/drizzle/0000_initial.sql +52 -0
  87. package/drizzle/0001_bounty_and_registry.sql +82 -0
  88. package/drizzle/0002_sync_columns.sql +7 -0
  89. package/drizzle/0003_graceful_bloodscream.sql +86 -0
  90. package/drizzle/meta/0000_snapshot.json +321 -0
  91. package/drizzle/meta/0003_snapshot.json +878 -0
  92. package/drizzle/meta/_journal.json +34 -0
  93. package/drizzle/relations.ts +3 -0
  94. package/drizzle/schema.ts +145 -0
  95. package/lib/actions.js +47 -0
  96. package/lib/agents.js +166 -0
  97. package/lib/ai/agent.js +96 -0
  98. package/lib/ai/autonomous-engine.js +261 -0
  99. package/lib/ai/index.js +359 -0
  100. package/lib/ai/model-router.js +254 -0
  101. package/lib/ai/model.js +73 -0
  102. package/lib/ai/tools.js +84 -0
  103. package/lib/auth/actions.js +28 -0
  104. package/lib/auth/config.js +27 -0
  105. package/lib/auth/edge-config.js +27 -0
  106. package/lib/auth/index.js +27 -0
  107. package/lib/auth/middleware.js +53 -0
  108. package/lib/bounty/actions.js +119 -0
  109. package/lib/bounty/findings.js +64 -0
  110. package/lib/bounty/programs.js +34 -0
  111. package/lib/bounty/sync-targets.js +267 -0
  112. package/lib/bounty/targets.js +33 -0
  113. package/lib/channels/base.js +56 -0
  114. package/lib/channels/index.js +15 -0
  115. package/lib/channels/telegram.js +148 -0
  116. package/lib/chat/actions.js +288 -0
  117. package/lib/chat/api.js +135 -0
  118. package/lib/chat/components/app-sidebar.js +237 -0
  119. package/lib/chat/components/app-sidebar.jsx +289 -0
  120. package/lib/chat/components/chat-header.js +27 -0
  121. package/lib/chat/components/chat-header.jsx +37 -0
  122. package/lib/chat/components/chat-input.js +230 -0
  123. package/lib/chat/components/chat-input.jsx +228 -0
  124. package/lib/chat/components/chat-nav-context.js +11 -0
  125. package/lib/chat/components/chat-nav-context.jsx +11 -0
  126. package/lib/chat/components/chat-page.js +81 -0
  127. package/lib/chat/components/chat-page.jsx +100 -0
  128. package/lib/chat/components/chat.js +150 -0
  129. package/lib/chat/components/chat.jsx +182 -0
  130. package/lib/chat/components/chats-page.js +302 -0
  131. package/lib/chat/components/chats-page.jsx +330 -0
  132. package/lib/chat/components/crons-page.js +172 -0
  133. package/lib/chat/components/crons-page.jsx +244 -0
  134. package/lib/chat/components/enhanced-tool-call.js +103 -0
  135. package/lib/chat/components/enhanced-tool-call.jsx +139 -0
  136. package/lib/chat/components/findings-page.js +175 -0
  137. package/lib/chat/components/findings-page.jsx +214 -0
  138. package/lib/chat/components/greeting.js +22 -0
  139. package/lib/chat/components/greeting.jsx +26 -0
  140. package/lib/chat/components/icons.js +777 -0
  141. package/lib/chat/components/icons.jsx +741 -0
  142. package/lib/chat/components/index.js +26 -0
  143. package/lib/chat/components/mcp-page.js +260 -0
  144. package/lib/chat/components/mcp-page.jsx +355 -0
  145. package/lib/chat/components/message.js +289 -0
  146. package/lib/chat/components/message.jsx +315 -0
  147. package/lib/chat/components/messages.js +66 -0
  148. package/lib/chat/components/messages.jsx +77 -0
  149. package/lib/chat/components/notifications-page.js +56 -0
  150. package/lib/chat/components/notifications-page.jsx +87 -0
  151. package/lib/chat/components/page-layout.js +21 -0
  152. package/lib/chat/components/page-layout.jsx +28 -0
  153. package/lib/chat/components/registry-page.js +222 -0
  154. package/lib/chat/components/registry-page.jsx +255 -0
  155. package/lib/chat/components/settings-layout.js +40 -0
  156. package/lib/chat/components/settings-layout.jsx +54 -0
  157. package/lib/chat/components/settings-secrets-page.js +216 -0
  158. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  159. package/lib/chat/components/sidebar-history-item.js +132 -0
  160. package/lib/chat/components/sidebar-history-item.jsx +113 -0
  161. package/lib/chat/components/sidebar-history.js +115 -0
  162. package/lib/chat/components/sidebar-history.jsx +157 -0
  163. package/lib/chat/components/sidebar-user-nav.js +63 -0
  164. package/lib/chat/components/sidebar-user-nav.jsx +73 -0
  165. package/lib/chat/components/status-bar.js +39 -0
  166. package/lib/chat/components/status-bar.jsx +51 -0
  167. package/lib/chat/components/swarm-page.js +157 -0
  168. package/lib/chat/components/swarm-page.jsx +210 -0
  169. package/lib/chat/components/targets-page.js +376 -0
  170. package/lib/chat/components/targets-page.jsx +389 -0
  171. package/lib/chat/components/tool-call.js +86 -0
  172. package/lib/chat/components/tool-call.jsx +104 -0
  173. package/lib/chat/components/tool-panel.js +107 -0
  174. package/lib/chat/components/tool-panel.jsx +145 -0
  175. package/lib/chat/components/triggers-page.js +153 -0
  176. package/lib/chat/components/triggers-page.jsx +221 -0
  177. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  178. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  179. package/lib/chat/components/ui/dropdown-menu.js +98 -0
  180. package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
  181. package/lib/chat/components/ui/rename-dialog.js +74 -0
  182. package/lib/chat/components/ui/rename-dialog.jsx +72 -0
  183. package/lib/chat/components/ui/scroll-area.js +13 -0
  184. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  185. package/lib/chat/components/ui/separator.js +21 -0
  186. package/lib/chat/components/ui/separator.jsx +18 -0
  187. package/lib/chat/components/ui/sheet.js +75 -0
  188. package/lib/chat/components/ui/sheet.jsx +95 -0
  189. package/lib/chat/components/ui/sidebar.js +227 -0
  190. package/lib/chat/components/ui/sidebar.jsx +245 -0
  191. package/lib/chat/components/ui/tooltip.js +56 -0
  192. package/lib/chat/components/ui/tooltip.jsx +66 -0
  193. package/lib/chat/components/upgrade-dialog.js +151 -0
  194. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  195. package/lib/chat/utils.js +11 -0
  196. package/lib/cron.js +246 -0
  197. package/lib/db/api-keys.js +163 -0
  198. package/lib/db/chats.js +145 -0
  199. package/lib/db/index.js +52 -0
  200. package/lib/db/notifications.js +99 -0
  201. package/lib/db/schema.js +145 -0
  202. package/lib/db/update-check.js +96 -0
  203. package/lib/db/users.js +89 -0
  204. package/lib/mcp/actions.js +104 -0
  205. package/lib/mcp/client.js +79 -0
  206. package/lib/mcp/handler.js +57 -0
  207. package/lib/mcp/server.js +165 -0
  208. package/lib/paths.js +46 -0
  209. package/lib/registry/actions.js +164 -0
  210. package/lib/registry/catalog.js +137 -0
  211. package/lib/registry/tools.js +71 -0
  212. package/lib/tools/create-job.js +99 -0
  213. package/lib/tools/github.js +217 -0
  214. package/lib/tools/openai.js +35 -0
  215. package/lib/tools/telegram.js +292 -0
  216. package/lib/triggers.js +118 -0
  217. package/lib/utils/render-md.js +102 -0
  218. package/package.json +103 -0
  219. package/setup/lib/auth.mjs +81 -0
  220. package/setup/lib/env.mjs +21 -0
  221. package/setup/lib/fs-utils.mjs +20 -0
  222. package/setup/lib/github.mjs +149 -0
  223. package/setup/lib/prerequisites.mjs +155 -0
  224. package/setup/lib/prompts.mjs +267 -0
  225. package/setup/lib/providers.mjs +48 -0
  226. package/setup/lib/sync.mjs +125 -0
  227. package/setup/lib/targets.mjs +45 -0
  228. package/setup/lib/telegram-verify.mjs +63 -0
  229. package/setup/lib/telegram.mjs +76 -0
  230. package/setup/setup-telegram.mjs +264 -0
  231. package/setup/setup.mjs +842 -0
  232. package/templates/.dockerignore +5 -0
  233. package/templates/.env.example +63 -0
  234. package/templates/.github/workflows/auto-merge.yml +117 -0
  235. package/templates/.github/workflows/build-image.yml +36 -0
  236. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  237. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  238. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  239. package/templates/.github/workflows/run-job.yml +89 -0
  240. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  241. package/templates/.gitignore.template +45 -0
  242. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  243. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  244. package/templates/CLAUDE.md +29 -0
  245. package/templates/CLAUDE.md.template +307 -0
  246. package/templates/app/api/[...thepopebot]/route.js +1 -0
  247. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  248. package/templates/app/chat/[chatId]/page.js +8 -0
  249. package/templates/app/chats/page.js +7 -0
  250. package/templates/app/components/ascii-logo.jsx +10 -0
  251. package/templates/app/components/login-form.jsx +92 -0
  252. package/templates/app/components/setup-form.jsx +82 -0
  253. package/templates/app/components/theme-provider.jsx +11 -0
  254. package/templates/app/components/theme-toggle.jsx +38 -0
  255. package/templates/app/components/ui/button.jsx +21 -0
  256. package/templates/app/components/ui/card.jsx +23 -0
  257. package/templates/app/components/ui/input.jsx +10 -0
  258. package/templates/app/components/ui/label.jsx +10 -0
  259. package/templates/app/crons/page.js +5 -0
  260. package/templates/app/findings/page.js +7 -0
  261. package/templates/app/globals.css +90 -0
  262. package/templates/app/layout.js +19 -0
  263. package/templates/app/login/page.js +15 -0
  264. package/templates/app/notifications/page.js +7 -0
  265. package/templates/app/page.js +7 -0
  266. package/templates/app/settings/crons/page.js +5 -0
  267. package/templates/app/settings/layout.js +7 -0
  268. package/templates/app/settings/mcp/page.js +5 -0
  269. package/templates/app/settings/page.js +5 -0
  270. package/templates/app/settings/secrets/page.js +5 -0
  271. package/templates/app/settings/triggers/page.js +5 -0
  272. package/templates/app/stream/chat/route.js +1 -0
  273. package/templates/app/swarm/page.js +7 -0
  274. package/templates/app/targets/page.js +7 -0
  275. package/templates/app/toolbox/page.js +7 -0
  276. package/templates/app/triggers/page.js +5 -0
  277. package/templates/config/AGENT.md +34 -0
  278. package/templates/config/CRONS.json +56 -0
  279. package/templates/config/EVENT_HANDLER.md +224 -0
  280. package/templates/config/HEARTBEAT.md +3 -0
  281. package/templates/config/JOB_SUMMARY.md +130 -0
  282. package/templates/config/MCP_SERVERS.json +1 -0
  283. package/templates/config/SKILL_BUILDING_GUIDE.md +90 -0
  284. package/templates/config/SOUL.md +17 -0
  285. package/templates/config/TRIGGERS.json +58 -0
  286. package/templates/docker/event-handler/Dockerfile +20 -0
  287. package/templates/docker/event-handler/ecosystem.config.cjs +8 -0
  288. package/templates/docker/job-claude-code/Dockerfile +34 -0
  289. package/templates/docker/job-claude-code/entrypoint.sh +139 -0
  290. package/templates/docker/job-pi-coding-agent/Dockerfile +44 -0
  291. package/templates/docker/job-pi-coding-agent/entrypoint.sh +163 -0
  292. package/templates/docker-compose.yml +63 -0
  293. package/templates/instrumentation.js +6 -0
  294. package/templates/middleware.js +1 -0
  295. package/templates/next.config.mjs +3 -0
  296. package/templates/postcss.config.mjs +5 -0
  297. package/templates/skills/LICENSE +21 -0
  298. package/templates/skills/README.md +119 -0
  299. package/templates/skills/brave-search/SKILL.md +79 -0
  300. package/templates/skills/brave-search/content.js +86 -0
  301. package/templates/skills/brave-search/package-lock.json +621 -0
  302. package/templates/skills/brave-search/package.json +14 -0
  303. package/templates/skills/brave-search/search.js +199 -0
  304. package/templates/skills/browser-tools/SKILL.md +196 -0
  305. package/templates/skills/browser-tools/browser-content.js +103 -0
  306. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  307. package/templates/skills/browser-tools/browser-eval.js +53 -0
  308. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  309. package/templates/skills/browser-tools/browser-nav.js +44 -0
  310. package/templates/skills/browser-tools/browser-pick.js +162 -0
  311. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  312. package/templates/skills/browser-tools/browser-start.js +87 -0
  313. package/templates/skills/browser-tools/package-lock.json +2556 -0
  314. package/templates/skills/browser-tools/package.json +19 -0
  315. package/templates/skills/llm-secrets/SKILL.md +34 -0
  316. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  317. package/templates/skills/modify-self/SKILL.md +12 -0
@@ -0,0 +1,163 @@
1
+ import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'crypto';
2
+ import { eq } from 'drizzle-orm';
3
+ import { getDb } from './index.js';
4
+ import { settings } from './schema.js';
5
+
6
+ const KEY_PREFIX = 'tpb_';
7
+
8
+ // In-memory cache: { key_hash, id } or null
9
+ let _cache = null;
10
+
11
+ /**
12
+ * Generate a new API key: tpb_ + 64 hex chars (32 random bytes).
13
+ * @returns {string}
14
+ */
15
+ export function generateApiKey() {
16
+ return KEY_PREFIX + randomBytes(32).toString('hex');
17
+ }
18
+
19
+ /**
20
+ * Hash an API key using SHA-256.
21
+ * @param {string} key - Raw API key
22
+ * @returns {string} Hex digest
23
+ */
24
+ export function hashApiKey(key) {
25
+ return createHash('sha256').update(key).digest('hex');
26
+ }
27
+
28
+ /**
29
+ * Lazy-load the API key hash into the in-memory cache.
30
+ */
31
+ function _ensureCache() {
32
+ if (_cache !== null) return _cache;
33
+
34
+ const db = getDb();
35
+ const row = db
36
+ .select()
37
+ .from(settings)
38
+ .where(eq(settings.type, 'api_key'))
39
+ .get();
40
+
41
+ if (row) {
42
+ const parsed = JSON.parse(row.value);
43
+ _cache = { keyHash: parsed.key_hash, id: row.id };
44
+ } else {
45
+ _cache = false; // no key exists — distinguish from "not loaded yet"
46
+ }
47
+ return _cache;
48
+ }
49
+
50
+ /**
51
+ * Clear the in-memory cache (call after create/delete).
52
+ */
53
+ export function invalidateApiKeyCache() {
54
+ _cache = null;
55
+ }
56
+
57
+ /**
58
+ * Create (or replace) the API key. Deletes any existing key first.
59
+ * @param {string} createdBy - User ID
60
+ * @returns {{ key: string, record: object }}
61
+ */
62
+ export function createApiKeyRecord(createdBy) {
63
+ const db = getDb();
64
+
65
+ // Delete any existing API key
66
+ db.delete(settings).where(eq(settings.type, 'api_key')).run();
67
+
68
+ const key = generateApiKey();
69
+ const keyHash = hashApiKey(key);
70
+ const keyPrefix = key.slice(0, 8); // "tpb_" + first 4 hex chars
71
+ const now = Date.now();
72
+
73
+ const record = {
74
+ id: randomUUID(),
75
+ type: 'api_key',
76
+ key: 'api_key',
77
+ value: JSON.stringify({ key_prefix: keyPrefix, key_hash: keyHash, last_used_at: null }),
78
+ createdBy,
79
+ createdAt: now,
80
+ updatedAt: now,
81
+ };
82
+
83
+ db.insert(settings).values(record).run();
84
+ invalidateApiKeyCache();
85
+
86
+ return {
87
+ key,
88
+ record: {
89
+ id: record.id,
90
+ keyPrefix,
91
+ createdAt: now,
92
+ lastUsedAt: null,
93
+ },
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Get the current API key metadata (no hash).
99
+ * @returns {object|null}
100
+ */
101
+ export function getApiKey() {
102
+ const db = getDb();
103
+ const row = db
104
+ .select()
105
+ .from(settings)
106
+ .where(eq(settings.type, 'api_key'))
107
+ .get();
108
+
109
+ if (!row) return null;
110
+
111
+ const parsed = JSON.parse(row.value);
112
+ return {
113
+ id: row.id,
114
+ keyPrefix: parsed.key_prefix,
115
+ createdAt: row.createdAt,
116
+ lastUsedAt: parsed.last_used_at,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Delete the API key.
122
+ */
123
+ export function deleteApiKey() {
124
+ const db = getDb();
125
+ db.delete(settings).where(eq(settings.type, 'api_key')).run();
126
+ invalidateApiKeyCache();
127
+ }
128
+
129
+ /**
130
+ * Verify a raw API key against the cached hash.
131
+ * @param {string} rawKey - Raw API key from request header
132
+ * @returns {object|null} Record if valid, null otherwise
133
+ */
134
+ export function verifyApiKey(rawKey) {
135
+ if (!rawKey || !rawKey.startsWith(KEY_PREFIX)) return null;
136
+
137
+ const keyHash = hashApiKey(rawKey);
138
+ const cached = _ensureCache();
139
+
140
+ if (!cached) return null;
141
+ const a = Buffer.from(cached.keyHash, 'hex');
142
+ const b = Buffer.from(keyHash, 'hex');
143
+ if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
144
+
145
+ // Update last_used_at in background (non-blocking)
146
+ try {
147
+ const db = getDb();
148
+ const now = Date.now();
149
+ const row = db.select().from(settings).where(eq(settings.id, cached.id)).get();
150
+ if (row) {
151
+ const parsed = JSON.parse(row.value);
152
+ parsed.last_used_at = now;
153
+ db.update(settings)
154
+ .set({ value: JSON.stringify(parsed), updatedAt: now })
155
+ .where(eq(settings.id, cached.id))
156
+ .run();
157
+ }
158
+ } catch {
159
+ // Non-fatal: last_used_at is informational
160
+ }
161
+
162
+ return cached;
163
+ }
@@ -0,0 +1,145 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { eq, desc, asc } from 'drizzle-orm';
3
+ import { getDb } from './index.js';
4
+ import { chats, messages } from './schema.js';
5
+
6
+ /**
7
+ * Create a new chat.
8
+ * @param {string} userId
9
+ * @param {string} [title='New Chat']
10
+ * @param {string} [id] - Optional chat ID (UUID). Generated if not provided.
11
+ * @returns {object} The created chat
12
+ */
13
+ export function createChat(userId, title = 'New Chat', id = null) {
14
+ const db = getDb();
15
+ const now = Date.now();
16
+ const chat = {
17
+ id: id || randomUUID(),
18
+ userId,
19
+ title,
20
+ createdAt: now,
21
+ updatedAt: now,
22
+ };
23
+ db.insert(chats).values(chat).run();
24
+ return chat;
25
+ }
26
+
27
+ /**
28
+ * Get all chats for a user, ordered by most recently updated.
29
+ * @param {string} userId
30
+ * @returns {object[]}
31
+ */
32
+ export function getChatsByUser(userId) {
33
+ const db = getDb();
34
+ return db
35
+ .select()
36
+ .from(chats)
37
+ .where(eq(chats.userId, userId))
38
+ .orderBy(desc(chats.updatedAt))
39
+ .all();
40
+ }
41
+
42
+ /**
43
+ * Get a single chat by ID.
44
+ * @param {string} chatId
45
+ * @returns {object|undefined}
46
+ */
47
+ export function getChatById(chatId) {
48
+ const db = getDb();
49
+ return db.select().from(chats).where(eq(chats.id, chatId)).get();
50
+ }
51
+
52
+ /**
53
+ * Update a chat's title.
54
+ * @param {string} chatId
55
+ * @param {string} title
56
+ */
57
+ export function updateChatTitle(chatId, title) {
58
+ const db = getDb();
59
+ db.update(chats)
60
+ .set({ title, updatedAt: Date.now() })
61
+ .where(eq(chats.id, chatId))
62
+ .run();
63
+ }
64
+
65
+ /**
66
+ * Toggle a chat's starred status.
67
+ * @param {string} chatId
68
+ * @returns {number} The new starred value (0 or 1)
69
+ */
70
+ export function toggleChatStarred(chatId) {
71
+ const db = getDb();
72
+ const chat = db.select({ starred: chats.starred }).from(chats).where(eq(chats.id, chatId)).get();
73
+ const newValue = chat?.starred ? 0 : 1;
74
+ db.update(chats)
75
+ .set({ starred: newValue })
76
+ .where(eq(chats.id, chatId))
77
+ .run();
78
+ return newValue;
79
+ }
80
+
81
+ /**
82
+ * Delete a chat and all its messages.
83
+ * @param {string} chatId
84
+ */
85
+ export function deleteChat(chatId) {
86
+ const db = getDb();
87
+ db.delete(messages).where(eq(messages.chatId, chatId)).run();
88
+ db.delete(chats).where(eq(chats.id, chatId)).run();
89
+ }
90
+
91
+ /**
92
+ * Delete all chats and messages for a user.
93
+ * @param {string} userId
94
+ */
95
+ export function deleteAllChatsByUser(userId) {
96
+ const db = getDb();
97
+ const userChats = db
98
+ .select({ id: chats.id })
99
+ .from(chats)
100
+ .where(eq(chats.userId, userId))
101
+ .all();
102
+
103
+ for (const chat of userChats) {
104
+ db.delete(messages).where(eq(messages.chatId, chat.id)).run();
105
+ }
106
+ db.delete(chats).where(eq(chats.userId, userId)).run();
107
+ }
108
+
109
+ /**
110
+ * Get all messages for a chat, ordered by creation time.
111
+ * @param {string} chatId
112
+ * @returns {object[]}
113
+ */
114
+ export function getMessagesByChatId(chatId) {
115
+ const db = getDb();
116
+ return db
117
+ .select()
118
+ .from(messages)
119
+ .where(eq(messages.chatId, chatId))
120
+ .orderBy(asc(messages.createdAt))
121
+ .all();
122
+ }
123
+
124
+ /**
125
+ * Save a message to a chat. Also updates the chat's updatedAt timestamp.
126
+ * @param {string} chatId
127
+ * @param {string} role - 'user' or 'assistant'
128
+ * @param {string} content
129
+ * @param {string} [id] - Optional message ID
130
+ * @returns {object} The created message
131
+ */
132
+ export function saveMessage(chatId, role, content, id = null) {
133
+ const db = getDb();
134
+ const now = Date.now();
135
+ const message = {
136
+ id: id || randomUUID(),
137
+ chatId,
138
+ role,
139
+ content,
140
+ createdAt: now,
141
+ };
142
+ db.insert(messages).values(message).run();
143
+ db.update(chats).set({ updatedAt: now }).where(eq(chats.id, chatId)).run();
144
+ return message;
145
+ }
@@ -0,0 +1,52 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import Database from 'better-sqlite3';
4
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
5
+ import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
6
+ import { thepopebotDb, dataDir, PROJECT_ROOT } from '../paths.js';
7
+ import * as schema from './schema.js';
8
+
9
+ let _db = null;
10
+
11
+ /**
12
+ * Get or create the Drizzle database instance (lazy singleton).
13
+ * @returns {import('drizzle-orm/better-sqlite3').BetterSQLite3Database}
14
+ */
15
+ export function getDb() {
16
+ if (!_db) {
17
+ // Ensure data directory exists
18
+ if (!fs.existsSync(dataDir)) {
19
+ fs.mkdirSync(dataDir, { recursive: true });
20
+ }
21
+ const sqlite = new Database(thepopebotDb);
22
+ sqlite.pragma('journal_mode = WAL');
23
+ _db = drizzle(sqlite, { schema });
24
+ }
25
+ return _db;
26
+ }
27
+
28
+ /**
29
+ * Initialize the database — apply pending migrations.
30
+ * Called from instrumentation.js at server startup.
31
+ * Uses Drizzle Kit migrations from the package's drizzle/ folder.
32
+ */
33
+ export function initDatabase() {
34
+ if (!fs.existsSync(dataDir)) {
35
+ fs.mkdirSync(dataDir, { recursive: true });
36
+ }
37
+
38
+ const sqlite = new Database(thepopebotDb);
39
+ sqlite.pragma('journal_mode = WAL');
40
+ const db = drizzle(sqlite, { schema });
41
+
42
+ // Resolve migrations folder from the installed package.
43
+ // import.meta.url doesn't survive webpack bundling, so resolve from PROJECT_ROOT.
44
+ const migrationsFolder = path.join(PROJECT_ROOT, 'node_modules', 'thepopebot', 'drizzle');
45
+
46
+ migrate(db, { migrationsFolder });
47
+
48
+ sqlite.close();
49
+
50
+ // Force re-creation of drizzle instance on next getDb() call
51
+ _db = null;
52
+ }
@@ -0,0 +1,99 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { eq, desc, sql } from 'drizzle-orm';
3
+ import { getDb } from './index.js';
4
+ import { notifications, subscriptions } from './schema.js';
5
+
6
+ /**
7
+ * Create a notification, then distribute to all subscribers.
8
+ * @param {string} notificationText - Human-readable notification text
9
+ * @param {object} payload - Raw webhook payload
10
+ * @returns {object} The created notification
11
+ */
12
+ export async function createNotification(notificationText, payload) {
13
+ const db = getDb();
14
+ const now = Date.now();
15
+ const row = {
16
+ id: randomUUID(),
17
+ notification: notificationText,
18
+ payload: JSON.stringify(payload),
19
+ read: 0,
20
+ createdAt: now,
21
+ };
22
+ db.insert(notifications).values(row).run();
23
+
24
+ // Distribute to subscribers (fire-and-forget)
25
+ distributeNotification(notificationText).catch((err) => {
26
+ console.error('Failed to distribute notification:', err);
27
+ });
28
+
29
+ return row;
30
+ }
31
+
32
+ /**
33
+ * Get all notifications, newest first.
34
+ * @returns {object[]}
35
+ */
36
+ export function getNotifications() {
37
+ const db = getDb();
38
+ return db
39
+ .select()
40
+ .from(notifications)
41
+ .orderBy(desc(notifications.createdAt))
42
+ .all();
43
+ }
44
+
45
+ /**
46
+ * Get count of unread notifications.
47
+ * @returns {number}
48
+ */
49
+ export function getUnreadCount() {
50
+ const db = getDb();
51
+ const result = db
52
+ .select({ count: sql`count(*)` })
53
+ .from(notifications)
54
+ .where(eq(notifications.read, 0))
55
+ .get();
56
+ return result?.count ?? 0;
57
+ }
58
+
59
+ /**
60
+ * Mark all notifications as read.
61
+ */
62
+ export function markAllRead() {
63
+ const db = getDb();
64
+ db.update(notifications)
65
+ .set({ read: 1 })
66
+ .where(eq(notifications.read, 0))
67
+ .run();
68
+ }
69
+
70
+ /**
71
+ * Get all subscriptions.
72
+ * @returns {object[]}
73
+ */
74
+ export function getSubscriptions() {
75
+ const db = getDb();
76
+ return db.select().from(subscriptions).all();
77
+ }
78
+
79
+ /**
80
+ * Distribute a notification to all subscribers.
81
+ * @param {string} notificationText - The notification message
82
+ */
83
+ async function distributeNotification(notificationText) {
84
+ const subs = getSubscriptions();
85
+ if (!subs.length) return;
86
+
87
+ for (const sub of subs) {
88
+ try {
89
+ if (sub.platform === 'telegram') {
90
+ const botToken = process.env.TELEGRAM_BOT_TOKEN;
91
+ if (!botToken) continue;
92
+ const { sendMessage } = await import('../tools/telegram.js');
93
+ await sendMessage(botToken, sub.channelId, notificationText);
94
+ }
95
+ } catch (err) {
96
+ console.error(`Failed to send to ${sub.platform}/${sub.channelId}:`, err);
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,145 @@
1
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
2
+
3
+ export const users = sqliteTable('users', {
4
+ id: text('id').primaryKey(),
5
+ email: text('email').notNull().unique(),
6
+ passwordHash: text('password_hash').notNull(),
7
+ role: text('role').notNull().default('admin'),
8
+ createdAt: integer('created_at').notNull(),
9
+ updatedAt: integer('updated_at').notNull(),
10
+ });
11
+
12
+ export const chats = sqliteTable('chats', {
13
+ id: text('id').primaryKey(),
14
+ userId: text('user_id').notNull(),
15
+ title: text('title').notNull().default('New Chat'),
16
+ starred: integer('starred').notNull().default(0),
17
+ createdAt: integer('created_at').notNull(),
18
+ updatedAt: integer('updated_at').notNull(),
19
+ });
20
+
21
+ export const messages = sqliteTable('messages', {
22
+ id: text('id').primaryKey(),
23
+ chatId: text('chat_id').notNull(),
24
+ role: text('role').notNull(),
25
+ content: text('content').notNull(),
26
+ createdAt: integer('created_at').notNull(),
27
+ });
28
+
29
+ export const notifications = sqliteTable('notifications', {
30
+ id: text('id').primaryKey(),
31
+ notification: text('notification').notNull(),
32
+ payload: text('payload').notNull(),
33
+ read: integer('read').notNull().default(0),
34
+ createdAt: integer('created_at').notNull(),
35
+ });
36
+
37
+ export const subscriptions = sqliteTable('subscriptions', {
38
+ id: text('id').primaryKey(),
39
+ platform: text('platform').notNull(),
40
+ channelId: text('channel_id').notNull(),
41
+ createdAt: integer('created_at').notNull(),
42
+ });
43
+
44
+ export const settings = sqliteTable('settings', {
45
+ id: text('id').primaryKey(),
46
+ type: text('type').notNull(),
47
+ key: text('key').notNull(),
48
+ value: text('value').notNull(),
49
+ createdBy: text('created_by'),
50
+ createdAt: integer('created_at').notNull(),
51
+ updatedAt: integer('updated_at').notNull(),
52
+ });
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────────
55
+ // Bug Bounty
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+
58
+ export const programs = sqliteTable('programs', {
59
+ id: text('id').primaryKey(),
60
+ name: text('name').notNull(),
61
+ platform: text('platform').notNull().default('custom'), // hackerone, bugcrowd, intigriti, yeswehack, federacy, custom
62
+ url: text('url'),
63
+ scopeUrl: text('scope_url'),
64
+ minBounty: integer('min_bounty'),
65
+ maxBounty: integer('max_bounty'),
66
+ status: text('status').notNull().default('active'), // active, paused, retired
67
+ notes: text('notes'),
68
+ syncHandle: text('sync_handle'), // platform-specific handle for synced programs
69
+ lastSyncedAt: integer('last_synced_at'),
70
+ createdAt: integer('created_at').notNull(),
71
+ updatedAt: integer('updated_at').notNull(),
72
+ });
73
+
74
+ export const targets = sqliteTable('targets', {
75
+ id: text('id').primaryKey(),
76
+ programId: text('program_id'),
77
+ type: text('type').notNull().default('domain'), // domain, ip, url, wildcard, api, mobile, cidr
78
+ value: text('value').notNull(),
79
+ status: text('status').notNull().default('in_scope'), // in_scope, out_of_scope, testing, completed
80
+ technologies: text('technologies'), // JSON array of detected tech
81
+ notes: text('notes'),
82
+ lastScannedAt: integer('last_scanned_at'),
83
+ syncSource: text('sync_source'), // hackerone, bugcrowd, intigriti, yeswehack, federacy
84
+ syncProgramHandle: text('sync_program_handle'), // handle from synced platform
85
+ createdAt: integer('created_at').notNull(),
86
+ updatedAt: integer('updated_at').notNull(),
87
+ });
88
+
89
+ export const findings = sqliteTable('findings', {
90
+ id: text('id').primaryKey(),
91
+ targetId: text('target_id'),
92
+ title: text('title').notNull(),
93
+ severity: text('severity').notNull().default('info'), // critical, high, medium, low, info
94
+ type: text('type').notNull(), // xss, sqli, ssrf, idor, rce, lfi, open_redirect, subdomain_takeover, info_disclosure, misconfig, etc.
95
+ status: text('status').notNull().default('new'), // new, triaging, confirmed, reported, duplicate, resolved, bounty_paid
96
+ description: text('description'),
97
+ stepsToReproduce: text('steps_to_reproduce'),
98
+ impact: text('impact'),
99
+ evidence: text('evidence'), // JSON array of screenshot URLs or file paths
100
+ bountyAmount: integer('bounty_amount'),
101
+ reportUrl: text('report_url'),
102
+ agentId: text('agent_id'), // which agent discovered this
103
+ toolId: text('tool_id'), // which tool found it
104
+ rawOutput: text('raw_output'), // raw tool output
105
+ reportedAt: integer('reported_at'),
106
+ createdAt: integer('created_at').notNull(),
107
+ updatedAt: integer('updated_at').notNull(),
108
+ });
109
+
110
+ // ─────────────────────────────────────────────────────────────────────────────
111
+ // Tool Registry
112
+ // ─────────────────────────────────────────────────────────────────────────────
113
+
114
+ export const tools = sqliteTable('tools', {
115
+ id: text('id').primaryKey(),
116
+ catalogId: text('catalog_id'), // links to built-in catalog entry
117
+ name: text('name').notNull(),
118
+ slug: text('slug').notNull().unique(),
119
+ category: text('category').notNull(),
120
+ description: text('description'),
121
+ dockerImage: text('docker_image'),
122
+ installCmd: text('install_cmd'),
123
+ sourceUrl: text('source_url'), // GitHub URL
124
+ version: text('version'),
125
+ installed: integer('installed').notNull().default(0),
126
+ enabled: integer('enabled').notNull().default(1),
127
+ config: text('config'), // JSON tool-specific config
128
+ mcpServerId: text('mcp_server_id'), // if exposed via MCP
129
+ createdAt: integer('created_at').notNull(),
130
+ updatedAt: integer('updated_at').notNull(),
131
+ });
132
+
133
+ export const dockerContainers = sqliteTable('docker_containers', {
134
+ id: text('id').primaryKey(),
135
+ toolId: text('tool_id'),
136
+ containerId: text('container_id'), // Docker container ID
137
+ imageName: text('image_name').notNull(),
138
+ status: text('status').notNull().default('created'), // created, running, stopped, error
139
+ agentId: text('agent_id'),
140
+ ports: text('ports'), // JSON port mapping
141
+ env: text('env'), // JSON env vars (secrets redacted)
142
+ logs: text('logs'),
143
+ createdAt: integer('created_at').notNull(),
144
+ stoppedAt: integer('stopped_at'),
145
+ });
@@ -0,0 +1,96 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { eq, and } from 'drizzle-orm';
3
+ import { getDb } from './index.js';
4
+ import { settings } from './schema.js';
5
+
6
+ /**
7
+ * Get the stored available version from the DB.
8
+ * @returns {string|null}
9
+ */
10
+ export function getAvailableVersion() {
11
+ const db = getDb();
12
+ const row = db
13
+ .select()
14
+ .from(settings)
15
+ .where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
16
+ .get();
17
+
18
+ return row ? row.value : null;
19
+ }
20
+
21
+ /**
22
+ * Set the available version in the DB (delete + insert upsert).
23
+ * @param {string} version
24
+ */
25
+ export function setAvailableVersion(version) {
26
+ const db = getDb();
27
+ db.delete(settings)
28
+ .where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
29
+ .run();
30
+
31
+ const now = Date.now();
32
+ db.insert(settings).values({
33
+ id: randomUUID(),
34
+ type: 'update',
35
+ key: 'available_version',
36
+ value: version,
37
+ createdAt: now,
38
+ updatedAt: now,
39
+ }).run();
40
+ }
41
+
42
+ /**
43
+ * Clear the available version from the DB.
44
+ */
45
+ export function clearAvailableVersion() {
46
+ const db = getDb();
47
+ db.delete(settings)
48
+ .where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
49
+ .run();
50
+ }
51
+
52
+ /**
53
+ * Get the stored release notes from the DB.
54
+ * @returns {string|null}
55
+ */
56
+ export function getReleaseNotes() {
57
+ const db = getDb();
58
+ const row = db
59
+ .select()
60
+ .from(settings)
61
+ .where(and(eq(settings.type, 'update'), eq(settings.key, 'release_notes')))
62
+ .get();
63
+
64
+ return row ? row.value : null;
65
+ }
66
+
67
+ /**
68
+ * Set the release notes in the DB (delete + insert upsert).
69
+ * @param {string} notes
70
+ */
71
+ export function setReleaseNotes(notes) {
72
+ const db = getDb();
73
+ db.delete(settings)
74
+ .where(and(eq(settings.type, 'update'), eq(settings.key, 'release_notes')))
75
+ .run();
76
+
77
+ const now = Date.now();
78
+ db.insert(settings).values({
79
+ id: randomUUID(),
80
+ type: 'update',
81
+ key: 'release_notes',
82
+ value: notes,
83
+ createdAt: now,
84
+ updatedAt: now,
85
+ }).run();
86
+ }
87
+
88
+ /**
89
+ * Clear the release notes from the DB.
90
+ */
91
+ export function clearReleaseNotes() {
92
+ const db = getDb();
93
+ db.delete(settings)
94
+ .where(and(eq(settings.type, 'update'), eq(settings.key, 'release_notes')))
95
+ .run();
96
+ }