@kinqs/brainrouter-mcp-server 0.3.4

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 (337) hide show
  1. package/.env.example +144 -0
  2. package/README.md +56 -0
  3. package/agents/README.md +120 -0
  4. package/agents/code-reviewer.md +97 -0
  5. package/agents/security-auditor.md +101 -0
  6. package/agents/test-engineer.md +95 -0
  7. package/dist/__tests__/agent_mode.test.d.ts +1 -0
  8. package/dist/__tests__/api-routes.test.d.ts +1 -0
  9. package/dist/__tests__/api-routes.test.js +170 -0
  10. package/dist/__tests__/crypto.test.d.ts +1 -0
  11. package/dist/__tests__/crypto.test.js +28 -0
  12. package/dist/__tests__/host-integrations.test.d.ts +1 -0
  13. package/dist/__tests__/host-integrations.test.js +82 -0
  14. package/dist/__tests__/integration.test.d.ts +1 -0
  15. package/dist/__tests__/integration.test.js +50 -0
  16. package/dist/__tests__/loader.test.d.ts +1 -0
  17. package/dist/__tests__/loader.test.js +89 -0
  18. package/dist/__tests__/neural-spark.test.d.ts +1 -0
  19. package/dist/__tests__/neural-spark.test.js +112 -0
  20. package/dist/__tests__/pagination.test.d.ts +1 -0
  21. package/dist/__tests__/pagination.test.js +23 -0
  22. package/dist/__tests__/redaction.test.d.ts +1 -0
  23. package/dist/__tests__/redaction.test.js +17 -0
  24. package/dist/__tests__/registry.test.d.ts +1 -0
  25. package/dist/__tests__/registry.test.js +56 -0
  26. package/dist/__tests__/retry.test.d.ts +1 -0
  27. package/dist/__tests__/retry.test.js +30 -0
  28. package/dist/__tests__/skill-activation.test.d.ts +1 -0
  29. package/dist/__tests__/skill-activation.test.js +112 -0
  30. package/dist/__tests__/working-memory.test.d.ts +1 -0
  31. package/dist/__tests__/working-memory.test.js +200 -0
  32. package/dist/__tests__/workspace-paths.test.d.ts +1 -0
  33. package/dist/__tests__/workspace-paths.test.js +56 -0
  34. package/dist/__tests__/writer.test.d.ts +1 -0
  35. package/dist/__tests__/writer.test.js +94 -0
  36. package/dist/api/auth/crypto.d.ts +4 -0
  37. package/dist/api/auth/crypto.js +54 -0
  38. package/dist/api/middleware/auth.d.ts +12 -0
  39. package/dist/api/middleware/auth.js +90 -0
  40. package/dist/api/pagination.d.ts +18 -0
  41. package/dist/api/pagination.js +32 -0
  42. package/dist/api/routes/auth.d.ts +1 -0
  43. package/dist/api/routes/auth.js +130 -0
  44. package/dist/api/routes/chat-completions.d.ts +7 -0
  45. package/dist/api/routes/chat-completions.js +474 -0
  46. package/dist/api/routes/contradictions.d.ts +1 -0
  47. package/dist/api/routes/contradictions.js +28 -0
  48. package/dist/api/routes/evidence.d.ts +1 -0
  49. package/dist/api/routes/evidence.js +59 -0
  50. package/dist/api/routes/governance.d.ts +1 -0
  51. package/dist/api/routes/governance.js +95 -0
  52. package/dist/api/routes/graph.d.ts +1 -0
  53. package/dist/api/routes/graph.js +25 -0
  54. package/dist/api/routes/hooks.d.ts +1 -0
  55. package/dist/api/routes/hooks.js +88 -0
  56. package/dist/api/routes/memories.d.ts +1 -0
  57. package/dist/api/routes/memories.js +92 -0
  58. package/dist/api/routes/persona.d.ts +1 -0
  59. package/dist/api/routes/persona.js +9 -0
  60. package/dist/api/routes/scenes.d.ts +1 -0
  61. package/dist/api/routes/scenes.js +35 -0
  62. package/dist/api/routes/skills.d.ts +1 -0
  63. package/dist/api/routes/skills.js +14 -0
  64. package/dist/api/routes/stats.d.ts +1 -0
  65. package/dist/api/routes/stats.js +8 -0
  66. package/dist/api/routes/users.d.ts +1 -0
  67. package/dist/api/routes/users.js +82 -0
  68. package/dist/api/routes/working.d.ts +1 -0
  69. package/dist/api/routes/working.js +88 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +492 -0
  72. package/dist/integrations/claude-code.d.ts +12 -0
  73. package/dist/integrations/claude-code.js +35 -0
  74. package/dist/integrations/codex.d.ts +12 -0
  75. package/dist/integrations/codex.js +34 -0
  76. package/dist/integrations/generic-mcp.d.ts +52 -0
  77. package/dist/integrations/generic-mcp.js +118 -0
  78. package/dist/loader.d.ts +29 -0
  79. package/dist/loader.js +200 -0
  80. package/dist/memory/capture.d.ts +35 -0
  81. package/dist/memory/capture.js +230 -0
  82. package/dist/memory/config.d.ts +2 -0
  83. package/dist/memory/config.js +3 -0
  84. package/dist/memory/engine.d.ts +203 -0
  85. package/dist/memory/engine.js +626 -0
  86. package/dist/memory/llm-semaphore.d.ts +41 -0
  87. package/dist/memory/llm-semaphore.js +81 -0
  88. package/dist/memory/memory-type-config.d.ts +11 -0
  89. package/dist/memory/memory-type-config.js +65 -0
  90. package/dist/memory/pipeline/cognitive-contradiction.d.ts +7 -0
  91. package/dist/memory/pipeline/cognitive-contradiction.js +59 -0
  92. package/dist/memory/pipeline/cognitive-dedup.d.ts +23 -0
  93. package/dist/memory/pipeline/cognitive-dedup.js +38 -0
  94. package/dist/memory/pipeline/cognitive-extractor.d.ts +21 -0
  95. package/dist/memory/pipeline/cognitive-extractor.js +183 -0
  96. package/dist/memory/pipeline/contextual-focus-builder.d.ts +13 -0
  97. package/dist/memory/pipeline/contextual-focus-builder.js +135 -0
  98. package/dist/memory/pipeline/focus-direction-shift.d.ts +10 -0
  99. package/dist/memory/pipeline/focus-direction-shift.js +27 -0
  100. package/dist/memory/pipeline/graph-builder.d.ts +11 -0
  101. package/dist/memory/pipeline/graph-builder.js +88 -0
  102. package/dist/memory/pipeline/graph-recall.d.ts +13 -0
  103. package/dist/memory/pipeline/graph-recall.js +55 -0
  104. package/dist/memory/pipeline/identity-distiller.d.ts +15 -0
  105. package/dist/memory/pipeline/identity-distiller.js +40 -0
  106. package/dist/memory/pipeline/l1-contradiction.d.ts +7 -0
  107. package/dist/memory/pipeline/l1-contradiction.js +66 -0
  108. package/dist/memory/pipeline/l1-dedup.d.ts +23 -0
  109. package/dist/memory/pipeline/l1-dedup.js +39 -0
  110. package/dist/memory/pipeline/l1-extractor.d.ts +21 -0
  111. package/dist/memory/pipeline/l1-extractor.js +180 -0
  112. package/dist/memory/pipeline/l2-direction-shift.d.ts +10 -0
  113. package/dist/memory/pipeline/l2-direction-shift.js +27 -0
  114. package/dist/memory/pipeline/l2-scene.d.ts +15 -0
  115. package/dist/memory/pipeline/l2-scene.js +140 -0
  116. package/dist/memory/pipeline/l3-distiller.d.ts +15 -0
  117. package/dist/memory/pipeline/l3-distiller.js +40 -0
  118. package/dist/memory/pipeline/neural-spark.d.ts +27 -0
  119. package/dist/memory/pipeline/neural-spark.js +78 -0
  120. package/dist/memory/pipeline/skill-prewarm.d.ts +63 -0
  121. package/dist/memory/pipeline/skill-prewarm.js +127 -0
  122. package/dist/memory/pipeline/task-queue.d.ts +54 -0
  123. package/dist/memory/pipeline/task-queue.js +117 -0
  124. package/dist/memory/prompts/cognitive-contradiction.d.ts +1 -0
  125. package/dist/memory/prompts/cognitive-contradiction.js +25 -0
  126. package/dist/memory/prompts/cognitive-extraction.d.ts +10 -0
  127. package/dist/memory/prompts/cognitive-extraction.js +114 -0
  128. package/dist/memory/prompts/core-identity.d.ts +6 -0
  129. package/dist/memory/prompts/core-identity.js +60 -0
  130. package/dist/memory/prompts/focus-direction-shift.d.ts +5 -0
  131. package/dist/memory/prompts/focus-direction-shift.js +32 -0
  132. package/dist/memory/prompts/focus-scene-cluster.d.ts +2 -0
  133. package/dist/memory/prompts/focus-scene-cluster.js +33 -0
  134. package/dist/memory/prompts/focus-scene.d.ts +7 -0
  135. package/dist/memory/prompts/focus-scene.js +40 -0
  136. package/dist/memory/prompts/graph-extraction-batch.d.ts +14 -0
  137. package/dist/memory/prompts/graph-extraction-batch.js +54 -0
  138. package/dist/memory/prompts/graph-extraction.d.ts +2 -0
  139. package/dist/memory/prompts/graph-extraction.js +53 -0
  140. package/dist/memory/prompts/l1-contradiction-batch.d.ts +16 -0
  141. package/dist/memory/prompts/l1-contradiction-batch.js +47 -0
  142. package/dist/memory/prompts/l1-contradiction.d.ts +1 -0
  143. package/dist/memory/prompts/l1-contradiction.js +25 -0
  144. package/dist/memory/prompts/l1-extraction.d.ts +10 -0
  145. package/dist/memory/prompts/l1-extraction.js +114 -0
  146. package/dist/memory/prompts/l2-direction-shift.d.ts +5 -0
  147. package/dist/memory/prompts/l2-direction-shift.js +32 -0
  148. package/dist/memory/prompts/l2-scene-cluster.d.ts +2 -0
  149. package/dist/memory/prompts/l2-scene-cluster.js +33 -0
  150. package/dist/memory/prompts/l2-scene.d.ts +7 -0
  151. package/dist/memory/prompts/l2-scene.js +40 -0
  152. package/dist/memory/prompts/l3-persona.d.ts +6 -0
  153. package/dist/memory/prompts/l3-persona.js +60 -0
  154. package/dist/memory/recall.d.ts +47 -0
  155. package/dist/memory/recall.js +427 -0
  156. package/dist/memory/redaction.d.ts +1 -0
  157. package/dist/memory/redaction.js +24 -0
  158. package/dist/memory/retry.d.ts +13 -0
  159. package/dist/memory/retry.js +53 -0
  160. package/dist/memory/scheduler.d.ts +9 -0
  161. package/dist/memory/scheduler.js +16 -0
  162. package/dist/memory/skill-hints-loader.d.ts +30 -0
  163. package/dist/memory/skill-hints-loader.js +100 -0
  164. package/dist/memory/store/embedding.d.ts +16 -0
  165. package/dist/memory/store/embedding.js +68 -0
  166. package/dist/memory/store/reranker.d.ts +24 -0
  167. package/dist/memory/store/reranker.js +83 -0
  168. package/dist/memory/store/sqlite.d.ts +167 -0
  169. package/dist/memory/store/sqlite.js +1816 -0
  170. package/dist/memory/store/types.d.ts +101 -0
  171. package/dist/memory/store/types.js +1 -0
  172. package/dist/memory/types.d.ts +207 -0
  173. package/dist/memory/types.js +7 -0
  174. package/dist/memory/validation.d.ts +441 -0
  175. package/dist/memory/validation.js +129 -0
  176. package/dist/memory/working/canvas.d.ts +5 -0
  177. package/dist/memory/working/canvas.js +43 -0
  178. package/dist/memory/working/offload.d.ts +71 -0
  179. package/dist/memory/working/offload.js +211 -0
  180. package/dist/memory/working/step-log.d.ts +16 -0
  181. package/dist/memory/working/step-log.js +35 -0
  182. package/dist/registry.d.ts +34 -0
  183. package/dist/registry.js +305 -0
  184. package/dist/resolver.d.ts +17 -0
  185. package/dist/resolver.js +126 -0
  186. package/dist/scripts/validate-foreign-workspace-path.d.ts +1 -0
  187. package/dist/scripts/validate-foreign-workspace-path.js +39 -0
  188. package/dist/tools/agent_memory_tools.d.ts +485 -0
  189. package/dist/tools/agent_memory_tools.js +793 -0
  190. package/dist/tools/create_skill.d.ts +46 -0
  191. package/dist/tools/create_skill.js +46 -0
  192. package/dist/tools/get_doc.d.ts +21 -0
  193. package/dist/tools/get_doc.js +24 -0
  194. package/dist/tools/get_persona.d.ts +15 -0
  195. package/dist/tools/get_persona.js +20 -0
  196. package/dist/tools/get_reference.d.ts +15 -0
  197. package/dist/tools/get_reference.js +20 -0
  198. package/dist/tools/get_skill.d.ts +34 -0
  199. package/dist/tools/get_skill.js +65 -0
  200. package/dist/tools/get_template_doc.d.ts +21 -0
  201. package/dist/tools/get_template_doc.js +24 -0
  202. package/dist/tools/list_docs.d.ts +15 -0
  203. package/dist/tools/list_docs.js +16 -0
  204. package/dist/tools/list_skills.d.ts +18 -0
  205. package/dist/tools/list_skills.js +17 -0
  206. package/dist/tools/list_template_docs.d.ts +15 -0
  207. package/dist/tools/list_template_docs.js +16 -0
  208. package/dist/tools/memory-engineering.d.ts +225 -0
  209. package/dist/tools/memory-engineering.js +284 -0
  210. package/dist/tools/memory-explain.d.ts +34 -0
  211. package/dist/tools/memory-explain.js +109 -0
  212. package/dist/tools/memory-governance.d.ts +171 -0
  213. package/dist/tools/memory-governance.js +224 -0
  214. package/dist/tools/memory-hooks.d.ts +67 -0
  215. package/dist/tools/memory-hooks.js +102 -0
  216. package/dist/tools/memory-working.d.ts +98 -0
  217. package/dist/tools/memory-working.js +101 -0
  218. package/dist/tools/memory_capture_turn.d.ts +66 -0
  219. package/dist/tools/memory_capture_turn.js +85 -0
  220. package/dist/tools/memory_consolidate.d.ts +55 -0
  221. package/dist/tools/memory_consolidate.js +176 -0
  222. package/dist/tools/memory_contradictions.d.ts +53 -0
  223. package/dist/tools/memory_contradictions.js +52 -0
  224. package/dist/tools/memory_graph_query.d.ts +51 -0
  225. package/dist/tools/memory_graph_query.js +35 -0
  226. package/dist/tools/memory_mark_cited.d.ts +43 -0
  227. package/dist/tools/memory_mark_cited.js +63 -0
  228. package/dist/tools/memory_recall.d.ts +77 -0
  229. package/dist/tools/memory_recall.js +81 -0
  230. package/dist/tools/memory_register_skill_hints.d.ts +49 -0
  231. package/dist/tools/memory_register_skill_hints.js +55 -0
  232. package/dist/tools/memory_resolve_session.d.ts +24 -0
  233. package/dist/tools/memory_resolve_session.js +133 -0
  234. package/dist/tools/memory_search.d.ts +146 -0
  235. package/dist/tools/memory_search.js +84 -0
  236. package/dist/tools/search_skills.d.ts +18 -0
  237. package/dist/tools/search_skills.js +17 -0
  238. package/dist/tools/update_doc.d.ts +24 -0
  239. package/dist/tools/update_doc.js +35 -0
  240. package/dist/tools/update_skill.d.ts +30 -0
  241. package/dist/tools/update_skill.js +80 -0
  242. package/dist/types.d.ts +81 -0
  243. package/dist/types.js +4 -0
  244. package/dist/writer.d.ts +30 -0
  245. package/dist/writer.js +220 -0
  246. package/docs/TEMPLATE ONLY +1 -0
  247. package/docs/api/API.md +64 -0
  248. package/docs/api/security/SECURITY.md +58 -0
  249. package/docs/deployment/DockerDeployment.md +30 -0
  250. package/docs/design/Design.md +59 -0
  251. package/docs/design/themes/apple.md +101 -0
  252. package/docs/design/themes/dieter-grid.md +100 -0
  253. package/docs/design/themes/gallery-white.md +100 -0
  254. package/docs/design/themes/pinterest.md +101 -0
  255. package/docs/design/themes/realty-open-house.md +101 -0
  256. package/docs/design/themes/vodafone.md +101 -0
  257. package/docs/hooks/Hooks.md +30 -0
  258. package/docs/schema/Schema.md +35 -0
  259. package/docs/strategy/ScalingStrategy.md +19 -0
  260. package/package.json +88 -0
  261. package/references/accessibility-checklist.md +160 -0
  262. package/references/orchestration-patterns.md +370 -0
  263. package/references/performance-checklist.md +153 -0
  264. package/references/security-checklist.md +134 -0
  265. package/references/testing-patterns.md +236 -0
  266. package/skills/agent/adr-skill/SKILL.md +299 -0
  267. package/skills/agent/agentic-engineering-workflow/SKILL.md +95 -0
  268. package/skills/agent/bootstrap-skill/SKILL.md +103 -0
  269. package/skills/agent/context-engineering/SKILL.md +307 -0
  270. package/skills/agent/debugging-and-error-recovery/SKILL.md +308 -0
  271. package/skills/agent/developer-growth-analysis/SKILL.md +328 -0
  272. package/skills/agent/doubt-driven-skill/SKILL.md +249 -0
  273. package/skills/agent/handover-skill/SKILL.md +112 -0
  274. package/skills/agent/idea-refine-skill/SKILL.md +185 -0
  275. package/skills/agent/idea-refine-skill/examples.md +238 -0
  276. package/skills/agent/idea-refine-skill/frameworks.md +99 -0
  277. package/skills/agent/idea-refine-skill/refinement-criteria.md +113 -0
  278. package/skills/agent/interview-skill/SKILL.md +226 -0
  279. package/skills/agent/planning-skill/SKILL.md +270 -0
  280. package/skills/agent/skill-authoring/SKILL.md +189 -0
  281. package/skills/agent/source-driven-skill/SKILL.md +197 -0
  282. package/skills/agent/spec-driven-skill/SKILL.md +221 -0
  283. package/skills/agent/sync-skill/SKILL.md +92 -0
  284. package/skills/agent/using-agent-skills/SKILL.md +189 -0
  285. package/skills/api/a11y-skill/SKILL.md +88 -0
  286. package/skills/api/api-skill/SKILL.md +123 -0
  287. package/skills/api/auth-skill/SKILL.md +80 -0
  288. package/skills/api/debug-skill/SKILL.md +535 -0
  289. package/skills/api/performance-skill/SKILL.md +100 -0
  290. package/skills/api/testing-skill/SKILL.md +100 -0
  291. package/skills/codebase/code-review-and-quality/SKILL.md +228 -0
  292. package/skills/codebase/code-simplification/SKILL.md +352 -0
  293. package/skills/codebase/code-structure-cleanup/SKILL.md +142 -0
  294. package/skills/codebase/concerns-skill/SKILL.md +89 -0
  295. package/skills/codebase/conventions-skill/SKILL.md +95 -0
  296. package/skills/codebase/doc-management-skill/SKILL.md +47 -0
  297. package/skills/codebase/git-workflow-skill/SKILL.md +312 -0
  298. package/skills/communication/1-3-1-rule/SKILL.md +120 -0
  299. package/skills/design/brutalist-skill/SKILL.md +131 -0
  300. package/skills/design/concept-diagrams/SKILL.md +387 -0
  301. package/skills/design/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
  302. package/skills/design/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
  303. package/skills/design/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
  304. package/skills/design/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
  305. package/skills/design/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
  306. package/skills/design/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
  307. package/skills/design/concept-diagrams/examples/electricity-grid-flow.md +182 -0
  308. package/skills/design/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
  309. package/skills/design/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
  310. package/skills/design/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
  311. package/skills/design/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
  312. package/skills/design/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
  313. package/skills/design/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
  314. package/skills/design/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
  315. package/skills/design/concept-diagrams/examples/wind-turbine-structure.md +338 -0
  316. package/skills/design/concept-diagrams/references/dashboard-patterns.md +43 -0
  317. package/skills/design/concept-diagrams/references/infrastructure-patterns.md +144 -0
  318. package/skills/design/concept-diagrams/references/physical-shape-cookbook.md +42 -0
  319. package/skills/design/concept-diagrams/templates/template.html +174 -0
  320. package/skills/design/gpt-tasteskill/SKILL.md +114 -0
  321. package/skills/design/minimalist-skill/SKILL.md +116 -0
  322. package/skills/design/output-skill/SKILL.md +87 -0
  323. package/skills/design/redesign-skill/SKILL.md +213 -0
  324. package/skills/design/soft-skill/SKILL.md +132 -0
  325. package/skills/design/stitch-skill/EXAMPLE.md +121 -0
  326. package/skills/design/stitch-skill/SKILL.md +222 -0
  327. package/skills/design/taste-skill/SKILL.md +269 -0
  328. package/skills/devops/ci-cd-skill/SKILL.md +402 -0
  329. package/skills/devops/docker-skill/SKILL.md +297 -0
  330. package/skills/devops/domain-skill/SKILL.md +234 -0
  331. package/skills/lifecycle/changelog-generator/SKILL.md +135 -0
  332. package/skills/lifecycle/incremental-skill/SKILL.md +257 -0
  333. package/skills/lifecycle/migration-skill/SKILL.md +218 -0
  334. package/skills/lifecycle/shipping-skill/SKILL.md +321 -0
  335. package/skills/memory/agent-memory/SKILL.md +122 -0
  336. package/skills/qa/browser-testing-skill/SKILL.md +314 -0
  337. package/skills/ux/adversarial-ux-skill/SKILL.md +168 -0
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { ExternalApiError, fetchWithExternalRetry, retryExternalCall } from "../memory/retry.js";
3
+ describe("external API retry helpers", () => {
4
+ it("retries retryable HTTP errors and returns the successful response", async () => {
5
+ const responses = [
6
+ new Response("rate limited", { status: 429, statusText: "Too Many Requests" }),
7
+ new Response("unavailable", { status: 503, statusText: "Service Unavailable" }),
8
+ new Response(JSON.stringify({ ok: true }), { status: 200 }),
9
+ ];
10
+ const fetchMock = vi.fn(async () => responses.shift());
11
+ vi.stubGlobal("fetch", fetchMock);
12
+ const response = await fetchWithExternalRetry("https://example.test/api", {}, {
13
+ label: "test API",
14
+ sleep: async () => undefined,
15
+ });
16
+ expect(response.status).toBe(200);
17
+ expect(fetchMock).toHaveBeenCalledTimes(3);
18
+ vi.unstubAllGlobals();
19
+ });
20
+ it("does not retry non-retryable external API errors", async () => {
21
+ const operation = vi.fn(async () => {
22
+ throw new ExternalApiError("bad request", 400);
23
+ });
24
+ await expect(retryExternalCall(operation, {
25
+ label: "test API",
26
+ sleep: async () => undefined,
27
+ })).rejects.toThrow("bad request");
28
+ expect(operation).toHaveBeenCalledTimes(1);
29
+ });
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { detectPrewarmSkills, decayPotential, spikeSkill } from "../memory/pipeline/skill-prewarm.js";
3
+ class InMemoryActivationStore {
4
+ activations = new Map();
5
+ hints = new Map();
6
+ activationWriteCount = 0;
7
+ getSkillActivations(userId) {
8
+ return [...(this.activations.get(userId) ?? [])].sort((a, b) => b.potential - a.potential);
9
+ }
10
+ upsertSkillActivations(userId, records) {
11
+ this.activationWriteCount += 1;
12
+ const current = new Map((this.activations.get(userId) ?? []).map((record) => [record.skillName, record]));
13
+ for (const record of records) {
14
+ current.set(record.skillName, record);
15
+ }
16
+ this.activations.set(userId, [...current.values()]);
17
+ }
18
+ upsertSkillHints(skillName, hints, sourceFile = "") {
19
+ this.hints.set(skillName, {
20
+ skillName,
21
+ hints,
22
+ sourceFile,
23
+ registeredAt: "2026-05-20T00:00:00.000Z",
24
+ });
25
+ }
26
+ getSkillHints(skillName) {
27
+ return this.hints.get(skillName)?.hints ?? null;
28
+ }
29
+ }
30
+ function createStore() {
31
+ return new InMemoryActivationStore();
32
+ }
33
+ describe("skill activation potential routing", () => {
34
+ it("persists skill activations through the store contract", () => {
35
+ const store = createStore();
36
+ store.upsertSkillActivations("user-1", [
37
+ { skillName: "testing-skill", potential: 1.25, lastDecayTime: "2026-05-20T00:00:00.000Z" },
38
+ ]);
39
+ expect(store.getSkillActivations("user-1")).toEqual([
40
+ { skillName: "testing-skill", potential: 1.25, lastDecayTime: "2026-05-20T00:00:00.000Z" },
41
+ ]);
42
+ expect(store.getSkillActivations("user-2")).toEqual([]);
43
+ });
44
+ it("spikes a skill and caps potential at the configured maximum", () => {
45
+ const store = createStore();
46
+ const now = new Date("2026-05-20T00:00:00.000Z");
47
+ spikeSkill({
48
+ userId: "user-1",
49
+ skillName: "incremental-implementation",
50
+ store,
51
+ now,
52
+ config: { spikeAmount: 1, maxPotential: 2, minTurnDecay: 0, halfLifeMinutes: 10 },
53
+ });
54
+ spikeSkill({
55
+ userId: "user-1",
56
+ skillName: "incremental-implementation",
57
+ store,
58
+ now,
59
+ config: { spikeAmount: 1.5, maxPotential: 2, minTurnDecay: 0, halfLifeMinutes: 10 },
60
+ });
61
+ expect(store.getSkillActivations("user-1")).toEqual([
62
+ { skillName: "incremental-implementation", potential: 2, lastDecayTime: now.toISOString() },
63
+ ]);
64
+ });
65
+ it("applies exponential decay by half-life and minimum per-turn decay", () => {
66
+ const lastDecayTime = "2026-05-20T00:00:00.000Z";
67
+ const thirtyMinutesLater = new Date("2026-05-20T00:30:00.000Z");
68
+ const sameTurn = new Date(lastDecayTime);
69
+ expect(decayPotential({
70
+ potential: 4,
71
+ lastDecayTime,
72
+ now: thirtyMinutesLater,
73
+ halfLifeMinutes: 10,
74
+ minTurnDecay: 0,
75
+ })).toBeCloseTo(0.5, 5);
76
+ expect(decayPotential({
77
+ potential: 4,
78
+ lastDecayTime,
79
+ now: sameTurn,
80
+ halfLifeMinutes: 10,
81
+ minTurnDecay: 0.05,
82
+ })).toBeCloseTo(3.8, 5);
83
+ });
84
+ it("returns thresholded prewarm hints sorted by decayed potential without writing decay state", () => {
85
+ const store = createStore();
86
+ const now = new Date("2026-05-20T00:10:00.000Z");
87
+ store.upsertSkillHints("testing-skill", "Test carefully", "testing/SKILL.md");
88
+ store.upsertSkillHints("incremental-implementation", "Ship in small steps", "implementation/SKILL.md");
89
+ store.upsertSkillActivations("user-1", [
90
+ { skillName: "testing-skill", potential: 4, lastDecayTime: "2026-05-20T00:00:00.000Z" },
91
+ { skillName: "incremental-implementation", potential: 2, lastDecayTime: "2026-05-20T00:00:00.000Z" },
92
+ { skillName: "missing-hints", potential: 4, lastDecayTime: "2026-05-20T00:00:00.000Z" },
93
+ ]);
94
+ const writeCountBeforeDetect = store.activationWriteCount;
95
+ const results = detectPrewarmSkills({
96
+ userId: "user-1",
97
+ store,
98
+ now,
99
+ threshold: 0.75,
100
+ config: { halfLifeMinutes: 10, minTurnDecay: 0 },
101
+ });
102
+ expect(results.map((result) => result.skillName)).toEqual([
103
+ "testing-skill",
104
+ "incremental-implementation",
105
+ ]);
106
+ expect(results[0].potential).toBeCloseTo(2, 5);
107
+ expect(results[1].potential).toBeCloseTo(1, 5);
108
+ expect(store.activationWriteCount).toBe(writeCountBeforeDetect);
109
+ expect(store.getSkillActivations("user-1").find((record) => record.skillName === "testing-skill")?.lastDecayTime)
110
+ .toBe("2026-05-20T00:00:00.000Z");
111
+ });
112
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,200 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { describe, expect, it } from "vitest";
6
+ import { handleMemoryWorkingTool } from "../tools/memory-working.js";
7
+ function parseToolJson(result) {
8
+ return JSON.parse(result.content[0].text);
9
+ }
10
+ describe("short-term working memory tools", () => {
11
+ it("writes refs and updates the Mermaid canvas during a 100-tool-call session", async () => {
12
+ const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-working-"));
13
+ const userId = "user-1";
14
+ const sessionKey = "session-100";
15
+ let workDir = "";
16
+ for (let index = 0; index < 100; index += 1) {
17
+ const offload = parseToolJson(await handleMemoryWorkingTool("memory_working_offload", {
18
+ workspacePath,
19
+ userId,
20
+ sessionKey,
21
+ payload: `tool output ${index} ${"x".repeat(120)}`,
22
+ title: `Tool call ${index}`,
23
+ summary: `Summary ${index}`,
24
+ kind: "tool_output",
25
+ }));
26
+ workDir = offload.state.workDir;
27
+ }
28
+ // Working memory now lives under the user home — never inside the
29
+ // workspace tree. The workDir is partitioned by user + workspace-hash
30
+ // + sessionKey.
31
+ expect(workDir.startsWith(join(homedir(), ".brainrouter", "work", userId))).toBe(true);
32
+ expect(workDir.endsWith(sessionKey)).toBe(true);
33
+ expect(existsSync(join(workDir, "steps.jsonl"))).toBe(true);
34
+ expect(existsSync(join(workDir, "canvas.mmd"))).toBe(true);
35
+ expect(existsSync(join(workDir, "refs"))).toBe(true);
36
+ expect(readFileSync(join(workDir, "canvas.mmd"), "utf8")).toContain("flowchart TD");
37
+ });
38
+ it("returns working context without raw payloads unless a ref is requested", async () => {
39
+ const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-working-context-"));
40
+ const userId = "user-1";
41
+ const sessionKey = "context-session";
42
+ const payload = "raw payload that should stay out of injected state";
43
+ const offload = parseToolJson(await handleMemoryWorkingTool("memory_working_offload", {
44
+ workspacePath,
45
+ userId,
46
+ sessionKey,
47
+ payload,
48
+ title: "Context payload",
49
+ summary: "Safe summary",
50
+ }));
51
+ const context = parseToolJson(await handleMemoryWorkingTool("memory_working_context", {
52
+ workspacePath,
53
+ userId,
54
+ sessionKey,
55
+ }));
56
+ expect(context.canvas).toContain("flowchart TD");
57
+ expect(JSON.stringify(context.state.injectedState)).not.toContain(payload);
58
+ expect(context.ref).toBeUndefined();
59
+ const withRef = parseToolJson(await handleMemoryWorkingTool("memory_working_context", {
60
+ workspacePath,
61
+ userId,
62
+ sessionKey,
63
+ nodeId: offload.nodeId,
64
+ }));
65
+ expect(withRef.ref.content).toContain(payload);
66
+ });
67
+ it("uses aggressive pressure above 85 percent estimated context fill", async () => {
68
+ const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-working-aggressive-"));
69
+ const userId = "user-1";
70
+ const sessionKey = "aggressive-session";
71
+ const result = parseToolJson(await handleMemoryWorkingTool("memory_working_offload", {
72
+ workspacePath,
73
+ userId,
74
+ sessionKey,
75
+ payload: "large result",
76
+ title: "Large result",
77
+ contextWindowTokens: 100,
78
+ estimatedTokens: 86,
79
+ }));
80
+ expect(result.pressureLevel).toBe("aggressive");
81
+ expect(result.state.injectedState.rawPayloadsIncluded).toBe(false);
82
+ });
83
+ it("returns an annotated canvas with the active working node highlighted", async () => {
84
+ const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-working-annotated-"));
85
+ const userId = "user-1";
86
+ const sessionKey = "annotated-session";
87
+ const offload = parseToolJson(await handleMemoryWorkingTool("memory_working_offload", {
88
+ workspacePath,
89
+ userId,
90
+ sessionKey,
91
+ payload: "active payload",
92
+ title: "Active payload",
93
+ summary: "Highlighted summary",
94
+ }));
95
+ const context = parseToolJson(await handleMemoryWorkingTool("memory_working_context", {
96
+ workspacePath,
97
+ userId,
98
+ sessionKey,
99
+ activeNodeId: offload.nodeId,
100
+ }));
101
+ expect(context.canvas).toContain(`style ${offload.nodeId} fill:#2b6cb0`);
102
+ expect(context.canvas).toContain("🌟 Active payload");
103
+ expect(context.annotatedCanvas).toBe(context.canvas);
104
+ });
105
+ it("partitions working memory files by user ID", async () => {
106
+ const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-working-users-"));
107
+ const sessionKey = "shared-session";
108
+ await handleMemoryWorkingTool("memory_working_offload", {
109
+ workspacePath,
110
+ userId: "user-a",
111
+ sessionKey,
112
+ payload: "payload for user a",
113
+ title: "User A",
114
+ });
115
+ await handleMemoryWorkingTool("memory_working_offload", {
116
+ workspacePath,
117
+ userId: "user-b",
118
+ sessionKey,
119
+ payload: "payload for user b",
120
+ title: "User B",
121
+ });
122
+ const userAContext = parseToolJson(await handleMemoryWorkingTool("memory_working_context", {
123
+ workspacePath,
124
+ userId: "user-a",
125
+ sessionKey,
126
+ }));
127
+ const userBContext = parseToolJson(await handleMemoryWorkingTool("memory_working_context", {
128
+ workspacePath,
129
+ userId: "user-b",
130
+ sessionKey,
131
+ }));
132
+ // workDir layout is `~/.brainrouter/work/<userId>/<workspaceHash>/<sessionKey>`.
133
+ // We anchor on the user partition + session terminator; the workspace
134
+ // hash slot in between is a sha256 of the resolved workspace path.
135
+ expect(userAContext.workDir.startsWith(join(homedir(), ".brainrouter", "work", "user-a"))).toBe(true);
136
+ expect(userAContext.workDir.endsWith(sessionKey)).toBe(true);
137
+ expect(userBContext.workDir.startsWith(join(homedir(), ".brainrouter", "work", "user-b"))).toBe(true);
138
+ expect(userBContext.workDir.endsWith(sessionKey)).toBe(true);
139
+ expect(userAContext.canvas).toContain("User A");
140
+ expect(userAContext.canvas).not.toContain("User B");
141
+ expect(userBContext.canvas).toContain("User B");
142
+ expect(userBContext.canvas).not.toContain("User A");
143
+ });
144
+ it("uses the authenticated MCP user when a tool call omits userId", async () => {
145
+ const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-working-default-user-"));
146
+ const sessionKey = "authenticated-session";
147
+ await handleMemoryWorkingTool("memory_working_offload", {
148
+ workspacePath,
149
+ sessionKey,
150
+ payload: "payload for authenticated user",
151
+ title: "Authenticated user",
152
+ }, { defaultUserId: "admin" });
153
+ const adminContext = parseToolJson(await handleMemoryWorkingTool("memory_working_context", {
154
+ workspacePath,
155
+ sessionKey,
156
+ }, { defaultUserId: "admin" }));
157
+ const fallbackContext = parseToolJson(await handleMemoryWorkingTool("memory_working_context", {
158
+ workspacePath,
159
+ sessionKey,
160
+ }));
161
+ expect(adminContext.workDir.startsWith(join(homedir(), ".brainrouter", "work", "admin"))).toBe(true);
162
+ expect(adminContext.workDir.endsWith(sessionKey)).toBe(true);
163
+ expect(adminContext.canvas).toContain("Authenticated user");
164
+ expect(fallbackContext.workDir.startsWith(join(homedir(), ".brainrouter", "work", "default"))).toBe(true);
165
+ expect(fallbackContext.workDir.endsWith(sessionKey)).toBe(true);
166
+ expect(fallbackContext.canvas).not.toContain("Authenticated user");
167
+ });
168
+ it("routes foreign absolute workspace paths to the user fallback instead of the process cwd", async () => {
169
+ if (process.platform === "win32")
170
+ return;
171
+ const foreignWorkspacePath = "c:\\Users\\Miu\\Desktop\\Tung\\review paper 1";
172
+ const pollutedPath = resolve(foreignWorkspacePath);
173
+ rmSync(pollutedPath, { recursive: true, force: true });
174
+ const result = parseToolJson(await handleMemoryWorkingTool("memory_working_offload", {
175
+ workspacePath: foreignWorkspacePath,
176
+ userId: "user-1",
177
+ sessionKey: "foreign-session",
178
+ payload: "foreign path payload",
179
+ title: "Foreign path payload",
180
+ }));
181
+ expect(result.state.workDir).toContain(join(homedir(), ".brainrouter", "work", "user-1"));
182
+ expect(result.state.workDir).not.toContain(foreignWorkspacePath);
183
+ expect(existsSync(join(result.state.workDir, "refs"))).toBe(true);
184
+ expect(existsSync(pollutedPath)).toBe(false);
185
+ rmSync(result.state.workDir, { recursive: true, force: true });
186
+ });
187
+ it("treats listed workspace IDs as fallback-store IDs, not relative paths", async () => {
188
+ const sessionKey = "workspace-id-session";
189
+ const result = parseToolJson(await handleMemoryWorkingTool("memory_working_offload", {
190
+ workspacePath: "abc123abc123",
191
+ userId: "workspace-id-user",
192
+ sessionKey,
193
+ payload: "payload for listed workspace id",
194
+ title: "Listed workspace id",
195
+ }));
196
+ expect(result.state.workDir).toBe(join(homedir(), ".brainrouter", "work", "workspace-id-user", "abc123abc123", sessionKey));
197
+ expect(existsSync(join(resolve("abc123abc123"), ".brainrouter"))).toBe(false);
198
+ rmSync(result.state.workDir, { recursive: true, force: true });
199
+ });
200
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, rmSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { describe, expect, it } from "vitest";
6
+ import { getSafeWorkspacePath, isForeignAbsolutePath } from "../resolver.js";
7
+ import { handleMemoryResolveSession } from "../tools/memory_resolve_session.js";
8
+ function mcpCacheFile(workspacePath, name) {
9
+ const hash = createHash("sha256").update(workspacePath).digest("hex").slice(0, 12);
10
+ return join(homedir(), ".brainrouter", "mcp-cache", hash, name);
11
+ }
12
+ function parseToolJson(result) {
13
+ return JSON.parse(result.content[0].text);
14
+ }
15
+ describe("workspace path compatibility", () => {
16
+ it("detects Windows absolute paths as foreign on POSIX hosts", () => {
17
+ expect(isForeignAbsolutePath("c:\\Users\\Miu\\Desktop\\Tung\\review paper 1")).toBe(process.platform !== "win32");
18
+ });
19
+ it("uses a fallback workspace for foreign absolute paths", () => {
20
+ const foreignWorkspacePath = "c:\\Users\\Miu\\Desktop\\Tung\\review paper 1";
21
+ const safePath = getSafeWorkspacePath(foreignWorkspacePath);
22
+ if (process.platform === "win32") {
23
+ expect(safePath).toBe(resolve(foreignWorkspacePath));
24
+ }
25
+ else {
26
+ expect(safePath).toContain(join(homedir(), ".brainrouter", "fallback-workspaces"));
27
+ expect(safePath).not.toContain(foreignWorkspacePath);
28
+ }
29
+ });
30
+ it("caches resolved sessions under the user-home MCP cache for foreign absolute paths", async () => {
31
+ if (process.platform === "win32")
32
+ return;
33
+ const foreignWorkspacePath = "c:\\Users\\Miu\\Desktop\\Tung\\review paper 1";
34
+ const pollutedPath = resolve(foreignWorkspacePath);
35
+ const safePath = getSafeWorkspacePath(foreignWorkspacePath);
36
+ const cacheFile = mcpCacheFile(safePath, "active_session.json");
37
+ rmSync(pollutedPath, { recursive: true, force: true });
38
+ rmSync(safePath, { recursive: true, force: true });
39
+ rmSync(cacheFile, { force: true });
40
+ const result = parseToolJson(await handleMemoryResolveSession({
41
+ workspacePath: foreignWorkspacePath,
42
+ }));
43
+ const cached = JSON.parse(readFileSync(cacheFile, "utf8"));
44
+ expect(result.source).toBe("new_workspace_generation");
45
+ expect(cached.sessionKey).toBe(result.sessionKey);
46
+ expect(cached.workspace).toBe(foreignWorkspacePath);
47
+ expect(cached.cacheWorkspace).toBe(safePath);
48
+ // Critical regression guard: neither the polluted Windows-style path
49
+ // NOR the safe fallback workspace should have a `.brainrouter/` shell
50
+ // created inside it. The cache lives entirely under `~/.brainrouter/`.
51
+ expect(existsSync(pollutedPath)).toBe(false);
52
+ expect(existsSync(join(safePath, ".brainrouter"))).toBe(false);
53
+ rmSync(safePath, { recursive: true, force: true });
54
+ rmSync(cacheFile, { force: true });
55
+ });
56
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { scaffoldSkill, updateSkillSection, updateDocSection } from '../writer.js';
3
+ import { readFileSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'fs';
4
+ import { join, resolve } from 'path';
5
+ import { tmpdir } from 'os';
6
+ describe('writer.ts', () => {
7
+ const localRoot = join(tmpdir(), 'brainrouter-writer-test-local');
8
+ const globalRoot = join(tmpdir(), 'brainrouter-writer-test-global');
9
+ beforeEach(() => {
10
+ rmSync(localRoot, { recursive: true, force: true });
11
+ rmSync(globalRoot, { recursive: true, force: true });
12
+ mkdirSync(localRoot, { recursive: true });
13
+ mkdirSync(globalRoot, { recursive: true });
14
+ });
15
+ it('should scaffold a new skill in targetRoot (local)', () => {
16
+ const params = {
17
+ name: 'new-skill',
18
+ category: 'agent',
19
+ description: 'New description',
20
+ targetRoot: localRoot,
21
+ project: 'TestProject',
22
+ };
23
+ const path = scaffoldSkill(params);
24
+ expect(existsSync(path)).toBe(true);
25
+ expect(path).toContain(localRoot);
26
+ expect(path).toContain(join('projects', 'TestProject', 'skills', 'agent', 'new-skill', 'SKILL.md'));
27
+ const content = readFileSync(path, 'utf-8');
28
+ expect(content).toContain('name: new-skill');
29
+ expect(content).toContain('description: New description');
30
+ });
31
+ it('should scaffold a new skill in targetRoot (global)', () => {
32
+ const params = {
33
+ name: 'global-skill',
34
+ category: 'universal',
35
+ description: 'Global description',
36
+ targetRoot: globalRoot,
37
+ project: 'Universal',
38
+ };
39
+ const path = scaffoldSkill(params);
40
+ expect(existsSync(path)).toBe(true);
41
+ expect(path).toContain(globalRoot);
42
+ expect(path).toContain(join('projects', 'Universal', 'skills', 'universal', 'global-skill', 'SKILL.md'));
43
+ const content = readFileSync(path, 'utf-8');
44
+ expect(content).toContain('name: global-skill');
45
+ });
46
+ it('should scaffold a new project-specific skill in targetRoot (global)', () => {
47
+ const params = {
48
+ name: 'storage-skill',
49
+ category: 'api',
50
+ description: 'Storage description',
51
+ targetRoot: globalRoot,
52
+ project: 'TestProject',
53
+ };
54
+ const path = scaffoldSkill(params);
55
+ expect(existsSync(path)).toBe(true);
56
+ expect(path).toContain(join(globalRoot, 'projects', 'TestProject', 'skills', 'api', 'storage-skill', 'SKILL.md'));
57
+ const content = readFileSync(path, 'utf-8');
58
+ expect(content).toContain('name: storage-skill');
59
+ });
60
+ it('should block writes outside of targetRoot', () => {
61
+ const outsidePath = resolve('/tmp/not-my-project/SKILL.md');
62
+ expect(() => {
63
+ updateSkillSection(outsidePath, 'workflow', 'content', localRoot);
64
+ }).toThrow(/WRITE BLOCKED/);
65
+ });
66
+ it('should update a skill section and preserve frontmatter', () => {
67
+ const skillDir = join(localRoot, 'projects', 'TestProject', 'skills', 'agent', 'test-skill');
68
+ mkdirSync(skillDir, { recursive: true });
69
+ const skillPath = join(skillDir, 'SKILL.md');
70
+ const initial = `---
71
+ name: test-skill
72
+ description: old desc
73
+ ---
74
+ ## Workflow
75
+ Old step.
76
+ `;
77
+ writeFileSync(skillPath, initial);
78
+ updateSkillSection(skillPath, 'workflow', 'New step.', localRoot);
79
+ const updated = readFileSync(skillPath, 'utf-8');
80
+ expect(updated).toContain('description: old desc');
81
+ expect(updated).toContain('New step.');
82
+ expect(updated).not.toContain('Old step.');
83
+ });
84
+ it('should update a doc section in localRoot', () => {
85
+ const docDir = join(localRoot, 'docs', 'api');
86
+ mkdirSync(docDir, { recursive: true });
87
+ const docPath = join(docDir, 'API.md');
88
+ writeFileSync(docPath, '## Endpoints\n- GET /old');
89
+ updateDocSection(docPath, 'Endpoints', '- GET /new', localRoot);
90
+ const updated = readFileSync(docPath, 'utf-8');
91
+ expect(updated).toContain('- GET /new');
92
+ expect(updated).not.toContain('- GET /old');
93
+ });
94
+ });
@@ -0,0 +1,4 @@
1
+ export declare function hashPassword(password: string): Promise<string>;
2
+ export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
3
+ export declare function signJwt(payload: Record<string, unknown>, secret: string, expiresInSecs: number): string;
4
+ export declare function verifyJwt(token: string, secret: string): Record<string, unknown> | null;
@@ -0,0 +1,54 @@
1
+ import { createHmac, randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
2
+ import { promisify } from "node:util";
3
+ const scrypt = promisify(scryptCallback);
4
+ function base64UrlEncode(input) {
5
+ const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
6
+ return buf.toString("base64url");
7
+ }
8
+ function base64UrlDecode(input) {
9
+ return Buffer.from(input, "base64url");
10
+ }
11
+ export async function hashPassword(password) {
12
+ const salt = randomBytes(16);
13
+ const derived = await scrypt(password, salt, 64);
14
+ return `${salt.toString("hex")}:${derived.toString("hex")}`;
15
+ }
16
+ export async function verifyPassword(password, stored) {
17
+ const [saltHex, hashHex] = stored.split(":");
18
+ if (!saltHex || !hashHex)
19
+ return false;
20
+ const salt = Buffer.from(saltHex, "hex");
21
+ const storedHash = Buffer.from(hashHex, "hex");
22
+ const derived = await scrypt(password, salt, storedHash.length);
23
+ if (derived.length !== storedHash.length)
24
+ return false;
25
+ return timingSafeEqual(derived, storedHash);
26
+ }
27
+ export function signJwt(payload, secret, expiresInSecs) {
28
+ const now = Math.floor(Date.now() / 1000);
29
+ const body = { ...payload, iat: now, exp: now + expiresInSecs };
30
+ const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
31
+ const claims = base64UrlEncode(JSON.stringify(body));
32
+ const signature = createHmac("sha256", secret).update(`${header}.${claims}`).digest("base64url");
33
+ return `${header}.${claims}.${signature}`;
34
+ }
35
+ export function verifyJwt(token, secret) {
36
+ const parts = token.split(".");
37
+ if (parts.length !== 3)
38
+ return null;
39
+ const [header, claims, signature] = parts;
40
+ const expected = createHmac("sha256", secret).update(`${header}.${claims}`).digest();
41
+ const actual = Buffer.from(signature, "base64url");
42
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
43
+ return null;
44
+ try {
45
+ const payload = JSON.parse(base64UrlDecode(claims).toString("utf8"));
46
+ const exp = typeof payload.exp === "number" ? payload.exp : 0;
47
+ if (exp <= Math.floor(Date.now() / 1000))
48
+ return null;
49
+ return payload;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
@@ -0,0 +1,12 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ export type AuthedRequest = Request & {
3
+ userId?: string;
4
+ isAdmin?: boolean;
5
+ email?: string;
6
+ };
7
+ export declare const USING_FALLBACK_JWT_SECRET: boolean;
8
+ export declare const JWT_SECRET: string;
9
+ export declare function requireAuth(req: AuthedRequest, res: Response, next: NextFunction): void;
10
+ export declare function requireJwt(req: AuthedRequest, res: Response, next: NextFunction): void;
11
+ export declare function requireAnyAuth(req: AuthedRequest, res: Response, next: NextFunction): void;
12
+ export declare function requireAdmin(req: AuthedRequest, res: Response, next: NextFunction): void;