@digimuza/saas-core 0.0.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 (474) hide show
  1. package/CLAUDE.md +1165 -0
  2. package/LICENSE +21 -0
  3. package/README.md +234 -0
  4. package/dist/api-keys/index.d.ts +9 -0
  5. package/dist/api-keys/index.d.ts.map +1 -0
  6. package/dist/api-keys/index.js +37 -0
  7. package/dist/api-keys/index.js.map +1 -0
  8. package/dist/api-keys/keys.d.ts +55 -0
  9. package/dist/api-keys/keys.d.ts.map +1 -0
  10. package/dist/api-keys/keys.js +92 -0
  11. package/dist/api-keys/keys.js.map +1 -0
  12. package/dist/api-keys/service.d.ts +111 -0
  13. package/dist/api-keys/service.d.ts.map +1 -0
  14. package/dist/api-keys/service.js +210 -0
  15. package/dist/api-keys/service.js.map +1 -0
  16. package/dist/audit/index.d.ts +5 -0
  17. package/dist/audit/index.d.ts.map +1 -0
  18. package/dist/audit/index.js +44 -0
  19. package/dist/audit/index.js.map +1 -0
  20. package/dist/audit/service.d.ts +88 -0
  21. package/dist/audit/service.d.ts.map +1 -0
  22. package/dist/audit/service.js +128 -0
  23. package/dist/audit/service.js.map +1 -0
  24. package/dist/auth/client.d.ts +5 -0
  25. package/dist/auth/client.d.ts.map +1 -0
  26. package/dist/auth/client.js +26 -0
  27. package/dist/auth/client.js.map +1 -0
  28. package/dist/auth/cookies.d.ts +51 -0
  29. package/dist/auth/cookies.d.ts.map +1 -0
  30. package/dist/auth/cookies.js +58 -0
  31. package/dist/auth/cookies.js.map +1 -0
  32. package/dist/auth/index.d.ts +14 -0
  33. package/dist/auth/index.d.ts.map +1 -0
  34. package/dist/auth/index.js +46 -0
  35. package/dist/auth/index.js.map +1 -0
  36. package/dist/auth/service.d.ts +67 -0
  37. package/dist/auth/service.d.ts.map +1 -0
  38. package/dist/auth/service.js +275 -0
  39. package/dist/auth/service.js.map +1 -0
  40. package/dist/auth/types.d.ts +56 -0
  41. package/dist/auth/types.d.ts.map +1 -0
  42. package/dist/auth/types.js +2 -0
  43. package/dist/auth/types.js.map +1 -0
  44. package/dist/authorization/index.d.ts +4 -0
  45. package/dist/authorization/index.d.ts.map +1 -0
  46. package/dist/authorization/index.js +24 -0
  47. package/dist/authorization/index.js.map +1 -0
  48. package/dist/authorization/service.d.ts +92 -0
  49. package/dist/authorization/service.d.ts.map +1 -0
  50. package/dist/authorization/service.js +254 -0
  51. package/dist/authorization/service.js.map +1 -0
  52. package/dist/billing/index.d.ts +16 -0
  53. package/dist/billing/index.d.ts.map +1 -0
  54. package/dist/billing/index.js +47 -0
  55. package/dist/billing/index.js.map +1 -0
  56. package/dist/billing/provider.d.ts +114 -0
  57. package/dist/billing/provider.d.ts.map +1 -0
  58. package/dist/billing/provider.js +2 -0
  59. package/dist/billing/provider.js.map +1 -0
  60. package/dist/billing/revolut.d.ts +68 -0
  61. package/dist/billing/revolut.d.ts.map +1 -0
  62. package/dist/billing/revolut.js +181 -0
  63. package/dist/billing/revolut.js.map +1 -0
  64. package/dist/billing/service.d.ts +124 -0
  65. package/dist/billing/service.d.ts.map +1 -0
  66. package/dist/billing/service.js +470 -0
  67. package/dist/billing/service.js.map +1 -0
  68. package/dist/cli/commands/dev.d.ts +43 -0
  69. package/dist/cli/commands/dev.d.ts.map +1 -0
  70. package/dist/cli/commands/dev.js +212 -0
  71. package/dist/cli/commands/dev.js.map +1 -0
  72. package/dist/cli/commands/project.d.ts +12 -0
  73. package/dist/cli/commands/project.d.ts.map +1 -0
  74. package/dist/cli/commands/project.js +49 -0
  75. package/dist/cli/commands/project.js.map +1 -0
  76. package/dist/cli/docker.d.ts +50 -0
  77. package/dist/cli/docker.d.ts.map +1 -0
  78. package/dist/cli/docker.js +84 -0
  79. package/dist/cli/docker.js.map +1 -0
  80. package/dist/cli/index.d.ts +6 -0
  81. package/dist/cli/index.d.ts.map +1 -0
  82. package/dist/cli/index.js +198 -0
  83. package/dist/cli/index.js.map +1 -0
  84. package/dist/cli/output.d.ts +30 -0
  85. package/dist/cli/output.d.ts.map +1 -0
  86. package/dist/cli/output.js +66 -0
  87. package/dist/cli/output.js.map +1 -0
  88. package/dist/cli/templates.d.ts +52 -0
  89. package/dist/cli/templates.d.ts.map +1 -0
  90. package/dist/cli/templates.js +225 -0
  91. package/dist/cli/templates.js.map +1 -0
  92. package/dist/config/client.d.ts +60 -0
  93. package/dist/config/client.d.ts.map +1 -0
  94. package/dist/config/client.js +94 -0
  95. package/dist/config/client.js.map +1 -0
  96. package/dist/config/config-loader.d.ts +2 -0
  97. package/dist/config/config-loader.d.ts.map +1 -0
  98. package/dist/config/config-loader.js +2 -0
  99. package/dist/config/config-loader.js.map +1 -0
  100. package/dist/config/env.d.ts +24 -0
  101. package/dist/config/env.d.ts.map +1 -0
  102. package/dist/config/env.js +40 -0
  103. package/dist/config/env.js.map +1 -0
  104. package/dist/config/errors.d.ts +17 -0
  105. package/dist/config/errors.d.ts.map +1 -0
  106. package/dist/config/errors.js +26 -0
  107. package/dist/config/errors.js.map +1 -0
  108. package/dist/config/index.d.ts +72 -0
  109. package/dist/config/index.d.ts.map +1 -0
  110. package/dist/config/index.js +106 -0
  111. package/dist/config/index.js.map +1 -0
  112. package/dist/config/runtime.d.ts +15 -0
  113. package/dist/config/runtime.d.ts.map +1 -0
  114. package/dist/config/runtime.js +22 -0
  115. package/dist/config/runtime.js.map +1 -0
  116. package/dist/config/sections.d.ts +226 -0
  117. package/dist/config/sections.d.ts.map +1 -0
  118. package/dist/config/sections.js +141 -0
  119. package/dist/config/sections.js.map +1 -0
  120. package/dist/db/client.d.ts +43 -0
  121. package/dist/db/client.d.ts.map +1 -0
  122. package/dist/db/client.js +77 -0
  123. package/dist/db/client.js.map +1 -0
  124. package/dist/db/errors.d.ts +39 -0
  125. package/dist/db/errors.d.ts.map +1 -0
  126. package/dist/db/errors.js +67 -0
  127. package/dist/db/errors.js.map +1 -0
  128. package/dist/db/index.d.ts +10 -0
  129. package/dist/db/index.d.ts.map +1 -0
  130. package/dist/db/index.js +10 -0
  131. package/dist/db/index.js.map +1 -0
  132. package/dist/db/migrate.d.ts +21 -0
  133. package/dist/db/migrate.d.ts.map +1 -0
  134. package/dist/db/migrate.js +28 -0
  135. package/dist/db/migrate.js.map +1 -0
  136. package/dist/db/schema/_shared.d.ts +30 -0
  137. package/dist/db/schema/_shared.d.ts.map +1 -0
  138. package/dist/db/schema/_shared.js +36 -0
  139. package/dist/db/schema/_shared.js.map +1 -0
  140. package/dist/db/schema/apikeys.d.ts +263 -0
  141. package/dist/db/schema/apikeys.d.ts.map +1 -0
  142. package/dist/db/schema/apikeys.js +70 -0
  143. package/dist/db/schema/apikeys.js.map +1 -0
  144. package/dist/db/schema/authorization.d.ts +465 -0
  145. package/dist/db/schema/authorization.d.ts.map +1 -0
  146. package/dist/db/schema/authorization.js +134 -0
  147. package/dist/db/schema/authorization.js.map +1 -0
  148. package/dist/db/schema/billing.d.ts +1100 -0
  149. package/dist/db/schema/billing.d.ts.map +1 -0
  150. package/dist/db/schema/billing.js +223 -0
  151. package/dist/db/schema/billing.js.map +1 -0
  152. package/dist/db/schema/events.d.ts +161 -0
  153. package/dist/db/schema/events.d.ts.map +1 -0
  154. package/dist/db/schema/events.js +66 -0
  155. package/dist/db/schema/events.js.map +1 -0
  156. package/dist/db/schema/features.d.ts +243 -0
  157. package/dist/db/schema/features.d.ts.map +1 -0
  158. package/dist/db/schema/features.js +87 -0
  159. package/dist/db/schema/features.js.map +1 -0
  160. package/dist/db/schema/files.d.ts +253 -0
  161. package/dist/db/schema/files.d.ts.map +1 -0
  162. package/dist/db/schema/files.js +74 -0
  163. package/dist/db/schema/files.js.map +1 -0
  164. package/dist/db/schema/index.d.ts +38 -0
  165. package/dist/db/schema/index.d.ts.map +1 -0
  166. package/dist/db/schema/index.js +38 -0
  167. package/dist/db/schema/index.js.map +1 -0
  168. package/dist/db/schema/invitations.d.ts +214 -0
  169. package/dist/db/schema/invitations.d.ts.map +1 -0
  170. package/dist/db/schema/invitations.js +80 -0
  171. package/dist/db/schema/invitations.js.map +1 -0
  172. package/dist/db/schema/messaging.d.ts +442 -0
  173. package/dist/db/schema/messaging.d.ts.map +1 -0
  174. package/dist/db/schema/messaging.js +120 -0
  175. package/dist/db/schema/messaging.js.map +1 -0
  176. package/dist/db/schema/users.d.ts +246 -0
  177. package/dist/db/schema/users.d.ts.map +1 -0
  178. package/dist/db/schema/users.js +60 -0
  179. package/dist/db/schema/users.js.map +1 -0
  180. package/dist/db/schema/webhooks.d.ts +693 -0
  181. package/dist/db/schema/webhooks.d.ts.map +1 -0
  182. package/dist/db/schema/webhooks.js +147 -0
  183. package/dist/db/schema/webhooks.js.map +1 -0
  184. package/dist/db/schema/workspaces.d.ts +196 -0
  185. package/dist/db/schema/workspaces.d.ts.map +1 -0
  186. package/dist/db/schema/workspaces.js +57 -0
  187. package/dist/db/schema/workspaces.js.map +1 -0
  188. package/dist/db/types.d.ts +18 -0
  189. package/dist/db/types.d.ts.map +1 -0
  190. package/dist/db/types.js +2 -0
  191. package/dist/db/types.js.map +1 -0
  192. package/dist/email/adapters.d.ts +29 -0
  193. package/dist/email/adapters.d.ts.map +1 -0
  194. package/dist/email/adapters.js +60 -0
  195. package/dist/email/adapters.js.map +1 -0
  196. package/dist/email/index.d.ts +20 -0
  197. package/dist/email/index.d.ts.map +1 -0
  198. package/dist/email/index.js +79 -0
  199. package/dist/email/index.js.map +1 -0
  200. package/dist/email/provider.d.ts +47 -0
  201. package/dist/email/provider.d.ts.map +1 -0
  202. package/dist/email/provider.js +7 -0
  203. package/dist/email/provider.js.map +1 -0
  204. package/dist/email/resend.d.ts +22 -0
  205. package/dist/email/resend.d.ts.map +1 -0
  206. package/dist/email/resend.js +51 -0
  207. package/dist/email/resend.js.map +1 -0
  208. package/dist/email/service.d.ts +55 -0
  209. package/dist/email/service.d.ts.map +1 -0
  210. package/dist/email/service.js +87 -0
  211. package/dist/email/service.js.map +1 -0
  212. package/dist/email/templates.d.ts +81 -0
  213. package/dist/email/templates.d.ts.map +1 -0
  214. package/dist/email/templates.js +84 -0
  215. package/dist/email/templates.js.map +1 -0
  216. package/dist/entitlements/index.d.ts +10 -0
  217. package/dist/entitlements/index.d.ts.map +1 -0
  218. package/dist/entitlements/index.js +28 -0
  219. package/dist/entitlements/index.js.map +1 -0
  220. package/dist/entitlements/service.d.ts +83 -0
  221. package/dist/entitlements/service.d.ts.map +1 -0
  222. package/dist/entitlements/service.js +158 -0
  223. package/dist/entitlements/service.js.map +1 -0
  224. package/dist/errors.d.ts +85 -0
  225. package/dist/errors.d.ts.map +1 -0
  226. package/dist/errors.js +95 -0
  227. package/dist/errors.js.map +1 -0
  228. package/dist/events/core-events.d.ts +120 -0
  229. package/dist/events/core-events.d.ts.map +1 -0
  230. package/dist/events/core-events.js +109 -0
  231. package/dist/events/core-events.js.map +1 -0
  232. package/dist/events/index.d.ts +15 -0
  233. package/dist/events/index.d.ts.map +1 -0
  234. package/dist/events/index.js +58 -0
  235. package/dist/events/index.js.map +1 -0
  236. package/dist/events/publisher.d.ts +46 -0
  237. package/dist/events/publisher.d.ts.map +1 -0
  238. package/dist/events/publisher.js +54 -0
  239. package/dist/events/publisher.js.map +1 -0
  240. package/dist/events/registry.d.ts +87 -0
  241. package/dist/events/registry.d.ts.map +1 -0
  242. package/dist/events/registry.js +34 -0
  243. package/dist/events/registry.js.map +1 -0
  244. package/dist/events/service.d.ts +78 -0
  245. package/dist/events/service.d.ts.map +1 -0
  246. package/dist/events/service.js +182 -0
  247. package/dist/events/service.js.map +1 -0
  248. package/dist/features/bucket.d.ts +22 -0
  249. package/dist/features/bucket.d.ts.map +1 -0
  250. package/dist/features/bucket.js +42 -0
  251. package/dist/features/bucket.js.map +1 -0
  252. package/dist/features/index.d.ts +15 -0
  253. package/dist/features/index.d.ts.map +1 -0
  254. package/dist/features/index.js +78 -0
  255. package/dist/features/index.js.map +1 -0
  256. package/dist/features/registry.d.ts +51 -0
  257. package/dist/features/registry.d.ts.map +1 -0
  258. package/dist/features/registry.js +52 -0
  259. package/dist/features/registry.js.map +1 -0
  260. package/dist/features/service.d.ts +78 -0
  261. package/dist/features/service.d.ts.map +1 -0
  262. package/dist/features/service.js +207 -0
  263. package/dist/features/service.js.map +1 -0
  264. package/dist/files/index.d.ts +6 -0
  265. package/dist/files/index.d.ts.map +1 -0
  266. package/dist/files/index.js +64 -0
  267. package/dist/files/index.js.map +1 -0
  268. package/dist/files/service.d.ts +92 -0
  269. package/dist/files/service.d.ts.map +1 -0
  270. package/dist/files/service.js +303 -0
  271. package/dist/files/service.js.map +1 -0
  272. package/dist/index.d.ts +33 -0
  273. package/dist/index.d.ts.map +1 -0
  274. package/dist/index.js +33 -0
  275. package/dist/index.js.map +1 -0
  276. package/dist/jobs/backend.d.ts +23 -0
  277. package/dist/jobs/backend.d.ts.map +1 -0
  278. package/dist/jobs/backend.js +23 -0
  279. package/dist/jobs/backend.js.map +1 -0
  280. package/dist/jobs/index.d.ts +29 -0
  281. package/dist/jobs/index.d.ts.map +1 -0
  282. package/dist/jobs/index.js +114 -0
  283. package/dist/jobs/index.js.map +1 -0
  284. package/dist/jobs/service.d.ts +67 -0
  285. package/dist/jobs/service.d.ts.map +1 -0
  286. package/dist/jobs/service.js +139 -0
  287. package/dist/jobs/service.js.map +1 -0
  288. package/dist/jobs/types.d.ts +78 -0
  289. package/dist/jobs/types.d.ts.map +1 -0
  290. package/dist/jobs/types.js +4 -0
  291. package/dist/jobs/types.js.map +1 -0
  292. package/dist/logger/default.d.ts +7 -0
  293. package/dist/logger/default.d.ts.map +1 -0
  294. package/dist/logger/default.js +28 -0
  295. package/dist/logger/default.js.map +1 -0
  296. package/dist/logger/index.d.ts +24 -0
  297. package/dist/logger/index.d.ts.map +1 -0
  298. package/dist/logger/index.js +90 -0
  299. package/dist/logger/index.js.map +1 -0
  300. package/dist/logger/redact.d.ts +40 -0
  301. package/dist/logger/redact.d.ts.map +1 -0
  302. package/dist/logger/redact.js +178 -0
  303. package/dist/logger/redact.js.map +1 -0
  304. package/dist/logger/service.d.ts +14 -0
  305. package/dist/logger/service.d.ts.map +1 -0
  306. package/dist/logger/service.js +56 -0
  307. package/dist/logger/service.js.map +1 -0
  308. package/dist/logger/sinks.d.ts +37 -0
  309. package/dist/logger/sinks.d.ts.map +1 -0
  310. package/dist/logger/sinks.js +135 -0
  311. package/dist/logger/sinks.js.map +1 -0
  312. package/dist/logger/types.d.ts +47 -0
  313. package/dist/logger/types.d.ts.map +1 -0
  314. package/dist/logger/types.js +8 -0
  315. package/dist/logger/types.js.map +1 -0
  316. package/dist/notifications/index.d.ts +8 -0
  317. package/dist/notifications/index.d.ts.map +1 -0
  318. package/dist/notifications/index.js +72 -0
  319. package/dist/notifications/index.js.map +1 -0
  320. package/dist/notifications/service.d.ts +79 -0
  321. package/dist/notifications/service.d.ts.map +1 -0
  322. package/dist/notifications/service.js +164 -0
  323. package/dist/notifications/service.js.map +1 -0
  324. package/dist/realtime/channels.d.ts +37 -0
  325. package/dist/realtime/channels.d.ts.map +1 -0
  326. package/dist/realtime/channels.js +58 -0
  327. package/dist/realtime/channels.js.map +1 -0
  328. package/dist/realtime/index.d.ts +19 -0
  329. package/dist/realtime/index.d.ts.map +1 -0
  330. package/dist/realtime/index.js +118 -0
  331. package/dist/realtime/index.js.map +1 -0
  332. package/dist/realtime/postgres.d.ts +48 -0
  333. package/dist/realtime/postgres.d.ts.map +1 -0
  334. package/dist/realtime/postgres.js +151 -0
  335. package/dist/realtime/postgres.js.map +1 -0
  336. package/dist/realtime/service.d.ts +75 -0
  337. package/dist/realtime/service.d.ts.map +1 -0
  338. package/dist/realtime/service.js +103 -0
  339. package/dist/realtime/service.js.map +1 -0
  340. package/dist/realtime/sse.d.ts +55 -0
  341. package/dist/realtime/sse.d.ts.map +1 -0
  342. package/dist/realtime/sse.js +123 -0
  343. package/dist/realtime/sse.js.map +1 -0
  344. package/dist/realtime/transport.d.ts +52 -0
  345. package/dist/realtime/transport.d.ts.map +1 -0
  346. package/dist/realtime/transport.js +2 -0
  347. package/dist/realtime/transport.js.map +1 -0
  348. package/dist/storage/index.d.ts +18 -0
  349. package/dist/storage/index.d.ts.map +1 -0
  350. package/dist/storage/index.js +89 -0
  351. package/dist/storage/index.js.map +1 -0
  352. package/dist/storage/keys.d.ts +50 -0
  353. package/dist/storage/keys.d.ts.map +1 -0
  354. package/dist/storage/keys.js +120 -0
  355. package/dist/storage/keys.js.map +1 -0
  356. package/dist/storage/memory.d.ts +32 -0
  357. package/dist/storage/memory.d.ts.map +1 -0
  358. package/dist/storage/memory.js +84 -0
  359. package/dist/storage/memory.js.map +1 -0
  360. package/dist/storage/provider.d.ts +93 -0
  361. package/dist/storage/provider.d.ts.map +1 -0
  362. package/dist/storage/provider.js +2 -0
  363. package/dist/storage/provider.js.map +1 -0
  364. package/dist/storage/s3.d.ts +34 -0
  365. package/dist/storage/s3.d.ts.map +1 -0
  366. package/dist/storage/s3.js +175 -0
  367. package/dist/storage/s3.js.map +1 -0
  368. package/dist/storage/service.d.ts +86 -0
  369. package/dist/storage/service.d.ts.map +1 -0
  370. package/dist/storage/service.js +107 -0
  371. package/dist/storage/service.js.map +1 -0
  372. package/dist/storage/sigv4.d.ts +58 -0
  373. package/dist/storage/sigv4.d.ts.map +1 -0
  374. package/dist/storage/sigv4.js +114 -0
  375. package/dist/storage/sigv4.js.map +1 -0
  376. package/dist/streams/client.d.ts +91 -0
  377. package/dist/streams/client.d.ts.map +1 -0
  378. package/dist/streams/client.js +128 -0
  379. package/dist/streams/client.js.map +1 -0
  380. package/dist/streams/events.d.ts +68 -0
  381. package/dist/streams/events.d.ts.map +1 -0
  382. package/dist/streams/events.js +81 -0
  383. package/dist/streams/events.js.map +1 -0
  384. package/dist/streams/index.d.ts +47 -0
  385. package/dist/streams/index.d.ts.map +1 -0
  386. package/dist/streams/index.js +47 -0
  387. package/dist/streams/index.js.map +1 -0
  388. package/dist/streams/realtime.d.ts +27 -0
  389. package/dist/streams/realtime.d.ts.map +1 -0
  390. package/dist/streams/realtime.js +50 -0
  391. package/dist/streams/realtime.js.map +1 -0
  392. package/dist/streams/state.d.ts +26 -0
  393. package/dist/streams/state.d.ts.map +1 -0
  394. package/dist/streams/state.js +34 -0
  395. package/dist/streams/state.js.map +1 -0
  396. package/dist/users/index.d.ts +9 -0
  397. package/dist/users/index.d.ts.map +1 -0
  398. package/dist/users/index.js +23 -0
  399. package/dist/users/index.js.map +1 -0
  400. package/dist/users/service.d.ts +43 -0
  401. package/dist/users/service.d.ts.map +1 -0
  402. package/dist/users/service.js +113 -0
  403. package/dist/users/service.js.map +1 -0
  404. package/dist/webhooks/inbound.d.ts +70 -0
  405. package/dist/webhooks/inbound.d.ts.map +1 -0
  406. package/dist/webhooks/inbound.js +146 -0
  407. package/dist/webhooks/inbound.js.map +1 -0
  408. package/dist/webhooks/index.d.ts +12 -0
  409. package/dist/webhooks/index.d.ts.map +1 -0
  410. package/dist/webhooks/index.js +65 -0
  411. package/dist/webhooks/index.js.map +1 -0
  412. package/dist/webhooks/outgoing.d.ts +89 -0
  413. package/dist/webhooks/outgoing.d.ts.map +1 -0
  414. package/dist/webhooks/outgoing.js +375 -0
  415. package/dist/webhooks/outgoing.js.map +1 -0
  416. package/dist/webhooks/signature.d.ts +40 -0
  417. package/dist/webhooks/signature.d.ts.map +1 -0
  418. package/dist/webhooks/signature.js +77 -0
  419. package/dist/webhooks/signature.js.map +1 -0
  420. package/dist/workspaces/context.d.ts +30 -0
  421. package/dist/workspaces/context.d.ts.map +1 -0
  422. package/dist/workspaces/context.js +12 -0
  423. package/dist/workspaces/context.js.map +1 -0
  424. package/dist/workspaces/index.d.ts +5 -0
  425. package/dist/workspaces/index.d.ts.map +1 -0
  426. package/dist/workspaces/index.js +21 -0
  427. package/dist/workspaces/index.js.map +1 -0
  428. package/dist/workspaces/service.d.ts +126 -0
  429. package/dist/workspaces/service.d.ts.map +1 -0
  430. package/dist/workspaces/service.js +495 -0
  431. package/dist/workspaces/service.js.map +1 -0
  432. package/docs/README.md +71 -0
  433. package/docs/api-keys.md +184 -0
  434. package/docs/audit.md +198 -0
  435. package/docs/auth.md +196 -0
  436. package/docs/authorization.md +225 -0
  437. package/docs/billing.md +293 -0
  438. package/docs/cli.md +238 -0
  439. package/docs/config.md +163 -0
  440. package/docs/db.md +219 -0
  441. package/docs/email.md +222 -0
  442. package/docs/entitlements.md +194 -0
  443. package/docs/errors.md +158 -0
  444. package/docs/events.md +279 -0
  445. package/docs/feature-flags.md +237 -0
  446. package/docs/files.md +229 -0
  447. package/docs/jobs.md +288 -0
  448. package/docs/logger.md +269 -0
  449. package/docs/notifications.md +179 -0
  450. package/docs/publishing.md +128 -0
  451. package/docs/realtime.md +360 -0
  452. package/docs/storage.md +279 -0
  453. package/docs/streams.md +296 -0
  454. package/docs/users.md +117 -0
  455. package/docs/webhooks.md +272 -0
  456. package/docs/workspaces.md +269 -0
  457. package/drizzle/0000_init_users.sql +21 -0
  458. package/drizzle/0001_workspaces_roles_permissions.sql +86 -0
  459. package/drizzle/0002_billing_entitlements.sql +97 -0
  460. package/drizzle/0003_events.sql +17 -0
  461. package/drizzle/0004_notifications_audit.sql +64 -0
  462. package/drizzle/0005_api_keys_webhooks.sql +77 -0
  463. package/drizzle/0006_feature_flags.sql +22 -0
  464. package/drizzle/0007_files.sql +23 -0
  465. package/drizzle/meta/0000_snapshot.json +160 -0
  466. package/drizzle/meta/0001_snapshot.json +837 -0
  467. package/drizzle/meta/0002_snapshot.json +1580 -0
  468. package/drizzle/meta/0003_snapshot.json +1751 -0
  469. package/drizzle/meta/0004_snapshot.json +2109 -0
  470. package/drizzle/meta/0005_snapshot.json +2719 -0
  471. package/drizzle/meta/0006_snapshot.json +2909 -0
  472. package/drizzle/meta/0007_snapshot.json +3113 -0
  473. package/drizzle/meta/_journal.json +62 -0
  474. package/package.json +180 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Digimuza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # @digimuza/saas-core
2
+
3
+ A reusable, strongly typed SaaS foundation. One npm package, subpath exports,
4
+ PostgreSQL + Drizzle.
5
+
6
+ It answers the questions every SaaS application has to answer — who is this user,
7
+ what workspace are they in, what can they do, what have they paid for, what is
8
+ happening, and how do we run the slow parts — so that individual products only
9
+ contain product logic.
10
+
11
+ Architecture rules and the full roadmap: [CLAUDE.md](./CLAUDE.md).
12
+
13
+ Two layers of documentation, neither repeating the other:
14
+
15
+ - **[`src/README.md`](./src/README.md)** — a short README beside each module's code:
16
+ what it is for, what you actually do with it, the gotchas. Start here.
17
+ - **[`docs/`](./docs/README.md)** — the reference: every exported function, its
18
+ signature, and why it behaves the way it does.
19
+
20
+ **Status: Phases 1–12 complete.**
21
+
22
+ ---
23
+
24
+ ## Bootstrap
25
+
26
+ ```bash
27
+ npm install @digimuza/saas-core
28
+ npx sc init
29
+ # Project name (my-app):
30
+ npx sc start
31
+ ```
32
+
33
+ `sc init` creates a project-owned `docker-compose.yml` containing PostgreSQL.
34
+ `sc start` launches it and waits until the database is ready.
35
+
36
+ See [docs/cli.md](./docs/cli.md).
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ npm install @digimuza/saas-core drizzle-orm zod
42
+ npm install @trigger.dev/sdk # only if you use background jobs
43
+ npm install rxjs # only if you use reactive streams
44
+ ```
45
+
46
+ `drizzle-orm`, `zod`, `@trigger.dev/sdk` and `rxjs` are **peer** dependencies. Your
47
+ application references exported table objects, defines Trigger.dev tasks against
48
+ the same SDK instance the core triggers through, and pipes the core's Observables
49
+ with its own operators; two copies of any of them would not interoperate.
50
+
51
+ Configuration comes from **Infisical**, not `.env` files:
52
+
53
+ ```bash
54
+ infisical run -- npm run dev
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Modules
60
+
61
+ | Module | Import from | Guide |
62
+ | --- | --- | --- |
63
+ | Configuration | `/config`, `/config/client` | [docs/config.md](./docs/config.md) |
64
+ | Database & schema | `/db`, `/db/schema` | [docs/db.md](./docs/db.md) |
65
+ | Authentication | `/auth` | [docs/auth.md](./docs/auth.md) |
66
+ | Users & profiles | `/users` | [docs/users.md](./docs/users.md) |
67
+ | Workspaces & members | `/workspaces` | [docs/workspaces.md](./docs/workspaces.md) |
68
+ | Roles & permissions | `/authorization` | [docs/authorization.md](./docs/authorization.md) |
69
+ | Billing | `/billing` | [docs/billing.md](./docs/billing.md) |
70
+ | Entitlements | `/entitlements` | [docs/entitlements.md](./docs/entitlements.md) |
71
+ | Events | `/events` | [docs/events.md](./docs/events.md) |
72
+ | Background jobs | `/jobs` | [docs/jobs.md](./docs/jobs.md) |
73
+ | Realtime | `/realtime` | [docs/realtime.md](./docs/realtime.md) |
74
+ | Streams (RxJS) | `/streams`, `/streams/client` | [docs/streams.md](./docs/streams.md) |
75
+ | Email | `/email` | [docs/email.md](./docs/email.md) |
76
+ | Notifications | `/notifications` | [docs/notifications.md](./docs/notifications.md) |
77
+ | Audit | `/audit` | [docs/audit.md](./docs/audit.md) |
78
+ | API keys | `/api-keys` | [docs/api-keys.md](./docs/api-keys.md) |
79
+ | Webhooks | `/webhooks` | [docs/webhooks.md](./docs/webhooks.md) |
80
+ | Feature flags | `/feature-flags` | [docs/feature-flags.md](./docs/feature-flags.md) |
81
+ | Storage | `/storage` | [docs/storage.md](./docs/storage.md) |
82
+ | Files | `/files` | [docs/files.md](./docs/files.md) |
83
+ | Logger | `/logger` | [docs/logger.md](./docs/logger.md) |
84
+ | CLI | `saas-core` binary | [docs/cli.md](./docs/cli.md) |
85
+ | Publishing | release workflow | [docs/publishing.md](./docs/publishing.md) |
86
+ | Errors | root or `/errors` | [docs/errors.md](./docs/errors.md) |
87
+
88
+ Every phase in [CLAUDE.md](./CLAUDE.md) now has an implementation. Observability
89
+ beyond the logger — tracing, metrics — is the remaining gap.
90
+
91
+ ---
92
+
93
+ ## The shape of a request
94
+
95
+ ```ts
96
+ import { auth } from "@/lib/auth";
97
+ import { workspaces } from "@digimuza/saas-core/workspaces";
98
+ import { authorization } from "@digimuza/saas-core/authorization";
99
+ import { entitlements } from "@digimuza/saas-core/entitlements";
100
+ import { jobs } from "@digimuza/saas-core/jobs";
101
+
102
+ // 1. Who is this?
103
+ const user = await auth.requireUser();
104
+
105
+ // 2. Which workspace — verified, not taken on trust from the URL.
106
+ const context = await workspaces.requireContext({
107
+ userId: user.id,
108
+ workspaceId: params.workspaceId,
109
+ });
110
+
111
+ // 3. May they? 4. Has the workspace paid for it?
112
+ await authorization.require(context, "projects.create");
113
+ await entitlements.require(context, "ai_generation");
114
+
115
+ // 5. Do the slow part durably.
116
+ await jobs.trigger("generate-project", { name }, {
117
+ workspaceId: context.workspaceId,
118
+ actorId: user.id,
119
+ });
120
+ ```
121
+
122
+ Steps 3 and 4 are separate on purpose. Authorization asks whether the *user* is
123
+ permitted; entitlements ask whether the *workspace* has paid. A client renders an
124
+ access error for one and an upsell for the other.
125
+
126
+ ---
127
+
128
+ ## Design principles you will notice in the code
129
+
130
+ **Tenant isolation is enforced by PostgreSQL, not by application code.**
131
+ `workspace_member_roles` stores `workspace_id` beside `user_id` and `role_id` so
132
+ two composite foreign keys share it — granting a member of workspace A a role from
133
+ workspace B is rejected by the database, and switching `workspace_id` to satisfy
134
+ one key breaks the other. Application code that forgets a check cannot bypass it.
135
+
136
+ **A `workspaceId` from a request cannot reach a query.** Tenant-scoped operations
137
+ take a `WorkspaceContext`, which only a verified membership lookup produces.
138
+
139
+ **Non-members get 404, not 403.** A 403 confirms a workspace exists and enables
140
+ tenant enumeration.
141
+
142
+ **Provider details stay behind adapters.** WorkOS is confined to `/auth`, Revolut
143
+ to one file in `/billing`, Trigger.dev to `/jobs`. Tests enforce each boundary, so
144
+ application code never imports a vendor SDK — and swapping one means writing an
145
+ adapter, not editing services.
146
+
147
+ **Only the config module reads `process.env`.** Enforced by a test across the whole
148
+ package.
149
+
150
+ **Idempotency and state changes are one transaction.** Webhook ingestion claims an
151
+ event with a unique-constraint insert rather than a check-then-act; domain events
152
+ are written in the same transaction as the change they describe and dispatched only
153
+ after it commits.
154
+
155
+ **Limits fail closed.** An entitlement the plan does not mention is zero, never
156
+ unlimited.
157
+
158
+ **Realtime channels are verified server-side, and there is no wildcard.** A client
159
+ names the channels it wants, so every name is parsed and then checked against
160
+ membership. Browsers never touch `LISTEN`/`NOTIFY` — the server mediates.
161
+
162
+ **The logger redacts by key and by value.** Credentials never reach a log line,
163
+ whether they arrive under a suspicious field name or as a suspicious-looking value
164
+ under an innocent one.
165
+
166
+ **Object keys are validated before they are prefixed, not after.** A `../` in a
167
+ storage key is a cross-tenant read with no database involved; the scoped API takes
168
+ paths rather than keys so the tenant prefix cannot be bypassed.
169
+
170
+ **Secrets are hashed and revealed once.** API keys and invitation tokens store
171
+ only a digest; a database leak does not let an attacker use them. Comparisons are
172
+ constant-time.
173
+
174
+ **The audit trail is immutable in the database, not by convention.** A trigger
175
+ rejects `UPDATE` and `DELETE` on `audit_logs`, so a migration or a stray `psql`
176
+ session cannot quietly rewrite history.
177
+
178
+ ---
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ npm run typecheck
184
+ npm test
185
+ npm run build
186
+ npm run db:generate # after editing src/db/schema
187
+ infisical run -- npm run db:migrate
188
+ ```
189
+
190
+ Tests run against **real PostgreSQL** — `@electric-sql/pglite`, a devDependency —
191
+ with the real migrations applied. The guarantees under test *are* database
192
+ constraints (composite foreign keys, partial unique indexes, cascades), and a mock
193
+ would happily accept the cross-tenant writes the schema exists to reject.
194
+
195
+ ---
196
+
197
+ ## Known gaps
198
+
199
+ - **No live-service verification.** PGlite is real PostgreSQL, so the schema and
200
+ constraints are genuinely exercised — but postgres.js against a server has never
201
+ been run, and neither has any Revolut HTTP call or Trigger.dev enqueue.
202
+ - **The publish workflow has never run on GitHub Actions.** Every gate was
203
+ executed locally, the YAML parses, and `npm publish --dry-run` succeeds — but no
204
+ Actions run and no real publish has happened, and `NPM_TOKEN` is not yet set.
205
+ - **The CLI has never started a real container.** Template generation, Docker
206
+ preflight, compose argument construction and every error path are tested with an
207
+ injected runner, and the generated compose file passes `docker compose config` —
208
+ but no Docker daemon was available to bring the stack up end to end.
209
+ - **Revolut's `createCustomer` and `createCheckout` are unverified** against live
210
+ credentials. Signature verification is implemented and unit-tested; the HTTP
211
+ calls are written to the documented endpoints and nothing more.
212
+ - **Revolut has no hosted portal and no server-side subscription object,** so
213
+ `getPortal()` returns `{ url: undefined }` and cancellation is recorded locally.
214
+ See [docs/billing.md](./docs/billing.md#revoluts-capability-boundaries).
215
+ - **Sending email is not transactional with the database.** A retried request can
216
+ send twice; `idempotencyKey` is the only defence, and a provider that ignores it
217
+ will deliver twice regardless.
218
+ - **Resend's HTTP call is unverified** against live credentials, like Revolut's.
219
+ - **Outgoing webhook URL checking is a first pass, not a complete SSRF defence.**
220
+ Loopback, RFC1918 and link-local hosts are refused, but DNS can still resolve a
221
+ public name to a private address at connect time; production should also route
222
+ egress through a proxy. See
223
+ [docs/webhooks.md](./docs/webhooks.md#url-safety).
224
+ - **`entitlements.consume()` is not transactional** across its check and insert, so
225
+ concurrent callers can overshoot a quota by their in-flight quantity. Deliberate;
226
+ see [docs/entitlements.md](./docs/entitlements.md#it-is-not-transactional-on-purpose).
227
+ - **Server-only enforcement is a runtime guard,** not build-time. The
228
+ `server-only` package cannot be used here because it breaks plain-Node consumers;
229
+ see [docs/config.md](./docs/config.md#why-not-the-server-only-package).
230
+ - **The default realtime transport fans out to every server** (one PostgreSQL
231
+ notification channel, filtered per subscriber), and SSE holds a long-lived
232
+ request that serverless platforms will cut at their duration cap. Both are
233
+ documented trade-offs with a transport seam to replace;
234
+ see [docs/realtime.md](./docs/realtime.md#the-default-transport-postgresql-listennotify).
@@ -0,0 +1,9 @@
1
+ import { type ApiKeysService } from "./service.ts";
2
+ export { DEFAULT_KEY_PREFIX, generateApiKey, hashApiKeySecret, maskApiKey, parseApiKey, secretMatchesHash, type GeneratedApiKey, type ParsedApiKey, } from "./keys.ts";
3
+ export { createApiKeysService, unsafeContextFromPrincipal, type ApiKeyPrincipal, type ApiKeysService, type ApiKeysServiceDeps, type ApiKeySummary, type CreateApiKeyInput, type CreatedApiKey, } from "./service.ts";
4
+ /**
5
+ * Default service, wired to membership verification — so `resolveContext` can
6
+ * refuse a key whose creator has left the workspace.
7
+ */
8
+ export declare const apiKeys: ApiKeysService;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api-keys/index.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AAEtB;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,cAKpB,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Public API keys surface: `@digimuza/saas-core/api-keys`. Server-only.
3
+ *
4
+ * Workspace-scoped machine credentials. A key is not a user: it holds *scopes*,
5
+ * not roles, and authorization for a key is a scope check.
6
+ *
7
+ * ```ts
8
+ * import { apiKeys } from "@digimuza/saas-core/api-keys";
9
+ *
10
+ * const { key, token } = await apiKeys.create(context, {
11
+ * name: "CI deploy",
12
+ * scopes: ["projects.read", "projects.deploy"],
13
+ * expiresAt: new Date(Date.now() + 90 * 86_400_000),
14
+ * });
15
+ * // `token` exists exactly once. Only its hash is stored.
16
+ *
17
+ * // On an incoming request:
18
+ * const principal = await apiKeys.validate(request.headers.get("authorization")!);
19
+ * apiKeys.requireScope(principal, "projects.deploy");
20
+ * ```
21
+ */
22
+ import { getDatabase } from "../db/client.js";
23
+ import { workspaces } from "../workspaces/index.js";
24
+ import { createApiKeysService } from "./service.js";
25
+ export { DEFAULT_KEY_PREFIX, generateApiKey, hashApiKeySecret, maskApiKey, parseApiKey, secretMatchesHash, } from "./keys.js";
26
+ export { createApiKeysService, unsafeContextFromPrincipal, } from "./service.js";
27
+ /**
28
+ * Default service, wired to membership verification — so `resolveContext` can
29
+ * refuse a key whose creator has left the workspace.
30
+ */
31
+ export const apiKeys = createApiKeysService({
32
+ get db() {
33
+ return getDatabase();
34
+ },
35
+ workspaces,
36
+ });
37
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api-keys/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAuB,MAAM,cAAc,CAAC;AAEzE,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,iBAAiB,GAGlB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,oBAAoB,EACpB,0BAA0B,GAO3B,MAAM,cAAc,CAAC;AAEtB;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAmB,oBAAoB,CAAC;IAC1D,IAAI,EAAE;QACJ,OAAO,WAAW,EAAE,CAAC;IACvB,CAAC;IACD,UAAU;CACX,CAAC,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Key format: `<prefix>_<keyId>_<secret>`.
3
+ *
4
+ * Three parts, each earning its place:
5
+ *
6
+ * - **prefix** identifies the key as ours at a glance, so a leaked string is
7
+ * recognisable in a paste or a log and secret scanners can match on it.
8
+ * - **keyId** is stored in the clear and indexed, so validation is one lookup
9
+ * rather than a scan over every hash.
10
+ * - **secret** is the only part that is hashed, and the only part never stored.
11
+ *
12
+ * Both random parts are **hex**, not base64url. base64url includes `_`, which
13
+ * would collide with the `_` delimiter and make the token ambiguous to split —
14
+ * a bug that only shows up for the fraction of keys whose random bytes happen to
15
+ * encode one. Hex costs a few extra characters and removes the failure mode.
16
+ */
17
+ export declare const DEFAULT_KEY_PREFIX = "dgk";
18
+ export interface ParsedApiKey {
19
+ readonly prefix: string;
20
+ readonly keyId: string;
21
+ readonly secret: string;
22
+ }
23
+ export interface GeneratedApiKey extends ParsedApiKey {
24
+ /** The full key. Shown once, never stored. */
25
+ readonly token: string;
26
+ readonly keyHash: string;
27
+ }
28
+ /**
29
+ * Hash the secret half of a key.
30
+ *
31
+ * SHA-256, not bcrypt or Argon2. Those exist to make *low-entropy* human
32
+ * passwords expensive to guess; the secret here is 256 bits of CSPRNG output, so
33
+ * there is no dictionary to attack. A slow KDF would add tens of milliseconds to
34
+ * every authenticated request and buy nothing.
35
+ */
36
+ export declare function hashApiKeySecret(secret: string): string;
37
+ export declare function generateApiKey(prefix?: string): GeneratedApiKey;
38
+ /**
39
+ * Parse a presented key.
40
+ *
41
+ * Returns `undefined` rather than throwing, and never explains *why* — a caller
42
+ * presenting a malformed key learns only that it was rejected. Distinguishing
43
+ * "wrong shape" from "unknown key" from "revoked" would let an attacker probe.
44
+ */
45
+ export declare function parseApiKey(token: string): ParsedApiKey | undefined;
46
+ /**
47
+ * Compare a presented secret against a stored hash in constant time.
48
+ *
49
+ * A plain `===` on hex digests leaks, through timing, how many leading characters
50
+ * matched — which is enough to reconstruct a digest one character at a time.
51
+ */
52
+ export declare function secretMatchesHash(secret: string, keyHash: string): boolean;
53
+ /** The displayable form of a key: enough to recognise, not enough to use. */
54
+ export declare function maskApiKey(prefix: string, keyId: string): string;
55
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/api-keys/keys.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAOxC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,cAAc,CAAC,MAAM,GAAE,MAA2B,GAAG,eAAe,CAiBnF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAuBnE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAS1E;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE"}
@@ -0,0 +1,92 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { InvalidInputError } from "../errors.js";
3
+ /**
4
+ * Key format: `<prefix>_<keyId>_<secret>`.
5
+ *
6
+ * Three parts, each earning its place:
7
+ *
8
+ * - **prefix** identifies the key as ours at a glance, so a leaked string is
9
+ * recognisable in a paste or a log and secret scanners can match on it.
10
+ * - **keyId** is stored in the clear and indexed, so validation is one lookup
11
+ * rather than a scan over every hash.
12
+ * - **secret** is the only part that is hashed, and the only part never stored.
13
+ *
14
+ * Both random parts are **hex**, not base64url. base64url includes `_`, which
15
+ * would collide with the `_` delimiter and make the token ambiguous to split —
16
+ * a bug that only shows up for the fraction of keys whose random bytes happen to
17
+ * encode one. Hex costs a few extra characters and removes the failure mode.
18
+ */
19
+ export const DEFAULT_KEY_PREFIX = "dgk";
20
+ /** 128 bits — enough to identify, and it is not a secret. */
21
+ const KEY_ID_BYTES = 16;
22
+ /** 256 bits. What makes a fast hash sufficient. */
23
+ const SECRET_BYTES = 32;
24
+ /**
25
+ * Hash the secret half of a key.
26
+ *
27
+ * SHA-256, not bcrypt or Argon2. Those exist to make *low-entropy* human
28
+ * passwords expensive to guess; the secret here is 256 bits of CSPRNG output, so
29
+ * there is no dictionary to attack. A slow KDF would add tens of milliseconds to
30
+ * every authenticated request and buy nothing.
31
+ */
32
+ export function hashApiKeySecret(secret) {
33
+ return createHash("sha256").update(secret).digest("hex");
34
+ }
35
+ export function generateApiKey(prefix = DEFAULT_KEY_PREFIX) {
36
+ if (!/^[a-z][a-z0-9]{1,15}$/.test(prefix)) {
37
+ throw new InvalidInputError("An API key prefix must be 2-16 lowercase alphanumerics starting with a letter.");
38
+ }
39
+ const keyId = randomBytes(KEY_ID_BYTES).toString("hex");
40
+ const secret = randomBytes(SECRET_BYTES).toString("hex");
41
+ return {
42
+ prefix,
43
+ keyId,
44
+ secret,
45
+ token: `${prefix}_${keyId}_${secret}`,
46
+ keyHash: hashApiKeySecret(secret),
47
+ };
48
+ }
49
+ /**
50
+ * Parse a presented key.
51
+ *
52
+ * Returns `undefined` rather than throwing, and never explains *why* — a caller
53
+ * presenting a malformed key learns only that it was rejected. Distinguishing
54
+ * "wrong shape" from "unknown key" from "revoked" would let an attacker probe.
55
+ */
56
+ export function parseApiKey(token) {
57
+ // Bounded before any work, so an oversized string costs nothing.
58
+ if (typeof token !== "string" || token.length < 16 || token.length > 256) {
59
+ return undefined;
60
+ }
61
+ const parts = token.split("_");
62
+ if (parts.length !== 3) {
63
+ return undefined;
64
+ }
65
+ const [prefix, keyId, secret] = parts;
66
+ // Exact hex lengths; anything else is not a key we issued.
67
+ if (!/^[a-z][a-z0-9]{1,15}$/.test(prefix) ||
68
+ !new RegExp(`^[0-9a-f]{${KEY_ID_BYTES * 2}}$`).test(keyId) ||
69
+ !new RegExp(`^[0-9a-f]{${SECRET_BYTES * 2}}$`).test(secret)) {
70
+ return undefined;
71
+ }
72
+ return { prefix, keyId, secret };
73
+ }
74
+ /**
75
+ * Compare a presented secret against a stored hash in constant time.
76
+ *
77
+ * A plain `===` on hex digests leaks, through timing, how many leading characters
78
+ * matched — which is enough to reconstruct a digest one character at a time.
79
+ */
80
+ export function secretMatchesHash(secret, keyHash) {
81
+ const presented = Buffer.from(hashApiKeySecret(secret), "utf8");
82
+ const stored = Buffer.from(keyHash, "utf8");
83
+ if (presented.length !== stored.length) {
84
+ return false;
85
+ }
86
+ return timingSafeEqual(presented, stored);
87
+ }
88
+ /** The displayable form of a key: enough to recognise, not enough to use. */
89
+ export function maskApiKey(prefix, keyId) {
90
+ return `${prefix}_${keyId.slice(0, 6)}…`;
91
+ }
92
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/api-keys/keys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import type { CoreDatabase } from "../db/types.ts";
2
+ import { type WorkspaceContext } from "../workspaces/context.ts";
3
+ import type { WorkspacesService } from "../workspaces/service.ts";
4
+ /**
5
+ * A validated key, as a principal.
6
+ *
7
+ * Note what is absent: there is no user. An API key is a workspace-scoped
8
+ * machine principal holding *scopes*, not a person holding roles. Authorization
9
+ * for a key is a scope check, which is why this is not a `WorkspaceContext`.
10
+ */
11
+ export interface ApiKeyPrincipal {
12
+ readonly apiKeyId: string;
13
+ readonly keyId: string;
14
+ readonly workspaceId: string;
15
+ readonly name: string;
16
+ readonly scopes: readonly string[];
17
+ /** Who created it, if that user still exists. */
18
+ readonly createdByUserId: string | null;
19
+ }
20
+ /** A key as shown in a list: never the secret, and never the hash. */
21
+ export interface ApiKeySummary {
22
+ readonly id: string;
23
+ readonly name: string;
24
+ /** `dgk_a1b2c3…` — enough to match a log line to a row. */
25
+ readonly masked: string;
26
+ readonly scopes: readonly string[];
27
+ readonly createdByUserId: string | null;
28
+ readonly createdAt: Date;
29
+ readonly lastUsedAt: Date | null;
30
+ readonly expiresAt: Date | null;
31
+ readonly revokedAt: Date | null;
32
+ }
33
+ export interface CreateApiKeyInput {
34
+ readonly name: string;
35
+ readonly scopes?: readonly string[];
36
+ /** Null or omitted for a key that never expires. */
37
+ readonly expiresAt?: Date | null;
38
+ /** Overrides the default `dgk`. 2-16 lowercase alphanumerics. */
39
+ readonly prefix?: string;
40
+ }
41
+ export interface CreatedApiKey {
42
+ readonly key: ApiKeySummary;
43
+ /**
44
+ * The full key, shown once.
45
+ *
46
+ * Only its hash is stored, so this is the one and only time it exists. Losing
47
+ * it means revoking and issuing a new one.
48
+ */
49
+ readonly token: string;
50
+ }
51
+ export interface ApiKeysService {
52
+ /** Issue a key for the caller's workspace. */
53
+ create(context: WorkspaceContext, input: CreateApiKeyInput): Promise<CreatedApiKey>;
54
+ /** The workspace's keys, newest first. Never includes secrets. */
55
+ list(context: WorkspaceContext, options?: {
56
+ includeRevoked?: boolean;
57
+ }): Promise<ApiKeySummary[]>;
58
+ /**
59
+ * Validate a presented key.
60
+ *
61
+ * @throws UnauthorizedError for anything invalid — unknown, malformed, revoked
62
+ * or expired all produce the same error, so a caller cannot probe.
63
+ */
64
+ validate(token: string): Promise<ApiKeyPrincipal>;
65
+ /** As {@link validate}, but returns undefined instead of throwing. */
66
+ tryValidate(token: string): Promise<ApiKeyPrincipal | undefined>;
67
+ /**
68
+ * Validate, then confirm the key's creator is *still* a member of the
69
+ * workspace, and return a context usable with the rest of the core.
70
+ *
71
+ * The membership re-check is the point. Without it a key would keep working
72
+ * after the person who created it lost access — the classic way a machine
73
+ * credential outlives the authority it was granted under.
74
+ */
75
+ resolveContext(token: string): Promise<{
76
+ context: WorkspaceContext;
77
+ principal: ApiKeyPrincipal;
78
+ }>;
79
+ /** Revoke immediately. Idempotent. */
80
+ revoke(context: WorkspaceContext, apiKeyId: string): Promise<void>;
81
+ /** Assert a scope, or throw {@link ForbiddenError}. */
82
+ requireScope(principal: ApiKeyPrincipal, scope: string): void;
83
+ /** Whether the principal holds every listed scope. */
84
+ hasScopes(principal: ApiKeyPrincipal, scopes: readonly string[]): boolean;
85
+ }
86
+ export interface ApiKeysServiceDeps {
87
+ readonly db: CoreDatabase;
88
+ /** Used by {@link ApiKeysService.resolveContext} to re-check membership. */
89
+ readonly workspaces?: Pick<WorkspacesService, "resolveContext">;
90
+ readonly now?: () => Date;
91
+ /**
92
+ * How stale `last_used_at` may get before it is rewritten.
93
+ *
94
+ * Updating on every request would turn validation into a write. A minute is
95
+ * enough to answer "is this key still in use?".
96
+ */
97
+ readonly lastUsedThrottleMs?: number;
98
+ }
99
+ export declare function createApiKeysService(deps: ApiKeysServiceDeps): ApiKeysService;
100
+ /**
101
+ * Build a context from a principal without re-checking membership.
102
+ *
103
+ * Internal escape hatch, exported because a machine-to-machine integration may
104
+ * legitimately have no user behind it at all — in which case there is no
105
+ * membership to verify and the key itself is the authority. Prefer
106
+ * `resolveContext`, which does verify.
107
+ *
108
+ * @internal
109
+ */
110
+ export declare function unsafeContextFromPrincipal(principal: ApiKeyPrincipal): WorkspaceContext;
111
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/api-keys/service.ts"],"names":[],"mappings":"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"}