alepha 0.20.2 → 0.20.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 +0 -1
- package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/assets/swagger-ui/swagger-ui.css +1 -1
- package/dist/api/audits/index.browser.js +49 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +49 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +16 -75
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +1 -10
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.browser.js +37 -0
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +4 -65
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +207 -5184
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +2 -4
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/bucket/index.js +5 -1
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +5 -1
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +217 -11647
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +706 -42
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.js +7 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +41 -64
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +47 -0
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.js +15 -0
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.js +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +2 -8
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/smtp/index.js +2 -10522
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.d.ts +4 -8085
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +3 -33554
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js +32 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +1 -361
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +14 -406
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +96 -5117
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +23 -419
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +17 -20
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +2 -613
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +17 -20
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js +22 -17
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js +78 -2
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +22 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +102 -4
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.d.ts +1 -411
- package/dist/react/testing/index.d.ts.map +1 -1
- package/dist/react/testing/index.js +13 -12293
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.js +3 -0
- package/dist/react/ui/index.js.map +1 -1
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -83
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -391
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +2 -391
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +2 -325
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +3 -1362
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1 -1054
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -1224
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +1 -4
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +19 -4
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +1 -514
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4 -4356
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.js +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/websocket/index.browser.js +21 -0
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.js +21 -0
- package/dist/websocket/index.js.map +1 -1
- package/package.json +18 -15
- package/src/api/files/__tests__/FileController.spec.ts +1 -1
- package/src/api/jobs/__tests__/$job.spec.ts +5 -1
- package/src/api/users/schemas/userQuerySchema.ts +0 -1
- package/src/api/users/services/UserService.ts +1 -5
- package/src/api/verifications/__tests__/CodeVerification.spec.ts +14 -0
- package/src/api/verifications/__tests__/LinkVerification.spec.ts +14 -0
- package/src/api/verifications/services/VerificationService.ts +1 -0
- package/src/cli/core/__tests__/init.spec.ts +208 -0
- package/src/cli/core/commands/init.ts +12 -0
- package/src/cli/core/services/PackageManagerUtils.ts +23 -6
- package/src/cli/core/services/ProjectScaffolder.ts +298 -20
- package/src/cli/core/tasks/BuildDockerTask.ts +9 -10
- package/src/cli/core/tasks/BuildServerTask.ts +8 -0
- package/src/cli/core/templates/apiIndexTs.ts +23 -1
- package/src/cli/core/templates/componentsJsonTs.ts +39 -0
- package/src/cli/core/templates/mainCss.ts +1 -0
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +77 -0
- package/src/cli/core/templates/saasAdminPagesTsx.ts +26 -0
- package/src/cli/core/templates/saasAuthLayoutTsx.ts +20 -0
- package/src/cli/core/templates/saasAuthPagesTsx.ts +62 -0
- package/src/cli/core/templates/saasRealmProviderTs.ts +46 -0
- package/src/cli/core/templates/webAppRouterTs.ts +104 -1
- package/src/cli/core/templates/webIndexTs.ts +23 -1
- package/src/cli/platform/__tests__/SecretsCommand.spec.ts +2 -0
- package/src/command/providers/CliProvider.ts +1 -1
- package/src/core/interfaces/Service.ts +3 -1
- package/src/core/providers/TypeProvider.ts +1 -1
- package/src/logger/services/Logger.ts +1 -1
- package/src/mcp/__tests__/$resource.spec.ts +1 -1
- package/src/mcp/__tests__/$tool.spec.ts +1 -1
- package/src/mcp/__tests__/McpServerProvider.spec.ts +1 -1
- package/src/orm/__tests__/$repository-tests.ts +1 -0
- package/src/orm/__tests__/orm-next-tests.ts +2 -67
- package/src/orm/__tests__/orm-next.spec.ts +0 -21
- package/src/orm/core/index.shared.ts +0 -2
- package/src/orm/core/index.ts +1 -2
- package/src/orm/core/primitives/$repository.ts +3 -6
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +0 -5
- package/src/orm/core/providers/drivers/NodeSqliteProvider.ts +11 -13
- package/src/orm/core/services/ModelBuilder.ts +1 -13
- package/src/orm/core/services/Repository.ts +1 -42
- package/src/orm/core/services/SqliteModelBuilder.ts +2 -33
- package/src/orm/postgres/services/PostgresModelBuilder.ts +10 -45
- package/src/react/intro/components/GettingStartedAuthSlide.tsx +11 -4
- package/src/react/router/__tests__/ReactBrowserProvider.browser.spec.ts +213 -2
- package/src/react/router/providers/ReactBrowserProvider.ts +73 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +1 -1
- package/src/react/router/providers/ReactPreloadProvider.ts +1 -1
- package/src/react/router/providers/ReactServerProvider.ts +1 -0
- package/src/scheduler/providers/CronProvider.ts +1 -1
- package/src/security/primitives/$basicAuth.ts +1 -1
- package/src/server/auth/providers/ServerAuthProvider.ts +5 -1
- package/src/server/core/interfaces/ServerRequest.ts +1 -0
- package/src/server/core/providers/ServerProvider.ts +1 -1
- package/src/server/core/providers/ServerRouterProvider.ts +2 -2
- package/src/server/core/services/HttpClient.ts +1 -1
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
- package/dist/react/testing/chunk-DBEY4PJZ.js +0 -16
- package/src/orm/core/__tests__/parseQueryString.spec.ts +0 -196
- package/src/orm/core/helpers/parseQueryString.ts +0 -502
- package/src/orm/core/primitives/$view.ts +0 -88
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/api/verifications/entities/verifications.ts","../../../src/api/verifications/schemas/verificationSettingsSchema.ts","../../../src/api/verifications/parameters/VerificationParameters.ts","../../../src/api/verifications/schemas/requestVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/validateVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/verificationTypeEnumSchema.ts","../../../src/api/verifications/services/VerificationService.ts","../../../src/api/verifications/controllers/VerificationController.ts","../../../src/api/verifications/jobs/VerificationJobs.ts","../../../src/api/verifications/index.ts"],"mappings":";;;;;;;;;cAKa,aAAA,EAAa,aAAA,CAAA,eAAA,UAAA,OAAA;gDAwCxB,QAAA,CAAA,OAAA;;;;;;;;;;cAEW,wBAAA,WAAwB,OAAA;gDAAuB,QAAA,CAAA,OAAA;;;;;;;;;;cAC/C,8BAAA,EAA8B,aAAA,CAAA,aAAA,UAAA,OAAA;gDAA6B,QAAA,CAAA,OAAA;;;;;;;;;;KAC5D,kBAAA,GAAqB,MAAA,QAAc,aAAA,CAAc,MAAA;;;cC9ChD,0BAAA,WAA0B,OAAA;;iBAsErC,QAAA,CAAA,QAAA;;;;;;;;;;;;;;KAEU,oBAAA,GAAuB,MAAA,QAAc,0BAAA;;;;;;cChEpC,mBAAA,EAAmB,QAAA,CAAA,IAAA,UAAA,OAAA;;iBAmB9B,QAAA,CAAA,QAAA;;;;;;;;;;;;;;KAEU,mBAAA,GAAsB,MAAA,QAAc,mBAAA,CAAoB,MAAA;AAAA;EAAA,UAGxD,KAAA;IAAA,CACP,mBAAA,CAAoB,GAAA,GAAM,mBAAA;EAAA;AAAA;AAAA,cAMlB,sBAAA;EAAA,mBACQ,OAAA,EAAO,QAAA;;;;;;;;;;;;;;;;EAEnB,GAAA,iBAAoB,oBAAA,CAAA,CACzB,GAAA,EAAK,CAAA,GACJ,oBAAA,CAAqB,CAAA;AAAA;;;cC5Cb,qCAAA,WAAqC,OAAA;SAgBhD,QAAA,CAAA,OAAA;;;;;KAEU,2BAAA,GAA8B,MAAA,QACjC,qCAAA;;;cCnBI,sCAAA,WAAsC,OAAA;MASjD,QAAA,CAAA,QAAA;;;KAEU,gCAAA,GAAmC,MAAA,QACtC,sCAAA;;;cCZI,0BAAA,EAAqD,QAAA,CAA3B,OAAA;AAAA,KAC3B,oBAAA,GAAuB,MAAA,QAAc,0BAAA;;;cCWpC,mBAAA;EAAA,mBACQ,GAAA,EADW,gBAAA,CACR,MAAA;EAAA,mBACH,gBAAA,EAAgB,gBAAA;EAAA,mBAChB,sBAAA,EAAsB,sBAAA;EAAA,mBACtB,sBAAA,EAAsB,aAAA,CAAA,UAAA,UAAA,OAAA;kDADA,QAAA,CAAA,OAAA;;;;;;;;;;EAG5B,WAAA,CACX,KAAA,EAAO,iBAAA,GACN,OAAA,CAAQ,kBAAA;EAmCJ,kBAAA,CAAmB,KAAA,EAAO,iBAAA,GAAiB,OAAA,eAAA,QAAA,UAAA,OAAA;kDAAA,QAAA,CAAA,OAAA;;;;;;;;;;kDAAA,QAAA,CAAA,OAAA;;;;;;;;;;ENrD1B;;;;;EM+EX,kBAAA,CACX,KAAA,EAAO,iBAAA,GACN,OAAA,CAAQ,2BAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/api/verifications/entities/verifications.ts","../../../src/api/verifications/schemas/verificationSettingsSchema.ts","../../../src/api/verifications/parameters/VerificationParameters.ts","../../../src/api/verifications/schemas/requestVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/validateVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/verificationTypeEnumSchema.ts","../../../src/api/verifications/services/VerificationService.ts","../../../src/api/verifications/controllers/VerificationController.ts","../../../src/api/verifications/jobs/VerificationJobs.ts","../../../src/api/verifications/index.ts"],"mappings":";;;;;;;;;cAKa,aAAA,EAAa,aAAA,CAAA,eAAA,UAAA,OAAA;gDAwCxB,QAAA,CAAA,OAAA;;;;;;;;;;cAEW,wBAAA,WAAwB,OAAA;gDAAuB,QAAA,CAAA,OAAA;;;;;;;;;;cAC/C,8BAAA,EAA8B,aAAA,CAAA,aAAA,UAAA,OAAA;gDAA6B,QAAA,CAAA,OAAA;;;;;;;;;;KAC5D,kBAAA,GAAqB,MAAA,QAAc,aAAA,CAAc,MAAA;;;cC9ChD,0BAAA,WAA0B,OAAA;;iBAsErC,QAAA,CAAA,QAAA;;;;;;;;;;;;;;KAEU,oBAAA,GAAuB,MAAA,QAAc,0BAAA;;;;;;cChEpC,mBAAA,EAAmB,QAAA,CAAA,IAAA,UAAA,OAAA;;iBAmB9B,QAAA,CAAA,QAAA;;;;;;;;;;;;;;KAEU,mBAAA,GAAsB,MAAA,QAAc,mBAAA,CAAoB,MAAA;AAAA;EAAA,UAGxD,KAAA;IAAA,CACP,mBAAA,CAAoB,GAAA,GAAM,mBAAA;EAAA;AAAA;AAAA,cAMlB,sBAAA;EAAA,mBACQ,OAAA,EAAO,QAAA;;;;;;;;;;;;;;;;EAEnB,GAAA,iBAAoB,oBAAA,CAAA,CACzB,GAAA,EAAK,CAAA,GACJ,oBAAA,CAAqB,CAAA;AAAA;;;cC5Cb,qCAAA,WAAqC,OAAA;SAgBhD,QAAA,CAAA,OAAA;;;;;KAEU,2BAAA,GAA8B,MAAA,QACjC,qCAAA;;;cCnBI,sCAAA,WAAsC,OAAA;MASjD,QAAA,CAAA,QAAA;;;KAEU,gCAAA,GAAmC,MAAA,QACtC,sCAAA;;;cCZI,0BAAA,EAAqD,QAAA,CAA3B,OAAA;AAAA,KAC3B,oBAAA,GAAuB,MAAA,QAAc,0BAAA;;;cCWpC,mBAAA;EAAA,mBACQ,GAAA,EADW,gBAAA,CACR,MAAA;EAAA,mBACH,gBAAA,EAAgB,gBAAA;EAAA,mBAChB,sBAAA,EAAsB,sBAAA;EAAA,mBACtB,sBAAA,EAAsB,aAAA,CAAA,UAAA,UAAA,OAAA;kDADA,QAAA,CAAA,OAAA;;;;;;;;;;EAG5B,WAAA,CACX,KAAA,EAAO,iBAAA,GACN,OAAA,CAAQ,kBAAA;EAmCJ,kBAAA,CAAmB,KAAA,EAAO,iBAAA,GAAiB,OAAA,eAAA,QAAA,UAAA,OAAA;kDAAA,QAAA,CAAA,OAAA;;;;;;;;;;kDAAA,QAAA,CAAA,OAAA;;;;;;;;;;ENrD1B;;;;;EM+EX,kBAAA,CACX,KAAA,EAAO,iBAAA,GACN,OAAA,CAAQ,2BAAA;EAoEE,UAAA,CACX,KAAA,EAAO,iBAAA,EACP,IAAA,WACC,OAAA,CAAQ,gCAAA;EA+EJ,QAAA,CAAS,IAAA;EAIT,aAAA,CAAc,IAAA,EAAM,oBAAA;AAAA;AAAA,UAcZ,iBAAA;EACf,IAAA,EAAM,oBAAA;EACN,MAAA;AAAA;;;cCzPW,sBAAA;EAAA,mBACQ,mBAAA,EAAmB,mBAAA;EAAA,SAEtB,GAAA;EAAA,SACA,KAAA;EAAA,SAEA,uBAAA,mBAAuB,iBAAA;;YALD,QAAA,CAAA,OAAA;IAAA;;;;;;;;;;;WA0BtB,wBAAA,mBAAwB,iBAAA;;YArBD,QAAA,CAAA,OAAA;IAAA;;;;;;;;;;;;;cCN5B,gBAAA;EAAA,mBACQ,sBAAA,EAAsB,aAAA,CAAA,UAAA,UAAA,OAAA;kDADd,QAAA,CAAA,OAAA;;;;;;;;;;qBAER,sBAAA,EAAsB,sBAAA;EAAA,mBACtB,gBAAA,EAAgB,gBAAA;EAAA,SAEnB,YAAA,EAFmB,mBAAA,CAEP,kBAAA;AAAA;;;;ARP9B;;;;;;;;;;cSyBa,qBAAA,EAAqB,QAAA,CAAA,OAAA,CAQhC,QAAA,CARgC,MAAA"}
|
|
@@ -218,7 +218,8 @@ var VerificationService = class {
|
|
|
218
218
|
const verification = await this.verificationRepository.create({
|
|
219
219
|
type: entry.type,
|
|
220
220
|
target: entry.target,
|
|
221
|
-
code: this.hashCode(token)
|
|
221
|
+
code: this.hashCode(token),
|
|
222
|
+
createdAt: this.dateTimeProvider.nowISOString()
|
|
222
223
|
});
|
|
223
224
|
this.log.info("Verification created", {
|
|
224
225
|
id: verification.id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/verifications/schemas/requestVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/validateVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/verificationTypeEnumSchema.ts","../../../src/api/verifications/entities/verifications.ts","../../../src/api/verifications/schemas/verificationSettingsSchema.ts","../../../src/api/verifications/parameters/VerificationParameters.ts","../../../src/api/verifications/services/VerificationService.ts","../../../src/api/verifications/controllers/VerificationController.ts","../../../src/api/verifications/jobs/VerificationJobs.ts","../../../src/api/verifications/index.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const requestVerificationCodeResponseSchema = t.object({\n token: t.string({\n description:\n \"The verification token (6-digit code for phone, UUID for email). The caller should send this to the user via their preferred notification method.\",\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before your verification token expires.\",\n }),\n verificationCooldown: t.integer({\n description:\n \"Cooldown period in seconds before you can request another verification.\",\n }),\n maxVerificationAttempts: t.integer({\n description:\n \"Maximum number of verification attempts allowed before the token is locked.\",\n }),\n});\n\nexport type RequestVerificationResponse = Static<\n typeof requestVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const validateVerificationCodeResponseSchema = t.object({\n ok: t.boolean({\n description: \"Indicates whether the verification was successful.\",\n }),\n alreadyVerified: t.optional(\n t.boolean({\n description: \"Indicates whether the target was already verified.\",\n }),\n ),\n});\n\nexport type ValidateVerificationCodeResponse = Static<\n typeof validateVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationTypeEnumSchema = t.enum([\"code\", \"link\"]);\nexport type VerificationTypeEnum = Static<typeof verificationTypeEnumSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport const verifications = $entity({\n name: \"verification\",\n schema: t.object({\n id: db.primaryKey(t.bigint()),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n version: db.version(),\n\n type: verificationTypeEnumSchema,\n\n target: t.text({\n description: \"Can be a phone (E.164 format) or email address\",\n }),\n\n code: t.text({\n description: \"Hashed verification token (n-digit code or UUID)\",\n }),\n\n verifiedAt: t.optional(\n t.datetime({\n description: \"When it was successfully verified\",\n }),\n ),\n\n attempts: db.default(\n t.integer({\n description: \"Number of failed attempts (to prevent brute-force)\",\n }),\n 0,\n ),\n }),\n indexes: [\n \"createdAt\",\n {\n columns: [\"target\", \"code\"],\n },\n ],\n});\n\nexport const verificationEntitySchema = verifications.schema;\nexport const verificationEntityInsertSchema = verifications.insertSchema;\nexport type VerificationEntity = Static<typeof verifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationSettingsSchema = t.object({\n code: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeLength: t.integer({\n description: \"Length of the verification code.\",\n minimum: 4,\n maximum: 12,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification code expires.\",\n minimum: 60, // 1 minute\n maximum: 3600, // 1 hour\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to code verifications.\",\n },\n ),\n link: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification token expires.\",\n minimum: 60, // 1 minute\n maximum: 7200, // 2 hours\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to link verifications.\",\n },\n ),\n purgeDays: t.integer({\n description:\n \"Number of days after which expired verifications are automatically deleted. Set to 0 to disable auto-deletion.\",\n minimum: 0,\n maximum: 365,\n }),\n});\n\nexport type VerificationSettings = Static<typeof verificationSettingsSchema>;\n","import { $atom, $state, type Static } from \"alepha\";\nimport {\n type VerificationSettings,\n verificationSettingsSchema,\n} from \"../schemas/verificationSettingsSchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Verification settings configuration atom\n */\nexport const verificationOptions = $atom({\n name: \"alepha.api.verifications.options\",\n schema: verificationSettingsSchema,\n default: {\n code: {\n maxAttempts: 5,\n codeLength: 6,\n codeExpiration: 300, // 5 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n link: {\n maxAttempts: 3, // Lower since UUIDs are harder to guess\n codeExpiration: 1800, // 30 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n purgeDays: 1,\n },\n});\n\nexport type VerificationOptions = Static<typeof verificationOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [verificationOptions.key]: VerificationOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class VerificationParameters {\n protected readonly options = $state(verificationOptions);\n\n public get<K extends keyof VerificationSettings>(\n key: K,\n ): VerificationSettings[K] {\n return this.options[key];\n }\n}\n","import { createHash, randomInt, randomUUID } from \"node:crypto\";\nimport { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { BadRequestError, NotFoundError } from \"alepha/server\";\nimport {\n type VerificationEntity,\n verifications,\n} from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\nimport type { RequestVerificationResponse } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport type { ValidateVerificationCodeResponse } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport type { VerificationTypeEnum } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport class VerificationService {\n protected readonly log = $logger();\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly verificationRepository = $repository(verifications);\n\n public async findByEntry(\n entry: VerificationEntry,\n ): Promise<VerificationEntity> {\n this.log.trace(\"Finding verification by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n const results = await this.verificationRepository.findMany({\n limit: 1, // only need the most recent entry\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n },\n });\n\n if (results.length === 0) {\n this.log.debug(\"Verification entry not found\", {\n type: entry.type,\n target: entry.target,\n });\n throw new NotFoundError(\"Verification entry not found\");\n }\n\n this.log.debug(\"Verification entry found\", {\n id: results[0].id,\n type: entry.type,\n target: entry.target,\n });\n\n return results[0];\n }\n\n public findRecentsByEntry(entry: VerificationEntry) {\n this.log.trace(\"Finding recent verifications by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n return this.verificationRepository.findMany({\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n createdAt: {\n gte: this.dateTimeProvider.now().startOf(\"day\").toISOString(),\n },\n },\n });\n }\n\n /**\n * Creates a verification entry and returns the token.\n * The caller is responsible for sending notifications with the token.\n * This allows for context-specific notifications (e.g., password reset vs email verification).\n */\n public async createVerification(\n entry: VerificationEntry,\n ): Promise<RequestVerificationResponse> {\n this.log.trace(\"Creating verification\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const recents = await this.findRecentsByEntry(entry);\n if (recents.length >= settings.limitPerDay) {\n this.log.warn(\"Daily verification limit reached\", {\n type: entry.type,\n target: entry.target,\n limit: settings.limitPerDay,\n count: recents.length,\n });\n throw new BadRequestError(\n `Maximum number of verification requests per day reached (${settings.limitPerDay})`,\n );\n }\n\n const existingVerification = recents[0];\n if (existingVerification) {\n const nowSec = this.dateTimeProvider.now().unix();\n const createdAtSec = this.dateTimeProvider\n .of(existingVerification.createdAt)\n .unix();\n\n const diffSec = nowSec - createdAtSec;\n if (diffSec < settings.verificationCooldown) {\n const remainingCooldown = Math.floor(\n settings.verificationCooldown - diffSec,\n );\n this.log.debug(\"Verification on cooldown\", {\n type: entry.type,\n target: entry.target,\n remainingSeconds: remainingCooldown,\n });\n throw new BadRequestError(\n `Verification is on cooldown for ${remainingCooldown} seconds`,\n );\n }\n }\n\n const token = this.generateToken(entry.type);\n\n const verification = await this.verificationRepository.create({\n type: entry.type,\n target: entry.target,\n code: this.hashCode(token),\n });\n\n this.log.info(\"Verification created\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n expiresInSeconds: settings.codeExpiration,\n });\n\n return {\n token,\n codeExpiration: settings.codeExpiration,\n verificationCooldown: settings.verificationCooldown,\n maxVerificationAttempts: settings.maxAttempts,\n };\n }\n\n public async verifyCode(\n entry: VerificationEntry,\n code: string,\n ): Promise<ValidateVerificationCodeResponse> {\n this.log.trace(\"Verifying code\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const verification = await this.findByEntry(entry);\n if (verification.verifiedAt) {\n this.log.debug(\"Verification already verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n verifiedAt: verification.verifiedAt,\n });\n return { ok: true, alreadyVerified: true };\n }\n\n // DO NOT DELETE THE VERIFICATION WHEN IT IS REJECTED,\n // or we won't be able to cooldown the verification\n\n const now = this.dateTimeProvider.now();\n const expirationDate = this.dateTimeProvider\n .of(verification.createdAt)\n .add(settings.codeExpiration, \"seconds\");\n\n if (now > expirationDate) {\n this.log.warn(\"Verification code expired\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n createdAt: verification.createdAt,\n expiredAt: expirationDate.toISOString(),\n });\n throw new BadRequestError(\"Verification code has expired\");\n }\n\n if (verification.attempts >= settings.maxAttempts) {\n this.log.warn(\"Verification locked due to max attempts\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: verification.attempts,\n maxAttempts: settings.maxAttempts,\n });\n throw new BadRequestError(\n \"Maximum number of attempts reached - verification is locked\",\n );\n }\n\n if (verification.code !== this.hashCode(code)) {\n const newAttempts = verification.attempts + 1;\n this.log.warn(\"Invalid verification code\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: newAttempts,\n maxAttempts: settings.maxAttempts,\n });\n await this.verificationRepository.updateById(verification.id, {\n attempts: newAttempts,\n });\n throw new BadRequestError(\"Invalid verification code\");\n }\n\n await this.verificationRepository.updateById(verification.id, {\n verifiedAt: this.dateTimeProvider.nowISOString(),\n });\n\n this.log.info(\"Verification code verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n });\n\n return { ok: true };\n }\n\n public hashCode(code: string): string {\n return createHash(\"sha256\").update(code).digest(\"hex\");\n }\n\n public generateToken(type: VerificationTypeEnum): string {\n if (type === \"code\") {\n const settings = this.verificationParameters.get(\"code\");\n return randomInt(0, 1_000_000)\n .toString()\n .padStart(settings.codeLength, \"0\");\n } else if (type === \"link\") {\n return randomUUID();\n }\n\n throw new BadRequestError(`Invalid verification type: ${type}`);\n }\n}\n\nexport interface VerificationEntry {\n type: VerificationTypeEnum;\n target: string;\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { requestVerificationCodeResponseSchema } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport { validateVerificationCodeResponseSchema } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\nimport { VerificationService } from \"../services/VerificationService.ts\";\n\nexport class VerificationController {\n protected readonly verificationService = $inject(VerificationService);\n\n public readonly url = \"/verifications\";\n public readonly group = \"verifications\";\n\n public readonly requestVerificationCode = $action({\n path: `${this.url}/:type`,\n group: this.group,\n method: \"POST\",\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n }),\n response: requestVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return await this.verificationService.createVerification({\n type: params.type,\n target: body.target,\n });\n },\n });\n\n public readonly validateVerificationCode = $action({\n path: `${this.url}/:type/validate`,\n group: this.group,\n method: \"POST\",\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n token: t.text({\n description:\n \"The verification token (6-digit code for phone, UUID for email).\",\n }),\n }),\n response: validateVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return this.verificationService.verifyCode(\n {\n type: params.type,\n target: body.target,\n },\n body.token,\n );\n },\n });\n}\n","import { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $repository } from \"alepha/orm\";\nimport { $scheduler } from \"alepha/scheduler\";\nimport { verifications } from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\n\nexport class VerificationJobs {\n protected readonly verificationRepository = $repository(verifications);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n\n public readonly cleanExpired = $scheduler({\n name: \"api:verifications:cleanExpired\",\n cron: \"0 * * * *\", // Hourly at minute 0\n description: \"Clean expired verifications\",\n handler: async () => {\n const purgeDays = this.verificationParameters.get(\"purgeDays\");\n if (purgeDays <= 0) {\n return; // Auto deletion is disabled\n }\n\n const dayMs = 24 * 60 * 60 * 1000;\n const purgeThreshold =\n this.dateTimeProvider.nowMillis() - purgeDays * dayMs;\n\n await this.verificationRepository.deleteMany({\n createdAt: {\n lt: this.dateTimeProvider.of(purgeThreshold).toISOString(),\n },\n });\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { VerificationController } from \"./controllers/VerificationController.ts\";\nimport { VerificationJobs } from \"./jobs/VerificationJobs.ts\";\nimport { VerificationParameters } from \"./parameters/VerificationParameters.ts\";\nimport { VerificationService } from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/VerificationController.ts\";\nexport * from \"./entities/verifications.ts\";\nexport * from \"./jobs/VerificationJobs.ts\";\nexport * from \"./parameters/VerificationParameters.ts\";\nexport * from \"./schemas/requestVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/validateVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/verificationTypeEnumSchema.ts\";\nexport * from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Email and phone verification workflows.\n *\n * **Features:**\n * - Verification token generation\n * - Verification code sending\n * - Verification completion tracking\n * - Resend functionality\n *\n * @module alepha.api.verifications\n */\nexport const AlephaApiVerification = $module({\n name: \"alepha.api.verifications\",\n services: [\n VerificationController,\n VerificationJobs,\n VerificationService,\n VerificationParameters,\n ],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/verifications/schemas/requestVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/validateVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/verificationTypeEnumSchema.ts","../../../src/api/verifications/entities/verifications.ts","../../../src/api/verifications/schemas/verificationSettingsSchema.ts","../../../src/api/verifications/parameters/VerificationParameters.ts","../../../src/api/verifications/services/VerificationService.ts","../../../src/api/verifications/controllers/VerificationController.ts","../../../src/api/verifications/jobs/VerificationJobs.ts","../../../src/api/verifications/index.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const requestVerificationCodeResponseSchema = t.object({\n token: t.string({\n description:\n \"The verification token (6-digit code for phone, UUID for email). The caller should send this to the user via their preferred notification method.\",\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before your verification token expires.\",\n }),\n verificationCooldown: t.integer({\n description:\n \"Cooldown period in seconds before you can request another verification.\",\n }),\n maxVerificationAttempts: t.integer({\n description:\n \"Maximum number of verification attempts allowed before the token is locked.\",\n }),\n});\n\nexport type RequestVerificationResponse = Static<\n typeof requestVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const validateVerificationCodeResponseSchema = t.object({\n ok: t.boolean({\n description: \"Indicates whether the verification was successful.\",\n }),\n alreadyVerified: t.optional(\n t.boolean({\n description: \"Indicates whether the target was already verified.\",\n }),\n ),\n});\n\nexport type ValidateVerificationCodeResponse = Static<\n typeof validateVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationTypeEnumSchema = t.enum([\"code\", \"link\"]);\nexport type VerificationTypeEnum = Static<typeof verificationTypeEnumSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport const verifications = $entity({\n name: \"verification\",\n schema: t.object({\n id: db.primaryKey(t.bigint()),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n version: db.version(),\n\n type: verificationTypeEnumSchema,\n\n target: t.text({\n description: \"Can be a phone (E.164 format) or email address\",\n }),\n\n code: t.text({\n description: \"Hashed verification token (n-digit code or UUID)\",\n }),\n\n verifiedAt: t.optional(\n t.datetime({\n description: \"When it was successfully verified\",\n }),\n ),\n\n attempts: db.default(\n t.integer({\n description: \"Number of failed attempts (to prevent brute-force)\",\n }),\n 0,\n ),\n }),\n indexes: [\n \"createdAt\",\n {\n columns: [\"target\", \"code\"],\n },\n ],\n});\n\nexport const verificationEntitySchema = verifications.schema;\nexport const verificationEntityInsertSchema = verifications.insertSchema;\nexport type VerificationEntity = Static<typeof verifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationSettingsSchema = t.object({\n code: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeLength: t.integer({\n description: \"Length of the verification code.\",\n minimum: 4,\n maximum: 12,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification code expires.\",\n minimum: 60, // 1 minute\n maximum: 3600, // 1 hour\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to code verifications.\",\n },\n ),\n link: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification token expires.\",\n minimum: 60, // 1 minute\n maximum: 7200, // 2 hours\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to link verifications.\",\n },\n ),\n purgeDays: t.integer({\n description:\n \"Number of days after which expired verifications are automatically deleted. Set to 0 to disable auto-deletion.\",\n minimum: 0,\n maximum: 365,\n }),\n});\n\nexport type VerificationSettings = Static<typeof verificationSettingsSchema>;\n","import { $atom, $state, type Static } from \"alepha\";\nimport {\n type VerificationSettings,\n verificationSettingsSchema,\n} from \"../schemas/verificationSettingsSchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Verification settings configuration atom\n */\nexport const verificationOptions = $atom({\n name: \"alepha.api.verifications.options\",\n schema: verificationSettingsSchema,\n default: {\n code: {\n maxAttempts: 5,\n codeLength: 6,\n codeExpiration: 300, // 5 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n link: {\n maxAttempts: 3, // Lower since UUIDs are harder to guess\n codeExpiration: 1800, // 30 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n purgeDays: 1,\n },\n});\n\nexport type VerificationOptions = Static<typeof verificationOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [verificationOptions.key]: VerificationOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class VerificationParameters {\n protected readonly options = $state(verificationOptions);\n\n public get<K extends keyof VerificationSettings>(\n key: K,\n ): VerificationSettings[K] {\n return this.options[key];\n }\n}\n","import { createHash, randomInt, randomUUID } from \"node:crypto\";\nimport { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { BadRequestError, NotFoundError } from \"alepha/server\";\nimport {\n type VerificationEntity,\n verifications,\n} from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\nimport type { RequestVerificationResponse } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport type { ValidateVerificationCodeResponse } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport type { VerificationTypeEnum } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport class VerificationService {\n protected readonly log = $logger();\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly verificationRepository = $repository(verifications);\n\n public async findByEntry(\n entry: VerificationEntry,\n ): Promise<VerificationEntity> {\n this.log.trace(\"Finding verification by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n const results = await this.verificationRepository.findMany({\n limit: 1, // only need the most recent entry\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n },\n });\n\n if (results.length === 0) {\n this.log.debug(\"Verification entry not found\", {\n type: entry.type,\n target: entry.target,\n });\n throw new NotFoundError(\"Verification entry not found\");\n }\n\n this.log.debug(\"Verification entry found\", {\n id: results[0].id,\n type: entry.type,\n target: entry.target,\n });\n\n return results[0];\n }\n\n public findRecentsByEntry(entry: VerificationEntry) {\n this.log.trace(\"Finding recent verifications by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n return this.verificationRepository.findMany({\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n createdAt: {\n gte: this.dateTimeProvider.now().startOf(\"day\").toISOString(),\n },\n },\n });\n }\n\n /**\n * Creates a verification entry and returns the token.\n * The caller is responsible for sending notifications with the token.\n * This allows for context-specific notifications (e.g., password reset vs email verification).\n */\n public async createVerification(\n entry: VerificationEntry,\n ): Promise<RequestVerificationResponse> {\n this.log.trace(\"Creating verification\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const recents = await this.findRecentsByEntry(entry);\n if (recents.length >= settings.limitPerDay) {\n this.log.warn(\"Daily verification limit reached\", {\n type: entry.type,\n target: entry.target,\n limit: settings.limitPerDay,\n count: recents.length,\n });\n throw new BadRequestError(\n `Maximum number of verification requests per day reached (${settings.limitPerDay})`,\n );\n }\n\n const existingVerification = recents[0];\n if (existingVerification) {\n const nowSec = this.dateTimeProvider.now().unix();\n const createdAtSec = this.dateTimeProvider\n .of(existingVerification.createdAt)\n .unix();\n\n const diffSec = nowSec - createdAtSec;\n if (diffSec < settings.verificationCooldown) {\n const remainingCooldown = Math.floor(\n settings.verificationCooldown - diffSec,\n );\n this.log.debug(\"Verification on cooldown\", {\n type: entry.type,\n target: entry.target,\n remainingSeconds: remainingCooldown,\n });\n throw new BadRequestError(\n `Verification is on cooldown for ${remainingCooldown} seconds`,\n );\n }\n }\n\n const token = this.generateToken(entry.type);\n\n const verification = await this.verificationRepository.create({\n type: entry.type,\n target: entry.target,\n code: this.hashCode(token),\n createdAt: this.dateTimeProvider.nowISOString(),\n });\n\n this.log.info(\"Verification created\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n expiresInSeconds: settings.codeExpiration,\n });\n\n return {\n token,\n codeExpiration: settings.codeExpiration,\n verificationCooldown: settings.verificationCooldown,\n maxVerificationAttempts: settings.maxAttempts,\n };\n }\n\n public async verifyCode(\n entry: VerificationEntry,\n code: string,\n ): Promise<ValidateVerificationCodeResponse> {\n this.log.trace(\"Verifying code\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const verification = await this.findByEntry(entry);\n if (verification.verifiedAt) {\n this.log.debug(\"Verification already verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n verifiedAt: verification.verifiedAt,\n });\n return { ok: true, alreadyVerified: true };\n }\n\n // DO NOT DELETE THE VERIFICATION WHEN IT IS REJECTED,\n // or we won't be able to cooldown the verification\n\n const now = this.dateTimeProvider.now();\n const expirationDate = this.dateTimeProvider\n .of(verification.createdAt)\n .add(settings.codeExpiration, \"seconds\");\n\n if (now > expirationDate) {\n this.log.warn(\"Verification code expired\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n createdAt: verification.createdAt,\n expiredAt: expirationDate.toISOString(),\n });\n throw new BadRequestError(\"Verification code has expired\");\n }\n\n if (verification.attempts >= settings.maxAttempts) {\n this.log.warn(\"Verification locked due to max attempts\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: verification.attempts,\n maxAttempts: settings.maxAttempts,\n });\n throw new BadRequestError(\n \"Maximum number of attempts reached - verification is locked\",\n );\n }\n\n if (verification.code !== this.hashCode(code)) {\n const newAttempts = verification.attempts + 1;\n this.log.warn(\"Invalid verification code\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: newAttempts,\n maxAttempts: settings.maxAttempts,\n });\n await this.verificationRepository.updateById(verification.id, {\n attempts: newAttempts,\n });\n throw new BadRequestError(\"Invalid verification code\");\n }\n\n await this.verificationRepository.updateById(verification.id, {\n verifiedAt: this.dateTimeProvider.nowISOString(),\n });\n\n this.log.info(\"Verification code verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n });\n\n return { ok: true };\n }\n\n public hashCode(code: string): string {\n return createHash(\"sha256\").update(code).digest(\"hex\");\n }\n\n public generateToken(type: VerificationTypeEnum): string {\n if (type === \"code\") {\n const settings = this.verificationParameters.get(\"code\");\n return randomInt(0, 1_000_000)\n .toString()\n .padStart(settings.codeLength, \"0\");\n } else if (type === \"link\") {\n return randomUUID();\n }\n\n throw new BadRequestError(`Invalid verification type: ${type}`);\n }\n}\n\nexport interface VerificationEntry {\n type: VerificationTypeEnum;\n target: string;\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { requestVerificationCodeResponseSchema } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport { validateVerificationCodeResponseSchema } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\nimport { VerificationService } from \"../services/VerificationService.ts\";\n\nexport class VerificationController {\n protected readonly verificationService = $inject(VerificationService);\n\n public readonly url = \"/verifications\";\n public readonly group = \"verifications\";\n\n public readonly requestVerificationCode = $action({\n path: `${this.url}/:type`,\n group: this.group,\n method: \"POST\",\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n }),\n response: requestVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return await this.verificationService.createVerification({\n type: params.type,\n target: body.target,\n });\n },\n });\n\n public readonly validateVerificationCode = $action({\n path: `${this.url}/:type/validate`,\n group: this.group,\n method: \"POST\",\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n token: t.text({\n description:\n \"The verification token (6-digit code for phone, UUID for email).\",\n }),\n }),\n response: validateVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return this.verificationService.verifyCode(\n {\n type: params.type,\n target: body.target,\n },\n body.token,\n );\n },\n });\n}\n","import { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $repository } from \"alepha/orm\";\nimport { $scheduler } from \"alepha/scheduler\";\nimport { verifications } from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\n\nexport class VerificationJobs {\n protected readonly verificationRepository = $repository(verifications);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n\n public readonly cleanExpired = $scheduler({\n name: \"api:verifications:cleanExpired\",\n cron: \"0 * * * *\", // Hourly at minute 0\n description: \"Clean expired verifications\",\n handler: async () => {\n const purgeDays = this.verificationParameters.get(\"purgeDays\");\n if (purgeDays <= 0) {\n return; // Auto deletion is disabled\n }\n\n const dayMs = 24 * 60 * 60 * 1000;\n const purgeThreshold =\n this.dateTimeProvider.nowMillis() - purgeDays * dayMs;\n\n await this.verificationRepository.deleteMany({\n createdAt: {\n lt: this.dateTimeProvider.of(purgeThreshold).toISOString(),\n },\n });\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { VerificationController } from \"./controllers/VerificationController.ts\";\nimport { VerificationJobs } from \"./jobs/VerificationJobs.ts\";\nimport { VerificationParameters } from \"./parameters/VerificationParameters.ts\";\nimport { VerificationService } from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/VerificationController.ts\";\nexport * from \"./entities/verifications.ts\";\nexport * from \"./jobs/VerificationJobs.ts\";\nexport * from \"./parameters/VerificationParameters.ts\";\nexport * from \"./schemas/requestVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/validateVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/verificationTypeEnumSchema.ts\";\nexport * from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Email and phone verification workflows.\n *\n * **Features:**\n * - Verification token generation\n * - Verification code sending\n * - Verification completion tracking\n * - Resend functionality\n *\n * @module alepha.api.verifications\n */\nexport const AlephaApiVerification = $module({\n name: \"alepha.api.verifications\",\n services: [\n VerificationController,\n VerificationJobs,\n VerificationService,\n VerificationParameters,\n ],\n});\n"],"mappings":"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"}
|
package/dist/bucket/index.js
CHANGED
|
@@ -314,7 +314,11 @@ var LocalFileStorageProvider = class {
|
|
|
314
314
|
var CloudflareR2Provider = class {
|
|
315
315
|
alepha = $inject(Alepha);
|
|
316
316
|
log = $logger();
|
|
317
|
-
env = $env(t.object({
|
|
317
|
+
env = $env(t.object({
|
|
318
|
+
/**
|
|
319
|
+
* The actual R2 bucket name in Cloudflare.
|
|
320
|
+
*/
|
|
321
|
+
R2_BUCKET_NAME: t.string({ description: "R2 bucket name in Cloudflare" }) }));
|
|
318
322
|
r2;
|
|
319
323
|
/**
|
|
320
324
|
* Get the R2 bucket name from environment.
|
package/dist/bucket/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/bucket/errors/InvalidFileError.ts","../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/providers/LocalFileStorageProvider.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/index.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport class InvalidFileError extends AlephaError {\n public readonly status = 400;\n}\n","import type { FileLike } from \"alepha\";\n\nexport abstract class FileStorageProvider {\n /**\n * Uploads a file to the storage.\n *\n * @param bucketName - Container name\n * @param file - File to upload\n * @param fileId - Optional file identifier. If not provided, a unique ID will be generated.\n * @return The identifier of the uploaded file.\n */\n abstract upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string>;\n\n /**\n * Downloads a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to download\n * @return The downloaded file as a FileLike object.\n */\n abstract download(bucketName: string, fileId: string): Promise<FileLike>;\n\n /**\n * Check if fileId exists in the storage bucket.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to stream\n * @return True is the file exists, false otherwise.\n */\n abstract exists(bucketName: string, fileId: string): Promise<boolean>;\n\n /**\n * Delete permanently a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to delete\n */\n abstract delete(bucketName: string, fileId: string): Promise<void>;\n}\n","import { AlephaError } from \"alepha\";\n\nexport class FileNotFoundError extends AlephaError {\n public readonly status = 404;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { $inject, type FileLike } from \"alepha\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\ninterface StoredFile {\n buffer: Buffer;\n name: string;\n type: string;\n size: number;\n}\n\nexport class MemoryFileStorageProvider implements FileStorageProvider {\n public readonly files: Record<string, StoredFile> = {};\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId();\n\n // Consume the stream and store as buffer so downloads are repeatable\n const chunks: Uint8Array[] = [];\n for await (const chunk of file.stream() as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n this.files[`${bucketName}/${fileId}`] = {\n buffer,\n name: file.name,\n type: file.type,\n size: file.size,\n };\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const fileKey = `${bucketName}/${fileId}`;\n const stored = this.files[fileKey];\n\n if (!stored) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n // Create a fresh FileLike with a new stream from the stored buffer\n return this.fileSystem.createFile({\n stream: new Blob([new Uint8Array(stored.buffer)]).stream(),\n name: stored.name,\n type: stored.type,\n size: stored.size,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n return `${bucketName}/${fileId}` in this.files;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const fileKey = `${bucketName}/${fileId}`;\n if (!(fileKey in this.files)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n delete this.files[fileKey];\n }\n\n protected createId(): string {\n return randomUUID();\n }\n}\n","import {\n $inject,\n createPrimitive,\n type FileLike,\n KIND,\n Primitive,\n type Service,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { InvalidFileError } from \"../errors/InvalidFileError.ts\";\nimport { FileStorageProvider } from \"../providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"../providers/MemoryFileStorageProvider.ts\";\n\n/**\n * Creates a bucket primitive for file storage and management with configurable validation.\n *\n * Provides a comprehensive file storage system that handles uploads, downloads, validation,\n * and management across multiple storage backends with MIME type and size limit controls.\n *\n * **Key Features**\n * - Multi-provider support (filesystem, cloud storage, in-memory)\n * - Automatic MIME type and file size validation\n * - Event integration for file operations monitoring\n * - Flexible per-bucket and per-operation configuration\n * - Smart file type and size detection\n *\n * **Common Use Cases**\n * - User profile pictures and document uploads\n * - Product images and media management\n * - Document storage and retrieval systems\n *\n * @example\n * ```ts\n * class MediaService {\n * images = $bucket({\n * name: \"user-images\",\n * mimeTypes: [\"image/jpeg\", \"image/png\", \"image/gif\"],\n * maxSize: 5 // 5MB limit\n * });\n *\n * documents = $bucket({\n * name: \"documents\",\n * mimeTypes: [\"application/pdf\", \"text/plain\"],\n * maxSize: 50 // 50MB limit\n * });\n *\n * async uploadProfileImage(file: FileLike, userId: string): Promise<string> {\n * const fileId = await this.images.upload(file);\n * await this.userService.updateProfileImage(userId, fileId);\n * return fileId;\n * }\n *\n * async downloadDocument(documentId: string): Promise<FileLike> {\n * return await this.documents.download(documentId);\n * }\n *\n * async deleteDocument(documentId: string): Promise<void> {\n * await this.documents.delete(documentId);\n * }\n * }\n * ```\n */\nexport const $bucket = (options: BucketPrimitiveOptions) =>\n createPrimitive(BucketPrimitive, options);\n\nexport interface BucketPrimitiveOptions extends BucketFileOptions {\n /**\n * File storage provider configuration for the bucket.\n *\n * Options:\n * - **\"memory\"**: In-memory storage (default for development, lost on restart)\n * - **Service<FileStorageProvider>**: Custom provider class (e.g., S3FileStorageProvider, AzureBlobProvider)\n * - **undefined**: Uses the default file storage provider from dependency injection\n *\n * **Provider Selection Guidelines**:\n * - **Development**: Use \"memory\" for fast, simple testing without external dependencies\n * - **Production**: Use cloud providers (S3, Azure Blob, Google Cloud Storage) for scalability\n * - **Local deployment**: Use filesystem providers for on-premise installations\n * - **Hybrid**: Use different providers for different bucket types (temp files vs permanent storage)\n *\n * **Provider Capabilities**:\n * - File persistence and durability guarantees\n * - Scalability and performance characteristics\n * - Geographic distribution and CDN integration\n * - Cost implications for storage and bandwidth\n * - Backup and disaster recovery features\n *\n * @default Uses injected FileStorageProvider\n * @example \"memory\"\n * @example S3FileStorageProvider\n * @example AzureBlobStorageProvider\n */\n provider?: Service<FileStorageProvider> | \"memory\";\n\n /**\n * Unique name identifier for the bucket.\n *\n * This name is used for:\n * - Storage backend organization and partitioning\n * - File path generation and URL construction\n * - Logging, monitoring, and debugging\n * - Access control and permissions management\n * - Backup and replication configuration\n *\n * **Naming Conventions**:\n * - Use lowercase with hyphens for consistency\n * - Include purpose or content type in the name\n * - Avoid spaces and special characters\n * - Consider environment prefixes for deployment isolation\n *\n * If not provided, defaults to the property key where the bucket is declared.\n *\n * @example \"user-avatars\"\n * @example \"product-images\"\n * @example \"legal-documents\"\n * @example \"temp-processing-files\"\n */\n name?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Human-readable description of the bucket's purpose and contents.\n *\n * Used for:\n * - Documentation generation and API references\n * - Developer onboarding and system understanding\n * - Monitoring dashboards and admin interfaces\n * - Compliance and audit documentation\n *\n * **Description Best Practices**:\n * - Explain what types of files this bucket stores\n * - Mention any special handling or processing requirements\n * - Include information about retention policies if applicable\n * - Note any compliance or security considerations\n *\n * @example \"User profile pictures and avatar images\"\n * @example \"Product catalog images with automated thumbnail generation\"\n * @example \"Legal documents requiring long-term retention\"\n * @example \"Temporary files for data processing workflows\"\n */\n description?: string;\n\n /**\n * Array of allowed MIME types for files uploaded to this bucket.\n *\n * When specified, only files with these exact MIME types will be accepted.\n * Files with disallowed MIME types will be rejected with an InvalidFileError.\n *\n * **MIME Type Categories**:\n * - Images: \"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/svg+xml\"\n * - Documents: \"application/pdf\", \"text/plain\", \"text/csv\"\n * - Office: \"application/msword\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n * - Archives: \"application/zip\", \"application/x-tar\", \"application/gzip\"\n * - Media: \"video/mp4\", \"audio/mpeg\", \"audio/wav\"\n *\n * **Security Considerations**:\n * - Always validate MIME types for user uploads\n * - Be cautious with executable file types\n * - Consider using allow-lists rather than deny-lists\n * - Remember that MIME types can be spoofed by malicious users\n *\n * If not specified, all MIME types are allowed (not recommended for user uploads).\n *\n * @example [\"image/jpeg\", \"image/png\"] // Only JPEG and PNG images\n * @example [\"application/pdf\", \"text/plain\"] // Documents only\n * @example [\"video/mp4\", \"video/webm\"] // Video files\n */\n mimeTypes?: string[];\n\n /**\n * Maximum file size allowed in megabytes (MB).\n *\n * Files larger than this limit will be rejected with an InvalidFileError.\n * This helps prevent:\n * - Storage quota exhaustion\n * - Memory issues during file processing\n * - Long upload times and timeouts\n * - Abuse of storage resources\n *\n * **Size Guidelines by File Type**:\n * - Profile images: 1-5 MB\n * - Product photos: 5-10 MB\n * - Documents: 10-50 MB\n * - Video files: 50-500 MB\n * - Data files: 100-1000 MB\n *\n * **Considerations**:\n * - Consider your storage costs and limits\n * - Factor in network upload speeds for users\n * - Account for processing requirements (thumbnails, compression)\n * - Set reasonable limits based on actual use cases\n *\n * @default 10 MB\n *\n * @example 1 // 1MB for small images\n * @example 25 // 25MB for documents\n * @example 100 // 100MB for media files\n */\n maxSize?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BucketPrimitive extends Primitive<BucketPrimitiveOptions> {\n public readonly provider = this.$provider();\n protected readonly fileSystem = $inject(FileSystemProvider);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Uploads a file to the bucket.\n */\n public async upload(\n file: FileLike,\n options?: BucketFileOptions,\n ): Promise<string> {\n if (file instanceof File) {\n // our createFile is smarter than the browser's File constructor\n // by doing this, we can guess the MIME type and size!\n file = this.fileSystem.createFile({ file });\n }\n\n options = {\n ...this.options,\n ...options,\n };\n\n const mimeTypes = options.mimeTypes ?? undefined;\n const maxSize = options.maxSize ?? 10; // Default to 10 MB if not specified\n\n if (mimeTypes) {\n const mimeType = file.type || \"application/octet-stream\";\n if (!mimeTypes.includes(mimeType)) {\n throw new InvalidFileError(\n `MIME type ${mimeType} is not allowed in bucket ${this.name}`,\n );\n }\n }\n\n // check size in bytes, convert MB to bytes\n if (file.size > maxSize * 1024 * 1024) {\n throw new InvalidFileError(\n `File size ${file.size} exceeds the maximum size of ${maxSize} MB in bucket ${this.name}`,\n );\n }\n\n const id = await this.provider.upload(this.name, file);\n\n await this.alepha.events.emit(\"bucket:file:uploaded\", {\n id,\n bucket: this,\n file,\n options,\n });\n\n return id;\n }\n\n /**\n * Delete permanently a file from the bucket.\n */\n public async delete(fileId: string, skipHook = false): Promise<void> {\n await this.provider.delete(this.name, fileId);\n\n if (skipHook) {\n return;\n }\n\n await this.alepha.events.emit(\"bucket:file:deleted\", {\n id: fileId,\n bucket: this,\n });\n }\n\n /**\n * Checks if a file exists in the bucket.\n */\n public async exists(fileId: string): Promise<boolean> {\n return this.provider.exists(this.name, fileId);\n }\n\n /**\n * Downloads a file from the bucket.\n */\n public async download(fileId: string): Promise<FileLike> {\n return this.provider.download(this.name, fileId);\n }\n\n protected $provider() {\n if (!this.options.provider) {\n return this.alepha.inject(FileStorageProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemoryFileStorageProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n$bucket[KIND] = BucketPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Optional description of the bucket.\n */\n description?: string;\n\n /**\n * Allowed MIME types.\n */\n mimeTypes?: string[];\n\n /**\n * Maximum size of the files in the bucket.\n *\n * @default 10\n */\n maxSize?: number;\n}\n","import { randomUUID } from \"node:crypto\";\nimport type * as fs from \"node:fs\";\nimport { createReadStream } from \"node:fs\";\nimport { mkdir, stat, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n $atom,\n $hook,\n $inject,\n $state,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Local file storage configuration atom\n */\nexport const localFileStorageOptions = $atom({\n name: \"alepha.bucket.local.options\",\n schema: t.object({\n storagePath: t.string({\n description: \"Directory path where files will be stored\",\n }),\n }),\n default: {\n storagePath: \"node_modules/.alepha/buckets\",\n },\n});\n\nexport type LocalFileStorageProviderOptions = Static<\n typeof localFileStorageOptions.schema\n>;\n\ndeclare module \"alepha\" {\n interface State {\n [localFileStorageOptions.key]: LocalFileStorageProviderOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class LocalFileStorageProvider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly fileSystemProvider = $inject(FileSystemProvider);\n protected readonly options = $state(localFileStorageOptions);\n\n protected get storagePath(): string {\n return this.options.storagePath;\n }\n\n protected readonly onConfigure = $hook({\n on: \"configure\",\n handler: async () => {\n if (\n this.alepha.isTest() &&\n this.storagePath === localFileStorageOptions.options.default.storagePath\n ) {\n this.alepha.store.set(localFileStorageOptions, {\n storagePath: join(tmpdir(), `alepha-test-${Date.now()}`),\n });\n }\n },\n });\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n try {\n await mkdir(this.storagePath, { recursive: true });\n } catch {}\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n\n await mkdir(join(this.storagePath, bucket.name), {\n recursive: true,\n });\n\n this.log.debug(`Bucket '${bucket.name}' at ${this.storagePath} OK`);\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId(file.type);\n\n this.log.trace(`Uploading file to ${bucketName}`);\n\n await this.fileSystemProvider.writeFile(\n this.path(bucketName, fileId),\n file,\n );\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const filePath = this.path(bucketName, fileId);\n\n try {\n const stats = await stat(filePath);\n const mimeType = this.fileDetector.getContentType(fileId);\n\n return this.fileSystemProvider.createFile({\n stream: createReadStream(filePath),\n name: fileId,\n type: mimeType,\n size: stats.size,\n });\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Invalid file operation\", { cause: error });\n }\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n try {\n await stat(this.path(bucketName, fileId));\n return true;\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n return false;\n }\n throw new AlephaError(\"Error checking file existence\", { cause: error });\n }\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n try {\n return await unlink(this.path(bucketName, fileId));\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Error deleting file\", { cause: error });\n }\n }\n\n protected stat(bucket: string, fileId: string): Promise<fs.Stats> {\n return stat(this.path(bucket, fileId));\n }\n\n protected createId(mimeType: string): string {\n const ext = this.fileDetector.getExtensionFromMimeType(mimeType);\n return `${randomUUID()}.${ext}`;\n }\n\n protected path(bucket: string, fileId = \"\"): string {\n return join(this.storagePath, bucket, fileId);\n }\n\n protected isErrorNoEntry(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n }\n}\n","import {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * R2Bucket interface matching Cloudflare's R2 API.\n */\nexport interface R2Bucket {\n put(\n key: string,\n value:\n | ReadableStream\n | ArrayBuffer\n | ArrayBufferView\n | string\n | Blob\n | null,\n options?: R2PutOptions,\n ): Promise<R2Object | null>;\n get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;\n head(key: string): Promise<R2Object | null>;\n delete(keys: string | string[]): Promise<void>;\n list(options?: R2ListOptions): Promise<R2Objects>;\n createMultipartUpload(\n key: string,\n options?: R2MultipartOptions,\n ): Promise<R2MultipartUpload>;\n}\n\nexport interface R2Object {\n key: string;\n version: string;\n size: number;\n etag: string;\n httpEtag: string;\n checksums: R2Checksums;\n uploaded: Date;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n range?: R2Range;\n storageClass: string;\n}\n\nexport interface R2ObjectBody extends R2Object {\n body: ReadableStream;\n bodyUsed: boolean;\n arrayBuffer(): Promise<ArrayBuffer>;\n text(): Promise<string>;\n json<T>(): Promise<T>;\n blob(): Promise<Blob>;\n}\n\nexport interface R2PutOptions {\n onlyIf?: R2Conditional;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n md5?: ArrayBuffer | string;\n sha1?: ArrayBuffer | string;\n sha256?: ArrayBuffer | string;\n sha384?: ArrayBuffer | string;\n sha512?: ArrayBuffer | string;\n storageClass?: string;\n}\n\nexport interface R2GetOptions {\n onlyIf?: R2Conditional;\n range?: R2Range;\n}\n\nexport interface R2ListOptions {\n limit?: number;\n prefix?: string;\n cursor?: string;\n delimiter?: string;\n startAfter?: string;\n include?: (\"httpMetadata\" | \"customMetadata\")[];\n}\n\nexport interface R2Objects {\n objects: R2Object[];\n truncated: boolean;\n cursor?: string;\n delimitedPrefixes: string[];\n}\n\nexport interface R2Checksums {\n md5?: ArrayBuffer;\n sha1?: ArrayBuffer;\n sha256?: ArrayBuffer;\n sha384?: ArrayBuffer;\n sha512?: ArrayBuffer;\n}\n\nexport interface R2HTTPMetadata {\n contentType?: string;\n contentLanguage?: string;\n contentDisposition?: string;\n contentEncoding?: string;\n cacheControl?: string;\n cacheExpiry?: Date;\n}\n\nexport interface R2Conditional {\n etagMatches?: string;\n etagDoesNotMatch?: string;\n uploadedBefore?: Date;\n uploadedAfter?: Date;\n secondsGranularity?: boolean;\n}\n\nexport interface R2Range {\n offset?: number;\n length?: number;\n suffix?: number;\n}\n\nexport interface R2MultipartOptions {\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n storageClass?: string;\n}\n\nexport interface R2MultipartUpload {\n key: string;\n uploadId: string;\n uploadPart(\n partNumber: number,\n value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,\n ): Promise<R2UploadedPart>;\n abort(): Promise<void>;\n complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;\n}\n\nexport interface R2UploadedPart {\n partNumber: number;\n etag: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cloudflare R2 storage provider.\n *\n * Uses a single R2 bucket binding for all $bucket primitives.\n * Files are organized as: {APP_NAME}/{bucketName}/{fileId}\n *\n * **Required environment variables:**\n * - `R2_BUCKET_NAME` - The actual R2 bucket name in Cloudflare\n *\n * **Optional (uses core Alepha env):**\n * - `APP_NAME` - Prefix for all files (for multi-app setups sharing one R2 bucket)\n *\n * @example\n * ```bash\n * # .env\n * APP_NAME=myapp # optional, used as prefix\n * R2_BUCKET_NAME=storage\n * ```\n *\n * @example\n * ```toml\n * # wrangler.toml - automatically generated by alepha build\n * [[r2_buckets]]\n * binding = \"R2\"\n * bucket_name = \"storage\"\n * ```\n *\n * @example\n * ```ts\n * // Define buckets with validation rules\n * const avatars = $bucket({\n * name: \"avatars\",\n * maxFileSize: 5 * 1024 * 1024,\n * allowedMimeTypes: [\"image/*\"],\n * });\n *\n * const documents = $bucket({\n * name: \"documents\",\n * maxFileSize: 50 * 1024 * 1024,\n * allowedMimeTypes: [\"application/pdf\"],\n * });\n *\n * // Files stored at: myapp/avatars/uuid.png, myapp/documents/uuid.pdf\n * ```\n */\nexport class CloudflareR2Provider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n /**\n * The actual R2 bucket name in Cloudflare.\n */\n R2_BUCKET_NAME: t.string({\n description: \"R2 bucket name in Cloudflare\",\n }),\n }),\n );\n\n protected r2?: R2Bucket;\n\n /**\n * Get the R2 bucket name from environment.\n */\n public get bucketName(): string {\n return this.env.R2_BUCKET_NAME;\n }\n\n /**\n * Get the optional prefix from APP_NAME environment variable.\n * Used for multi-app setups sharing the same R2 bucket.\n */\n public get prefix(): string | undefined {\n return this.alepha.env.APP_NAME;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n const cloudflareEnv = this.alepha.get(\"cloudflare.env\") as\n | Record<string, unknown>\n | undefined;\n if (!cloudflareEnv) {\n throw new AlephaError(\n \"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.\",\n );\n }\n\n const binding = cloudflareEnv[this.bucketName] as R2Bucket | undefined;\n if (!binding) {\n throw new AlephaError(\n `R2 binding '${this.bucketName}' not found in Cloudflare Workers environment.`,\n );\n }\n\n this.r2 = binding;\n\n const prefixStr = this.prefix ? `${this.prefix}/` : \"\";\n this.log.info(\n `R2 storage ready (bucket: ${this.bucketName}, prefix: ${prefixStr || \"(none)\"})`,\n );\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n this.log.debug(\n `Bucket '${bucket.name}' -> ${prefixStr}${bucket.name}/`,\n );\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n const r2 = this.getR2();\n fileId ??= this.createId(file.name);\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Uploading '${key}'`);\n\n const arrayBuffer = await file.arrayBuffer();\n\n await r2.put(key, arrayBuffer, {\n httpMetadata: {\n contentType: file.type,\n },\n customMetadata: {\n originalName: file.name,\n bucket: bucketName,\n },\n });\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Downloading '${key}'`);\n\n const object = await r2.get(key);\n if (!object) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n const originalName = object.customMetadata?.originalName ?? fileId;\n const contentType =\n object.httpMetadata?.contentType ?? \"application/octet-stream\";\n\n return {\n name: originalName,\n type: contentType,\n size: object.size,\n lastModified: object.uploaded.getTime(),\n stream: () => object.body,\n arrayBuffer: () => object.arrayBuffer(),\n text: () => object.text(),\n };\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Checking '${key}'`);\n\n const object = await r2.head(key);\n return object !== null;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Deleting '${key}'`);\n\n // R2's delete doesn't throw if the file doesn't exist,\n // so we check existence first for consistency with other providers\n const exists = await this.exists(bucketName, fileId);\n if (!exists) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n await r2.delete(key);\n }\n\n /**\n * Build the full R2 key: {prefix}/{bucketName}/{fileId}\n */\n protected key(bucketName: string, fileId: string): string {\n const parts = [bucketName, fileId];\n if (this.prefix) {\n parts.unshift(this.prefix);\n }\n return parts.join(\"/\");\n }\n\n protected getR2(): R2Bucket {\n if (!this.r2) {\n throw new AlephaError(\"R2 storage not initialized. Call start() first.\");\n }\n return this.r2;\n }\n\n protected createId(filename: string): string {\n const ext = filename.includes(\".\") ? filename.split(\".\").pop() : \"\";\n const id = crypto.randomUUID();\n return ext ? `${id}.${ext}` : id;\n }\n}\n","import { $module, type FileLike } from \"alepha\";\nimport {\n $bucket,\n type BucketFileOptions,\n type BucketPrimitive,\n} from \"./primitives/$bucket.ts\";\nimport { FileStorageProvider } from \"./providers/FileStorageProvider.ts\";\nimport { LocalFileStorageProvider } from \"./providers/LocalFileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/FileNotFoundError.ts\";\nexport * from \"./primitives/$bucket.ts\";\nexport * from \"./providers/CloudflareR2Provider.ts\";\nexport * from \"./providers/FileStorageProvider.ts\";\nexport * from \"./providers/LocalFileStorageProvider.ts\";\nexport * from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Hooks {\n /**\n * Triggered when a file is uploaded to a bucket.\n * Can be used to perform actions after a file is uploaded, like creating a database record!\n */\n \"bucket:file:uploaded\": {\n id: string;\n file: FileLike;\n bucket: BucketPrimitive;\n options: BucketFileOptions;\n };\n /**\n * Triggered when a file is deleted from a bucket.\n */\n \"bucket:file:deleted\": {\n id: string;\n bucket: BucketPrimitive;\n };\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Unified file storage abstraction across multiple backends.\n *\n * **Features:**\n * - File storage buckets with constraints\n * - Unified API across all storage backends\n * - MIME type validation\n * - File size limits\n * - Upload/download/delete operations\n * - TTL-based file expiration\n * - Providers: Memory (testing), Local filesystem, AWS S3 / Cloudflare R2 / MinIO, Azure Blob Storage, Vercel Blob\n *\n * @module alepha.bucket\n */\nexport const AlephaBucket = $module({\n name: \"alepha.bucket\",\n primitives: [$bucket],\n services: [FileStorageProvider],\n variants: [MemoryFileStorageProvider, LocalFileStorageProvider],\n register: (alepha) => {\n alepha.with({\n optional: true,\n provide: FileStorageProvider,\n use:\n alepha.isTest() || alepha.isServerless()\n ? MemoryFileStorageProvider\n : LocalFileStorageProvider,\n });\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/bucket/errors/InvalidFileError.ts","../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/providers/LocalFileStorageProvider.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/index.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport class InvalidFileError extends AlephaError {\n public readonly status = 400;\n}\n","import type { FileLike } from \"alepha\";\n\nexport abstract class FileStorageProvider {\n /**\n * Uploads a file to the storage.\n *\n * @param bucketName - Container name\n * @param file - File to upload\n * @param fileId - Optional file identifier. If not provided, a unique ID will be generated.\n * @return The identifier of the uploaded file.\n */\n abstract upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string>;\n\n /**\n * Downloads a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to download\n * @return The downloaded file as a FileLike object.\n */\n abstract download(bucketName: string, fileId: string): Promise<FileLike>;\n\n /**\n * Check if fileId exists in the storage bucket.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to stream\n * @return True is the file exists, false otherwise.\n */\n abstract exists(bucketName: string, fileId: string): Promise<boolean>;\n\n /**\n * Delete permanently a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to delete\n */\n abstract delete(bucketName: string, fileId: string): Promise<void>;\n}\n","import { AlephaError } from \"alepha\";\n\nexport class FileNotFoundError extends AlephaError {\n public readonly status = 404;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { $inject, type FileLike } from \"alepha\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\ninterface StoredFile {\n buffer: Buffer;\n name: string;\n type: string;\n size: number;\n}\n\nexport class MemoryFileStorageProvider implements FileStorageProvider {\n public readonly files: Record<string, StoredFile> = {};\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId();\n\n // Consume the stream and store as buffer so downloads are repeatable\n const chunks: Uint8Array[] = [];\n for await (const chunk of file.stream() as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n this.files[`${bucketName}/${fileId}`] = {\n buffer,\n name: file.name,\n type: file.type,\n size: file.size,\n };\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const fileKey = `${bucketName}/${fileId}`;\n const stored = this.files[fileKey];\n\n if (!stored) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n // Create a fresh FileLike with a new stream from the stored buffer\n return this.fileSystem.createFile({\n stream: new Blob([new Uint8Array(stored.buffer)]).stream(),\n name: stored.name,\n type: stored.type,\n size: stored.size,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n return `${bucketName}/${fileId}` in this.files;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const fileKey = `${bucketName}/${fileId}`;\n if (!(fileKey in this.files)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n delete this.files[fileKey];\n }\n\n protected createId(): string {\n return randomUUID();\n }\n}\n","import {\n $inject,\n createPrimitive,\n type FileLike,\n KIND,\n Primitive,\n type Service,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { InvalidFileError } from \"../errors/InvalidFileError.ts\";\nimport { FileStorageProvider } from \"../providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"../providers/MemoryFileStorageProvider.ts\";\n\n/**\n * Creates a bucket primitive for file storage and management with configurable validation.\n *\n * Provides a comprehensive file storage system that handles uploads, downloads, validation,\n * and management across multiple storage backends with MIME type and size limit controls.\n *\n * **Key Features**\n * - Multi-provider support (filesystem, cloud storage, in-memory)\n * - Automatic MIME type and file size validation\n * - Event integration for file operations monitoring\n * - Flexible per-bucket and per-operation configuration\n * - Smart file type and size detection\n *\n * **Common Use Cases**\n * - User profile pictures and document uploads\n * - Product images and media management\n * - Document storage and retrieval systems\n *\n * @example\n * ```ts\n * class MediaService {\n * images = $bucket({\n * name: \"user-images\",\n * mimeTypes: [\"image/jpeg\", \"image/png\", \"image/gif\"],\n * maxSize: 5 // 5MB limit\n * });\n *\n * documents = $bucket({\n * name: \"documents\",\n * mimeTypes: [\"application/pdf\", \"text/plain\"],\n * maxSize: 50 // 50MB limit\n * });\n *\n * async uploadProfileImage(file: FileLike, userId: string): Promise<string> {\n * const fileId = await this.images.upload(file);\n * await this.userService.updateProfileImage(userId, fileId);\n * return fileId;\n * }\n *\n * async downloadDocument(documentId: string): Promise<FileLike> {\n * return await this.documents.download(documentId);\n * }\n *\n * async deleteDocument(documentId: string): Promise<void> {\n * await this.documents.delete(documentId);\n * }\n * }\n * ```\n */\nexport const $bucket = (options: BucketPrimitiveOptions) =>\n createPrimitive(BucketPrimitive, options);\n\nexport interface BucketPrimitiveOptions extends BucketFileOptions {\n /**\n * File storage provider configuration for the bucket.\n *\n * Options:\n * - **\"memory\"**: In-memory storage (default for development, lost on restart)\n * - **Service<FileStorageProvider>**: Custom provider class (e.g., S3FileStorageProvider, AzureBlobProvider)\n * - **undefined**: Uses the default file storage provider from dependency injection\n *\n * **Provider Selection Guidelines**:\n * - **Development**: Use \"memory\" for fast, simple testing without external dependencies\n * - **Production**: Use cloud providers (S3, Azure Blob, Google Cloud Storage) for scalability\n * - **Local deployment**: Use filesystem providers for on-premise installations\n * - **Hybrid**: Use different providers for different bucket types (temp files vs permanent storage)\n *\n * **Provider Capabilities**:\n * - File persistence and durability guarantees\n * - Scalability and performance characteristics\n * - Geographic distribution and CDN integration\n * - Cost implications for storage and bandwidth\n * - Backup and disaster recovery features\n *\n * @default Uses injected FileStorageProvider\n * @example \"memory\"\n * @example S3FileStorageProvider\n * @example AzureBlobStorageProvider\n */\n provider?: Service<FileStorageProvider> | \"memory\";\n\n /**\n * Unique name identifier for the bucket.\n *\n * This name is used for:\n * - Storage backend organization and partitioning\n * - File path generation and URL construction\n * - Logging, monitoring, and debugging\n * - Access control and permissions management\n * - Backup and replication configuration\n *\n * **Naming Conventions**:\n * - Use lowercase with hyphens for consistency\n * - Include purpose or content type in the name\n * - Avoid spaces and special characters\n * - Consider environment prefixes for deployment isolation\n *\n * If not provided, defaults to the property key where the bucket is declared.\n *\n * @example \"user-avatars\"\n * @example \"product-images\"\n * @example \"legal-documents\"\n * @example \"temp-processing-files\"\n */\n name?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Human-readable description of the bucket's purpose and contents.\n *\n * Used for:\n * - Documentation generation and API references\n * - Developer onboarding and system understanding\n * - Monitoring dashboards and admin interfaces\n * - Compliance and audit documentation\n *\n * **Description Best Practices**:\n * - Explain what types of files this bucket stores\n * - Mention any special handling or processing requirements\n * - Include information about retention policies if applicable\n * - Note any compliance or security considerations\n *\n * @example \"User profile pictures and avatar images\"\n * @example \"Product catalog images with automated thumbnail generation\"\n * @example \"Legal documents requiring long-term retention\"\n * @example \"Temporary files for data processing workflows\"\n */\n description?: string;\n\n /**\n * Array of allowed MIME types for files uploaded to this bucket.\n *\n * When specified, only files with these exact MIME types will be accepted.\n * Files with disallowed MIME types will be rejected with an InvalidFileError.\n *\n * **MIME Type Categories**:\n * - Images: \"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/svg+xml\"\n * - Documents: \"application/pdf\", \"text/plain\", \"text/csv\"\n * - Office: \"application/msword\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n * - Archives: \"application/zip\", \"application/x-tar\", \"application/gzip\"\n * - Media: \"video/mp4\", \"audio/mpeg\", \"audio/wav\"\n *\n * **Security Considerations**:\n * - Always validate MIME types for user uploads\n * - Be cautious with executable file types\n * - Consider using allow-lists rather than deny-lists\n * - Remember that MIME types can be spoofed by malicious users\n *\n * If not specified, all MIME types are allowed (not recommended for user uploads).\n *\n * @example [\"image/jpeg\", \"image/png\"] // Only JPEG and PNG images\n * @example [\"application/pdf\", \"text/plain\"] // Documents only\n * @example [\"video/mp4\", \"video/webm\"] // Video files\n */\n mimeTypes?: string[];\n\n /**\n * Maximum file size allowed in megabytes (MB).\n *\n * Files larger than this limit will be rejected with an InvalidFileError.\n * This helps prevent:\n * - Storage quota exhaustion\n * - Memory issues during file processing\n * - Long upload times and timeouts\n * - Abuse of storage resources\n *\n * **Size Guidelines by File Type**:\n * - Profile images: 1-5 MB\n * - Product photos: 5-10 MB\n * - Documents: 10-50 MB\n * - Video files: 50-500 MB\n * - Data files: 100-1000 MB\n *\n * **Considerations**:\n * - Consider your storage costs and limits\n * - Factor in network upload speeds for users\n * - Account for processing requirements (thumbnails, compression)\n * - Set reasonable limits based on actual use cases\n *\n * @default 10 MB\n *\n * @example 1 // 1MB for small images\n * @example 25 // 25MB for documents\n * @example 100 // 100MB for media files\n */\n maxSize?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BucketPrimitive extends Primitive<BucketPrimitiveOptions> {\n public readonly provider = this.$provider();\n protected readonly fileSystem = $inject(FileSystemProvider);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Uploads a file to the bucket.\n */\n public async upload(\n file: FileLike,\n options?: BucketFileOptions,\n ): Promise<string> {\n if (file instanceof File) {\n // our createFile is smarter than the browser's File constructor\n // by doing this, we can guess the MIME type and size!\n file = this.fileSystem.createFile({ file });\n }\n\n options = {\n ...this.options,\n ...options,\n };\n\n const mimeTypes = options.mimeTypes ?? undefined;\n const maxSize = options.maxSize ?? 10; // Default to 10 MB if not specified\n\n if (mimeTypes) {\n const mimeType = file.type || \"application/octet-stream\";\n if (!mimeTypes.includes(mimeType)) {\n throw new InvalidFileError(\n `MIME type ${mimeType} is not allowed in bucket ${this.name}`,\n );\n }\n }\n\n // check size in bytes, convert MB to bytes\n if (file.size > maxSize * 1024 * 1024) {\n throw new InvalidFileError(\n `File size ${file.size} exceeds the maximum size of ${maxSize} MB in bucket ${this.name}`,\n );\n }\n\n const id = await this.provider.upload(this.name, file);\n\n await this.alepha.events.emit(\"bucket:file:uploaded\", {\n id,\n bucket: this,\n file,\n options,\n });\n\n return id;\n }\n\n /**\n * Delete permanently a file from the bucket.\n */\n public async delete(fileId: string, skipHook = false): Promise<void> {\n await this.provider.delete(this.name, fileId);\n\n if (skipHook) {\n return;\n }\n\n await this.alepha.events.emit(\"bucket:file:deleted\", {\n id: fileId,\n bucket: this,\n });\n }\n\n /**\n * Checks if a file exists in the bucket.\n */\n public async exists(fileId: string): Promise<boolean> {\n return this.provider.exists(this.name, fileId);\n }\n\n /**\n * Downloads a file from the bucket.\n */\n public async download(fileId: string): Promise<FileLike> {\n return this.provider.download(this.name, fileId);\n }\n\n protected $provider() {\n if (!this.options.provider) {\n return this.alepha.inject(FileStorageProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemoryFileStorageProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n$bucket[KIND] = BucketPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Optional description of the bucket.\n */\n description?: string;\n\n /**\n * Allowed MIME types.\n */\n mimeTypes?: string[];\n\n /**\n * Maximum size of the files in the bucket.\n *\n * @default 10\n */\n maxSize?: number;\n}\n","import { randomUUID } from \"node:crypto\";\nimport type * as fs from \"node:fs\";\nimport { createReadStream } from \"node:fs\";\nimport { mkdir, stat, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n $atom,\n $hook,\n $inject,\n $state,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Local file storage configuration atom\n */\nexport const localFileStorageOptions = $atom({\n name: \"alepha.bucket.local.options\",\n schema: t.object({\n storagePath: t.string({\n description: \"Directory path where files will be stored\",\n }),\n }),\n default: {\n storagePath: \"node_modules/.alepha/buckets\",\n },\n});\n\nexport type LocalFileStorageProviderOptions = Static<\n typeof localFileStorageOptions.schema\n>;\n\ndeclare module \"alepha\" {\n interface State {\n [localFileStorageOptions.key]: LocalFileStorageProviderOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class LocalFileStorageProvider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly fileSystemProvider = $inject(FileSystemProvider);\n protected readonly options = $state(localFileStorageOptions);\n\n protected get storagePath(): string {\n return this.options.storagePath;\n }\n\n protected readonly onConfigure = $hook({\n on: \"configure\",\n handler: async () => {\n if (\n this.alepha.isTest() &&\n this.storagePath === localFileStorageOptions.options.default.storagePath\n ) {\n this.alepha.store.set(localFileStorageOptions, {\n storagePath: join(tmpdir(), `alepha-test-${Date.now()}`),\n });\n }\n },\n });\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n try {\n await mkdir(this.storagePath, { recursive: true });\n } catch {}\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n\n await mkdir(join(this.storagePath, bucket.name), {\n recursive: true,\n });\n\n this.log.debug(`Bucket '${bucket.name}' at ${this.storagePath} OK`);\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId(file.type);\n\n this.log.trace(`Uploading file to ${bucketName}`);\n\n await this.fileSystemProvider.writeFile(\n this.path(bucketName, fileId),\n file,\n );\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const filePath = this.path(bucketName, fileId);\n\n try {\n const stats = await stat(filePath);\n const mimeType = this.fileDetector.getContentType(fileId);\n\n return this.fileSystemProvider.createFile({\n stream: createReadStream(filePath),\n name: fileId,\n type: mimeType,\n size: stats.size,\n });\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Invalid file operation\", { cause: error });\n }\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n try {\n await stat(this.path(bucketName, fileId));\n return true;\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n return false;\n }\n throw new AlephaError(\"Error checking file existence\", { cause: error });\n }\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n try {\n return await unlink(this.path(bucketName, fileId));\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Error deleting file\", { cause: error });\n }\n }\n\n protected stat(bucket: string, fileId: string): Promise<fs.Stats> {\n return stat(this.path(bucket, fileId));\n }\n\n protected createId(mimeType: string): string {\n const ext = this.fileDetector.getExtensionFromMimeType(mimeType);\n return `${randomUUID()}.${ext}`;\n }\n\n protected path(bucket: string, fileId = \"\"): string {\n return join(this.storagePath, bucket, fileId);\n }\n\n protected isErrorNoEntry(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n }\n}\n","import {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * R2Bucket interface matching Cloudflare's R2 API.\n */\nexport interface R2Bucket {\n put(\n key: string,\n value:\n | ReadableStream\n | ArrayBuffer\n | ArrayBufferView\n | string\n | Blob\n | null,\n options?: R2PutOptions,\n ): Promise<R2Object | null>;\n get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;\n head(key: string): Promise<R2Object | null>;\n delete(keys: string | string[]): Promise<void>;\n list(options?: R2ListOptions): Promise<R2Objects>;\n createMultipartUpload(\n key: string,\n options?: R2MultipartOptions,\n ): Promise<R2MultipartUpload>;\n}\n\nexport interface R2Object {\n key: string;\n version: string;\n size: number;\n etag: string;\n httpEtag: string;\n checksums: R2Checksums;\n uploaded: Date;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n range?: R2Range;\n storageClass: string;\n}\n\nexport interface R2ObjectBody extends R2Object {\n body: ReadableStream;\n bodyUsed: boolean;\n arrayBuffer(): Promise<ArrayBuffer>;\n text(): Promise<string>;\n json<T>(): Promise<T>;\n blob(): Promise<Blob>;\n}\n\nexport interface R2PutOptions {\n onlyIf?: R2Conditional;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n md5?: ArrayBuffer | string;\n sha1?: ArrayBuffer | string;\n sha256?: ArrayBuffer | string;\n sha384?: ArrayBuffer | string;\n sha512?: ArrayBuffer | string;\n storageClass?: string;\n}\n\nexport interface R2GetOptions {\n onlyIf?: R2Conditional;\n range?: R2Range;\n}\n\nexport interface R2ListOptions {\n limit?: number;\n prefix?: string;\n cursor?: string;\n delimiter?: string;\n startAfter?: string;\n include?: (\"httpMetadata\" | \"customMetadata\")[];\n}\n\nexport interface R2Objects {\n objects: R2Object[];\n truncated: boolean;\n cursor?: string;\n delimitedPrefixes: string[];\n}\n\nexport interface R2Checksums {\n md5?: ArrayBuffer;\n sha1?: ArrayBuffer;\n sha256?: ArrayBuffer;\n sha384?: ArrayBuffer;\n sha512?: ArrayBuffer;\n}\n\nexport interface R2HTTPMetadata {\n contentType?: string;\n contentLanguage?: string;\n contentDisposition?: string;\n contentEncoding?: string;\n cacheControl?: string;\n cacheExpiry?: Date;\n}\n\nexport interface R2Conditional {\n etagMatches?: string;\n etagDoesNotMatch?: string;\n uploadedBefore?: Date;\n uploadedAfter?: Date;\n secondsGranularity?: boolean;\n}\n\nexport interface R2Range {\n offset?: number;\n length?: number;\n suffix?: number;\n}\n\nexport interface R2MultipartOptions {\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n storageClass?: string;\n}\n\nexport interface R2MultipartUpload {\n key: string;\n uploadId: string;\n uploadPart(\n partNumber: number,\n value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,\n ): Promise<R2UploadedPart>;\n abort(): Promise<void>;\n complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;\n}\n\nexport interface R2UploadedPart {\n partNumber: number;\n etag: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cloudflare R2 storage provider.\n *\n * Uses a single R2 bucket binding for all $bucket primitives.\n * Files are organized as: {APP_NAME}/{bucketName}/{fileId}\n *\n * **Required environment variables:**\n * - `R2_BUCKET_NAME` - The actual R2 bucket name in Cloudflare\n *\n * **Optional (uses core Alepha env):**\n * - `APP_NAME` - Prefix for all files (for multi-app setups sharing one R2 bucket)\n *\n * @example\n * ```bash\n * # .env\n * APP_NAME=myapp # optional, used as prefix\n * R2_BUCKET_NAME=storage\n * ```\n *\n * @example\n * ```toml\n * # wrangler.toml - automatically generated by alepha build\n * [[r2_buckets]]\n * binding = \"R2\"\n * bucket_name = \"storage\"\n * ```\n *\n * @example\n * ```ts\n * // Define buckets with validation rules\n * const avatars = $bucket({\n * name: \"avatars\",\n * maxFileSize: 5 * 1024 * 1024,\n * allowedMimeTypes: [\"image/*\"],\n * });\n *\n * const documents = $bucket({\n * name: \"documents\",\n * maxFileSize: 50 * 1024 * 1024,\n * allowedMimeTypes: [\"application/pdf\"],\n * });\n *\n * // Files stored at: myapp/avatars/uuid.png, myapp/documents/uuid.pdf\n * ```\n */\nexport class CloudflareR2Provider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n /**\n * The actual R2 bucket name in Cloudflare.\n */\n R2_BUCKET_NAME: t.string({\n description: \"R2 bucket name in Cloudflare\",\n }),\n }),\n );\n\n protected r2?: R2Bucket;\n\n /**\n * Get the R2 bucket name from environment.\n */\n public get bucketName(): string {\n return this.env.R2_BUCKET_NAME;\n }\n\n /**\n * Get the optional prefix from APP_NAME environment variable.\n * Used for multi-app setups sharing the same R2 bucket.\n */\n public get prefix(): string | undefined {\n return this.alepha.env.APP_NAME;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n const cloudflareEnv = this.alepha.get(\"cloudflare.env\") as\n | Record<string, unknown>\n | undefined;\n if (!cloudflareEnv) {\n throw new AlephaError(\n \"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.\",\n );\n }\n\n const binding = cloudflareEnv[this.bucketName] as R2Bucket | undefined;\n if (!binding) {\n throw new AlephaError(\n `R2 binding '${this.bucketName}' not found in Cloudflare Workers environment.`,\n );\n }\n\n this.r2 = binding;\n\n const prefixStr = this.prefix ? `${this.prefix}/` : \"\";\n this.log.info(\n `R2 storage ready (bucket: ${this.bucketName}, prefix: ${prefixStr || \"(none)\"})`,\n );\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n this.log.debug(\n `Bucket '${bucket.name}' -> ${prefixStr}${bucket.name}/`,\n );\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n const r2 = this.getR2();\n fileId ??= this.createId(file.name);\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Uploading '${key}'`);\n\n const arrayBuffer = await file.arrayBuffer();\n\n await r2.put(key, arrayBuffer, {\n httpMetadata: {\n contentType: file.type,\n },\n customMetadata: {\n originalName: file.name,\n bucket: bucketName,\n },\n });\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Downloading '${key}'`);\n\n const object = await r2.get(key);\n if (!object) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n const originalName = object.customMetadata?.originalName ?? fileId;\n const contentType =\n object.httpMetadata?.contentType ?? \"application/octet-stream\";\n\n return {\n name: originalName,\n type: contentType,\n size: object.size,\n lastModified: object.uploaded.getTime(),\n stream: () => object.body,\n arrayBuffer: () => object.arrayBuffer(),\n text: () => object.text(),\n };\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Checking '${key}'`);\n\n const object = await r2.head(key);\n return object !== null;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Deleting '${key}'`);\n\n // R2's delete doesn't throw if the file doesn't exist,\n // so we check existence first for consistency with other providers\n const exists = await this.exists(bucketName, fileId);\n if (!exists) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n await r2.delete(key);\n }\n\n /**\n * Build the full R2 key: {prefix}/{bucketName}/{fileId}\n */\n protected key(bucketName: string, fileId: string): string {\n const parts = [bucketName, fileId];\n if (this.prefix) {\n parts.unshift(this.prefix);\n }\n return parts.join(\"/\");\n }\n\n protected getR2(): R2Bucket {\n if (!this.r2) {\n throw new AlephaError(\"R2 storage not initialized. Call start() first.\");\n }\n return this.r2;\n }\n\n protected createId(filename: string): string {\n const ext = filename.includes(\".\") ? filename.split(\".\").pop() : \"\";\n const id = crypto.randomUUID();\n return ext ? `${id}.${ext}` : id;\n }\n}\n","import { $module, type FileLike } from \"alepha\";\nimport {\n $bucket,\n type BucketFileOptions,\n type BucketPrimitive,\n} from \"./primitives/$bucket.ts\";\nimport { FileStorageProvider } from \"./providers/FileStorageProvider.ts\";\nimport { LocalFileStorageProvider } from \"./providers/LocalFileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/FileNotFoundError.ts\";\nexport * from \"./primitives/$bucket.ts\";\nexport * from \"./providers/CloudflareR2Provider.ts\";\nexport * from \"./providers/FileStorageProvider.ts\";\nexport * from \"./providers/LocalFileStorageProvider.ts\";\nexport * from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Hooks {\n /**\n * Triggered when a file is uploaded to a bucket.\n * Can be used to perform actions after a file is uploaded, like creating a database record!\n */\n \"bucket:file:uploaded\": {\n id: string;\n file: FileLike;\n bucket: BucketPrimitive;\n options: BucketFileOptions;\n };\n /**\n * Triggered when a file is deleted from a bucket.\n */\n \"bucket:file:deleted\": {\n id: string;\n bucket: BucketPrimitive;\n };\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Unified file storage abstraction across multiple backends.\n *\n * **Features:**\n * - File storage buckets with constraints\n * - Unified API across all storage backends\n * - MIME type validation\n * - File size limits\n * - Upload/download/delete operations\n * - TTL-based file expiration\n * - Providers: Memory (testing), Local filesystem, AWS S3 / Cloudflare R2 / MinIO, Azure Blob Storage, Vercel Blob\n *\n * @module alepha.bucket\n */\nexport const AlephaBucket = $module({\n name: \"alepha.bucket\",\n primitives: [$bucket],\n services: [FileStorageProvider],\n variants: [MemoryFileStorageProvider, LocalFileStorageProvider],\n register: (alepha) => {\n alepha.with({\n optional: true,\n provide: FileStorageProvider,\n use:\n alepha.isTest() || alepha.isServerless()\n ? MemoryFileStorageProvider\n : LocalFileStorageProvider,\n });\n },\n});\n"],"mappings":"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"}
|
|
@@ -219,7 +219,11 @@ $bucket[KIND] = BucketPrimitive;
|
|
|
219
219
|
var CloudflareR2Provider = class {
|
|
220
220
|
alepha = $inject(Alepha);
|
|
221
221
|
log = $logger();
|
|
222
|
-
env = $env(t.object({
|
|
222
|
+
env = $env(t.object({
|
|
223
|
+
/**
|
|
224
|
+
* The actual R2 bucket name in Cloudflare.
|
|
225
|
+
*/
|
|
226
|
+
R2_BUCKET_NAME: t.string({ description: "R2 bucket name in Cloudflare" }) }));
|
|
223
227
|
r2;
|
|
224
228
|
/**
|
|
225
229
|
* Get the R2 bucket name from environment.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.workerd.js","names":[],"sources":["../../src/bucket/errors/InvalidFileError.ts","../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/index.workerd.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport class InvalidFileError extends AlephaError {\n public readonly status = 400;\n}\n","import type { FileLike } from \"alepha\";\n\nexport abstract class FileStorageProvider {\n /**\n * Uploads a file to the storage.\n *\n * @param bucketName - Container name\n * @param file - File to upload\n * @param fileId - Optional file identifier. If not provided, a unique ID will be generated.\n * @return The identifier of the uploaded file.\n */\n abstract upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string>;\n\n /**\n * Downloads a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to download\n * @return The downloaded file as a FileLike object.\n */\n abstract download(bucketName: string, fileId: string): Promise<FileLike>;\n\n /**\n * Check if fileId exists in the storage bucket.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to stream\n * @return True is the file exists, false otherwise.\n */\n abstract exists(bucketName: string, fileId: string): Promise<boolean>;\n\n /**\n * Delete permanently a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to delete\n */\n abstract delete(bucketName: string, fileId: string): Promise<void>;\n}\n","import { AlephaError } from \"alepha\";\n\nexport class FileNotFoundError extends AlephaError {\n public readonly status = 404;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { $inject, type FileLike } from \"alepha\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\ninterface StoredFile {\n buffer: Buffer;\n name: string;\n type: string;\n size: number;\n}\n\nexport class MemoryFileStorageProvider implements FileStorageProvider {\n public readonly files: Record<string, StoredFile> = {};\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId();\n\n // Consume the stream and store as buffer so downloads are repeatable\n const chunks: Uint8Array[] = [];\n for await (const chunk of file.stream() as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n this.files[`${bucketName}/${fileId}`] = {\n buffer,\n name: file.name,\n type: file.type,\n size: file.size,\n };\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const fileKey = `${bucketName}/${fileId}`;\n const stored = this.files[fileKey];\n\n if (!stored) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n // Create a fresh FileLike with a new stream from the stored buffer\n return this.fileSystem.createFile({\n stream: new Blob([new Uint8Array(stored.buffer)]).stream(),\n name: stored.name,\n type: stored.type,\n size: stored.size,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n return `${bucketName}/${fileId}` in this.files;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const fileKey = `${bucketName}/${fileId}`;\n if (!(fileKey in this.files)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n delete this.files[fileKey];\n }\n\n protected createId(): string {\n return randomUUID();\n }\n}\n","import {\n $inject,\n createPrimitive,\n type FileLike,\n KIND,\n Primitive,\n type Service,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { InvalidFileError } from \"../errors/InvalidFileError.ts\";\nimport { FileStorageProvider } from \"../providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"../providers/MemoryFileStorageProvider.ts\";\n\n/**\n * Creates a bucket primitive for file storage and management with configurable validation.\n *\n * Provides a comprehensive file storage system that handles uploads, downloads, validation,\n * and management across multiple storage backends with MIME type and size limit controls.\n *\n * **Key Features**\n * - Multi-provider support (filesystem, cloud storage, in-memory)\n * - Automatic MIME type and file size validation\n * - Event integration for file operations monitoring\n * - Flexible per-bucket and per-operation configuration\n * - Smart file type and size detection\n *\n * **Common Use Cases**\n * - User profile pictures and document uploads\n * - Product images and media management\n * - Document storage and retrieval systems\n *\n * @example\n * ```ts\n * class MediaService {\n * images = $bucket({\n * name: \"user-images\",\n * mimeTypes: [\"image/jpeg\", \"image/png\", \"image/gif\"],\n * maxSize: 5 // 5MB limit\n * });\n *\n * documents = $bucket({\n * name: \"documents\",\n * mimeTypes: [\"application/pdf\", \"text/plain\"],\n * maxSize: 50 // 50MB limit\n * });\n *\n * async uploadProfileImage(file: FileLike, userId: string): Promise<string> {\n * const fileId = await this.images.upload(file);\n * await this.userService.updateProfileImage(userId, fileId);\n * return fileId;\n * }\n *\n * async downloadDocument(documentId: string): Promise<FileLike> {\n * return await this.documents.download(documentId);\n * }\n *\n * async deleteDocument(documentId: string): Promise<void> {\n * await this.documents.delete(documentId);\n * }\n * }\n * ```\n */\nexport const $bucket = (options: BucketPrimitiveOptions) =>\n createPrimitive(BucketPrimitive, options);\n\nexport interface BucketPrimitiveOptions extends BucketFileOptions {\n /**\n * File storage provider configuration for the bucket.\n *\n * Options:\n * - **\"memory\"**: In-memory storage (default for development, lost on restart)\n * - **Service<FileStorageProvider>**: Custom provider class (e.g., S3FileStorageProvider, AzureBlobProvider)\n * - **undefined**: Uses the default file storage provider from dependency injection\n *\n * **Provider Selection Guidelines**:\n * - **Development**: Use \"memory\" for fast, simple testing without external dependencies\n * - **Production**: Use cloud providers (S3, Azure Blob, Google Cloud Storage) for scalability\n * - **Local deployment**: Use filesystem providers for on-premise installations\n * - **Hybrid**: Use different providers for different bucket types (temp files vs permanent storage)\n *\n * **Provider Capabilities**:\n * - File persistence and durability guarantees\n * - Scalability and performance characteristics\n * - Geographic distribution and CDN integration\n * - Cost implications for storage and bandwidth\n * - Backup and disaster recovery features\n *\n * @default Uses injected FileStorageProvider\n * @example \"memory\"\n * @example S3FileStorageProvider\n * @example AzureBlobStorageProvider\n */\n provider?: Service<FileStorageProvider> | \"memory\";\n\n /**\n * Unique name identifier for the bucket.\n *\n * This name is used for:\n * - Storage backend organization and partitioning\n * - File path generation and URL construction\n * - Logging, monitoring, and debugging\n * - Access control and permissions management\n * - Backup and replication configuration\n *\n * **Naming Conventions**:\n * - Use lowercase with hyphens for consistency\n * - Include purpose or content type in the name\n * - Avoid spaces and special characters\n * - Consider environment prefixes for deployment isolation\n *\n * If not provided, defaults to the property key where the bucket is declared.\n *\n * @example \"user-avatars\"\n * @example \"product-images\"\n * @example \"legal-documents\"\n * @example \"temp-processing-files\"\n */\n name?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Human-readable description of the bucket's purpose and contents.\n *\n * Used for:\n * - Documentation generation and API references\n * - Developer onboarding and system understanding\n * - Monitoring dashboards and admin interfaces\n * - Compliance and audit documentation\n *\n * **Description Best Practices**:\n * - Explain what types of files this bucket stores\n * - Mention any special handling or processing requirements\n * - Include information about retention policies if applicable\n * - Note any compliance or security considerations\n *\n * @example \"User profile pictures and avatar images\"\n * @example \"Product catalog images with automated thumbnail generation\"\n * @example \"Legal documents requiring long-term retention\"\n * @example \"Temporary files for data processing workflows\"\n */\n description?: string;\n\n /**\n * Array of allowed MIME types for files uploaded to this bucket.\n *\n * When specified, only files with these exact MIME types will be accepted.\n * Files with disallowed MIME types will be rejected with an InvalidFileError.\n *\n * **MIME Type Categories**:\n * - Images: \"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/svg+xml\"\n * - Documents: \"application/pdf\", \"text/plain\", \"text/csv\"\n * - Office: \"application/msword\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n * - Archives: \"application/zip\", \"application/x-tar\", \"application/gzip\"\n * - Media: \"video/mp4\", \"audio/mpeg\", \"audio/wav\"\n *\n * **Security Considerations**:\n * - Always validate MIME types for user uploads\n * - Be cautious with executable file types\n * - Consider using allow-lists rather than deny-lists\n * - Remember that MIME types can be spoofed by malicious users\n *\n * If not specified, all MIME types are allowed (not recommended for user uploads).\n *\n * @example [\"image/jpeg\", \"image/png\"] // Only JPEG and PNG images\n * @example [\"application/pdf\", \"text/plain\"] // Documents only\n * @example [\"video/mp4\", \"video/webm\"] // Video files\n */\n mimeTypes?: string[];\n\n /**\n * Maximum file size allowed in megabytes (MB).\n *\n * Files larger than this limit will be rejected with an InvalidFileError.\n * This helps prevent:\n * - Storage quota exhaustion\n * - Memory issues during file processing\n * - Long upload times and timeouts\n * - Abuse of storage resources\n *\n * **Size Guidelines by File Type**:\n * - Profile images: 1-5 MB\n * - Product photos: 5-10 MB\n * - Documents: 10-50 MB\n * - Video files: 50-500 MB\n * - Data files: 100-1000 MB\n *\n * **Considerations**:\n * - Consider your storage costs and limits\n * - Factor in network upload speeds for users\n * - Account for processing requirements (thumbnails, compression)\n * - Set reasonable limits based on actual use cases\n *\n * @default 10 MB\n *\n * @example 1 // 1MB for small images\n * @example 25 // 25MB for documents\n * @example 100 // 100MB for media files\n */\n maxSize?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BucketPrimitive extends Primitive<BucketPrimitiveOptions> {\n public readonly provider = this.$provider();\n protected readonly fileSystem = $inject(FileSystemProvider);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Uploads a file to the bucket.\n */\n public async upload(\n file: FileLike,\n options?: BucketFileOptions,\n ): Promise<string> {\n if (file instanceof File) {\n // our createFile is smarter than the browser's File constructor\n // by doing this, we can guess the MIME type and size!\n file = this.fileSystem.createFile({ file });\n }\n\n options = {\n ...this.options,\n ...options,\n };\n\n const mimeTypes = options.mimeTypes ?? undefined;\n const maxSize = options.maxSize ?? 10; // Default to 10 MB if not specified\n\n if (mimeTypes) {\n const mimeType = file.type || \"application/octet-stream\";\n if (!mimeTypes.includes(mimeType)) {\n throw new InvalidFileError(\n `MIME type ${mimeType} is not allowed in bucket ${this.name}`,\n );\n }\n }\n\n // check size in bytes, convert MB to bytes\n if (file.size > maxSize * 1024 * 1024) {\n throw new InvalidFileError(\n `File size ${file.size} exceeds the maximum size of ${maxSize} MB in bucket ${this.name}`,\n );\n }\n\n const id = await this.provider.upload(this.name, file);\n\n await this.alepha.events.emit(\"bucket:file:uploaded\", {\n id,\n bucket: this,\n file,\n options,\n });\n\n return id;\n }\n\n /**\n * Delete permanently a file from the bucket.\n */\n public async delete(fileId: string, skipHook = false): Promise<void> {\n await this.provider.delete(this.name, fileId);\n\n if (skipHook) {\n return;\n }\n\n await this.alepha.events.emit(\"bucket:file:deleted\", {\n id: fileId,\n bucket: this,\n });\n }\n\n /**\n * Checks if a file exists in the bucket.\n */\n public async exists(fileId: string): Promise<boolean> {\n return this.provider.exists(this.name, fileId);\n }\n\n /**\n * Downloads a file from the bucket.\n */\n public async download(fileId: string): Promise<FileLike> {\n return this.provider.download(this.name, fileId);\n }\n\n protected $provider() {\n if (!this.options.provider) {\n return this.alepha.inject(FileStorageProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemoryFileStorageProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n$bucket[KIND] = BucketPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Optional description of the bucket.\n */\n description?: string;\n\n /**\n * Allowed MIME types.\n */\n mimeTypes?: string[];\n\n /**\n * Maximum size of the files in the bucket.\n *\n * @default 10\n */\n maxSize?: number;\n}\n","import {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * R2Bucket interface matching Cloudflare's R2 API.\n */\nexport interface R2Bucket {\n put(\n key: string,\n value:\n | ReadableStream\n | ArrayBuffer\n | ArrayBufferView\n | string\n | Blob\n | null,\n options?: R2PutOptions,\n ): Promise<R2Object | null>;\n get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;\n head(key: string): Promise<R2Object | null>;\n delete(keys: string | string[]): Promise<void>;\n list(options?: R2ListOptions): Promise<R2Objects>;\n createMultipartUpload(\n key: string,\n options?: R2MultipartOptions,\n ): Promise<R2MultipartUpload>;\n}\n\nexport interface R2Object {\n key: string;\n version: string;\n size: number;\n etag: string;\n httpEtag: string;\n checksums: R2Checksums;\n uploaded: Date;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n range?: R2Range;\n storageClass: string;\n}\n\nexport interface R2ObjectBody extends R2Object {\n body: ReadableStream;\n bodyUsed: boolean;\n arrayBuffer(): Promise<ArrayBuffer>;\n text(): Promise<string>;\n json<T>(): Promise<T>;\n blob(): Promise<Blob>;\n}\n\nexport interface R2PutOptions {\n onlyIf?: R2Conditional;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n md5?: ArrayBuffer | string;\n sha1?: ArrayBuffer | string;\n sha256?: ArrayBuffer | string;\n sha384?: ArrayBuffer | string;\n sha512?: ArrayBuffer | string;\n storageClass?: string;\n}\n\nexport interface R2GetOptions {\n onlyIf?: R2Conditional;\n range?: R2Range;\n}\n\nexport interface R2ListOptions {\n limit?: number;\n prefix?: string;\n cursor?: string;\n delimiter?: string;\n startAfter?: string;\n include?: (\"httpMetadata\" | \"customMetadata\")[];\n}\n\nexport interface R2Objects {\n objects: R2Object[];\n truncated: boolean;\n cursor?: string;\n delimitedPrefixes: string[];\n}\n\nexport interface R2Checksums {\n md5?: ArrayBuffer;\n sha1?: ArrayBuffer;\n sha256?: ArrayBuffer;\n sha384?: ArrayBuffer;\n sha512?: ArrayBuffer;\n}\n\nexport interface R2HTTPMetadata {\n contentType?: string;\n contentLanguage?: string;\n contentDisposition?: string;\n contentEncoding?: string;\n cacheControl?: string;\n cacheExpiry?: Date;\n}\n\nexport interface R2Conditional {\n etagMatches?: string;\n etagDoesNotMatch?: string;\n uploadedBefore?: Date;\n uploadedAfter?: Date;\n secondsGranularity?: boolean;\n}\n\nexport interface R2Range {\n offset?: number;\n length?: number;\n suffix?: number;\n}\n\nexport interface R2MultipartOptions {\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n storageClass?: string;\n}\n\nexport interface R2MultipartUpload {\n key: string;\n uploadId: string;\n uploadPart(\n partNumber: number,\n value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,\n ): Promise<R2UploadedPart>;\n abort(): Promise<void>;\n complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;\n}\n\nexport interface R2UploadedPart {\n partNumber: number;\n etag: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cloudflare R2 storage provider.\n *\n * Uses a single R2 bucket binding for all $bucket primitives.\n * Files are organized as: {APP_NAME}/{bucketName}/{fileId}\n *\n * **Required environment variables:**\n * - `R2_BUCKET_NAME` - The actual R2 bucket name in Cloudflare\n *\n * **Optional (uses core Alepha env):**\n * - `APP_NAME` - Prefix for all files (for multi-app setups sharing one R2 bucket)\n *\n * @example\n * ```bash\n * # .env\n * APP_NAME=myapp # optional, used as prefix\n * R2_BUCKET_NAME=storage\n * ```\n *\n * @example\n * ```toml\n * # wrangler.toml - automatically generated by alepha build\n * [[r2_buckets]]\n * binding = \"R2\"\n * bucket_name = \"storage\"\n * ```\n *\n * @example\n * ```ts\n * // Define buckets with validation rules\n * const avatars = $bucket({\n * name: \"avatars\",\n * maxFileSize: 5 * 1024 * 1024,\n * allowedMimeTypes: [\"image/*\"],\n * });\n *\n * const documents = $bucket({\n * name: \"documents\",\n * maxFileSize: 50 * 1024 * 1024,\n * allowedMimeTypes: [\"application/pdf\"],\n * });\n *\n * // Files stored at: myapp/avatars/uuid.png, myapp/documents/uuid.pdf\n * ```\n */\nexport class CloudflareR2Provider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n /**\n * The actual R2 bucket name in Cloudflare.\n */\n R2_BUCKET_NAME: t.string({\n description: \"R2 bucket name in Cloudflare\",\n }),\n }),\n );\n\n protected r2?: R2Bucket;\n\n /**\n * Get the R2 bucket name from environment.\n */\n public get bucketName(): string {\n return this.env.R2_BUCKET_NAME;\n }\n\n /**\n * Get the optional prefix from APP_NAME environment variable.\n * Used for multi-app setups sharing the same R2 bucket.\n */\n public get prefix(): string | undefined {\n return this.alepha.env.APP_NAME;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n const cloudflareEnv = this.alepha.get(\"cloudflare.env\") as\n | Record<string, unknown>\n | undefined;\n if (!cloudflareEnv) {\n throw new AlephaError(\n \"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.\",\n );\n }\n\n const binding = cloudflareEnv[this.bucketName] as R2Bucket | undefined;\n if (!binding) {\n throw new AlephaError(\n `R2 binding '${this.bucketName}' not found in Cloudflare Workers environment.`,\n );\n }\n\n this.r2 = binding;\n\n const prefixStr = this.prefix ? `${this.prefix}/` : \"\";\n this.log.info(\n `R2 storage ready (bucket: ${this.bucketName}, prefix: ${prefixStr || \"(none)\"})`,\n );\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n this.log.debug(\n `Bucket '${bucket.name}' -> ${prefixStr}${bucket.name}/`,\n );\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n const r2 = this.getR2();\n fileId ??= this.createId(file.name);\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Uploading '${key}'`);\n\n const arrayBuffer = await file.arrayBuffer();\n\n await r2.put(key, arrayBuffer, {\n httpMetadata: {\n contentType: file.type,\n },\n customMetadata: {\n originalName: file.name,\n bucket: bucketName,\n },\n });\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Downloading '${key}'`);\n\n const object = await r2.get(key);\n if (!object) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n const originalName = object.customMetadata?.originalName ?? fileId;\n const contentType =\n object.httpMetadata?.contentType ?? \"application/octet-stream\";\n\n return {\n name: originalName,\n type: contentType,\n size: object.size,\n lastModified: object.uploaded.getTime(),\n stream: () => object.body,\n arrayBuffer: () => object.arrayBuffer(),\n text: () => object.text(),\n };\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Checking '${key}'`);\n\n const object = await r2.head(key);\n return object !== null;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Deleting '${key}'`);\n\n // R2's delete doesn't throw if the file doesn't exist,\n // so we check existence first for consistency with other providers\n const exists = await this.exists(bucketName, fileId);\n if (!exists) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n await r2.delete(key);\n }\n\n /**\n * Build the full R2 key: {prefix}/{bucketName}/{fileId}\n */\n protected key(bucketName: string, fileId: string): string {\n const parts = [bucketName, fileId];\n if (this.prefix) {\n parts.unshift(this.prefix);\n }\n return parts.join(\"/\");\n }\n\n protected getR2(): R2Bucket {\n if (!this.r2) {\n throw new AlephaError(\"R2 storage not initialized. Call start() first.\");\n }\n return this.r2;\n }\n\n protected createId(filename: string): string {\n const ext = filename.includes(\".\") ? filename.split(\".\").pop() : \"\";\n const id = crypto.randomUUID();\n return ext ? `${id}.${ext}` : id;\n }\n}\n","import { $module } from \"alepha\";\nimport { $bucket } from \"./primitives/$bucket.ts\";\nimport { CloudflareR2Provider } from \"./providers/CloudflareR2Provider.ts\";\nimport { FileStorageProvider } from \"./providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/FileNotFoundError.ts\";\nexport * from \"./primitives/$bucket.ts\";\nexport * from \"./providers/CloudflareR2Provider.ts\";\nexport * from \"./providers/FileStorageProvider.ts\";\nexport * from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaBucket = $module({\n name: \"alepha.bucket\",\n primitives: [$bucket],\n services: [\n FileStorageProvider,\n MemoryFileStorageProvider,\n CloudflareR2Provider,\n ],\n register: (alepha) => {\n alepha.with({\n optional: true,\n provide: FileStorageProvider,\n use: alepha.isTest() ? MemoryFileStorageProvider : CloudflareR2Provider,\n });\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.workerd.js","names":[],"sources":["../../src/bucket/errors/InvalidFileError.ts","../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/index.workerd.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport class InvalidFileError extends AlephaError {\n public readonly status = 400;\n}\n","import type { FileLike } from \"alepha\";\n\nexport abstract class FileStorageProvider {\n /**\n * Uploads a file to the storage.\n *\n * @param bucketName - Container name\n * @param file - File to upload\n * @param fileId - Optional file identifier. If not provided, a unique ID will be generated.\n * @return The identifier of the uploaded file.\n */\n abstract upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string>;\n\n /**\n * Downloads a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to download\n * @return The downloaded file as a FileLike object.\n */\n abstract download(bucketName: string, fileId: string): Promise<FileLike>;\n\n /**\n * Check if fileId exists in the storage bucket.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to stream\n * @return True is the file exists, false otherwise.\n */\n abstract exists(bucketName: string, fileId: string): Promise<boolean>;\n\n /**\n * Delete permanently a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to delete\n */\n abstract delete(bucketName: string, fileId: string): Promise<void>;\n}\n","import { AlephaError } from \"alepha\";\n\nexport class FileNotFoundError extends AlephaError {\n public readonly status = 404;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { $inject, type FileLike } from \"alepha\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\ninterface StoredFile {\n buffer: Buffer;\n name: string;\n type: string;\n size: number;\n}\n\nexport class MemoryFileStorageProvider implements FileStorageProvider {\n public readonly files: Record<string, StoredFile> = {};\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId();\n\n // Consume the stream and store as buffer so downloads are repeatable\n const chunks: Uint8Array[] = [];\n for await (const chunk of file.stream() as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n this.files[`${bucketName}/${fileId}`] = {\n buffer,\n name: file.name,\n type: file.type,\n size: file.size,\n };\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const fileKey = `${bucketName}/${fileId}`;\n const stored = this.files[fileKey];\n\n if (!stored) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n // Create a fresh FileLike with a new stream from the stored buffer\n return this.fileSystem.createFile({\n stream: new Blob([new Uint8Array(stored.buffer)]).stream(),\n name: stored.name,\n type: stored.type,\n size: stored.size,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n return `${bucketName}/${fileId}` in this.files;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const fileKey = `${bucketName}/${fileId}`;\n if (!(fileKey in this.files)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n delete this.files[fileKey];\n }\n\n protected createId(): string {\n return randomUUID();\n }\n}\n","import {\n $inject,\n createPrimitive,\n type FileLike,\n KIND,\n Primitive,\n type Service,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { InvalidFileError } from \"../errors/InvalidFileError.ts\";\nimport { FileStorageProvider } from \"../providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"../providers/MemoryFileStorageProvider.ts\";\n\n/**\n * Creates a bucket primitive for file storage and management with configurable validation.\n *\n * Provides a comprehensive file storage system that handles uploads, downloads, validation,\n * and management across multiple storage backends with MIME type and size limit controls.\n *\n * **Key Features**\n * - Multi-provider support (filesystem, cloud storage, in-memory)\n * - Automatic MIME type and file size validation\n * - Event integration for file operations monitoring\n * - Flexible per-bucket and per-operation configuration\n * - Smart file type and size detection\n *\n * **Common Use Cases**\n * - User profile pictures and document uploads\n * - Product images and media management\n * - Document storage and retrieval systems\n *\n * @example\n * ```ts\n * class MediaService {\n * images = $bucket({\n * name: \"user-images\",\n * mimeTypes: [\"image/jpeg\", \"image/png\", \"image/gif\"],\n * maxSize: 5 // 5MB limit\n * });\n *\n * documents = $bucket({\n * name: \"documents\",\n * mimeTypes: [\"application/pdf\", \"text/plain\"],\n * maxSize: 50 // 50MB limit\n * });\n *\n * async uploadProfileImage(file: FileLike, userId: string): Promise<string> {\n * const fileId = await this.images.upload(file);\n * await this.userService.updateProfileImage(userId, fileId);\n * return fileId;\n * }\n *\n * async downloadDocument(documentId: string): Promise<FileLike> {\n * return await this.documents.download(documentId);\n * }\n *\n * async deleteDocument(documentId: string): Promise<void> {\n * await this.documents.delete(documentId);\n * }\n * }\n * ```\n */\nexport const $bucket = (options: BucketPrimitiveOptions) =>\n createPrimitive(BucketPrimitive, options);\n\nexport interface BucketPrimitiveOptions extends BucketFileOptions {\n /**\n * File storage provider configuration for the bucket.\n *\n * Options:\n * - **\"memory\"**: In-memory storage (default for development, lost on restart)\n * - **Service<FileStorageProvider>**: Custom provider class (e.g., S3FileStorageProvider, AzureBlobProvider)\n * - **undefined**: Uses the default file storage provider from dependency injection\n *\n * **Provider Selection Guidelines**:\n * - **Development**: Use \"memory\" for fast, simple testing without external dependencies\n * - **Production**: Use cloud providers (S3, Azure Blob, Google Cloud Storage) for scalability\n * - **Local deployment**: Use filesystem providers for on-premise installations\n * - **Hybrid**: Use different providers for different bucket types (temp files vs permanent storage)\n *\n * **Provider Capabilities**:\n * - File persistence and durability guarantees\n * - Scalability and performance characteristics\n * - Geographic distribution and CDN integration\n * - Cost implications for storage and bandwidth\n * - Backup and disaster recovery features\n *\n * @default Uses injected FileStorageProvider\n * @example \"memory\"\n * @example S3FileStorageProvider\n * @example AzureBlobStorageProvider\n */\n provider?: Service<FileStorageProvider> | \"memory\";\n\n /**\n * Unique name identifier for the bucket.\n *\n * This name is used for:\n * - Storage backend organization and partitioning\n * - File path generation and URL construction\n * - Logging, monitoring, and debugging\n * - Access control and permissions management\n * - Backup and replication configuration\n *\n * **Naming Conventions**:\n * - Use lowercase with hyphens for consistency\n * - Include purpose or content type in the name\n * - Avoid spaces and special characters\n * - Consider environment prefixes for deployment isolation\n *\n * If not provided, defaults to the property key where the bucket is declared.\n *\n * @example \"user-avatars\"\n * @example \"product-images\"\n * @example \"legal-documents\"\n * @example \"temp-processing-files\"\n */\n name?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Human-readable description of the bucket's purpose and contents.\n *\n * Used for:\n * - Documentation generation and API references\n * - Developer onboarding and system understanding\n * - Monitoring dashboards and admin interfaces\n * - Compliance and audit documentation\n *\n * **Description Best Practices**:\n * - Explain what types of files this bucket stores\n * - Mention any special handling or processing requirements\n * - Include information about retention policies if applicable\n * - Note any compliance or security considerations\n *\n * @example \"User profile pictures and avatar images\"\n * @example \"Product catalog images with automated thumbnail generation\"\n * @example \"Legal documents requiring long-term retention\"\n * @example \"Temporary files for data processing workflows\"\n */\n description?: string;\n\n /**\n * Array of allowed MIME types for files uploaded to this bucket.\n *\n * When specified, only files with these exact MIME types will be accepted.\n * Files with disallowed MIME types will be rejected with an InvalidFileError.\n *\n * **MIME Type Categories**:\n * - Images: \"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/svg+xml\"\n * - Documents: \"application/pdf\", \"text/plain\", \"text/csv\"\n * - Office: \"application/msword\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n * - Archives: \"application/zip\", \"application/x-tar\", \"application/gzip\"\n * - Media: \"video/mp4\", \"audio/mpeg\", \"audio/wav\"\n *\n * **Security Considerations**:\n * - Always validate MIME types for user uploads\n * - Be cautious with executable file types\n * - Consider using allow-lists rather than deny-lists\n * - Remember that MIME types can be spoofed by malicious users\n *\n * If not specified, all MIME types are allowed (not recommended for user uploads).\n *\n * @example [\"image/jpeg\", \"image/png\"] // Only JPEG and PNG images\n * @example [\"application/pdf\", \"text/plain\"] // Documents only\n * @example [\"video/mp4\", \"video/webm\"] // Video files\n */\n mimeTypes?: string[];\n\n /**\n * Maximum file size allowed in megabytes (MB).\n *\n * Files larger than this limit will be rejected with an InvalidFileError.\n * This helps prevent:\n * - Storage quota exhaustion\n * - Memory issues during file processing\n * - Long upload times and timeouts\n * - Abuse of storage resources\n *\n * **Size Guidelines by File Type**:\n * - Profile images: 1-5 MB\n * - Product photos: 5-10 MB\n * - Documents: 10-50 MB\n * - Video files: 50-500 MB\n * - Data files: 100-1000 MB\n *\n * **Considerations**:\n * - Consider your storage costs and limits\n * - Factor in network upload speeds for users\n * - Account for processing requirements (thumbnails, compression)\n * - Set reasonable limits based on actual use cases\n *\n * @default 10 MB\n *\n * @example 1 // 1MB for small images\n * @example 25 // 25MB for documents\n * @example 100 // 100MB for media files\n */\n maxSize?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BucketPrimitive extends Primitive<BucketPrimitiveOptions> {\n public readonly provider = this.$provider();\n protected readonly fileSystem = $inject(FileSystemProvider);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Uploads a file to the bucket.\n */\n public async upload(\n file: FileLike,\n options?: BucketFileOptions,\n ): Promise<string> {\n if (file instanceof File) {\n // our createFile is smarter than the browser's File constructor\n // by doing this, we can guess the MIME type and size!\n file = this.fileSystem.createFile({ file });\n }\n\n options = {\n ...this.options,\n ...options,\n };\n\n const mimeTypes = options.mimeTypes ?? undefined;\n const maxSize = options.maxSize ?? 10; // Default to 10 MB if not specified\n\n if (mimeTypes) {\n const mimeType = file.type || \"application/octet-stream\";\n if (!mimeTypes.includes(mimeType)) {\n throw new InvalidFileError(\n `MIME type ${mimeType} is not allowed in bucket ${this.name}`,\n );\n }\n }\n\n // check size in bytes, convert MB to bytes\n if (file.size > maxSize * 1024 * 1024) {\n throw new InvalidFileError(\n `File size ${file.size} exceeds the maximum size of ${maxSize} MB in bucket ${this.name}`,\n );\n }\n\n const id = await this.provider.upload(this.name, file);\n\n await this.alepha.events.emit(\"bucket:file:uploaded\", {\n id,\n bucket: this,\n file,\n options,\n });\n\n return id;\n }\n\n /**\n * Delete permanently a file from the bucket.\n */\n public async delete(fileId: string, skipHook = false): Promise<void> {\n await this.provider.delete(this.name, fileId);\n\n if (skipHook) {\n return;\n }\n\n await this.alepha.events.emit(\"bucket:file:deleted\", {\n id: fileId,\n bucket: this,\n });\n }\n\n /**\n * Checks if a file exists in the bucket.\n */\n public async exists(fileId: string): Promise<boolean> {\n return this.provider.exists(this.name, fileId);\n }\n\n /**\n * Downloads a file from the bucket.\n */\n public async download(fileId: string): Promise<FileLike> {\n return this.provider.download(this.name, fileId);\n }\n\n protected $provider() {\n if (!this.options.provider) {\n return this.alepha.inject(FileStorageProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemoryFileStorageProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n$bucket[KIND] = BucketPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Optional description of the bucket.\n */\n description?: string;\n\n /**\n * Allowed MIME types.\n */\n mimeTypes?: string[];\n\n /**\n * Maximum size of the files in the bucket.\n *\n * @default 10\n */\n maxSize?: number;\n}\n","import {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * R2Bucket interface matching Cloudflare's R2 API.\n */\nexport interface R2Bucket {\n put(\n key: string,\n value:\n | ReadableStream\n | ArrayBuffer\n | ArrayBufferView\n | string\n | Blob\n | null,\n options?: R2PutOptions,\n ): Promise<R2Object | null>;\n get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;\n head(key: string): Promise<R2Object | null>;\n delete(keys: string | string[]): Promise<void>;\n list(options?: R2ListOptions): Promise<R2Objects>;\n createMultipartUpload(\n key: string,\n options?: R2MultipartOptions,\n ): Promise<R2MultipartUpload>;\n}\n\nexport interface R2Object {\n key: string;\n version: string;\n size: number;\n etag: string;\n httpEtag: string;\n checksums: R2Checksums;\n uploaded: Date;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n range?: R2Range;\n storageClass: string;\n}\n\nexport interface R2ObjectBody extends R2Object {\n body: ReadableStream;\n bodyUsed: boolean;\n arrayBuffer(): Promise<ArrayBuffer>;\n text(): Promise<string>;\n json<T>(): Promise<T>;\n blob(): Promise<Blob>;\n}\n\nexport interface R2PutOptions {\n onlyIf?: R2Conditional;\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n md5?: ArrayBuffer | string;\n sha1?: ArrayBuffer | string;\n sha256?: ArrayBuffer | string;\n sha384?: ArrayBuffer | string;\n sha512?: ArrayBuffer | string;\n storageClass?: string;\n}\n\nexport interface R2GetOptions {\n onlyIf?: R2Conditional;\n range?: R2Range;\n}\n\nexport interface R2ListOptions {\n limit?: number;\n prefix?: string;\n cursor?: string;\n delimiter?: string;\n startAfter?: string;\n include?: (\"httpMetadata\" | \"customMetadata\")[];\n}\n\nexport interface R2Objects {\n objects: R2Object[];\n truncated: boolean;\n cursor?: string;\n delimitedPrefixes: string[];\n}\n\nexport interface R2Checksums {\n md5?: ArrayBuffer;\n sha1?: ArrayBuffer;\n sha256?: ArrayBuffer;\n sha384?: ArrayBuffer;\n sha512?: ArrayBuffer;\n}\n\nexport interface R2HTTPMetadata {\n contentType?: string;\n contentLanguage?: string;\n contentDisposition?: string;\n contentEncoding?: string;\n cacheControl?: string;\n cacheExpiry?: Date;\n}\n\nexport interface R2Conditional {\n etagMatches?: string;\n etagDoesNotMatch?: string;\n uploadedBefore?: Date;\n uploadedAfter?: Date;\n secondsGranularity?: boolean;\n}\n\nexport interface R2Range {\n offset?: number;\n length?: number;\n suffix?: number;\n}\n\nexport interface R2MultipartOptions {\n httpMetadata?: R2HTTPMetadata;\n customMetadata?: Record<string, string>;\n storageClass?: string;\n}\n\nexport interface R2MultipartUpload {\n key: string;\n uploadId: string;\n uploadPart(\n partNumber: number,\n value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,\n ): Promise<R2UploadedPart>;\n abort(): Promise<void>;\n complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;\n}\n\nexport interface R2UploadedPart {\n partNumber: number;\n etag: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cloudflare R2 storage provider.\n *\n * Uses a single R2 bucket binding for all $bucket primitives.\n * Files are organized as: {APP_NAME}/{bucketName}/{fileId}\n *\n * **Required environment variables:**\n * - `R2_BUCKET_NAME` - The actual R2 bucket name in Cloudflare\n *\n * **Optional (uses core Alepha env):**\n * - `APP_NAME` - Prefix for all files (for multi-app setups sharing one R2 bucket)\n *\n * @example\n * ```bash\n * # .env\n * APP_NAME=myapp # optional, used as prefix\n * R2_BUCKET_NAME=storage\n * ```\n *\n * @example\n * ```toml\n * # wrangler.toml - automatically generated by alepha build\n * [[r2_buckets]]\n * binding = \"R2\"\n * bucket_name = \"storage\"\n * ```\n *\n * @example\n * ```ts\n * // Define buckets with validation rules\n * const avatars = $bucket({\n * name: \"avatars\",\n * maxFileSize: 5 * 1024 * 1024,\n * allowedMimeTypes: [\"image/*\"],\n * });\n *\n * const documents = $bucket({\n * name: \"documents\",\n * maxFileSize: 50 * 1024 * 1024,\n * allowedMimeTypes: [\"application/pdf\"],\n * });\n *\n * // Files stored at: myapp/avatars/uuid.png, myapp/documents/uuid.pdf\n * ```\n */\nexport class CloudflareR2Provider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n /**\n * The actual R2 bucket name in Cloudflare.\n */\n R2_BUCKET_NAME: t.string({\n description: \"R2 bucket name in Cloudflare\",\n }),\n }),\n );\n\n protected r2?: R2Bucket;\n\n /**\n * Get the R2 bucket name from environment.\n */\n public get bucketName(): string {\n return this.env.R2_BUCKET_NAME;\n }\n\n /**\n * Get the optional prefix from APP_NAME environment variable.\n * Used for multi-app setups sharing the same R2 bucket.\n */\n public get prefix(): string | undefined {\n return this.alepha.env.APP_NAME;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n const cloudflareEnv = this.alepha.get(\"cloudflare.env\") as\n | Record<string, unknown>\n | undefined;\n if (!cloudflareEnv) {\n throw new AlephaError(\n \"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.\",\n );\n }\n\n const binding = cloudflareEnv[this.bucketName] as R2Bucket | undefined;\n if (!binding) {\n throw new AlephaError(\n `R2 binding '${this.bucketName}' not found in Cloudflare Workers environment.`,\n );\n }\n\n this.r2 = binding;\n\n const prefixStr = this.prefix ? `${this.prefix}/` : \"\";\n this.log.info(\n `R2 storage ready (bucket: ${this.bucketName}, prefix: ${prefixStr || \"(none)\"})`,\n );\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n this.log.debug(\n `Bucket '${bucket.name}' -> ${prefixStr}${bucket.name}/`,\n );\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n const r2 = this.getR2();\n fileId ??= this.createId(file.name);\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Uploading '${key}'`);\n\n const arrayBuffer = await file.arrayBuffer();\n\n await r2.put(key, arrayBuffer, {\n httpMetadata: {\n contentType: file.type,\n },\n customMetadata: {\n originalName: file.name,\n bucket: bucketName,\n },\n });\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Downloading '${key}'`);\n\n const object = await r2.get(key);\n if (!object) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n const originalName = object.customMetadata?.originalName ?? fileId;\n const contentType =\n object.httpMetadata?.contentType ?? \"application/octet-stream\";\n\n return {\n name: originalName,\n type: contentType,\n size: object.size,\n lastModified: object.uploaded.getTime(),\n stream: () => object.body,\n arrayBuffer: () => object.arrayBuffer(),\n text: () => object.text(),\n };\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Checking '${key}'`);\n\n const object = await r2.head(key);\n return object !== null;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Deleting '${key}'`);\n\n // R2's delete doesn't throw if the file doesn't exist,\n // so we check existence first for consistency with other providers\n const exists = await this.exists(bucketName, fileId);\n if (!exists) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n await r2.delete(key);\n }\n\n /**\n * Build the full R2 key: {prefix}/{bucketName}/{fileId}\n */\n protected key(bucketName: string, fileId: string): string {\n const parts = [bucketName, fileId];\n if (this.prefix) {\n parts.unshift(this.prefix);\n }\n return parts.join(\"/\");\n }\n\n protected getR2(): R2Bucket {\n if (!this.r2) {\n throw new AlephaError(\"R2 storage not initialized. Call start() first.\");\n }\n return this.r2;\n }\n\n protected createId(filename: string): string {\n const ext = filename.includes(\".\") ? filename.split(\".\").pop() : \"\";\n const id = crypto.randomUUID();\n return ext ? `${id}.${ext}` : id;\n }\n}\n","import { $module } from \"alepha\";\nimport { $bucket } from \"./primitives/$bucket.ts\";\nimport { CloudflareR2Provider } from \"./providers/CloudflareR2Provider.ts\";\nimport { FileStorageProvider } from \"./providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/FileNotFoundError.ts\";\nexport * from \"./primitives/$bucket.ts\";\nexport * from \"./providers/CloudflareR2Provider.ts\";\nexport * from \"./providers/FileStorageProvider.ts\";\nexport * from \"./providers/MemoryFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaBucket = $module({\n name: \"alepha.bucket\",\n primitives: [$bucket],\n services: [\n FileStorageProvider,\n MemoryFileStorageProvider,\n CloudflareR2Provider,\n ],\n register: (alepha) => {\n alepha.with({\n optional: true,\n provide: FileStorageProvider,\n use: alepha.isTest() ? MemoryFileStorageProvider : CloudflareR2Provider,\n });\n },\n});\n"],"mappings":"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"}
|