palaryn 0.1.0 → 0.3.2

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 (344) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/anomaly/detector.d.ts +7 -4
  5. package/dist/src/anomaly/detector.d.ts.map +1 -1
  6. package/dist/src/anomaly/detector.js +22 -12
  7. package/dist/src/anomaly/detector.js.map +1 -1
  8. package/dist/src/audit/logger.d.ts +10 -0
  9. package/dist/src/audit/logger.d.ts.map +1 -1
  10. package/dist/src/audit/logger.js +52 -38
  11. package/dist/src/audit/logger.js.map +1 -1
  12. package/dist/src/auth/routes.d.ts.map +1 -1
  13. package/dist/src/auth/routes.js +35 -0
  14. package/dist/src/auth/routes.js.map +1 -1
  15. package/dist/src/budget/manager.d.ts +5 -0
  16. package/dist/src/budget/manager.d.ts.map +1 -1
  17. package/dist/src/budget/manager.js +32 -0
  18. package/dist/src/budget/manager.js.map +1 -1
  19. package/dist/src/budget/model-pricing.d.ts +20 -0
  20. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  21. package/dist/src/budget/model-pricing.js +107 -0
  22. package/dist/src/budget/model-pricing.js.map +1 -0
  23. package/dist/src/budget/usage-extractor.d.ts +3 -1
  24. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  25. package/dist/src/budget/usage-extractor.js +47 -3
  26. package/dist/src/budget/usage-extractor.js.map +1 -1
  27. package/dist/src/config/defaults.d.ts.map +1 -1
  28. package/dist/src/config/defaults.js +65 -13
  29. package/dist/src/config/defaults.js.map +1 -1
  30. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  31. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  32. package/dist/src/dlp/tool-patterns.js +34 -0
  33. package/dist/src/dlp/tool-patterns.js.map +1 -0
  34. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  35. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  36. package/dist/src/executor/filesystem-executor.js +192 -0
  37. package/dist/src/executor/filesystem-executor.js.map +1 -0
  38. package/dist/src/executor/http-executor.d.ts.map +1 -1
  39. package/dist/src/executor/http-executor.js +22 -2
  40. package/dist/src/executor/http-executor.js.map +1 -1
  41. package/dist/src/executor/index.d.ts +4 -0
  42. package/dist/src/executor/index.d.ts.map +1 -1
  43. package/dist/src/executor/index.js +9 -1
  44. package/dist/src/executor/index.js.map +1 -1
  45. package/dist/src/executor/shell-executor.d.ts +22 -0
  46. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  47. package/dist/src/executor/shell-executor.js +119 -0
  48. package/dist/src/executor/shell-executor.js.map +1 -0
  49. package/dist/src/executor/sql-executor.d.ts +29 -0
  50. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  51. package/dist/src/executor/sql-executor.js +114 -0
  52. package/dist/src/executor/sql-executor.js.map +1 -0
  53. package/dist/src/executor/websocket-executor.d.ts +26 -0
  54. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  55. package/dist/src/executor/websocket-executor.js +205 -0
  56. package/dist/src/executor/websocket-executor.js.map +1 -0
  57. package/dist/src/interceptor/index.d.ts +2 -0
  58. package/dist/src/interceptor/index.d.ts.map +1 -0
  59. package/dist/src/interceptor/index.js +6 -0
  60. package/dist/src/interceptor/index.js.map +1 -0
  61. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  62. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  63. package/dist/src/interceptor/provider-interceptor.js +302 -0
  64. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  65. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  66. package/dist/src/mcp/auth-verifier.js +3 -2
  67. package/dist/src/mcp/auth-verifier.js.map +1 -1
  68. package/dist/src/mcp/bridge.d.ts +14 -10
  69. package/dist/src/mcp/bridge.d.ts.map +1 -1
  70. package/dist/src/mcp/bridge.js +51 -227
  71. package/dist/src/mcp/bridge.js.map +1 -1
  72. package/dist/src/mcp/http-transport.d.ts +2 -0
  73. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  74. package/dist/src/mcp/http-transport.js +117 -66
  75. package/dist/src/mcp/http-transport.js.map +1 -1
  76. package/dist/src/mcp/internal-auth.d.ts +13 -0
  77. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  78. package/dist/src/mcp/internal-auth.js +12 -0
  79. package/dist/src/mcp/internal-auth.js.map +1 -0
  80. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  81. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  82. package/dist/src/mcp/tool-definitions.js +491 -0
  83. package/dist/src/mcp/tool-definitions.js.map +1 -0
  84. package/dist/src/middleware/auth.js.map +1 -1
  85. package/dist/src/middleware/session.js.map +1 -1
  86. package/dist/src/middleware/validate.d.ts +8 -0
  87. package/dist/src/middleware/validate.d.ts.map +1 -1
  88. package/dist/src/middleware/validate.js +45 -0
  89. package/dist/src/middleware/validate.js.map +1 -1
  90. package/dist/src/policy/engine.d.ts +4 -0
  91. package/dist/src/policy/engine.d.ts.map +1 -1
  92. package/dist/src/policy/engine.js +117 -0
  93. package/dist/src/policy/engine.js.map +1 -1
  94. package/dist/src/saas/routes.d.ts.map +1 -1
  95. package/dist/src/saas/routes.js +355 -22
  96. package/dist/src/saas/routes.js.map +1 -1
  97. package/dist/src/server/app.d.ts.map +1 -1
  98. package/dist/src/server/app.js +24 -3
  99. package/dist/src/server/app.js.map +1 -1
  100. package/dist/src/server/gateway.d.ts.map +1 -1
  101. package/dist/src/server/gateway.js +17 -0
  102. package/dist/src/server/gateway.js.map +1 -1
  103. package/dist/src/server/index.d.ts.map +1 -1
  104. package/dist/src/server/index.js +18 -0
  105. package/dist/src/server/index.js.map +1 -1
  106. package/dist/src/storage/interfaces.d.ts +14 -3
  107. package/dist/src/storage/interfaces.d.ts.map +1 -1
  108. package/dist/src/storage/memory.d.ts +2 -0
  109. package/dist/src/storage/memory.d.ts.map +1 -1
  110. package/dist/src/storage/memory.js +6 -0
  111. package/dist/src/storage/memory.js.map +1 -1
  112. package/dist/src/storage/postgres.d.ts +5 -0
  113. package/dist/src/storage/postgres.d.ts.map +1 -1
  114. package/dist/src/storage/postgres.js +16 -0
  115. package/dist/src/storage/postgres.js.map +1 -1
  116. package/dist/src/storage/redis.d.ts +10 -0
  117. package/dist/src/storage/redis.d.ts.map +1 -1
  118. package/dist/src/storage/redis.js +65 -0
  119. package/dist/src/storage/redis.js.map +1 -1
  120. package/dist/src/types/budget.d.ts +4 -0
  121. package/dist/src/types/budget.d.ts.map +1 -1
  122. package/dist/src/types/config.d.ts +58 -0
  123. package/dist/src/types/config.d.ts.map +1 -1
  124. package/dist/src/types/events.d.ts +1 -0
  125. package/dist/src/types/events.d.ts.map +1 -1
  126. package/dist/src/types/policy.d.ts +11 -1
  127. package/dist/src/types/policy.d.ts.map +1 -1
  128. package/dist/src/types/tool-result.d.ts +11 -0
  129. package/dist/src/types/tool-result.d.ts.map +1 -1
  130. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  131. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  132. package/dist/tests/unit/app-routes.test.js +715 -0
  133. package/dist/tests/unit/app-routes.test.js.map +1 -0
  134. package/dist/tests/unit/audit-logger.test.js +105 -0
  135. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  136. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  137. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  138. package/dist/tests/unit/auth-providers.test.js +279 -0
  139. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  140. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  141. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  142. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  143. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  144. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  145. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  146. package/dist/tests/unit/auth-verifier.test.js +505 -0
  147. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  148. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  149. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  150. package/dist/tests/unit/billing-routes.test.js +432 -0
  151. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  152. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  153. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  154. package/dist/tests/unit/config-defaults.test.js +119 -0
  155. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  156. package/dist/tests/unit/defaults.test.js +0 -10
  157. package/dist/tests/unit/defaults.test.js.map +1 -1
  158. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  159. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  160. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  161. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  162. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  163. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  164. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  165. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  166. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  167. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  168. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  169. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  170. package/dist/tests/unit/logger.test.d.ts +2 -0
  171. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  172. package/dist/tests/unit/logger.test.js +97 -0
  173. package/dist/tests/unit/logger.test.js.map +1 -0
  174. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  175. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  176. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  177. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  178. package/dist/tests/unit/metrics.test.js +102 -0
  179. package/dist/tests/unit/metrics.test.js.map +1 -1
  180. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  181. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  182. package/dist/tests/unit/model-pricing.test.js +87 -0
  183. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  184. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  185. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  186. package/dist/tests/unit/oauth-stores.test.js +260 -0
  187. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  188. package/dist/tests/unit/policy-engine.test.js +466 -0
  189. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  190. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  191. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  192. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  193. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  194. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  195. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  196. package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
  197. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  198. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  199. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  200. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  201. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  202. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  203. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  204. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  205. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  206. package/dist/tests/unit/saas-routes.test.js +3 -3
  207. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  208. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  209. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  210. package/dist/tests/unit/shell-executor.test.js +145 -0
  211. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  212. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  213. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  214. package/dist/tests/unit/sql-executor.test.js +177 -0
  215. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  216. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  217. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  218. package/dist/tests/unit/stream-proxy.test.js +147 -0
  219. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  220. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  221. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  222. package/dist/tests/unit/tool-definitions.test.js +184 -0
  223. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  224. package/dist/tests/unit/usage-extractor.test.js +140 -0
  225. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  226. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  227. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  228. package/dist/tests/unit/webhook-handler.test.js +453 -0
  229. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  230. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  231. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  232. package/dist/tests/unit/webhook-routes.test.js +69 -0
  233. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  234. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  235. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  236. package/dist/tests/unit/websocket-executor.test.js +121 -0
  237. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  238. package/package.json +8 -2
  239. package/policy-packs/demo_fail.yaml +41 -0
  240. package/policy-packs/full_tools.yaml +136 -0
  241. package/src/admin/index.ts +1 -0
  242. package/src/admin/routes.ts +509 -0
  243. package/src/admin/templates.ts +572 -0
  244. package/src/anomaly/detector.ts +730 -0
  245. package/src/anomaly/index.ts +1 -0
  246. package/src/approval/manager.ts +569 -0
  247. package/src/approval/webhook.ts +133 -0
  248. package/src/audit/logger.ts +490 -0
  249. package/src/auth/index.ts +5 -0
  250. package/src/auth/password.ts +21 -0
  251. package/src/auth/pkce.ts +22 -0
  252. package/src/auth/providers.ts +208 -0
  253. package/src/auth/routes.ts +561 -0
  254. package/src/auth/session.ts +84 -0
  255. package/src/billing/index.ts +6 -0
  256. package/src/billing/plan-enforcer.ts +135 -0
  257. package/src/billing/routes.ts +229 -0
  258. package/src/billing/stripe-client.ts +58 -0
  259. package/src/billing/webhook-handler.ts +182 -0
  260. package/src/billing/webhook-routes.ts +28 -0
  261. package/src/budget/manager.ts +679 -0
  262. package/src/budget/model-pricing.ts +119 -0
  263. package/src/budget/usage-extractor.ts +214 -0
  264. package/src/cli.ts +91 -0
  265. package/src/config/defaults.ts +261 -0
  266. package/src/config/validate.ts +88 -0
  267. package/src/dlp/composite-scanner.ts +213 -0
  268. package/src/dlp/index.ts +9 -0
  269. package/src/dlp/interfaces.ts +34 -0
  270. package/src/dlp/patterns.ts +30 -0
  271. package/src/dlp/prompt-injection-backend.ts +181 -0
  272. package/src/dlp/prompt-injection-patterns.ts +302 -0
  273. package/src/dlp/regex-backend.ts +181 -0
  274. package/src/dlp/scanner.ts +502 -0
  275. package/src/dlp/text-normalizer.ts +225 -0
  276. package/src/dlp/tool-patterns.ts +35 -0
  277. package/src/dlp/trufflehog-backend.ts +190 -0
  278. package/src/executor/filesystem-executor.ts +196 -0
  279. package/src/executor/http-executor.ts +349 -0
  280. package/src/executor/index.ts +9 -0
  281. package/src/executor/interfaces.ts +11 -0
  282. package/src/executor/noop-executor.ts +23 -0
  283. package/src/executor/registry.ts +64 -0
  284. package/src/executor/shell-executor.ts +148 -0
  285. package/src/executor/slack-executor.ts +176 -0
  286. package/src/executor/sql-executor.ts +146 -0
  287. package/src/executor/websocket-executor.ts +211 -0
  288. package/src/index.ts +24 -0
  289. package/src/interceptor/index.ts +1 -0
  290. package/src/interceptor/provider-interceptor.ts +315 -0
  291. package/src/mcp/auth-verifier.ts +152 -0
  292. package/src/mcp/bridge.ts +703 -0
  293. package/src/mcp/http-transport.ts +698 -0
  294. package/src/mcp/index.ts +9 -0
  295. package/src/mcp/internal-auth.ts +14 -0
  296. package/src/mcp/oauth-pages.ts +139 -0
  297. package/src/mcp/oauth-postgres-stores.ts +278 -0
  298. package/src/mcp/oauth-provider.ts +536 -0
  299. package/src/mcp/oauth-stores.ts +202 -0
  300. package/src/mcp/server.ts +55 -0
  301. package/src/mcp/tool-definitions.ts +562 -0
  302. package/src/metrics/collector.ts +357 -0
  303. package/src/metrics/index.ts +1 -0
  304. package/src/middleware/auth.ts +814 -0
  305. package/src/middleware/session.ts +85 -0
  306. package/src/middleware/validate.ts +130 -0
  307. package/src/policy/engine.ts +815 -0
  308. package/src/policy/index.ts +2 -0
  309. package/src/policy/opa-engine.ts +829 -0
  310. package/src/proxy/forward-proxy.ts +649 -0
  311. package/src/proxy/index.ts +1 -0
  312. package/src/ratelimit/limiter.ts +196 -0
  313. package/src/replay/engine.ts +142 -0
  314. package/src/replay/index.ts +1 -0
  315. package/src/saas/index.ts +1 -0
  316. package/src/saas/routes.ts +2178 -0
  317. package/src/server/app.ts +985 -0
  318. package/src/server/errors.ts +49 -0
  319. package/src/server/gateway.ts +1130 -0
  320. package/src/server/index.ts +307 -0
  321. package/src/server/logger.ts +255 -0
  322. package/src/server/stream-proxy.ts +202 -0
  323. package/src/storage/file-persistence.ts +315 -0
  324. package/src/storage/index.ts +4 -0
  325. package/src/storage/interfaces.ts +287 -0
  326. package/src/storage/memory.ts +686 -0
  327. package/src/storage/postgres.ts +1831 -0
  328. package/src/storage/redis.ts +835 -0
  329. package/src/tracing/index.ts +1 -0
  330. package/src/tracing/provider.ts +100 -0
  331. package/src/trust/calculator.ts +141 -0
  332. package/src/trust/index.ts +7 -0
  333. package/src/types/budget.ts +36 -0
  334. package/src/types/config.ts +278 -0
  335. package/src/types/events.ts +41 -0
  336. package/src/types/express.d.ts +14 -0
  337. package/src/types/index.ts +7 -0
  338. package/src/types/policy.ts +83 -0
  339. package/src/types/stripe-config.ts +11 -0
  340. package/src/types/subscription.ts +59 -0
  341. package/src/types/tool-call.ts +47 -0
  342. package/src/types/tool-result.ts +82 -0
  343. package/src/types/user.ts +125 -0
  344. package/tsconfig.json +24 -0
@@ -0,0 +1,561 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { randomUUID } from 'crypto';
3
+ import { OAuthConfig, OAuthProvider, OAuthProfile } from '../types/user';
4
+ import {
5
+ UserStore,
6
+ OAuthAccountStore,
7
+ SessionStore,
8
+ WorkspaceStore,
9
+ WorkspaceMemberStore,
10
+ UserApiKeyStore,
11
+ } from '../storage/interfaces';
12
+ import { generateCodeVerifier } from './pkce';
13
+ import { generateNonce } from './pkce';
14
+ import {
15
+ buildGoogleAuthUrl, exchangeGoogleCode, getGoogleUserInfo,
16
+ buildGitHubAuthUrl, exchangeGitHubCode, getGitHubUserInfo,
17
+ } from './providers';
18
+ import {
19
+ generateSessionId, encryptState, decryptState, encryptToken,
20
+ SESSION_COOKIE_NAME, STATE_COOKIE_NAME,
21
+ } from './session';
22
+ import { hashPassword, verifyPassword } from './password';
23
+
24
+ export interface AuthRouteDeps {
25
+ config: OAuthConfig;
26
+ userStore: UserStore;
27
+ oauthAccountStore: OAuthAccountStore;
28
+ sessionStore: SessionStore;
29
+ workspaceStore: WorkspaceStore;
30
+ workspaceMemberStore: WorkspaceMemberStore;
31
+ userApiKeyStore?: UserApiKeyStore;
32
+ }
33
+
34
+ export function createAuthRouter(deps: AuthRouteDeps): Router {
35
+ const router = Router();
36
+ const { config, userStore, oauthAccountStore, sessionStore, workspaceStore, workspaceMemberStore, userApiKeyStore } = deps;
37
+ const isProduction = process.env.NODE_ENV === 'production';
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // GET /auth/:provider/authorize — redirect to OAuth provider
41
+ // ---------------------------------------------------------------------------
42
+ router.get('/:provider/authorize', (req: Request, res: Response) => {
43
+ const provider = req.params.provider as OAuthProvider;
44
+ const baseUrl = `${req.protocol}://${req.get('host')}`;
45
+
46
+ if (provider === 'google' && config.google) {
47
+ const redirectUri = config.google.redirect_uri || `${baseUrl}/auth/google/callback`;
48
+ const nonce = generateNonce();
49
+ const codeVerifier = generateCodeVerifier();
50
+
51
+ const state = encryptState({
52
+ provider: 'google',
53
+ code_verifier: codeVerifier,
54
+ nonce,
55
+ redirect_uri: redirectUri,
56
+ created_at: Date.now(),
57
+ }, config.session_secret);
58
+
59
+ const { url } = buildGoogleAuthUrl(config.google, redirectUri, state, codeVerifier);
60
+
61
+ res.cookie(STATE_COOKIE_NAME, state, {
62
+ httpOnly: true,
63
+ secure: isProduction,
64
+ sameSite: 'lax',
65
+ maxAge: 600_000, // 10 min
66
+ path: '/',
67
+ });
68
+
69
+ res.redirect(url);
70
+ } else if (provider === 'github' && config.github) {
71
+ const redirectUri = config.github.redirect_uri || `${baseUrl}/auth/github/callback`;
72
+ const nonce = generateNonce();
73
+
74
+ const state = encryptState({
75
+ provider: 'github',
76
+ nonce,
77
+ redirect_uri: redirectUri,
78
+ created_at: Date.now(),
79
+ }, config.session_secret);
80
+
81
+ const url = buildGitHubAuthUrl(config.github, redirectUri, state);
82
+
83
+ res.cookie(STATE_COOKIE_NAME, state, {
84
+ httpOnly: true,
85
+ secure: isProduction,
86
+ sameSite: 'lax',
87
+ maxAge: 600_000,
88
+ path: '/',
89
+ });
90
+
91
+ res.redirect(url);
92
+ } else {
93
+ res.status(400).json({ error: `Unsupported or unconfigured provider: ${provider}` });
94
+ }
95
+ });
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // GET /auth/:provider/callback — handle OAuth callback
99
+ // ---------------------------------------------------------------------------
100
+ router.get('/:provider/callback', async (req: Request, res: Response) => {
101
+ const provider = req.params.provider as OAuthProvider;
102
+ const { code, state: stateParam, error } = req.query as Record<string, string>;
103
+
104
+ if (error) {
105
+ return res.redirect(`/login?error=${encodeURIComponent(error)}`);
106
+ }
107
+
108
+ if (!code || !stateParam) {
109
+ return res.redirect('/login?error=missing_params');
110
+ }
111
+
112
+ // Validate state cookie
113
+ const stateCookie = req.cookies?.[STATE_COOKIE_NAME];
114
+ if (!stateCookie || stateCookie !== stateParam) {
115
+ return res.redirect('/login?error=invalid_state');
116
+ }
117
+
118
+ // Clear state cookie
119
+ res.clearCookie(STATE_COOKIE_NAME, { path: '/' });
120
+
121
+ let flowState;
122
+ try {
123
+ flowState = decryptState(stateCookie, config.session_secret);
124
+ } catch {
125
+ return res.redirect('/login?error=invalid_state');
126
+ }
127
+
128
+ // Check state expiry (10 min max)
129
+ if (Date.now() - flowState.created_at > 600_000) {
130
+ return res.redirect('/login?error=state_expired');
131
+ }
132
+
133
+ try {
134
+ let profile: OAuthProfile;
135
+ let accessToken: string;
136
+ let refreshToken: string | undefined;
137
+
138
+ if (provider === 'google' && config.google) {
139
+ const tokens = await exchangeGoogleCode(
140
+ config.google,
141
+ flowState.redirect_uri,
142
+ code,
143
+ flowState.code_verifier || '',
144
+ );
145
+ accessToken = tokens.access_token;
146
+ refreshToken = tokens.refresh_token;
147
+ profile = await getGoogleUserInfo(tokens.access_token);
148
+ } else if (provider === 'github' && config.github) {
149
+ const tokens = await exchangeGitHubCode(config.github, code);
150
+ accessToken = tokens.access_token;
151
+ profile = await getGitHubUserInfo(tokens.access_token);
152
+ } else {
153
+ return res.redirect('/login?error=unsupported_provider');
154
+ }
155
+
156
+ // Find or create user + link OAuth account
157
+ const { user, isNewUser, needsLinking } = findOrCreateUser(profile, accessToken, refreshToken);
158
+
159
+ // If an existing account with the same email was found but OAuth is not
160
+ // linked, redirect to login with a message instead of auto-linking.
161
+ if (needsLinking) {
162
+ return res.redirect('/login?error=account_exists&message=' +
163
+ encodeURIComponent('An account with this email already exists. Please sign in with your password first, then link your OAuth provider from settings.'));
164
+ }
165
+
166
+ // Create session
167
+ const sessionId = generateSessionId();
168
+ const now = new Date().toISOString();
169
+
170
+ // Find user's first workspace
171
+ const memberships = workspaceMemberStore.getByUser(user.id);
172
+ const workspaceId = memberships.length > 0 ? memberships[0].workspace_id : undefined;
173
+
174
+ sessionStore.create({
175
+ id: sessionId,
176
+ user_id: user.id,
177
+ workspace_id: workspaceId,
178
+ ip_address: req.ip || req.socket.remoteAddress,
179
+ user_agent: req.get('user-agent'),
180
+ expires_at: new Date(Date.now() + config.session_ttl_seconds * 1000).toISOString(),
181
+ last_active_at: now,
182
+ created_at: now,
183
+ });
184
+
185
+ // Set session cookie
186
+ res.cookie(SESSION_COOKIE_NAME, sessionId, {
187
+ httpOnly: true,
188
+ secure: isProduction,
189
+ sameSite: 'lax',
190
+ maxAge: config.session_ttl_seconds * 1000,
191
+ path: '/',
192
+ });
193
+
194
+ // Redirect based on user state
195
+ if (isNewUser || !user.onboarding_completed) {
196
+ return res.redirect('/onboarding');
197
+ }
198
+ return res.redirect('/dashboard');
199
+ } catch (err) {
200
+ console.error(`[auth] OAuth callback error (${provider}):`, err);
201
+ return res.redirect('/login?error=auth_failed');
202
+ }
203
+ });
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // POST /auth/register — create account with email + password
207
+ // ---------------------------------------------------------------------------
208
+ router.post('/register', async (req: Request, res: Response) => {
209
+ const { email, password, display_name } = req.body || {};
210
+
211
+ if (!email || !password) {
212
+ res.status(400).json({ error: 'email and password are required' });
213
+ return;
214
+ }
215
+
216
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
217
+ if (!emailRegex.test(email)) {
218
+ res.status(400).json({ error: 'Invalid email format' });
219
+ return;
220
+ }
221
+
222
+ if (password.length < 6) {
223
+ res.status(400).json({ error: 'Password must be at least 6 characters' });
224
+ return;
225
+ }
226
+
227
+ const existing = userStore.getByEmail(email);
228
+ if (existing) {
229
+ // Return identical response to prevent account enumeration
230
+ res.status(201).json({
231
+ user: { id: randomUUID(), email, display_name: display_name || email.split('@')[0] },
232
+ });
233
+ return;
234
+ }
235
+
236
+ const now = new Date().toISOString();
237
+ const userId = randomUUID();
238
+ const displayName = display_name || email.split('@')[0];
239
+ userStore.create({
240
+ id: userId,
241
+ email,
242
+ display_name: displayName,
243
+ password_hash: await hashPassword(password),
244
+ status: 'active',
245
+ onboarding_completed: false,
246
+ created_at: now,
247
+ updated_at: now,
248
+ });
249
+
250
+ // Auto-login: create session
251
+ const sessionId = generateSessionId();
252
+ sessionStore.create({
253
+ id: sessionId,
254
+ user_id: userId,
255
+ ip_address: req.ip || req.socket.remoteAddress,
256
+ user_agent: req.get('user-agent'),
257
+ expires_at: new Date(Date.now() + config.session_ttl_seconds * 1000).toISOString(),
258
+ last_active_at: now,
259
+ created_at: now,
260
+ });
261
+
262
+ res.cookie(SESSION_COOKIE_NAME, sessionId, {
263
+ httpOnly: true,
264
+ secure: isProduction,
265
+ sameSite: 'lax',
266
+ maxAge: config.session_ttl_seconds * 1000,
267
+ path: '/',
268
+ });
269
+
270
+ res.status(201).json({
271
+ user: { id: userId, email, display_name: displayName },
272
+ });
273
+ });
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // POST /auth/login — sign in with email + password
277
+ // ---------------------------------------------------------------------------
278
+ router.post('/login', async (req: Request, res: Response) => {
279
+ const { email, password } = req.body || {};
280
+
281
+ if (!email || !password) {
282
+ res.status(400).json({ error: 'email and password are required' });
283
+ return;
284
+ }
285
+
286
+ const user = userStore.getByEmail(email);
287
+ if (!user || !user.password_hash) {
288
+ res.status(401).json({ error: 'Invalid email or password' });
289
+ return;
290
+ }
291
+
292
+ if (!(await verifyPassword(password, user.password_hash))) {
293
+ res.status(401).json({ error: 'Invalid email or password' });
294
+ return;
295
+ }
296
+
297
+ if (user.status !== 'active') {
298
+ res.status(403).json({ error: 'Account is suspended' });
299
+ return;
300
+ }
301
+
302
+ const now = new Date().toISOString();
303
+ const sessionId = generateSessionId();
304
+
305
+ const memberships = workspaceMemberStore.getByUser(user.id);
306
+ const workspaceId = memberships.length > 0 ? memberships[0].workspace_id : undefined;
307
+
308
+ sessionStore.create({
309
+ id: sessionId,
310
+ user_id: user.id,
311
+ workspace_id: workspaceId,
312
+ ip_address: req.ip || req.socket.remoteAddress,
313
+ user_agent: req.get('user-agent'),
314
+ expires_at: new Date(Date.now() + config.session_ttl_seconds * 1000).toISOString(),
315
+ last_active_at: now,
316
+ created_at: now,
317
+ });
318
+
319
+ res.cookie(SESSION_COOKIE_NAME, sessionId, {
320
+ httpOnly: true,
321
+ secure: isProduction,
322
+ sameSite: 'lax',
323
+ maxAge: config.session_ttl_seconds * 1000,
324
+ path: '/',
325
+ });
326
+
327
+ res.json({
328
+ user: {
329
+ id: user.id,
330
+ email: user.email,
331
+ display_name: user.display_name,
332
+ onboarding_completed: user.onboarding_completed,
333
+ default_workspace_id: workspaceId,
334
+ },
335
+ });
336
+ });
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // GET /auth/me — return current user + session info
340
+ // ---------------------------------------------------------------------------
341
+ router.get('/me', (req: Request, res: Response) => {
342
+ const sessionUser = (req as any).sessionUser;
343
+ if (!sessionUser) {
344
+ res.status(401).json({ error: 'Not authenticated' });
345
+ return;
346
+ }
347
+
348
+ const oauthAccounts = oauthAccountStore.getByUserId(sessionUser.id);
349
+ const memberships = workspaceMemberStore.getByUser(sessionUser.id);
350
+ const workspaces = memberships.map(m => {
351
+ const ws = workspaceStore.getById(m.workspace_id);
352
+ return ws ? { ...ws, role: m.role } : null;
353
+ }).filter(Boolean);
354
+
355
+ // Pick the first workspace as default
356
+ const defaultWorkspaceId = workspaces.length > 0 ? (workspaces[0] as any).id : undefined;
357
+
358
+ res.json({
359
+ user: {
360
+ id: sessionUser.id,
361
+ email: sessionUser.email,
362
+ display_name: sessionUser.display_name,
363
+ avatar_url: sessionUser.avatar_url,
364
+ status: sessionUser.status,
365
+ onboarding_completed: sessionUser.onboarding_completed,
366
+ default_workspace_id: defaultWorkspaceId,
367
+ created_at: sessionUser.created_at,
368
+ },
369
+ providers: oauthAccounts.map(a => ({
370
+ provider: a.provider,
371
+ email: a.provider_email,
372
+ })),
373
+ workspaces,
374
+ session: {
375
+ workspace_id: (req as any).sessionData?.workspace_id,
376
+ expires_at: (req as any).sessionData?.expires_at,
377
+ },
378
+ });
379
+ });
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // PUT /auth/password — change password for current user (or admin sets for another user by email)
383
+ // ---------------------------------------------------------------------------
384
+ router.put('/password', async (req: Request, res: Response) => {
385
+ const sessionUser = (req as any).sessionUser;
386
+ if (!sessionUser) {
387
+ res.status(401).json({ error: 'Not authenticated' });
388
+ return;
389
+ }
390
+
391
+ const { new_password, email } = req.body || {};
392
+ if (!new_password || new_password.length < 6) {
393
+ res.status(400).json({ error: 'new_password must be at least 6 characters' });
394
+ return;
395
+ }
396
+
397
+ // If email is provided and differs from session user, check admin
398
+ let targetUser = sessionUser;
399
+ if (email && email !== sessionUser.email) {
400
+ // Only workspace owners can reset other users' passwords
401
+ const memberships = workspaceMemberStore.getByUser(sessionUser.id);
402
+ const isOwner = memberships.some(m => m.role === 'owner');
403
+ if (!isOwner) {
404
+ res.status(403).json({ error: 'Only workspace owners can reset other users passwords' });
405
+ return;
406
+ }
407
+ const found = userStore.getByEmail(email);
408
+ if (!found) {
409
+ res.status(404).json({ error: 'User not found' });
410
+ return;
411
+ }
412
+ targetUser = found;
413
+ }
414
+
415
+ const newHash = await hashPassword(new_password);
416
+ userStore.update(targetUser.id, { password_hash: newHash, updated_at: new Date().toISOString() });
417
+
418
+ res.json({ ok: true, email: targetUser.email });
419
+ });
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // DELETE /auth/users/:id — delete a user and all related data (self only)
423
+ // ---------------------------------------------------------------------------
424
+ router.delete('/users/:id', (req: Request, res: Response) => {
425
+ const sessionUser = (req as any).sessionUser;
426
+ if (!sessionUser) {
427
+ res.status(401).json({ error: 'Not authenticated' });
428
+ return;
429
+ }
430
+
431
+ const targetId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
432
+
433
+ // Only allow deleting yourself (no admin role needed)
434
+ if (sessionUser.id !== targetId) {
435
+ res.status(403).json({ error: 'Can only delete your own account' });
436
+ return;
437
+ }
438
+
439
+ // Delete workspace memberships and owned workspaces
440
+ const memberships = workspaceMemberStore.getByUser(targetId);
441
+ for (const m of memberships) {
442
+ // If sole owner of workspace, delete the workspace too
443
+ const wsMembers = workspaceMemberStore.getByWorkspace(m.workspace_id);
444
+ const otherOwners = wsMembers.filter(wm => wm.user_id !== targetId && wm.role === 'owner');
445
+ if (otherOwners.length === 0) {
446
+ // Delete all members of this workspace
447
+ for (const wm of wsMembers) {
448
+ workspaceMemberStore.delete(wm.id);
449
+ }
450
+ // Delete workspace API keys
451
+ if (userApiKeyStore) {
452
+ const keys = userApiKeyStore.getByWorkspace(m.workspace_id);
453
+ for (const k of keys) {
454
+ userApiKeyStore.delete(k.id);
455
+ }
456
+ }
457
+ workspaceStore.delete(m.workspace_id);
458
+ } else {
459
+ workspaceMemberStore.delete(m.id);
460
+ }
461
+ }
462
+
463
+ // Delete OAuth accounts
464
+ const oauthAccounts = oauthAccountStore.getByUserId(targetId);
465
+ for (const oa of oauthAccounts) {
466
+ oauthAccountStore.delete(oa.id);
467
+ }
468
+
469
+ // Delete user API keys
470
+ if (userApiKeyStore) {
471
+ const keys = userApiKeyStore.getByUser(targetId);
472
+ for (const k of keys) {
473
+ userApiKeyStore.delete(k.id);
474
+ }
475
+ }
476
+
477
+ // Clear session
478
+ const sessionId = req.cookies?.[SESSION_COOKIE_NAME];
479
+ if (sessionId) {
480
+ sessionStore.delete(sessionId);
481
+ }
482
+
483
+ // Delete user
484
+ userStore.delete(targetId);
485
+
486
+ res.clearCookie(SESSION_COOKIE_NAME, { path: '/' });
487
+ res.json({ status: 'ok', deleted_user_id: targetId });
488
+ });
489
+
490
+ // ---------------------------------------------------------------------------
491
+ // POST /auth/logout — clear session
492
+ // ---------------------------------------------------------------------------
493
+ router.post('/logout', (req: Request, res: Response) => {
494
+ const sessionId = req.cookies?.[SESSION_COOKIE_NAME];
495
+ if (sessionId) {
496
+ sessionStore.delete(sessionId);
497
+ }
498
+ res.clearCookie(SESSION_COOKIE_NAME, { path: '/' });
499
+ res.json({ status: 'ok' });
500
+ });
501
+
502
+ // ---------------------------------------------------------------------------
503
+ // Helper: find or create user from OAuth profile
504
+ // ---------------------------------------------------------------------------
505
+ function findOrCreateUser(profile: OAuthProfile, accessToken: string, refreshToken?: string): {
506
+ user: any;
507
+ isNewUser: boolean;
508
+ needsLinking?: boolean;
509
+ } {
510
+ const now = new Date().toISOString();
511
+
512
+ // Check if OAuth account already exists
513
+ const existingOAuth = oauthAccountStore.getByProvider(profile.provider, profile.provider_user_id);
514
+ if (existingOAuth) {
515
+ // Update tokens
516
+ oauthAccountStore.update(existingOAuth.id, {
517
+ access_token_encrypted: encryptToken(accessToken, config.session_secret),
518
+ refresh_token_encrypted: refreshToken ? encryptToken(refreshToken, config.session_secret) : undefined,
519
+ updated_at: now,
520
+ });
521
+ const user = userStore.getById(existingOAuth.user_id)!;
522
+ return { user, isNewUser: false };
523
+ }
524
+
525
+ // Check if user exists with same email — do NOT auto-link
526
+ const existingUser = userStore.getByEmail(profile.email);
527
+ if (existingUser) {
528
+ return { user: existingUser, isNewUser: false, needsLinking: true };
529
+ }
530
+
531
+ // Create new user
532
+ const user = {
533
+ id: randomUUID(),
534
+ email: profile.email,
535
+ display_name: profile.display_name,
536
+ avatar_url: profile.avatar_url,
537
+ status: 'active' as const,
538
+ onboarding_completed: false,
539
+ created_at: now,
540
+ updated_at: now,
541
+ };
542
+ userStore.create(user);
543
+
544
+ // Create OAuth account link for new users only
545
+ oauthAccountStore.create({
546
+ id: randomUUID(),
547
+ user_id: user.id,
548
+ provider: profile.provider,
549
+ provider_user_id: profile.provider_user_id,
550
+ provider_email: profile.email,
551
+ access_token_encrypted: encryptToken(accessToken, config.session_secret),
552
+ refresh_token_encrypted: refreshToken ? encryptToken(refreshToken, config.session_secret) : undefined,
553
+ created_at: now,
554
+ updated_at: now,
555
+ });
556
+
557
+ return { user, isNewUser: true };
558
+ }
559
+
560
+ return router;
561
+ }
@@ -0,0 +1,84 @@
1
+ import * as crypto from 'crypto';
2
+ import { OAuthFlowState, OAuthConfig } from '../types/user';
3
+
4
+ /**
5
+ * Generate a cryptographically random session ID (256-bit).
6
+ */
7
+ export function generateSessionId(): string {
8
+ return crypto.randomBytes(32).toString('hex');
9
+ }
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // OAuth state cookie (encrypted with AES-256-GCM)
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function deriveKey(secret: string, context: string = 'palaryn-session'): Buffer {
16
+ return Buffer.from(crypto.hkdfSync('sha256', Buffer.from(secret, 'utf-8'), Buffer.alloc(32, 0), Buffer.from(context, 'utf-8'), 32));
17
+ }
18
+
19
+ /**
20
+ * Encrypt an OAuth flow state object into a cookie-safe string.
21
+ */
22
+ export function encryptState(state: OAuthFlowState, secret: string): string {
23
+ const key = deriveKey(secret);
24
+ const iv = crypto.randomBytes(12);
25
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
26
+ const plaintext = JSON.stringify(state);
27
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
28
+ const tag = cipher.getAuthTag();
29
+ // Format: base64url(iv + tag + ciphertext)
30
+ return Buffer.concat([iv, tag, encrypted]).toString('base64url');
31
+ }
32
+
33
+ /**
34
+ * Decrypt an OAuth flow state from a cookie value.
35
+ */
36
+ export function decryptState(encrypted: string, secret: string): OAuthFlowState {
37
+ const key = deriveKey(secret);
38
+ const buf = Buffer.from(encrypted, 'base64url');
39
+ const iv = buf.subarray(0, 12);
40
+ const tag = buf.subarray(12, 28);
41
+ const ciphertext = buf.subarray(28);
42
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
43
+ decipher.setAuthTag(tag);
44
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8');
45
+ return JSON.parse(plaintext);
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Token encryption (AES-256-GCM for storing OAuth tokens at rest)
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export function encryptToken(token: string, secret: string): string {
53
+ const key = deriveKey(secret);
54
+ const iv = crypto.randomBytes(12);
55
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
56
+ const encrypted = Buffer.concat([cipher.update(token, 'utf-8'), cipher.final()]);
57
+ const tag = cipher.getAuthTag();
58
+ return Buffer.concat([iv, tag, encrypted]).toString('base64');
59
+ }
60
+
61
+ export function decryptToken(encryptedToken: string, secret: string): string {
62
+ const key = deriveKey(secret);
63
+ const buf = Buffer.from(encryptedToken, 'base64');
64
+ const iv = buf.subarray(0, 12);
65
+ const tag = buf.subarray(12, 28);
66
+ const ciphertext = buf.subarray(28);
67
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
68
+ decipher.setAuthTag(tag);
69
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8');
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Cookie helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export interface SessionCookieOptions {
77
+ name: string;
78
+ maxAge: number; // seconds
79
+ secure: boolean;
80
+ domain?: string;
81
+ }
82
+
83
+ export const SESSION_COOKIE_NAME = 'pn_session';
84
+ export const STATE_COOKIE_NAME = 'pn_oauth_state';
@@ -0,0 +1,6 @@
1
+ export { StripeClient } from './stripe-client';
2
+ export { PlanEnforcer } from './plan-enforcer';
3
+ export { WebhookHandler } from './webhook-handler';
4
+ export { createWebhookRouter } from './webhook-routes';
5
+ export { createBillingRouter } from './routes';
6
+ export type { BillingRouteDeps } from './routes';