@trokky/trokky 2.0.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/dist/adapters/filesystem/filesystem-adapter.d.ts +80 -0
- package/dist/adapters/filesystem/filesystem-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem/filesystem-adapter.js +1370 -0
- package/dist/adapters/filesystem/filesystem-adapter.js.map +1 -0
- package/dist/adapters/filesystem/index.d.ts +29 -0
- package/dist/adapters/filesystem/index.d.ts.map +1 -0
- package/dist/adapters/filesystem/index.js +31 -0
- package/dist/adapters/filesystem/index.js.map +1 -0
- package/dist/adapters/filesystem/types.d.ts +111 -0
- package/dist/adapters/filesystem/types.d.ts.map +1 -0
- package/dist/adapters/filesystem/types.js +2 -0
- package/dist/adapters/filesystem/types.js.map +1 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.d.ts +111 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.js +1363 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.js.map +1 -0
- package/dist/adapters/filesystem-data/index.d.ts +3 -0
- package/dist/adapters/filesystem-data/index.d.ts.map +1 -0
- package/dist/adapters/filesystem-data/index.js +13 -0
- package/dist/adapters/filesystem-data/index.js.map +1 -0
- package/dist/adapters/filesystem-data/types.d.ts +147 -0
- package/dist/adapters/filesystem-data/types.d.ts.map +1 -0
- package/dist/adapters/filesystem-data/types.js +2 -0
- package/dist/adapters/filesystem-data/types.js.map +1 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.d.ts +32 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.js +639 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.js.map +1 -0
- package/dist/adapters/filesystem-media/index.d.ts +3 -0
- package/dist/adapters/filesystem-media/index.d.ts.map +1 -0
- package/dist/adapters/filesystem-media/index.js +13 -0
- package/dist/adapters/filesystem-media/index.js.map +1 -0
- package/dist/adapters/filesystem-media/types.d.ts +76 -0
- package/dist/adapters/filesystem-media/types.d.ts.map +1 -0
- package/dist/adapters/filesystem-media/types.js +2 -0
- package/dist/adapters/filesystem-media/types.js.map +1 -0
- package/dist/adapters/postgres-data/index.d.ts +3 -0
- package/dist/adapters/postgres-data/index.d.ts.map +1 -0
- package/dist/adapters/postgres-data/index.js +13 -0
- package/dist/adapters/postgres-data/index.js.map +1 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.d.ts +154 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.d.ts.map +1 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.js +1432 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.js.map +1 -0
- package/dist/adapters/postgres-data/types.d.ts +139 -0
- package/dist/adapters/postgres-data/types.d.ts.map +1 -0
- package/dist/adapters/postgres-data/types.js +2 -0
- package/dist/adapters/postgres-data/types.js.map +1 -0
- package/dist/core/adapters/registry.d.ts +79 -0
- package/dist/core/adapters/registry.d.ts.map +1 -0
- package/dist/core/adapters/registry.js +149 -0
- package/dist/core/adapters/registry.js.map +1 -0
- package/dist/core/core/engine.d.ts +551 -0
- package/dist/core/core/engine.d.ts.map +1 -0
- package/dist/core/core/engine.js +1074 -0
- package/dist/core/core/engine.js.map +1 -0
- package/dist/core/crypto/adapter.d.ts +63 -0
- package/dist/core/crypto/adapter.d.ts.map +1 -0
- package/dist/core/crypto/adapter.js +63 -0
- package/dist/core/crypto/adapter.js.map +1 -0
- package/dist/core/crypto/fallback-adapter.d.ts +23 -0
- package/dist/core/crypto/fallback-adapter.d.ts.map +1 -0
- package/dist/core/crypto/fallback-adapter.js +195 -0
- package/dist/core/crypto/fallback-adapter.js.map +1 -0
- package/dist/core/crypto/node-adapter.d.ts +20 -0
- package/dist/core/crypto/node-adapter.d.ts.map +1 -0
- package/dist/core/crypto/node-adapter.js +85 -0
- package/dist/core/crypto/node-adapter.js.map +1 -0
- package/dist/core/crypto/password-hash.d.ts +73 -0
- package/dist/core/crypto/password-hash.d.ts.map +1 -0
- package/dist/core/crypto/password-hash.js +244 -0
- package/dist/core/crypto/password-hash.js.map +1 -0
- package/dist/core/crypto/webcrypto-adapter.d.ts +24 -0
- package/dist/core/crypto/webcrypto-adapter.d.ts.map +1 -0
- package/dist/core/crypto/webcrypto-adapter.js +158 -0
- package/dist/core/crypto/webcrypto-adapter.js.map +1 -0
- package/dist/core/errors/index.d.ts +30 -0
- package/dist/core/errors/index.d.ts.map +1 -0
- package/dist/core/errors/index.js +61 -0
- package/dist/core/errors/index.js.map +1 -0
- package/dist/core/events/event-bus.d.ts +121 -0
- package/dist/core/events/event-bus.d.ts.map +1 -0
- package/dist/core/events/event-bus.js +631 -0
- package/dist/core/events/event-bus.js.map +1 -0
- package/dist/core/events/index.d.ts +17 -0
- package/dist/core/events/index.d.ts.map +1 -0
- package/dist/core/events/index.js +22 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/events/memory-storage.d.ts +57 -0
- package/dist/core/events/memory-storage.d.ts.map +1 -0
- package/dist/core/events/memory-storage.js +175 -0
- package/dist/core/events/memory-storage.js.map +1 -0
- package/dist/core/events/types.d.ts +434 -0
- package/dist/core/events/types.d.ts.map +1 -0
- package/dist/core/events/types.js +9 -0
- package/dist/core/events/types.js.map +1 -0
- package/dist/core/events/utils.d.ts +134 -0
- package/dist/core/events/utils.d.ts.map +1 -0
- package/dist/core/events/utils.js +333 -0
- package/dist/core/events/utils.js.map +1 -0
- package/dist/core/fields/conditional.d.ts +115 -0
- package/dist/core/fields/conditional.d.ts.map +1 -0
- package/dist/core/fields/conditional.js +296 -0
- package/dist/core/fields/conditional.js.map +1 -0
- package/dist/core/fields/field-type.d.ts +218 -0
- package/dist/core/fields/field-type.d.ts.map +1 -0
- package/dist/core/fields/field-type.js +17 -0
- package/dist/core/fields/field-type.js.map +1 -0
- package/dist/core/fields/helpers.d.ts +86 -0
- package/dist/core/fields/helpers.d.ts.map +1 -0
- package/dist/core/fields/helpers.js +260 -0
- package/dist/core/fields/helpers.js.map +1 -0
- package/dist/core/fields/index.d.ts +5 -0
- package/dist/core/fields/index.d.ts.map +1 -0
- package/dist/core/fields/index.js +6 -0
- package/dist/core/fields/index.js.map +1 -0
- package/dist/core/fields/registry.d.ts +78 -0
- package/dist/core/fields/registry.d.ts.map +1 -0
- package/dist/core/fields/registry.js +185 -0
- package/dist/core/fields/registry.js.map +1 -0
- package/dist/core/index.d.ts +39 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +57 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/media/image-processor.d.ts +24 -0
- package/dist/core/media/image-processor.d.ts.map +1 -0
- package/dist/core/media/image-processor.js +69 -0
- package/dist/core/media/image-processor.js.map +1 -0
- package/dist/core/media/processors/cloudflare-images.d.ts +83 -0
- package/dist/core/media/processors/cloudflare-images.d.ts.map +1 -0
- package/dist/core/media/processors/cloudflare-images.js +241 -0
- package/dist/core/media/processors/cloudflare-images.js.map +1 -0
- package/dist/core/media/processors/cloudflare-transform-store.d.ts +127 -0
- package/dist/core/media/processors/cloudflare-transform-store.d.ts.map +1 -0
- package/dist/core/media/processors/cloudflare-transform-store.js +237 -0
- package/dist/core/media/processors/cloudflare-transform-store.js.map +1 -0
- package/dist/core/media/processors/cloudflare-transformations.d.ts +81 -0
- package/dist/core/media/processors/cloudflare-transformations.d.ts.map +1 -0
- package/dist/core/media/processors/cloudflare-transformations.js +144 -0
- package/dist/core/media/processors/cloudflare-transformations.js.map +1 -0
- package/dist/core/media/processors/index.d.ts +9 -0
- package/dist/core/media/processors/index.d.ts.map +1 -0
- package/dist/core/media/processors/index.js +13 -0
- package/dist/core/media/processors/index.js.map +1 -0
- package/dist/core/media/processors/noop.d.ts +22 -0
- package/dist/core/media/processors/noop.d.ts.map +1 -0
- package/dist/core/media/processors/noop.js +38 -0
- package/dist/core/media/processors/noop.js.map +1 -0
- package/dist/core/media/processors/sharp.d.ts +30 -0
- package/dist/core/media/processors/sharp.d.ts.map +1 -0
- package/dist/core/media/processors/sharp.js +126 -0
- package/dist/core/media/processors/sharp.js.map +1 -0
- package/dist/core/media/processors/types.d.ts +67 -0
- package/dist/core/media/processors/types.d.ts.map +1 -0
- package/dist/core/media/processors/types.js +42 -0
- package/dist/core/media/processors/types.js.map +1 -0
- package/dist/core/media/processors/workers-images.d.ts +112 -0
- package/dist/core/media/processors/workers-images.d.ts.map +1 -0
- package/dist/core/media/processors/workers-images.js +257 -0
- package/dist/core/media/processors/workers-images.js.map +1 -0
- package/dist/core/schema/registry.d.ts +41 -0
- package/dist/core/schema/registry.d.ts.map +1 -0
- package/dist/core/schema/registry.js +248 -0
- package/dist/core/schema/registry.js.map +1 -0
- package/dist/core/schema/singleton.d.ts +107 -0
- package/dist/core/schema/singleton.d.ts.map +1 -0
- package/dist/core/schema/singleton.js +180 -0
- package/dist/core/schema/singleton.js.map +1 -0
- package/dist/core/security/auth.d.ts +120 -0
- package/dist/core/security/auth.d.ts.map +1 -0
- package/dist/core/security/auth.js +274 -0
- package/dist/core/security/auth.js.map +1 -0
- package/dist/core/security/captcha/index.d.ts +21 -0
- package/dist/core/security/captcha/index.d.ts.map +1 -0
- package/dist/core/security/captcha/index.js +34 -0
- package/dist/core/security/captcha/index.js.map +1 -0
- package/dist/core/security/captcha/recaptcha.d.ts +41 -0
- package/dist/core/security/captcha/recaptcha.d.ts.map +1 -0
- package/dist/core/security/captcha/recaptcha.js +89 -0
- package/dist/core/security/captcha/recaptcha.js.map +1 -0
- package/dist/core/security/captcha/turnstile.d.ts +41 -0
- package/dist/core/security/captcha/turnstile.d.ts.map +1 -0
- package/dist/core/security/captcha/turnstile.js +90 -0
- package/dist/core/security/captcha/turnstile.js.map +1 -0
- package/dist/core/security/captcha/types.d.ts +66 -0
- package/dist/core/security/captcha/types.d.ts.map +1 -0
- package/dist/core/security/captcha/types.js +7 -0
- package/dist/core/security/captcha/types.js.map +1 -0
- package/dist/core/security/mfa/email-otp.d.ts +91 -0
- package/dist/core/security/mfa/email-otp.d.ts.map +1 -0
- package/dist/core/security/mfa/email-otp.js +264 -0
- package/dist/core/security/mfa/email-otp.js.map +1 -0
- package/dist/core/security/mfa/index.d.ts +8 -0
- package/dist/core/security/mfa/index.d.ts.map +1 -0
- package/dist/core/security/mfa/index.js +8 -0
- package/dist/core/security/mfa/index.js.map +1 -0
- package/dist/core/security/mfa/totp.d.ts +87 -0
- package/dist/core/security/mfa/totp.d.ts.map +1 -0
- package/dist/core/security/mfa/totp.js +293 -0
- package/dist/core/security/mfa/totp.js.map +1 -0
- package/dist/core/security/middleware.d.ts +102 -0
- package/dist/core/security/middleware.d.ts.map +1 -0
- package/dist/core/security/middleware.js +261 -0
- package/dist/core/security/middleware.js.map +1 -0
- package/dist/core/security/oauth/google.d.ts +76 -0
- package/dist/core/security/oauth/google.d.ts.map +1 -0
- package/dist/core/security/oauth/google.js +154 -0
- package/dist/core/security/oauth/google.js.map +1 -0
- package/dist/core/security/oauth/index.d.ts +8 -0
- package/dist/core/security/oauth/index.d.ts.map +1 -0
- package/dist/core/security/oauth/index.js +7 -0
- package/dist/core/security/oauth/index.js.map +1 -0
- package/dist/core/security/oauth2/authorization-server.d.ts +195 -0
- package/dist/core/security/oauth2/authorization-server.d.ts.map +1 -0
- package/dist/core/security/oauth2/authorization-server.js +569 -0
- package/dist/core/security/oauth2/authorization-server.js.map +1 -0
- package/dist/core/security/oauth2/index.d.ts +9 -0
- package/dist/core/security/oauth2/index.d.ts.map +1 -0
- package/dist/core/security/oauth2/index.js +9 -0
- package/dist/core/security/oauth2/index.js.map +1 -0
- package/dist/core/security/rate-limiter.d.ts +15 -0
- package/dist/core/security/rate-limiter.d.ts.map +1 -0
- package/dist/core/security/rate-limiter.js +48 -0
- package/dist/core/security/rate-limiter.js.map +1 -0
- package/dist/core/security/validation.d.ts +20 -0
- package/dist/core/security/validation.d.ts.map +1 -0
- package/dist/core/security/validation.js +273 -0
- package/dist/core/security/validation.js.map +1 -0
- package/dist/core/services/auth-service.d.ts +128 -0
- package/dist/core/services/auth-service.d.ts.map +1 -0
- package/dist/core/services/auth-service.js +530 -0
- package/dist/core/services/auth-service.js.map +1 -0
- package/dist/core/services/captcha-service.d.ts +44 -0
- package/dist/core/services/captcha-service.d.ts.map +1 -0
- package/dist/core/services/captcha-service.js +82 -0
- package/dist/core/services/captcha-service.js.map +1 -0
- package/dist/core/services/document-service.d.ts +59 -0
- package/dist/core/services/document-service.d.ts.map +1 -0
- package/dist/core/services/document-service.js +269 -0
- package/dist/core/services/document-service.js.map +1 -0
- package/dist/core/services/media-service.d.ts +40 -0
- package/dist/core/services/media-service.d.ts.map +1 -0
- package/dist/core/services/media-service.js +396 -0
- package/dist/core/services/media-service.js.map +1 -0
- package/dist/core/services/mfa-service.d.ts +123 -0
- package/dist/core/services/mfa-service.d.ts.map +1 -0
- package/dist/core/services/mfa-service.js +606 -0
- package/dist/core/services/mfa-service.js.map +1 -0
- package/dist/core/services/oauth-service.d.ts +58 -0
- package/dist/core/services/oauth-service.d.ts.map +1 -0
- package/dist/core/services/oauth-service.js +253 -0
- package/dist/core/services/oauth-service.js.map +1 -0
- package/dist/core/services/oauth2-server-service.d.ts +61 -0
- package/dist/core/services/oauth2-server-service.d.ts.map +1 -0
- package/dist/core/services/oauth2-server-service.js +259 -0
- package/dist/core/services/oauth2-server-service.js.map +1 -0
- package/dist/core/services/passkey-service.d.ts +59 -0
- package/dist/core/services/passkey-service.d.ts.map +1 -0
- package/dist/core/services/passkey-service.js +238 -0
- package/dist/core/services/passkey-service.js.map +1 -0
- package/dist/core/services/token-service.d.ts +31 -0
- package/dist/core/services/token-service.d.ts.map +1 -0
- package/dist/core/services/token-service.js +132 -0
- package/dist/core/services/token-service.js.map +1 -0
- package/dist/core/services/trusted-device-service.d.ts +39 -0
- package/dist/core/services/trusted-device-service.d.ts.map +1 -0
- package/dist/core/services/trusted-device-service.js +123 -0
- package/dist/core/services/trusted-device-service.js.map +1 -0
- package/dist/core/services/user-service.d.ts +38 -0
- package/dist/core/services/user-service.d.ts.map +1 -0
- package/dist/core/services/user-service.js +265 -0
- package/dist/core/services/user-service.js.map +1 -0
- package/dist/core/studio/integration.d.ts +54 -0
- package/dist/core/studio/integration.d.ts.map +1 -0
- package/dist/core/studio/integration.js +326 -0
- package/dist/core/studio/integration.js.map +1 -0
- package/dist/core/types/index.d.ts +282 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +83 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/core/types/mail.d.ts +90 -0
- package/dist/core/types/mail.d.ts.map +1 -0
- package/dist/core/types/mail.js +7 -0
- package/dist/core/types/mail.js.map +1 -0
- package/dist/core/types/oauth2.d.ts +9 -0
- package/dist/core/types/oauth2.d.ts.map +1 -0
- package/dist/core/types/oauth2.js +7 -0
- package/dist/core/types/oauth2.js.map +1 -0
- package/dist/core/types/storage-adapters.d.ts +611 -0
- package/dist/core/types/storage-adapters.d.ts.map +1 -0
- package/dist/core/types/storage-adapters.js +14 -0
- package/dist/core/types/storage-adapters.js.map +1 -0
- package/dist/core/types/user.d.ts +10 -0
- package/dist/core/types/user.d.ts.map +1 -0
- package/dist/core/types/user.js +45 -0
- package/dist/core/types/user.js.map +1 -0
- package/dist/core/utils/id-generator.d.ts +16 -0
- package/dist/core/utils/id-generator.d.ts.map +1 -0
- package/dist/core/utils/id-generator.js +61 -0
- package/dist/core/utils/id-generator.js.map +1 -0
- package/dist/core/utils/logger.d.ts +100 -0
- package/dist/core/utils/logger.d.ts.map +1 -0
- package/dist/core/utils/logger.js +212 -0
- package/dist/core/utils/logger.js.map +1 -0
- package/dist/core/utils/reference-expander.d.ts +57 -0
- package/dist/core/utils/reference-expander.d.ts.map +1 -0
- package/dist/core/utils/reference-expander.js +192 -0
- package/dist/core/utils/reference-expander.js.map +1 -0
- package/dist/core/utils/universal-crypto.d.ts +46 -0
- package/dist/core/utils/universal-crypto.d.ts.map +1 -0
- package/dist/core/utils/universal-crypto.js +171 -0
- package/dist/core/utils/universal-crypto.js.map +1 -0
- package/dist/core/validation/validator.d.ts +11 -0
- package/dist/core/validation/validator.d.ts.map +1 -0
- package/dist/core/validation/validator.js +164 -0
- package/dist/core/validation/validator.js.map +1 -0
- package/dist/i18n/config.d.ts +4077 -0
- package/dist/i18n/config.d.ts.map +1 -0
- package/dist/i18n/config.js +116 -0
- package/dist/i18n/config.js.map +1 -0
- package/dist/i18n/hooks.d.ts +86 -0
- package/dist/i18n/hooks.d.ts.map +1 -0
- package/dist/i18n/hooks.js +122 -0
- package/dist/i18n/hooks.js.map +1 -0
- package/dist/i18n/index.d.ts +39 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +40 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/locales/en/auth.json +108 -0
- package/dist/i18n/locales/en/common.json +100 -0
- package/dist/i18n/locales/en/errors.json +55 -0
- package/dist/i18n/locales/en/fields.json +456 -0
- package/dist/i18n/locales/en/studio.json +1329 -0
- package/dist/i18n/locales/fr/auth.json +108 -0
- package/dist/i18n/locales/fr/common.json +100 -0
- package/dist/i18n/locales/fr/errors.json +55 -0
- package/dist/i18n/locales/fr/fields.json +456 -0
- package/dist/i18n/locales/fr/studio.json +1274 -0
- package/dist/i18n/provider.d.ts +45 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +49 -0
- package/dist/i18n/provider.js.map +1 -0
- package/dist/i18n/types.d.ts +8 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +7 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/express/adapter.d.ts +33 -0
- package/dist/integrations/express/adapter.d.ts.map +1 -0
- package/dist/integrations/express/adapter.js +203 -0
- package/dist/integrations/express/adapter.js.map +1 -0
- package/dist/integrations/express/config.d.ts +599 -0
- package/dist/integrations/express/config.d.ts.map +1 -0
- package/dist/integrations/express/config.js +166 -0
- package/dist/integrations/express/config.js.map +1 -0
- package/dist/integrations/express/index.d.ts +10 -0
- package/dist/integrations/express/index.d.ts.map +1 -0
- package/dist/integrations/express/index.js +9 -0
- package/dist/integrations/express/index.js.map +1 -0
- package/dist/integrations/express/integration.d.ts +111 -0
- package/dist/integrations/express/integration.d.ts.map +1 -0
- package/dist/integrations/express/integration.js +730 -0
- package/dist/integrations/express/integration.js.map +1 -0
- package/dist/integrations/express/media-rewrite-middleware.d.ts +78 -0
- package/dist/integrations/express/media-rewrite-middleware.d.ts.map +1 -0
- package/dist/integrations/express/media-rewrite-middleware.js +211 -0
- package/dist/integrations/express/media-rewrite-middleware.js.map +1 -0
- package/dist/integrations/express/media-url-generator.d.ts +52 -0
- package/dist/integrations/express/media-url-generator.d.ts.map +1 -0
- package/dist/integrations/express/media-url-generator.js +107 -0
- package/dist/integrations/express/media-url-generator.js.map +1 -0
- package/dist/integrations/express/middleware.d.ts +25 -0
- package/dist/integrations/express/middleware.d.ts.map +1 -0
- package/dist/integrations/express/middleware.js +130 -0
- package/dist/integrations/express/middleware.js.map +1 -0
- package/dist/integrations/express/server.d.ts +99 -0
- package/dist/integrations/express/server.d.ts.map +1 -0
- package/dist/integrations/express/server.js +597 -0
- package/dist/integrations/express/server.js.map +1 -0
- package/dist/integrations/express/types.d.ts +106 -0
- package/dist/integrations/express/types.d.ts.map +1 -0
- package/dist/integrations/express/types.js +2 -0
- package/dist/integrations/express/types.js.map +1 -0
- package/dist/mail/adapters/console.d.ts +30 -0
- package/dist/mail/adapters/console.d.ts.map +1 -0
- package/dist/mail/adapters/console.js +169 -0
- package/dist/mail/adapters/console.js.map +1 -0
- package/dist/mail/adapters/resend.d.ts +18 -0
- package/dist/mail/adapters/resend.d.ts.map +1 -0
- package/dist/mail/adapters/resend.js +148 -0
- package/dist/mail/adapters/resend.js.map +1 -0
- package/dist/mail/adapters/smtp.d.ts +22 -0
- package/dist/mail/adapters/smtp.d.ts.map +1 -0
- package/dist/mail/adapters/smtp.js +180 -0
- package/dist/mail/adapters/smtp.js.map +1 -0
- package/dist/mail/index.d.ts +15 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +13 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/mail-service.d.ts +180 -0
- package/dist/mail/mail-service.d.ts.map +1 -0
- package/dist/mail/mail-service.js +370 -0
- package/dist/mail/mail-service.js.map +1 -0
- package/dist/mail/notification-service.d.ts +82 -0
- package/dist/mail/notification-service.d.ts.map +1 -0
- package/dist/mail/notification-service.js +262 -0
- package/dist/mail/notification-service.js.map +1 -0
- package/dist/mail/templates/built-in-templates.d.ts +47 -0
- package/dist/mail/templates/built-in-templates.d.ts.map +1 -0
- package/dist/mail/templates/built-in-templates.js +416 -0
- package/dist/mail/templates/built-in-templates.js.map +1 -0
- package/dist/mail/templates/custom-template-renderer.d.ts +115 -0
- package/dist/mail/templates/custom-template-renderer.d.ts.map +1 -0
- package/dist/mail/templates/custom-template-renderer.js +225 -0
- package/dist/mail/templates/custom-template-renderer.js.map +1 -0
- package/dist/mail/types.d.ts +7 -0
- package/dist/mail/types.d.ts.map +1 -0
- package/dist/mail/types.js +7 -0
- package/dist/mail/types.js.map +1 -0
- package/dist/routes/auth/captcha.d.ts +33 -0
- package/dist/routes/auth/captcha.d.ts.map +1 -0
- package/dist/routes/auth/captcha.js +102 -0
- package/dist/routes/auth/captcha.js.map +1 -0
- package/dist/routes/auth/change-password.d.ts +17 -0
- package/dist/routes/auth/change-password.d.ts.map +1 -0
- package/dist/routes/auth/change-password.js +121 -0
- package/dist/routes/auth/change-password.js.map +1 -0
- package/dist/routes/auth/index.d.ts +13 -0
- package/dist/routes/auth/index.d.ts.map +1 -0
- package/dist/routes/auth/index.js +35 -0
- package/dist/routes/auth/index.js.map +1 -0
- package/dist/routes/auth/mfa.d.ts +121 -0
- package/dist/routes/auth/mfa.d.ts.map +1 -0
- package/dist/routes/auth/mfa.js +1143 -0
- package/dist/routes/auth/mfa.js.map +1 -0
- package/dist/routes/auth/oauth.d.ts +37 -0
- package/dist/routes/auth/oauth.d.ts.map +1 -0
- package/dist/routes/auth/oauth.js +420 -0
- package/dist/routes/auth/oauth.js.map +1 -0
- package/dist/routes/auth/oauth2-server.d.ts +70 -0
- package/dist/routes/auth/oauth2-server.d.ts.map +1 -0
- package/dist/routes/auth/oauth2-server.js +775 -0
- package/dist/routes/auth/oauth2-server.js.map +1 -0
- package/dist/routes/auth/passkey.d.ts +65 -0
- package/dist/routes/auth/passkey.d.ts.map +1 -0
- package/dist/routes/auth/passkey.js +910 -0
- package/dist/routes/auth/passkey.js.map +1 -0
- package/dist/routes/auth/password-reset.d.ts +23 -0
- package/dist/routes/auth/password-reset.d.ts.map +1 -0
- package/dist/routes/auth/password-reset.js +336 -0
- package/dist/routes/auth/password-reset.js.map +1 -0
- package/dist/routes/handlers/audit.d.ts +21 -0
- package/dist/routes/handlers/audit.d.ts.map +1 -0
- package/dist/routes/handlers/audit.js +124 -0
- package/dist/routes/handlers/audit.js.map +1 -0
- package/dist/routes/handlers/auth.d.ts +150 -0
- package/dist/routes/handlers/auth.d.ts.map +1 -0
- package/dist/routes/handlers/auth.js +512 -0
- package/dist/routes/handlers/auth.js.map +1 -0
- package/dist/routes/handlers/base.d.ts +34 -0
- package/dist/routes/handlers/base.d.ts.map +1 -0
- package/dist/routes/handlers/base.js +366 -0
- package/dist/routes/handlers/base.js.map +1 -0
- package/dist/routes/handlers/config.d.ts +31 -0
- package/dist/routes/handlers/config.d.ts.map +1 -0
- package/dist/routes/handlers/config.js +483 -0
- package/dist/routes/handlers/config.js.map +1 -0
- package/dist/routes/handlers/documents.d.ts +50 -0
- package/dist/routes/handlers/documents.d.ts.map +1 -0
- package/dist/routes/handlers/documents.js +708 -0
- package/dist/routes/handlers/documents.js.map +1 -0
- package/dist/routes/handlers/index.d.ts +12 -0
- package/dist/routes/handlers/index.d.ts.map +1 -0
- package/dist/routes/handlers/index.js +11 -0
- package/dist/routes/handlers/index.js.map +1 -0
- package/dist/routes/handlers/media.d.ts +24 -0
- package/dist/routes/handlers/media.d.ts.map +1 -0
- package/dist/routes/handlers/media.js +432 -0
- package/dist/routes/handlers/media.js.map +1 -0
- package/dist/routes/handlers/search.d.ts +10 -0
- package/dist/routes/handlers/search.d.ts.map +1 -0
- package/dist/routes/handlers/search.js +153 -0
- package/dist/routes/handlers/search.js.map +1 -0
- package/dist/routes/handlers/tokens.d.ts +14 -0
- package/dist/routes/handlers/tokens.d.ts.map +1 -0
- package/dist/routes/handlers/tokens.js +109 -0
- package/dist/routes/handlers/tokens.js.map +1 -0
- package/dist/routes/handlers/types.d.ts +13 -0
- package/dist/routes/handlers/types.d.ts.map +1 -0
- package/dist/routes/handlers/types.js +5 -0
- package/dist/routes/handlers/types.js.map +1 -0
- package/dist/routes/handlers/users.d.ts +16 -0
- package/dist/routes/handlers/users.d.ts.map +1 -0
- package/dist/routes/handlers/users.js +198 -0
- package/dist/routes/handlers/users.js.map +1 -0
- package/dist/routes/handlers/webhooks.d.ts +37 -0
- package/dist/routes/handlers/webhooks.d.ts.map +1 -0
- package/dist/routes/handlers/webhooks.js +343 -0
- package/dist/routes/handlers/webhooks.js.map +1 -0
- package/dist/routes/index.d.ts +5 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +5 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/routes.d.ts +29 -0
- package/dist/routes/routes.d.ts.map +1 -0
- package/dist/routes/routes.js +531 -0
- package/dist/routes/routes.js.map +1 -0
- package/dist/routes/slug-processor.d.ts +32 -0
- package/dist/routes/slug-processor.d.ts.map +1 -0
- package/dist/routes/slug-processor.js +328 -0
- package/dist/routes/slug-processor.js.map +1 -0
- package/dist/routes/types.d.ts +31 -0
- package/dist/routes/types.d.ts.map +1 -0
- package/dist/routes/types.js +7 -0
- package/dist/routes/types.js.map +1 -0
- package/dist/routes/users/index.d.ts +7 -0
- package/dist/routes/users/index.d.ts.map +1 -0
- package/dist/routes/users/index.js +7 -0
- package/dist/routes/users/index.js.map +1 -0
- package/dist/structure/builder/StructureBuilder.d.ts +209 -0
- package/dist/structure/builder/StructureBuilder.d.ts.map +1 -0
- package/dist/structure/builder/StructureBuilder.js +425 -0
- package/dist/structure/builder/StructureBuilder.js.map +1 -0
- package/dist/structure/builder/index.d.ts +8 -0
- package/dist/structure/builder/index.d.ts.map +1 -0
- package/dist/structure/builder/index.js +6 -0
- package/dist/structure/builder/index.js.map +1 -0
- package/dist/structure/builder/navigation-tree-builder.d.ts +33 -0
- package/dist/structure/builder/navigation-tree-builder.d.ts.map +1 -0
- package/dist/structure/builder/navigation-tree-builder.js +284 -0
- package/dist/structure/builder/navigation-tree-builder.js.map +1 -0
- package/dist/structure/errors/index.d.ts +72 -0
- package/dist/structure/errors/index.d.ts.map +1 -0
- package/dist/structure/errors/index.js +136 -0
- package/dist/structure/errors/index.js.map +1 -0
- package/dist/structure/index.d.ts +14 -0
- package/dist/structure/index.d.ts.map +1 -0
- package/dist/structure/index.js +16 -0
- package/dist/structure/index.js.map +1 -0
- package/dist/structure/types/filters.d.ts +167 -0
- package/dist/structure/types/filters.d.ts.map +1 -0
- package/dist/structure/types/filters.js +38 -0
- package/dist/structure/types/filters.js.map +1 -0
- package/dist/structure/types/index.d.ts +11 -0
- package/dist/structure/types/index.d.ts.map +1 -0
- package/dist/structure/types/index.js +7 -0
- package/dist/structure/types/index.js.map +1 -0
- package/dist/structure/types/permissions.d.ts +159 -0
- package/dist/structure/types/permissions.d.ts.map +1 -0
- package/dist/structure/types/permissions.js +49 -0
- package/dist/structure/types/permissions.js.map +1 -0
- package/dist/structure/types/structure.d.ts +342 -0
- package/dist/structure/types/structure.d.ts.map +1 -0
- package/dist/structure/types/structure.js +6 -0
- package/dist/structure/types/structure.js.map +1 -0
- package/dist/structure/types/views.d.ts +278 -0
- package/dist/structure/types/views.d.ts.map +1 -0
- package/dist/structure/types/views.js +6 -0
- package/dist/structure/types/views.js.map +1 -0
- package/dist/structure/utils/hash.d.ts +61 -0
- package/dist/structure/utils/hash.d.ts.map +1 -0
- package/dist/structure/utils/hash.js +118 -0
- package/dist/structure/utils/hash.js.map +1 -0
- package/dist/structure/utils/index.d.ts +10 -0
- package/dist/structure/utils/index.d.ts.map +1 -0
- package/dist/structure/utils/index.js +7 -0
- package/dist/structure/utils/index.js.map +1 -0
- package/dist/structure/utils/permission-checker.d.ts +57 -0
- package/dist/structure/utils/permission-checker.d.ts.map +1 -0
- package/dist/structure/utils/permission-checker.js +286 -0
- package/dist/structure/utils/permission-checker.js.map +1 -0
- package/dist/structure/utils/query-builder.d.ts +82 -0
- package/dist/structure/utils/query-builder.d.ts.map +1 -0
- package/dist/structure/utils/query-builder.js +489 -0
- package/dist/structure/utils/query-builder.js.map +1 -0
- package/dist/structure/utils/structure-merger.d.ts +91 -0
- package/dist/structure/utils/structure-merger.d.ts.map +1 -0
- package/dist/structure/utils/structure-merger.js +445 -0
- package/dist/structure/utils/structure-merger.js.map +1 -0
- package/dist/structure/validation/schemas.d.ts +3107 -0
- package/dist/structure/validation/schemas.d.ts.map +1 -0
- package/dist/structure/validation/schemas.js +436 -0
- package/dist/structure/validation/schemas.js.map +1 -0
- package/dist/types/api-requests.d.ts +334 -0
- package/dist/types/api-requests.d.ts.map +1 -0
- package/dist/types/api-requests.js +8 -0
- package/dist/types/api-requests.js.map +1 -0
- package/dist/types/auth.d.ts +394 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +8 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/document.d.ts +49 -0
- package/dist/types/document.d.ts.map +1 -0
- package/dist/types/document.js +6 -0
- package/dist/types/document.js.map +1 -0
- package/dist/types/field-system.d.ts +197 -0
- package/dist/types/field-system.d.ts.map +1 -0
- package/dist/types/field-system.js +8 -0
- package/dist/types/field-system.js.map +1 -0
- package/dist/types/fields.d.ts +17 -0
- package/dist/types/fields.d.ts.map +1 -0
- package/dist/types/fields.js +21 -0
- package/dist/types/fields.js.map +1 -0
- package/dist/types/http.d.ts +162 -0
- package/dist/types/http.d.ts.map +1 -0
- package/dist/types/http.js +8 -0
- package/dist/types/http.js.map +1 -0
- package/dist/types/i18n.d.ts +56 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +25 -0
- package/dist/types/i18n.js.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mail.d.ts +224 -0
- package/dist/types/mail.d.ts.map +1 -0
- package/dist/types/mail.js +8 -0
- package/dist/types/mail.js.map +1 -0
- package/dist/types/media.d.ts +56 -0
- package/dist/types/media.d.ts.map +1 -0
- package/dist/types/media.js +6 -0
- package/dist/types/media.js.map +1 -0
- package/dist/types/oauth2.d.ts +287 -0
- package/dist/types/oauth2.d.ts.map +1 -0
- package/dist/types/oauth2.js +43 -0
- package/dist/types/oauth2.js.map +1 -0
- package/dist/types/passkey.d.ts +217 -0
- package/dist/types/passkey.d.ts.map +1 -0
- package/dist/types/passkey.js +8 -0
- package/dist/types/passkey.js.map +1 -0
- package/dist/types/slug.d.ts +99 -0
- package/dist/types/slug.d.ts.map +1 -0
- package/dist/types/slug.js +8 -0
- package/dist/types/slug.js.map +1 -0
- package/package.json +183 -0
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
import { promises as fs, constants } from 'fs';
|
|
2
|
+
import * as fsExtra from 'fs-extra';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { SecurityValidator, InvalidInputError } from '../../core/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated FilesystemAdapter is deprecated. Use the split storage architecture instead:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // Old way (deprecated)
|
|
10
|
+
* const adapter = new FilesystemAdapter({ contentDir: './content', mediaDir: './media' })
|
|
11
|
+
*
|
|
12
|
+
* // New way (recommended)
|
|
13
|
+
* const trokky = await TrokkyExpress.create({
|
|
14
|
+
* storage: {
|
|
15
|
+
* data: { adapter: 'filesystem-data', options: { contentDir: './data/content' } },
|
|
16
|
+
* media: { adapter: 'filesystem-media', options: { mediaDir: './data/media' } }
|
|
17
|
+
* }
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* This provides better separation of concerns, improved scalability, and clearer configuration.
|
|
22
|
+
* See migration guide in docs for more details.
|
|
23
|
+
*/
|
|
24
|
+
export class FilesystemAdapter {
|
|
25
|
+
config;
|
|
26
|
+
// Security limits
|
|
27
|
+
MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
|
28
|
+
MAX_DOCUMENT_SIZE = 10 * 1024 * 1024; // 10MB
|
|
29
|
+
constructor(config = {}) {
|
|
30
|
+
// Show deprecation warning
|
|
31
|
+
if (!config.silent) {
|
|
32
|
+
console.warn('⚠️ [DEPRECATED] FilesystemAdapter is deprecated. Please migrate to the split storage architecture:\n' +
|
|
33
|
+
' Use TrokkyExpress.create() with storage.data and storage.media adapters.\n' +
|
|
34
|
+
' See migration guide: https://docs.trokky.dev/migration/split-storage');
|
|
35
|
+
}
|
|
36
|
+
this.config = {
|
|
37
|
+
contentDir: config.contentDir || './content',
|
|
38
|
+
mediaDir: config.mediaDir || './media',
|
|
39
|
+
usersDir: config.usersDir || './users',
|
|
40
|
+
tokensDir: config.tokensDir || './tokens',
|
|
41
|
+
webhooksDir: config.webhooksDir || './webhooks',
|
|
42
|
+
createDirs: config.createDirs ?? true,
|
|
43
|
+
prettyJson: config.prettyJson ?? true,
|
|
44
|
+
jsonSpaces: config.jsonSpaces ?? 2,
|
|
45
|
+
syncWrites: config.syncWrites ?? false,
|
|
46
|
+
fileMode: config.fileMode ?? 0o644,
|
|
47
|
+
dirMode: config.dirMode ?? 0o755,
|
|
48
|
+
silent: config.silent ?? false,
|
|
49
|
+
mediaBaseUrl: config.mediaBaseUrl
|
|
50
|
+
};
|
|
51
|
+
// Initialize directories
|
|
52
|
+
if (this.config.createDirs) {
|
|
53
|
+
this.initializeDirectories();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async initializeDirectories() {
|
|
57
|
+
try {
|
|
58
|
+
await fsExtra.ensureDir(this.config.contentDir, { mode: this.config.dirMode });
|
|
59
|
+
await fsExtra.ensureDir(this.config.mediaDir, { mode: this.config.dirMode });
|
|
60
|
+
await fsExtra.ensureDir(this.config.usersDir, { mode: this.config.dirMode });
|
|
61
|
+
await fsExtra.ensureDir(this.config.webhooksDir, { mode: this.config.dirMode });
|
|
62
|
+
// Create metadata directory for media files
|
|
63
|
+
const mediaMetadataDir = path.join(this.config.mediaDir, '.metadata');
|
|
64
|
+
await fsExtra.ensureDir(mediaMetadataDir, { mode: this.config.dirMode });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// Don't throw on directory creation failure during initialization
|
|
68
|
+
// Let individual operations handle their own directory creation
|
|
69
|
+
if (!this.config.silent) {
|
|
70
|
+
console.warn(`Warning: Failed to initialize directories: ${error}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Document operations
|
|
75
|
+
async getDocument(collection, id) {
|
|
76
|
+
try {
|
|
77
|
+
// Input validation is handled in getDocumentPath
|
|
78
|
+
const filePath = this.getDocumentPath(collection, id);
|
|
79
|
+
// Check if file exists
|
|
80
|
+
try {
|
|
81
|
+
await fs.access(filePath, constants.F_OK);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
87
|
+
const documentFile = this.safeParseJSON(content, this.dateReviver);
|
|
88
|
+
// Convert stored data back to Document format
|
|
89
|
+
const document = {
|
|
90
|
+
id: documentFile.id,
|
|
91
|
+
_id: documentFile.id,
|
|
92
|
+
...documentFile.data,
|
|
93
|
+
_collection: documentFile.collection,
|
|
94
|
+
_createdAt: documentFile.metadata.createdAt instanceof Date ? documentFile.metadata.createdAt : new Date(documentFile.metadata.createdAt),
|
|
95
|
+
_updatedAt: documentFile.metadata.updatedAt instanceof Date ? documentFile.metadata.updatedAt : new Date(documentFile.metadata.updatedAt),
|
|
96
|
+
_revision: documentFile.metadata.revision,
|
|
97
|
+
_status: documentFile.metadata.status,
|
|
98
|
+
_createdBy: documentFile.metadata.createdBy,
|
|
99
|
+
_updatedBy: documentFile.metadata.updatedBy,
|
|
100
|
+
_createdByType: documentFile.metadata.createdByType,
|
|
101
|
+
_updatedByType: documentFile.metadata.updatedByType
|
|
102
|
+
};
|
|
103
|
+
return document;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new Error(`Failed to read document ${collection}/${id}: ${error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async saveDocument(collection, id, data, auditContext) {
|
|
110
|
+
try {
|
|
111
|
+
// Validate document data
|
|
112
|
+
SecurityValidator.validateDocumentData(data);
|
|
113
|
+
// Check document size to prevent DoS attacks
|
|
114
|
+
const dataSize = JSON.stringify(data).length;
|
|
115
|
+
if (dataSize > this.MAX_DOCUMENT_SIZE) {
|
|
116
|
+
throw new InvalidInputError(`Document size ${dataSize} exceeds maximum allowed size of ${this.MAX_DOCUMENT_SIZE} bytes`, 'size');
|
|
117
|
+
}
|
|
118
|
+
// Input validation is handled in getDocumentPath
|
|
119
|
+
const filePath = this.getDocumentPath(collection, id);
|
|
120
|
+
const collectionDir = path.dirname(filePath);
|
|
121
|
+
// Ensure collection directory exists
|
|
122
|
+
await fsExtra.ensureDir(collectionDir, { mode: this.config.dirMode });
|
|
123
|
+
// Check if document already exists to determine if it's an update
|
|
124
|
+
let existingDoc = null;
|
|
125
|
+
try {
|
|
126
|
+
existingDoc = await this.getDocument(collection, id);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Document doesn't exist, this is a new document
|
|
130
|
+
}
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const isUpdate = existingDoc !== null;
|
|
133
|
+
const documentFile = {
|
|
134
|
+
id,
|
|
135
|
+
collection,
|
|
136
|
+
data: { ...data },
|
|
137
|
+
metadata: {
|
|
138
|
+
createdAt: isUpdate ? existingDoc._createdAt : now,
|
|
139
|
+
updatedAt: now,
|
|
140
|
+
revision: isUpdate ? (existingDoc._revision || 0) + 1 : 1,
|
|
141
|
+
status: existingDoc?._status,
|
|
142
|
+
// Add audit metadata
|
|
143
|
+
createdBy: isUpdate ? existingDoc._createdBy : auditContext?.userId,
|
|
144
|
+
updatedBy: auditContext?.userId,
|
|
145
|
+
createdByType: isUpdate ? existingDoc._createdByType : auditContext?.userType,
|
|
146
|
+
updatedByType: auditContext?.userType
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
// Write to file with Date serialization - use atomic write to prevent race conditions
|
|
150
|
+
const jsonContent = this.config.prettyJson
|
|
151
|
+
? JSON.stringify(documentFile, this.dateReplacer, this.config.jsonSpaces)
|
|
152
|
+
: JSON.stringify(documentFile, this.dateReplacer);
|
|
153
|
+
await this.atomicWriteFile(filePath, jsonContent);
|
|
154
|
+
if (this.config.syncWrites) {
|
|
155
|
+
// Force sync to disk
|
|
156
|
+
const fileHandle = await fs.open(filePath, 'r+');
|
|
157
|
+
await fileHandle.sync();
|
|
158
|
+
await fileHandle.close();
|
|
159
|
+
}
|
|
160
|
+
// Return the document in the expected format
|
|
161
|
+
const document = {
|
|
162
|
+
id,
|
|
163
|
+
_id: id,
|
|
164
|
+
...data,
|
|
165
|
+
_collection: collection,
|
|
166
|
+
_createdAt: documentFile.metadata.createdAt,
|
|
167
|
+
_updatedAt: documentFile.metadata.updatedAt,
|
|
168
|
+
_revision: documentFile.metadata.revision,
|
|
169
|
+
_status: documentFile.metadata.status,
|
|
170
|
+
_createdBy: documentFile.metadata.createdBy,
|
|
171
|
+
_updatedBy: documentFile.metadata.updatedBy,
|
|
172
|
+
_createdByType: documentFile.metadata.createdByType,
|
|
173
|
+
_updatedByType: documentFile.metadata.updatedByType
|
|
174
|
+
};
|
|
175
|
+
return document;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new Error(`Failed to save document ${collection}/${id}: ${error}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async listDocuments(collection, options = {}) {
|
|
182
|
+
try {
|
|
183
|
+
// Validate inputs
|
|
184
|
+
SecurityValidator.validateCollectionName(collection);
|
|
185
|
+
SecurityValidator.sanitizeListOptions(options);
|
|
186
|
+
// Apply resource limits
|
|
187
|
+
const limit = Math.min(options.limit || 1000, 1000); // Cap at 1000 documents
|
|
188
|
+
const offset = Math.max(options.offset || 0, 0);
|
|
189
|
+
const collectionDir = path.join(this.config.contentDir, collection);
|
|
190
|
+
this.validateSecurePath(collectionDir, this.config.contentDir);
|
|
191
|
+
// Check if collection directory exists
|
|
192
|
+
try {
|
|
193
|
+
await fs.access(collectionDir, constants.F_OK);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const files = await fs.readdir(collectionDir);
|
|
199
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
200
|
+
let documents = [];
|
|
201
|
+
// Read all documents
|
|
202
|
+
for (const file of jsonFiles) {
|
|
203
|
+
const id = path.basename(file, '.json');
|
|
204
|
+
try {
|
|
205
|
+
const doc = await this.getDocument(collection, id);
|
|
206
|
+
if (doc) {
|
|
207
|
+
documents.push(doc);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// Skip corrupted files but log the error
|
|
212
|
+
if (!this.config.silent) {
|
|
213
|
+
console.warn(`Skipping corrupted document ${collection}/${id}: ${error}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Apply filtering
|
|
218
|
+
if (options.filter) {
|
|
219
|
+
documents = documents.filter(doc => this.matchesFilter(doc, options.filter));
|
|
220
|
+
}
|
|
221
|
+
// Apply sorting
|
|
222
|
+
if (options.sort) {
|
|
223
|
+
const sortFields = Array.isArray(options.sort) ? options.sort : [options.sort];
|
|
224
|
+
documents = this.sortDocuments(documents, sortFields);
|
|
225
|
+
}
|
|
226
|
+
// Apply pagination with validated limits
|
|
227
|
+
return documents.slice(offset, offset + limit);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
throw new Error(`Failed to list documents in collection ${collection}: ${error}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async deleteDocument(collection, id) {
|
|
234
|
+
try {
|
|
235
|
+
// Input validation is handled in getDocumentPath
|
|
236
|
+
const filePath = this.getDocumentPath(collection, id);
|
|
237
|
+
// Atomic delete - check and delete in one operation
|
|
238
|
+
await fs.unlink(filePath);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error.code === 'ENOENT') {
|
|
242
|
+
throw new Error(`Document ${collection}/${id} not found`);
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`Failed to delete document ${collection}/${id}: ${error}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Media operations
|
|
248
|
+
async uploadFile(file, metadata) {
|
|
249
|
+
try {
|
|
250
|
+
// Validate media metadata
|
|
251
|
+
this.validateMediaMetadata(metadata);
|
|
252
|
+
// Use secure path methods
|
|
253
|
+
const filePath = this.getMediaPath(metadata.id, metadata.extension);
|
|
254
|
+
const metadataPath = this.getMediaMetadataPath(metadata.id);
|
|
255
|
+
// Ensure media directory exists
|
|
256
|
+
await fsExtra.ensureDir(path.dirname(filePath), { mode: this.config.dirMode });
|
|
257
|
+
await fsExtra.ensureDir(path.dirname(metadataPath), { mode: this.config.dirMode });
|
|
258
|
+
// Write file content
|
|
259
|
+
const buffer = await this.fileToBuffer(file);
|
|
260
|
+
await fs.writeFile(filePath, buffer, { mode: this.config.fileMode });
|
|
261
|
+
const now = new Date();
|
|
262
|
+
const fileMetadata = {
|
|
263
|
+
id: metadata.id,
|
|
264
|
+
filename: metadata.filename,
|
|
265
|
+
contentType: metadata.contentType,
|
|
266
|
+
size: metadata.size,
|
|
267
|
+
extension: metadata.extension,
|
|
268
|
+
originalPath: filePath,
|
|
269
|
+
createdAt: now,
|
|
270
|
+
updatedAt: now
|
|
271
|
+
};
|
|
272
|
+
// Write metadata atomically
|
|
273
|
+
const metadataContent = this.config.prettyJson
|
|
274
|
+
? JSON.stringify(fileMetadata, this.dateReplacer, this.config.jsonSpaces)
|
|
275
|
+
: JSON.stringify(fileMetadata, this.dateReplacer);
|
|
276
|
+
await this.atomicWriteFile(metadataPath, metadataContent);
|
|
277
|
+
// Generate file URL (relative path for portability)
|
|
278
|
+
const relativeUrl = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
279
|
+
const mediaFile = {
|
|
280
|
+
id: metadata.id,
|
|
281
|
+
filename: metadata.filename,
|
|
282
|
+
contentType: metadata.contentType,
|
|
283
|
+
size: metadata.size,
|
|
284
|
+
metadata: {
|
|
285
|
+
path: relativeUrl,
|
|
286
|
+
extension: metadata.extension,
|
|
287
|
+
originalFilename: metadata.filename
|
|
288
|
+
},
|
|
289
|
+
_createdAt: now
|
|
290
|
+
};
|
|
291
|
+
return mediaFile;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
throw new Error(`Failed to upload file ${metadata.filename}: ${error}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async getFile(id) {
|
|
298
|
+
try {
|
|
299
|
+
// Input validation is handled in getMediaMetadataPath
|
|
300
|
+
const metadataPath = this.getMediaMetadataPath(id);
|
|
301
|
+
// Check if metadata file exists
|
|
302
|
+
try {
|
|
303
|
+
await fs.access(metadataPath, constants.F_OK);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf-8');
|
|
309
|
+
const fileMetadata = this.safeParseJSON(metadataContent, this.dateReviver);
|
|
310
|
+
const filePath = this.getMediaPath(id, fileMetadata.extension);
|
|
311
|
+
// Check if actual file exists
|
|
312
|
+
try {
|
|
313
|
+
await fs.access(filePath, constants.F_OK);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Metadata exists but file is missing
|
|
317
|
+
throw new Error(`Media file ${id} metadata found but file is missing`);
|
|
318
|
+
}
|
|
319
|
+
const relativeUrl = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
320
|
+
const mediaFile = {
|
|
321
|
+
id: fileMetadata.id,
|
|
322
|
+
filename: fileMetadata.filename,
|
|
323
|
+
contentType: fileMetadata.contentType,
|
|
324
|
+
size: fileMetadata.size,
|
|
325
|
+
metadata: {
|
|
326
|
+
// Include basic file metadata
|
|
327
|
+
path: relativeUrl,
|
|
328
|
+
extension: fileMetadata.extension,
|
|
329
|
+
originalFilename: fileMetadata.filename,
|
|
330
|
+
// Include all user-editable metadata fields (include even if empty string)
|
|
331
|
+
...(fileMetadata.title !== undefined && { title: fileMetadata.title }),
|
|
332
|
+
...(fileMetadata.alt !== undefined && { alt: fileMetadata.alt }),
|
|
333
|
+
...(fileMetadata.author !== undefined && { author: fileMetadata.author }),
|
|
334
|
+
...(fileMetadata.credit !== undefined && { credit: fileMetadata.credit }),
|
|
335
|
+
// Include image processing metadata with updated URLs
|
|
336
|
+
...(fileMetadata.imageVariants && {
|
|
337
|
+
imageVariants: this.updateVariantUrls(fileMetadata.imageVariants, fileMetadata.id)
|
|
338
|
+
}),
|
|
339
|
+
...(fileMetadata.originalDimensions && { originalDimensions: fileMetadata.originalDimensions })
|
|
340
|
+
},
|
|
341
|
+
_createdAt: fileMetadata.createdAt instanceof Date ? fileMetadata.createdAt : new Date(fileMetadata.createdAt)
|
|
342
|
+
};
|
|
343
|
+
return mediaFile;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
throw new Error(`Failed to get file ${id}: ${error}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async updateFile(id, metadata) {
|
|
350
|
+
try {
|
|
351
|
+
// Input validation
|
|
352
|
+
SecurityValidator.validateDocumentId(id);
|
|
353
|
+
const metadataPath = this.getMediaMetadataPath(id);
|
|
354
|
+
// Check if metadata file exists
|
|
355
|
+
try {
|
|
356
|
+
await fs.access(metadataPath, constants.F_OK);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
throw new Error(`Media file ${id} not found`);
|
|
360
|
+
}
|
|
361
|
+
// Read existing metadata
|
|
362
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf-8');
|
|
363
|
+
const existingMetadata = this.safeParseJSON(metadataContent, this.dateReviver);
|
|
364
|
+
// Update metadata with new values
|
|
365
|
+
const updatedMetadata = {
|
|
366
|
+
...existingMetadata,
|
|
367
|
+
...metadata,
|
|
368
|
+
updatedAt: new Date(),
|
|
369
|
+
// Preserve core fields that shouldn't be changed
|
|
370
|
+
id: existingMetadata.id,
|
|
371
|
+
filename: existingMetadata.filename,
|
|
372
|
+
contentType: existingMetadata.contentType,
|
|
373
|
+
size: existingMetadata.size,
|
|
374
|
+
extension: existingMetadata.extension,
|
|
375
|
+
originalPath: existingMetadata.originalPath,
|
|
376
|
+
createdAt: existingMetadata.createdAt
|
|
377
|
+
};
|
|
378
|
+
// Write updated metadata atomically
|
|
379
|
+
const updatedMetadataContent = this.config.prettyJson
|
|
380
|
+
? JSON.stringify(updatedMetadata, this.dateReplacer, this.config.jsonSpaces)
|
|
381
|
+
: JSON.stringify(updatedMetadata, this.dateReplacer);
|
|
382
|
+
await this.atomicWriteFile(metadataPath, updatedMetadataContent);
|
|
383
|
+
// Return updated MediaFile
|
|
384
|
+
const filePath = this.getMediaPath(id, updatedMetadata.extension);
|
|
385
|
+
const relativeUrl = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
386
|
+
const mediaFile = {
|
|
387
|
+
id: updatedMetadata.id,
|
|
388
|
+
filename: updatedMetadata.filename,
|
|
389
|
+
contentType: updatedMetadata.contentType,
|
|
390
|
+
size: updatedMetadata.size,
|
|
391
|
+
metadata: {
|
|
392
|
+
path: relativeUrl,
|
|
393
|
+
extension: updatedMetadata.extension,
|
|
394
|
+
originalFilename: updatedMetadata.filename,
|
|
395
|
+
// Include custom metadata fields
|
|
396
|
+
...metadata
|
|
397
|
+
},
|
|
398
|
+
_createdAt: updatedMetadata.createdAt instanceof Date ? updatedMetadata.createdAt : new Date(updatedMetadata.createdAt)
|
|
399
|
+
};
|
|
400
|
+
return mediaFile;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
console.error('[ERROR] FilesystemAdapter.updateFile failed:', error);
|
|
404
|
+
throw new Error(`Failed to update file metadata ${id}: ${error}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async getFileContent(id) {
|
|
408
|
+
try {
|
|
409
|
+
// Input validation is handled in getMediaMetadataPath
|
|
410
|
+
const metadataPath = this.getMediaMetadataPath(id);
|
|
411
|
+
// Check if metadata file exists
|
|
412
|
+
try {
|
|
413
|
+
await fs.access(metadataPath, constants.F_OK);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf-8');
|
|
419
|
+
const fileMetadata = this.safeParseJSON(metadataContent, this.dateReviver);
|
|
420
|
+
const filePath = this.getMediaPath(id, fileMetadata.extension);
|
|
421
|
+
// Check if actual file exists
|
|
422
|
+
try {
|
|
423
|
+
await fs.access(filePath, constants.F_OK);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
// Read file content as buffer and convert to ArrayBuffer
|
|
429
|
+
const buffer = await fs.readFile(filePath);
|
|
430
|
+
const sliced = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
431
|
+
// Ensure we return ArrayBuffer, not SharedArrayBuffer
|
|
432
|
+
if (sliced instanceof ArrayBuffer) {
|
|
433
|
+
return sliced;
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// Copy SharedArrayBuffer to ArrayBuffer
|
|
437
|
+
const sharedBuffer = sliced;
|
|
438
|
+
const arrayBuffer = new ArrayBuffer(sharedBuffer.byteLength);
|
|
439
|
+
new Uint8Array(arrayBuffer).set(new Uint8Array(sharedBuffer));
|
|
440
|
+
return arrayBuffer;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
throw new Error(`Failed to get file content ${id}: ${error}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async deleteFile(id) {
|
|
448
|
+
try {
|
|
449
|
+
// Input validation is handled in getMediaMetadataPath
|
|
450
|
+
const metadataPath = this.getMediaMetadataPath(id);
|
|
451
|
+
// Read metadata to get file extension
|
|
452
|
+
let extension = '';
|
|
453
|
+
try {
|
|
454
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf-8');
|
|
455
|
+
const fileMetadata = this.safeParseJSON(metadataContent, this.dateReviver);
|
|
456
|
+
extension = fileMetadata.extension;
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
throw new Error(`Media file ${id} not found`);
|
|
460
|
+
}
|
|
461
|
+
const filePath = this.getMediaPath(id, extension);
|
|
462
|
+
// Delete metadata FIRST, then main file
|
|
463
|
+
// This ensures if file deletion fails, the file won't show up in listMedia()
|
|
464
|
+
// since listMedia() uses metadata files as source of truth
|
|
465
|
+
const unlinkWithErrorCheck = async (path) => {
|
|
466
|
+
try {
|
|
467
|
+
await fs.unlink(path);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
// Only ignore "file not found" errors, throw everything else
|
|
471
|
+
if (error.code !== 'ENOENT') {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
// Delete metadata first, then file
|
|
477
|
+
await unlinkWithErrorCheck(metadataPath);
|
|
478
|
+
await unlinkWithErrorCheck(filePath);
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
throw new Error(`Failed to delete file ${id}: ${error}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async listMedia(options = {}) {
|
|
485
|
+
try {
|
|
486
|
+
// Apply resource limits
|
|
487
|
+
const limit = Math.min(options.limit || 1000, 1000);
|
|
488
|
+
const offset = Math.max(options.offset || 0, 0);
|
|
489
|
+
const metadataDir = path.join(this.config.mediaDir, '.metadata');
|
|
490
|
+
// Check if metadata directory exists
|
|
491
|
+
try {
|
|
492
|
+
await fs.access(metadataDir, constants.F_OK);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return { items: [], total: 0 };
|
|
496
|
+
}
|
|
497
|
+
const files = await fs.readdir(metadataDir);
|
|
498
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
499
|
+
const mediaFiles = [];
|
|
500
|
+
// Read all media metadata files
|
|
501
|
+
for (const file of jsonFiles) {
|
|
502
|
+
const id = path.basename(file, '.json');
|
|
503
|
+
try {
|
|
504
|
+
const mediaFile = await this.getFile(id);
|
|
505
|
+
if (mediaFile) {
|
|
506
|
+
mediaFiles.push(mediaFile);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
// Skip corrupted files but log the error
|
|
511
|
+
console.error(`[ERROR] Failed to load media file ${id}:`, error);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Sort by creation date (newest first)
|
|
515
|
+
mediaFiles.sort((a, b) => new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime());
|
|
516
|
+
// Store total before pagination
|
|
517
|
+
const total = mediaFiles.length;
|
|
518
|
+
// Apply pagination
|
|
519
|
+
const items = mediaFiles.slice(offset, offset + limit);
|
|
520
|
+
return { items, total };
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
console.error('[ERROR] FilesystemAdapter.listMedia failed:', error);
|
|
524
|
+
throw new Error(`Failed to list media files: ${error}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// User operations (system entities, stored separately from content)
|
|
528
|
+
async getUser(id) {
|
|
529
|
+
try {
|
|
530
|
+
const filePath = this.getUserPath(id);
|
|
531
|
+
// Check if file exists
|
|
532
|
+
try {
|
|
533
|
+
await fs.access(filePath, constants.F_OK);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
539
|
+
const user = this.safeParseJSON(content, this.dateReviver);
|
|
540
|
+
return user;
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
throw new Error(`Failed to read user ${id}: ${error}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async saveUser(id, userData) {
|
|
547
|
+
try {
|
|
548
|
+
const filePath = this.getUserPath(id);
|
|
549
|
+
const userDir = path.dirname(filePath);
|
|
550
|
+
// Ensure users directory exists
|
|
551
|
+
await fsExtra.ensureDir(userDir, { mode: this.config.dirMode });
|
|
552
|
+
// Check if user already exists
|
|
553
|
+
let existingUser = null;
|
|
554
|
+
try {
|
|
555
|
+
existingUser = await this.getUser(id);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// User doesn't exist, this is a new user
|
|
559
|
+
}
|
|
560
|
+
const now = new Date().toISOString();
|
|
561
|
+
const isUpdate = existingUser !== null;
|
|
562
|
+
const user = {
|
|
563
|
+
id,
|
|
564
|
+
username: userData.username || existingUser?.username || '',
|
|
565
|
+
email: userData.email || existingUser?.email || '',
|
|
566
|
+
passwordHash: userData.passwordHash || existingUser?.passwordHash || '',
|
|
567
|
+
firstName: userData.firstName || existingUser?.firstName || '',
|
|
568
|
+
lastName: userData.lastName || existingUser?.lastName || '',
|
|
569
|
+
role: userData.role || existingUser?.role || 'viewer',
|
|
570
|
+
permissions: userData.permissions || existingUser?.permissions || ['content:read'],
|
|
571
|
+
isActive: userData.isActive ?? existingUser?.isActive ?? true,
|
|
572
|
+
profileImage: userData.profileImage || existingUser?.profileImage,
|
|
573
|
+
preferences: userData.preferences || existingUser?.preferences || {},
|
|
574
|
+
lastLoginAt: userData.lastLoginAt || existingUser?.lastLoginAt,
|
|
575
|
+
createdAt: existingUser?.createdAt || now,
|
|
576
|
+
updatedAt: now
|
|
577
|
+
};
|
|
578
|
+
// Write to file atomically
|
|
579
|
+
const jsonContent = this.config.prettyJson
|
|
580
|
+
? JSON.stringify(user, this.dateReplacer, this.config.jsonSpaces)
|
|
581
|
+
: JSON.stringify(user, this.dateReplacer);
|
|
582
|
+
await this.atomicWriteFile(filePath, jsonContent);
|
|
583
|
+
if (this.config.syncWrites) {
|
|
584
|
+
// Force sync to disk
|
|
585
|
+
const fileHandle = await fs.open(filePath, 'r+');
|
|
586
|
+
await fileHandle.sync();
|
|
587
|
+
await fileHandle.close();
|
|
588
|
+
}
|
|
589
|
+
return user;
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
throw new Error(`Failed to save user ${id}: ${error}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async listUsers(options = {}) {
|
|
596
|
+
try {
|
|
597
|
+
// Apply resource limits
|
|
598
|
+
const limit = Math.min(options.limit || 1000, 1000);
|
|
599
|
+
const offset = Math.max(options.offset || 0, 0);
|
|
600
|
+
// Check if users directory exists
|
|
601
|
+
try {
|
|
602
|
+
await fs.access(this.config.usersDir, constants.F_OK);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
const files = await fs.readdir(this.config.usersDir);
|
|
608
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
609
|
+
let users = [];
|
|
610
|
+
// Read all users
|
|
611
|
+
for (const file of jsonFiles) {
|
|
612
|
+
const id = path.basename(file, '.json');
|
|
613
|
+
try {
|
|
614
|
+
const user = await this.getUser(id);
|
|
615
|
+
if (user) {
|
|
616
|
+
users.push(user);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
// Skip corrupted files but log the error
|
|
621
|
+
if (!this.config.silent) {
|
|
622
|
+
console.warn(`Skipping corrupted user ${id}: ${error}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// Apply filtering
|
|
627
|
+
if (options.role) {
|
|
628
|
+
users = users.filter(user => user.role === options.role);
|
|
629
|
+
}
|
|
630
|
+
if (options.isActive !== undefined) {
|
|
631
|
+
users = users.filter(user => user.isActive === options.isActive);
|
|
632
|
+
}
|
|
633
|
+
// Sort by creation date (newest first)
|
|
634
|
+
users.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
635
|
+
// Apply pagination
|
|
636
|
+
return users.slice(offset, offset + limit);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
throw new Error(`Failed to list users: ${error}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async deleteUser(id) {
|
|
643
|
+
try {
|
|
644
|
+
const filePath = this.getUserPath(id);
|
|
645
|
+
// Atomic delete
|
|
646
|
+
await fs.unlink(filePath);
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
if (error.code === 'ENOENT') {
|
|
650
|
+
throw new Error(`User ${id} not found`);
|
|
651
|
+
}
|
|
652
|
+
throw new Error(`Failed to delete user ${id}: ${error}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async getUserByUsername(username) {
|
|
656
|
+
try {
|
|
657
|
+
const users = await this.listUsers();
|
|
658
|
+
return users.find(user => user.username === username) || null;
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
throw new Error(`Failed to get user by username ${username}: ${error}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async getUserByEmail(email) {
|
|
665
|
+
try {
|
|
666
|
+
const users = await this.listUsers();
|
|
667
|
+
return users.find(user => user.email === email) || null;
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
throw new Error(`Failed to get user by email ${email}: ${error}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async getUserByOAuthProvider(provider, providerId) {
|
|
674
|
+
if (!provider || !providerId) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const users = await this.listUsers();
|
|
679
|
+
return users.find(user => user.oauthProviders?.some(p => p.provider === provider && p.providerId === providerId)) || null;
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
throw new Error(`Failed to get user by OAuth provider ${provider}:${providerId}: ${error}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// App Token operations (system entities, stored separately from content)
|
|
686
|
+
async getAppToken(id) {
|
|
687
|
+
try {
|
|
688
|
+
const filePath = this.getTokenPath(id);
|
|
689
|
+
// Check if file exists
|
|
690
|
+
try {
|
|
691
|
+
await fs.access(filePath, constants.F_OK);
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
697
|
+
const token = this.safeParseJSON(content, this.dateReviver);
|
|
698
|
+
return token;
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
throw new Error(`Failed to read app token ${id}: ${error}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async saveAppToken(id, tokenData) {
|
|
705
|
+
try {
|
|
706
|
+
const filePath = this.getTokenPath(id);
|
|
707
|
+
const tokenDir = path.dirname(filePath);
|
|
708
|
+
// Ensure tokens directory exists
|
|
709
|
+
await fsExtra.ensureDir(tokenDir, { mode: this.config.dirMode });
|
|
710
|
+
// Check if token already exists
|
|
711
|
+
let existingToken = null;
|
|
712
|
+
try {
|
|
713
|
+
existingToken = await this.getAppToken(id);
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
// Token doesn't exist, this is a new token
|
|
717
|
+
}
|
|
718
|
+
const now = new Date().toISOString();
|
|
719
|
+
const isUpdate = existingToken !== null;
|
|
720
|
+
const token = {
|
|
721
|
+
id,
|
|
722
|
+
name: tokenData.name || existingToken?.name || '',
|
|
723
|
+
description: tokenData.description || existingToken?.description,
|
|
724
|
+
tokenHash: tokenData.tokenHash || existingToken?.tokenHash || '',
|
|
725
|
+
permissions: tokenData.permissions || existingToken?.permissions || [],
|
|
726
|
+
createdBy: tokenData.createdBy || existingToken?.createdBy || '',
|
|
727
|
+
isActive: tokenData.isActive ?? existingToken?.isActive ?? true,
|
|
728
|
+
lastUsedAt: tokenData.lastUsedAt || existingToken?.lastUsedAt,
|
|
729
|
+
usageCount: tokenData.usageCount ?? existingToken?.usageCount ?? 0,
|
|
730
|
+
expiresAt: tokenData.expiresAt || existingToken?.expiresAt,
|
|
731
|
+
createdAt: existingToken?.createdAt || now,
|
|
732
|
+
updatedAt: now
|
|
733
|
+
};
|
|
734
|
+
// Validate required fields
|
|
735
|
+
if (!token.name) {
|
|
736
|
+
throw new Error('Token name is required');
|
|
737
|
+
}
|
|
738
|
+
if (!token.tokenHash) {
|
|
739
|
+
throw new Error('Token hash is required');
|
|
740
|
+
}
|
|
741
|
+
if (!token.createdBy) {
|
|
742
|
+
throw new Error('Token createdBy is required');
|
|
743
|
+
}
|
|
744
|
+
// Write token to file
|
|
745
|
+
const jsonContent = this.config.prettyJson
|
|
746
|
+
? JSON.stringify(token, this.dateReplacer, this.config.jsonSpaces)
|
|
747
|
+
: JSON.stringify(token, this.dateReplacer);
|
|
748
|
+
if (this.config.syncWrites) {
|
|
749
|
+
await fs.writeFile(filePath, jsonContent, { mode: this.config.fileMode, flag: 'w' });
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
await fs.writeFile(filePath, jsonContent, { mode: this.config.fileMode });
|
|
753
|
+
}
|
|
754
|
+
return token;
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
throw new Error(`Failed to save app token ${id}: ${error}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async listAppTokens(options = {}) {
|
|
761
|
+
try {
|
|
762
|
+
const limit = Math.min(options.limit || 1000, 1000);
|
|
763
|
+
const offset = Math.max(options.offset || 0, 0);
|
|
764
|
+
// Ensure tokens directory exists
|
|
765
|
+
await fsExtra.ensureDir(this.config.tokensDir, { mode: this.config.dirMode });
|
|
766
|
+
let files;
|
|
767
|
+
try {
|
|
768
|
+
files = await fs.readdir(this.config.tokensDir);
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
if (error.code === 'ENOENT') {
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
777
|
+
const tokens = [];
|
|
778
|
+
for (const file of jsonFiles) {
|
|
779
|
+
const id = path.basename(file, '.json');
|
|
780
|
+
try {
|
|
781
|
+
const token = await this.getAppToken(id);
|
|
782
|
+
if (token) {
|
|
783
|
+
tokens.push(token);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
// Skip corrupted files but log the error
|
|
788
|
+
if (!this.config.silent) {
|
|
789
|
+
console.warn(`Skipping corrupted app token ${id}: ${error}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// Apply filtering
|
|
794
|
+
let filteredTokens = tokens;
|
|
795
|
+
if (options.createdBy) {
|
|
796
|
+
filteredTokens = filteredTokens.filter(token => token.createdBy === options.createdBy);
|
|
797
|
+
}
|
|
798
|
+
if (options.isActive !== undefined) {
|
|
799
|
+
filteredTokens = filteredTokens.filter(token => token.isActive === options.isActive);
|
|
800
|
+
}
|
|
801
|
+
// Sort by creation date (newest first)
|
|
802
|
+
filteredTokens.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
803
|
+
// Apply pagination
|
|
804
|
+
return filteredTokens.slice(offset, offset + limit);
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
throw new Error(`Failed to list app tokens: ${error}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async deleteAppToken(id) {
|
|
811
|
+
try {
|
|
812
|
+
const filePath = this.getTokenPath(id);
|
|
813
|
+
// Atomic delete
|
|
814
|
+
await fs.unlink(filePath);
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
if (error.code === 'ENOENT') {
|
|
818
|
+
throw new Error(`App token ${id} not found`);
|
|
819
|
+
}
|
|
820
|
+
throw new Error(`Failed to delete app token ${id}: ${error}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async getAppTokenByHash(hash) {
|
|
824
|
+
try {
|
|
825
|
+
const tokens = await this.listAppTokens();
|
|
826
|
+
return tokens.find(token => token.tokenHash === hash) || null;
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
throw new Error(`Failed to get app token by hash: ${error}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Webhook operations (system entities, stored separately from content)
|
|
833
|
+
async getWebhook(id) {
|
|
834
|
+
try {
|
|
835
|
+
const filePath = this.getWebhookPath(id);
|
|
836
|
+
// Check if file exists
|
|
837
|
+
try {
|
|
838
|
+
await fs.access(filePath, constants.F_OK);
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
844
|
+
const webhook = this.safeParseJSON(content, this.dateReviver);
|
|
845
|
+
return webhook;
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
throw new Error(`Failed to read webhook ${id}: ${error}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async saveWebhook(id, webhookData) {
|
|
852
|
+
try {
|
|
853
|
+
SecurityValidator.validateDocumentId(id);
|
|
854
|
+
const filePath = this.getWebhookPath(id);
|
|
855
|
+
const webhookDir = path.dirname(filePath);
|
|
856
|
+
// Ensure webhook directory exists
|
|
857
|
+
await fsExtra.ensureDir(webhookDir, { mode: this.config.dirMode });
|
|
858
|
+
// Check if webhook already exists
|
|
859
|
+
let existingWebhook = null;
|
|
860
|
+
try {
|
|
861
|
+
existingWebhook = await this.getWebhook(id);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
// Webhook doesn't exist, this is a new webhook
|
|
865
|
+
}
|
|
866
|
+
const now = new Date();
|
|
867
|
+
const isUpdate = existingWebhook !== null;
|
|
868
|
+
const webhook = {
|
|
869
|
+
id,
|
|
870
|
+
name: webhookData.name || existingWebhook?.name || 'Untitled Webhook',
|
|
871
|
+
url: webhookData.url || existingWebhook?.url || '',
|
|
872
|
+
events: webhookData.events || existingWebhook?.events || [],
|
|
873
|
+
active: webhookData.active !== undefined ? webhookData.active : (existingWebhook?.active ?? true),
|
|
874
|
+
secret: webhookData.secret || existingWebhook?.secret || crypto.randomUUID(),
|
|
875
|
+
headers: webhookData.headers || existingWebhook?.headers || {},
|
|
876
|
+
retryPolicy: webhookData.retryPolicy || existingWebhook?.retryPolicy || {
|
|
877
|
+
maxRetries: 3,
|
|
878
|
+
backoffType: 'exponential',
|
|
879
|
+
baseDelay: 1000,
|
|
880
|
+
maxDelay: 30000,
|
|
881
|
+
retryOnStatus: [500, 502, 503, 504, 408, 429]
|
|
882
|
+
},
|
|
883
|
+
createdBy: webhookData.createdBy || existingWebhook?.createdBy || 'system',
|
|
884
|
+
createdAt: isUpdate ? existingWebhook.createdAt : now,
|
|
885
|
+
updatedAt: now
|
|
886
|
+
};
|
|
887
|
+
// Write to file with Date serialization
|
|
888
|
+
const jsonContent = this.config.prettyJson
|
|
889
|
+
? JSON.stringify(webhook, this.dateReplacer, this.config.jsonSpaces)
|
|
890
|
+
: JSON.stringify(webhook, this.dateReplacer);
|
|
891
|
+
await fs.writeFile(filePath, jsonContent, { mode: this.config.fileMode });
|
|
892
|
+
if (this.config.syncWrites) {
|
|
893
|
+
const fd = await fs.open(filePath, 'r+');
|
|
894
|
+
await fd.sync();
|
|
895
|
+
await fd.close();
|
|
896
|
+
}
|
|
897
|
+
return webhook;
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
throw new Error(`Failed to save webhook ${id}: ${error}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async listWebhooks(options = {}) {
|
|
904
|
+
try {
|
|
905
|
+
// Ensure webhooks directory exists
|
|
906
|
+
await fsExtra.ensureDir(this.config.webhooksDir, { mode: this.config.dirMode });
|
|
907
|
+
const files = await fs.readdir(this.config.webhooksDir);
|
|
908
|
+
const webhookFiles = files.filter(file => file.endsWith('.json'));
|
|
909
|
+
const webhooks = [];
|
|
910
|
+
for (const file of webhookFiles) {
|
|
911
|
+
try {
|
|
912
|
+
const id = path.basename(file, '.json');
|
|
913
|
+
const webhook = await this.getWebhook(id);
|
|
914
|
+
if (webhook) {
|
|
915
|
+
webhooks.push(webhook);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
// Skip invalid webhook files but log the issue
|
|
920
|
+
if (!this.config.silent) {
|
|
921
|
+
console.warn(`Warning: Could not read webhook file ${file}: ${error}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// Apply filters
|
|
926
|
+
let filteredWebhooks = webhooks;
|
|
927
|
+
if (options.active !== undefined) {
|
|
928
|
+
filteredWebhooks = filteredWebhooks.filter(webhook => webhook.active === options.active);
|
|
929
|
+
}
|
|
930
|
+
if (options.events && options.events.length > 0) {
|
|
931
|
+
filteredWebhooks = filteredWebhooks.filter(webhook => options.events.some((eventPattern) => webhook.events.some(webhookEvent => this.matchesEventPattern(webhookEvent, eventPattern))));
|
|
932
|
+
}
|
|
933
|
+
if (options.createdBy) {
|
|
934
|
+
filteredWebhooks = filteredWebhooks.filter(webhook => webhook.createdBy === options.createdBy);
|
|
935
|
+
}
|
|
936
|
+
// Sort by creation date (newest first)
|
|
937
|
+
filteredWebhooks.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
938
|
+
// Apply pagination
|
|
939
|
+
if (options.offset) {
|
|
940
|
+
filteredWebhooks = filteredWebhooks.slice(options.offset);
|
|
941
|
+
}
|
|
942
|
+
if (options.limit) {
|
|
943
|
+
filteredWebhooks = filteredWebhooks.slice(0, options.limit);
|
|
944
|
+
}
|
|
945
|
+
return filteredWebhooks;
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
throw new Error(`Failed to list webhooks: ${error}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
async deleteWebhook(id) {
|
|
952
|
+
try {
|
|
953
|
+
const filePath = this.getWebhookPath(id);
|
|
954
|
+
// Check if webhook exists
|
|
955
|
+
try {
|
|
956
|
+
await fs.access(filePath, constants.F_OK);
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
// Webhook doesn't exist, nothing to delete
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
await fs.unlink(filePath);
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
throw new Error(`Failed to delete webhook ${id}: ${error}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// Path helpers for app tokens
|
|
969
|
+
getTokenPath(id) {
|
|
970
|
+
SecurityValidator.validateDocumentId(id);
|
|
971
|
+
return path.join(this.config.tokensDir, `${id}.json`);
|
|
972
|
+
}
|
|
973
|
+
// Path helpers for webhooks
|
|
974
|
+
getWebhookPath(id) {
|
|
975
|
+
SecurityValidator.validateDocumentId(id);
|
|
976
|
+
const webhookPath = path.join(this.config.webhooksDir, `${id}.json`);
|
|
977
|
+
this.validateSecurePath(webhookPath, this.config.webhooksDir);
|
|
978
|
+
return webhookPath;
|
|
979
|
+
}
|
|
980
|
+
// Helper method for event pattern matching
|
|
981
|
+
matchesEventPattern(webhookEvent, pattern) {
|
|
982
|
+
if (pattern === '*')
|
|
983
|
+
return true;
|
|
984
|
+
if (pattern.endsWith('*')) {
|
|
985
|
+
return webhookEvent.startsWith(pattern.slice(0, -1));
|
|
986
|
+
}
|
|
987
|
+
return webhookEvent === pattern;
|
|
988
|
+
}
|
|
989
|
+
// Utility operations
|
|
990
|
+
async healthCheck() {
|
|
991
|
+
try {
|
|
992
|
+
// Ensure directories exist first
|
|
993
|
+
await fsExtra.ensureDir(this.config.contentDir, { mode: this.config.dirMode });
|
|
994
|
+
await fsExtra.ensureDir(this.config.mediaDir, { mode: this.config.dirMode });
|
|
995
|
+
await fsExtra.ensureDir(this.config.usersDir, { mode: this.config.dirMode });
|
|
996
|
+
await fsExtra.ensureDir(this.config.tokensDir, { mode: this.config.dirMode });
|
|
997
|
+
// Check if directories are accessible
|
|
998
|
+
await fs.access(this.config.contentDir, constants.R_OK | constants.W_OK);
|
|
999
|
+
await fs.access(this.config.mediaDir, constants.R_OK | constants.W_OK);
|
|
1000
|
+
await fs.access(this.config.usersDir, constants.R_OK | constants.W_OK);
|
|
1001
|
+
await fs.access(this.config.tokensDir, constants.R_OK | constants.W_OK);
|
|
1002
|
+
// Try to write a test file
|
|
1003
|
+
const testFile = path.join(this.config.contentDir, '.health-check');
|
|
1004
|
+
await fs.writeFile(testFile, 'test');
|
|
1005
|
+
await fs.unlink(testFile);
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
async migrate(migrations) {
|
|
1013
|
+
// For filesystem adapter, migrations might involve reorganizing file structure
|
|
1014
|
+
// This is a placeholder - actual migration logic would depend on specific needs
|
|
1015
|
+
const migrationLogPath = path.join(this.config.contentDir, '.migrations.json');
|
|
1016
|
+
let appliedMigrations = [];
|
|
1017
|
+
try {
|
|
1018
|
+
const logContent = await fs.readFile(migrationLogPath, 'utf-8');
|
|
1019
|
+
appliedMigrations = JSON.parse(logContent);
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
// No migration log exists yet
|
|
1023
|
+
}
|
|
1024
|
+
for (const migration of migrations) {
|
|
1025
|
+
if (!appliedMigrations.includes(migration.version)) {
|
|
1026
|
+
try {
|
|
1027
|
+
await migration.up();
|
|
1028
|
+
appliedMigrations.push(migration.version);
|
|
1029
|
+
// Update migration log
|
|
1030
|
+
await fs.writeFile(migrationLogPath, JSON.stringify(appliedMigrations, null, 2));
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
// Rollback on failure
|
|
1034
|
+
try {
|
|
1035
|
+
await migration.down();
|
|
1036
|
+
}
|
|
1037
|
+
catch (rollbackError) {
|
|
1038
|
+
throw new Error(`Migration ${migration.version} failed and rollback failed: ${error}, rollback: ${rollbackError}`);
|
|
1039
|
+
}
|
|
1040
|
+
throw new Error(`Migration ${migration.version} failed: ${error}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// Private helper methods
|
|
1046
|
+
getDocumentPath(collection, id) {
|
|
1047
|
+
// Validate inputs for security
|
|
1048
|
+
SecurityValidator.validateCollectionName(collection);
|
|
1049
|
+
SecurityValidator.validateDocumentId(id);
|
|
1050
|
+
const documentPath = path.join(this.config.contentDir, collection, `${id}.json`);
|
|
1051
|
+
this.validateSecurePath(documentPath, this.config.contentDir);
|
|
1052
|
+
return documentPath;
|
|
1053
|
+
}
|
|
1054
|
+
getMediaPath(id, extension) {
|
|
1055
|
+
// Validate inputs
|
|
1056
|
+
SecurityValidator.validateDocumentId(id);
|
|
1057
|
+
this.validateFileExtension(extension);
|
|
1058
|
+
const mediaPath = path.join(this.config.mediaDir, `${id}.${extension}`);
|
|
1059
|
+
this.validateSecurePath(mediaPath, this.config.mediaDir);
|
|
1060
|
+
return mediaPath;
|
|
1061
|
+
}
|
|
1062
|
+
getMediaMetadataPath(id) {
|
|
1063
|
+
// Validate inputs
|
|
1064
|
+
SecurityValidator.validateDocumentId(id);
|
|
1065
|
+
const metadataPath = path.join(this.config.mediaDir, '.metadata', `${id}.json`);
|
|
1066
|
+
this.validateSecurePath(metadataPath, this.config.mediaDir);
|
|
1067
|
+
return metadataPath;
|
|
1068
|
+
}
|
|
1069
|
+
getUserPath(id) {
|
|
1070
|
+
// Validate inputs
|
|
1071
|
+
SecurityValidator.validateDocumentId(id);
|
|
1072
|
+
const userPath = path.join(this.config.usersDir, `${id}.json`);
|
|
1073
|
+
this.validateSecurePath(userPath, this.config.usersDir);
|
|
1074
|
+
return userPath;
|
|
1075
|
+
}
|
|
1076
|
+
validateSecurePath(targetPath, basePath) {
|
|
1077
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
1078
|
+
const resolvedBase = path.resolve(basePath);
|
|
1079
|
+
if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
|
|
1080
|
+
throw new InvalidInputError('Path traversal attempt detected', 'path');
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
validateFileExtension(extension) {
|
|
1084
|
+
// Basic extension validation - prevent executable extensions and dangerous files
|
|
1085
|
+
const dangerousExtensions = [
|
|
1086
|
+
'exe', 'bat', 'cmd', 'com', 'pif', 'scr', 'vbs', 'vbe', 'js', 'jse',
|
|
1087
|
+
'wsf', 'wsh', 'msi', 'msp', 'mst', 'reg', 'scf', 'lnk', 'inf',
|
|
1088
|
+
'php', 'php3', 'php4', 'php5', 'phtml', 'pl', 'py', 'rb', 'sh'
|
|
1089
|
+
];
|
|
1090
|
+
const cleanExt = extension.toLowerCase().replace(/^\./, '');
|
|
1091
|
+
if (dangerousExtensions.includes(cleanExt)) {
|
|
1092
|
+
throw new InvalidInputError(`File extension '${extension}' is not allowed for security reasons`, 'extension');
|
|
1093
|
+
}
|
|
1094
|
+
// Ensure extension is alphanumeric with some common safe characters
|
|
1095
|
+
if (!/^[a-z0-9_-]+$/i.test(cleanExt)) {
|
|
1096
|
+
throw new InvalidInputError(`Invalid file extension format: ${extension}`, 'extension');
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
validateMediaMetadata(metadata) {
|
|
1100
|
+
// Validate required fields
|
|
1101
|
+
if (!metadata.id || !metadata.filename || !metadata.contentType || !metadata.extension) {
|
|
1102
|
+
throw new InvalidInputError('Missing required metadata fields', 'metadata');
|
|
1103
|
+
}
|
|
1104
|
+
// Validate file size
|
|
1105
|
+
if (metadata.size > this.MAX_FILE_SIZE) {
|
|
1106
|
+
throw new InvalidInputError(`File size ${metadata.size} exceeds maximum allowed size of ${this.MAX_FILE_SIZE} bytes`, 'size');
|
|
1107
|
+
}
|
|
1108
|
+
// Validate filename format
|
|
1109
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(metadata.filename)) {
|
|
1110
|
+
throw new InvalidInputError('Invalid filename format. Only alphanumeric characters, dots, underscores, and hyphens are allowed', 'filename');
|
|
1111
|
+
}
|
|
1112
|
+
// Validate content type format
|
|
1113
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_.]*$/.test(metadata.contentType)) {
|
|
1114
|
+
throw new InvalidInputError('Invalid content type format', 'contentType');
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
safeParseJSON(content, reviver) {
|
|
1118
|
+
// Check content size to prevent DoS attacks
|
|
1119
|
+
if (content.length > this.MAX_DOCUMENT_SIZE) {
|
|
1120
|
+
throw new InvalidInputError(`JSON content size ${content.length} exceeds maximum allowed size of ${this.MAX_DOCUMENT_SIZE} bytes`, 'size');
|
|
1121
|
+
}
|
|
1122
|
+
try {
|
|
1123
|
+
return JSON.parse(content, reviver);
|
|
1124
|
+
}
|
|
1125
|
+
catch (error) {
|
|
1126
|
+
throw new InvalidInputError(`Invalid JSON format: ${error}`, 'json');
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
async atomicWriteFile(filePath, content) {
|
|
1130
|
+
// Write to temporary file first, then rename to prevent race conditions
|
|
1131
|
+
const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substr(2, 9)}`;
|
|
1132
|
+
try {
|
|
1133
|
+
await fs.writeFile(tempPath, content, {
|
|
1134
|
+
mode: this.config.fileMode,
|
|
1135
|
+
flag: 'wx' // Fail if file exists
|
|
1136
|
+
});
|
|
1137
|
+
// Atomic rename
|
|
1138
|
+
await fs.rename(tempPath, filePath);
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
// Clean up temp file if it exists
|
|
1142
|
+
try {
|
|
1143
|
+
await fs.unlink(tempPath);
|
|
1144
|
+
}
|
|
1145
|
+
catch {
|
|
1146
|
+
// Ignore cleanup errors
|
|
1147
|
+
}
|
|
1148
|
+
throw error;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
dateReplacer = (key, value) => {
|
|
1152
|
+
// Convert Date objects to ISO strings
|
|
1153
|
+
if (value instanceof Date) {
|
|
1154
|
+
return value.toISOString();
|
|
1155
|
+
}
|
|
1156
|
+
return value;
|
|
1157
|
+
};
|
|
1158
|
+
dateReviver = (key, value) => {
|
|
1159
|
+
// Convert ISO date strings back to Date objects
|
|
1160
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
|
|
1161
|
+
return new Date(value);
|
|
1162
|
+
}
|
|
1163
|
+
return value;
|
|
1164
|
+
};
|
|
1165
|
+
reviveDatesInObject(obj) {
|
|
1166
|
+
if (obj === null || typeof obj !== 'object') {
|
|
1167
|
+
return obj;
|
|
1168
|
+
}
|
|
1169
|
+
if (Array.isArray(obj)) {
|
|
1170
|
+
return obj.map(item => this.reviveDatesInObject(item));
|
|
1171
|
+
}
|
|
1172
|
+
const result = {};
|
|
1173
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1174
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
|
|
1175
|
+
// console.log(`Converting date string ${value} to Date object`)
|
|
1176
|
+
result[key] = new Date(value);
|
|
1177
|
+
}
|
|
1178
|
+
else if (typeof value === 'object' && value !== null) {
|
|
1179
|
+
result[key] = this.reviveDatesInObject(value);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
result[key] = value;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return result;
|
|
1186
|
+
}
|
|
1187
|
+
async fileToBuffer(file) {
|
|
1188
|
+
// Convert File to Buffer for Node.js filesystem
|
|
1189
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
1190
|
+
return Buffer.from(arrayBuffer);
|
|
1191
|
+
}
|
|
1192
|
+
matchesFilter(document, filter) {
|
|
1193
|
+
for (const [field, value] of Object.entries(filter)) {
|
|
1194
|
+
const docValue = document[field];
|
|
1195
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1196
|
+
// Handle comparison operators
|
|
1197
|
+
for (const [op, compareValue] of Object.entries(value)) {
|
|
1198
|
+
switch (op) {
|
|
1199
|
+
case '$eq':
|
|
1200
|
+
if (docValue !== compareValue)
|
|
1201
|
+
return false;
|
|
1202
|
+
break;
|
|
1203
|
+
case '$ne':
|
|
1204
|
+
if (docValue === compareValue)
|
|
1205
|
+
return false;
|
|
1206
|
+
break;
|
|
1207
|
+
case '$gt':
|
|
1208
|
+
if (typeof docValue !== 'number' || typeof compareValue !== 'number' || docValue <= compareValue)
|
|
1209
|
+
return false;
|
|
1210
|
+
break;
|
|
1211
|
+
case '$gte':
|
|
1212
|
+
if (typeof docValue !== 'number' || typeof compareValue !== 'number' || docValue < compareValue)
|
|
1213
|
+
return false;
|
|
1214
|
+
break;
|
|
1215
|
+
case '$lt':
|
|
1216
|
+
if (typeof docValue !== 'number' || typeof compareValue !== 'number' || docValue >= compareValue)
|
|
1217
|
+
return false;
|
|
1218
|
+
break;
|
|
1219
|
+
case '$lte':
|
|
1220
|
+
if (typeof docValue !== 'number' || typeof compareValue !== 'number' || docValue > compareValue)
|
|
1221
|
+
return false;
|
|
1222
|
+
break;
|
|
1223
|
+
case '$in':
|
|
1224
|
+
if (!Array.isArray(compareValue) || !compareValue.includes(docValue))
|
|
1225
|
+
return false;
|
|
1226
|
+
break;
|
|
1227
|
+
case '$nin':
|
|
1228
|
+
if (!Array.isArray(compareValue) || compareValue.includes(docValue))
|
|
1229
|
+
return false;
|
|
1230
|
+
break;
|
|
1231
|
+
default:
|
|
1232
|
+
// Unknown operator, skip
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
// Direct value comparison
|
|
1239
|
+
if (docValue !== value)
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
sortDocuments(documents, sortFields) {
|
|
1246
|
+
return documents.sort((a, b) => {
|
|
1247
|
+
for (const sortField of sortFields) {
|
|
1248
|
+
const [field, direction = 'asc'] = sortField.split('.');
|
|
1249
|
+
const aValue = a[field];
|
|
1250
|
+
const bValue = b[field];
|
|
1251
|
+
let comparison = 0;
|
|
1252
|
+
if (aValue < bValue)
|
|
1253
|
+
comparison = -1;
|
|
1254
|
+
else if (aValue > bValue)
|
|
1255
|
+
comparison = 1;
|
|
1256
|
+
if (direction === 'desc')
|
|
1257
|
+
comparison *= -1;
|
|
1258
|
+
if (comparison !== 0)
|
|
1259
|
+
return comparison;
|
|
1260
|
+
}
|
|
1261
|
+
return 0;
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
// Variant file operations
|
|
1265
|
+
async saveVariantFile(parentId, variantName, buffer, format) {
|
|
1266
|
+
try {
|
|
1267
|
+
// Create variants directory if it doesn't exist
|
|
1268
|
+
const variantsDir = path.join(this.config.mediaDir, 'variants');
|
|
1269
|
+
await fsExtra.ensureDir(variantsDir, { mode: this.config.dirMode });
|
|
1270
|
+
// Create parent directory for this image's variants
|
|
1271
|
+
const parentVariantsDir = path.join(variantsDir, parentId);
|
|
1272
|
+
await fsExtra.ensureDir(parentVariantsDir, { mode: this.config.dirMode });
|
|
1273
|
+
// Save variant file
|
|
1274
|
+
const variantFilename = `${variantName}.${format}`;
|
|
1275
|
+
const variantPath = path.join(parentVariantsDir, variantFilename);
|
|
1276
|
+
await fs.writeFile(variantPath, buffer);
|
|
1277
|
+
// Return relative path for URL generation
|
|
1278
|
+
return `variants/${parentId}/${variantFilename}`;
|
|
1279
|
+
}
|
|
1280
|
+
catch (error) {
|
|
1281
|
+
console.error('[ERROR] FilesystemAdapter.saveVariantFile failed:', error);
|
|
1282
|
+
throw new Error(`Failed to save variant file ${parentId}/${variantName}: ${error}`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
getVariantUrl(parentId, variantName) {
|
|
1286
|
+
// No URL generation - let frontend handle URL construction
|
|
1287
|
+
return '';
|
|
1288
|
+
}
|
|
1289
|
+
updateVariantUrls(imageVariants, parentId) {
|
|
1290
|
+
// Update variant URLs to use the new API format
|
|
1291
|
+
const updatedVariants = {};
|
|
1292
|
+
for (const [variantName, variantData] of Object.entries(imageVariants)) {
|
|
1293
|
+
updatedVariants[variantName] = {
|
|
1294
|
+
...variantData,
|
|
1295
|
+
url: this.getVariantUrl(parentId, variantName)
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return updatedVariants;
|
|
1299
|
+
}
|
|
1300
|
+
async getVariantContent(parentId, variantName) {
|
|
1301
|
+
try {
|
|
1302
|
+
// First check if the parent media file exists
|
|
1303
|
+
const mediaFile = await this.getFile(parentId);
|
|
1304
|
+
if (!mediaFile) {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
// Get variant info from metadata to determine format
|
|
1308
|
+
const variants = mediaFile.metadata?.imageVariants;
|
|
1309
|
+
if (!variants || !variants[variantName]) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
const variantInfo = variants[variantName];
|
|
1313
|
+
const variantFilename = `${variantName}.${variantInfo.format || 'webp'}`;
|
|
1314
|
+
const variantPath = path.join(this.config.mediaDir, 'variants', parentId, variantFilename);
|
|
1315
|
+
// Security check: ensure the resolved path is still within the media directory
|
|
1316
|
+
const resolvedPath = path.resolve(variantPath);
|
|
1317
|
+
const mediaBaseDir = path.resolve(this.config.mediaDir);
|
|
1318
|
+
if (!resolvedPath.startsWith(mediaBaseDir)) {
|
|
1319
|
+
throw new Error('Access denied: path traversal detected');
|
|
1320
|
+
}
|
|
1321
|
+
// Check if variant file exists
|
|
1322
|
+
try {
|
|
1323
|
+
await fs.access(resolvedPath, constants.F_OK);
|
|
1324
|
+
const stats = await fs.stat(resolvedPath);
|
|
1325
|
+
if (!stats.isFile()) {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
catch {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
// Read and return variant file content
|
|
1333
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
1334
|
+
const sliced = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
1335
|
+
// Ensure we return ArrayBuffer, not SharedArrayBuffer
|
|
1336
|
+
if (sliced instanceof ArrayBuffer) {
|
|
1337
|
+
return sliced;
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
// Copy SharedArrayBuffer to ArrayBuffer
|
|
1341
|
+
const sharedBuffer = sliced;
|
|
1342
|
+
const arrayBuffer = new ArrayBuffer(sharedBuffer.byteLength);
|
|
1343
|
+
new Uint8Array(arrayBuffer).set(new Uint8Array(sharedBuffer));
|
|
1344
|
+
return arrayBuffer;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
catch (error) {
|
|
1348
|
+
console.error(`[ERROR] FilesystemAdapter.getVariantContent failed for ${parentId}/${variantName}:`, error);
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
async deleteVariantFiles(parentId) {
|
|
1353
|
+
try {
|
|
1354
|
+
const parentVariantsDir = path.join(this.config.mediaDir, 'variants', parentId);
|
|
1355
|
+
// Check if directory exists before trying to delete
|
|
1356
|
+
try {
|
|
1357
|
+
await fs.access(parentVariantsDir, constants.F_OK);
|
|
1358
|
+
await fs.rm(parentVariantsDir, { recursive: true, force: true });
|
|
1359
|
+
}
|
|
1360
|
+
catch {
|
|
1361
|
+
// Directory doesn't exist, nothing to delete
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
catch (error) {
|
|
1365
|
+
console.error('[ERROR] FilesystemAdapter.deleteVariantFiles failed:', error);
|
|
1366
|
+
throw new Error(`Failed to delete variant files for ${parentId}: ${error}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
//# sourceMappingURL=filesystem-adapter.js.map
|