@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,1363 @@
|
|
|
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, createLogger } from '../../core/index.js';
|
|
5
|
+
export class FilesystemDataAdapter {
|
|
6
|
+
/** Resolves once the configured directories exist. Rejects if they could not be created. */
|
|
7
|
+
ready;
|
|
8
|
+
config;
|
|
9
|
+
logger = createLogger('adapter', 'FilesystemDataAdapter');
|
|
10
|
+
// Security limits
|
|
11
|
+
MAX_DOCUMENT_SIZE = 10 * 1024 * 1024; // 10MB
|
|
12
|
+
// Ceiling on any list operation. Matches the postgres adapter's MAX_LIST_LIMIT
|
|
13
|
+
// so both backends agree on how much a single call can return.
|
|
14
|
+
MAX_LIST_LIMIT = 1000;
|
|
15
|
+
userWriteLocks = new Map();
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = {
|
|
18
|
+
contentDir: config.contentDir || './content',
|
|
19
|
+
usersDir: config.usersDir || './users',
|
|
20
|
+
tokensDir: config.tokensDir || './tokens',
|
|
21
|
+
webhooksDir: config.webhooksDir || './webhooks',
|
|
22
|
+
settingsDir: config.settingsDir || './settings',
|
|
23
|
+
auditLogsDir: config.auditLogsDir || './audit-logs',
|
|
24
|
+
createDirs: config.createDirs ?? true,
|
|
25
|
+
prettyJson: config.prettyJson ?? true,
|
|
26
|
+
jsonSpaces: config.jsonSpaces ?? 2,
|
|
27
|
+
syncWrites: config.syncWrites ?? false,
|
|
28
|
+
fileMode: config.fileMode ?? 0o644,
|
|
29
|
+
dirMode: config.dirMode ?? 0o755,
|
|
30
|
+
silent: config.silent ?? false
|
|
31
|
+
};
|
|
32
|
+
// Directory creation is async and a constructor cannot await it. Keeping the promise
|
|
33
|
+
// gives callers something to wait on: without it the adapter can still be creating
|
|
34
|
+
// its directories after a caller believes it is done, which is how a teardown ends up
|
|
35
|
+
// racing ensureDir and failing with ENOTEMPTY. The catch only stops an early failure
|
|
36
|
+
// becoming an unhandled rejection; `ready` still rejects for whoever awaits it.
|
|
37
|
+
this.ready = this.config.createDirs ? this.initializeDirectories() : Promise.resolve();
|
|
38
|
+
this.ready.catch(() => undefined);
|
|
39
|
+
if (!this.config.silent) {
|
|
40
|
+
this.logger.info('FilesystemDataAdapter initialized', {
|
|
41
|
+
contentDir: this.config.contentDir,
|
|
42
|
+
usersDir: this.config.usersDir,
|
|
43
|
+
tokensDir: this.config.tokensDir
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async initializeDirectories() {
|
|
48
|
+
try {
|
|
49
|
+
await fsExtra.ensureDir(this.config.contentDir, { mode: this.config.dirMode });
|
|
50
|
+
await fsExtra.ensureDir(this.config.usersDir, { mode: this.config.dirMode });
|
|
51
|
+
await fsExtra.ensureDir(this.config.tokensDir, { mode: this.config.dirMode });
|
|
52
|
+
await fsExtra.ensureDir(this.config.webhooksDir, { mode: this.config.dirMode });
|
|
53
|
+
await fsExtra.ensureDir(this.config.settingsDir, { mode: this.config.dirMode });
|
|
54
|
+
await fsExtra.ensureDir(this.config.auditLogsDir, { mode: this.config.dirMode });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!this.config.silent) {
|
|
58
|
+
this.logger.error('Failed to initialize directories', error);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ==========================================================================
|
|
64
|
+
// DOCUMENT OPERATIONS
|
|
65
|
+
// ==========================================================================
|
|
66
|
+
async getDocument(collection, id) {
|
|
67
|
+
try {
|
|
68
|
+
SecurityValidator.validateCollectionName(collection);
|
|
69
|
+
SecurityValidator.validateDocumentId(id);
|
|
70
|
+
const filePath = this.getDocumentPath(collection, id);
|
|
71
|
+
try {
|
|
72
|
+
await fs.access(filePath, constants.F_OK);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null; // File doesn't exist
|
|
76
|
+
}
|
|
77
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
78
|
+
const documentFile = JSON.parse(fileContent);
|
|
79
|
+
return this.documentFileToDocument(documentFile, collection);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.logger.error(`Failed to get document ${id} from collection ${collection}`, error);
|
|
83
|
+
throw new Error(`Failed to get document: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async documentExists(collection, id) {
|
|
87
|
+
SecurityValidator.validateCollectionName(collection);
|
|
88
|
+
SecurityValidator.validateDocumentId(id);
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(this.getDocumentPath(collection, id), constants.F_OK);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Missing file, or a collection directory that was never created.
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async saveDocument(collection, id, data, auditContext) {
|
|
99
|
+
try {
|
|
100
|
+
SecurityValidator.validateCollectionName(collection);
|
|
101
|
+
SecurityValidator.validateDocumentId(id);
|
|
102
|
+
const collectionDir = path.join(this.config.contentDir, collection);
|
|
103
|
+
await fsExtra.ensureDir(collectionDir, { mode: this.config.dirMode });
|
|
104
|
+
const filePath = this.getDocumentPath(collection, id);
|
|
105
|
+
let documentFile;
|
|
106
|
+
// Check if document exists
|
|
107
|
+
let isUpdate = false;
|
|
108
|
+
try {
|
|
109
|
+
await fs.access(filePath, constants.F_OK);
|
|
110
|
+
const existingContent = await fs.readFile(filePath, 'utf8');
|
|
111
|
+
const existingDoc = JSON.parse(existingContent);
|
|
112
|
+
// Document exists, this is an update
|
|
113
|
+
isUpdate = true;
|
|
114
|
+
// Use _status from data if explicitly provided, otherwise preserve existing status
|
|
115
|
+
const status = data._status ?? existingDoc.metadata.status;
|
|
116
|
+
documentFile = {
|
|
117
|
+
id,
|
|
118
|
+
collection,
|
|
119
|
+
data,
|
|
120
|
+
metadata: {
|
|
121
|
+
createdAt: existingDoc.metadata.createdAt,
|
|
122
|
+
updatedAt: new Date(),
|
|
123
|
+
revision: existingDoc.metadata.revision + 1,
|
|
124
|
+
status,
|
|
125
|
+
// Preserve original creator, update current user
|
|
126
|
+
createdBy: existingDoc.metadata.createdBy,
|
|
127
|
+
createdByType: existingDoc.metadata.createdByType,
|
|
128
|
+
updatedBy: auditContext?.userId,
|
|
129
|
+
updatedByType: auditContext?.userType
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Document doesn't exist, this is a new document
|
|
135
|
+
// Extract _status from data if provided (for restore), otherwise default to 'draft'
|
|
136
|
+
const status = data._status || 'draft';
|
|
137
|
+
documentFile = {
|
|
138
|
+
id,
|
|
139
|
+
collection,
|
|
140
|
+
data,
|
|
141
|
+
metadata: {
|
|
142
|
+
createdAt: new Date(),
|
|
143
|
+
updatedAt: new Date(),
|
|
144
|
+
revision: 1,
|
|
145
|
+
status,
|
|
146
|
+
createdBy: auditContext?.userId,
|
|
147
|
+
createdByType: auditContext?.userType,
|
|
148
|
+
updatedBy: auditContext?.userId,
|
|
149
|
+
updatedByType: auditContext?.userType
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Validate content size
|
|
154
|
+
const jsonContent = this.formatJson(documentFile);
|
|
155
|
+
if (jsonContent.length > this.MAX_DOCUMENT_SIZE) {
|
|
156
|
+
throw new InvalidInputError(`Document content too large (${jsonContent.length} bytes). Maximum size is ${this.MAX_DOCUMENT_SIZE} bytes.`, 'size');
|
|
157
|
+
}
|
|
158
|
+
// Write document file
|
|
159
|
+
await this.atomicWriteFile(filePath, jsonContent);
|
|
160
|
+
if (!this.config.silent) {
|
|
161
|
+
this.logger.info(`Document ${isUpdate ? 'updated' : 'created'}`, { collection, id, revision: documentFile.metadata.revision });
|
|
162
|
+
}
|
|
163
|
+
return this.documentFileToDocument(documentFile, collection);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.logger.error(`Failed to save document ${id} in collection ${collection}`, error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async listDocuments(collection, options = {}) {
|
|
171
|
+
try {
|
|
172
|
+
SecurityValidator.validateCollectionName(collection);
|
|
173
|
+
const collectionDir = path.join(this.config.contentDir, collection);
|
|
174
|
+
// Check if collection directory exists
|
|
175
|
+
try {
|
|
176
|
+
await fs.access(collectionDir, constants.F_OK);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return []; // Collection doesn't exist
|
|
180
|
+
}
|
|
181
|
+
const files = await fs.readdir(collectionDir);
|
|
182
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
183
|
+
// Read all documents
|
|
184
|
+
const documents = [];
|
|
185
|
+
for (const file of jsonFiles) {
|
|
186
|
+
try {
|
|
187
|
+
const filePath = path.join(collectionDir, file);
|
|
188
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
189
|
+
const documentFile = JSON.parse(fileContent);
|
|
190
|
+
documents.push(this.documentFileToDocument(documentFile, collection));
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
this.logger.warn(`Skipping invalid document file ${file}`, error);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Apply filters
|
|
198
|
+
let filteredDocuments = documents;
|
|
199
|
+
if (options.filter) {
|
|
200
|
+
filteredDocuments = documents.filter(doc => {
|
|
201
|
+
for (const [key, value] of Object.entries(options.filter)) {
|
|
202
|
+
if (doc[key] !== value) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// Apply sorting - default to newest first (_createdAt desc)
|
|
210
|
+
if (options.sort) {
|
|
211
|
+
const sortFields = Array.isArray(options.sort) ? options.sort : [options.sort];
|
|
212
|
+
filteredDocuments.sort((a, b) => {
|
|
213
|
+
for (const field of sortFields) {
|
|
214
|
+
let fieldName = field;
|
|
215
|
+
let direction = 'asc';
|
|
216
|
+
// Handle descending sort (field prefixed with -)
|
|
217
|
+
// Two grammars reach here. The routes layer normalises every incoming sort into
|
|
218
|
+
// `field.asc` / `field.desc` (routes/handlers/documents.ts), which is what the
|
|
219
|
+
// postgres adapter parses. This adapter understood only the `-field` form, so it
|
|
220
|
+
// looked for a property literally named "field.asc", found none, and returned
|
|
221
|
+
// documents unsorted -- silently, for every API request.
|
|
222
|
+
if (field.startsWith('-')) {
|
|
223
|
+
fieldName = field.substring(1);
|
|
224
|
+
direction = 'desc';
|
|
225
|
+
}
|
|
226
|
+
else if (field.includes('.')) {
|
|
227
|
+
const [name, dir] = field.split('.');
|
|
228
|
+
fieldName = name;
|
|
229
|
+
direction = dir?.toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
230
|
+
}
|
|
231
|
+
const aVal = a[fieldName];
|
|
232
|
+
const bVal = b[fieldName];
|
|
233
|
+
let comparison = 0;
|
|
234
|
+
if (aVal < bVal)
|
|
235
|
+
comparison = -1;
|
|
236
|
+
if (aVal > bVal)
|
|
237
|
+
comparison = 1;
|
|
238
|
+
if (comparison !== 0) {
|
|
239
|
+
return direction === 'desc' ? -comparison : comparison;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return 0;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Default sort: newest first (_createdAt descending)
|
|
247
|
+
filteredDocuments.sort((a, b) => {
|
|
248
|
+
const aDate = new Date(a._createdAt).getTime();
|
|
249
|
+
const bDate = new Date(b._createdAt).getTime();
|
|
250
|
+
return bDate - aDate; // Descending order (newest first)
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
// Apply pagination. No implicit default limit -- an unbounded call returns
|
|
254
|
+
// everything -- but never more than MAX_LIST_LIMIT rows in one response.
|
|
255
|
+
const { offset = 0, limit } = options;
|
|
256
|
+
const start = offset;
|
|
257
|
+
const effectiveLimit = Math.min(limit || this.MAX_LIST_LIMIT, this.MAX_LIST_LIMIT);
|
|
258
|
+
return filteredDocuments.slice(start, start + effectiveLimit);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.logger.error(`Failed to list documents in collection ${collection}`, error);
|
|
262
|
+
throw new Error(`Failed to list documents: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async countDocuments(collection, filter) {
|
|
266
|
+
try {
|
|
267
|
+
SecurityValidator.validateCollectionName(collection);
|
|
268
|
+
const collectionDir = path.join(this.config.contentDir, collection);
|
|
269
|
+
try {
|
|
270
|
+
await fs.access(collectionDir, constants.F_OK);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const files = await fs.readdir(collectionDir);
|
|
276
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
277
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
278
|
+
return jsonFiles.length;
|
|
279
|
+
}
|
|
280
|
+
// With filter, we need to read files to check values
|
|
281
|
+
let count = 0;
|
|
282
|
+
for (const file of jsonFiles) {
|
|
283
|
+
try {
|
|
284
|
+
const filePath = path.join(collectionDir, file);
|
|
285
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
286
|
+
const documentFile = JSON.parse(fileContent);
|
|
287
|
+
const doc = this.documentFileToDocument(documentFile, collection);
|
|
288
|
+
// Same strict, type-preserving equality as listDocuments, now against
|
|
289
|
+
// the full document shape rather than a four-field projection.
|
|
290
|
+
const matches = Object.entries(filter).every(([key, value]) => doc[key] === value);
|
|
291
|
+
if (matches)
|
|
292
|
+
count++;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return count;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
this.logger.error(`Failed to count documents in collection ${collection}`, error);
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async deleteDocument(collection, id) {
|
|
306
|
+
try {
|
|
307
|
+
SecurityValidator.validateCollectionName(collection);
|
|
308
|
+
SecurityValidator.validateDocumentId(id);
|
|
309
|
+
const filePath = this.getDocumentPath(collection, id);
|
|
310
|
+
try {
|
|
311
|
+
await fs.unlink(filePath);
|
|
312
|
+
if (!this.config.silent) {
|
|
313
|
+
this.logger.info('Document deleted', { collection, id });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (error.code === 'ENOENT') {
|
|
318
|
+
throw new Error(`Document ${id} not found in collection ${collection}`);
|
|
319
|
+
}
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
this.logger.error(`Failed to delete document ${id} from collection ${collection}`, error);
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ==========================================================================
|
|
329
|
+
// USER OPERATIONS
|
|
330
|
+
// ==========================================================================
|
|
331
|
+
async getUser(id) {
|
|
332
|
+
try {
|
|
333
|
+
SecurityValidator.validateDocumentId(id);
|
|
334
|
+
const filePath = this.getUserPath(id);
|
|
335
|
+
try {
|
|
336
|
+
await fs.access(filePath, constants.F_OK);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
342
|
+
const userFile = JSON.parse(fileContent);
|
|
343
|
+
return this.userFileToUser(userFile);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
this.logger.error(`Failed to get user ${id}`, error);
|
|
347
|
+
throw new Error(`Failed to get user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async saveUser(id, userData) {
|
|
351
|
+
SecurityValidator.validateDocumentId(id);
|
|
352
|
+
return this.withUserLock(id, () => this.writeUserFile(id, userData));
|
|
353
|
+
}
|
|
354
|
+
async saveUserIf(id, userData, condition) {
|
|
355
|
+
SecurityValidator.validateDocumentId(id);
|
|
356
|
+
return this.withUserLock(id, async () => {
|
|
357
|
+
const filePath = this.getUserPath(id);
|
|
358
|
+
let existingUser;
|
|
359
|
+
try {
|
|
360
|
+
const existingContent = await fs.readFile(filePath, 'utf8');
|
|
361
|
+
existingUser = JSON.parse(existingContent);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
// A missing user is a condition miss; anything else (permissions, I/O,
|
|
365
|
+
// corrupt JSON) is a storage failure and must surface as one.
|
|
366
|
+
if (error.code === 'ENOENT') {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
this.logger.error(`Failed to read user ${id} for conditional update`, error);
|
|
370
|
+
throw new Error(`Failed to read user: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
371
|
+
}
|
|
372
|
+
if (existingUser.passwordHash !== condition.passwordHash) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return this.writeUserFile(id, userData);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async writeUserFile(id, userData) {
|
|
379
|
+
try {
|
|
380
|
+
await fsExtra.ensureDir(this.config.usersDir, { mode: this.config.dirMode });
|
|
381
|
+
const filePath = this.getUserPath(id);
|
|
382
|
+
let userFile;
|
|
383
|
+
let isUpdate = false;
|
|
384
|
+
// Check if user exists
|
|
385
|
+
try {
|
|
386
|
+
await fs.access(filePath, constants.F_OK);
|
|
387
|
+
const existingContent = await fs.readFile(filePath, 'utf8');
|
|
388
|
+
const existingUser = JSON.parse(existingContent);
|
|
389
|
+
// User exists, this is an update
|
|
390
|
+
isUpdate = true;
|
|
391
|
+
const updateData = userData;
|
|
392
|
+
// A plain spread copies explicitly-`undefined` keys over the existing
|
|
393
|
+
// value, which would clear a field the caller never meant to touch.
|
|
394
|
+
// Only keys that are actually present AND not `undefined` take effect;
|
|
395
|
+
// an explicit `null` still gets through, and clears the field.
|
|
396
|
+
const presentUpdates = Object.fromEntries(Object.entries(updateData).filter(([, value]) => value !== undefined));
|
|
397
|
+
userFile = {
|
|
398
|
+
...existingUser,
|
|
399
|
+
...presentUpdates,
|
|
400
|
+
id,
|
|
401
|
+
updatedAt: new Date().toISOString()
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// User doesn't exist, this is a new user.
|
|
406
|
+
// Persist every supplied field: the create branch used to build the
|
|
407
|
+
// record field by field and silently drop `mfa`, `passkeys` and
|
|
408
|
+
// `lastLoginAt`, so `trokky restore` lost them.
|
|
409
|
+
const createData = userData;
|
|
410
|
+
userFile = {
|
|
411
|
+
id,
|
|
412
|
+
username: createData.username,
|
|
413
|
+
email: createData.email,
|
|
414
|
+
passwordHash: createData.passwordHash || '', // Use provided passwordHash from caller
|
|
415
|
+
firstName: createData.firstName,
|
|
416
|
+
lastName: createData.lastName,
|
|
417
|
+
role: createData.role,
|
|
418
|
+
permissions: createData.permissions || [],
|
|
419
|
+
isActive: createData.isActive ?? true,
|
|
420
|
+
profileImage: createData.profileImage,
|
|
421
|
+
preferences: createData.preferences,
|
|
422
|
+
oauthProviders: createData.oauthProviders,
|
|
423
|
+
mfa: createData.mfa,
|
|
424
|
+
passkeys: createData.passkeys,
|
|
425
|
+
lastLoginAt: createData.lastLoginAt,
|
|
426
|
+
createdAt: new Date().toISOString(),
|
|
427
|
+
updatedAt: new Date().toISOString()
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// Write user file
|
|
431
|
+
const jsonContent = this.formatJson(userFile);
|
|
432
|
+
await this.atomicWriteFile(filePath, jsonContent);
|
|
433
|
+
if (!this.config.silent) {
|
|
434
|
+
this.logger.info(`User ${isUpdate ? 'updated' : 'created'}`, {
|
|
435
|
+
id,
|
|
436
|
+
username: userFile.username,
|
|
437
|
+
email: userFile.email
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return this.userFileToUser(userFile);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
this.logger.error(`Failed to save user ${id}`, error);
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async listUsers(options = {}) {
|
|
448
|
+
try {
|
|
449
|
+
// Ensure users directory exists
|
|
450
|
+
try {
|
|
451
|
+
await fs.access(this.config.usersDir, constants.F_OK);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
const files = await fs.readdir(this.config.usersDir);
|
|
457
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
458
|
+
const users = [];
|
|
459
|
+
for (const file of jsonFiles) {
|
|
460
|
+
try {
|
|
461
|
+
const filePath = path.join(this.config.usersDir, file);
|
|
462
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
463
|
+
const userFile = JSON.parse(fileContent);
|
|
464
|
+
const user = this.userFileToUser(userFile);
|
|
465
|
+
users.push(user);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
this.logger.warn(`Skipping invalid user file ${file}`, error);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Apply filters
|
|
473
|
+
let filteredUsers = users;
|
|
474
|
+
if (options.role) {
|
|
475
|
+
filteredUsers = filteredUsers.filter(user => user.role === options.role);
|
|
476
|
+
}
|
|
477
|
+
if (options.isActive !== undefined) {
|
|
478
|
+
filteredUsers = filteredUsers.filter(user => user.isActive === options.isActive);
|
|
479
|
+
}
|
|
480
|
+
// Order newest first. Directory order is whatever readdir returns, which
|
|
481
|
+
// is arbitrary and differs from the postgres adapter's `createdAt DESC`.
|
|
482
|
+
filteredUsers.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
483
|
+
// Apply pagination. No implicit default limit, capped at MAX_LIST_LIMIT.
|
|
484
|
+
const { offset = 0, limit } = options;
|
|
485
|
+
const start = offset;
|
|
486
|
+
const effectiveLimit = Math.min(limit || this.MAX_LIST_LIMIT, this.MAX_LIST_LIMIT);
|
|
487
|
+
return filteredUsers.slice(start, start + effectiveLimit);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
this.logger.error('Failed to list users', error);
|
|
491
|
+
throw new Error(`Failed to list users: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async deleteUser(id) {
|
|
495
|
+
try {
|
|
496
|
+
SecurityValidator.validateDocumentId(id);
|
|
497
|
+
const filePath = this.getUserPath(id);
|
|
498
|
+
try {
|
|
499
|
+
await fs.unlink(filePath);
|
|
500
|
+
if (!this.config.silent) {
|
|
501
|
+
this.logger.info('User deleted', { id });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
if (error.code === 'ENOENT') {
|
|
506
|
+
throw new Error(`User ${id} not found`);
|
|
507
|
+
}
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
this.logger.error(`Failed to delete user ${id}`, error);
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async getUserByUsername(username) {
|
|
517
|
+
try {
|
|
518
|
+
const users = await this.listUsers();
|
|
519
|
+
return users.find(user => user.username === username) || null;
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.logger.error(`Failed to get user by username ${username}`, error);
|
|
523
|
+
throw new Error(`Failed to get user by username: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async getUserByEmail(email) {
|
|
527
|
+
try {
|
|
528
|
+
const users = await this.listUsers();
|
|
529
|
+
return users.find(user => user.email === email) || null;
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
this.logger.error(`Failed to get user by email ${email}`, error);
|
|
533
|
+
throw new Error(`Failed to get user by email: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async isUsernameAvailable(username) {
|
|
537
|
+
const user = await this.getUserByUsername(username);
|
|
538
|
+
return user === null;
|
|
539
|
+
}
|
|
540
|
+
async isEmailAvailable(email) {
|
|
541
|
+
const user = await this.getUserByEmail(email);
|
|
542
|
+
return user === null;
|
|
543
|
+
}
|
|
544
|
+
async getUserByOAuthProvider(provider, providerId) {
|
|
545
|
+
if (!provider || !providerId) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const users = await this.listUsers();
|
|
550
|
+
return users.find(user => user.oauthProviders?.some(p => p.provider === provider && p.providerId === providerId)) || null;
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
this.logger.error(`Failed to get user by OAuth provider ${provider}:${providerId}`, error);
|
|
554
|
+
throw new Error(`Failed to get user by OAuth provider: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// ==========================================================================
|
|
558
|
+
// APP TOKEN OPERATIONS
|
|
559
|
+
// ==========================================================================
|
|
560
|
+
async getAppToken(id) {
|
|
561
|
+
try {
|
|
562
|
+
SecurityValidator.validateDocumentId(id);
|
|
563
|
+
const filePath = this.getTokenPath(id);
|
|
564
|
+
try {
|
|
565
|
+
await fs.access(filePath, constants.F_OK);
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
571
|
+
const tokenFile = JSON.parse(fileContent);
|
|
572
|
+
return this.tokenFileToAppToken(tokenFile);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
this.logger.error(`Failed to get app token ${id}`, error);
|
|
576
|
+
throw new Error(`Failed to get app token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async saveAppToken(id, tokenData) {
|
|
580
|
+
try {
|
|
581
|
+
SecurityValidator.validateDocumentId(id);
|
|
582
|
+
await fsExtra.ensureDir(this.config.tokensDir, { mode: this.config.dirMode });
|
|
583
|
+
const filePath = this.getTokenPath(id);
|
|
584
|
+
let tokenFile;
|
|
585
|
+
let isUpdate = false;
|
|
586
|
+
// Check if token exists
|
|
587
|
+
try {
|
|
588
|
+
await fs.access(filePath, constants.F_OK);
|
|
589
|
+
const existingContent = await fs.readFile(filePath, 'utf8');
|
|
590
|
+
const existingToken = JSON.parse(existingContent);
|
|
591
|
+
// Token exists, this is an update
|
|
592
|
+
isUpdate = true;
|
|
593
|
+
const updateData = tokenData;
|
|
594
|
+
tokenFile = {
|
|
595
|
+
...existingToken,
|
|
596
|
+
...updateData,
|
|
597
|
+
id,
|
|
598
|
+
updatedAt: new Date().toISOString()
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
// Token doesn't exist, this is a new token
|
|
603
|
+
// Check if we received a complete AppToken object (from core engine)
|
|
604
|
+
if ('tokenHash' in tokenData && 'createdBy' in tokenData) {
|
|
605
|
+
// Complete AppToken object from core engine
|
|
606
|
+
const completeToken = tokenData;
|
|
607
|
+
tokenFile = {
|
|
608
|
+
...completeToken,
|
|
609
|
+
id // Ensure ID matches
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// CreateAppTokenData from API
|
|
614
|
+
const createData = tokenData;
|
|
615
|
+
tokenFile = {
|
|
616
|
+
id,
|
|
617
|
+
name: createData.name,
|
|
618
|
+
description: createData.description,
|
|
619
|
+
tokenHash: createData.tokenHash || '', // Use provided tokenHash from caller
|
|
620
|
+
permissions: createData.permissions,
|
|
621
|
+
createdBy: createData.createdBy || '', // Use provided createdBy from caller
|
|
622
|
+
isActive: true,
|
|
623
|
+
usageCount: 0,
|
|
624
|
+
expiresAt: createData.expiresAt,
|
|
625
|
+
createdAt: new Date().toISOString(),
|
|
626
|
+
updatedAt: new Date().toISOString()
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Write token file
|
|
631
|
+
const jsonContent = this.formatJson(tokenFile);
|
|
632
|
+
await this.atomicWriteFile(filePath, jsonContent);
|
|
633
|
+
if (!this.config.silent) {
|
|
634
|
+
this.logger.debug(`App token ${isUpdate ? 'updated' : 'created'}`, {
|
|
635
|
+
id,
|
|
636
|
+
name: tokenFile.name
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return this.tokenFileToAppToken(tokenFile);
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
this.logger.error(`Failed to save app token ${id}`, error);
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async listAppTokens(options = {}) {
|
|
647
|
+
try {
|
|
648
|
+
// Ensure tokens directory exists
|
|
649
|
+
try {
|
|
650
|
+
await fs.access(this.config.tokensDir, constants.F_OK);
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
const files = await fs.readdir(this.config.tokensDir);
|
|
656
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
657
|
+
const tokens = [];
|
|
658
|
+
for (const file of jsonFiles) {
|
|
659
|
+
try {
|
|
660
|
+
const filePath = path.join(this.config.tokensDir, file);
|
|
661
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
662
|
+
const tokenFile = JSON.parse(fileContent);
|
|
663
|
+
const token = this.tokenFileToAppToken(tokenFile);
|
|
664
|
+
tokens.push(token);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
this.logger.warn(`Skipping invalid token file ${file}`, error);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// Apply filters
|
|
672
|
+
let filteredTokens = tokens;
|
|
673
|
+
if (options.createdBy) {
|
|
674
|
+
filteredTokens = filteredTokens.filter(token => token.createdBy === options.createdBy);
|
|
675
|
+
}
|
|
676
|
+
if (options.isActive !== undefined) {
|
|
677
|
+
filteredTokens = filteredTokens.filter(token => token.isActive === options.isActive);
|
|
678
|
+
}
|
|
679
|
+
// Apply pagination
|
|
680
|
+
const { offset = 0, limit } = options;
|
|
681
|
+
const start = offset;
|
|
682
|
+
const end = limit ? start + limit : undefined;
|
|
683
|
+
return filteredTokens.slice(start, end);
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
this.logger.error('Failed to list app tokens', error);
|
|
687
|
+
throw new Error(`Failed to list app tokens: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async deleteAppToken(id) {
|
|
691
|
+
try {
|
|
692
|
+
SecurityValidator.validateDocumentId(id);
|
|
693
|
+
const filePath = this.getTokenPath(id);
|
|
694
|
+
try {
|
|
695
|
+
await fs.unlink(filePath);
|
|
696
|
+
if (!this.config.silent) {
|
|
697
|
+
this.logger.debug('App token deleted', { id });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
if (error.code === 'ENOENT') {
|
|
702
|
+
throw new Error(`App token ${id} not found`);
|
|
703
|
+
}
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
this.logger.error(`Failed to delete app token ${id}`, error);
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async getAppTokenByHash(hash) {
|
|
713
|
+
try {
|
|
714
|
+
const tokens = await this.listAppTokens();
|
|
715
|
+
return tokens.find(token => token.tokenHash === hash) || null;
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
this.logger.error(`Failed to get app token by hash`, error);
|
|
719
|
+
throw new Error(`Failed to get app token by hash: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// ==========================================================================
|
|
723
|
+
// WEBHOOK OPERATIONS
|
|
724
|
+
// ==========================================================================
|
|
725
|
+
async getWebhook(id) {
|
|
726
|
+
try {
|
|
727
|
+
const filePath = this.getWebhookPath(id);
|
|
728
|
+
// Check if file exists
|
|
729
|
+
try {
|
|
730
|
+
await fs.access(filePath, constants.F_OK);
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
736
|
+
const webhook = this.safeParseJSON(content, this.dateReviver);
|
|
737
|
+
this.logger.debug(`Webhook retrieved: ${id}`);
|
|
738
|
+
return webhook;
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
this.logger.error(`Failed to get webhook ${id}`, error);
|
|
742
|
+
throw new Error(`Failed to get webhook ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async saveWebhook(id, webhookData) {
|
|
746
|
+
try {
|
|
747
|
+
SecurityValidator.validateDocumentId(id);
|
|
748
|
+
const filePath = this.getWebhookPath(id);
|
|
749
|
+
const webhookDir = path.dirname(filePath);
|
|
750
|
+
// Ensure webhook directory exists
|
|
751
|
+
await fsExtra.ensureDir(webhookDir, { mode: this.config.dirMode });
|
|
752
|
+
// Check if webhook already exists
|
|
753
|
+
let existingWebhook = null;
|
|
754
|
+
try {
|
|
755
|
+
existingWebhook = await this.getWebhook(id);
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
// Webhook doesn't exist, this is a new webhook
|
|
759
|
+
}
|
|
760
|
+
const now = new Date();
|
|
761
|
+
const isUpdate = existingWebhook !== null;
|
|
762
|
+
const webhook = {
|
|
763
|
+
id,
|
|
764
|
+
name: webhookData.name || existingWebhook?.name || 'Untitled Webhook',
|
|
765
|
+
url: webhookData.url || existingWebhook?.url || '',
|
|
766
|
+
events: webhookData.events || existingWebhook?.events || [],
|
|
767
|
+
active: webhookData.active !== undefined ? webhookData.active : (existingWebhook?.active ?? true),
|
|
768
|
+
secret: webhookData.secret || existingWebhook?.secret || '',
|
|
769
|
+
headers: webhookData.headers || existingWebhook?.headers || {},
|
|
770
|
+
retryPolicy: webhookData.retryPolicy || existingWebhook?.retryPolicy || {
|
|
771
|
+
maxRetries: 3,
|
|
772
|
+
backoffType: 'exponential',
|
|
773
|
+
baseDelay: 1000,
|
|
774
|
+
maxDelay: 30000,
|
|
775
|
+
retryOnStatus: [500, 502, 503, 504, 408, 429]
|
|
776
|
+
},
|
|
777
|
+
createdBy: webhookData.createdBy || existingWebhook?.createdBy || 'system',
|
|
778
|
+
createdAt: isUpdate ? existingWebhook.createdAt : now,
|
|
779
|
+
updatedAt: now
|
|
780
|
+
};
|
|
781
|
+
// Write to file with Date serialization
|
|
782
|
+
const jsonContent = this.config.prettyJson
|
|
783
|
+
? JSON.stringify(webhook, this.dateReplacer, this.config.jsonSpaces)
|
|
784
|
+
: JSON.stringify(webhook, this.dateReplacer);
|
|
785
|
+
await fs.writeFile(filePath, jsonContent, { mode: this.config.fileMode });
|
|
786
|
+
if (this.config.syncWrites) {
|
|
787
|
+
const fd = await fs.open(filePath, 'r+');
|
|
788
|
+
await fd.sync();
|
|
789
|
+
await fd.close();
|
|
790
|
+
}
|
|
791
|
+
this.logger.info(`Webhook saved: ${id} (${isUpdate ? 'updated' : 'created'})`);
|
|
792
|
+
return webhook;
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
this.logger.error(`Failed to save webhook ${id}`, error);
|
|
796
|
+
throw new Error(`Failed to save webhook ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async listWebhooks(options = {}) {
|
|
800
|
+
try {
|
|
801
|
+
// Ensure webhooks directory exists
|
|
802
|
+
await fsExtra.ensureDir(this.config.webhooksDir, { mode: this.config.dirMode });
|
|
803
|
+
const files = await fs.readdir(this.config.webhooksDir);
|
|
804
|
+
const webhookFiles = files.filter(file => file.endsWith('.json'));
|
|
805
|
+
const webhooks = [];
|
|
806
|
+
for (const file of webhookFiles) {
|
|
807
|
+
try {
|
|
808
|
+
const id = path.basename(file, '.json');
|
|
809
|
+
const webhook = await this.getWebhook(id);
|
|
810
|
+
if (webhook) {
|
|
811
|
+
webhooks.push(webhook);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
// Skip invalid webhook files but log the issue
|
|
816
|
+
if (!this.config.silent) {
|
|
817
|
+
this.logger.warn(`Could not read webhook file ${file}`, error);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Apply filters
|
|
822
|
+
let filteredWebhooks = webhooks;
|
|
823
|
+
if (options.active !== undefined) {
|
|
824
|
+
filteredWebhooks = filteredWebhooks.filter(webhook => webhook.active === options.active);
|
|
825
|
+
}
|
|
826
|
+
if (options.events && options.events.length > 0) {
|
|
827
|
+
filteredWebhooks = filteredWebhooks.filter(webhook => options.events.some((eventPattern) => webhook.events.some((webhookEvent) => this.matchesEventPattern(webhookEvent, eventPattern))));
|
|
828
|
+
}
|
|
829
|
+
if (options.createdBy) {
|
|
830
|
+
filteredWebhooks = filteredWebhooks.filter(webhook => webhook.createdBy === options.createdBy);
|
|
831
|
+
}
|
|
832
|
+
// Sort by creation date (newest first)
|
|
833
|
+
filteredWebhooks.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
834
|
+
// Apply pagination
|
|
835
|
+
if (options.offset) {
|
|
836
|
+
filteredWebhooks = filteredWebhooks.slice(options.offset);
|
|
837
|
+
}
|
|
838
|
+
if (options.limit) {
|
|
839
|
+
filteredWebhooks = filteredWebhooks.slice(0, options.limit);
|
|
840
|
+
}
|
|
841
|
+
this.logger.debug(`Listed webhooks: ${filteredWebhooks.length} results`);
|
|
842
|
+
return filteredWebhooks;
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
this.logger.error('Failed to list webhooks', error);
|
|
846
|
+
throw new Error(`Failed to list webhooks: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async deleteWebhook(id) {
|
|
850
|
+
try {
|
|
851
|
+
const filePath = this.getWebhookPath(id);
|
|
852
|
+
// Check if webhook exists
|
|
853
|
+
try {
|
|
854
|
+
await fs.access(filePath, constants.F_OK);
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
// Webhook doesn't exist, nothing to delete
|
|
858
|
+
this.logger.debug(`Webhook ${id} does not exist, nothing to delete`);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
await fs.unlink(filePath);
|
|
862
|
+
this.logger.info(`Webhook deleted: ${id}`);
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
this.logger.error(`Failed to delete webhook ${id}`, error);
|
|
866
|
+
throw new Error(`Failed to delete webhook ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// ==========================================================================
|
|
870
|
+
// PRIVATE HELPER METHODS FOR WEBHOOKS
|
|
871
|
+
// ==========================================================================
|
|
872
|
+
getWebhookPath(id) {
|
|
873
|
+
SecurityValidator.validateDocumentId(id);
|
|
874
|
+
const webhookPath = path.join(this.config.webhooksDir, `${id}.json`);
|
|
875
|
+
this.validateSecurePath(webhookPath, this.config.webhooksDir);
|
|
876
|
+
return webhookPath;
|
|
877
|
+
}
|
|
878
|
+
matchesEventPattern(webhookEvent, pattern) {
|
|
879
|
+
if (pattern === '*')
|
|
880
|
+
return true;
|
|
881
|
+
if (pattern.endsWith('*')) {
|
|
882
|
+
return webhookEvent.startsWith(pattern.slice(0, -1));
|
|
883
|
+
}
|
|
884
|
+
return webhookEvent === pattern;
|
|
885
|
+
}
|
|
886
|
+
// ==========================================================================
|
|
887
|
+
// UTILITY OPERATIONS
|
|
888
|
+
// ==========================================================================
|
|
889
|
+
async healthCheck() {
|
|
890
|
+
try {
|
|
891
|
+
// Directories may still be being created; healthCheck is the documented way to wait.
|
|
892
|
+
await this.ready;
|
|
893
|
+
// Test directory access
|
|
894
|
+
await fs.access(this.config.contentDir, constants.R_OK | constants.W_OK);
|
|
895
|
+
await fs.access(this.config.usersDir, constants.R_OK | constants.W_OK);
|
|
896
|
+
await fs.access(this.config.tokensDir, constants.R_OK | constants.W_OK);
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
this.logger.error('Health check failed', error);
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async migrate(migrations) {
|
|
905
|
+
// Basic migration support - implement as needed
|
|
906
|
+
this.logger.info('Migration support not implemented for filesystem adapter', {
|
|
907
|
+
migrationsCount: migrations.length
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
// ==========================================================================
|
|
911
|
+
// PRIVATE HELPER METHODS
|
|
912
|
+
// ==========================================================================
|
|
913
|
+
getDocumentPath(collection, id) {
|
|
914
|
+
return path.join(this.config.contentDir, collection, `${id}.json`);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* The single stored-file -> Document projection.
|
|
918
|
+
*
|
|
919
|
+
* Every read path (get, save, list, count) goes through this so the shape
|
|
920
|
+
* cannot drift again: `listDocuments` used to drop `_createdBy`/`_updatedBy`
|
|
921
|
+
* and the `*ByType` fields, and `countDocuments` filtered against a partial
|
|
922
|
+
* document with only `id`/`_status`/`_createdAt`/`_updatedAt`, so a filter on
|
|
923
|
+
* `_createdBy` or `_revision` never matched anything.
|
|
924
|
+
*/
|
|
925
|
+
documentFileToDocument(documentFile, collection) {
|
|
926
|
+
return {
|
|
927
|
+
id: documentFile.id,
|
|
928
|
+
// Both identities, same value: consumers read one or the other.
|
|
929
|
+
_id: documentFile.id,
|
|
930
|
+
_collection: collection,
|
|
931
|
+
_createdAt: new Date(documentFile.metadata.createdAt),
|
|
932
|
+
_updatedAt: new Date(documentFile.metadata.updatedAt),
|
|
933
|
+
_revision: documentFile.metadata.revision,
|
|
934
|
+
_status: documentFile.metadata.status,
|
|
935
|
+
_createdBy: documentFile.metadata.createdBy,
|
|
936
|
+
_updatedBy: documentFile.metadata.updatedBy,
|
|
937
|
+
_createdByType: documentFile.metadata.createdByType,
|
|
938
|
+
_updatedByType: documentFile.metadata.updatedByType,
|
|
939
|
+
...documentFile.data
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Serialize writes to a single user file within this process so that
|
|
944
|
+
* compare-and-set operations (saveUserIf) cannot interleave with saveUser.
|
|
945
|
+
*/
|
|
946
|
+
async withUserLock(id, operation) {
|
|
947
|
+
const previous = this.userWriteLocks.get(id) ?? Promise.resolve();
|
|
948
|
+
const current = previous.then(operation, operation);
|
|
949
|
+
const settled = current.then(() => undefined, () => undefined);
|
|
950
|
+
this.userWriteLocks.set(id, settled);
|
|
951
|
+
try {
|
|
952
|
+
return await current;
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
if (this.userWriteLocks.get(id) === settled) {
|
|
956
|
+
this.userWriteLocks.delete(id);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
getUserPath(id) {
|
|
961
|
+
return path.join(this.config.usersDir, `${id}.json`);
|
|
962
|
+
}
|
|
963
|
+
getTokenPath(id) {
|
|
964
|
+
return path.join(this.config.tokensDir, `${id}.json`);
|
|
965
|
+
}
|
|
966
|
+
formatJson(data) {
|
|
967
|
+
if (this.config.prettyJson) {
|
|
968
|
+
return JSON.stringify(data, null, this.config.jsonSpaces);
|
|
969
|
+
}
|
|
970
|
+
return JSON.stringify(data);
|
|
971
|
+
}
|
|
972
|
+
async atomicWriteFile(filePath, content) {
|
|
973
|
+
const tempPath = `${filePath}.tmp`;
|
|
974
|
+
try {
|
|
975
|
+
await fs.writeFile(tempPath, content, {
|
|
976
|
+
mode: this.config.fileMode,
|
|
977
|
+
encoding: 'utf8'
|
|
978
|
+
});
|
|
979
|
+
if (this.config.syncWrites) {
|
|
980
|
+
const fd = await fs.open(tempPath, 'r+');
|
|
981
|
+
await fd.sync();
|
|
982
|
+
await fd.close();
|
|
983
|
+
}
|
|
984
|
+
await fs.rename(tempPath, filePath);
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
// Clean up temp file on error
|
|
988
|
+
try {
|
|
989
|
+
await fs.unlink(tempPath);
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
// Ignore cleanup errors
|
|
993
|
+
}
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* A field cleared by writing `null` round-trips through JSON as `null`, but
|
|
999
|
+
* the contract says an absent or cleared user field reads back `undefined` on
|
|
1000
|
+
* every adapter. Normalise on the way out.
|
|
1001
|
+
*/
|
|
1002
|
+
orUndefined(value) {
|
|
1003
|
+
return value ?? undefined;
|
|
1004
|
+
}
|
|
1005
|
+
userFileToUser(userFile) {
|
|
1006
|
+
return {
|
|
1007
|
+
id: userFile.id,
|
|
1008
|
+
username: userFile.username,
|
|
1009
|
+
email: userFile.email,
|
|
1010
|
+
passwordHash: userFile.passwordHash,
|
|
1011
|
+
firstName: userFile.firstName,
|
|
1012
|
+
lastName: userFile.lastName,
|
|
1013
|
+
role: userFile.role,
|
|
1014
|
+
// Created users always get `[]`; only an update that explicitly clears
|
|
1015
|
+
// permissions makes them absent, and absent reads back as `undefined`.
|
|
1016
|
+
permissions: this.orUndefined(userFile.permissions),
|
|
1017
|
+
isActive: userFile.isActive,
|
|
1018
|
+
profileImage: this.orUndefined(userFile.profileImage),
|
|
1019
|
+
preferences: this.orUndefined(userFile.preferences),
|
|
1020
|
+
oauthProviders: this.orUndefined(userFile.oauthProviders),
|
|
1021
|
+
// Written to disk by the spread in writeUserFile but previously dropped
|
|
1022
|
+
// here, so every read returned a user without them: TOTP enrolment always
|
|
1023
|
+
// failed with "No pending TOTP setup found" and passkeys never persisted.
|
|
1024
|
+
mfa: this.orUndefined(userFile.mfa),
|
|
1025
|
+
passkeys: this.orUndefined(userFile.passkeys),
|
|
1026
|
+
lastLoginAt: this.orUndefined(userFile.lastLoginAt),
|
|
1027
|
+
createdAt: userFile.createdAt,
|
|
1028
|
+
updatedAt: userFile.updatedAt
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
tokenFileToAppToken(tokenFile) {
|
|
1032
|
+
return {
|
|
1033
|
+
id: tokenFile.id,
|
|
1034
|
+
name: tokenFile.name,
|
|
1035
|
+
description: tokenFile.description,
|
|
1036
|
+
tokenHash: tokenFile.tokenHash,
|
|
1037
|
+
permissions: tokenFile.permissions,
|
|
1038
|
+
createdBy: tokenFile.createdBy,
|
|
1039
|
+
isActive: tokenFile.isActive,
|
|
1040
|
+
lastUsedAt: tokenFile.lastUsedAt,
|
|
1041
|
+
usageCount: tokenFile.usageCount,
|
|
1042
|
+
expiresAt: tokenFile.expiresAt,
|
|
1043
|
+
createdAt: tokenFile.createdAt,
|
|
1044
|
+
updatedAt: tokenFile.updatedAt
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
// JSON parsing and serialization helpers
|
|
1048
|
+
safeParseJSON(content, reviver) {
|
|
1049
|
+
// Check content size to prevent DoS attacks
|
|
1050
|
+
if (content.length > this.MAX_DOCUMENT_SIZE) {
|
|
1051
|
+
throw new InvalidInputError(`JSON content size ${content.length} exceeds maximum allowed size of ${this.MAX_DOCUMENT_SIZE} bytes`, 'size');
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
return JSON.parse(content, reviver);
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
throw new InvalidInputError(`Invalid JSON format: ${error}`, 'json');
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
dateReplacer = (key, value) => {
|
|
1061
|
+
// Convert Date objects to ISO strings
|
|
1062
|
+
if (value instanceof Date) {
|
|
1063
|
+
return value.toISOString();
|
|
1064
|
+
}
|
|
1065
|
+
return value;
|
|
1066
|
+
};
|
|
1067
|
+
dateReviver = (key, value) => {
|
|
1068
|
+
// Convert ISO date strings back to Date objects
|
|
1069
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
|
|
1070
|
+
return new Date(value);
|
|
1071
|
+
}
|
|
1072
|
+
return value;
|
|
1073
|
+
};
|
|
1074
|
+
validateSecurePath(targetPath, basePath) {
|
|
1075
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
1076
|
+
const resolvedBase = path.resolve(basePath);
|
|
1077
|
+
if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
|
|
1078
|
+
throw new InvalidInputError('Path traversal attempt detected', 'path');
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// ==========================================================================
|
|
1082
|
+
// SETTINGS OPERATIONS
|
|
1083
|
+
// ==========================================================================
|
|
1084
|
+
/**
|
|
1085
|
+
* Get studio settings
|
|
1086
|
+
*/
|
|
1087
|
+
async getSettings() {
|
|
1088
|
+
try {
|
|
1089
|
+
const settingsPath = path.join(this.config.settingsDir, 'studio.json');
|
|
1090
|
+
// Check if settings file exists
|
|
1091
|
+
try {
|
|
1092
|
+
await fs.access(settingsPath, constants.F_OK);
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
// Settings file doesn't exist
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const fileData = await fs.readFile(settingsPath, 'utf-8');
|
|
1099
|
+
const parsed = JSON.parse(fileData, this.dateReviver);
|
|
1100
|
+
this.logger.debug('Settings loaded', { path: settingsPath });
|
|
1101
|
+
return parsed;
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
this.logger.error('Failed to load settings', { error });
|
|
1105
|
+
throw new InvalidInputError(`Failed to load settings: ${error}`, 'settings');
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Save studio settings
|
|
1110
|
+
*/
|
|
1111
|
+
async saveSettings(settings) {
|
|
1112
|
+
try {
|
|
1113
|
+
// Validate settings object
|
|
1114
|
+
if (!settings || typeof settings !== 'object') {
|
|
1115
|
+
throw new InvalidInputError('Settings must be a valid object', 'settings');
|
|
1116
|
+
}
|
|
1117
|
+
if (!settings.id || !settings.publicUrl || !settings.studioTitle || !settings.defaultTheme) {
|
|
1118
|
+
throw new InvalidInputError('Settings missing required fields', 'settings');
|
|
1119
|
+
}
|
|
1120
|
+
const settingsPath = path.join(this.config.settingsDir, 'studio.json');
|
|
1121
|
+
// Ensure settings directory exists
|
|
1122
|
+
await fsExtra.ensureDir(this.config.settingsDir, { mode: this.config.dirMode });
|
|
1123
|
+
// Validate secure path
|
|
1124
|
+
this.validateSecurePath(settingsPath, this.config.settingsDir);
|
|
1125
|
+
const jsonData = this.formatJson(settings);
|
|
1126
|
+
if (this.config.syncWrites) {
|
|
1127
|
+
await fs.writeFile(settingsPath, jsonData, {
|
|
1128
|
+
encoding: 'utf-8',
|
|
1129
|
+
mode: this.config.fileMode,
|
|
1130
|
+
flag: 'w'
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
await fs.writeFile(settingsPath, jsonData, {
|
|
1135
|
+
encoding: 'utf-8',
|
|
1136
|
+
mode: this.config.fileMode
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
this.logger.info('Settings saved', {
|
|
1140
|
+
path: settingsPath,
|
|
1141
|
+
settingsId: settings.id
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
this.logger.error('Failed to save settings', { error, settingsId: settings.id });
|
|
1146
|
+
throw new InvalidInputError(`Failed to save settings: ${error}`, 'settings');
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
// ==========================================================================
|
|
1150
|
+
// AUDIT LOG OPERATIONS
|
|
1151
|
+
// ==========================================================================
|
|
1152
|
+
/**
|
|
1153
|
+
* Create an audit log entry
|
|
1154
|
+
*/
|
|
1155
|
+
async createAuditLog(auditLog) {
|
|
1156
|
+
try {
|
|
1157
|
+
const auditLogId = `audit-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
1158
|
+
const auditLogPath = path.join(this.config.auditLogsDir, `${auditLogId}.json`);
|
|
1159
|
+
// Ensure audit logs directory exists
|
|
1160
|
+
await fsExtra.ensureDir(this.config.auditLogsDir, { mode: this.config.dirMode });
|
|
1161
|
+
// Validate secure path
|
|
1162
|
+
this.validateSecurePath(auditLogPath, this.config.auditLogsDir);
|
|
1163
|
+
const auditLogFile = {
|
|
1164
|
+
id: auditLogId,
|
|
1165
|
+
documentId: auditLog.documentId,
|
|
1166
|
+
collection: auditLog.collection,
|
|
1167
|
+
operation: auditLog.operation,
|
|
1168
|
+
actorId: auditLog.actorId,
|
|
1169
|
+
actorType: auditLog.actorType,
|
|
1170
|
+
actorUsername: auditLog.actorUsername,
|
|
1171
|
+
changes: auditLog.changes,
|
|
1172
|
+
timestamp: auditLog.timestamp.toISOString(),
|
|
1173
|
+
revision: auditLog.revision,
|
|
1174
|
+
ipAddress: auditLog.ipAddress,
|
|
1175
|
+
userAgent: auditLog.userAgent,
|
|
1176
|
+
sessionId: auditLog.sessionId,
|
|
1177
|
+
metadata: auditLog.metadata
|
|
1178
|
+
};
|
|
1179
|
+
const jsonData = this.formatJson(auditLogFile);
|
|
1180
|
+
if (this.config.syncWrites) {
|
|
1181
|
+
await fs.writeFile(auditLogPath, jsonData, {
|
|
1182
|
+
encoding: 'utf-8',
|
|
1183
|
+
mode: this.config.fileMode,
|
|
1184
|
+
flag: 'w'
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
await fs.writeFile(auditLogPath, jsonData, {
|
|
1189
|
+
encoding: 'utf-8',
|
|
1190
|
+
mode: this.config.fileMode
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
this.logger.debug('Audit log created', {
|
|
1194
|
+
auditLogId,
|
|
1195
|
+
documentId: auditLog.documentId,
|
|
1196
|
+
operation: auditLog.operation,
|
|
1197
|
+
actorId: auditLog.actorId
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
...auditLog,
|
|
1201
|
+
id: auditLogId
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
catch (error) {
|
|
1205
|
+
this.logger.error('Failed to create audit log', { error, auditLog });
|
|
1206
|
+
throw new InvalidInputError(`Failed to create audit log: ${error}`, 'auditLog');
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Get audit logs for a specific document
|
|
1211
|
+
*/
|
|
1212
|
+
async getDocumentAuditLogs(documentId, options = {}) {
|
|
1213
|
+
try {
|
|
1214
|
+
const auditLogs = [];
|
|
1215
|
+
const auditLogFiles = await fs.readdir(this.config.auditLogsDir);
|
|
1216
|
+
// Filter files by document ID (read each file to check)
|
|
1217
|
+
for (const filename of auditLogFiles) {
|
|
1218
|
+
if (!filename.endsWith('.json'))
|
|
1219
|
+
continue;
|
|
1220
|
+
const auditLogPath = path.join(this.config.auditLogsDir, filename);
|
|
1221
|
+
this.validateSecurePath(auditLogPath, this.config.auditLogsDir);
|
|
1222
|
+
try {
|
|
1223
|
+
const fileContent = await fs.readFile(auditLogPath, 'utf-8');
|
|
1224
|
+
const auditLogFile = JSON.parse(fileContent);
|
|
1225
|
+
if (auditLogFile.documentId === documentId) {
|
|
1226
|
+
auditLogs.push({
|
|
1227
|
+
id: auditLogFile.id,
|
|
1228
|
+
documentId: auditLogFile.documentId,
|
|
1229
|
+
collection: auditLogFile.collection,
|
|
1230
|
+
operation: auditLogFile.operation,
|
|
1231
|
+
actorId: auditLogFile.actorId,
|
|
1232
|
+
actorType: auditLogFile.actorType,
|
|
1233
|
+
actorUsername: auditLogFile.actorUsername,
|
|
1234
|
+
changes: auditLogFile.changes,
|
|
1235
|
+
timestamp: new Date(auditLogFile.timestamp),
|
|
1236
|
+
revision: auditLogFile.revision,
|
|
1237
|
+
ipAddress: auditLogFile.ipAddress,
|
|
1238
|
+
userAgent: auditLogFile.userAgent,
|
|
1239
|
+
sessionId: auditLogFile.sessionId,
|
|
1240
|
+
metadata: auditLogFile.metadata
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
catch (parseError) {
|
|
1245
|
+
this.logger.warn('Failed to parse audit log file', { filename, parseError });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
// Sort by timestamp (newest first)
|
|
1249
|
+
auditLogs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
1250
|
+
// Apply pagination
|
|
1251
|
+
const offset = options.offset || 0;
|
|
1252
|
+
const limit = options.limit || 100;
|
|
1253
|
+
return auditLogs.slice(offset, offset + limit);
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
this.logger.error('Failed to get document audit logs', { error, documentId });
|
|
1257
|
+
throw new InvalidInputError(`Failed to get document audit logs: ${error}`, 'documentId');
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Get audit logs for a collection
|
|
1262
|
+
*/
|
|
1263
|
+
async getCollectionAuditLogs(collection, options = {}) {
|
|
1264
|
+
try {
|
|
1265
|
+
const auditLogs = [];
|
|
1266
|
+
const auditLogFiles = await fs.readdir(this.config.auditLogsDir);
|
|
1267
|
+
// Filter files by collection (read each file to check)
|
|
1268
|
+
for (const filename of auditLogFiles) {
|
|
1269
|
+
if (!filename.endsWith('.json'))
|
|
1270
|
+
continue;
|
|
1271
|
+
const auditLogPath = path.join(this.config.auditLogsDir, filename);
|
|
1272
|
+
this.validateSecurePath(auditLogPath, this.config.auditLogsDir);
|
|
1273
|
+
try {
|
|
1274
|
+
const fileContent = await fs.readFile(auditLogPath, 'utf-8');
|
|
1275
|
+
const auditLogFile = JSON.parse(fileContent);
|
|
1276
|
+
if (auditLogFile.collection === collection) {
|
|
1277
|
+
auditLogs.push({
|
|
1278
|
+
id: auditLogFile.id,
|
|
1279
|
+
documentId: auditLogFile.documentId,
|
|
1280
|
+
collection: auditLogFile.collection,
|
|
1281
|
+
operation: auditLogFile.operation,
|
|
1282
|
+
actorId: auditLogFile.actorId,
|
|
1283
|
+
actorType: auditLogFile.actorType,
|
|
1284
|
+
actorUsername: auditLogFile.actorUsername,
|
|
1285
|
+
changes: auditLogFile.changes,
|
|
1286
|
+
timestamp: new Date(auditLogFile.timestamp),
|
|
1287
|
+
revision: auditLogFile.revision,
|
|
1288
|
+
ipAddress: auditLogFile.ipAddress,
|
|
1289
|
+
userAgent: auditLogFile.userAgent,
|
|
1290
|
+
sessionId: auditLogFile.sessionId,
|
|
1291
|
+
metadata: auditLogFile.metadata
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
catch (parseError) {
|
|
1296
|
+
this.logger.warn('Failed to parse audit log file', { filename, parseError });
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// Sort by timestamp (newest first)
|
|
1300
|
+
auditLogs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
1301
|
+
// Apply pagination
|
|
1302
|
+
const offset = options.offset || 0;
|
|
1303
|
+
const limit = options.limit || 100;
|
|
1304
|
+
return auditLogs.slice(offset, offset + limit);
|
|
1305
|
+
}
|
|
1306
|
+
catch (error) {
|
|
1307
|
+
this.logger.error('Failed to get collection audit logs', { error, collection });
|
|
1308
|
+
throw new InvalidInputError(`Failed to get collection audit logs: ${error}`, 'collection');
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Get audit logs by actor
|
|
1313
|
+
*/
|
|
1314
|
+
async getActorAuditLogs(actorId, options = {}) {
|
|
1315
|
+
try {
|
|
1316
|
+
const auditLogs = [];
|
|
1317
|
+
const auditLogFiles = await fs.readdir(this.config.auditLogsDir);
|
|
1318
|
+
// Filter files by actor ID (read each file to check)
|
|
1319
|
+
for (const filename of auditLogFiles) {
|
|
1320
|
+
if (!filename.endsWith('.json'))
|
|
1321
|
+
continue;
|
|
1322
|
+
const auditLogPath = path.join(this.config.auditLogsDir, filename);
|
|
1323
|
+
this.validateSecurePath(auditLogPath, this.config.auditLogsDir);
|
|
1324
|
+
try {
|
|
1325
|
+
const fileContent = await fs.readFile(auditLogPath, 'utf-8');
|
|
1326
|
+
const auditLogFile = JSON.parse(fileContent);
|
|
1327
|
+
if (auditLogFile.actorId === actorId) {
|
|
1328
|
+
auditLogs.push({
|
|
1329
|
+
id: auditLogFile.id,
|
|
1330
|
+
documentId: auditLogFile.documentId,
|
|
1331
|
+
collection: auditLogFile.collection,
|
|
1332
|
+
operation: auditLogFile.operation,
|
|
1333
|
+
actorId: auditLogFile.actorId,
|
|
1334
|
+
actorType: auditLogFile.actorType,
|
|
1335
|
+
actorUsername: auditLogFile.actorUsername,
|
|
1336
|
+
changes: auditLogFile.changes,
|
|
1337
|
+
timestamp: new Date(auditLogFile.timestamp),
|
|
1338
|
+
revision: auditLogFile.revision,
|
|
1339
|
+
ipAddress: auditLogFile.ipAddress,
|
|
1340
|
+
userAgent: auditLogFile.userAgent,
|
|
1341
|
+
sessionId: auditLogFile.sessionId,
|
|
1342
|
+
metadata: auditLogFile.metadata
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
catch (parseError) {
|
|
1347
|
+
this.logger.warn('Failed to parse audit log file', { filename, parseError });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
// Sort by timestamp (newest first)
|
|
1351
|
+
auditLogs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
1352
|
+
// Apply pagination
|
|
1353
|
+
const offset = options.offset || 0;
|
|
1354
|
+
const limit = options.limit || 100;
|
|
1355
|
+
return auditLogs.slice(offset, offset + limit);
|
|
1356
|
+
}
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
this.logger.error('Failed to get actor audit logs', { error, actorId });
|
|
1359
|
+
throw new InvalidInputError(`Failed to get actor audit logs: ${error}`, 'actorId');
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
//# sourceMappingURL=filesystem-data-adapter.js.map
|