alepha 0.15.1 → 0.15.3

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 (523) hide show
  1. package/README.md +68 -80
  2. package/dist/api/audits/index.d.ts +10 -33
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +10 -33
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +10 -3
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +10 -3
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +162 -155
  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.d.ts +10 -4
  19. package/dist/api/notifications/index.d.ts.map +1 -1
  20. package/dist/api/notifications/index.js +10 -4
  21. package/dist/api/notifications/index.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +43 -50
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +30 -37
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/api/users/index.d.ts +1081 -760
  27. package/dist/api/users/index.d.ts.map +1 -1
  28. package/dist/api/users/index.js +2539 -218
  29. package/dist/api/users/index.js.map +1 -1
  30. package/dist/api/verifications/index.d.ts +138 -132
  31. package/dist/api/verifications/index.d.ts.map +1 -1
  32. package/dist/api/verifications/index.js +12 -4
  33. package/dist/api/verifications/index.js.map +1 -1
  34. package/dist/batch/index.d.ts +20 -40
  35. package/dist/batch/index.d.ts.map +1 -1
  36. package/dist/batch/index.js +31 -44
  37. package/dist/batch/index.js.map +1 -1
  38. package/dist/bucket/index.d.ts +440 -8
  39. package/dist/bucket/index.d.ts.map +1 -1
  40. package/dist/bucket/index.js +1861 -12
  41. package/dist/bucket/index.js.map +1 -1
  42. package/dist/cache/core/index.d.ts +179 -7
  43. package/dist/cache/core/index.d.ts.map +1 -1
  44. package/dist/cache/core/index.js +213 -7
  45. package/dist/cache/core/index.js.map +1 -1
  46. package/dist/cache/redis/index.d.ts +1 -0
  47. package/dist/cache/redis/index.d.ts.map +1 -1
  48. package/dist/cache/redis/index.js +4 -0
  49. package/dist/cache/redis/index.js.map +1 -1
  50. package/dist/cli/index.d.ts +638 -5645
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +2550 -368
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/command/index.d.ts +203 -45
  55. package/dist/command/index.d.ts.map +1 -1
  56. package/dist/command/index.js +2060 -71
  57. package/dist/command/index.js.map +1 -1
  58. package/dist/core/index.browser.js +70 -40
  59. package/dist/core/index.browser.js.map +1 -1
  60. package/dist/core/index.d.ts +34 -13
  61. package/dist/core/index.d.ts.map +1 -1
  62. package/dist/core/index.js +90 -40
  63. package/dist/core/index.js.map +1 -1
  64. package/dist/core/index.native.js +70 -40
  65. package/dist/core/index.native.js.map +1 -1
  66. package/dist/datetime/index.d.ts +15 -0
  67. package/dist/datetime/index.d.ts.map +1 -1
  68. package/dist/datetime/index.js +15 -0
  69. package/dist/datetime/index.js.map +1 -1
  70. package/dist/email/index.d.ts +323 -20
  71. package/dist/email/index.d.ts.map +1 -1
  72. package/dist/email/index.js +1857 -7
  73. package/dist/email/index.js.map +1 -1
  74. package/dist/fake/index.d.ts +90 -8
  75. package/dist/fake/index.d.ts.map +1 -1
  76. package/dist/fake/index.js +91 -20
  77. package/dist/fake/index.js.map +1 -1
  78. package/dist/lock/core/index.d.ts +11 -4
  79. package/dist/lock/core/index.d.ts.map +1 -1
  80. package/dist/lock/core/index.js +11 -4
  81. package/dist/lock/core/index.js.map +1 -1
  82. package/dist/logger/index.d.ts +17 -66
  83. package/dist/logger/index.d.ts.map +1 -1
  84. package/dist/logger/index.js +14 -63
  85. package/dist/logger/index.js.map +1 -1
  86. package/dist/mcp/index.d.ts +10 -30
  87. package/dist/mcp/index.d.ts.map +1 -1
  88. package/dist/mcp/index.js +12 -35
  89. package/dist/mcp/index.js.map +1 -1
  90. package/dist/orm/index.browser.js +3 -3
  91. package/dist/orm/index.browser.js.map +1 -1
  92. package/dist/orm/index.bun.js +39 -20
  93. package/dist/orm/index.bun.js.map +1 -1
  94. package/dist/orm/index.d.ts +517 -540
  95. package/dist/orm/index.d.ts.map +1 -1
  96. package/dist/orm/index.js +58 -71
  97. package/dist/orm/index.js.map +1 -1
  98. package/dist/queue/core/index.d.ts +18 -10
  99. package/dist/queue/core/index.d.ts.map +1 -1
  100. package/dist/queue/core/index.js +14 -6
  101. package/dist/queue/core/index.js.map +1 -1
  102. package/dist/react/auth/index.browser.js +108 -0
  103. package/dist/react/auth/index.browser.js.map +1 -0
  104. package/dist/react/auth/index.d.ts +100 -0
  105. package/dist/react/auth/index.d.ts.map +1 -0
  106. package/dist/react/auth/index.js +145 -0
  107. package/dist/react/auth/index.js.map +1 -0
  108. package/dist/react/core/index.d.ts +469 -0
  109. package/dist/react/core/index.d.ts.map +1 -0
  110. package/dist/react/core/index.js +464 -0
  111. package/dist/react/core/index.js.map +1 -0
  112. package/dist/react/form/index.d.ts +232 -0
  113. package/dist/react/form/index.d.ts.map +1 -0
  114. package/dist/react/form/index.js +432 -0
  115. package/dist/react/form/index.js.map +1 -0
  116. package/dist/react/head/index.browser.js +423 -0
  117. package/dist/react/head/index.browser.js.map +1 -0
  118. package/dist/react/head/index.d.ts +288 -0
  119. package/dist/react/head/index.d.ts.map +1 -0
  120. package/dist/react/head/index.js +465 -0
  121. package/dist/react/head/index.js.map +1 -0
  122. package/dist/react/i18n/index.d.ts +175 -0
  123. package/dist/react/i18n/index.d.ts.map +1 -0
  124. package/dist/react/i18n/index.js +224 -0
  125. package/dist/react/i18n/index.js.map +1 -0
  126. package/dist/react/router/index.browser.js +1974 -0
  127. package/dist/react/router/index.browser.js.map +1 -0
  128. package/dist/react/router/index.d.ts +1956 -0
  129. package/dist/react/router/index.d.ts.map +1 -0
  130. package/dist/react/router/index.js +4722 -0
  131. package/dist/react/router/index.js.map +1 -0
  132. package/dist/react/websocket/index.d.ts +117 -0
  133. package/dist/react/websocket/index.d.ts.map +1 -0
  134. package/dist/react/websocket/index.js +107 -0
  135. package/dist/react/websocket/index.js.map +1 -0
  136. package/dist/redis/index.bun.js +4 -0
  137. package/dist/redis/index.bun.js.map +1 -1
  138. package/dist/redis/index.d.ts +41 -44
  139. package/dist/redis/index.d.ts.map +1 -1
  140. package/dist/redis/index.js +16 -25
  141. package/dist/redis/index.js.map +1 -1
  142. package/dist/retry/index.d.ts +11 -2
  143. package/dist/retry/index.d.ts.map +1 -1
  144. package/dist/retry/index.js +11 -2
  145. package/dist/retry/index.js.map +1 -1
  146. package/dist/scheduler/index.d.ts +11 -2
  147. package/dist/scheduler/index.d.ts.map +1 -1
  148. package/dist/scheduler/index.js +11 -2
  149. package/dist/scheduler/index.js.map +1 -1
  150. package/dist/security/index.d.ts +140 -49
  151. package/dist/security/index.d.ts.map +1 -1
  152. package/dist/security/index.js +164 -32
  153. package/dist/security/index.js.map +1 -1
  154. package/dist/server/auth/index.d.ts +12 -7
  155. package/dist/server/auth/index.d.ts.map +1 -1
  156. package/dist/server/auth/index.js +12 -7
  157. package/dist/server/auth/index.js.map +1 -1
  158. package/dist/server/cache/index.d.ts +7 -22
  159. package/dist/server/cache/index.d.ts.map +1 -1
  160. package/dist/server/cache/index.js +7 -22
  161. package/dist/server/cache/index.js.map +1 -1
  162. package/dist/server/compress/index.d.ts +10 -2
  163. package/dist/server/compress/index.d.ts.map +1 -1
  164. package/dist/server/compress/index.js +10 -2
  165. package/dist/server/compress/index.js.map +1 -1
  166. package/dist/server/cookies/index.d.ts +40 -16
  167. package/dist/server/cookies/index.d.ts.map +1 -1
  168. package/dist/server/cookies/index.js +7 -5
  169. package/dist/server/cookies/index.js.map +1 -1
  170. package/dist/server/core/index.d.ts +124 -23
  171. package/dist/server/core/index.d.ts.map +1 -1
  172. package/dist/server/core/index.js +231 -14
  173. package/dist/server/core/index.js.map +1 -1
  174. package/dist/server/cors/index.d.ts +13 -23
  175. package/dist/server/cors/index.d.ts.map +1 -1
  176. package/dist/server/cors/index.js +7 -21
  177. package/dist/server/cors/index.js.map +1 -1
  178. package/dist/server/health/index.d.ts +8 -2
  179. package/dist/server/health/index.d.ts.map +1 -1
  180. package/dist/server/health/index.js +8 -2
  181. package/dist/server/health/index.js.map +1 -1
  182. package/dist/server/helmet/index.d.ts +11 -3
  183. package/dist/server/helmet/index.d.ts.map +1 -1
  184. package/dist/server/helmet/index.js +11 -3
  185. package/dist/server/helmet/index.js.map +1 -1
  186. package/dist/server/links/index.d.ts +11 -6
  187. package/dist/server/links/index.d.ts.map +1 -1
  188. package/dist/server/links/index.js +11 -6
  189. package/dist/server/links/index.js.map +1 -1
  190. package/dist/server/metrics/index.d.ts +10 -3
  191. package/dist/server/metrics/index.d.ts.map +1 -1
  192. package/dist/server/metrics/index.js +10 -3
  193. package/dist/server/metrics/index.js.map +1 -1
  194. package/dist/server/multipart/index.d.ts +9 -3
  195. package/dist/server/multipart/index.d.ts.map +1 -1
  196. package/dist/server/multipart/index.js +9 -3
  197. package/dist/server/multipart/index.js.map +1 -1
  198. package/dist/server/proxy/index.d.ts +8 -2
  199. package/dist/server/proxy/index.d.ts.map +1 -1
  200. package/dist/server/proxy/index.js +8 -2
  201. package/dist/server/proxy/index.js.map +1 -1
  202. package/dist/server/rate-limit/index.d.ts +30 -35
  203. package/dist/server/rate-limit/index.d.ts.map +1 -1
  204. package/dist/server/rate-limit/index.js +18 -55
  205. package/dist/server/rate-limit/index.js.map +1 -1
  206. package/dist/server/static/index.d.ts +137 -4
  207. package/dist/server/static/index.d.ts.map +1 -1
  208. package/dist/server/static/index.js +1853 -5
  209. package/dist/server/static/index.js.map +1 -1
  210. package/dist/server/swagger/index.d.ts +309 -6
  211. package/dist/server/swagger/index.d.ts.map +1 -1
  212. package/dist/server/swagger/index.js +1854 -6
  213. package/dist/server/swagger/index.js.map +1 -1
  214. package/dist/sms/index.d.ts +309 -7
  215. package/dist/sms/index.d.ts.map +1 -1
  216. package/dist/sms/index.js +1856 -7
  217. package/dist/sms/index.js.map +1 -1
  218. package/dist/system/index.browser.js +1218 -0
  219. package/dist/system/index.browser.js.map +1 -0
  220. package/dist/{file → system}/index.d.ts +343 -16
  221. package/dist/system/index.d.ts.map +1 -0
  222. package/dist/{file → system}/index.js +419 -22
  223. package/dist/system/index.js.map +1 -0
  224. package/dist/thread/index.d.ts +11 -2
  225. package/dist/thread/index.d.ts.map +1 -1
  226. package/dist/thread/index.js +11 -2
  227. package/dist/thread/index.js.map +1 -1
  228. package/dist/topic/core/index.d.ts +12 -5
  229. package/dist/topic/core/index.d.ts.map +1 -1
  230. package/dist/topic/core/index.js +12 -5
  231. package/dist/topic/core/index.js.map +1 -1
  232. package/dist/vite/index.d.ts +5 -6272
  233. package/dist/vite/index.d.ts.map +1 -1
  234. package/dist/vite/index.js +23 -10
  235. package/dist/vite/index.js.map +1 -1
  236. package/dist/websocket/index.d.ts +12 -8
  237. package/dist/websocket/index.d.ts.map +1 -1
  238. package/dist/websocket/index.js +12 -8
  239. package/dist/websocket/index.js.map +1 -1
  240. package/package.json +82 -11
  241. package/src/api/audits/index.ts +10 -33
  242. package/src/api/files/__tests__/$bucket.spec.ts +1 -1
  243. package/src/api/files/controllers/AdminFileStatsController.spec.ts +1 -1
  244. package/src/api/files/controllers/FileController.spec.ts +1 -1
  245. package/src/api/files/index.ts +10 -3
  246. package/src/api/files/jobs/FileJobs.spec.ts +1 -1
  247. package/src/api/files/services/FileService.spec.ts +1 -1
  248. package/src/api/jobs/index.ts +10 -3
  249. package/src/api/keys/controllers/AdminApiKeyController.ts +75 -0
  250. package/src/api/keys/controllers/ApiKeyController.ts +103 -0
  251. package/src/api/keys/entities/apiKeyEntity.ts +41 -0
  252. package/src/api/keys/index.ts +49 -0
  253. package/src/api/keys/schemas/adminApiKeyQuerySchema.ts +7 -0
  254. package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +17 -0
  255. package/src/api/keys/schemas/createApiKeyBodySchema.ts +7 -0
  256. package/src/api/keys/schemas/createApiKeyResponseSchema.ts +11 -0
  257. package/src/api/keys/schemas/listApiKeyResponseSchema.ts +15 -0
  258. package/src/api/keys/schemas/revokeApiKeyParamsSchema.ts +5 -0
  259. package/src/api/keys/schemas/revokeApiKeyResponseSchema.ts +5 -0
  260. package/src/api/keys/services/ApiKeyService.spec.ts +553 -0
  261. package/src/api/keys/services/ApiKeyService.ts +306 -0
  262. package/src/api/logs/TODO.md +55 -0
  263. package/src/api/notifications/index.ts +10 -4
  264. package/src/api/parameters/index.ts +9 -30
  265. package/src/api/parameters/primitives/$config.ts +12 -4
  266. package/src/api/parameters/services/ConfigStore.ts +9 -3
  267. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1035 -0
  268. package/src/api/users/__tests__/ApiKeys.spec.ts +401 -0
  269. package/src/api/users/index.ts +14 -3
  270. package/src/api/users/primitives/$realm.ts +33 -5
  271. package/src/api/users/providers/RealmProvider.ts +1 -12
  272. package/src/api/users/services/SessionService.ts +1 -1
  273. package/src/api/verifications/controllers/VerificationController.ts +2 -0
  274. package/src/api/verifications/index.ts +10 -4
  275. package/src/batch/index.ts +9 -36
  276. package/src/batch/primitives/$batch.ts +0 -8
  277. package/src/batch/providers/BatchProvider.ts +29 -2
  278. package/src/bucket/__tests__/shared.ts +1 -1
  279. package/src/bucket/index.ts +13 -6
  280. package/src/bucket/primitives/$bucket.ts +1 -1
  281. package/src/bucket/providers/LocalFileStorageProvider.ts +1 -1
  282. package/src/bucket/providers/MemoryFileStorageProvider.ts +1 -1
  283. package/src/cache/core/__tests__/shared.ts +30 -0
  284. package/src/cache/core/index.ts +11 -6
  285. package/src/cache/core/primitives/$cache.spec.ts +5 -0
  286. package/src/cache/core/providers/CacheProvider.ts +17 -0
  287. package/src/cache/core/providers/MemoryCacheProvider.ts +300 -1
  288. package/src/cache/redis/__tests__/cache-redis.spec.ts +5 -0
  289. package/src/cache/redis/providers/RedisCacheProvider.ts +9 -0
  290. package/src/cli/apps/AlephaCli.ts +1 -14
  291. package/src/cli/apps/AlephaPackageBuilderCli.ts +10 -1
  292. package/src/cli/atoms/buildOptions.ts +99 -9
  293. package/src/cli/commands/build.ts +150 -37
  294. package/src/cli/commands/db.ts +22 -18
  295. package/src/cli/commands/deploy.ts +1 -1
  296. package/src/cli/commands/dev.ts +1 -20
  297. package/src/cli/commands/gen/env.ts +5 -2
  298. package/src/cli/commands/gen/openapi.ts +5 -2
  299. package/src/cli/commands/init.spec.ts +588 -0
  300. package/src/cli/commands/init.ts +115 -58
  301. package/src/cli/commands/lint.ts +7 -1
  302. package/src/cli/commands/typecheck.ts +11 -0
  303. package/src/cli/providers/AppEntryProvider.ts +1 -1
  304. package/src/cli/providers/ViteBuildProvider.ts +8 -50
  305. package/src/cli/providers/ViteDevServerProvider.ts +35 -16
  306. package/src/cli/services/AlephaCliUtils.ts +52 -121
  307. package/src/cli/services/PackageManagerUtils.ts +129 -11
  308. package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
  309. package/src/cli/services/ProjectScaffolder.ts +148 -81
  310. package/src/cli/services/ViteUtils.ts +82 -0
  311. package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +37 -24
  312. package/src/cli/templates/apiAppSecurityTs.ts +11 -0
  313. package/src/cli/templates/apiIndexTs.ts +30 -0
  314. package/src/cli/templates/gitignore.ts +39 -0
  315. package/src/cli/{assets → templates}/mainCss.ts +11 -2
  316. package/src/cli/templates/mainServerTs.ts +33 -0
  317. package/src/cli/templates/webAppRouterTs.ts +74 -0
  318. package/src/cli/templates/webHelloComponentTsx.ts +30 -0
  319. package/src/command/helpers/Runner.spec.ts +139 -0
  320. package/src/command/helpers/Runner.ts +7 -22
  321. package/src/command/index.ts +12 -4
  322. package/src/command/providers/CliProvider.spec.ts +1392 -0
  323. package/src/command/providers/CliProvider.ts +320 -47
  324. package/src/core/Alepha.ts +34 -27
  325. package/src/core/__tests__/Alepha-start.spec.ts +4 -4
  326. package/src/core/helpers/jsonSchemaToTypeBox.spec.ts +771 -0
  327. package/src/core/helpers/jsonSchemaToTypeBox.ts +62 -10
  328. package/src/core/index.shared.ts +1 -0
  329. package/src/core/index.ts +20 -0
  330. package/src/core/providers/EventManager.spec.ts +0 -71
  331. package/src/core/providers/EventManager.ts +3 -15
  332. package/src/core/providers/Json.ts +2 -14
  333. package/src/datetime/index.ts +15 -0
  334. package/src/email/index.ts +10 -5
  335. package/src/email/providers/LocalEmailProvider.spec.ts +1 -1
  336. package/src/email/providers/LocalEmailProvider.ts +1 -1
  337. package/src/fake/__tests__/keyName.example.ts +1 -1
  338. package/src/fake/__tests__/keyName.spec.ts +5 -5
  339. package/src/fake/index.ts +9 -6
  340. package/src/fake/providers/FakeProvider.spec.ts +258 -40
  341. package/src/fake/providers/FakeProvider.ts +133 -19
  342. package/src/lock/core/index.ts +11 -4
  343. package/src/logger/index.ts +17 -66
  344. package/src/mcp/index.ts +10 -27
  345. package/src/mcp/transports/SseMcpTransport.ts +0 -11
  346. package/src/orm/__tests__/PostgresProvider.spec.ts +2 -2
  347. package/src/orm/index.browser.ts +2 -2
  348. package/src/orm/index.bun.ts +5 -3
  349. package/src/orm/index.ts +23 -53
  350. package/src/orm/providers/drivers/BunSqliteProvider.ts +5 -1
  351. package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
  352. package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
  353. package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
  354. package/src/orm/services/Repository.ts +7 -3
  355. package/src/queue/core/index.ts +14 -6
  356. package/src/react/auth/__tests__/$auth.spec.ts +202 -0
  357. package/src/react/auth/hooks/useAuth.ts +32 -0
  358. package/src/react/auth/index.browser.ts +13 -0
  359. package/src/react/auth/index.shared.ts +2 -0
  360. package/src/react/auth/index.ts +48 -0
  361. package/src/react/auth/providers/ReactAuthProvider.ts +16 -0
  362. package/src/react/auth/services/ReactAuth.ts +135 -0
  363. package/src/react/core/__tests__/Router.spec.tsx +169 -0
  364. package/src/react/core/components/ClientOnly.tsx +49 -0
  365. package/src/react/core/components/ErrorBoundary.tsx +73 -0
  366. package/src/react/core/contexts/AlephaContext.ts +7 -0
  367. package/src/react/core/contexts/AlephaProvider.tsx +42 -0
  368. package/src/react/core/hooks/useAction.browser.spec.tsx +569 -0
  369. package/src/react/core/hooks/useAction.ts +480 -0
  370. package/src/react/core/hooks/useAlepha.ts +26 -0
  371. package/src/react/core/hooks/useClient.ts +17 -0
  372. package/src/react/core/hooks/useEvents.ts +51 -0
  373. package/src/react/core/hooks/useInject.ts +12 -0
  374. package/src/react/core/hooks/useStore.ts +52 -0
  375. package/src/react/core/index.ts +90 -0
  376. package/src/react/form/components/FormState.tsx +17 -0
  377. package/src/react/form/errors/FormValidationError.ts +18 -0
  378. package/src/react/form/hooks/useForm.browser.spec.tsx +366 -0
  379. package/src/react/form/hooks/useForm.ts +47 -0
  380. package/src/react/form/hooks/useFormState.ts +130 -0
  381. package/src/react/form/index.ts +44 -0
  382. package/src/react/form/services/FormModel.ts +614 -0
  383. package/src/react/head/helpers/SeoExpander.spec.ts +203 -0
  384. package/src/react/head/helpers/SeoExpander.ts +142 -0
  385. package/src/react/head/hooks/useHead.spec.tsx +288 -0
  386. package/src/react/head/hooks/useHead.ts +62 -0
  387. package/src/react/head/index.browser.ts +26 -0
  388. package/src/react/head/index.ts +44 -0
  389. package/src/react/head/interfaces/Head.ts +105 -0
  390. package/src/react/head/primitives/$head.ts +25 -0
  391. package/src/react/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
  392. package/src/react/head/providers/BrowserHeadProvider.ts +212 -0
  393. package/src/react/head/providers/HeadProvider.ts +168 -0
  394. package/src/react/head/providers/ServerHeadProvider.ts +31 -0
  395. package/src/react/i18n/__tests__/integration.spec.tsx +239 -0
  396. package/src/react/i18n/components/Localize.spec.tsx +357 -0
  397. package/src/react/i18n/components/Localize.tsx +35 -0
  398. package/src/react/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  399. package/src/react/i18n/hooks/useI18n.ts +18 -0
  400. package/src/react/i18n/index.ts +41 -0
  401. package/src/react/i18n/primitives/$dictionary.ts +69 -0
  402. package/src/react/i18n/providers/I18nProvider.spec.ts +389 -0
  403. package/src/react/i18n/providers/I18nProvider.ts +278 -0
  404. package/src/react/router/__tests__/page-head-browser.browser.spec.ts +95 -0
  405. package/src/react/router/__tests__/page-head.spec.ts +48 -0
  406. package/src/react/router/__tests__/seo-head.spec.ts +125 -0
  407. package/src/react/router/atoms/ssrManifestAtom.ts +58 -0
  408. package/src/react/router/components/ErrorViewer.tsx +872 -0
  409. package/src/react/router/components/Link.tsx +23 -0
  410. package/src/react/router/components/NestedView.tsx +223 -0
  411. package/src/react/router/components/NotFound.tsx +30 -0
  412. package/src/react/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  413. package/src/react/router/contexts/RouterLayerContext.ts +12 -0
  414. package/src/react/router/errors/Redirection.ts +28 -0
  415. package/src/react/router/hooks/useActive.ts +52 -0
  416. package/src/react/router/hooks/useQueryParams.ts +63 -0
  417. package/src/react/router/hooks/useRouter.ts +20 -0
  418. package/src/react/router/hooks/useRouterState.ts +11 -0
  419. package/src/react/router/index.browser.ts +45 -0
  420. package/src/react/router/index.shared.ts +19 -0
  421. package/src/react/router/index.ts +146 -0
  422. package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
  423. package/src/react/router/primitives/$page.spec.tsx +676 -0
  424. package/src/react/router/primitives/$page.ts +489 -0
  425. package/src/react/router/providers/ReactBrowserProvider.ts +312 -0
  426. package/src/react/router/providers/ReactBrowserRendererProvider.ts +25 -0
  427. package/src/react/router/providers/ReactBrowserRouterProvider.ts +168 -0
  428. package/src/react/router/providers/ReactPageProvider.ts +726 -0
  429. package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
  430. package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
  431. package/src/react/router/providers/ReactServerProvider.spec.tsx +316 -0
  432. package/src/react/router/providers/ReactServerProvider.ts +487 -0
  433. package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
  434. package/src/react/router/providers/ReactServerTemplateProvider.ts +542 -0
  435. package/src/react/router/providers/SSRManifestProvider.ts +334 -0
  436. package/src/react/router/services/ReactPageServerService.ts +48 -0
  437. package/src/react/router/services/ReactPageService.ts +27 -0
  438. package/src/react/router/services/ReactRouter.ts +262 -0
  439. package/src/react/websocket/hooks/useRoom.tsx +242 -0
  440. package/src/react/websocket/index.ts +7 -0
  441. package/src/redis/__tests__/redis.spec.ts +13 -0
  442. package/src/redis/index.ts +9 -25
  443. package/src/redis/providers/BunRedisProvider.ts +9 -0
  444. package/src/redis/providers/NodeRedisProvider.ts +8 -0
  445. package/src/redis/providers/RedisProvider.ts +16 -0
  446. package/src/retry/index.ts +11 -2
  447. package/src/router/index.ts +15 -0
  448. package/src/scheduler/index.ts +11 -2
  449. package/src/security/__tests__/BasicAuth.spec.ts +2 -0
  450. package/src/security/__tests__/ServerSecurityProvider.spec.ts +90 -5
  451. package/src/security/index.ts +15 -10
  452. package/src/security/interfaces/IssuerResolver.ts +27 -0
  453. package/src/security/primitives/$issuer.ts +55 -0
  454. package/src/security/providers/SecurityProvider.ts +179 -0
  455. package/src/security/providers/ServerBasicAuthProvider.ts +6 -2
  456. package/src/security/providers/ServerSecurityProvider.ts +63 -41
  457. package/src/server/auth/index.ts +12 -7
  458. package/src/server/cache/index.ts +7 -22
  459. package/src/server/compress/index.ts +10 -2
  460. package/src/server/cookies/index.ts +7 -5
  461. package/src/server/cookies/primitives/$cookie.ts +33 -11
  462. package/src/server/core/index.ts +16 -6
  463. package/src/server/core/interfaces/ServerRequest.ts +83 -1
  464. package/src/server/core/primitives/$action.spec.ts +1 -1
  465. package/src/server/core/primitives/$action.ts +8 -3
  466. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
  467. package/src/server/core/providers/NodeHttpServerProvider.ts +9 -3
  468. package/src/server/core/services/ServerRequestParser.spec.ts +520 -0
  469. package/src/server/core/services/ServerRequestParser.ts +306 -13
  470. package/src/server/cors/index.ts +7 -21
  471. package/src/server/cors/primitives/$cors.ts +6 -2
  472. package/src/server/health/index.ts +8 -2
  473. package/src/server/helmet/index.ts +11 -3
  474. package/src/server/links/index.ts +11 -6
  475. package/src/server/metrics/index.ts +10 -3
  476. package/src/server/multipart/index.ts +9 -3
  477. package/src/server/proxy/index.ts +8 -2
  478. package/src/server/rate-limit/index.ts +21 -25
  479. package/src/server/rate-limit/primitives/$rateLimit.ts +6 -2
  480. package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +38 -14
  481. package/src/server/rate-limit/providers/ServerRateLimitProvider.ts +22 -56
  482. package/src/server/static/index.ts +8 -2
  483. package/src/server/static/providers/ServerStaticProvider.ts +1 -1
  484. package/src/server/swagger/index.ts +9 -4
  485. package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
  486. package/src/sms/index.ts +9 -5
  487. package/src/sms/providers/LocalSmsProvider.spec.ts +1 -1
  488. package/src/sms/providers/LocalSmsProvider.ts +1 -1
  489. package/src/system/index.browser.ts +36 -0
  490. package/src/system/index.ts +62 -0
  491. package/src/system/index.workerd.ts +1 -0
  492. package/src/{file → system}/providers/FileSystemProvider.ts +24 -0
  493. package/src/{file → system}/providers/MemoryFileSystemProvider.ts +116 -3
  494. package/src/system/providers/MemoryShellProvider.ts +164 -0
  495. package/src/{file → system}/providers/NodeFileSystemProvider.spec.ts +2 -2
  496. package/src/{file → system}/providers/NodeFileSystemProvider.ts +47 -2
  497. package/src/system/providers/NodeShellProvider.ts +184 -0
  498. package/src/system/providers/ShellProvider.ts +74 -0
  499. package/src/{file → system}/services/FileDetector.spec.ts +2 -2
  500. package/src/thread/index.ts +11 -2
  501. package/src/topic/core/index.ts +12 -5
  502. package/src/vite/tasks/buildClient.ts +2 -7
  503. package/src/vite/tasks/buildServer.ts +19 -13
  504. package/src/vite/tasks/generateCloudflare.ts +10 -7
  505. package/src/vite/tasks/generateDocker.ts +4 -0
  506. package/src/websocket/index.ts +12 -8
  507. package/dist/file/index.d.ts.map +0 -1
  508. package/dist/file/index.js.map +0 -1
  509. package/src/cli/assets/apiIndexTs.ts +0 -16
  510. package/src/cli/assets/mainServerTs.ts +0 -24
  511. package/src/cli/assets/webAppRouterTs.ts +0 -16
  512. package/src/cli/assets/webHelloComponentTsx.ts +0 -20
  513. package/src/cli/providers/ViteTemplateProvider.ts +0 -27
  514. package/src/file/index.ts +0 -43
  515. /package/src/cli/{assets → templates}/apiHelloControllerTs.ts +0 -0
  516. /package/src/cli/{assets → templates}/biomeJson.ts +0 -0
  517. /package/src/cli/{assets → templates}/dummySpecTs.ts +0 -0
  518. /package/src/cli/{assets → templates}/editorconfig.ts +0 -0
  519. /package/src/cli/{assets → templates}/mainBrowserTs.ts +0 -0
  520. /package/src/cli/{assets → templates}/tsconfigJson.ts +0 -0
  521. /package/src/cli/{assets → templates}/webIndexTs.ts +0 -0
  522. /package/src/{file → system}/errors/FileError.ts +0 -0
  523. /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,222 +1897,2062 @@ 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 = this.identities(userRealmName);
1939
- const users = this.users(userRealmName);
1940
- await this.randomDelay();
1941
- try {
1942
- const where = users.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.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.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
- return {
2092
- user,
2093
- expiresIn: expiresAt.unix() - now.unix(),
2094
- sessionId: session.id
2095
- };
2096
2105
  }
2097
- async deleteSession(refreshToken, userRealmName) {
2098
- this.log.trace("Deleting session");
2099
- const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } }).catch(() => void 0);
2100
- await this.sessions(userRealmName).deleteOne({ refreshToken });
2101
- this.log.debug("Session deleted");
2102
- if (session) {
2103
- const { name } = this.realmProvider.getRealm(userRealmName);
2104
- await this.auditService.recordAuth("logout", {
2105
- userId: session.userId,
2106
- userRealm: name,
2107
- sessionId: session.id,
2108
- description: "User logged out"
2109
- });
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]);
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]);
2110
2123
  }
2124
+ let result = Array.from(entries);
2125
+ if (!options?.hidden) result = result.filter((entry) => !entry.startsWith("."));
2126
+ return result.sort();
2111
2127
  }
2112
- async link(provider, profile, userRealmName) {
2113
- this.log.trace("Linking OAuth2 profile", {
2114
- provider,
2115
- profileSub: profile.sub,
2116
- email: profile.email
2117
- });
2118
- const realm = this.realmProvider.getRealm(userRealmName);
2119
- const identities = this.identities(userRealmName);
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
+ const p = mkdir(path, {
3260
+ recursive: options.recursive ?? true,
3261
+ mode: options.mode
3262
+ });
3263
+ if (options.force === false) await p;
3264
+ else await p.catch(() => {});
3265
+ }
3266
+ /**
3267
+ * Lists files in a directory.
3268
+ *
3269
+ * @param path - The directory path to list
3270
+ * @param options - List options
3271
+ * @returns Array of filenames
3272
+ *
3273
+ * @example
3274
+ * ```typescript
3275
+ * const fs = alepha.inject(NodeFileSystemProvider);
3276
+ *
3277
+ * // List files in a directory
3278
+ * const files = await fs.ls("/tmp");
3279
+ * console.log(files); // ["file1.txt", "file2.txt", "subdir"]
3280
+ *
3281
+ * // List with hidden files
3282
+ * const allFiles = await fs.ls("/tmp", { hidden: true });
3283
+ *
3284
+ * // List recursively
3285
+ * const allFilesRecursive = await fs.ls("/tmp", { recursive: true });
3286
+ * ```
3287
+ */
3288
+ async ls(path, options) {
3289
+ const entries = await readdir(path);
3290
+ const filteredEntries = options?.hidden ? entries : entries.filter((e) => !e.startsWith("."));
3291
+ if (options?.recursive) {
3292
+ const allFiles = [];
3293
+ for (const entry of filteredEntries) {
3294
+ const fullPath = join(path, entry);
3295
+ if ((await stat(fullPath)).isDirectory()) {
3296
+ allFiles.push(entry);
3297
+ const subFiles = await this.ls(fullPath, options);
3298
+ allFiles.push(...subFiles.map((f) => join(entry, f)));
3299
+ } else allFiles.push(entry);
3300
+ }
3301
+ return allFiles;
3302
+ }
3303
+ return filteredEntries;
3304
+ }
3305
+ /**
3306
+ * Checks if a file or directory exists.
3307
+ *
3308
+ * @param path - The path to check
3309
+ * @returns True if the path exists, false otherwise
3310
+ *
3311
+ * @example
3312
+ * ```typescript
3313
+ * const fs = alepha.inject(NodeFileSystemProvider);
3314
+ *
3315
+ * if (await fs.exists("/tmp/file.txt")) {
3316
+ * console.log("File exists");
3317
+ * }
3318
+ * ```
3319
+ */
3320
+ async exists(path) {
3321
+ try {
3322
+ await access(path);
3323
+ return true;
3324
+ } catch {
3325
+ return false;
3326
+ }
3327
+ }
3328
+ /**
3329
+ * Reads the content of a file.
3330
+ *
3331
+ * @param path - The file path to read
3332
+ * @returns The file content as a Buffer
3333
+ *
3334
+ * @example
3335
+ * ```typescript
3336
+ * const fs = alepha.inject(NodeFileSystemProvider);
3337
+ *
3338
+ * const buffer = await fs.readFile("/tmp/file.txt");
3339
+ * console.log(buffer.toString("utf-8"));
3340
+ * ```
3341
+ */
3342
+ async readFile(path) {
3343
+ return await readFile(path);
3344
+ }
3345
+ /**
3346
+ * Writes data to a file.
3347
+ *
3348
+ * @param path - The file path to write to
3349
+ * @param data - The data to write (Buffer or string)
3350
+ *
3351
+ * @example
3352
+ * ```typescript
3353
+ * const fs = alepha.inject(NodeFileSystemProvider);
3354
+ *
3355
+ * // Write string
3356
+ * await fs.writeFile("/tmp/file.txt", "Hello, world!");
3357
+ *
3358
+ * // Write Buffer
3359
+ * await fs.writeFile("/tmp/file.bin", Buffer.from([0x01, 0x02, 0x03]));
3360
+ * ```
3361
+ */
3362
+ async writeFile(path, data) {
3363
+ if (isFileLike(data)) {
3364
+ await writeFile(path, Readable.from(data.stream()));
3365
+ return;
3366
+ }
3367
+ await writeFile(path, data);
3368
+ }
3369
+ /**
3370
+ * Reads the content of a file as a string.
3371
+ *
3372
+ * @param path - The file path to read
3373
+ * @returns The file content as a string
3374
+ *
3375
+ * @example
3376
+ * ```typescript
3377
+ * const fs = alepha.inject(NodeFileSystemProvider);
3378
+ * const content = await fs.readTextFile("/tmp/file.txt");
3379
+ * ```
3380
+ */
3381
+ async readTextFile(path) {
3382
+ return (await this.readFile(path)).toString("utf-8");
3383
+ }
3384
+ /**
3385
+ * Reads the content of a file as JSON.
3386
+ *
3387
+ * @param path - The file path to read
3388
+ * @returns The parsed JSON content
3389
+ *
3390
+ * @example
3391
+ * ```typescript
3392
+ * const fs = alepha.inject(NodeFileSystemProvider);
3393
+ * const config = await fs.readJsonFile<{ name: string }>("/tmp/config.json");
3394
+ * ```
3395
+ */
3396
+ async readJsonFile(path) {
3397
+ const text = await this.readTextFile(path);
3398
+ return this.json.parse(text);
3399
+ }
3400
+ /**
3401
+ * Creates a FileLike object from a Web File.
3402
+ *
3403
+ * @protected
3404
+ */
3405
+ createFileFromWebFile(source, options = {}) {
3406
+ const name = options.name ?? source.name;
3407
+ return {
3408
+ name,
3409
+ type: options.type ?? (source.type || this.detector.getContentType(name)),
3410
+ size: options.size ?? source.size ?? 0,
3411
+ lastModified: source.lastModified || Date.now(),
3412
+ stream: () => source.stream(),
3413
+ arrayBuffer: async () => {
3414
+ return await source.arrayBuffer();
3415
+ },
3416
+ text: async () => {
3417
+ return await source.text();
3418
+ }
3419
+ };
3420
+ }
3421
+ /**
3422
+ * Creates a FileLike object from a Buffer.
3423
+ *
3424
+ * @protected
3425
+ */
3426
+ createFileFromBuffer(source, options = {}) {
3427
+ const name = options.name ?? "file";
3428
+ return {
3429
+ name,
3430
+ type: options.type ?? this.detector.getContentType(options.name ?? name),
3431
+ size: source.byteLength,
3432
+ lastModified: Date.now(),
3433
+ stream: () => Readable.from(source),
3434
+ arrayBuffer: async () => {
3435
+ return this.bufferToArrayBuffer(source);
3436
+ },
3437
+ text: async () => {
3438
+ return source.toString("utf-8");
3439
+ }
3440
+ };
3441
+ }
3442
+ /**
3443
+ * Creates a FileLike object from a stream.
3444
+ *
3445
+ * @protected
3446
+ */
3447
+ createFileFromStream(source, options = {}) {
3448
+ let buffer = null;
3449
+ return {
3450
+ name: options.name ?? "file",
3451
+ type: options.type ?? this.detector.getContentType(options.name ?? "file"),
3452
+ size: options.size ?? 0,
3453
+ lastModified: Date.now(),
3454
+ stream: () => source,
3455
+ _buffer: null,
3456
+ arrayBuffer: async () => {
3457
+ buffer ??= await this.streamToBuffer(source);
3458
+ return this.bufferToArrayBuffer(buffer);
3459
+ },
3460
+ text: async () => {
3461
+ buffer ??= await this.streamToBuffer(source);
3462
+ return buffer.toString("utf-8");
3463
+ }
3464
+ };
3465
+ }
3466
+ /**
3467
+ * Creates a FileLike object from a URL.
3468
+ *
3469
+ * @protected
3470
+ */
3471
+ createFileFromUrl(url, options = {}) {
3472
+ const parsedUrl = new URL(url);
3473
+ const filename = options.name || parsedUrl.pathname.split("/").pop() || "file";
3474
+ let buffer = null;
3475
+ return {
3476
+ name: filename,
3477
+ type: options.type ?? this.detector.getContentType(filename),
3478
+ size: 0,
3479
+ lastModified: Date.now(),
3480
+ stream: () => this.createStreamFromUrl(url),
3481
+ arrayBuffer: async () => {
3482
+ buffer ??= await this.loadFromUrl(url);
3483
+ return this.bufferToArrayBuffer(buffer);
3484
+ },
3485
+ text: async () => {
3486
+ buffer ??= await this.loadFromUrl(url);
3487
+ return buffer.toString("utf-8");
3488
+ },
3489
+ filepath: url
3490
+ };
3491
+ }
3492
+ /**
3493
+ * Gets a streaming response from a URL.
3494
+ *
3495
+ * @protected
3496
+ */
3497
+ getStreamingResponse(url) {
3498
+ const stream = new PassThrough();
3499
+ fetch(url).then((res) => Readable.fromWeb(res.body).pipe(stream)).catch((err) => stream.destroy(err));
3500
+ return stream;
3501
+ }
3502
+ /**
3503
+ * Loads data from a URL.
3504
+ *
3505
+ * @protected
3506
+ */
3507
+ async loadFromUrl(url) {
3508
+ const parsedUrl = new URL(url);
3509
+ if (parsedUrl.protocol === "file:") return await readFile(fileURLToPath(url));
3510
+ else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
3511
+ const response = await fetch(url);
3512
+ if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
3513
+ const arrayBuffer = await response.arrayBuffer();
3514
+ return Buffer.from(arrayBuffer);
3515
+ } else throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
3516
+ }
3517
+ /**
3518
+ * Creates a stream from a URL.
3519
+ *
3520
+ * @protected
3521
+ */
3522
+ createStreamFromUrl(url) {
3523
+ const parsedUrl = new URL(url);
3524
+ if (parsedUrl.protocol === "file:") return createReadStream(fileURLToPath(url));
3525
+ else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return this.getStreamingResponse(url);
3526
+ else throw new AlephaError(`Unsupported protocol: ${parsedUrl.protocol}`);
3527
+ }
3528
+ /**
3529
+ * Converts a stream-like object to a Buffer.
3530
+ *
3531
+ * @protected
3532
+ */
3533
+ async streamToBuffer(streamLike) {
3534
+ const stream = streamLike instanceof Readable ? streamLike : Readable.fromWeb(streamLike);
3535
+ return new Promise((resolve, reject) => {
3536
+ const buffer = [];
3537
+ stream.on("data", (chunk) => buffer.push(Buffer.from(chunk)));
3538
+ stream.on("end", () => resolve(Buffer.concat(buffer)));
3539
+ stream.on("error", (err) => reject(new AlephaError("Error converting stream", { cause: err })));
3540
+ });
3541
+ }
3542
+ /**
3543
+ * Converts a Node.js Buffer to an ArrayBuffer.
3544
+ *
3545
+ * @protected
3546
+ */
3547
+ bufferToArrayBuffer(buffer) {
3548
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
3549
+ }
3550
+ };
3551
+
3552
+ //#endregion
3553
+ //#region ../../src/system/providers/NodeShellProvider.ts
3554
+ /**
3555
+ * Node.js implementation of ShellProvider.
3556
+ *
3557
+ * Executes shell commands using Node.js child_process module.
3558
+ * Supports binary resolution from node_modules/.bin for local packages.
3559
+ */
3560
+ var NodeShellProvider = class {
3561
+ log = $logger();
3562
+ fs = $inject(FileSystemProvider);
3563
+ /**
3564
+ * Run a shell command or binary.
3565
+ */
3566
+ async run(command, options = {}) {
3567
+ const { resolve = false, capture = false, root, env } = options;
3568
+ const cwd = root ?? process.cwd();
3569
+ this.log.debug(`Shell: ${command}`, {
3570
+ cwd,
3571
+ resolve,
3572
+ capture
3573
+ });
3574
+ let executable;
3575
+ let args;
3576
+ if (resolve) {
3577
+ const [bin, ...rest] = command.split(" ");
3578
+ executable = await this.resolveExecutable(bin, cwd);
3579
+ args = rest;
3580
+ } else [executable, ...args] = command.split(" ");
3581
+ if (capture) return this.execCapture(command, {
3582
+ cwd,
3583
+ env
3584
+ });
3585
+ return this.execInherit(executable, args, {
3586
+ cwd,
3587
+ env
3588
+ });
3589
+ }
3590
+ /**
3591
+ * Execute command with inherited stdio (streams to terminal).
3592
+ */
3593
+ async execInherit(executable, args, options) {
3594
+ const proc = spawn(executable, args, {
3595
+ stdio: "inherit",
3596
+ cwd: options.cwd,
3597
+ env: {
3598
+ ...process.env,
3599
+ ...options.env
3600
+ }
3601
+ });
3602
+ return new Promise((resolve, reject) => {
3603
+ proc.on("exit", (code) => {
3604
+ if (code === 0 || code === null) resolve("");
3605
+ else reject(new AlephaError(`Command exited with code ${code}`));
3606
+ });
3607
+ proc.on("error", reject);
3608
+ });
3609
+ }
3610
+ /**
3611
+ * Execute command and capture stdout.
3612
+ */
3613
+ execCapture(command, options) {
3614
+ return new Promise((resolve, reject) => {
3615
+ exec(command, {
3616
+ cwd: options.cwd,
3617
+ env: {
3618
+ ...process.env,
3619
+ LOG_FORMAT: "pretty",
3620
+ ...options.env
3621
+ }
3622
+ }, (err, stdout) => {
3623
+ if (err) {
3624
+ err.stdout = stdout;
3625
+ reject(err);
3626
+ } else resolve(stdout);
3627
+ });
3628
+ });
3629
+ }
3630
+ /**
3631
+ * Resolve executable path from node_modules/.bin.
3632
+ *
3633
+ * Search order:
3634
+ * 1. Local: node_modules/.bin/
3635
+ * 2. Pnpm nested: node_modules/alepha/node_modules/.bin/
3636
+ * 3. Monorepo: Walk up to 3 parent directories
3637
+ */
3638
+ async resolveExecutable(name, root) {
3639
+ const suffix = process.platform === "win32" ? ".cmd" : "";
3640
+ let execPath = await this.findExecutable(root, `node_modules/.bin/${name}${suffix}`);
3641
+ if (!execPath) execPath = await this.findExecutable(root, `node_modules/alepha/node_modules/.bin/${name}${suffix}`);
3642
+ if (!execPath) {
3643
+ let parentDir = this.fs.join(root, "..");
3644
+ for (let i = 0; i < 3; i++) {
3645
+ execPath = await this.findExecutable(parentDir, `node_modules/.bin/${name}${suffix}`);
3646
+ if (execPath) break;
3647
+ parentDir = this.fs.join(parentDir, "..");
3648
+ }
3649
+ }
3650
+ if (!execPath) throw new AlephaError(`Could not find executable for '${name}'. Make sure the package is installed.`);
3651
+ return execPath;
3652
+ }
3653
+ /**
3654
+ * Check if executable exists at path.
3655
+ */
3656
+ async findExecutable(root, relativePath) {
3657
+ const fullPath = this.fs.join(root, relativePath);
3658
+ if (await this.fs.exists(fullPath)) return fullPath;
3659
+ }
3660
+ /**
3661
+ * Check if a command is installed and available in the system PATH.
3662
+ */
3663
+ isInstalled(command) {
3664
+ return new Promise((resolve) => {
3665
+ exec(process.platform === "win32" ? `where ${command}` : `command -v ${command}`, (error) => resolve(!error));
3666
+ });
3667
+ }
3668
+ };
3669
+
3670
+ //#endregion
3671
+ //#region ../../src/system/providers/ShellProvider.ts
3672
+ /**
3673
+ * Abstract provider for executing shell commands and binaries.
3674
+ *
3675
+ * Implementations:
3676
+ * - `NodeShellProvider` - Real shell execution using Node.js child_process
3677
+ * - `MemoryShellProvider` - In-memory mock for testing
3678
+ *
3679
+ * @example
3680
+ * ```typescript
3681
+ * class MyService {
3682
+ * protected readonly shell = $inject(ShellProvider);
3683
+ *
3684
+ * async build() {
3685
+ * // Run shell command directly
3686
+ * await this.shell.run("yarn install");
3687
+ *
3688
+ * // Run local binary with resolution
3689
+ * await this.shell.run("vite build", { resolve: true });
3690
+ *
3691
+ * // Capture output
3692
+ * const output = await this.shell.run("echo hello", { capture: true });
3693
+ * }
3694
+ * }
3695
+ * ```
3696
+ */
3697
+ var ShellProvider = class {};
3698
+
3699
+ //#endregion
3700
+ //#region ../../src/system/index.ts
3701
+ /**
3702
+ * | type | quality | stability |
3703
+ * |------|---------|-----------|
3704
+ * | tooling | standard | stable |
3705
+ *
3706
+ * System-level abstractions for portable code across runtimes.
3707
+ *
3708
+ * **Features:**
3709
+ * - File system operations (read, write, exists, etc.)
3710
+ * - Shell command execution
3711
+ * - File type detection and MIME utilities
3712
+ * - Memory implementations for testing
3713
+ *
3714
+ * @module alepha.system
3715
+ */
3716
+ const AlephaSystem = $module({
3717
+ name: "alepha.system",
3718
+ primitives: [],
3719
+ services: [
3720
+ FileDetector,
3721
+ FileSystemProvider,
3722
+ MemoryFileSystemProvider,
3723
+ NodeFileSystemProvider,
3724
+ ShellProvider,
3725
+ MemoryShellProvider,
3726
+ NodeShellProvider
3727
+ ],
3728
+ register: (alepha) => alepha.with({
3729
+ optional: true,
3730
+ provide: FileSystemProvider,
3731
+ use: NodeFileSystemProvider
3732
+ }).with({
3733
+ optional: true,
3734
+ provide: ShellProvider,
3735
+ use: alepha.isTest() ? MemoryShellProvider : NodeShellProvider
3736
+ })
3737
+ });
3738
+
3739
+ //#endregion
3740
+ //#region ../../src/api/users/services/SessionService.ts
3741
+ var SessionService = class {
3742
+ alepha = $inject(Alepha);
3743
+ fsp = $inject(FileSystemProvider);
3744
+ dateTimeProvider = $inject(DateTimeProvider);
3745
+ cryptoProvider = $inject(CryptoProvider);
3746
+ log = $logger();
3747
+ realmProvider = $inject(RealmProvider);
3748
+ fileController = $client();
3749
+ auditService = $inject(AuditService);
3750
+ users(userRealmName) {
3751
+ return this.realmProvider.userRepository(userRealmName);
3752
+ }
3753
+ sessions(userRealmName) {
3754
+ return this.realmProvider.sessionRepository(userRealmName);
3755
+ }
3756
+ identities(userRealmName) {
3757
+ return this.realmProvider.identityRepository(userRealmName);
3758
+ }
3759
+ /**
3760
+ * Random delay to prevent timing attacks (50-200ms)
3761
+ * Uses cryptographically secure random number generation
3762
+ */
3763
+ randomDelay() {
3764
+ return new Promise((resolve) => setTimeout(resolve, randomInt(50, 201)));
3765
+ }
3766
+ /**
3767
+ * Validate user credentials and return the user if valid.
3768
+ */
3769
+ async login(provider, username, password, userRealmName) {
3770
+ const { settings, name } = this.realmProvider.getRealm(userRealmName);
3771
+ const isEmail = username.includes("@");
3772
+ const isPhone = /^[+\d][\d\s()-]+$/.test(username);
3773
+ const isUsername = !isEmail && !isPhone;
3774
+ const identities = this.identities(userRealmName);
3775
+ const users = this.users(userRealmName);
3776
+ await this.randomDelay();
3777
+ try {
3778
+ const where = users.createQueryWhere();
3779
+ where.realm = name;
3780
+ if (settings.usernameEnabled !== false && isUsername) {
3781
+ if (settings.usernameRegExp) {
3782
+ if (!new RegExp(settings.usernameRegExp).test(username)) {
3783
+ this.log.warn("Username does not match required format", {
3784
+ provider,
3785
+ username,
3786
+ realm: name
3787
+ });
3788
+ await this.auditService.recordAuth("login_failed", {
3789
+ userRealm: name,
3790
+ description: "Username does not match required format",
3791
+ metadata: {
3792
+ provider,
3793
+ username
3794
+ }
3795
+ });
3796
+ throw new InvalidCredentialsError();
3797
+ }
3798
+ }
3799
+ where.username = username;
3800
+ } else if (settings.emailEnabled !== false && isEmail) where.email = username;
3801
+ else if (settings.phoneEnabled === true && isPhone) where.phoneNumber = username;
3802
+ else {
3803
+ this.log.warn("Invalid login identifier format", {
3804
+ provider,
3805
+ username,
3806
+ realm: name
3807
+ });
3808
+ await this.auditService.recordAuth("login_failed", {
3809
+ userRealm: name,
3810
+ description: "Invalid login identifier format",
3811
+ metadata: {
3812
+ provider,
3813
+ username
3814
+ }
3815
+ });
3816
+ throw new InvalidCredentialsError();
3817
+ }
3818
+ const user = await users.findOne({ where }).catch(() => void 0);
3819
+ if (!user) {
3820
+ this.log.warn("User not found during login attempt", {
3821
+ provider,
3822
+ username,
3823
+ realm: name
3824
+ });
3825
+ await this.auditService.recordAuth("login_failed", {
3826
+ userRealm: name,
3827
+ description: "User not found",
3828
+ metadata: {
3829
+ provider,
3830
+ username
3831
+ }
3832
+ });
3833
+ throw new InvalidCredentialsError();
3834
+ }
3835
+ const identity = await identities.findOne({ where: {
3836
+ provider: { eq: provider },
3837
+ userId: { eq: user.id }
3838
+ } });
3839
+ const storedPassword = identity.password;
3840
+ if (!storedPassword) {
3841
+ this.log.error("Identity has no password configured", {
3842
+ provider,
3843
+ username,
3844
+ identityId: identity.id,
3845
+ realm: name
3846
+ });
3847
+ throw new InvalidCredentialsError();
3848
+ }
3849
+ if (!await this.cryptoProvider.verifyPassword(password, storedPassword)) {
3850
+ this.log.warn("Invalid password during login attempt", {
3851
+ provider,
3852
+ username,
3853
+ realm: name
3854
+ });
3855
+ await this.auditService.recordAuth("login_failed", {
3856
+ userRealm: name,
3857
+ resourceId: user.id,
3858
+ description: "Invalid password",
3859
+ metadata: {
3860
+ provider,
3861
+ username
3862
+ }
3863
+ });
3864
+ throw new InvalidCredentialsError();
3865
+ }
3866
+ await this.auditService.recordAuth("login", {
3867
+ userId: user.id,
3868
+ userEmail: user.email ?? void 0,
3869
+ userRealm: name,
3870
+ resourceId: user.id,
3871
+ description: `User logged in via ${provider}`,
3872
+ metadata: {
3873
+ provider,
3874
+ username
3875
+ }
3876
+ });
3877
+ return user;
3878
+ } catch (error) {
3879
+ if (error instanceof InvalidCredentialsError) throw error;
3880
+ this.log.warn("Error during login attempt", error);
3881
+ throw new InvalidCredentialsError();
3882
+ }
3883
+ }
3884
+ async createSession(user, expiresIn, userRealmName) {
3885
+ this.log.trace("Creating session", {
3886
+ userId: user.id,
3887
+ expiresIn
3888
+ });
3889
+ const request = this.alepha.context.get("request");
3890
+ const refreshToken = this.cryptoProvider.randomUUID();
3891
+ const expiresAt = this.dateTimeProvider.now().add(expiresIn, "seconds").toISOString();
3892
+ const session = await this.sessions(userRealmName).create({
3893
+ userId: user.id,
3894
+ expiresAt,
3895
+ ip: request?.ip,
3896
+ userAgent: request?.userAgent,
3897
+ refreshToken
3898
+ });
3899
+ this.log.info("Session created", {
3900
+ sessionId: session.id,
3901
+ userId: user.id,
3902
+ ip: request?.ip
3903
+ });
3904
+ return {
3905
+ refreshToken,
3906
+ sessionId: session.id
3907
+ };
3908
+ }
3909
+ async refreshSession(refreshToken, userRealmName) {
3910
+ this.log.trace("Refreshing session");
3911
+ const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } });
3912
+ const now = this.dateTimeProvider.now();
3913
+ const expiresAt = this.dateTimeProvider.of(session.expiresAt);
3914
+ if (this.dateTimeProvider.of(session.expiresAt) < now) {
3915
+ this.log.debug("Session expired during refresh", {
3916
+ sessionId: session.id,
3917
+ userId: session.userId
3918
+ });
3919
+ await this.sessions(userRealmName).deleteById(refreshToken);
3920
+ throw new UnauthorizedError("Session expired");
3921
+ }
3922
+ const user = await this.users(userRealmName).findOne({ where: { id: { eq: session.userId } } });
3923
+ this.log.debug("Session refreshed", {
3924
+ sessionId: session.id,
3925
+ userId: session.userId
3926
+ });
3927
+ return {
3928
+ user,
3929
+ expiresIn: expiresAt.unix() - now.unix(),
3930
+ sessionId: session.id
3931
+ };
3932
+ }
3933
+ async deleteSession(refreshToken, userRealmName) {
3934
+ this.log.trace("Deleting session");
3935
+ const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } }).catch(() => void 0);
3936
+ await this.sessions(userRealmName).deleteOne({ refreshToken });
3937
+ this.log.debug("Session deleted");
3938
+ if (session) {
3939
+ const { name } = this.realmProvider.getRealm(userRealmName);
3940
+ await this.auditService.recordAuth("logout", {
3941
+ userId: session.userId,
3942
+ userRealm: name,
3943
+ sessionId: session.id,
3944
+ description: "User logged out"
3945
+ });
3946
+ }
3947
+ }
3948
+ async link(provider, profile, userRealmName) {
3949
+ this.log.trace("Linking OAuth2 profile", {
3950
+ provider,
3951
+ profileSub: profile.sub,
3952
+ email: profile.email
3953
+ });
3954
+ const realm = this.realmProvider.getRealm(userRealmName);
3955
+ const identities = this.identities(userRealmName);
2120
3956
  const users = this.users(userRealmName);
2121
3957
  const identity = await identities.findOne({ where: {
2122
3958
  provider,
@@ -2242,6 +4078,472 @@ var SessionService = class {
2242
4078
  }
2243
4079
  };
2244
4080
 
4081
+ //#endregion
4082
+ //#region ../../src/api/keys/schemas/adminApiKeyQuerySchema.ts
4083
+ const adminApiKeyQuerySchema = t.extend(pageQuerySchema, {
4084
+ userId: t.optional(t.uuid()),
4085
+ includeRevoked: t.optional(t.boolean())
4086
+ });
4087
+
4088
+ //#endregion
4089
+ //#region ../../src/api/keys/schemas/adminApiKeyResourceSchema.ts
4090
+ const adminApiKeyResourceSchema = t.object({
4091
+ id: t.uuid(),
4092
+ userId: t.uuid(),
4093
+ name: t.string(),
4094
+ description: t.optional(t.string()),
4095
+ tokenPrefix: t.string(),
4096
+ tokenSuffix: t.string(),
4097
+ roles: t.array(t.string()),
4098
+ createdAt: t.datetime(),
4099
+ lastUsedAt: t.optional(t.datetime()),
4100
+ lastUsedIp: t.optional(t.string()),
4101
+ expiresAt: t.optional(t.datetime()),
4102
+ revokedAt: t.optional(t.datetime()),
4103
+ usageCount: t.integer()
4104
+ });
4105
+
4106
+ //#endregion
4107
+ //#region ../../src/api/keys/entities/apiKeyEntity.ts
4108
+ const apiKeyEntity = $entity({
4109
+ name: "api_keys",
4110
+ schema: t.object({
4111
+ id: db.primaryKey(t.uuid()),
4112
+ createdAt: db.createdAt(),
4113
+ updatedAt: db.updatedAt(),
4114
+ userId: t.uuid(),
4115
+ name: t.text({ maxLength: 100 }),
4116
+ description: t.optional(t.text({ maxLength: 500 })),
4117
+ tokenHash: t.string({ maxLength: 256 }),
4118
+ tokenPrefix: t.string({ maxLength: 10 }),
4119
+ tokenSuffix: t.string({ maxLength: 8 }),
4120
+ roles: db.default(t.array(t.string()), []),
4121
+ lastUsedAt: t.optional(t.datetime()),
4122
+ lastUsedIp: t.optional(t.string({ maxLength: 45 })),
4123
+ usageCount: db.default(t.integer(), 0),
4124
+ expiresAt: t.optional(t.datetime()),
4125
+ revokedAt: t.optional(t.datetime())
4126
+ }),
4127
+ indexes: [{
4128
+ columns: ["userId", "name"],
4129
+ unique: true
4130
+ }, {
4131
+ columns: ["tokenHash"],
4132
+ unique: true
4133
+ }]
4134
+ });
4135
+
4136
+ //#endregion
4137
+ //#region ../../src/api/keys/services/ApiKeyService.ts
4138
+ var ApiKeyService = class {
4139
+ alepha = $inject(Alepha);
4140
+ dateTimeProvider = $inject(DateTimeProvider);
4141
+ log = $logger();
4142
+ repo = $repository(apiKeyEntity);
4143
+ /**
4144
+ * Cache validated API keys for 15 minutes.
4145
+ */
4146
+ validationCache = $cache({
4147
+ name: "api-key-validation",
4148
+ ttl: [15, "minutes"]
4149
+ });
4150
+ /**
4151
+ * Create an issuer resolver for API key authentication.
4152
+ * Lower priority means it runs before JWT resolver.
4153
+ *
4154
+ * @param options.priority - Priority of this resolver (default: 50, JWT is 100)
4155
+ * @param options.prefix - API key prefix to match in Bearer header (default: "ak")
4156
+ */
4157
+ createResolver(options = {}) {
4158
+ const { priority = 50, prefix = "ak" } = options;
4159
+ const prefixPattern = `${prefix}_`;
4160
+ return {
4161
+ priority,
4162
+ onRequest: async (req) => {
4163
+ let token = (typeof req.url === "string" ? new URL(req.url) : req.url).searchParams.get("api_key");
4164
+ if (!token) {
4165
+ const auth = req.headers.authorization;
4166
+ if (auth?.startsWith("Bearer ")) {
4167
+ const bearerToken = auth.slice(7);
4168
+ if (bearerToken.startsWith(prefixPattern)) token = bearerToken;
4169
+ }
4170
+ }
4171
+ if (!token) return null;
4172
+ return this.validate(token);
4173
+ }
4174
+ };
4175
+ }
4176
+ /**
4177
+ * Create a new API key for a user.
4178
+ * Returns both the API key entity and the plain token (which is only available once).
4179
+ */
4180
+ async create(options) {
4181
+ const prefix = options.prefix ?? "ak";
4182
+ const token = `${prefix}_${randomBytes(24).toString("base64url")}`;
4183
+ const hash = this.hashToken(token);
4184
+ const suffix = token.slice(-8);
4185
+ const apiKey = await this.repo.create({
4186
+ userId: options.userId,
4187
+ name: options.name,
4188
+ description: options.description,
4189
+ tokenHash: hash,
4190
+ tokenPrefix: prefix,
4191
+ tokenSuffix: suffix,
4192
+ roles: options.roles,
4193
+ expiresAt: options.expiresAt?.toISOString()
4194
+ });
4195
+ this.log.info("API key created", {
4196
+ apiKeyId: apiKey.id,
4197
+ userId: options.userId,
4198
+ name: options.name
4199
+ });
4200
+ return {
4201
+ apiKey,
4202
+ token
4203
+ };
4204
+ }
4205
+ /**
4206
+ * List all non-revoked API keys for a user.
4207
+ */
4208
+ async list(userId) {
4209
+ return this.repo.findMany({
4210
+ where: {
4211
+ userId: { eq: userId },
4212
+ revokedAt: { isNull: true }
4213
+ },
4214
+ orderBy: {
4215
+ column: "createdAt",
4216
+ direction: "desc"
4217
+ }
4218
+ });
4219
+ }
4220
+ /**
4221
+ * Find all API keys with optional filtering (admin only).
4222
+ */
4223
+ async findAll(query) {
4224
+ query.sort ??= "-createdAt";
4225
+ const where = this.repo.createQueryWhere();
4226
+ if (query.userId) where.userId = { eq: query.userId };
4227
+ if (!query.includeRevoked) where.revokedAt = { isNull: true };
4228
+ return this.repo.paginate(query, { where }, { count: true });
4229
+ }
4230
+ /**
4231
+ * Get an API key by ID (admin only).
4232
+ */
4233
+ async getById(id) {
4234
+ const apiKey = await this.repo.findById(id).catch(() => null);
4235
+ if (!apiKey) throw new NotFoundError("API key not found");
4236
+ return apiKey;
4237
+ }
4238
+ /**
4239
+ * Revoke any API key (admin only).
4240
+ */
4241
+ async revokeByAdmin(id) {
4242
+ const apiKey = await this.repo.findById(id).catch(() => null);
4243
+ if (!apiKey) throw new NotFoundError("API key not found");
4244
+ if (apiKey.revokedAt) return;
4245
+ await this.validationCache.invalidate(apiKey.tokenHash);
4246
+ await this.repo.updateById(id, { revokedAt: this.dateTimeProvider.now().toISOString() });
4247
+ this.log.info("API key revoked by admin", {
4248
+ apiKeyId: id,
4249
+ userId: apiKey.userId
4250
+ });
4251
+ }
4252
+ /**
4253
+ * Revoke an API key. Only the owner can revoke their own keys.
4254
+ */
4255
+ async revoke(id, userId) {
4256
+ const apiKey = await this.repo.findById(id).catch(() => null);
4257
+ if (!apiKey) throw new NotFoundError("API key not found");
4258
+ if (apiKey.userId !== userId) throw new ForbiddenError("Not your API key");
4259
+ await this.validationCache.invalidate(apiKey.tokenHash);
4260
+ await this.repo.updateById(id, { revokedAt: this.dateTimeProvider.now().toISOString() });
4261
+ this.log.info("API key revoked", {
4262
+ apiKeyId: id,
4263
+ userId
4264
+ });
4265
+ }
4266
+ /**
4267
+ * Validate an API key token and return user info if valid.
4268
+ */
4269
+ async validate(token) {
4270
+ if (!token.includes("_")) return null;
4271
+ const hash = this.hashToken(token);
4272
+ let apiKey = await this.validationCache.get(hash);
4273
+ if (apiKey === void 0) {
4274
+ apiKey = await this.repo.findOne({ where: { tokenHash: { eq: hash } } }).catch(() => null);
4275
+ if (apiKey) await this.validationCache.set(hash, apiKey);
4276
+ }
4277
+ if (!apiKey) return null;
4278
+ if (apiKey.revokedAt) return null;
4279
+ if (apiKey.expiresAt && this.dateTimeProvider.now().isAfter(apiKey.expiresAt)) return null;
4280
+ this.updateUsage(apiKey.id).catch((error) => {
4281
+ this.log.warn("Failed to update API key usage", { error });
4282
+ });
4283
+ return {
4284
+ id: apiKey.userId,
4285
+ roles: apiKey.roles
4286
+ };
4287
+ }
4288
+ /**
4289
+ * Update usage statistics for an API key.
4290
+ */
4291
+ async updateUsage(id) {
4292
+ const request = this.alepha.context.get("request");
4293
+ await this.repo.updateById(id, {
4294
+ lastUsedAt: this.dateTimeProvider.now().toISOString(),
4295
+ lastUsedIp: request?.ip,
4296
+ usageCount: sql`${this.repo.table.usageCount} + 1`
4297
+ });
4298
+ }
4299
+ /**
4300
+ * Hash a token using SHA-256.
4301
+ */
4302
+ hashToken(token) {
4303
+ return createHash("sha256").update(token).digest("hex");
4304
+ }
4305
+ };
4306
+
4307
+ //#endregion
4308
+ //#region ../../src/api/keys/controllers/AdminApiKeyController.ts
4309
+ /**
4310
+ * REST API controller for admin API key management.
4311
+ * Admins can list, view, and revoke any API key.
4312
+ */
4313
+ var AdminApiKeyController = class {
4314
+ url = "/admin/api-keys";
4315
+ group = "admin:api-keys";
4316
+ apiKeyService = $inject(ApiKeyService);
4317
+ /**
4318
+ * Find all API keys with optional filtering.
4319
+ */
4320
+ findApiKeys = $action({
4321
+ path: this.url,
4322
+ group: this.group,
4323
+ secure: true,
4324
+ description: "Find API keys with pagination and filtering",
4325
+ schema: {
4326
+ query: adminApiKeyQuerySchema,
4327
+ response: t.page(adminApiKeyResourceSchema)
4328
+ },
4329
+ handler: ({ query }) => {
4330
+ const { userId, includeRevoked, ...pagination } = query;
4331
+ return this.apiKeyService.findAll({
4332
+ userId,
4333
+ includeRevoked,
4334
+ ...pagination
4335
+ });
4336
+ }
4337
+ });
4338
+ /**
4339
+ * Get an API key by ID.
4340
+ */
4341
+ getApiKey = $action({
4342
+ path: `${this.url}/:id`,
4343
+ group: this.group,
4344
+ secure: true,
4345
+ description: "Get an API key by ID",
4346
+ schema: {
4347
+ params: t.object({ id: t.uuid() }),
4348
+ response: adminApiKeyResourceSchema
4349
+ },
4350
+ handler: ({ params }) => this.apiKeyService.getById(params.id)
4351
+ });
4352
+ /**
4353
+ * Revoke any API key.
4354
+ */
4355
+ revokeApiKey = $action({
4356
+ method: "DELETE",
4357
+ path: `${this.url}/:id`,
4358
+ group: this.group,
4359
+ secure: true,
4360
+ description: "Revoke an API key",
4361
+ schema: {
4362
+ params: t.object({ id: t.uuid() }),
4363
+ response: okSchema
4364
+ },
4365
+ handler: async ({ params }) => {
4366
+ await this.apiKeyService.revokeByAdmin(params.id);
4367
+ return {
4368
+ ok: true,
4369
+ id: params.id
4370
+ };
4371
+ }
4372
+ });
4373
+ };
4374
+
4375
+ //#endregion
4376
+ //#region ../../src/api/keys/schemas/createApiKeyBodySchema.ts
4377
+ const createApiKeyBodySchema = t.object({
4378
+ name: t.text({
4379
+ minLength: 1,
4380
+ maxLength: 100
4381
+ }),
4382
+ description: t.optional(t.text({ maxLength: 500 })),
4383
+ expiresAt: t.optional(t.datetime())
4384
+ });
4385
+
4386
+ //#endregion
4387
+ //#region ../../src/api/keys/schemas/createApiKeyResponseSchema.ts
4388
+ const createApiKeyResponseSchema = t.object({
4389
+ id: t.uuid(),
4390
+ name: t.string(),
4391
+ token: t.string(),
4392
+ tokenSuffix: t.string(),
4393
+ roles: t.array(t.string()),
4394
+ createdAt: t.datetime(),
4395
+ expiresAt: t.optional(t.datetime())
4396
+ });
4397
+
4398
+ //#endregion
4399
+ //#region ../../src/api/keys/schemas/listApiKeyResponseSchema.ts
4400
+ const listApiKeyItemSchema = t.object({
4401
+ id: t.uuid(),
4402
+ name: t.string(),
4403
+ tokenPrefix: t.string(),
4404
+ tokenSuffix: t.string(),
4405
+ roles: t.array(t.string()),
4406
+ createdAt: t.datetime(),
4407
+ lastUsedAt: t.optional(t.datetime()),
4408
+ expiresAt: t.optional(t.datetime()),
4409
+ usageCount: t.integer()
4410
+ });
4411
+ const listApiKeyResponseSchema = t.array(listApiKeyItemSchema);
4412
+
4413
+ //#endregion
4414
+ //#region ../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts
4415
+ const revokeApiKeyParamsSchema = t.object({ id: t.uuid() });
4416
+
4417
+ //#endregion
4418
+ //#region ../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts
4419
+ const revokeApiKeyResponseSchema = t.object({ ok: t.boolean() });
4420
+
4421
+ //#endregion
4422
+ //#region ../../src/api/keys/controllers/ApiKeyController.ts
4423
+ /**
4424
+ * REST API controller for user's own API key management.
4425
+ * Users can create, list, and revoke their own API keys.
4426
+ */
4427
+ var ApiKeyController = class {
4428
+ url = "/api-keys";
4429
+ group = "api-keys";
4430
+ apiKeyService = $inject(ApiKeyService);
4431
+ /**
4432
+ * Create a new API key for the authenticated user.
4433
+ * The token is only returned once upon creation.
4434
+ */
4435
+ createApiKey = $action({
4436
+ method: "POST",
4437
+ path: this.url,
4438
+ group: this.group,
4439
+ description: "Create a new API key",
4440
+ secure: true,
4441
+ schema: {
4442
+ body: createApiKeyBodySchema,
4443
+ response: createApiKeyResponseSchema
4444
+ },
4445
+ handler: async (request) => {
4446
+ const { apiKey, token } = await this.apiKeyService.create({
4447
+ userId: request.user.id,
4448
+ name: request.body.name,
4449
+ description: request.body.description,
4450
+ roles: request.user.roles ?? [],
4451
+ expiresAt: request.body.expiresAt ? new Date(request.body.expiresAt) : void 0
4452
+ });
4453
+ return {
4454
+ id: apiKey.id,
4455
+ name: apiKey.name,
4456
+ token,
4457
+ tokenSuffix: apiKey.tokenSuffix,
4458
+ roles: apiKey.roles,
4459
+ createdAt: apiKey.createdAt,
4460
+ expiresAt: apiKey.expiresAt
4461
+ };
4462
+ }
4463
+ });
4464
+ /**
4465
+ * List all active API keys for the authenticated user.
4466
+ * Does not return the actual tokens.
4467
+ */
4468
+ listApiKeys = $action({
4469
+ path: this.url,
4470
+ group: this.group,
4471
+ description: "List your API keys",
4472
+ secure: true,
4473
+ schema: { response: listApiKeyResponseSchema },
4474
+ handler: async (request) => {
4475
+ return (await this.apiKeyService.list(request.user.id)).map((apiKey) => ({
4476
+ id: apiKey.id,
4477
+ name: apiKey.name,
4478
+ tokenPrefix: apiKey.tokenPrefix,
4479
+ tokenSuffix: apiKey.tokenSuffix,
4480
+ roles: apiKey.roles,
4481
+ createdAt: apiKey.createdAt,
4482
+ lastUsedAt: apiKey.lastUsedAt,
4483
+ expiresAt: apiKey.expiresAt,
4484
+ usageCount: apiKey.usageCount
4485
+ }));
4486
+ }
4487
+ });
4488
+ /**
4489
+ * Revoke an API key. Only the owner can revoke their own keys.
4490
+ */
4491
+ revokeApiKey = $action({
4492
+ method: "DELETE",
4493
+ path: `${this.url}/:id`,
4494
+ group: this.group,
4495
+ description: "Revoke an API key",
4496
+ secure: true,
4497
+ schema: {
4498
+ params: revokeApiKeyParamsSchema,
4499
+ response: revokeApiKeyResponseSchema
4500
+ },
4501
+ handler: async (request) => {
4502
+ await this.apiKeyService.revoke(request.params.id, request.user.id);
4503
+ return { ok: true };
4504
+ }
4505
+ });
4506
+ };
4507
+
4508
+ //#endregion
4509
+ //#region ../../src/api/keys/index.ts
4510
+ /**
4511
+ * | type | quality | stability |
4512
+ * |------|---------|--------------|
4513
+ * | backend | good | experimental |
4514
+ *
4515
+ * API key management module for programmatic access.
4516
+ *
4517
+ * **Features:**
4518
+ * - Create API keys with role snapshots
4519
+ * - List and revoke API keys
4520
+ * - 15-minute validation caching
4521
+ * - Query param (?api_key=) and Bearer header support
4522
+ *
4523
+ * **Integration:**
4524
+ * To enable API key authentication for an issuer, register the resolver:
4525
+ *
4526
+ * ```ts
4527
+ * class MyApp {
4528
+ * apiKeyService = $inject(ApiKeyService);
4529
+ * issuer = $issuer({
4530
+ * secret: env.APP_SECRET,
4531
+ * resolvers: [this.apiKeyService.createResolver()],
4532
+ * });
4533
+ * }
4534
+ * ```
4535
+ *
4536
+ * @module alepha.api.keys
4537
+ */
4538
+ const AlephaApiKeys = $module({
4539
+ name: "alepha.api.keys",
4540
+ services: [
4541
+ ApiKeyService,
4542
+ ApiKeyController,
4543
+ AdminApiKeyController
4544
+ ]
4545
+ });
4546
+
2245
4547
  //#endregion
2246
4548
  //#region ../../src/api/users/primitives/$realm.ts
2247
4549
  /**
@@ -2270,10 +4572,18 @@ const $realm = (options = {}) => {
2270
4572
  const realmRegistration = realmProvider.register(name, options);
2271
4573
  alepha.with(AlephaApiFiles);
2272
4574
  alepha.with(AlephaApiAudits);
4575
+ alepha.with(AlephaApiJobs);
4576
+ const customResolvers = [...options.issuer?.resolvers ?? []];
4577
+ if (options.apiKeys) {
4578
+ alepha.with(AlephaApiKeys);
4579
+ const apiKeyService = alepha.inject(ApiKeyService);
4580
+ customResolvers.push(apiKeyService.createResolver());
4581
+ }
2273
4582
  const realm = $issuer({
2274
4583
  ...options.issuer,
2275
4584
  name,
2276
4585
  secret: options.secret ?? securityProvider.secretKey,
4586
+ resolvers: customResolvers,
2277
4587
  roles: options.issuer?.roles ?? [{
2278
4588
  name: "admin",
2279
4589
  permissions: [{ name: "*" }]
@@ -2380,10 +4690,21 @@ const resetPasswordSchema = t.object({
2380
4690
  //#endregion
2381
4691
  //#region ../../src/api/users/index.ts
2382
4692
  /**
2383
- * Provides user management API endpoints for Alepha applications.
4693
+ * | type | quality | stability |
4694
+ * |------|---------|-----------|
4695
+ * | backend | epic | stable |
4696
+ *
4697
+ * Complete user management with multi-realm support for multi-tenant applications.
2384
4698
  *
2385
- * This module includes user CRUD operations, authentication endpoints,
2386
- * password reset functionality, and user profile management capabilities.
4699
+ * **Features:**
4700
+ * - User registration, login, and profile management
4701
+ * - Password reset workflows
4702
+ * - Email verification
4703
+ * - Session management with multiple devices
4704
+ * - Identity management (social logins, SSO)
4705
+ * - Multi-realm support for tenant isolation
4706
+ * - Credential management
4707
+ * - Entities: `users`, `identities`, `sessions`
2387
4708
  *
2388
4709
  * @module alepha.api.users
2389
4710
  */