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.
- package/README.md +68 -80
- package/dist/api/audits/index.d.ts +10 -33
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +10 -33
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +10 -3
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +10 -3
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +162 -155
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +10 -3
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +413 -0
- package/dist/api/keys/index.d.ts.map +1 -0
- package/dist/api/keys/index.js +476 -0
- package/dist/api/keys/index.js.map +1 -0
- package/dist/api/notifications/index.d.ts +10 -4
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +10 -4
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +43 -50
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +30 -37
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.d.ts +1081 -760
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +2539 -218
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +138 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +12 -4
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.d.ts +20 -40
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +31 -44
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +440 -8
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +1861 -12
- package/dist/bucket/index.js.map +1 -1
- package/dist/cache/core/index.d.ts +179 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +213 -7
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/redis/index.d.ts +1 -0
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js +4 -0
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/cli/index.d.ts +638 -5645
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2550 -368
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +203 -45
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2060 -71
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +70 -40
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +34 -13
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +90 -40
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +70 -40
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +15 -0
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +15 -0
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +323 -20
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +1857 -7
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +90 -8
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +91 -20
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +11 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +11 -4
- package/dist/lock/core/index.js.map +1 -1
- package/dist/logger/index.d.ts +17 -66
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +14 -63
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +10 -30
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +12 -35
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.browser.js +3 -3
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +39 -20
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +517 -540
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +58 -71
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +18 -10
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js +14 -6
- package/dist/queue/core/index.js.map +1 -1
- package/dist/react/auth/index.browser.js +108 -0
- package/dist/react/auth/index.browser.js.map +1 -0
- package/dist/react/auth/index.d.ts +100 -0
- package/dist/react/auth/index.d.ts.map +1 -0
- package/dist/react/auth/index.js +145 -0
- package/dist/react/auth/index.js.map +1 -0
- package/dist/react/core/index.d.ts +469 -0
- package/dist/react/core/index.d.ts.map +1 -0
- package/dist/react/core/index.js +464 -0
- package/dist/react/core/index.js.map +1 -0
- package/dist/react/form/index.d.ts +232 -0
- package/dist/react/form/index.d.ts.map +1 -0
- package/dist/react/form/index.js +432 -0
- package/dist/react/form/index.js.map +1 -0
- package/dist/react/head/index.browser.js +423 -0
- package/dist/react/head/index.browser.js.map +1 -0
- package/dist/react/head/index.d.ts +288 -0
- package/dist/react/head/index.d.ts.map +1 -0
- package/dist/react/head/index.js +465 -0
- package/dist/react/head/index.js.map +1 -0
- package/dist/react/i18n/index.d.ts +175 -0
- package/dist/react/i18n/index.d.ts.map +1 -0
- package/dist/react/i18n/index.js +224 -0
- package/dist/react/i18n/index.js.map +1 -0
- package/dist/react/router/index.browser.js +1974 -0
- package/dist/react/router/index.browser.js.map +1 -0
- package/dist/react/router/index.d.ts +1956 -0
- package/dist/react/router/index.d.ts.map +1 -0
- package/dist/react/router/index.js +4722 -0
- package/dist/react/router/index.js.map +1 -0
- package/dist/react/websocket/index.d.ts +117 -0
- package/dist/react/websocket/index.d.ts.map +1 -0
- package/dist/react/websocket/index.js +107 -0
- package/dist/react/websocket/index.js.map +1 -0
- package/dist/redis/index.bun.js +4 -0
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.d.ts +41 -44
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js +16 -25
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +11 -2
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/retry/index.js +11 -2
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +11 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +11 -2
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +140 -49
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +164 -32
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +12 -7
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +12 -7
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +7 -22
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +7 -22
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +10 -2
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +10 -2
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts +40 -16
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +7 -5
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +124 -23
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +231 -14
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +13 -23
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js +7 -21
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/health/index.d.ts +8 -2
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js +8 -2
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/helmet/index.d.ts +11 -3
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/helmet/index.js +11 -3
- package/dist/server/helmet/index.js.map +1 -1
- package/dist/server/links/index.d.ts +11 -6
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +11 -6
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +10 -3
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +10 -3
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/multipart/index.d.ts +9 -3
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/multipart/index.js +9 -3
- package/dist/server/multipart/index.js.map +1 -1
- package/dist/server/proxy/index.d.ts +8 -2
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/proxy/index.js +8 -2
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +30 -35
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js +18 -55
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.d.ts +137 -4
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/static/index.js +1853 -5
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +309 -6
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1854 -6
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +309 -7
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +1856 -7
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js +1218 -0
- package/dist/system/index.browser.js.map +1 -0
- package/dist/{file → system}/index.d.ts +343 -16
- package/dist/system/index.d.ts.map +1 -0
- package/dist/{file → system}/index.js +419 -22
- package/dist/system/index.js.map +1 -0
- package/dist/thread/index.d.ts +11 -2
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/thread/index.js +11 -2
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.d.ts +12 -5
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/core/index.js +12 -5
- package/dist/topic/core/index.js.map +1 -1
- package/dist/vite/index.d.ts +5 -6272
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +23 -10
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +12 -8
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +12 -8
- package/dist/websocket/index.js.map +1 -1
- package/package.json +82 -11
- package/src/api/audits/index.ts +10 -33
- package/src/api/files/__tests__/$bucket.spec.ts +1 -1
- package/src/api/files/controllers/AdminFileStatsController.spec.ts +1 -1
- package/src/api/files/controllers/FileController.spec.ts +1 -1
- package/src/api/files/index.ts +10 -3
- package/src/api/files/jobs/FileJobs.spec.ts +1 -1
- package/src/api/files/services/FileService.spec.ts +1 -1
- package/src/api/jobs/index.ts +10 -3
- package/src/api/keys/controllers/AdminApiKeyController.ts +75 -0
- package/src/api/keys/controllers/ApiKeyController.ts +103 -0
- package/src/api/keys/entities/apiKeyEntity.ts +41 -0
- package/src/api/keys/index.ts +49 -0
- package/src/api/keys/schemas/adminApiKeyQuerySchema.ts +7 -0
- package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +17 -0
- package/src/api/keys/schemas/createApiKeyBodySchema.ts +7 -0
- package/src/api/keys/schemas/createApiKeyResponseSchema.ts +11 -0
- package/src/api/keys/schemas/listApiKeyResponseSchema.ts +15 -0
- package/src/api/keys/schemas/revokeApiKeyParamsSchema.ts +5 -0
- package/src/api/keys/schemas/revokeApiKeyResponseSchema.ts +5 -0
- package/src/api/keys/services/ApiKeyService.spec.ts +553 -0
- package/src/api/keys/services/ApiKeyService.ts +306 -0
- package/src/api/logs/TODO.md +55 -0
- package/src/api/notifications/index.ts +10 -4
- package/src/api/parameters/index.ts +9 -30
- package/src/api/parameters/primitives/$config.ts +12 -4
- package/src/api/parameters/services/ConfigStore.ts +9 -3
- package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1035 -0
- package/src/api/users/__tests__/ApiKeys.spec.ts +401 -0
- package/src/api/users/index.ts +14 -3
- package/src/api/users/primitives/$realm.ts +33 -5
- package/src/api/users/providers/RealmProvider.ts +1 -12
- package/src/api/users/services/SessionService.ts +1 -1
- package/src/api/verifications/controllers/VerificationController.ts +2 -0
- package/src/api/verifications/index.ts +10 -4
- package/src/batch/index.ts +9 -36
- package/src/batch/primitives/$batch.ts +0 -8
- package/src/batch/providers/BatchProvider.ts +29 -2
- package/src/bucket/__tests__/shared.ts +1 -1
- package/src/bucket/index.ts +13 -6
- package/src/bucket/primitives/$bucket.ts +1 -1
- package/src/bucket/providers/LocalFileStorageProvider.ts +1 -1
- package/src/bucket/providers/MemoryFileStorageProvider.ts +1 -1
- package/src/cache/core/__tests__/shared.ts +30 -0
- package/src/cache/core/index.ts +11 -6
- package/src/cache/core/primitives/$cache.spec.ts +5 -0
- package/src/cache/core/providers/CacheProvider.ts +17 -0
- package/src/cache/core/providers/MemoryCacheProvider.ts +300 -1
- package/src/cache/redis/__tests__/cache-redis.spec.ts +5 -0
- package/src/cache/redis/providers/RedisCacheProvider.ts +9 -0
- package/src/cli/apps/AlephaCli.ts +1 -14
- package/src/cli/apps/AlephaPackageBuilderCli.ts +10 -1
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +150 -37
- package/src/cli/commands/db.ts +22 -18
- package/src/cli/commands/deploy.ts +1 -1
- package/src/cli/commands/dev.ts +1 -20
- package/src/cli/commands/gen/env.ts +5 -2
- package/src/cli/commands/gen/openapi.ts +5 -2
- package/src/cli/commands/init.spec.ts +588 -0
- package/src/cli/commands/init.ts +115 -58
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/commands/typecheck.ts +11 -0
- package/src/cli/providers/AppEntryProvider.ts +1 -1
- package/src/cli/providers/ViteBuildProvider.ts +8 -50
- package/src/cli/providers/ViteDevServerProvider.ts +35 -16
- package/src/cli/services/AlephaCliUtils.ts +52 -121
- package/src/cli/services/PackageManagerUtils.ts +129 -11
- package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
- package/src/cli/services/ProjectScaffolder.ts +148 -81
- package/src/cli/services/ViteUtils.ts +82 -0
- package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +37 -24
- package/src/cli/templates/apiAppSecurityTs.ts +11 -0
- package/src/cli/templates/apiIndexTs.ts +30 -0
- package/src/cli/templates/gitignore.ts +39 -0
- package/src/cli/{assets → templates}/mainCss.ts +11 -2
- package/src/cli/templates/mainServerTs.ts +33 -0
- package/src/cli/templates/webAppRouterTs.ts +74 -0
- package/src/cli/templates/webHelloComponentTsx.ts +30 -0
- package/src/command/helpers/Runner.spec.ts +139 -0
- package/src/command/helpers/Runner.ts +7 -22
- package/src/command/index.ts +12 -4
- package/src/command/providers/CliProvider.spec.ts +1392 -0
- package/src/command/providers/CliProvider.ts +320 -47
- package/src/core/Alepha.ts +34 -27
- package/src/core/__tests__/Alepha-start.spec.ts +4 -4
- package/src/core/helpers/jsonSchemaToTypeBox.spec.ts +771 -0
- package/src/core/helpers/jsonSchemaToTypeBox.ts +62 -10
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +20 -0
- package/src/core/providers/EventManager.spec.ts +0 -71
- package/src/core/providers/EventManager.ts +3 -15
- package/src/core/providers/Json.ts +2 -14
- package/src/datetime/index.ts +15 -0
- package/src/email/index.ts +10 -5
- package/src/email/providers/LocalEmailProvider.spec.ts +1 -1
- package/src/email/providers/LocalEmailProvider.ts +1 -1
- package/src/fake/__tests__/keyName.example.ts +1 -1
- package/src/fake/__tests__/keyName.spec.ts +5 -5
- package/src/fake/index.ts +9 -6
- package/src/fake/providers/FakeProvider.spec.ts +258 -40
- package/src/fake/providers/FakeProvider.ts +133 -19
- package/src/lock/core/index.ts +11 -4
- package/src/logger/index.ts +17 -66
- package/src/mcp/index.ts +10 -27
- package/src/mcp/transports/SseMcpTransport.ts +0 -11
- package/src/orm/__tests__/PostgresProvider.spec.ts +2 -2
- package/src/orm/index.browser.ts +2 -2
- package/src/orm/index.bun.ts +5 -3
- package/src/orm/index.ts +23 -53
- package/src/orm/providers/drivers/BunSqliteProvider.ts +5 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
- package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
- package/src/orm/services/Repository.ts +7 -3
- package/src/queue/core/index.ts +14 -6
- package/src/react/auth/__tests__/$auth.spec.ts +202 -0
- package/src/react/auth/hooks/useAuth.ts +32 -0
- package/src/react/auth/index.browser.ts +13 -0
- package/src/react/auth/index.shared.ts +2 -0
- package/src/react/auth/index.ts +48 -0
- package/src/react/auth/providers/ReactAuthProvider.ts +16 -0
- package/src/react/auth/services/ReactAuth.ts +135 -0
- package/src/react/core/__tests__/Router.spec.tsx +169 -0
- package/src/react/core/components/ClientOnly.tsx +49 -0
- package/src/react/core/components/ErrorBoundary.tsx +73 -0
- package/src/react/core/contexts/AlephaContext.ts +7 -0
- package/src/react/core/contexts/AlephaProvider.tsx +42 -0
- package/src/react/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/react/core/hooks/useAction.ts +480 -0
- package/src/react/core/hooks/useAlepha.ts +26 -0
- package/src/react/core/hooks/useClient.ts +17 -0
- package/src/react/core/hooks/useEvents.ts +51 -0
- package/src/react/core/hooks/useInject.ts +12 -0
- package/src/react/core/hooks/useStore.ts +52 -0
- package/src/react/core/index.ts +90 -0
- package/src/react/form/components/FormState.tsx +17 -0
- package/src/react/form/errors/FormValidationError.ts +18 -0
- package/src/react/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/react/form/hooks/useForm.ts +47 -0
- package/src/react/form/hooks/useFormState.ts +130 -0
- package/src/react/form/index.ts +44 -0
- package/src/react/form/services/FormModel.ts +614 -0
- package/src/react/head/helpers/SeoExpander.spec.ts +203 -0
- package/src/react/head/helpers/SeoExpander.ts +142 -0
- package/src/react/head/hooks/useHead.spec.tsx +288 -0
- package/src/react/head/hooks/useHead.ts +62 -0
- package/src/react/head/index.browser.ts +26 -0
- package/src/react/head/index.ts +44 -0
- package/src/react/head/interfaces/Head.ts +105 -0
- package/src/react/head/primitives/$head.ts +25 -0
- package/src/react/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +212 -0
- package/src/react/head/providers/HeadProvider.ts +168 -0
- package/src/react/head/providers/ServerHeadProvider.ts +31 -0
- package/src/react/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/react/i18n/components/Localize.spec.tsx +357 -0
- package/src/react/i18n/components/Localize.tsx +35 -0
- package/src/react/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/react/i18n/hooks/useI18n.ts +18 -0
- package/src/react/i18n/index.ts +41 -0
- package/src/react/i18n/primitives/$dictionary.ts +69 -0
- package/src/react/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/react/i18n/providers/I18nProvider.ts +278 -0
- package/src/react/router/__tests__/page-head-browser.browser.spec.ts +95 -0
- package/src/react/router/__tests__/page-head.spec.ts +48 -0
- package/src/react/router/__tests__/seo-head.spec.ts +125 -0
- package/src/react/router/atoms/ssrManifestAtom.ts +58 -0
- package/src/react/router/components/ErrorViewer.tsx +872 -0
- package/src/react/router/components/Link.tsx +23 -0
- package/src/react/router/components/NestedView.tsx +223 -0
- package/src/react/router/components/NotFound.tsx +30 -0
- package/src/react/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
- package/src/react/router/contexts/RouterLayerContext.ts +12 -0
- package/src/react/router/errors/Redirection.ts +28 -0
- package/src/react/router/hooks/useActive.ts +52 -0
- package/src/react/router/hooks/useQueryParams.ts +63 -0
- package/src/react/router/hooks/useRouter.ts +20 -0
- package/src/react/router/hooks/useRouterState.ts +11 -0
- package/src/react/router/index.browser.ts +45 -0
- package/src/react/router/index.shared.ts +19 -0
- package/src/react/router/index.ts +146 -0
- package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
- package/src/react/router/primitives/$page.spec.tsx +676 -0
- package/src/react/router/primitives/$page.ts +489 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +312 -0
- package/src/react/router/providers/ReactBrowserRendererProvider.ts +25 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +168 -0
- package/src/react/router/providers/ReactPageProvider.ts +726 -0
- package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
- package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
- package/src/react/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/react/router/providers/ReactServerProvider.ts +487 -0
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +542 -0
- package/src/react/router/providers/SSRManifestProvider.ts +334 -0
- package/src/react/router/services/ReactPageServerService.ts +48 -0
- package/src/react/router/services/ReactPageService.ts +27 -0
- package/src/react/router/services/ReactRouter.ts +262 -0
- package/src/react/websocket/hooks/useRoom.tsx +242 -0
- package/src/react/websocket/index.ts +7 -0
- package/src/redis/__tests__/redis.spec.ts +13 -0
- package/src/redis/index.ts +9 -25
- package/src/redis/providers/BunRedisProvider.ts +9 -0
- package/src/redis/providers/NodeRedisProvider.ts +8 -0
- package/src/redis/providers/RedisProvider.ts +16 -0
- package/src/retry/index.ts +11 -2
- package/src/router/index.ts +15 -0
- package/src/scheduler/index.ts +11 -2
- package/src/security/__tests__/BasicAuth.spec.ts +2 -0
- package/src/security/__tests__/ServerSecurityProvider.spec.ts +90 -5
- package/src/security/index.ts +15 -10
- package/src/security/interfaces/IssuerResolver.ts +27 -0
- package/src/security/primitives/$issuer.ts +55 -0
- package/src/security/providers/SecurityProvider.ts +179 -0
- package/src/security/providers/ServerBasicAuthProvider.ts +6 -2
- package/src/security/providers/ServerSecurityProvider.ts +63 -41
- package/src/server/auth/index.ts +12 -7
- package/src/server/cache/index.ts +7 -22
- package/src/server/compress/index.ts +10 -2
- package/src/server/cookies/index.ts +7 -5
- package/src/server/cookies/primitives/$cookie.ts +33 -11
- package/src/server/core/index.ts +16 -6
- package/src/server/core/interfaces/ServerRequest.ts +83 -1
- package/src/server/core/primitives/$action.spec.ts +1 -1
- package/src/server/core/primitives/$action.ts +8 -3
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- package/src/server/core/providers/NodeHttpServerProvider.ts +9 -3
- package/src/server/core/services/ServerRequestParser.spec.ts +520 -0
- package/src/server/core/services/ServerRequestParser.ts +306 -13
- package/src/server/cors/index.ts +7 -21
- package/src/server/cors/primitives/$cors.ts +6 -2
- package/src/server/health/index.ts +8 -2
- package/src/server/helmet/index.ts +11 -3
- package/src/server/links/index.ts +11 -6
- package/src/server/metrics/index.ts +10 -3
- package/src/server/multipart/index.ts +9 -3
- package/src/server/proxy/index.ts +8 -2
- package/src/server/rate-limit/index.ts +21 -25
- package/src/server/rate-limit/primitives/$rateLimit.ts +6 -2
- package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +38 -14
- package/src/server/rate-limit/providers/ServerRateLimitProvider.ts +22 -56
- package/src/server/static/index.ts +8 -2
- package/src/server/static/providers/ServerStaticProvider.ts +1 -1
- package/src/server/swagger/index.ts +9 -4
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
- package/src/sms/index.ts +9 -5
- package/src/sms/providers/LocalSmsProvider.spec.ts +1 -1
- package/src/sms/providers/LocalSmsProvider.ts +1 -1
- package/src/system/index.browser.ts +36 -0
- package/src/system/index.ts +62 -0
- package/src/system/index.workerd.ts +1 -0
- package/src/{file → system}/providers/FileSystemProvider.ts +24 -0
- package/src/{file → system}/providers/MemoryFileSystemProvider.ts +116 -3
- package/src/system/providers/MemoryShellProvider.ts +164 -0
- package/src/{file → system}/providers/NodeFileSystemProvider.spec.ts +2 -2
- package/src/{file → system}/providers/NodeFileSystemProvider.ts +47 -2
- package/src/system/providers/NodeShellProvider.ts +184 -0
- package/src/system/providers/ShellProvider.ts +74 -0
- package/src/{file → system}/services/FileDetector.spec.ts +2 -2
- package/src/thread/index.ts +11 -2
- package/src/topic/core/index.ts +12 -5
- package/src/vite/tasks/buildClient.ts +2 -7
- package/src/vite/tasks/buildServer.ts +19 -13
- package/src/vite/tasks/generateCloudflare.ts +10 -7
- package/src/vite/tasks/generateDocker.ts +4 -0
- package/src/websocket/index.ts +12 -8
- package/dist/file/index.d.ts.map +0 -1
- package/dist/file/index.js.map +0 -1
- package/src/cli/assets/apiIndexTs.ts +0 -16
- package/src/cli/assets/mainServerTs.ts +0 -24
- package/src/cli/assets/webAppRouterTs.ts +0 -16
- package/src/cli/assets/webHelloComponentTsx.ts +0 -20
- package/src/cli/providers/ViteTemplateProvider.ts +0 -27
- package/src/file/index.ts +0 -43
- /package/src/cli/{assets → templates}/apiHelloControllerTs.ts +0 -0
- /package/src/cli/{assets → templates}/biomeJson.ts +0 -0
- /package/src/cli/{assets → templates}/dummySpecTs.ts +0 -0
- /package/src/cli/{assets → templates}/editorconfig.ts +0 -0
- /package/src/cli/{assets → templates}/mainBrowserTs.ts +0 -0
- /package/src/cli/{assets → templates}/tsconfigJson.ts +0 -0
- /package/src/cli/{assets → templates}/webIndexTs.ts +0 -0
- /package/src/{file → system}/errors/FileError.ts +0 -0
- /package/src/{file → system}/services/FileDetector.ts +0 -0
package/dist/api/users/index.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import { $atom, $context, $
|
|
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 {
|
|
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/
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
-
|
|
1918
|
-
|
|
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
|
-
|
|
1921
|
-
|
|
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
|
-
*
|
|
1925
|
-
* Uses cryptographically secure random number generation
|
|
2037
|
+
* Remove a file or directory from memory.
|
|
1926
2038
|
*/
|
|
1927
|
-
|
|
1928
|
-
|
|
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
|
-
*
|
|
2053
|
+
* Copy a file or directory in memory.
|
|
1932
2054
|
*/
|
|
1933
|
-
async
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
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
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
2386
|
-
*
|
|
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
|
*/
|