alepha 0.15.0 → 0.15.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 (551) hide show
  1. package/README.md +43 -98
  2. package/dist/api/audits/index.d.ts +630 -653
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +12 -35
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +365 -358
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +12 -5
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +255 -248
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/jobs/index.js +10 -3
  13. package/dist/api/jobs/index.js.map +1 -1
  14. package/dist/api/keys/index.d.ts +413 -0
  15. package/dist/api/keys/index.d.ts.map +1 -0
  16. package/dist/api/keys/index.js +476 -0
  17. package/dist/api/keys/index.js.map +1 -0
  18. package/dist/api/notifications/index.browser.js +4 -4
  19. package/dist/api/notifications/index.browser.js.map +1 -1
  20. package/dist/api/notifications/index.d.ts +84 -78
  21. package/dist/api/notifications/index.d.ts.map +1 -1
  22. package/dist/api/notifications/index.js +14 -8
  23. package/dist/api/notifications/index.js.map +1 -1
  24. package/dist/api/parameters/index.d.ts +528 -535
  25. package/dist/api/parameters/index.d.ts.map +1 -1
  26. package/dist/api/parameters/index.js +30 -37
  27. package/dist/api/parameters/index.js.map +1 -1
  28. package/dist/api/users/index.d.ts +1221 -910
  29. package/dist/api/users/index.d.ts.map +1 -1
  30. package/dist/api/users/index.js +2556 -248
  31. package/dist/api/users/index.js.map +1 -1
  32. package/dist/api/verifications/index.d.ts +142 -136
  33. package/dist/api/verifications/index.d.ts.map +1 -1
  34. package/dist/api/verifications/index.js +12 -4
  35. package/dist/api/verifications/index.js.map +1 -1
  36. package/dist/batch/index.d.ts +142 -162
  37. package/dist/batch/index.d.ts.map +1 -1
  38. package/dist/batch/index.js +31 -44
  39. package/dist/batch/index.js.map +1 -1
  40. package/dist/bucket/index.d.ts +595 -171
  41. package/dist/bucket/index.d.ts.map +1 -1
  42. package/dist/bucket/index.js +1856 -12
  43. package/dist/bucket/index.js.map +1 -1
  44. package/dist/cache/core/index.d.ts +225 -53
  45. package/dist/cache/core/index.d.ts.map +1 -1
  46. package/dist/cache/core/index.js +213 -7
  47. package/dist/cache/core/index.js.map +1 -1
  48. package/dist/cache/redis/index.d.ts +1 -0
  49. package/dist/cache/redis/index.d.ts.map +1 -1
  50. package/dist/cache/redis/index.js +6 -2
  51. package/dist/cache/redis/index.js.map +1 -1
  52. package/dist/cli/index.d.ts +834 -226
  53. package/dist/cli/index.d.ts.map +1 -1
  54. package/dist/cli/index.js +2872 -417
  55. package/dist/cli/index.js.map +1 -1
  56. package/dist/command/index.d.ts +458 -310
  57. package/dist/command/index.d.ts.map +1 -1
  58. package/dist/command/index.js +2011 -76
  59. package/dist/command/index.js.map +1 -1
  60. package/dist/core/index.browser.js +309 -97
  61. package/dist/core/index.browser.js.map +1 -1
  62. package/dist/core/index.d.ts +796 -701
  63. package/dist/core/index.d.ts.map +1 -1
  64. package/dist/core/index.js +329 -97
  65. package/dist/core/index.js.map +1 -1
  66. package/dist/core/index.native.js +309 -97
  67. package/dist/core/index.native.js.map +1 -1
  68. package/dist/datetime/index.d.ts +59 -44
  69. package/dist/datetime/index.d.ts.map +1 -1
  70. package/dist/datetime/index.js +15 -0
  71. package/dist/datetime/index.js.map +1 -1
  72. package/dist/email/index.d.ts +314 -19
  73. package/dist/email/index.d.ts.map +1 -1
  74. package/dist/email/index.js +1852 -7
  75. package/dist/email/index.js.map +1 -1
  76. package/dist/fake/index.d.ts +5500 -5418
  77. package/dist/fake/index.d.ts.map +1 -1
  78. package/dist/fake/index.js +113 -42
  79. package/dist/fake/index.js.map +1 -1
  80. package/dist/lock/core/index.d.ts +219 -212
  81. package/dist/lock/core/index.d.ts.map +1 -1
  82. package/dist/lock/core/index.js +11 -4
  83. package/dist/lock/core/index.js.map +1 -1
  84. package/dist/lock/redis/index.d.ts.map +1 -1
  85. package/dist/logger/index.d.ts +41 -90
  86. package/dist/logger/index.d.ts.map +1 -1
  87. package/dist/logger/index.js +15 -68
  88. package/dist/logger/index.js.map +1 -1
  89. package/dist/mcp/index.d.ts +228 -230
  90. package/dist/mcp/index.d.ts.map +1 -1
  91. package/dist/mcp/index.js +32 -31
  92. package/dist/mcp/index.js.map +1 -1
  93. package/dist/orm/index.browser.js +12 -12
  94. package/dist/orm/index.browser.js.map +1 -1
  95. package/dist/orm/index.bun.js +90 -80
  96. package/dist/orm/index.bun.js.map +1 -1
  97. package/dist/orm/index.d.ts +1434 -1459
  98. package/dist/orm/index.d.ts.map +1 -1
  99. package/dist/orm/index.js +112 -130
  100. package/dist/orm/index.js.map +1 -1
  101. package/dist/queue/core/index.d.ts +262 -254
  102. package/dist/queue/core/index.d.ts.map +1 -1
  103. package/dist/queue/core/index.js +14 -6
  104. package/dist/queue/core/index.js.map +1 -1
  105. package/dist/queue/redis/index.d.ts.map +1 -1
  106. package/dist/react/auth/index.browser.js +108 -0
  107. package/dist/react/auth/index.browser.js.map +1 -0
  108. package/dist/react/auth/index.d.ts +100 -0
  109. package/dist/react/auth/index.d.ts.map +1 -0
  110. package/dist/react/auth/index.js +145 -0
  111. package/dist/react/auth/index.js.map +1 -0
  112. package/dist/react/core/index.d.ts +469 -0
  113. package/dist/react/core/index.d.ts.map +1 -0
  114. package/dist/react/core/index.js +464 -0
  115. package/dist/react/core/index.js.map +1 -0
  116. package/dist/react/form/index.d.ts +232 -0
  117. package/dist/react/form/index.d.ts.map +1 -0
  118. package/dist/react/form/index.js +432 -0
  119. package/dist/react/form/index.js.map +1 -0
  120. package/dist/react/head/index.browser.js +423 -0
  121. package/dist/react/head/index.browser.js.map +1 -0
  122. package/dist/react/head/index.d.ts +288 -0
  123. package/dist/react/head/index.d.ts.map +1 -0
  124. package/dist/react/head/index.js +465 -0
  125. package/dist/react/head/index.js.map +1 -0
  126. package/dist/react/i18n/index.d.ts +175 -0
  127. package/dist/react/i18n/index.d.ts.map +1 -0
  128. package/dist/react/i18n/index.js +224 -0
  129. package/dist/react/i18n/index.js.map +1 -0
  130. package/dist/react/router/index.browser.js +1980 -0
  131. package/dist/react/router/index.browser.js.map +1 -0
  132. package/dist/react/router/index.d.ts +2068 -0
  133. package/dist/react/router/index.d.ts.map +1 -0
  134. package/dist/react/router/index.js +4932 -0
  135. package/dist/react/router/index.js.map +1 -0
  136. package/dist/react/websocket/index.d.ts +117 -0
  137. package/dist/react/websocket/index.d.ts.map +1 -0
  138. package/dist/react/websocket/index.js +107 -0
  139. package/dist/react/websocket/index.js.map +1 -0
  140. package/dist/redis/index.bun.js +4 -0
  141. package/dist/redis/index.bun.js.map +1 -1
  142. package/dist/redis/index.d.ts +127 -130
  143. package/dist/redis/index.d.ts.map +1 -1
  144. package/dist/redis/index.js +16 -25
  145. package/dist/redis/index.js.map +1 -1
  146. package/dist/retry/index.d.ts +80 -71
  147. package/dist/retry/index.d.ts.map +1 -1
  148. package/dist/retry/index.js +11 -2
  149. package/dist/retry/index.js.map +1 -1
  150. package/dist/router/index.d.ts +6 -6
  151. package/dist/router/index.d.ts.map +1 -1
  152. package/dist/scheduler/index.d.ts +119 -28
  153. package/dist/scheduler/index.d.ts.map +1 -1
  154. package/dist/scheduler/index.js +404 -3
  155. package/dist/scheduler/index.js.map +1 -1
  156. package/dist/security/index.d.ts +642 -228
  157. package/dist/security/index.d.ts.map +1 -1
  158. package/dist/security/index.js +1579 -37
  159. package/dist/security/index.js.map +1 -1
  160. package/dist/server/auth/index.d.ts +1141 -111
  161. package/dist/server/auth/index.d.ts.map +1 -1
  162. package/dist/server/auth/index.js +1261 -25
  163. package/dist/server/auth/index.js.map +1 -1
  164. package/dist/server/cache/index.d.ts +63 -78
  165. package/dist/server/cache/index.d.ts.map +1 -1
  166. package/dist/server/cache/index.js +7 -22
  167. package/dist/server/cache/index.js.map +1 -1
  168. package/dist/server/compress/index.d.ts +13 -5
  169. package/dist/server/compress/index.d.ts.map +1 -1
  170. package/dist/server/compress/index.js +10 -2
  171. package/dist/server/compress/index.js.map +1 -1
  172. package/dist/server/cookies/index.d.ts +46 -22
  173. package/dist/server/cookies/index.d.ts.map +1 -1
  174. package/dist/server/cookies/index.js +7 -5
  175. package/dist/server/cookies/index.js.map +1 -1
  176. package/dist/server/core/index.d.ts +307 -196
  177. package/dist/server/core/index.d.ts.map +1 -1
  178. package/dist/server/core/index.js +271 -38
  179. package/dist/server/core/index.js.map +1 -1
  180. package/dist/server/cors/index.d.ts +24 -34
  181. package/dist/server/cors/index.d.ts.map +1 -1
  182. package/dist/server/cors/index.js +7 -21
  183. package/dist/server/cors/index.js.map +1 -1
  184. package/dist/server/health/index.d.ts +25 -19
  185. package/dist/server/health/index.d.ts.map +1 -1
  186. package/dist/server/health/index.js +8 -2
  187. package/dist/server/health/index.js.map +1 -1
  188. package/dist/server/helmet/index.d.ts +13 -5
  189. package/dist/server/helmet/index.d.ts.map +1 -1
  190. package/dist/server/helmet/index.js +11 -3
  191. package/dist/server/helmet/index.js.map +1 -1
  192. package/dist/server/links/index.browser.js +9 -1
  193. package/dist/server/links/index.browser.js.map +1 -1
  194. package/dist/server/links/index.d.ts +133 -128
  195. package/dist/server/links/index.d.ts.map +1 -1
  196. package/dist/server/links/index.js +24 -11
  197. package/dist/server/links/index.js.map +1 -1
  198. package/dist/server/metrics/index.d.ts +524 -4
  199. package/dist/server/metrics/index.d.ts.map +1 -1
  200. package/dist/server/metrics/index.js +4472 -7
  201. package/dist/server/metrics/index.js.map +1 -1
  202. package/dist/server/multipart/index.d.ts +15 -9
  203. package/dist/server/multipart/index.d.ts.map +1 -1
  204. package/dist/server/multipart/index.js +9 -3
  205. package/dist/server/multipart/index.js.map +1 -1
  206. package/dist/server/proxy/index.d.ts +110 -104
  207. package/dist/server/proxy/index.d.ts.map +1 -1
  208. package/dist/server/proxy/index.js +8 -2
  209. package/dist/server/proxy/index.js.map +1 -1
  210. package/dist/server/rate-limit/index.d.ts +46 -51
  211. package/dist/server/rate-limit/index.d.ts.map +1 -1
  212. package/dist/server/rate-limit/index.js +18 -55
  213. package/dist/server/rate-limit/index.js.map +1 -1
  214. package/dist/server/static/index.d.ts +181 -48
  215. package/dist/server/static/index.d.ts.map +1 -1
  216. package/dist/server/static/index.js +1848 -5
  217. package/dist/server/static/index.js.map +1 -1
  218. package/dist/server/swagger/index.d.ts +348 -53
  219. package/dist/server/swagger/index.d.ts.map +1 -1
  220. package/dist/server/swagger/index.js +1849 -6
  221. package/dist/server/swagger/index.js.map +1 -1
  222. package/dist/sms/index.d.ts +312 -18
  223. package/dist/sms/index.d.ts.map +1 -1
  224. package/dist/sms/index.js +1854 -10
  225. package/dist/sms/index.js.map +1 -1
  226. package/dist/system/index.browser.js +496 -0
  227. package/dist/system/index.browser.js.map +1 -0
  228. package/dist/system/index.d.ts +1158 -0
  229. package/dist/system/index.d.ts.map +1 -0
  230. package/dist/{file → system}/index.js +412 -20
  231. package/dist/system/index.js.map +1 -0
  232. package/dist/thread/index.d.ts +82 -73
  233. package/dist/thread/index.d.ts.map +1 -1
  234. package/dist/thread/index.js +13 -4
  235. package/dist/thread/index.js.map +1 -1
  236. package/dist/topic/core/index.d.ts +330 -323
  237. package/dist/topic/core/index.d.ts.map +1 -1
  238. package/dist/topic/core/index.js +12 -5
  239. package/dist/topic/core/index.js.map +1 -1
  240. package/dist/topic/redis/index.d.ts +6 -6
  241. package/dist/topic/redis/index.d.ts.map +1 -1
  242. package/dist/vite/index.d.ts +163 -5825
  243. package/dist/vite/index.d.ts.map +1 -1
  244. package/dist/vite/index.js +130 -477
  245. package/dist/vite/index.js.map +1 -1
  246. package/dist/websocket/index.browser.js +3 -3
  247. package/dist/websocket/index.browser.js.map +1 -1
  248. package/dist/websocket/index.d.ts +287 -283
  249. package/dist/websocket/index.d.ts.map +1 -1
  250. package/dist/websocket/index.js +15 -11
  251. package/dist/websocket/index.js.map +1 -1
  252. package/package.json +86 -17
  253. package/src/api/audits/index.ts +10 -33
  254. package/src/api/files/__tests__/$bucket.spec.ts +1 -1
  255. package/src/api/files/controllers/AdminFileStatsController.spec.ts +1 -1
  256. package/src/api/files/controllers/FileController.spec.ts +1 -1
  257. package/src/api/files/index.ts +10 -3
  258. package/src/api/files/jobs/FileJobs.spec.ts +1 -1
  259. package/src/api/files/services/FileService.spec.ts +1 -1
  260. package/src/api/jobs/index.ts +10 -3
  261. package/src/api/keys/controllers/AdminApiKeyController.ts +75 -0
  262. package/src/api/keys/controllers/ApiKeyController.ts +103 -0
  263. package/src/api/keys/entities/apiKeyEntity.ts +41 -0
  264. package/src/api/keys/index.ts +49 -0
  265. package/src/api/keys/schemas/adminApiKeyQuerySchema.ts +7 -0
  266. package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +17 -0
  267. package/src/api/keys/schemas/createApiKeyBodySchema.ts +7 -0
  268. package/src/api/keys/schemas/createApiKeyResponseSchema.ts +11 -0
  269. package/src/api/keys/schemas/listApiKeyResponseSchema.ts +15 -0
  270. package/src/api/keys/schemas/revokeApiKeyParamsSchema.ts +5 -0
  271. package/src/api/keys/schemas/revokeApiKeyResponseSchema.ts +5 -0
  272. package/src/api/keys/services/ApiKeyService.spec.ts +553 -0
  273. package/src/api/keys/services/ApiKeyService.ts +306 -0
  274. package/src/api/logs/TODO.md +52 -0
  275. package/src/api/notifications/index.ts +10 -4
  276. package/src/api/parameters/index.ts +9 -30
  277. package/src/api/parameters/primitives/$config.ts +12 -4
  278. package/src/api/parameters/services/ConfigStore.ts +9 -3
  279. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1035 -0
  280. package/src/api/users/__tests__/ApiKeys.spec.ts +401 -0
  281. package/src/api/users/index.ts +14 -3
  282. package/src/api/users/primitives/$realm.ts +33 -5
  283. package/src/api/users/providers/RealmProvider.ts +1 -12
  284. package/src/api/users/services/SessionService.ts +1 -11
  285. package/src/api/verifications/controllers/VerificationController.ts +2 -0
  286. package/src/api/verifications/index.ts +10 -4
  287. package/src/batch/index.ts +9 -36
  288. package/src/batch/primitives/$batch.ts +0 -8
  289. package/src/batch/providers/BatchProvider.ts +29 -2
  290. package/src/bucket/__tests__/shared.ts +1 -1
  291. package/src/bucket/index.ts +13 -6
  292. package/src/bucket/primitives/$bucket.ts +1 -1
  293. package/src/bucket/providers/LocalFileStorageProvider.ts +1 -1
  294. package/src/bucket/providers/MemoryFileStorageProvider.ts +1 -1
  295. package/src/cache/core/__tests__/shared.ts +30 -0
  296. package/src/cache/core/index.ts +11 -6
  297. package/src/cache/core/primitives/$cache.spec.ts +5 -0
  298. package/src/cache/core/providers/CacheProvider.ts +17 -0
  299. package/src/cache/core/providers/MemoryCacheProvider.ts +300 -1
  300. package/src/cache/redis/__tests__/cache-redis.spec.ts +5 -0
  301. package/src/cache/redis/providers/RedisCacheProvider.ts +9 -0
  302. package/src/cli/apps/AlephaCli.ts +3 -16
  303. package/src/cli/apps/AlephaPackageBuilderCli.ts +10 -2
  304. package/src/cli/atoms/appEntryOptions.ts +13 -0
  305. package/src/cli/atoms/buildOptions.ts +1 -1
  306. package/src/cli/atoms/changelogOptions.ts +1 -1
  307. package/src/cli/commands/build.ts +64 -52
  308. package/src/cli/commands/db.ts +17 -11
  309. package/src/cli/commands/deploy.ts +1 -1
  310. package/src/cli/commands/dev.ts +13 -49
  311. package/src/cli/commands/gen/env.ts +6 -3
  312. package/src/cli/commands/gen/openapi.ts +5 -2
  313. package/src/cli/commands/init.spec.ts +544 -0
  314. package/src/cli/commands/init.ts +101 -58
  315. package/src/cli/commands/lint.ts +8 -2
  316. package/src/cli/commands/typecheck.ts +11 -0
  317. package/src/cli/defineConfig.ts +9 -0
  318. package/src/cli/index.ts +2 -1
  319. package/src/cli/providers/AppEntryProvider.ts +131 -0
  320. package/src/cli/providers/ViteBuildProvider.ts +40 -0
  321. package/src/cli/providers/ViteDevServerProvider.ts +378 -0
  322. package/src/cli/services/AlephaCliUtils.ts +39 -93
  323. package/src/cli/services/PackageManagerUtils.ts +140 -17
  324. package/src/cli/services/ProjectScaffolder.ts +169 -101
  325. package/src/cli/services/ViteUtils.ts +82 -0
  326. package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +41 -28
  327. package/src/cli/{assets → templates}/apiHelloControllerTs.ts +2 -1
  328. package/src/cli/{assets → templates}/biomeJson.ts +2 -1
  329. package/src/cli/{assets → templates}/dummySpecTs.ts +2 -1
  330. package/src/cli/{assets → templates}/editorconfig.ts +2 -1
  331. package/src/cli/templates/gitignore.ts +39 -0
  332. package/src/cli/{assets → templates}/mainBrowserTs.ts +2 -1
  333. package/src/cli/templates/mainCss.ts +33 -0
  334. package/src/cli/templates/mainServerTs.ts +33 -0
  335. package/src/cli/{assets → templates}/tsconfigJson.ts +2 -1
  336. package/src/cli/templates/webAppRouterTs.ts +50 -0
  337. package/src/cli/templates/webHelloComponentTsx.ts +20 -0
  338. package/src/command/helpers/Runner.spec.ts +4 -0
  339. package/src/command/helpers/Runner.ts +3 -21
  340. package/src/command/index.ts +12 -4
  341. package/src/command/providers/CliProvider.spec.ts +1067 -0
  342. package/src/command/providers/CliProvider.ts +203 -40
  343. package/src/core/Alepha.ts +3 -9
  344. package/src/core/__tests__/Alepha-start.spec.ts +4 -4
  345. package/src/core/helpers/jsonSchemaToTypeBox.spec.ts +771 -0
  346. package/src/core/helpers/jsonSchemaToTypeBox.ts +62 -10
  347. package/src/core/index.shared.ts +1 -0
  348. package/src/core/index.ts +20 -0
  349. package/src/core/primitives/$module.ts +12 -0
  350. package/src/core/providers/EventManager.spec.ts +0 -71
  351. package/src/core/providers/EventManager.ts +3 -15
  352. package/src/core/providers/Json.ts +2 -14
  353. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
  354. package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
  355. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  356. package/src/datetime/index.ts +15 -0
  357. package/src/email/index.ts +10 -5
  358. package/src/email/providers/LocalEmailProvider.spec.ts +1 -1
  359. package/src/email/providers/LocalEmailProvider.ts +1 -1
  360. package/src/fake/__tests__/keyName.example.ts +1 -1
  361. package/src/fake/__tests__/keyName.spec.ts +5 -5
  362. package/src/fake/index.ts +9 -6
  363. package/src/fake/providers/FakeProvider.spec.ts +258 -40
  364. package/src/fake/providers/FakeProvider.ts +133 -19
  365. package/src/lock/core/index.ts +11 -4
  366. package/src/logger/index.ts +17 -66
  367. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  368. package/src/mcp/errors/McpError.ts +30 -0
  369. package/src/mcp/index.ts +13 -27
  370. package/src/mcp/transports/SseMcpTransport.ts +6 -7
  371. package/src/orm/__tests__/PostgresProvider.spec.ts +2 -2
  372. package/src/orm/index.browser.ts +2 -2
  373. package/src/orm/index.bun.ts +4 -2
  374. package/src/orm/index.ts +21 -47
  375. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  376. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -0
  377. package/src/orm/services/Repository.ts +18 -3
  378. package/src/queue/core/index.ts +14 -6
  379. package/src/react/auth/__tests__/$auth.spec.ts +202 -0
  380. package/src/react/auth/hooks/useAuth.ts +32 -0
  381. package/src/react/auth/index.browser.ts +13 -0
  382. package/src/react/auth/index.shared.ts +2 -0
  383. package/src/react/auth/index.ts +48 -0
  384. package/src/react/auth/providers/ReactAuthProvider.ts +16 -0
  385. package/src/react/auth/services/ReactAuth.ts +135 -0
  386. package/src/react/core/__tests__/Router.spec.tsx +169 -0
  387. package/src/react/core/components/ClientOnly.tsx +49 -0
  388. package/src/react/core/components/ErrorBoundary.tsx +73 -0
  389. package/src/react/core/contexts/AlephaContext.ts +7 -0
  390. package/src/react/core/contexts/AlephaProvider.tsx +42 -0
  391. package/src/react/core/hooks/useAction.browser.spec.tsx +569 -0
  392. package/src/react/core/hooks/useAction.ts +480 -0
  393. package/src/react/core/hooks/useAlepha.ts +26 -0
  394. package/src/react/core/hooks/useClient.ts +17 -0
  395. package/src/react/core/hooks/useEvents.ts +51 -0
  396. package/src/react/core/hooks/useInject.ts +12 -0
  397. package/src/react/core/hooks/useStore.ts +52 -0
  398. package/src/react/core/index.ts +90 -0
  399. package/src/react/form/components/FormState.tsx +17 -0
  400. package/src/react/form/errors/FormValidationError.ts +18 -0
  401. package/src/react/form/hooks/useForm.browser.spec.tsx +366 -0
  402. package/src/react/form/hooks/useForm.ts +47 -0
  403. package/src/react/form/hooks/useFormState.ts +130 -0
  404. package/src/react/form/index.ts +44 -0
  405. package/src/react/form/services/FormModel.ts +614 -0
  406. package/src/react/head/helpers/SeoExpander.spec.ts +203 -0
  407. package/src/react/head/helpers/SeoExpander.ts +142 -0
  408. package/src/react/head/hooks/useHead.spec.tsx +288 -0
  409. package/src/react/head/hooks/useHead.ts +62 -0
  410. package/src/react/head/index.browser.ts +26 -0
  411. package/src/react/head/index.ts +44 -0
  412. package/src/react/head/interfaces/Head.ts +105 -0
  413. package/src/react/head/primitives/$head.ts +25 -0
  414. package/src/react/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
  415. package/src/react/head/providers/BrowserHeadProvider.ts +212 -0
  416. package/src/react/head/providers/HeadProvider.ts +168 -0
  417. package/src/react/head/providers/ServerHeadProvider.ts +31 -0
  418. package/src/react/i18n/__tests__/integration.spec.tsx +239 -0
  419. package/src/react/i18n/components/Localize.spec.tsx +357 -0
  420. package/src/react/i18n/components/Localize.tsx +35 -0
  421. package/src/react/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  422. package/src/react/i18n/hooks/useI18n.ts +18 -0
  423. package/src/react/i18n/index.ts +41 -0
  424. package/src/react/i18n/primitives/$dictionary.ts +69 -0
  425. package/src/react/i18n/providers/I18nProvider.spec.ts +389 -0
  426. package/src/react/i18n/providers/I18nProvider.ts +278 -0
  427. package/src/react/router/__tests__/page-head-browser.browser.spec.ts +95 -0
  428. package/src/react/router/__tests__/page-head.spec.ts +48 -0
  429. package/src/react/router/__tests__/seo-head.spec.ts +125 -0
  430. package/src/react/router/atoms/ssrManifestAtom.ts +58 -0
  431. package/src/react/router/components/ErrorViewer.tsx +872 -0
  432. package/src/react/router/components/Link.tsx +23 -0
  433. package/src/react/router/components/NestedView.tsx +223 -0
  434. package/src/react/router/components/NotFound.tsx +30 -0
  435. package/src/react/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  436. package/src/react/router/contexts/RouterLayerContext.ts +12 -0
  437. package/src/react/router/errors/Redirection.ts +28 -0
  438. package/src/react/router/hooks/useActive.ts +52 -0
  439. package/src/react/router/hooks/useQueryParams.ts +63 -0
  440. package/src/react/router/hooks/useRouter.ts +20 -0
  441. package/src/react/router/hooks/useRouterState.ts +11 -0
  442. package/src/react/router/index.browser.ts +45 -0
  443. package/src/react/router/index.shared.ts +19 -0
  444. package/src/react/router/index.ts +142 -0
  445. package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
  446. package/src/react/router/primitives/$page.spec.tsx +708 -0
  447. package/src/react/router/primitives/$page.ts +497 -0
  448. package/src/react/router/providers/ReactBrowserProvider.ts +309 -0
  449. package/src/react/router/providers/ReactBrowserRendererProvider.ts +25 -0
  450. package/src/react/router/providers/ReactBrowserRouterProvider.ts +168 -0
  451. package/src/react/router/providers/ReactPageProvider.ts +726 -0
  452. package/src/react/router/providers/ReactServerProvider.spec.tsx +316 -0
  453. package/src/react/router/providers/ReactServerProvider.ts +558 -0
  454. package/src/react/router/providers/ReactServerTemplateProvider.ts +979 -0
  455. package/src/react/router/providers/SSRManifestProvider.ts +334 -0
  456. package/src/react/router/services/ReactPageServerService.ts +48 -0
  457. package/src/react/router/services/ReactPageService.ts +27 -0
  458. package/src/react/router/services/ReactRouter.ts +262 -0
  459. package/src/react/websocket/hooks/useRoom.tsx +242 -0
  460. package/src/react/websocket/index.ts +7 -0
  461. package/src/redis/__tests__/redis.spec.ts +13 -0
  462. package/src/redis/index.ts +9 -25
  463. package/src/redis/providers/BunRedisProvider.ts +9 -0
  464. package/src/redis/providers/NodeRedisProvider.ts +8 -0
  465. package/src/redis/providers/RedisProvider.ts +16 -0
  466. package/src/retry/index.ts +11 -2
  467. package/src/router/index.ts +15 -0
  468. package/src/scheduler/index.ts +11 -2
  469. package/src/security/__tests__/BasicAuth.spec.ts +2 -0
  470. package/src/security/__tests__/ServerSecurityProvider.spec.ts +13 -5
  471. package/src/security/index.ts +15 -10
  472. package/src/security/interfaces/IssuerResolver.ts +27 -0
  473. package/src/security/primitives/$issuer.ts +55 -0
  474. package/src/security/providers/SecurityProvider.ts +179 -0
  475. package/src/security/providers/ServerBasicAuthProvider.ts +6 -2
  476. package/src/security/providers/ServerSecurityProvider.ts +36 -22
  477. package/src/server/auth/index.ts +12 -7
  478. package/src/server/cache/index.ts +7 -22
  479. package/src/server/compress/index.ts +10 -2
  480. package/src/server/cookies/index.ts +7 -5
  481. package/src/server/cookies/primitives/$cookie.ts +33 -11
  482. package/src/server/core/index.ts +17 -7
  483. package/src/server/core/interfaces/ServerRequest.ts +83 -1
  484. package/src/server/core/primitives/$action.spec.ts +1 -1
  485. package/src/server/core/primitives/$action.ts +8 -3
  486. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  487. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  488. package/src/server/core/providers/NodeHttpServerProvider.ts +77 -22
  489. package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
  490. package/src/server/core/providers/ServerProvider.ts +9 -12
  491. package/src/server/core/services/ServerRequestParser.spec.ts +520 -0
  492. package/src/server/core/services/ServerRequestParser.ts +306 -13
  493. package/src/server/cors/index.ts +7 -21
  494. package/src/server/cors/primitives/$cors.ts +6 -2
  495. package/src/server/health/index.ts +8 -2
  496. package/src/server/helmet/index.ts +11 -3
  497. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  498. package/src/server/links/index.browser.ts +2 -0
  499. package/src/server/links/index.ts +13 -6
  500. package/src/server/metrics/index.ts +10 -3
  501. package/src/server/multipart/index.ts +9 -3
  502. package/src/server/proxy/index.ts +8 -2
  503. package/src/server/rate-limit/index.ts +21 -25
  504. package/src/server/rate-limit/primitives/$rateLimit.ts +6 -2
  505. package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +38 -14
  506. package/src/server/rate-limit/providers/ServerRateLimitProvider.ts +22 -56
  507. package/src/server/static/index.ts +8 -2
  508. package/src/server/static/providers/ServerStaticProvider.ts +1 -1
  509. package/src/server/swagger/index.ts +9 -4
  510. package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
  511. package/src/sms/index.ts +9 -5
  512. package/src/sms/providers/LocalSmsProvider.spec.ts +1 -1
  513. package/src/sms/providers/LocalSmsProvider.ts +1 -1
  514. package/src/system/index.browser.ts +11 -0
  515. package/src/system/index.ts +62 -0
  516. package/src/{file → system}/providers/FileSystemProvider.ts +16 -0
  517. package/src/{file → system}/providers/MemoryFileSystemProvider.ts +116 -3
  518. package/src/system/providers/MemoryShellProvider.ts +164 -0
  519. package/src/{file → system}/providers/NodeFileSystemProvider.spec.ts +2 -2
  520. package/src/{file → system}/providers/NodeFileSystemProvider.ts +36 -0
  521. package/src/system/providers/NodeShellProvider.ts +184 -0
  522. package/src/system/providers/ShellProvider.ts +74 -0
  523. package/src/{file → system}/services/FileDetector.spec.ts +2 -2
  524. package/src/thread/index.ts +11 -2
  525. package/src/topic/core/index.ts +12 -5
  526. package/src/vite/index.ts +3 -2
  527. package/src/vite/tasks/buildClient.ts +2 -8
  528. package/src/vite/tasks/buildServer.ts +84 -21
  529. package/src/vite/tasks/copyAssets.ts +5 -4
  530. package/src/vite/tasks/generateSitemap.ts +64 -23
  531. package/src/vite/tasks/index.ts +0 -2
  532. package/src/vite/tasks/prerenderPages.ts +49 -24
  533. package/src/websocket/index.ts +12 -8
  534. package/dist/file/index.d.ts +0 -839
  535. package/dist/file/index.d.ts.map +0 -1
  536. package/dist/file/index.js.map +0 -1
  537. package/src/cli/assets/indexHtml.ts +0 -15
  538. package/src/cli/assets/mainServerTs.ts +0 -24
  539. package/src/cli/assets/webAppRouterTs.ts +0 -15
  540. package/src/cli/assets/webHelloComponentTsx.ts +0 -16
  541. package/src/cli/commands/format.ts +0 -23
  542. package/src/file/index.ts +0 -43
  543. package/src/vite/helpers/boot.ts +0 -117
  544. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  545. package/src/vite/tasks/devServer.ts +0 -71
  546. package/src/vite/tasks/runAlepha.ts +0 -270
  547. /package/dist/orm/{chunk-DtkW-qnP.js → chunk-DH6iiROE.js} +0 -0
  548. /package/src/cli/{assets → templates}/apiIndexTs.ts +0 -0
  549. /package/src/cli/{assets → templates}/webIndexTs.ts +0 -0
  550. /package/src/{file → system}/errors/FileError.ts +0 -0
  551. /package/src/{file → system}/services/FileDetector.ts +0 -0
@@ -1,22 +1,28 @@
1
- import { $atom, $context, $hook, $inject, $module, Alepha, AlephaError, t } from "alepha";
1
+ import { $atom, $context, $inject, $module, Alepha, AlephaError, Json, isFileLike, t } from "alepha";
2
2
  import { $notification, AlephaApiNotifications } from "alepha/api/notifications";
3
3
  import { AlephaApiVerification } from "alepha/api/verifications";
4
4
  import { AlephaEmail } from "alepha/email";
5
5
  import { AlephaServerCompress } from "alepha/server/compress";
6
6
  import { AlephaServerHelmet } from "alepha/server/helmet";
7
- import { $action, BadRequestError, ConflictError, HttpError, UnauthorizedError, okSchema } from "alepha/server";
8
- import { $entity, $repository, db, pageQuerySchema, parseQueryString } from "alepha/orm";
7
+ import { $action, BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, UnauthorizedError, okSchema } from "alepha/server";
8
+ import { $entity, $repository, db, pageQuerySchema, parseQueryString, sql } from "alepha/orm";
9
9
  import { AlephaApiAudits, AuditService } from "alepha/api/audits";
10
10
  import { $logger } from "alepha/logger";
11
11
  import { $bucket } from "alepha/bucket";
12
12
  import { $client } from "alepha/server/links";
13
13
  import { $authCredentials, $authGithub, $authGoogle, ServerAuthProvider, authenticationProviderSchema } from "alepha/server/auth";
14
- import { randomInt, randomUUID } from "node:crypto";
14
+ import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
15
15
  import { $cache } from "alepha/cache";
16
16
  import { DateTimeProvider } from "alepha/datetime";
17
17
  import { $issuer, CryptoProvider, InvalidCredentialsError, SecurityProvider } from "alepha/security";
18
- import { FileSystemProvider } from "alepha/file";
18
+ import { join } from "node:path";
19
+ import { createReadStream } from "node:fs";
20
+ import { access, copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
21
+ import { PassThrough, Readable } from "node:stream";
22
+ import { fileURLToPath } from "node:url";
23
+ import { exec, spawn } from "node:child_process";
19
24
  import { AlephaApiFiles } from "alepha/api/files";
25
+ import { AlephaApiJobs } from "alepha/api/jobs";
20
26
 
21
27
  //#region ../../src/api/users/schemas/identityQuerySchema.ts
22
28
  const identityQuerySchema = t.extend(pageQuerySchema, {
@@ -184,16 +190,6 @@ var RealmProvider = class {
184
190
  "image/webp"
185
191
  ]
186
192
  });
187
- onConfigure = $hook({
188
- on: "configure",
189
- handler: () => {
190
- this.alepha.store.set("alepha.server.security.system.user", {
191
- id: "00000000-0000-0000-0000-000000000000",
192
- name: "system",
193
- roles: ["admin"]
194
- });
195
- }
196
- });
197
193
  register(realmName, realmOptions = {}) {
198
194
  this.realms.set(realmName, {
199
195
  name: realmName,
@@ -1901,234 +1897,2061 @@ var UserController = class {
1901
1897
  };
1902
1898
 
1903
1899
  //#endregion
1904
- //#region ../../src/api/users/services/SessionService.ts
1905
- var SessionService = class {
1906
- alepha = $inject(Alepha);
1907
- fsp = $inject(FileSystemProvider);
1908
- dateTimeProvider = $inject(DateTimeProvider);
1909
- cryptoProvider = $inject(CryptoProvider);
1910
- log = $logger();
1911
- realmProvider = $inject(RealmProvider);
1912
- fileController = $client();
1913
- auditService = $inject(AuditService);
1914
- users(userRealmName) {
1915
- return this.realmProvider.userRepository(userRealmName);
1900
+ //#region ../../src/system/providers/FileSystemProvider.ts
1901
+ /**
1902
+ * FileSystem interface providing utilities for working with files.
1903
+ */
1904
+ var FileSystemProvider = class {};
1905
+
1906
+ //#endregion
1907
+ //#region ../../src/system/providers/MemoryFileSystemProvider.ts
1908
+ /**
1909
+ * In-memory implementation of FileSystemProvider for testing.
1910
+ *
1911
+ * This provider stores all files and directories in memory, making it ideal for
1912
+ * unit tests that need to verify file operations without touching the real file system.
1913
+ *
1914
+ * @example
1915
+ * ```typescript
1916
+ * // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider
1917
+ * const alepha = Alepha.create().with({
1918
+ * provide: FileSystemProvider,
1919
+ * use: MemoryFileSystemProvider,
1920
+ * });
1921
+ *
1922
+ * // Run code that uses FileSystemProvider
1923
+ * const service = alepha.inject(MyService);
1924
+ * await service.saveFile("test.txt", "Hello World");
1925
+ *
1926
+ * // Verify the file was written
1927
+ * const memoryFs = alepha.inject(MemoryFileSystemProvider);
1928
+ * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World");
1929
+ * ```
1930
+ */
1931
+ var MemoryFileSystemProvider = class {
1932
+ json = $inject(Json);
1933
+ /**
1934
+ * In-memory storage for files (path -> content)
1935
+ */
1936
+ files = /* @__PURE__ */ new Map();
1937
+ /**
1938
+ * In-memory storage for directories
1939
+ */
1940
+ directories = /* @__PURE__ */ new Set();
1941
+ /**
1942
+ * Track mkdir calls for test assertions
1943
+ */
1944
+ mkdirCalls = [];
1945
+ /**
1946
+ * Track writeFile calls for test assertions
1947
+ */
1948
+ writeFileCalls = [];
1949
+ /**
1950
+ * Track readFile calls for test assertions
1951
+ */
1952
+ readFileCalls = [];
1953
+ /**
1954
+ * Track rm calls for test assertions
1955
+ */
1956
+ rmCalls = [];
1957
+ /**
1958
+ * Track join calls for test assertions
1959
+ */
1960
+ joinCalls = [];
1961
+ /**
1962
+ * Error to throw on mkdir (for testing error handling)
1963
+ */
1964
+ mkdirError = null;
1965
+ /**
1966
+ * Error to throw on writeFile (for testing error handling)
1967
+ */
1968
+ writeFileError = null;
1969
+ /**
1970
+ * Error to throw on readFile (for testing error handling)
1971
+ */
1972
+ readFileError = null;
1973
+ constructor(options = {}) {
1974
+ this.mkdirError = options.mkdirError ?? null;
1975
+ this.writeFileError = options.writeFileError ?? null;
1976
+ this.readFileError = options.readFileError ?? null;
1916
1977
  }
1917
- sessions(userRealmName) {
1918
- return this.realmProvider.sessionRepository(userRealmName);
1978
+ /**
1979
+ * Join path segments using forward slashes.
1980
+ * Uses Node's path.join for proper normalization (handles .. and .)
1981
+ */
1982
+ join(...paths) {
1983
+ this.joinCalls.push(paths);
1984
+ return join(...paths);
1919
1985
  }
1920
- identities(userRealmName) {
1921
- return this.realmProvider.identityRepository(userRealmName);
1986
+ /**
1987
+ * Create a FileLike object from various sources.
1988
+ */
1989
+ createFile(options) {
1990
+ if ("path" in options) {
1991
+ const filePath = options.path;
1992
+ const buffer = this.files.get(filePath);
1993
+ if (buffer === void 0) throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
1994
+ return {
1995
+ name: options.name ?? filePath.split("/").pop() ?? "file",
1996
+ type: options.type ?? "application/octet-stream",
1997
+ size: buffer.byteLength,
1998
+ lastModified: Date.now(),
1999
+ stream: () => {
2000
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
2001
+ },
2002
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
2003
+ text: async () => buffer.toString("utf-8")
2004
+ };
2005
+ }
2006
+ if ("buffer" in options) {
2007
+ const buffer = options.buffer;
2008
+ return {
2009
+ name: options.name ?? "file",
2010
+ type: options.type ?? "application/octet-stream",
2011
+ size: buffer.byteLength,
2012
+ lastModified: Date.now(),
2013
+ stream: () => {
2014
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
2015
+ },
2016
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
2017
+ text: async () => buffer.toString("utf-8")
2018
+ };
2019
+ }
2020
+ if ("text" in options) {
2021
+ const buffer = Buffer.from(options.text, "utf-8");
2022
+ return {
2023
+ name: options.name ?? "file.txt",
2024
+ type: options.type ?? "text/plain",
2025
+ size: buffer.byteLength,
2026
+ lastModified: Date.now(),
2027
+ stream: () => {
2028
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
2029
+ },
2030
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
2031
+ text: async () => options.text
2032
+ };
2033
+ }
2034
+ throw new Error("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported.");
1922
2035
  }
1923
2036
  /**
1924
- * Random delay to prevent timing attacks (50-200ms)
1925
- * Uses cryptographically secure random number generation
2037
+ * Remove a file or directory from memory.
1926
2038
  */
1927
- randomDelay() {
1928
- return new Promise((resolve) => setTimeout(resolve, randomInt(50, 201)));
2039
+ async rm(path, options) {
2040
+ this.rmCalls.push({
2041
+ path,
2042
+ options
2043
+ });
2044
+ if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
2045
+ if (this.directories.has(path)) if (options?.recursive) {
2046
+ this.directories.delete(path);
2047
+ for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath);
2048
+ for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath);
2049
+ } else throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
2050
+ else this.files.delete(path);
1929
2051
  }
1930
2052
  /**
1931
- * Validate user credentials and return the user if valid.
2053
+ * Copy a file or directory in memory.
1932
2054
  */
1933
- async login(provider, username, password, userRealmName) {
1934
- const { settings, name } = this.realmProvider.getRealm(userRealmName);
1935
- const isEmail = username.includes("@");
1936
- const isPhone = /^[+\d][\d\s()-]+$/.test(username);
1937
- const isUsername = !isEmail && !isPhone;
1938
- const identities$1 = this.identities(userRealmName);
1939
- const users$1 = this.users(userRealmName);
1940
- await this.randomDelay();
1941
- try {
1942
- const where = users$1.createQueryWhere();
1943
- where.realm = name;
1944
- if (settings.usernameEnabled !== false && isUsername) {
1945
- if (settings.usernameRegExp) {
1946
- if (!new RegExp(settings.usernameRegExp).test(username)) {
1947
- this.log.warn("Username does not match required format", {
1948
- provider,
1949
- username,
1950
- realm: name
1951
- });
1952
- await this.auditService.recordAuth("login_failed", {
1953
- userRealm: name,
1954
- description: "Username does not match required format",
1955
- metadata: {
1956
- provider,
1957
- username
1958
- }
1959
- });
1960
- throw new InvalidCredentialsError();
1961
- }
1962
- }
1963
- where.username = username;
1964
- } else if (settings.emailEnabled !== false && isEmail) where.email = username;
1965
- else if (settings.phoneEnabled === true && isPhone) where.phoneNumber = username;
1966
- else {
1967
- this.log.warn("Invalid login identifier format", {
1968
- provider,
1969
- username,
1970
- realm: name
1971
- });
1972
- await this.auditService.recordAuth("login_failed", {
1973
- userRealm: name,
1974
- description: "Invalid login identifier format",
1975
- metadata: {
1976
- provider,
1977
- username
1978
- }
1979
- });
1980
- throw new InvalidCredentialsError();
1981
- }
1982
- const user = await users$1.findOne({ where }).catch(() => void 0);
1983
- if (!user) {
1984
- this.log.warn("User not found during login attempt", {
1985
- provider,
1986
- username,
1987
- realm: name
1988
- });
1989
- await this.auditService.recordAuth("login_failed", {
1990
- userRealm: name,
1991
- description: "User not found",
1992
- metadata: {
1993
- provider,
1994
- username
1995
- }
1996
- });
1997
- throw new InvalidCredentialsError();
1998
- }
1999
- const identity = await identities$1.findOne({ where: {
2000
- provider: { eq: provider },
2001
- userId: { eq: user.id }
2002
- } });
2003
- const storedPassword = identity.password;
2004
- if (!storedPassword) {
2005
- this.log.error("Identity has no password configured", {
2006
- provider,
2007
- username,
2008
- identityId: identity.id,
2009
- realm: name
2010
- });
2011
- throw new InvalidCredentialsError();
2055
+ async cp(src, dest, options) {
2056
+ if (this.directories.has(src)) {
2057
+ if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
2058
+ this.directories.add(dest);
2059
+ for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) {
2060
+ const newPath = filePath.replace(src, dest);
2061
+ this.files.set(newPath, Buffer.from(content));
2012
2062
  }
2013
- if (!await this.cryptoProvider.verifyPassword(password, storedPassword)) {
2014
- this.log.warn("Invalid password during login attempt", {
2015
- provider,
2016
- username,
2017
- realm: name
2018
- });
2019
- await this.auditService.recordAuth("login_failed", {
2020
- userRealm: name,
2021
- resourceId: user.id,
2022
- description: "Invalid password",
2023
- metadata: {
2024
- provider,
2025
- username
2026
- }
2027
- });
2028
- throw new InvalidCredentialsError();
2063
+ } else if (this.files.has(src)) {
2064
+ const content = this.files.get(src);
2065
+ this.files.set(dest, Buffer.from(content));
2066
+ } else throw new Error(`ENOENT: no such file or directory, cp '${src}'`);
2067
+ }
2068
+ /**
2069
+ * Move/rename a file or directory in memory.
2070
+ */
2071
+ async mv(src, dest) {
2072
+ if (this.directories.has(src)) {
2073
+ this.directories.delete(src);
2074
+ this.directories.add(dest);
2075
+ for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) {
2076
+ const newPath = filePath.replace(src, dest);
2077
+ this.files.delete(filePath);
2078
+ this.files.set(newPath, content);
2029
2079
  }
2030
- await this.auditService.recordAuth("login", {
2031
- userId: user.id,
2032
- userEmail: user.email ?? void 0,
2033
- userRealm: name,
2034
- resourceId: user.id,
2035
- description: `User logged in via ${provider}`,
2036
- metadata: {
2037
- provider,
2038
- username
2039
- }
2040
- });
2041
- return user;
2042
- } catch (error) {
2043
- if (error instanceof InvalidCredentialsError) throw error;
2044
- this.log.warn("Error during login attempt", error);
2045
- throw new InvalidCredentialsError();
2046
- }
2080
+ } else if (this.files.has(src)) {
2081
+ const content = this.files.get(src);
2082
+ this.files.delete(src);
2083
+ this.files.set(dest, content);
2084
+ } else throw new Error(`ENOENT: no such file or directory, mv '${src}'`);
2047
2085
  }
2048
- async createSession(user, expiresIn, userRealmName) {
2049
- this.log.trace("Creating session", {
2050
- userId: user.id,
2051
- expiresIn
2052
- });
2053
- const request = this.alepha.context.get("request");
2054
- const refreshToken = this.cryptoProvider.randomUUID();
2055
- const expiresAt = this.dateTimeProvider.now().add(expiresIn, "seconds").toISOString();
2056
- const session = await this.sessions(userRealmName).create({
2057
- userId: user.id,
2058
- expiresAt,
2059
- ip: request?.ip,
2060
- userAgent: request?.userAgent,
2061
- refreshToken
2062
- });
2063
- this.log.info("Session created", {
2064
- sessionId: session.id,
2065
- userId: user.id,
2066
- ip: request?.ip
2086
+ /**
2087
+ * Create a directory in memory.
2088
+ */
2089
+ async mkdir(path, options) {
2090
+ this.mkdirCalls.push({
2091
+ path,
2092
+ options
2067
2093
  });
2068
- return {
2069
- refreshToken,
2070
- sessionId: session.id
2071
- };
2072
- }
2073
- async refreshSession(refreshToken, userRealmName) {
2074
- this.log.trace("Refreshing session");
2075
- const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } });
2076
- const now = this.dateTimeProvider.now();
2077
- const expiresAt = this.dateTimeProvider.of(session.expiresAt);
2078
- if (this.dateTimeProvider.of(session.expiresAt) < now) {
2079
- this.log.debug("Session expired during refresh", {
2080
- sessionId: session.id,
2081
- userId: session.userId
2082
- });
2083
- await this.sessions(userRealmName).deleteById(refreshToken);
2084
- throw new UnauthorizedError("Session expired");
2094
+ if (this.mkdirError) throw this.mkdirError;
2095
+ if (this.directories.has(path) && !options?.recursive) throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
2096
+ this.directories.add(path);
2097
+ if (options?.recursive) {
2098
+ const parts = path.split("/").filter(Boolean);
2099
+ let current = "";
2100
+ for (const part of parts) {
2101
+ current = current ? `${current}/${part}` : part;
2102
+ this.directories.add(current);
2103
+ }
2085
2104
  }
2086
- const user = await this.users(userRealmName).findOne({ where: { id: { eq: session.userId } } });
2087
- this.log.debug("Session refreshed", {
2088
- sessionId: session.id,
2089
- userId: session.userId
2090
- });
2091
- const { name } = this.realmProvider.getRealm(userRealmName);
2092
- await this.auditService.recordAuth("token_refresh", {
2093
- userId: user.id,
2094
- userEmail: user.email ?? void 0,
2095
- userRealm: name,
2096
- sessionId: session.id,
2097
- description: "Session token refreshed"
2098
- });
2099
- return {
2100
- user,
2101
- expiresIn: expiresAt.unix() - now.unix(),
2102
- sessionId: session.id
2103
- };
2104
2105
  }
2105
- async deleteSession(refreshToken, userRealmName) {
2106
- this.log.trace("Deleting session");
2107
- const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } }).catch(() => void 0);
2108
- await this.sessions(userRealmName).deleteOne({ refreshToken });
2109
- this.log.debug("Session deleted");
2110
- if (session) {
2111
- const { name } = this.realmProvider.getRealm(userRealmName);
2112
- await this.auditService.recordAuth("logout", {
2113
- userId: session.userId,
2114
- userRealm: name,
2115
- sessionId: session.id,
2116
- description: "User logged out"
2117
- });
2106
+ /**
2107
+ * List files in a directory.
2108
+ */
2109
+ async ls(path, options) {
2110
+ const normalizedPath = path.replace(/\/$/, "");
2111
+ const entries = /* @__PURE__ */ new Set();
2112
+ for (const filePath of this.files.keys()) if (filePath.startsWith(`${normalizedPath}/`)) {
2113
+ const relativePath = filePath.slice(normalizedPath.length + 1);
2114
+ const parts = relativePath.split("/");
2115
+ if (options?.recursive) entries.add(relativePath);
2116
+ else entries.add(parts[0]);
2118
2117
  }
2118
+ for (const dirPath of this.directories) if (dirPath.startsWith(`${normalizedPath}/`) && dirPath !== normalizedPath) {
2119
+ const relativePath = dirPath.slice(normalizedPath.length + 1);
2120
+ const parts = relativePath.split("/");
2121
+ if (options?.recursive) entries.add(relativePath);
2122
+ else if (parts.length === 1) entries.add(parts[0]);
2123
+ }
2124
+ let result = Array.from(entries);
2125
+ if (!options?.hidden) result = result.filter((entry) => !entry.startsWith("."));
2126
+ return result.sort();
2119
2127
  }
2120
- async link(provider, profile, userRealmName) {
2121
- this.log.trace("Linking OAuth2 profile", {
2122
- provider,
2123
- profileSub: profile.sub,
2124
- email: profile.email
2125
- });
2126
- const realm = this.realmProvider.getRealm(userRealmName);
2127
- const identities$1 = this.identities(userRealmName);
2128
- const users$1 = this.users(userRealmName);
2129
- const identity = await identities$1.findOne({ where: {
2130
- provider,
2131
- providerUserId: profile.sub
2128
+ /**
2129
+ * Check if a file or directory exists in memory.
2130
+ */
2131
+ async exists(path) {
2132
+ return this.files.has(path) || this.directories.has(path);
2133
+ }
2134
+ /**
2135
+ * Read a file from memory.
2136
+ */
2137
+ async readFile(path) {
2138
+ this.readFileCalls.push(path);
2139
+ if (this.readFileError) throw this.readFileError;
2140
+ const content = this.files.get(path);
2141
+ if (!content) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
2142
+ return content;
2143
+ }
2144
+ /**
2145
+ * Read a file from memory as text.
2146
+ */
2147
+ async readTextFile(path) {
2148
+ return (await this.readFile(path)).toString("utf-8");
2149
+ }
2150
+ /**
2151
+ * Read a file from memory as JSON.
2152
+ */
2153
+ async readJsonFile(path) {
2154
+ const text = await this.readTextFile(path);
2155
+ return this.json.parse(text);
2156
+ }
2157
+ /**
2158
+ * Write a file to memory.
2159
+ */
2160
+ async writeFile(path, data) {
2161
+ const dataStr = typeof data === "string" ? data : data instanceof Buffer || data instanceof Uint8Array ? data.toString("utf-8") : await data.text();
2162
+ this.writeFileCalls.push({
2163
+ path,
2164
+ data: dataStr
2165
+ });
2166
+ if (this.writeFileError) throw this.writeFileError;
2167
+ const buffer = typeof data === "string" ? Buffer.from(data, "utf-8") : data instanceof Buffer ? data : data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(await data.text(), "utf-8");
2168
+ this.files.set(path, buffer);
2169
+ }
2170
+ /**
2171
+ * Reset all in-memory state (useful between tests).
2172
+ */
2173
+ reset() {
2174
+ this.files.clear();
2175
+ this.directories.clear();
2176
+ this.mkdirCalls = [];
2177
+ this.writeFileCalls = [];
2178
+ this.readFileCalls = [];
2179
+ this.rmCalls = [];
2180
+ this.joinCalls = [];
2181
+ this.mkdirError = null;
2182
+ this.writeFileError = null;
2183
+ this.readFileError = null;
2184
+ }
2185
+ /**
2186
+ * Check if a file was written during the test.
2187
+ *
2188
+ * @example
2189
+ * ```typescript
2190
+ * expect(fs.wasWritten("/project/tsconfig.json")).toBe(true);
2191
+ * ```
2192
+ */
2193
+ wasWritten(path) {
2194
+ return this.writeFileCalls.some((call) => call.path === path);
2195
+ }
2196
+ /**
2197
+ * Check if a file was written with content matching a pattern.
2198
+ *
2199
+ * @example
2200
+ * ```typescript
2201
+ * expect(fs.wasWrittenMatching("/project/tsconfig.json", /extends/)).toBe(true);
2202
+ * ```
2203
+ */
2204
+ wasWrittenMatching(path, pattern) {
2205
+ const call = this.writeFileCalls.find((c) => c.path === path);
2206
+ return call ? pattern.test(call.data) : false;
2207
+ }
2208
+ /**
2209
+ * Check if a file was read during the test.
2210
+ *
2211
+ * @example
2212
+ * ```typescript
2213
+ * expect(fs.wasRead("/project/package.json")).toBe(true);
2214
+ * ```
2215
+ */
2216
+ wasRead(path) {
2217
+ return this.readFileCalls.includes(path);
2218
+ }
2219
+ /**
2220
+ * Check if a file was deleted during the test.
2221
+ *
2222
+ * @example
2223
+ * ```typescript
2224
+ * expect(fs.wasDeleted("/project/old-file.txt")).toBe(true);
2225
+ * ```
2226
+ */
2227
+ wasDeleted(path) {
2228
+ return this.rmCalls.some((call) => call.path === path);
2229
+ }
2230
+ /**
2231
+ * Get the content of a file as a string (convenience method for testing).
2232
+ */
2233
+ getFileContent(path) {
2234
+ return this.files.get(path)?.toString("utf-8");
2235
+ }
2236
+ };
2237
+
2238
+ //#endregion
2239
+ //#region ../../src/system/providers/MemoryShellProvider.ts
2240
+ /**
2241
+ * In-memory implementation of ShellProvider for testing.
2242
+ *
2243
+ * Records all commands that would be executed without actually running them.
2244
+ * Can be configured to return specific outputs or throw errors for testing.
2245
+ *
2246
+ * @example
2247
+ * ```typescript
2248
+ * // In tests, substitute the real ShellProvider with MemoryShellProvider
2249
+ * const alepha = Alepha.create().with({
2250
+ * provide: ShellProvider,
2251
+ * use: MemoryShellProvider,
2252
+ * });
2253
+ *
2254
+ * // Configure mock behavior
2255
+ * const shell = alepha.inject(MemoryShellProvider);
2256
+ * shell.configure({
2257
+ * outputs: { "echo hello": "hello\n" },
2258
+ * errors: { "failing-cmd": "Command failed" },
2259
+ * });
2260
+ *
2261
+ * // Or use the fluent API
2262
+ * shell.outputs.set("another-cmd", "output");
2263
+ * shell.errors.set("another-error", "Error message");
2264
+ *
2265
+ * // Run code that uses ShellProvider
2266
+ * const service = alepha.inject(MyService);
2267
+ * await service.doSomething();
2268
+ *
2269
+ * // Verify commands were called
2270
+ * expect(shell.calls).toHaveLength(2);
2271
+ * expect(shell.calls[0].command).toBe("yarn install");
2272
+ * ```
2273
+ */
2274
+ var MemoryShellProvider = class {
2275
+ /**
2276
+ * All recorded shell calls.
2277
+ */
2278
+ calls = [];
2279
+ /**
2280
+ * Simulated outputs for specific commands.
2281
+ */
2282
+ outputs = /* @__PURE__ */ new Map();
2283
+ /**
2284
+ * Commands that should throw an error.
2285
+ */
2286
+ errors = /* @__PURE__ */ new Map();
2287
+ /**
2288
+ * Commands considered installed in the system PATH.
2289
+ */
2290
+ installedCommands = /* @__PURE__ */ new Set();
2291
+ /**
2292
+ * Configure the mock with predefined outputs, errors, and installed commands.
2293
+ */
2294
+ configure(options) {
2295
+ if (options.outputs) for (const [cmd, output] of Object.entries(options.outputs)) this.outputs.set(cmd, output);
2296
+ if (options.errors) for (const [cmd, error] of Object.entries(options.errors)) this.errors.set(cmd, error);
2297
+ if (options.installedCommands) for (const cmd of options.installedCommands) this.installedCommands.add(cmd);
2298
+ return this;
2299
+ }
2300
+ /**
2301
+ * Record command and return simulated output.
2302
+ */
2303
+ async run(command, options = {}) {
2304
+ this.calls.push({
2305
+ command,
2306
+ options
2307
+ });
2308
+ const errorMsg = this.errors.get(command);
2309
+ if (errorMsg) throw new Error(errorMsg);
2310
+ return this.outputs.get(command) ?? "";
2311
+ }
2312
+ /**
2313
+ * Check if a specific command was called.
2314
+ */
2315
+ wasCalled(command) {
2316
+ return this.calls.some((call) => call.command === command);
2317
+ }
2318
+ /**
2319
+ * Check if a command matching a pattern was called.
2320
+ */
2321
+ wasCalledMatching(pattern) {
2322
+ return this.calls.some((call) => pattern.test(call.command));
2323
+ }
2324
+ /**
2325
+ * Get all calls matching a pattern.
2326
+ */
2327
+ getCallsMatching(pattern) {
2328
+ return this.calls.filter((call) => pattern.test(call.command));
2329
+ }
2330
+ /**
2331
+ * Check if a command is installed.
2332
+ */
2333
+ async isInstalled(command) {
2334
+ return this.installedCommands.has(command);
2335
+ }
2336
+ /**
2337
+ * Reset all recorded state.
2338
+ */
2339
+ reset() {
2340
+ this.calls = [];
2341
+ this.outputs.clear();
2342
+ this.errors.clear();
2343
+ this.installedCommands.clear();
2344
+ }
2345
+ };
2346
+
2347
+ //#endregion
2348
+ //#region ../../src/system/services/FileDetector.ts
2349
+ /**
2350
+ * Service for detecting file types and getting content types.
2351
+ *
2352
+ * @example
2353
+ * ```typescript
2354
+ * const detector = alepha.inject(FileDetector);
2355
+ *
2356
+ * // Get content type from filename
2357
+ * const mimeType = detector.getContentType("image.png"); // "image/png"
2358
+ *
2359
+ * // Detect file type by magic bytes
2360
+ * const stream = createReadStream('image.png');
2361
+ * const result = await detector.detectFileType(stream, 'image.png');
2362
+ * console.log(result.mimeType); // 'image/png'
2363
+ * console.log(result.verified); // true if magic bytes match
2364
+ * ```
2365
+ */
2366
+ var FileDetector = class FileDetector {
2367
+ /**
2368
+ * Magic byte signatures for common file formats.
2369
+ * Each signature is represented as an array of bytes or null (wildcard).
2370
+ */
2371
+ static MAGIC_BYTES = {
2372
+ png: [{
2373
+ signature: [
2374
+ 137,
2375
+ 80,
2376
+ 78,
2377
+ 71,
2378
+ 13,
2379
+ 10,
2380
+ 26,
2381
+ 10
2382
+ ],
2383
+ mimeType: "image/png"
2384
+ }],
2385
+ jpg: [
2386
+ {
2387
+ signature: [
2388
+ 255,
2389
+ 216,
2390
+ 255,
2391
+ 224
2392
+ ],
2393
+ mimeType: "image/jpeg"
2394
+ },
2395
+ {
2396
+ signature: [
2397
+ 255,
2398
+ 216,
2399
+ 255,
2400
+ 225
2401
+ ],
2402
+ mimeType: "image/jpeg"
2403
+ },
2404
+ {
2405
+ signature: [
2406
+ 255,
2407
+ 216,
2408
+ 255,
2409
+ 226
2410
+ ],
2411
+ mimeType: "image/jpeg"
2412
+ },
2413
+ {
2414
+ signature: [
2415
+ 255,
2416
+ 216,
2417
+ 255,
2418
+ 227
2419
+ ],
2420
+ mimeType: "image/jpeg"
2421
+ },
2422
+ {
2423
+ signature: [
2424
+ 255,
2425
+ 216,
2426
+ 255,
2427
+ 232
2428
+ ],
2429
+ mimeType: "image/jpeg"
2430
+ }
2431
+ ],
2432
+ jpeg: [
2433
+ {
2434
+ signature: [
2435
+ 255,
2436
+ 216,
2437
+ 255,
2438
+ 224
2439
+ ],
2440
+ mimeType: "image/jpeg"
2441
+ },
2442
+ {
2443
+ signature: [
2444
+ 255,
2445
+ 216,
2446
+ 255,
2447
+ 225
2448
+ ],
2449
+ mimeType: "image/jpeg"
2450
+ },
2451
+ {
2452
+ signature: [
2453
+ 255,
2454
+ 216,
2455
+ 255,
2456
+ 226
2457
+ ],
2458
+ mimeType: "image/jpeg"
2459
+ },
2460
+ {
2461
+ signature: [
2462
+ 255,
2463
+ 216,
2464
+ 255,
2465
+ 227
2466
+ ],
2467
+ mimeType: "image/jpeg"
2468
+ },
2469
+ {
2470
+ signature: [
2471
+ 255,
2472
+ 216,
2473
+ 255,
2474
+ 232
2475
+ ],
2476
+ mimeType: "image/jpeg"
2477
+ }
2478
+ ],
2479
+ gif: [{
2480
+ signature: [
2481
+ 71,
2482
+ 73,
2483
+ 70,
2484
+ 56,
2485
+ 55,
2486
+ 97
2487
+ ],
2488
+ mimeType: "image/gif"
2489
+ }, {
2490
+ signature: [
2491
+ 71,
2492
+ 73,
2493
+ 70,
2494
+ 56,
2495
+ 57,
2496
+ 97
2497
+ ],
2498
+ mimeType: "image/gif"
2499
+ }],
2500
+ webp: [{
2501
+ signature: [
2502
+ 82,
2503
+ 73,
2504
+ 70,
2505
+ 70,
2506
+ null,
2507
+ null,
2508
+ null,
2509
+ null,
2510
+ 87,
2511
+ 69,
2512
+ 66,
2513
+ 80
2514
+ ],
2515
+ mimeType: "image/webp"
2516
+ }],
2517
+ bmp: [{
2518
+ signature: [66, 77],
2519
+ mimeType: "image/bmp"
2520
+ }],
2521
+ ico: [{
2522
+ signature: [
2523
+ 0,
2524
+ 0,
2525
+ 1,
2526
+ 0
2527
+ ],
2528
+ mimeType: "image/x-icon"
2529
+ }],
2530
+ tiff: [{
2531
+ signature: [
2532
+ 73,
2533
+ 73,
2534
+ 42,
2535
+ 0
2536
+ ],
2537
+ mimeType: "image/tiff"
2538
+ }, {
2539
+ signature: [
2540
+ 77,
2541
+ 77,
2542
+ 0,
2543
+ 42
2544
+ ],
2545
+ mimeType: "image/tiff"
2546
+ }],
2547
+ tif: [{
2548
+ signature: [
2549
+ 73,
2550
+ 73,
2551
+ 42,
2552
+ 0
2553
+ ],
2554
+ mimeType: "image/tiff"
2555
+ }, {
2556
+ signature: [
2557
+ 77,
2558
+ 77,
2559
+ 0,
2560
+ 42
2561
+ ],
2562
+ mimeType: "image/tiff"
2563
+ }],
2564
+ pdf: [{
2565
+ signature: [
2566
+ 37,
2567
+ 80,
2568
+ 68,
2569
+ 70,
2570
+ 45
2571
+ ],
2572
+ mimeType: "application/pdf"
2573
+ }],
2574
+ zip: [
2575
+ {
2576
+ signature: [
2577
+ 80,
2578
+ 75,
2579
+ 3,
2580
+ 4
2581
+ ],
2582
+ mimeType: "application/zip"
2583
+ },
2584
+ {
2585
+ signature: [
2586
+ 80,
2587
+ 75,
2588
+ 5,
2589
+ 6
2590
+ ],
2591
+ mimeType: "application/zip"
2592
+ },
2593
+ {
2594
+ signature: [
2595
+ 80,
2596
+ 75,
2597
+ 7,
2598
+ 8
2599
+ ],
2600
+ mimeType: "application/zip"
2601
+ }
2602
+ ],
2603
+ rar: [{
2604
+ signature: [
2605
+ 82,
2606
+ 97,
2607
+ 114,
2608
+ 33,
2609
+ 26,
2610
+ 7
2611
+ ],
2612
+ mimeType: "application/vnd.rar"
2613
+ }],
2614
+ "7z": [{
2615
+ signature: [
2616
+ 55,
2617
+ 122,
2618
+ 188,
2619
+ 175,
2620
+ 39,
2621
+ 28
2622
+ ],
2623
+ mimeType: "application/x-7z-compressed"
2624
+ }],
2625
+ tar: [{
2626
+ signature: [
2627
+ 117,
2628
+ 115,
2629
+ 116,
2630
+ 97,
2631
+ 114
2632
+ ],
2633
+ mimeType: "application/x-tar"
2634
+ }],
2635
+ gz: [{
2636
+ signature: [31, 139],
2637
+ mimeType: "application/gzip"
2638
+ }],
2639
+ tgz: [{
2640
+ signature: [31, 139],
2641
+ mimeType: "application/gzip"
2642
+ }],
2643
+ mp3: [
2644
+ {
2645
+ signature: [255, 251],
2646
+ mimeType: "audio/mpeg"
2647
+ },
2648
+ {
2649
+ signature: [255, 243],
2650
+ mimeType: "audio/mpeg"
2651
+ },
2652
+ {
2653
+ signature: [255, 242],
2654
+ mimeType: "audio/mpeg"
2655
+ },
2656
+ {
2657
+ signature: [
2658
+ 73,
2659
+ 68,
2660
+ 51
2661
+ ],
2662
+ mimeType: "audio/mpeg"
2663
+ }
2664
+ ],
2665
+ wav: [{
2666
+ signature: [
2667
+ 82,
2668
+ 73,
2669
+ 70,
2670
+ 70,
2671
+ null,
2672
+ null,
2673
+ null,
2674
+ null,
2675
+ 87,
2676
+ 65,
2677
+ 86,
2678
+ 69
2679
+ ],
2680
+ mimeType: "audio/wav"
2681
+ }],
2682
+ ogg: [{
2683
+ signature: [
2684
+ 79,
2685
+ 103,
2686
+ 103,
2687
+ 83
2688
+ ],
2689
+ mimeType: "audio/ogg"
2690
+ }],
2691
+ flac: [{
2692
+ signature: [
2693
+ 102,
2694
+ 76,
2695
+ 97,
2696
+ 67
2697
+ ],
2698
+ mimeType: "audio/flac"
2699
+ }],
2700
+ mp4: [
2701
+ {
2702
+ signature: [
2703
+ null,
2704
+ null,
2705
+ null,
2706
+ null,
2707
+ 102,
2708
+ 116,
2709
+ 121,
2710
+ 112
2711
+ ],
2712
+ mimeType: "video/mp4"
2713
+ },
2714
+ {
2715
+ signature: [
2716
+ null,
2717
+ null,
2718
+ null,
2719
+ null,
2720
+ 102,
2721
+ 116,
2722
+ 121,
2723
+ 112,
2724
+ 105,
2725
+ 115,
2726
+ 111,
2727
+ 109
2728
+ ],
2729
+ mimeType: "video/mp4"
2730
+ },
2731
+ {
2732
+ signature: [
2733
+ null,
2734
+ null,
2735
+ null,
2736
+ null,
2737
+ 102,
2738
+ 116,
2739
+ 121,
2740
+ 112,
2741
+ 109,
2742
+ 112,
2743
+ 52,
2744
+ 50
2745
+ ],
2746
+ mimeType: "video/mp4"
2747
+ }
2748
+ ],
2749
+ webm: [{
2750
+ signature: [
2751
+ 26,
2752
+ 69,
2753
+ 223,
2754
+ 163
2755
+ ],
2756
+ mimeType: "video/webm"
2757
+ }],
2758
+ avi: [{
2759
+ signature: [
2760
+ 82,
2761
+ 73,
2762
+ 70,
2763
+ 70,
2764
+ null,
2765
+ null,
2766
+ null,
2767
+ null,
2768
+ 65,
2769
+ 86,
2770
+ 73,
2771
+ 32
2772
+ ],
2773
+ mimeType: "video/x-msvideo"
2774
+ }],
2775
+ mov: [{
2776
+ signature: [
2777
+ null,
2778
+ null,
2779
+ null,
2780
+ null,
2781
+ 102,
2782
+ 116,
2783
+ 121,
2784
+ 112,
2785
+ 113,
2786
+ 116,
2787
+ 32,
2788
+ 32
2789
+ ],
2790
+ mimeType: "video/quicktime"
2791
+ }],
2792
+ mkv: [{
2793
+ signature: [
2794
+ 26,
2795
+ 69,
2796
+ 223,
2797
+ 163
2798
+ ],
2799
+ mimeType: "video/x-matroska"
2800
+ }],
2801
+ docx: [{
2802
+ signature: [
2803
+ 80,
2804
+ 75,
2805
+ 3,
2806
+ 4
2807
+ ],
2808
+ mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
2809
+ }],
2810
+ xlsx: [{
2811
+ signature: [
2812
+ 80,
2813
+ 75,
2814
+ 3,
2815
+ 4
2816
+ ],
2817
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
2818
+ }],
2819
+ pptx: [{
2820
+ signature: [
2821
+ 80,
2822
+ 75,
2823
+ 3,
2824
+ 4
2825
+ ],
2826
+ mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
2827
+ }],
2828
+ doc: [{
2829
+ signature: [
2830
+ 208,
2831
+ 207,
2832
+ 17,
2833
+ 224,
2834
+ 161,
2835
+ 177,
2836
+ 26,
2837
+ 225
2838
+ ],
2839
+ mimeType: "application/msword"
2840
+ }],
2841
+ xls: [{
2842
+ signature: [
2843
+ 208,
2844
+ 207,
2845
+ 17,
2846
+ 224,
2847
+ 161,
2848
+ 177,
2849
+ 26,
2850
+ 225
2851
+ ],
2852
+ mimeType: "application/vnd.ms-excel"
2853
+ }],
2854
+ ppt: [{
2855
+ signature: [
2856
+ 208,
2857
+ 207,
2858
+ 17,
2859
+ 224,
2860
+ 161,
2861
+ 177,
2862
+ 26,
2863
+ 225
2864
+ ],
2865
+ mimeType: "application/vnd.ms-powerpoint"
2866
+ }]
2867
+ };
2868
+ /**
2869
+ * All possible format signatures for checking against actual file content
2870
+ */
2871
+ static ALL_SIGNATURES = Object.entries(FileDetector.MAGIC_BYTES).flatMap(([ext, signatures]) => signatures.map((sig) => ({
2872
+ ext,
2873
+ ...sig
2874
+ })));
2875
+ /**
2876
+ * MIME type map for file extensions.
2877
+ *
2878
+ * Can be used to get the content type of file based on its extension.
2879
+ * Feel free to add more mime types in your project!
2880
+ */
2881
+ static mimeMap = {
2882
+ json: "application/json",
2883
+ txt: "text/plain",
2884
+ html: "text/html",
2885
+ htm: "text/html",
2886
+ xml: "application/xml",
2887
+ csv: "text/csv",
2888
+ pdf: "application/pdf",
2889
+ md: "text/markdown",
2890
+ markdown: "text/markdown",
2891
+ rtf: "application/rtf",
2892
+ css: "text/css",
2893
+ js: "application/javascript",
2894
+ mjs: "application/javascript",
2895
+ ts: "application/typescript",
2896
+ jsx: "text/jsx",
2897
+ tsx: "text/tsx",
2898
+ zip: "application/zip",
2899
+ rar: "application/vnd.rar",
2900
+ "7z": "application/x-7z-compressed",
2901
+ tar: "application/x-tar",
2902
+ gz: "application/gzip",
2903
+ tgz: "application/gzip",
2904
+ png: "image/png",
2905
+ jpg: "image/jpeg",
2906
+ jpeg: "image/jpeg",
2907
+ gif: "image/gif",
2908
+ webp: "image/webp",
2909
+ svg: "image/svg+xml",
2910
+ bmp: "image/bmp",
2911
+ ico: "image/x-icon",
2912
+ tiff: "image/tiff",
2913
+ tif: "image/tiff",
2914
+ mp3: "audio/mpeg",
2915
+ wav: "audio/wav",
2916
+ ogg: "audio/ogg",
2917
+ m4a: "audio/mp4",
2918
+ aac: "audio/aac",
2919
+ flac: "audio/flac",
2920
+ mp4: "video/mp4",
2921
+ webm: "video/webm",
2922
+ avi: "video/x-msvideo",
2923
+ mov: "video/quicktime",
2924
+ wmv: "video/x-ms-wmv",
2925
+ flv: "video/x-flv",
2926
+ mkv: "video/x-matroska",
2927
+ doc: "application/msword",
2928
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2929
+ xls: "application/vnd.ms-excel",
2930
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2931
+ ppt: "application/vnd.ms-powerpoint",
2932
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2933
+ woff: "font/woff",
2934
+ woff2: "font/woff2",
2935
+ ttf: "font/ttf",
2936
+ otf: "font/otf",
2937
+ eot: "application/vnd.ms-fontobject"
2938
+ };
2939
+ /**
2940
+ * Reverse MIME type map for looking up extensions from MIME types.
2941
+ * Prefers shorter, more common extensions when multiple exist.
2942
+ */
2943
+ static reverseMimeMap = (() => {
2944
+ const reverse = {};
2945
+ for (const [ext, mimeType] of Object.entries(FileDetector.mimeMap)) if (!reverse[mimeType]) reverse[mimeType] = ext;
2946
+ return reverse;
2947
+ })();
2948
+ /**
2949
+ * Returns the file extension for a given MIME type.
2950
+ *
2951
+ * @param mimeType - The MIME type to look up
2952
+ * @returns The file extension (without dot), or "bin" if not found
2953
+ *
2954
+ * @example
2955
+ * ```typescript
2956
+ * const detector = alepha.inject(FileDetector);
2957
+ * const ext = detector.getExtensionFromMimeType("image/png"); // "png"
2958
+ * const ext2 = detector.getExtensionFromMimeType("application/octet-stream"); // "bin"
2959
+ * ```
2960
+ */
2961
+ getExtensionFromMimeType(mimeType) {
2962
+ return FileDetector.reverseMimeMap[mimeType] || "bin";
2963
+ }
2964
+ /**
2965
+ * Returns the content type of file based on its filename.
2966
+ *
2967
+ * @param filename - The filename to check
2968
+ * @returns The MIME type
2969
+ *
2970
+ * @example
2971
+ * ```typescript
2972
+ * const detector = alepha.inject(FileDetector);
2973
+ * const mimeType = detector.getContentType("image.png"); // "image/png"
2974
+ * ```
2975
+ */
2976
+ getContentType(filename) {
2977
+ const ext = filename.toLowerCase().split(".").pop() || "";
2978
+ return FileDetector.mimeMap[ext] || "application/octet-stream";
2979
+ }
2980
+ /**
2981
+ * Detects the file type by checking magic bytes against the stream content.
2982
+ *
2983
+ * @param stream - The readable stream to check
2984
+ * @param filename - The filename (used to get the extension)
2985
+ * @returns File type information including MIME type, extension, and verification status
2986
+ *
2987
+ * @example
2988
+ * ```typescript
2989
+ * const detector = alepha.inject(FileDetector);
2990
+ * const stream = createReadStream('image.png');
2991
+ * const result = await detector.detectFileType(stream, 'image.png');
2992
+ * console.log(result.mimeType); // 'image/png'
2993
+ * console.log(result.verified); // true if magic bytes match
2994
+ * ```
2995
+ */
2996
+ async detectFileType(stream, filename) {
2997
+ const expectedMimeType = this.getContentType(filename);
2998
+ const lastDotIndex = filename.lastIndexOf(".");
2999
+ const ext = lastDotIndex > 0 ? filename.substring(lastDotIndex + 1).toLowerCase() : "";
3000
+ const { buffer, stream: newStream } = await this.peekBytes(stream, 16);
3001
+ const expectedSignatures = FileDetector.MAGIC_BYTES[ext];
3002
+ if (expectedSignatures) {
3003
+ for (const { signature, mimeType } of expectedSignatures) if (this.matchesSignature(buffer, signature)) return {
3004
+ mimeType,
3005
+ extension: ext,
3006
+ verified: true,
3007
+ stream: newStream
3008
+ };
3009
+ }
3010
+ for (const { ext: detectedExt, signature, mimeType } of FileDetector.ALL_SIGNATURES) if (detectedExt !== ext && this.matchesSignature(buffer, signature)) return {
3011
+ mimeType,
3012
+ extension: detectedExt,
3013
+ verified: true,
3014
+ stream: newStream
3015
+ };
3016
+ return {
3017
+ mimeType: expectedMimeType,
3018
+ extension: ext,
3019
+ verified: false,
3020
+ stream: newStream
3021
+ };
3022
+ }
3023
+ /**
3024
+ * Reads all bytes from a stream and returns the first N bytes along with a new stream containing all data.
3025
+ * This approach reads the entire stream upfront to avoid complex async handling issues.
3026
+ *
3027
+ * @protected
3028
+ */
3029
+ async peekBytes(stream, numBytes) {
3030
+ const chunks = [];
3031
+ for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3032
+ const allData = Buffer.concat(chunks);
3033
+ return {
3034
+ buffer: allData.subarray(0, numBytes),
3035
+ stream: Readable.from(allData)
3036
+ };
3037
+ }
3038
+ /**
3039
+ * Checks if a buffer matches a magic byte signature.
3040
+ *
3041
+ * @protected
3042
+ */
3043
+ matchesSignature(buffer, signature) {
3044
+ if (buffer.length < signature.length) return false;
3045
+ for (let i = 0; i < signature.length; i++) if (signature[i] !== null && buffer[i] !== signature[i]) return false;
3046
+ return true;
3047
+ }
3048
+ };
3049
+
3050
+ //#endregion
3051
+ //#region ../../src/system/providers/NodeFileSystemProvider.ts
3052
+ /**
3053
+ * Node.js implementation of FileSystem interface.
3054
+ *
3055
+ * @example
3056
+ * ```typescript
3057
+ * const fs = alepha.inject(NodeFileSystemProvider);
3058
+ *
3059
+ * // Create from URL
3060
+ * const file1 = fs.createFile({ url: "file:///path/to/file.png" });
3061
+ *
3062
+ * // Create from Buffer
3063
+ * const file2 = fs.createFile({ buffer: Buffer.from("hello"), name: "hello.txt" });
3064
+ *
3065
+ * // Create from text
3066
+ * const file3 = fs.createFile({ text: "Hello, world!", name: "greeting.txt" });
3067
+ *
3068
+ * // File operations
3069
+ * await fs.mkdir("/tmp/mydir", { recursive: true });
3070
+ * await fs.cp("/src/file.txt", "/dest/file.txt");
3071
+ * await fs.mv("/old/path.txt", "/new/path.txt");
3072
+ * const files = await fs.ls("/tmp");
3073
+ * await fs.rm("/tmp/file.txt");
3074
+ * ```
3075
+ */
3076
+ var NodeFileSystemProvider = class {
3077
+ detector = $inject(FileDetector);
3078
+ json = $inject(Json);
3079
+ join(...paths) {
3080
+ return join(...paths);
3081
+ }
3082
+ /**
3083
+ * Creates a FileLike object from various sources.
3084
+ *
3085
+ * @param options - Options for creating the file
3086
+ * @returns A FileLike object
3087
+ *
3088
+ * @example
3089
+ * ```typescript
3090
+ * const fs = alepha.inject(NodeFileSystemProvider);
3091
+ *
3092
+ * // From URL
3093
+ * const file1 = fs.createFile({ url: "https://example.com/image.png" });
3094
+ *
3095
+ * // From Buffer
3096
+ * const file2 = fs.createFile({
3097
+ * buffer: Buffer.from("hello"),
3098
+ * name: "hello.txt",
3099
+ * type: "text/plain"
3100
+ * });
3101
+ *
3102
+ * // From text
3103
+ * const file3 = fs.createFile({ text: "Hello!", name: "greeting.txt" });
3104
+ *
3105
+ * // From stream with detection
3106
+ * const stream = createReadStream("/path/to/file.png");
3107
+ * const file4 = fs.createFile({ stream, name: "image.png" });
3108
+ * ```
3109
+ */
3110
+ createFile(options) {
3111
+ if ("path" in options) {
3112
+ const path = options.path;
3113
+ const filename = path.split("/").pop() || "file";
3114
+ return this.createFileFromUrl(`file://${path}`, {
3115
+ type: options.type,
3116
+ name: options.name || filename
3117
+ });
3118
+ }
3119
+ if ("url" in options) return this.createFileFromUrl(options.url, {
3120
+ type: options.type,
3121
+ name: options.name
3122
+ });
3123
+ if ("response" in options) {
3124
+ if (!options.response.body) throw new AlephaError("Response has no body stream");
3125
+ const res = options.response;
3126
+ const sizeHeader = res.headers.get("content-length");
3127
+ const size = sizeHeader ? parseInt(sizeHeader, 10) : void 0;
3128
+ let name = options.name;
3129
+ const contentDisposition = res.headers.get("content-disposition");
3130
+ if (contentDisposition && !name) {
3131
+ const match = contentDisposition.match(/filename="?([^"]+)"?/);
3132
+ if (match) name = match[1];
3133
+ }
3134
+ const type = options.type || res.headers.get("content-type") || void 0;
3135
+ return this.createFileFromStream(options.response.body, {
3136
+ type,
3137
+ name,
3138
+ size
3139
+ });
3140
+ }
3141
+ if ("file" in options) return this.createFileFromWebFile(options.file, {
3142
+ type: options.type,
3143
+ name: options.name,
3144
+ size: options.size
3145
+ });
3146
+ if ("buffer" in options) return this.createFileFromBuffer(options.buffer, {
3147
+ type: options.type,
3148
+ name: options.name
3149
+ });
3150
+ if ("arrayBuffer" in options) return this.createFileFromBuffer(Buffer.from(options.arrayBuffer), {
3151
+ type: options.type,
3152
+ name: options.name
3153
+ });
3154
+ if ("text" in options) return this.createFileFromBuffer(Buffer.from(options.text, "utf-8"), {
3155
+ type: options.type || "text/plain",
3156
+ name: options.name || "file.txt"
3157
+ });
3158
+ if ("stream" in options) return this.createFileFromStream(options.stream, {
3159
+ type: options.type,
3160
+ name: options.name,
3161
+ size: options.size
3162
+ });
3163
+ throw new AlephaError("Invalid createFile options: no valid source provided");
3164
+ }
3165
+ /**
3166
+ * Removes a file or directory.
3167
+ *
3168
+ * @param path - The path to remove
3169
+ * @param options - Remove options
3170
+ *
3171
+ * @example
3172
+ * ```typescript
3173
+ * const fs = alepha.inject(NodeFileSystemProvider);
3174
+ *
3175
+ * // Remove a file
3176
+ * await fs.rm("/tmp/file.txt");
3177
+ *
3178
+ * // Remove a directory recursively
3179
+ * await fs.rm("/tmp/mydir", { recursive: true });
3180
+ *
3181
+ * // Remove with force (no error if doesn't exist)
3182
+ * await fs.rm("/tmp/maybe-exists.txt", { force: true });
3183
+ * ```
3184
+ */
3185
+ async rm(path, options) {
3186
+ await rm(path, options);
3187
+ }
3188
+ /**
3189
+ * Copies a file or directory.
3190
+ *
3191
+ * @param src - Source path
3192
+ * @param dest - Destination path
3193
+ * @param options - Copy options
3194
+ *
3195
+ * @example
3196
+ * ```typescript
3197
+ * const fs = alepha.inject(NodeFileSystemProvider);
3198
+ *
3199
+ * // Copy a file
3200
+ * await fs.cp("/src/file.txt", "/dest/file.txt");
3201
+ *
3202
+ * // Copy a directory recursively
3203
+ * await fs.cp("/src/dir", "/dest/dir", { recursive: true });
3204
+ *
3205
+ * // Copy with force (overwrite existing)
3206
+ * await fs.cp("/src/file.txt", "/dest/file.txt", { force: true });
3207
+ * ```
3208
+ */
3209
+ async cp(src, dest, options) {
3210
+ if ((await stat(src)).isDirectory()) {
3211
+ if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
3212
+ await cp(src, dest, {
3213
+ recursive: true,
3214
+ force: options?.force ?? false
3215
+ });
3216
+ } else await copyFile(src, dest);
3217
+ }
3218
+ /**
3219
+ * Moves/renames a file or directory.
3220
+ *
3221
+ * @param src - Source path
3222
+ * @param dest - Destination path
3223
+ *
3224
+ * @example
3225
+ * ```typescript
3226
+ * const fs = alepha.inject(NodeFileSystemProvider);
3227
+ *
3228
+ * // Move/rename a file
3229
+ * await fs.mv("/old/path.txt", "/new/path.txt");
3230
+ *
3231
+ * // Move a directory
3232
+ * await fs.mv("/old/dir", "/new/dir");
3233
+ * ```
3234
+ */
3235
+ async mv(src, dest) {
3236
+ await rename(src, dest);
3237
+ }
3238
+ /**
3239
+ * Creates a directory.
3240
+ *
3241
+ * @param path - The directory path to create
3242
+ * @param options - Mkdir options
3243
+ *
3244
+ * @example
3245
+ * ```typescript
3246
+ * const fs = alepha.inject(NodeFileSystemProvider);
3247
+ *
3248
+ * // Create a directory
3249
+ * await fs.mkdir("/tmp/mydir");
3250
+ *
3251
+ * // Create nested directories
3252
+ * await fs.mkdir("/tmp/path/to/dir", { recursive: true });
3253
+ *
3254
+ * // Create with specific permissions
3255
+ * await fs.mkdir("/tmp/mydir", { mode: 0o755 });
3256
+ * ```
3257
+ */
3258
+ async mkdir(path, options) {
3259
+ await mkdir(path, options);
3260
+ }
3261
+ /**
3262
+ * Lists files in a directory.
3263
+ *
3264
+ * @param path - The directory path to list
3265
+ * @param options - List options
3266
+ * @returns Array of filenames
3267
+ *
3268
+ * @example
3269
+ * ```typescript
3270
+ * const fs = alepha.inject(NodeFileSystemProvider);
3271
+ *
3272
+ * // List files in a directory
3273
+ * const files = await fs.ls("/tmp");
3274
+ * console.log(files); // ["file1.txt", "file2.txt", "subdir"]
3275
+ *
3276
+ * // List with hidden files
3277
+ * const allFiles = await fs.ls("/tmp", { hidden: true });
3278
+ *
3279
+ * // List recursively
3280
+ * const allFilesRecursive = await fs.ls("/tmp", { recursive: true });
3281
+ * ```
3282
+ */
3283
+ async ls(path, options) {
3284
+ const entries = await readdir(path);
3285
+ const filteredEntries = options?.hidden ? entries : entries.filter((e) => !e.startsWith("."));
3286
+ if (options?.recursive) {
3287
+ const allFiles = [];
3288
+ for (const entry of filteredEntries) {
3289
+ const fullPath = join(path, entry);
3290
+ if ((await stat(fullPath)).isDirectory()) {
3291
+ allFiles.push(entry);
3292
+ const subFiles = await this.ls(fullPath, options);
3293
+ allFiles.push(...subFiles.map((f) => join(entry, f)));
3294
+ } else allFiles.push(entry);
3295
+ }
3296
+ return allFiles;
3297
+ }
3298
+ return filteredEntries;
3299
+ }
3300
+ /**
3301
+ * Checks if a file or directory exists.
3302
+ *
3303
+ * @param path - The path to check
3304
+ * @returns True if the path exists, false otherwise
3305
+ *
3306
+ * @example
3307
+ * ```typescript
3308
+ * const fs = alepha.inject(NodeFileSystemProvider);
3309
+ *
3310
+ * if (await fs.exists("/tmp/file.txt")) {
3311
+ * console.log("File exists");
3312
+ * }
3313
+ * ```
3314
+ */
3315
+ async exists(path) {
3316
+ try {
3317
+ await access(path);
3318
+ return true;
3319
+ } catch {
3320
+ return false;
3321
+ }
3322
+ }
3323
+ /**
3324
+ * Reads the content of a file.
3325
+ *
3326
+ * @param path - The file path to read
3327
+ * @returns The file content as a Buffer
3328
+ *
3329
+ * @example
3330
+ * ```typescript
3331
+ * const fs = alepha.inject(NodeFileSystemProvider);
3332
+ *
3333
+ * const buffer = await fs.readFile("/tmp/file.txt");
3334
+ * console.log(buffer.toString("utf-8"));
3335
+ * ```
3336
+ */
3337
+ async readFile(path) {
3338
+ return await readFile(path);
3339
+ }
3340
+ /**
3341
+ * Writes data to a file.
3342
+ *
3343
+ * @param path - The file path to write to
3344
+ * @param data - The data to write (Buffer or string)
3345
+ *
3346
+ * @example
3347
+ * ```typescript
3348
+ * const fs = alepha.inject(NodeFileSystemProvider);
3349
+ *
3350
+ * // Write string
3351
+ * await fs.writeFile("/tmp/file.txt", "Hello, world!");
3352
+ *
3353
+ * // Write Buffer
3354
+ * await fs.writeFile("/tmp/file.bin", Buffer.from([0x01, 0x02, 0x03]));
3355
+ * ```
3356
+ */
3357
+ async writeFile(path, data) {
3358
+ if (isFileLike(data)) {
3359
+ await writeFile(path, Readable.from(data.stream()));
3360
+ return;
3361
+ }
3362
+ await writeFile(path, data);
3363
+ }
3364
+ /**
3365
+ * Reads the content of a file as a string.
3366
+ *
3367
+ * @param path - The file path to read
3368
+ * @returns The file content as a string
3369
+ *
3370
+ * @example
3371
+ * ```typescript
3372
+ * const fs = alepha.inject(NodeFileSystemProvider);
3373
+ * const content = await fs.readTextFile("/tmp/file.txt");
3374
+ * ```
3375
+ */
3376
+ async readTextFile(path) {
3377
+ return (await this.readFile(path)).toString("utf-8");
3378
+ }
3379
+ /**
3380
+ * Reads the content of a file as JSON.
3381
+ *
3382
+ * @param path - The file path to read
3383
+ * @returns The parsed JSON content
3384
+ *
3385
+ * @example
3386
+ * ```typescript
3387
+ * const fs = alepha.inject(NodeFileSystemProvider);
3388
+ * const config = await fs.readJsonFile<{ name: string }>("/tmp/config.json");
3389
+ * ```
3390
+ */
3391
+ async readJsonFile(path) {
3392
+ const text = await this.readTextFile(path);
3393
+ return this.json.parse(text);
3394
+ }
3395
+ /**
3396
+ * Creates a FileLike object from a Web File.
3397
+ *
3398
+ * @protected
3399
+ */
3400
+ createFileFromWebFile(source, options = {}) {
3401
+ const name = options.name ?? source.name;
3402
+ return {
3403
+ name,
3404
+ type: options.type ?? (source.type || this.detector.getContentType(name)),
3405
+ size: options.size ?? source.size ?? 0,
3406
+ lastModified: source.lastModified || Date.now(),
3407
+ stream: () => source.stream(),
3408
+ arrayBuffer: async () => {
3409
+ return await source.arrayBuffer();
3410
+ },
3411
+ text: async () => {
3412
+ return await source.text();
3413
+ }
3414
+ };
3415
+ }
3416
+ /**
3417
+ * Creates a FileLike object from a Buffer.
3418
+ *
3419
+ * @protected
3420
+ */
3421
+ createFileFromBuffer(source, options = {}) {
3422
+ const name = options.name ?? "file";
3423
+ return {
3424
+ name,
3425
+ type: options.type ?? this.detector.getContentType(options.name ?? name),
3426
+ size: source.byteLength,
3427
+ lastModified: Date.now(),
3428
+ stream: () => Readable.from(source),
3429
+ arrayBuffer: async () => {
3430
+ return this.bufferToArrayBuffer(source);
3431
+ },
3432
+ text: async () => {
3433
+ return source.toString("utf-8");
3434
+ }
3435
+ };
3436
+ }
3437
+ /**
3438
+ * Creates a FileLike object from a stream.
3439
+ *
3440
+ * @protected
3441
+ */
3442
+ createFileFromStream(source, options = {}) {
3443
+ let buffer = null;
3444
+ return {
3445
+ name: options.name ?? "file",
3446
+ type: options.type ?? this.detector.getContentType(options.name ?? "file"),
3447
+ size: options.size ?? 0,
3448
+ lastModified: Date.now(),
3449
+ stream: () => source,
3450
+ _buffer: null,
3451
+ arrayBuffer: async () => {
3452
+ buffer ??= await this.streamToBuffer(source);
3453
+ return this.bufferToArrayBuffer(buffer);
3454
+ },
3455
+ text: async () => {
3456
+ buffer ??= await this.streamToBuffer(source);
3457
+ return buffer.toString("utf-8");
3458
+ }
3459
+ };
3460
+ }
3461
+ /**
3462
+ * Creates a FileLike object from a URL.
3463
+ *
3464
+ * @protected
3465
+ */
3466
+ createFileFromUrl(url, options = {}) {
3467
+ const parsedUrl = new URL(url);
3468
+ const filename = options.name || parsedUrl.pathname.split("/").pop() || "file";
3469
+ let buffer = null;
3470
+ return {
3471
+ name: filename,
3472
+ type: options.type ?? this.detector.getContentType(filename),
3473
+ size: 0,
3474
+ lastModified: Date.now(),
3475
+ stream: () => this.createStreamFromUrl(url),
3476
+ arrayBuffer: async () => {
3477
+ buffer ??= await this.loadFromUrl(url);
3478
+ return this.bufferToArrayBuffer(buffer);
3479
+ },
3480
+ text: async () => {
3481
+ buffer ??= await this.loadFromUrl(url);
3482
+ return buffer.toString("utf-8");
3483
+ },
3484
+ filepath: url
3485
+ };
3486
+ }
3487
+ /**
3488
+ * Gets a streaming response from a URL.
3489
+ *
3490
+ * @protected
3491
+ */
3492
+ getStreamingResponse(url) {
3493
+ const stream = new PassThrough();
3494
+ fetch(url).then((res) => Readable.fromWeb(res.body).pipe(stream)).catch((err) => stream.destroy(err));
3495
+ return stream;
3496
+ }
3497
+ /**
3498
+ * Loads data from a URL.
3499
+ *
3500
+ * @protected
3501
+ */
3502
+ async loadFromUrl(url) {
3503
+ const parsedUrl = new URL(url);
3504
+ if (parsedUrl.protocol === "file:") return await readFile(fileURLToPath(url));
3505
+ else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
3506
+ const response = await fetch(url);
3507
+ if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
3508
+ const arrayBuffer = await response.arrayBuffer();
3509
+ return Buffer.from(arrayBuffer);
3510
+ } else throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
3511
+ }
3512
+ /**
3513
+ * Creates a stream from a URL.
3514
+ *
3515
+ * @protected
3516
+ */
3517
+ createStreamFromUrl(url) {
3518
+ const parsedUrl = new URL(url);
3519
+ if (parsedUrl.protocol === "file:") return createReadStream(fileURLToPath(url));
3520
+ else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return this.getStreamingResponse(url);
3521
+ else throw new AlephaError(`Unsupported protocol: ${parsedUrl.protocol}`);
3522
+ }
3523
+ /**
3524
+ * Converts a stream-like object to a Buffer.
3525
+ *
3526
+ * @protected
3527
+ */
3528
+ async streamToBuffer(streamLike) {
3529
+ const stream = streamLike instanceof Readable ? streamLike : Readable.fromWeb(streamLike);
3530
+ return new Promise((resolve, reject) => {
3531
+ const buffer = [];
3532
+ stream.on("data", (chunk) => buffer.push(Buffer.from(chunk)));
3533
+ stream.on("end", () => resolve(Buffer.concat(buffer)));
3534
+ stream.on("error", (err) => reject(new AlephaError("Error converting stream", { cause: err })));
3535
+ });
3536
+ }
3537
+ /**
3538
+ * Converts a Node.js Buffer to an ArrayBuffer.
3539
+ *
3540
+ * @protected
3541
+ */
3542
+ bufferToArrayBuffer(buffer) {
3543
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
3544
+ }
3545
+ };
3546
+
3547
+ //#endregion
3548
+ //#region ../../src/system/providers/NodeShellProvider.ts
3549
+ /**
3550
+ * Node.js implementation of ShellProvider.
3551
+ *
3552
+ * Executes shell commands using Node.js child_process module.
3553
+ * Supports binary resolution from node_modules/.bin for local packages.
3554
+ */
3555
+ var NodeShellProvider = class {
3556
+ log = $logger();
3557
+ fs = $inject(FileSystemProvider);
3558
+ /**
3559
+ * Run a shell command or binary.
3560
+ */
3561
+ async run(command, options = {}) {
3562
+ const { resolve = false, capture = false, root, env } = options;
3563
+ const cwd = root ?? process.cwd();
3564
+ this.log.debug(`Shell: ${command}`, {
3565
+ cwd,
3566
+ resolve,
3567
+ capture
3568
+ });
3569
+ let executable;
3570
+ let args;
3571
+ if (resolve) {
3572
+ const [bin, ...rest] = command.split(" ");
3573
+ executable = await this.resolveExecutable(bin, cwd);
3574
+ args = rest;
3575
+ } else [executable, ...args] = command.split(" ");
3576
+ if (capture) return this.execCapture(command, {
3577
+ cwd,
3578
+ env
3579
+ });
3580
+ return this.execInherit(executable, args, {
3581
+ cwd,
3582
+ env
3583
+ });
3584
+ }
3585
+ /**
3586
+ * Execute command with inherited stdio (streams to terminal).
3587
+ */
3588
+ async execInherit(executable, args, options) {
3589
+ const proc = spawn(executable, args, {
3590
+ stdio: "inherit",
3591
+ cwd: options.cwd,
3592
+ env: {
3593
+ ...process.env,
3594
+ ...options.env
3595
+ }
3596
+ });
3597
+ return new Promise((resolve, reject) => {
3598
+ proc.on("exit", (code) => {
3599
+ if (code === 0 || code === null) resolve("");
3600
+ else reject(new AlephaError(`Command exited with code ${code}`));
3601
+ });
3602
+ proc.on("error", reject);
3603
+ });
3604
+ }
3605
+ /**
3606
+ * Execute command and capture stdout.
3607
+ */
3608
+ execCapture(command, options) {
3609
+ return new Promise((resolve, reject) => {
3610
+ exec(command, {
3611
+ cwd: options.cwd,
3612
+ env: {
3613
+ ...process.env,
3614
+ LOG_FORMAT: "pretty",
3615
+ ...options.env
3616
+ }
3617
+ }, (err, stdout) => {
3618
+ if (err) {
3619
+ err.stdout = stdout;
3620
+ reject(err);
3621
+ } else resolve(stdout);
3622
+ });
3623
+ });
3624
+ }
3625
+ /**
3626
+ * Resolve executable path from node_modules/.bin.
3627
+ *
3628
+ * Search order:
3629
+ * 1. Local: node_modules/.bin/
3630
+ * 2. Pnpm nested: node_modules/alepha/node_modules/.bin/
3631
+ * 3. Monorepo: Walk up to 3 parent directories
3632
+ */
3633
+ async resolveExecutable(name, root) {
3634
+ const suffix = process.platform === "win32" ? ".cmd" : "";
3635
+ let execPath = await this.findExecutable(root, `node_modules/.bin/${name}${suffix}`);
3636
+ if (!execPath) execPath = await this.findExecutable(root, `node_modules/alepha/node_modules/.bin/${name}${suffix}`);
3637
+ if (!execPath) {
3638
+ let parentDir = this.fs.join(root, "..");
3639
+ for (let i = 0; i < 3; i++) {
3640
+ execPath = await this.findExecutable(parentDir, `node_modules/.bin/${name}${suffix}`);
3641
+ if (execPath) break;
3642
+ parentDir = this.fs.join(parentDir, "..");
3643
+ }
3644
+ }
3645
+ if (!execPath) throw new AlephaError(`Could not find executable for '${name}'. Make sure the package is installed.`);
3646
+ return execPath;
3647
+ }
3648
+ /**
3649
+ * Check if executable exists at path.
3650
+ */
3651
+ async findExecutable(root, relativePath) {
3652
+ const fullPath = this.fs.join(root, relativePath);
3653
+ if (await this.fs.exists(fullPath)) return fullPath;
3654
+ }
3655
+ /**
3656
+ * Check if a command is installed and available in the system PATH.
3657
+ */
3658
+ isInstalled(command) {
3659
+ return new Promise((resolve) => {
3660
+ exec(process.platform === "win32" ? `where ${command}` : `command -v ${command}`, (error) => resolve(!error));
3661
+ });
3662
+ }
3663
+ };
3664
+
3665
+ //#endregion
3666
+ //#region ../../src/system/providers/ShellProvider.ts
3667
+ /**
3668
+ * Abstract provider for executing shell commands and binaries.
3669
+ *
3670
+ * Implementations:
3671
+ * - `NodeShellProvider` - Real shell execution using Node.js child_process
3672
+ * - `MemoryShellProvider` - In-memory mock for testing
3673
+ *
3674
+ * @example
3675
+ * ```typescript
3676
+ * class MyService {
3677
+ * protected readonly shell = $inject(ShellProvider);
3678
+ *
3679
+ * async build() {
3680
+ * // Run shell command directly
3681
+ * await this.shell.run("yarn install");
3682
+ *
3683
+ * // Run local binary with resolution
3684
+ * await this.shell.run("vite build", { resolve: true });
3685
+ *
3686
+ * // Capture output
3687
+ * const output = await this.shell.run("echo hello", { capture: true });
3688
+ * }
3689
+ * }
3690
+ * ```
3691
+ */
3692
+ var ShellProvider = class {};
3693
+
3694
+ //#endregion
3695
+ //#region ../../src/system/index.ts
3696
+ /**
3697
+ * | type | quality | stability |
3698
+ * |------|---------|-----------|
3699
+ * | tooling | standard | stable |
3700
+ *
3701
+ * System-level abstractions for portable code across runtimes.
3702
+ *
3703
+ * **Features:**
3704
+ * - File system operations (read, write, exists, etc.)
3705
+ * - Shell command execution
3706
+ * - File type detection and MIME utilities
3707
+ * - Memory implementations for testing
3708
+ *
3709
+ * @module alepha.system
3710
+ */
3711
+ const AlephaSystem = $module({
3712
+ name: "alepha.system",
3713
+ primitives: [],
3714
+ services: [
3715
+ FileDetector,
3716
+ FileSystemProvider,
3717
+ MemoryFileSystemProvider,
3718
+ NodeFileSystemProvider,
3719
+ ShellProvider,
3720
+ MemoryShellProvider,
3721
+ NodeShellProvider
3722
+ ],
3723
+ register: (alepha) => alepha.with({
3724
+ optional: true,
3725
+ provide: FileSystemProvider,
3726
+ use: NodeFileSystemProvider
3727
+ }).with({
3728
+ optional: true,
3729
+ provide: ShellProvider,
3730
+ use: alepha.isTest() ? MemoryShellProvider : NodeShellProvider
3731
+ })
3732
+ });
3733
+
3734
+ //#endregion
3735
+ //#region ../../src/api/users/services/SessionService.ts
3736
+ var SessionService = class {
3737
+ alepha = $inject(Alepha);
3738
+ fsp = $inject(FileSystemProvider);
3739
+ dateTimeProvider = $inject(DateTimeProvider);
3740
+ cryptoProvider = $inject(CryptoProvider);
3741
+ log = $logger();
3742
+ realmProvider = $inject(RealmProvider);
3743
+ fileController = $client();
3744
+ auditService = $inject(AuditService);
3745
+ users(userRealmName) {
3746
+ return this.realmProvider.userRepository(userRealmName);
3747
+ }
3748
+ sessions(userRealmName) {
3749
+ return this.realmProvider.sessionRepository(userRealmName);
3750
+ }
3751
+ identities(userRealmName) {
3752
+ return this.realmProvider.identityRepository(userRealmName);
3753
+ }
3754
+ /**
3755
+ * Random delay to prevent timing attacks (50-200ms)
3756
+ * Uses cryptographically secure random number generation
3757
+ */
3758
+ randomDelay() {
3759
+ return new Promise((resolve) => setTimeout(resolve, randomInt(50, 201)));
3760
+ }
3761
+ /**
3762
+ * Validate user credentials and return the user if valid.
3763
+ */
3764
+ async login(provider, username, password, userRealmName) {
3765
+ const { settings, name } = this.realmProvider.getRealm(userRealmName);
3766
+ const isEmail = username.includes("@");
3767
+ const isPhone = /^[+\d][\d\s()-]+$/.test(username);
3768
+ const isUsername = !isEmail && !isPhone;
3769
+ const identities = this.identities(userRealmName);
3770
+ const users = this.users(userRealmName);
3771
+ await this.randomDelay();
3772
+ try {
3773
+ const where = users.createQueryWhere();
3774
+ where.realm = name;
3775
+ if (settings.usernameEnabled !== false && isUsername) {
3776
+ if (settings.usernameRegExp) {
3777
+ if (!new RegExp(settings.usernameRegExp).test(username)) {
3778
+ this.log.warn("Username does not match required format", {
3779
+ provider,
3780
+ username,
3781
+ realm: name
3782
+ });
3783
+ await this.auditService.recordAuth("login_failed", {
3784
+ userRealm: name,
3785
+ description: "Username does not match required format",
3786
+ metadata: {
3787
+ provider,
3788
+ username
3789
+ }
3790
+ });
3791
+ throw new InvalidCredentialsError();
3792
+ }
3793
+ }
3794
+ where.username = username;
3795
+ } else if (settings.emailEnabled !== false && isEmail) where.email = username;
3796
+ else if (settings.phoneEnabled === true && isPhone) where.phoneNumber = username;
3797
+ else {
3798
+ this.log.warn("Invalid login identifier format", {
3799
+ provider,
3800
+ username,
3801
+ realm: name
3802
+ });
3803
+ await this.auditService.recordAuth("login_failed", {
3804
+ userRealm: name,
3805
+ description: "Invalid login identifier format",
3806
+ metadata: {
3807
+ provider,
3808
+ username
3809
+ }
3810
+ });
3811
+ throw new InvalidCredentialsError();
3812
+ }
3813
+ const user = await users.findOne({ where }).catch(() => void 0);
3814
+ if (!user) {
3815
+ this.log.warn("User not found during login attempt", {
3816
+ provider,
3817
+ username,
3818
+ realm: name
3819
+ });
3820
+ await this.auditService.recordAuth("login_failed", {
3821
+ userRealm: name,
3822
+ description: "User not found",
3823
+ metadata: {
3824
+ provider,
3825
+ username
3826
+ }
3827
+ });
3828
+ throw new InvalidCredentialsError();
3829
+ }
3830
+ const identity = await identities.findOne({ where: {
3831
+ provider: { eq: provider },
3832
+ userId: { eq: user.id }
3833
+ } });
3834
+ const storedPassword = identity.password;
3835
+ if (!storedPassword) {
3836
+ this.log.error("Identity has no password configured", {
3837
+ provider,
3838
+ username,
3839
+ identityId: identity.id,
3840
+ realm: name
3841
+ });
3842
+ throw new InvalidCredentialsError();
3843
+ }
3844
+ if (!await this.cryptoProvider.verifyPassword(password, storedPassword)) {
3845
+ this.log.warn("Invalid password during login attempt", {
3846
+ provider,
3847
+ username,
3848
+ realm: name
3849
+ });
3850
+ await this.auditService.recordAuth("login_failed", {
3851
+ userRealm: name,
3852
+ resourceId: user.id,
3853
+ description: "Invalid password",
3854
+ metadata: {
3855
+ provider,
3856
+ username
3857
+ }
3858
+ });
3859
+ throw new InvalidCredentialsError();
3860
+ }
3861
+ await this.auditService.recordAuth("login", {
3862
+ userId: user.id,
3863
+ userEmail: user.email ?? void 0,
3864
+ userRealm: name,
3865
+ resourceId: user.id,
3866
+ description: `User logged in via ${provider}`,
3867
+ metadata: {
3868
+ provider,
3869
+ username
3870
+ }
3871
+ });
3872
+ return user;
3873
+ } catch (error) {
3874
+ if (error instanceof InvalidCredentialsError) throw error;
3875
+ this.log.warn("Error during login attempt", error);
3876
+ throw new InvalidCredentialsError();
3877
+ }
3878
+ }
3879
+ async createSession(user, expiresIn, userRealmName) {
3880
+ this.log.trace("Creating session", {
3881
+ userId: user.id,
3882
+ expiresIn
3883
+ });
3884
+ const request = this.alepha.context.get("request");
3885
+ const refreshToken = this.cryptoProvider.randomUUID();
3886
+ const expiresAt = this.dateTimeProvider.now().add(expiresIn, "seconds").toISOString();
3887
+ const session = await this.sessions(userRealmName).create({
3888
+ userId: user.id,
3889
+ expiresAt,
3890
+ ip: request?.ip,
3891
+ userAgent: request?.userAgent,
3892
+ refreshToken
3893
+ });
3894
+ this.log.info("Session created", {
3895
+ sessionId: session.id,
3896
+ userId: user.id,
3897
+ ip: request?.ip
3898
+ });
3899
+ return {
3900
+ refreshToken,
3901
+ sessionId: session.id
3902
+ };
3903
+ }
3904
+ async refreshSession(refreshToken, userRealmName) {
3905
+ this.log.trace("Refreshing session");
3906
+ const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } });
3907
+ const now = this.dateTimeProvider.now();
3908
+ const expiresAt = this.dateTimeProvider.of(session.expiresAt);
3909
+ if (this.dateTimeProvider.of(session.expiresAt) < now) {
3910
+ this.log.debug("Session expired during refresh", {
3911
+ sessionId: session.id,
3912
+ userId: session.userId
3913
+ });
3914
+ await this.sessions(userRealmName).deleteById(refreshToken);
3915
+ throw new UnauthorizedError("Session expired");
3916
+ }
3917
+ const user = await this.users(userRealmName).findOne({ where: { id: { eq: session.userId } } });
3918
+ this.log.debug("Session refreshed", {
3919
+ sessionId: session.id,
3920
+ userId: session.userId
3921
+ });
3922
+ return {
3923
+ user,
3924
+ expiresIn: expiresAt.unix() - now.unix(),
3925
+ sessionId: session.id
3926
+ };
3927
+ }
3928
+ async deleteSession(refreshToken, userRealmName) {
3929
+ this.log.trace("Deleting session");
3930
+ const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } }).catch(() => void 0);
3931
+ await this.sessions(userRealmName).deleteOne({ refreshToken });
3932
+ this.log.debug("Session deleted");
3933
+ if (session) {
3934
+ const { name } = this.realmProvider.getRealm(userRealmName);
3935
+ await this.auditService.recordAuth("logout", {
3936
+ userId: session.userId,
3937
+ userRealm: name,
3938
+ sessionId: session.id,
3939
+ description: "User logged out"
3940
+ });
3941
+ }
3942
+ }
3943
+ async link(provider, profile, userRealmName) {
3944
+ this.log.trace("Linking OAuth2 profile", {
3945
+ provider,
3946
+ profileSub: profile.sub,
3947
+ email: profile.email
3948
+ });
3949
+ const realm = this.realmProvider.getRealm(userRealmName);
3950
+ const identities = this.identities(userRealmName);
3951
+ const users = this.users(userRealmName);
3952
+ const identity = await identities.findOne({ where: {
3953
+ provider,
3954
+ providerUserId: profile.sub
2132
3955
  } }).catch(() => void 0);
2133
3956
  if (identity) {
2134
3957
  this.log.debug("Existing identity found", {
@@ -2136,19 +3959,19 @@ var SessionService = class {
2136
3959
  identityId: identity.id,
2137
3960
  userId: identity.userId
2138
3961
  });
2139
- const user$1 = await users$1.findById(identity.userId);
3962
+ const user = await users.findById(identity.userId);
2140
3963
  await this.auditService.recordAuth("login", {
2141
- userId: user$1.id,
2142
- userEmail: user$1.email ?? void 0,
3964
+ userId: user.id,
3965
+ userEmail: user.email ?? void 0,
2143
3966
  userRealm: realm.name,
2144
- resourceId: user$1.id,
3967
+ resourceId: user.id,
2145
3968
  description: `User logged in via OAuth2 (${provider})`,
2146
3969
  metadata: {
2147
3970
  provider,
2148
3971
  providerUserId: profile.sub
2149
3972
  }
2150
3973
  });
2151
- return user$1;
3974
+ return user;
2152
3975
  }
2153
3976
  if (!profile.email) {
2154
3977
  this.log.debug("OAuth2 profile has no email, returning profile as-is", {
@@ -2160,7 +3983,7 @@ var SessionService = class {
2160
3983
  ...profile
2161
3984
  };
2162
3985
  }
2163
- const existing = await users$1.findOne({ where: { email: profile.email } }).catch(() => void 0);
3986
+ const existing = await users.findOne({ where: { email: profile.email } }).catch(() => void 0);
2164
3987
  if (existing) {
2165
3988
  this.log.debug("Linking OAuth2 profile to existing user by email", {
2166
3989
  provider,
@@ -2168,7 +3991,7 @@ var SessionService = class {
2168
3991
  userId: existing.id,
2169
3992
  email: profile.email
2170
3993
  });
2171
- await identities$1.create({
3994
+ await identities.create({
2172
3995
  provider,
2173
3996
  providerUserId: profile.sub,
2174
3997
  userId: existing.id
@@ -2187,7 +4010,7 @@ var SessionService = class {
2187
4010
  });
2188
4011
  return existing;
2189
4012
  }
2190
- const user = await users$1.create({
4013
+ const user = await users.create({
2191
4014
  realm: realm.name,
2192
4015
  username: profile.email.split("@")[0],
2193
4016
  email: profile.email,
@@ -2204,7 +4027,7 @@ var SessionService = class {
2204
4027
  const file = this.fsp.createFile({ response });
2205
4028
  if (response.ok && response.body) {
2206
4029
  const fileEntity = await this.fileController.uploadFile({ body: { file } }, { user });
2207
- await users$1.updateById(user.id, { picture: fileEntity.id });
4030
+ await users.updateById(user.id, { picture: fileEntity.id });
2208
4031
  }
2209
4032
  } catch (error) {
2210
4033
  this.log.warn("Failed to fetch user profile picture", error);
@@ -2250,6 +4073,472 @@ var SessionService = class {
2250
4073
  }
2251
4074
  };
2252
4075
 
4076
+ //#endregion
4077
+ //#region ../../src/api/keys/schemas/adminApiKeyQuerySchema.ts
4078
+ const adminApiKeyQuerySchema = t.extend(pageQuerySchema, {
4079
+ userId: t.optional(t.uuid()),
4080
+ includeRevoked: t.optional(t.boolean())
4081
+ });
4082
+
4083
+ //#endregion
4084
+ //#region ../../src/api/keys/schemas/adminApiKeyResourceSchema.ts
4085
+ const adminApiKeyResourceSchema = t.object({
4086
+ id: t.uuid(),
4087
+ userId: t.uuid(),
4088
+ name: t.string(),
4089
+ description: t.optional(t.string()),
4090
+ tokenPrefix: t.string(),
4091
+ tokenSuffix: t.string(),
4092
+ roles: t.array(t.string()),
4093
+ createdAt: t.datetime(),
4094
+ lastUsedAt: t.optional(t.datetime()),
4095
+ lastUsedIp: t.optional(t.string()),
4096
+ expiresAt: t.optional(t.datetime()),
4097
+ revokedAt: t.optional(t.datetime()),
4098
+ usageCount: t.integer()
4099
+ });
4100
+
4101
+ //#endregion
4102
+ //#region ../../src/api/keys/entities/apiKeyEntity.ts
4103
+ const apiKeyEntity = $entity({
4104
+ name: "api_keys",
4105
+ schema: t.object({
4106
+ id: db.primaryKey(t.uuid()),
4107
+ createdAt: db.createdAt(),
4108
+ updatedAt: db.updatedAt(),
4109
+ userId: t.uuid(),
4110
+ name: t.text({ maxLength: 100 }),
4111
+ description: t.optional(t.text({ maxLength: 500 })),
4112
+ tokenHash: t.string({ maxLength: 256 }),
4113
+ tokenPrefix: t.string({ maxLength: 10 }),
4114
+ tokenSuffix: t.string({ maxLength: 8 }),
4115
+ roles: db.default(t.array(t.string()), []),
4116
+ lastUsedAt: t.optional(t.datetime()),
4117
+ lastUsedIp: t.optional(t.string({ maxLength: 45 })),
4118
+ usageCount: db.default(t.integer(), 0),
4119
+ expiresAt: t.optional(t.datetime()),
4120
+ revokedAt: t.optional(t.datetime())
4121
+ }),
4122
+ indexes: [{
4123
+ columns: ["userId", "name"],
4124
+ unique: true
4125
+ }, {
4126
+ columns: ["tokenHash"],
4127
+ unique: true
4128
+ }]
4129
+ });
4130
+
4131
+ //#endregion
4132
+ //#region ../../src/api/keys/services/ApiKeyService.ts
4133
+ var ApiKeyService = class {
4134
+ alepha = $inject(Alepha);
4135
+ dateTimeProvider = $inject(DateTimeProvider);
4136
+ log = $logger();
4137
+ repo = $repository(apiKeyEntity);
4138
+ /**
4139
+ * Cache validated API keys for 15 minutes.
4140
+ */
4141
+ validationCache = $cache({
4142
+ name: "api-key-validation",
4143
+ ttl: [15, "minutes"]
4144
+ });
4145
+ /**
4146
+ * Create an issuer resolver for API key authentication.
4147
+ * Lower priority means it runs before JWT resolver.
4148
+ *
4149
+ * @param options.priority - Priority of this resolver (default: 50, JWT is 100)
4150
+ * @param options.prefix - API key prefix to match in Bearer header (default: "ak")
4151
+ */
4152
+ createResolver(options = {}) {
4153
+ const { priority = 50, prefix = "ak" } = options;
4154
+ const prefixPattern = `${prefix}_`;
4155
+ return {
4156
+ priority,
4157
+ onRequest: async (req) => {
4158
+ let token = (typeof req.url === "string" ? new URL(req.url) : req.url).searchParams.get("api_key");
4159
+ if (!token) {
4160
+ const auth = req.headers.authorization;
4161
+ if (auth?.startsWith("Bearer ")) {
4162
+ const bearerToken = auth.slice(7);
4163
+ if (bearerToken.startsWith(prefixPattern)) token = bearerToken;
4164
+ }
4165
+ }
4166
+ if (!token) return null;
4167
+ return this.validate(token);
4168
+ }
4169
+ };
4170
+ }
4171
+ /**
4172
+ * Create a new API key for a user.
4173
+ * Returns both the API key entity and the plain token (which is only available once).
4174
+ */
4175
+ async create(options) {
4176
+ const prefix = options.prefix ?? "ak";
4177
+ const token = `${prefix}_${randomBytes(24).toString("base64url")}`;
4178
+ const hash = this.hashToken(token);
4179
+ const suffix = token.slice(-8);
4180
+ const apiKey = await this.repo.create({
4181
+ userId: options.userId,
4182
+ name: options.name,
4183
+ description: options.description,
4184
+ tokenHash: hash,
4185
+ tokenPrefix: prefix,
4186
+ tokenSuffix: suffix,
4187
+ roles: options.roles,
4188
+ expiresAt: options.expiresAt?.toISOString()
4189
+ });
4190
+ this.log.info("API key created", {
4191
+ apiKeyId: apiKey.id,
4192
+ userId: options.userId,
4193
+ name: options.name
4194
+ });
4195
+ return {
4196
+ apiKey,
4197
+ token
4198
+ };
4199
+ }
4200
+ /**
4201
+ * List all non-revoked API keys for a user.
4202
+ */
4203
+ async list(userId) {
4204
+ return this.repo.findMany({
4205
+ where: {
4206
+ userId: { eq: userId },
4207
+ revokedAt: { isNull: true }
4208
+ },
4209
+ orderBy: {
4210
+ column: "createdAt",
4211
+ direction: "desc"
4212
+ }
4213
+ });
4214
+ }
4215
+ /**
4216
+ * Find all API keys with optional filtering (admin only).
4217
+ */
4218
+ async findAll(query) {
4219
+ query.sort ??= "-createdAt";
4220
+ const where = this.repo.createQueryWhere();
4221
+ if (query.userId) where.userId = { eq: query.userId };
4222
+ if (!query.includeRevoked) where.revokedAt = { isNull: true };
4223
+ return this.repo.paginate(query, { where }, { count: true });
4224
+ }
4225
+ /**
4226
+ * Get an API key by ID (admin only).
4227
+ */
4228
+ async getById(id) {
4229
+ const apiKey = await this.repo.findById(id).catch(() => null);
4230
+ if (!apiKey) throw new NotFoundError("API key not found");
4231
+ return apiKey;
4232
+ }
4233
+ /**
4234
+ * Revoke any API key (admin only).
4235
+ */
4236
+ async revokeByAdmin(id) {
4237
+ const apiKey = await this.repo.findById(id).catch(() => null);
4238
+ if (!apiKey) throw new NotFoundError("API key not found");
4239
+ if (apiKey.revokedAt) return;
4240
+ await this.validationCache.invalidate(apiKey.tokenHash);
4241
+ await this.repo.updateById(id, { revokedAt: this.dateTimeProvider.now().toISOString() });
4242
+ this.log.info("API key revoked by admin", {
4243
+ apiKeyId: id,
4244
+ userId: apiKey.userId
4245
+ });
4246
+ }
4247
+ /**
4248
+ * Revoke an API key. Only the owner can revoke their own keys.
4249
+ */
4250
+ async revoke(id, userId) {
4251
+ const apiKey = await this.repo.findById(id).catch(() => null);
4252
+ if (!apiKey) throw new NotFoundError("API key not found");
4253
+ if (apiKey.userId !== userId) throw new ForbiddenError("Not your API key");
4254
+ await this.validationCache.invalidate(apiKey.tokenHash);
4255
+ await this.repo.updateById(id, { revokedAt: this.dateTimeProvider.now().toISOString() });
4256
+ this.log.info("API key revoked", {
4257
+ apiKeyId: id,
4258
+ userId
4259
+ });
4260
+ }
4261
+ /**
4262
+ * Validate an API key token and return user info if valid.
4263
+ */
4264
+ async validate(token) {
4265
+ if (!token.includes("_")) return null;
4266
+ const hash = this.hashToken(token);
4267
+ let apiKey = await this.validationCache.get(hash);
4268
+ if (apiKey === void 0) {
4269
+ apiKey = await this.repo.findOne({ where: { tokenHash: { eq: hash } } }).catch(() => null);
4270
+ if (apiKey) await this.validationCache.set(hash, apiKey);
4271
+ }
4272
+ if (!apiKey) return null;
4273
+ if (apiKey.revokedAt) return null;
4274
+ if (apiKey.expiresAt && this.dateTimeProvider.now().isAfter(apiKey.expiresAt)) return null;
4275
+ this.updateUsage(apiKey.id).catch((error) => {
4276
+ this.log.warn("Failed to update API key usage", { error });
4277
+ });
4278
+ return {
4279
+ id: apiKey.userId,
4280
+ roles: apiKey.roles
4281
+ };
4282
+ }
4283
+ /**
4284
+ * Update usage statistics for an API key.
4285
+ */
4286
+ async updateUsage(id) {
4287
+ const request = this.alepha.context.get("request");
4288
+ await this.repo.updateById(id, {
4289
+ lastUsedAt: this.dateTimeProvider.now().toISOString(),
4290
+ lastUsedIp: request?.ip,
4291
+ usageCount: sql`${this.repo.table.usageCount} + 1`
4292
+ });
4293
+ }
4294
+ /**
4295
+ * Hash a token using SHA-256.
4296
+ */
4297
+ hashToken(token) {
4298
+ return createHash("sha256").update(token).digest("hex");
4299
+ }
4300
+ };
4301
+
4302
+ //#endregion
4303
+ //#region ../../src/api/keys/controllers/AdminApiKeyController.ts
4304
+ /**
4305
+ * REST API controller for admin API key management.
4306
+ * Admins can list, view, and revoke any API key.
4307
+ */
4308
+ var AdminApiKeyController = class {
4309
+ url = "/admin/api-keys";
4310
+ group = "admin:api-keys";
4311
+ apiKeyService = $inject(ApiKeyService);
4312
+ /**
4313
+ * Find all API keys with optional filtering.
4314
+ */
4315
+ findApiKeys = $action({
4316
+ path: this.url,
4317
+ group: this.group,
4318
+ secure: true,
4319
+ description: "Find API keys with pagination and filtering",
4320
+ schema: {
4321
+ query: adminApiKeyQuerySchema,
4322
+ response: t.page(adminApiKeyResourceSchema)
4323
+ },
4324
+ handler: ({ query }) => {
4325
+ const { userId, includeRevoked, ...pagination } = query;
4326
+ return this.apiKeyService.findAll({
4327
+ userId,
4328
+ includeRevoked,
4329
+ ...pagination
4330
+ });
4331
+ }
4332
+ });
4333
+ /**
4334
+ * Get an API key by ID.
4335
+ */
4336
+ getApiKey = $action({
4337
+ path: `${this.url}/:id`,
4338
+ group: this.group,
4339
+ secure: true,
4340
+ description: "Get an API key by ID",
4341
+ schema: {
4342
+ params: t.object({ id: t.uuid() }),
4343
+ response: adminApiKeyResourceSchema
4344
+ },
4345
+ handler: ({ params }) => this.apiKeyService.getById(params.id)
4346
+ });
4347
+ /**
4348
+ * Revoke any API key.
4349
+ */
4350
+ revokeApiKey = $action({
4351
+ method: "DELETE",
4352
+ path: `${this.url}/:id`,
4353
+ group: this.group,
4354
+ secure: true,
4355
+ description: "Revoke an API key",
4356
+ schema: {
4357
+ params: t.object({ id: t.uuid() }),
4358
+ response: okSchema
4359
+ },
4360
+ handler: async ({ params }) => {
4361
+ await this.apiKeyService.revokeByAdmin(params.id);
4362
+ return {
4363
+ ok: true,
4364
+ id: params.id
4365
+ };
4366
+ }
4367
+ });
4368
+ };
4369
+
4370
+ //#endregion
4371
+ //#region ../../src/api/keys/schemas/createApiKeyBodySchema.ts
4372
+ const createApiKeyBodySchema = t.object({
4373
+ name: t.text({
4374
+ minLength: 1,
4375
+ maxLength: 100
4376
+ }),
4377
+ description: t.optional(t.text({ maxLength: 500 })),
4378
+ expiresAt: t.optional(t.datetime())
4379
+ });
4380
+
4381
+ //#endregion
4382
+ //#region ../../src/api/keys/schemas/createApiKeyResponseSchema.ts
4383
+ const createApiKeyResponseSchema = t.object({
4384
+ id: t.uuid(),
4385
+ name: t.string(),
4386
+ token: t.string(),
4387
+ tokenSuffix: t.string(),
4388
+ roles: t.array(t.string()),
4389
+ createdAt: t.datetime(),
4390
+ expiresAt: t.optional(t.datetime())
4391
+ });
4392
+
4393
+ //#endregion
4394
+ //#region ../../src/api/keys/schemas/listApiKeyResponseSchema.ts
4395
+ const listApiKeyItemSchema = t.object({
4396
+ id: t.uuid(),
4397
+ name: t.string(),
4398
+ tokenPrefix: t.string(),
4399
+ tokenSuffix: t.string(),
4400
+ roles: t.array(t.string()),
4401
+ createdAt: t.datetime(),
4402
+ lastUsedAt: t.optional(t.datetime()),
4403
+ expiresAt: t.optional(t.datetime()),
4404
+ usageCount: t.integer()
4405
+ });
4406
+ const listApiKeyResponseSchema = t.array(listApiKeyItemSchema);
4407
+
4408
+ //#endregion
4409
+ //#region ../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts
4410
+ const revokeApiKeyParamsSchema = t.object({ id: t.uuid() });
4411
+
4412
+ //#endregion
4413
+ //#region ../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts
4414
+ const revokeApiKeyResponseSchema = t.object({ ok: t.boolean() });
4415
+
4416
+ //#endregion
4417
+ //#region ../../src/api/keys/controllers/ApiKeyController.ts
4418
+ /**
4419
+ * REST API controller for user's own API key management.
4420
+ * Users can create, list, and revoke their own API keys.
4421
+ */
4422
+ var ApiKeyController = class {
4423
+ url = "/api-keys";
4424
+ group = "api-keys";
4425
+ apiKeyService = $inject(ApiKeyService);
4426
+ /**
4427
+ * Create a new API key for the authenticated user.
4428
+ * The token is only returned once upon creation.
4429
+ */
4430
+ createApiKey = $action({
4431
+ method: "POST",
4432
+ path: this.url,
4433
+ group: this.group,
4434
+ description: "Create a new API key",
4435
+ secure: true,
4436
+ schema: {
4437
+ body: createApiKeyBodySchema,
4438
+ response: createApiKeyResponseSchema
4439
+ },
4440
+ handler: async (request) => {
4441
+ const { apiKey, token } = await this.apiKeyService.create({
4442
+ userId: request.user.id,
4443
+ name: request.body.name,
4444
+ description: request.body.description,
4445
+ roles: request.user.roles ?? [],
4446
+ expiresAt: request.body.expiresAt ? new Date(request.body.expiresAt) : void 0
4447
+ });
4448
+ return {
4449
+ id: apiKey.id,
4450
+ name: apiKey.name,
4451
+ token,
4452
+ tokenSuffix: apiKey.tokenSuffix,
4453
+ roles: apiKey.roles,
4454
+ createdAt: apiKey.createdAt,
4455
+ expiresAt: apiKey.expiresAt
4456
+ };
4457
+ }
4458
+ });
4459
+ /**
4460
+ * List all active API keys for the authenticated user.
4461
+ * Does not return the actual tokens.
4462
+ */
4463
+ listApiKeys = $action({
4464
+ path: this.url,
4465
+ group: this.group,
4466
+ description: "List your API keys",
4467
+ secure: true,
4468
+ schema: { response: listApiKeyResponseSchema },
4469
+ handler: async (request) => {
4470
+ return (await this.apiKeyService.list(request.user.id)).map((apiKey) => ({
4471
+ id: apiKey.id,
4472
+ name: apiKey.name,
4473
+ tokenPrefix: apiKey.tokenPrefix,
4474
+ tokenSuffix: apiKey.tokenSuffix,
4475
+ roles: apiKey.roles,
4476
+ createdAt: apiKey.createdAt,
4477
+ lastUsedAt: apiKey.lastUsedAt,
4478
+ expiresAt: apiKey.expiresAt,
4479
+ usageCount: apiKey.usageCount
4480
+ }));
4481
+ }
4482
+ });
4483
+ /**
4484
+ * Revoke an API key. Only the owner can revoke their own keys.
4485
+ */
4486
+ revokeApiKey = $action({
4487
+ method: "DELETE",
4488
+ path: `${this.url}/:id`,
4489
+ group: this.group,
4490
+ description: "Revoke an API key",
4491
+ secure: true,
4492
+ schema: {
4493
+ params: revokeApiKeyParamsSchema,
4494
+ response: revokeApiKeyResponseSchema
4495
+ },
4496
+ handler: async (request) => {
4497
+ await this.apiKeyService.revoke(request.params.id, request.user.id);
4498
+ return { ok: true };
4499
+ }
4500
+ });
4501
+ };
4502
+
4503
+ //#endregion
4504
+ //#region ../../src/api/keys/index.ts
4505
+ /**
4506
+ * | type | quality | stability |
4507
+ * |------|---------|--------------|
4508
+ * | backend | good | experimental |
4509
+ *
4510
+ * API key management module for programmatic access.
4511
+ *
4512
+ * **Features:**
4513
+ * - Create API keys with role snapshots
4514
+ * - List and revoke API keys
4515
+ * - 15-minute validation caching
4516
+ * - Query param (?api_key=) and Bearer header support
4517
+ *
4518
+ * **Integration:**
4519
+ * To enable API key authentication for an issuer, register the resolver:
4520
+ *
4521
+ * ```ts
4522
+ * class MyApp {
4523
+ * apiKeyService = $inject(ApiKeyService);
4524
+ * issuer = $issuer({
4525
+ * secret: env.APP_SECRET,
4526
+ * resolvers: [this.apiKeyService.createResolver()],
4527
+ * });
4528
+ * }
4529
+ * ```
4530
+ *
4531
+ * @module alepha.api.keys
4532
+ */
4533
+ const AlephaApiKeys = $module({
4534
+ name: "alepha.api.keys",
4535
+ services: [
4536
+ ApiKeyService,
4537
+ ApiKeyController,
4538
+ AdminApiKeyController
4539
+ ]
4540
+ });
4541
+
2253
4542
  //#endregion
2254
4543
  //#region ../../src/api/users/primitives/$realm.ts
2255
4544
  /**
@@ -2278,10 +4567,18 @@ const $realm = (options = {}) => {
2278
4567
  const realmRegistration = realmProvider.register(name, options);
2279
4568
  alepha.with(AlephaApiFiles);
2280
4569
  alepha.with(AlephaApiAudits);
4570
+ alepha.with(AlephaApiJobs);
4571
+ const customResolvers = [...options.issuer?.resolvers ?? []];
4572
+ if (options.apiKeys) {
4573
+ alepha.with(AlephaApiKeys);
4574
+ const apiKeyService = alepha.inject(ApiKeyService);
4575
+ customResolvers.push(apiKeyService.createResolver());
4576
+ }
2281
4577
  const realm = $issuer({
2282
4578
  ...options.issuer,
2283
4579
  name,
2284
4580
  secret: options.secret ?? securityProvider.secretKey,
4581
+ resolvers: customResolvers,
2285
4582
  roles: options.issuer?.roles ?? [{
2286
4583
  name: "admin",
2287
4584
  permissions: [{ name: "*" }]
@@ -2308,21 +4605,21 @@ const $realm = (options = {}) => {
2308
4605
  ...options.issuer?.settings
2309
4606
  }
2310
4607
  });
2311
- realm.link = (name$1) => {
2312
- return (ctx) => sessionService.link(name$1, ctx.user, realm.name);
4608
+ realm.link = (name) => {
4609
+ return (ctx) => sessionService.link(name, ctx.user, realm.name);
2313
4610
  };
2314
- realm.login = (name$1) => {
4611
+ realm.login = (name) => {
2315
4612
  return (credentials) => {
2316
- return sessionService.login(name$1, credentials.username, credentials.password, realm.name);
4613
+ return sessionService.login(name, credentials.username, credentials.password, realm.name);
2317
4614
  };
2318
4615
  };
2319
- const identities$1 = options.identities ?? { credentials: true };
2320
- if (identities$1) {
4616
+ const identities = options.identities ?? { credentials: true };
4617
+ if (identities) {
2321
4618
  const auth = {};
2322
- if (identities$1.credentials) auth.credentials = $authCredentials(realm);
4619
+ if (identities.credentials) auth.credentials = $authCredentials(realm);
2323
4620
  else realmRegistration.settings.registrationAllowed = false;
2324
- if (identities$1.google) auth.google = $authGoogle(realm);
2325
- if (identities$1.github) auth.github = $authGithub(realm);
4621
+ if (identities.google) auth.google = $authGoogle(realm);
4622
+ if (identities.github) auth.github = $authGithub(realm);
2326
4623
  alepha.with(() => auth);
2327
4624
  }
2328
4625
  return realm;
@@ -2388,10 +4685,21 @@ const resetPasswordSchema = t.object({
2388
4685
  //#endregion
2389
4686
  //#region ../../src/api/users/index.ts
2390
4687
  /**
2391
- * Provides user management API endpoints for Alepha applications.
4688
+ * | type | quality | stability |
4689
+ * |------|---------|-----------|
4690
+ * | backend | epic | stable |
4691
+ *
4692
+ * Complete user management with multi-realm support for multi-tenant applications.
2392
4693
  *
2393
- * This module includes user CRUD operations, authentication endpoints,
2394
- * password reset functionality, and user profile management capabilities.
4694
+ * **Features:**
4695
+ * - User registration, login, and profile management
4696
+ * - Password reset workflows
4697
+ * - Email verification
4698
+ * - Session management with multiple devices
4699
+ * - Identity management (social logins, SSO)
4700
+ * - Multi-realm support for tenant isolation
4701
+ * - Credential management
4702
+ * - Entities: `users`, `identities`, `sessions`
2395
4703
  *
2396
4704
  * @module alepha.api.users
2397
4705
  */