@portaidentity/server 1.7.3 → 1.9.0
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/CHANGELOG.md +28 -0
- package/dist/applications/index.d.ts +2 -2
- package/dist/applications/index.d.ts.map +1 -1
- package/dist/applications/index.js +2 -2
- package/dist/applications/index.js.map +1 -1
- package/dist/applications/repository.d.ts +49 -4
- package/dist/applications/repository.d.ts.map +1 -1
- package/dist/applications/repository.js +148 -10
- package/dist/applications/repository.js.map +1 -1
- package/dist/applications/service.d.ts +35 -17
- package/dist/applications/service.d.ts.map +1 -1
- package/dist/applications/service.js +144 -35
- package/dist/applications/service.js.map +1 -1
- package/dist/applications/types.d.ts +1 -1
- package/dist/applications/types.d.ts.map +1 -1
- package/dist/applications/validators.d.ts +14 -0
- package/dist/applications/validators.d.ts.map +1 -0
- package/dist/applications/validators.js +14 -0
- package/dist/applications/validators.js.map +1 -0
- package/dist/auth/effective-branding.d.ts +35 -0
- package/dist/auth/effective-branding.d.ts.map +1 -0
- package/dist/auth/effective-branding.js +81 -0
- package/dist/auth/effective-branding.js.map +1 -0
- package/dist/auth/email-service.d.ts +3 -8
- package/dist/auth/email-service.d.ts.map +1 -1
- package/dist/auth/email-service.js +22 -30
- package/dist/auth/email-service.js.map +1 -1
- package/dist/auth/i18n.d.ts +5 -0
- package/dist/auth/i18n.d.ts.map +1 -1
- package/dist/auth/i18n.js +6 -5
- package/dist/auth/i18n.js.map +1 -1
- package/dist/auth/rate-limiter.d.ts +6 -10
- package/dist/auth/rate-limiter.d.ts.map +1 -1
- package/dist/auth/rate-limiter.js +12 -16
- package/dist/auth/rate-limiter.js.map +1 -1
- package/dist/auth/recovery-job-processor.d.ts.map +1 -1
- package/dist/auth/recovery-job-processor.js +1 -1
- package/dist/auth/recovery-job-processor.js.map +1 -1
- package/dist/auth/template-engine.d.ts +3 -1
- package/dist/auth/template-engine.d.ts.map +1 -1
- package/dist/auth/template-engine.js +1 -1
- package/dist/auth/template-engine.js.map +1 -1
- package/dist/auth/token-repository.d.ts +9 -3
- package/dist/auth/token-repository.d.ts.map +1 -1
- package/dist/auth/token-repository.js +17 -8
- package/dist/auth/token-repository.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +12 -12
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/error-handler.d.ts +3 -2
- package/dist/cli/error-handler.d.ts.map +1 -1
- package/dist/cli/error-handler.js +9 -2
- package/dist/cli/error-handler.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/clients/index.d.ts +1 -1
- package/dist/clients/index.js +1 -1
- package/dist/clients/repository.d.ts +24 -0
- package/dist/clients/repository.d.ts.map +1 -1
- package/dist/clients/repository.js +39 -0
- package/dist/clients/repository.js.map +1 -1
- package/dist/clients/secret-repository.d.ts +35 -8
- package/dist/clients/secret-repository.d.ts.map +1 -1
- package/dist/clients/secret-repository.js +69 -16
- package/dist/clients/secret-repository.js.map +1 -1
- package/dist/clients/secret-service.d.ts +5 -9
- package/dist/clients/secret-service.d.ts.map +1 -1
- package/dist/clients/secret-service.js +13 -22
- package/dist/clients/secret-service.js.map +1 -1
- package/dist/clients/service.d.ts +10 -13
- package/dist/clients/service.d.ts.map +1 -1
- package/dist/clients/service.js +121 -42
- package/dist/clients/service.js.map +1 -1
- package/dist/clients/types.d.ts +14 -1
- package/dist/clients/types.d.ts.map +1 -1
- package/dist/clients/types.js +1 -0
- package/dist/clients/types.js.map +1 -1
- package/dist/clients/validators.d.ts +83 -1
- package/dist/clients/validators.d.ts.map +1 -1
- package/dist/clients/validators.js +110 -0
- package/dist/clients/validators.js.map +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +23 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/custom-claims/repository.d.ts +14 -9
- package/dist/custom-claims/repository.d.ts.map +1 -1
- package/dist/custom-claims/repository.js +38 -11
- package/dist/custom-claims/repository.js.map +1 -1
- package/dist/custom-claims/service.d.ts +11 -6
- package/dist/custom-claims/service.d.ts.map +1 -1
- package/dist/custom-claims/service.js +43 -20
- package/dist/custom-claims/service.js.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/admin-permissions.d.ts +12 -7
- package/dist/lib/admin-permissions.d.ts.map +1 -1
- package/dist/lib/admin-permissions.js +26 -16
- package/dist/lib/admin-permissions.js.map +1 -1
- package/dist/lib/audit-log.d.ts +2 -2
- package/dist/lib/audit-log.d.ts.map +1 -1
- package/dist/lib/audit-log.js +1 -1
- package/dist/lib/audit-log.js.map +1 -1
- package/dist/lib/authority-revocation.d.ts +18 -0
- package/dist/lib/authority-revocation.d.ts.map +1 -0
- package/dist/lib/authority-revocation.js +37 -0
- package/dist/lib/authority-revocation.js.map +1 -0
- package/dist/lib/branding-assets.d.ts +35 -5
- package/dist/lib/branding-assets.d.ts.map +1 -1
- package/dist/lib/branding-assets.js +33 -26
- package/dist/lib/branding-assets.js.map +1 -1
- package/dist/lib/bulk-operations.d.ts +1 -1
- package/dist/lib/bulk-operations.d.ts.map +1 -1
- package/dist/lib/bulk-operations.js +1 -13
- package/dist/lib/bulk-operations.js.map +1 -1
- package/dist/lib/cursor.d.ts +2 -0
- package/dist/lib/cursor.d.ts.map +1 -1
- package/dist/lib/cursor.js +2 -7
- package/dist/lib/cursor.js.map +1 -1
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +5 -0
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/deletion-cleanup.d.ts +43 -0
- package/dist/lib/deletion-cleanup.d.ts.map +1 -0
- package/dist/lib/deletion-cleanup.js +207 -0
- package/dist/lib/deletion-cleanup.js.map +1 -0
- package/dist/lib/image-validator.d.ts +3 -1
- package/dist/lib/image-validator.d.ts.map +1 -1
- package/dist/lib/image-validator.js +32 -19
- package/dist/lib/image-validator.js.map +1 -1
- package/dist/lib/log-format.d.ts +20 -0
- package/dist/lib/log-format.d.ts.map +1 -0
- package/dist/lib/log-format.js +22 -0
- package/dist/lib/log-format.js.map +1 -0
- package/dist/lib/logger.d.ts +3 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +21 -4
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/redis.d.ts.map +1 -1
- package/dist/lib/redis.js +3 -0
- package/dist/lib/redis.js.map +1 -1
- package/dist/lib/session-tracking.d.ts +73 -11
- package/dist/lib/session-tracking.d.ts.map +1 -1
- package/dist/lib/session-tracking.js +76 -32
- package/dist/lib/session-tracking.js.map +1 -1
- package/dist/lib/signing-key-crypto.d.ts.map +1 -1
- package/dist/lib/signing-key-crypto.js +4 -5
- package/dist/lib/signing-key-crypto.js.map +1 -1
- package/dist/lib/signing-keys.d.ts +3 -4
- package/dist/lib/signing-keys.d.ts.map +1 -1
- package/dist/lib/signing-keys.js +66 -41
- package/dist/lib/signing-keys.js.map +1 -1
- package/dist/lib/super-admin-protection.d.ts +5 -5
- package/dist/lib/super-admin-protection.d.ts.map +1 -1
- package/dist/lib/super-admin-protection.js +6 -9
- package/dist/lib/super-admin-protection.js.map +1 -1
- package/dist/lib/system-config-catalog.d.ts +268 -0
- package/dist/lib/system-config-catalog.d.ts.map +1 -0
- package/dist/lib/system-config-catalog.js +90 -0
- package/dist/lib/system-config-catalog.js.map +1 -0
- package/dist/lib/system-config.d.ts +30 -33
- package/dist/lib/system-config.d.ts.map +1 -1
- package/dist/lib/system-config.js +78 -93
- package/dist/lib/system-config.js.map +1 -1
- package/dist/lib/with-timeout.d.ts +32 -0
- package/dist/lib/with-timeout.d.ts.map +1 -0
- package/dist/lib/with-timeout.js +45 -0
- package/dist/lib/with-timeout.js.map +1 -0
- package/dist/middleware/admin-auth.d.ts +6 -5
- package/dist/middleware/admin-auth.d.ts.map +1 -1
- package/dist/middleware/admin-auth.js +30 -20
- package/dist/middleware/admin-auth.js.map +1 -1
- package/dist/middleware/admin-cors.d.ts +3 -3
- package/dist/middleware/admin-cors.js +3 -3
- package/dist/middleware/admin-mutation-audit.d.ts +3 -2
- package/dist/middleware/admin-mutation-audit.d.ts.map +1 -1
- package/dist/middleware/admin-mutation-audit.js +12 -5
- package/dist/middleware/admin-mutation-audit.js.map +1 -1
- package/dist/middleware/admin-rate-limiter.d.ts +7 -8
- package/dist/middleware/admin-rate-limiter.d.ts.map +1 -1
- package/dist/middleware/admin-rate-limiter.js +7 -8
- package/dist/middleware/admin-rate-limiter.js.map +1 -1
- package/dist/middleware/client-secret-hash.d.ts +10 -29
- package/dist/middleware/client-secret-hash.d.ts.map +1 -1
- package/dist/middleware/client-secret-hash.js +161 -59
- package/dist/middleware/client-secret-hash.js.map +1 -1
- package/dist/middleware/health.d.ts +17 -0
- package/dist/middleware/health.d.ts.map +1 -1
- package/dist/middleware/health.js +25 -21
- package/dist/middleware/health.js.map +1 -1
- package/dist/middleware/ready.d.ts.map +1 -1
- package/dist/middleware/ready.js +1 -12
- package/dist/middleware/ready.js.map +1 -1
- package/dist/middleware/require-existing-organization.d.ts +18 -0
- package/dist/middleware/require-existing-organization.d.ts.map +1 -0
- package/dist/middleware/require-existing-organization.js +38 -0
- package/dist/middleware/require-existing-organization.js.map +1 -0
- package/dist/middleware/require-permission.d.ts.map +1 -1
- package/dist/middleware/require-permission.js +35 -1
- package/dist/middleware/require-permission.js.map +1 -1
- package/dist/middleware/require-user-organization.d.ts.map +1 -1
- package/dist/middleware/require-user-organization.js +4 -2
- package/dist/middleware/require-user-organization.js.map +1 -1
- package/dist/middleware/security-headers.d.ts +18 -0
- package/dist/middleware/security-headers.d.ts.map +1 -1
- package/dist/middleware/security-headers.js +32 -1
- package/dist/middleware/security-headers.js.map +1 -1
- package/dist/middleware/tenant-resolver.d.ts +1 -2
- package/dist/middleware/tenant-resolver.d.ts.map +1 -1
- package/dist/middleware/tenant-resolver.js +1 -5
- package/dist/middleware/tenant-resolver.js.map +1 -1
- package/dist/middleware/token-rate-limiter.d.ts +37 -16
- package/dist/middleware/token-rate-limiter.d.ts.map +1 -1
- package/dist/middleware/token-rate-limiter.js +126 -54
- package/dist/middleware/token-rate-limiter.js.map +1 -1
- package/dist/oidc/account-finder.d.ts +5 -5
- package/dist/oidc/account-finder.d.ts.map +1 -1
- package/dist/oidc/account-finder.js +31 -23
- package/dist/oidc/account-finder.js.map +1 -1
- package/dist/oidc/adapter-factory.d.ts.map +1 -1
- package/dist/oidc/adapter-factory.js +83 -3
- package/dist/oidc/adapter-factory.js.map +1 -1
- package/dist/oidc/configuration.d.ts +3 -8
- package/dist/oidc/configuration.d.ts.map +1 -1
- package/dist/oidc/configuration.js +67 -50
- package/dist/oidc/configuration.js.map +1 -1
- package/dist/oidc/provider.d.ts.map +1 -1
- package/dist/oidc/provider.js +7 -0
- package/dist/oidc/provider.js.map +1 -1
- package/dist/oidc/redis-adapter.d.ts +2 -3
- package/dist/oidc/redis-adapter.d.ts.map +1 -1
- package/dist/oidc/redis-adapter.js +54 -46
- package/dist/oidc/redis-adapter.js.map +1 -1
- package/dist/organizations/branding-url.d.ts +17 -0
- package/dist/organizations/branding-url.d.ts.map +1 -0
- package/dist/organizations/branding-url.js +38 -0
- package/dist/organizations/branding-url.js.map +1 -0
- package/dist/organizations/index.d.ts +2 -2
- package/dist/organizations/index.d.ts.map +1 -1
- package/dist/organizations/index.js +1 -1
- package/dist/organizations/index.js.map +1 -1
- package/dist/organizations/repository.d.ts +22 -32
- package/dist/organizations/repository.d.ts.map +1 -1
- package/dist/organizations/repository.js +64 -45
- package/dist/organizations/repository.js.map +1 -1
- package/dist/organizations/service.d.ts +14 -46
- package/dist/organizations/service.d.ts.map +1 -1
- package/dist/organizations/service.js +60 -112
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -15
- package/dist/organizations/types.d.ts.map +1 -1
- package/dist/organizations/validators.d.ts +41 -0
- package/dist/organizations/validators.d.ts.map +1 -0
- package/dist/organizations/validators.js +48 -0
- package/dist/organizations/validators.js.map +1 -0
- package/dist/portability/apply.d.ts +15 -0
- package/dist/portability/apply.d.ts.map +1 -0
- package/dist/portability/apply.js +274 -0
- package/dist/portability/apply.js.map +1 -0
- package/dist/portability/authorization.d.ts +30 -0
- package/dist/portability/authorization.d.ts.map +1 -0
- package/dist/portability/authorization.js +130 -0
- package/dist/portability/authorization.js.map +1 -0
- package/dist/portability/cleanup.d.ts +29 -0
- package/dist/portability/cleanup.d.ts.map +1 -0
- package/dist/portability/cleanup.js +167 -0
- package/dist/portability/cleanup.js.map +1 -0
- package/dist/portability/export.d.ts +18 -0
- package/dist/portability/export.d.ts.map +1 -0
- package/dist/portability/export.js +125 -0
- package/dist/portability/export.js.map +1 -0
- package/dist/portability/import-repository.d.ts +204 -0
- package/dist/portability/import-repository.d.ts.map +1 -0
- package/dist/portability/import-repository.js +348 -0
- package/dist/portability/import-repository.js.map +1 -0
- package/dist/portability/import-user-client-writers.d.ts +40 -0
- package/dist/portability/import-user-client-writers.d.ts.map +1 -0
- package/dist/portability/import-user-client-writers.js +171 -0
- package/dist/portability/import-user-client-writers.js.map +1 -0
- package/dist/portability/index.d.ts +8 -0
- package/dist/portability/index.d.ts.map +1 -0
- package/dist/portability/index.js +8 -0
- package/dist/portability/index.js.map +1 -0
- package/dist/portability/plan-support.d.ts +137 -0
- package/dist/portability/plan-support.d.ts.map +1 -0
- package/dist/portability/plan-support.js +337 -0
- package/dist/portability/plan-support.js.map +1 -0
- package/dist/portability/plan.d.ts +34 -0
- package/dist/portability/plan.d.ts.map +1 -0
- package/dist/portability/plan.js +450 -0
- package/dist/portability/plan.js.map +1 -0
- package/dist/portability/repository.d.ts +95 -0
- package/dist/portability/repository.d.ts.map +1 -0
- package/dist/portability/repository.js +390 -0
- package/dist/portability/repository.js.map +1 -0
- package/dist/portability/schema.d.ts +591 -0
- package/dist/portability/schema.d.ts.map +1 -0
- package/dist/portability/schema.js +459 -0
- package/dist/portability/schema.js.map +1 -0
- package/dist/portability/types.d.ts +382 -0
- package/dist/portability/types.d.ts.map +1 -0
- package/dist/portability/types.js +19 -0
- package/dist/portability/types.js.map +1 -0
- package/dist/rbac/errors.d.ts +4 -0
- package/dist/rbac/errors.d.ts.map +1 -1
- package/dist/rbac/errors.js +7 -0
- package/dist/rbac/errors.js.map +1 -1
- package/dist/rbac/index.d.ts +3 -3
- package/dist/rbac/index.d.ts.map +1 -1
- package/dist/rbac/index.js +3 -3
- package/dist/rbac/index.js.map +1 -1
- package/dist/rbac/mapping-repository.d.ts +84 -9
- package/dist/rbac/mapping-repository.d.ts.map +1 -1
- package/dist/rbac/mapping-repository.js +222 -55
- package/dist/rbac/mapping-repository.js.map +1 -1
- package/dist/rbac/permission-repository.d.ts +36 -19
- package/dist/rbac/permission-repository.d.ts.map +1 -1
- package/dist/rbac/permission-repository.js +93 -35
- package/dist/rbac/permission-repository.js.map +1 -1
- package/dist/rbac/permission-service.d.ts +18 -19
- package/dist/rbac/permission-service.d.ts.map +1 -1
- package/dist/rbac/permission-service.js +100 -57
- package/dist/rbac/permission-service.js.map +1 -1
- package/dist/rbac/role-repository.d.ts +29 -24
- package/dist/rbac/role-repository.d.ts.map +1 -1
- package/dist/rbac/role-repository.js +66 -43
- package/dist/rbac/role-repository.js.map +1 -1
- package/dist/rbac/role-service.d.ts +35 -32
- package/dist/rbac/role-service.d.ts.map +1 -1
- package/dist/rbac/role-service.js +210 -96
- package/dist/rbac/role-service.js.map +1 -1
- package/dist/rbac/slugs.d.ts +16 -27
- package/dist/rbac/slugs.d.ts.map +1 -1
- package/dist/rbac/slugs.js +36 -59
- package/dist/rbac/slugs.js.map +1 -1
- package/dist/rbac/types.d.ts +2 -4
- package/dist/rbac/types.d.ts.map +1 -1
- package/dist/rbac/types.js.map +1 -1
- package/dist/rbac/user-role-service.d.ts +23 -24
- package/dist/rbac/user-role-service.d.ts.map +1 -1
- package/dist/rbac/user-role-service.js +117 -61
- package/dist/rbac/user-role-service.js.map +1 -1
- package/dist/routes/applications.d.ts +4 -2
- package/dist/routes/applications.d.ts.map +1 -1
- package/dist/routes/applications.js +57 -19
- package/dist/routes/applications.js.map +1 -1
- package/dist/routes/audit.js +1 -1
- package/dist/routes/audit.js.map +1 -1
- package/dist/routes/branding.d.ts +3 -3
- package/dist/routes/branding.d.ts.map +1 -1
- package/dist/routes/branding.js +44 -16
- package/dist/routes/branding.js.map +1 -1
- package/dist/routes/bulk.d.ts +0 -5
- package/dist/routes/bulk.d.ts.map +1 -1
- package/dist/routes/bulk.js +3 -5
- package/dist/routes/bulk.js.map +1 -1
- package/dist/routes/clients.d.ts +4 -4
- package/dist/routes/clients.d.ts.map +1 -1
- package/dist/routes/clients.js +113 -52
- package/dist/routes/clients.js.map +1 -1
- package/dist/routes/config.d.ts +17 -12
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +165 -64
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-claims.d.ts.map +1 -1
- package/dist/routes/custom-claims.js +8 -2
- package/dist/routes/custom-claims.js.map +1 -1
- package/dist/routes/exports.d.ts +2 -1
- package/dist/routes/exports.d.ts.map +1 -1
- package/dist/routes/exports.js +57 -1
- package/dist/routes/exports.js.map +1 -1
- package/dist/routes/imports.d.ts +2 -2
- package/dist/routes/imports.d.ts.map +1 -1
- package/dist/routes/imports.js +95 -40
- package/dist/routes/imports.js.map +1 -1
- package/dist/routes/interactions.d.ts.map +1 -1
- package/dist/routes/interactions.js +113 -87
- package/dist/routes/interactions.js.map +1 -1
- package/dist/routes/invitation.d.ts.map +1 -1
- package/dist/routes/invitation.js +28 -27
- package/dist/routes/invitation.js.map +1 -1
- package/dist/routes/keys.d.ts.map +1 -1
- package/dist/routes/keys.js +26 -13
- package/dist/routes/keys.js.map +1 -1
- package/dist/routes/magic-link.d.ts.map +1 -1
- package/dist/routes/magic-link.js +5 -17
- package/dist/routes/magic-link.js.map +1 -1
- package/dist/routes/organizations.d.ts +1 -2
- package/dist/routes/organizations.d.ts.map +1 -1
- package/dist/routes/organizations.js +43 -104
- package/dist/routes/organizations.js.map +1 -1
- package/dist/routes/password-reset.d.ts.map +1 -1
- package/dist/routes/password-reset.js +13 -35
- package/dist/routes/password-reset.js.map +1 -1
- package/dist/routes/permissions.d.ts +1 -1
- package/dist/routes/permissions.d.ts.map +1 -1
- package/dist/routes/permissions.js +58 -17
- package/dist/routes/permissions.js.map +1 -1
- package/dist/routes/public-branding.d.ts +16 -0
- package/dist/routes/public-branding.d.ts.map +1 -0
- package/dist/routes/public-branding.js +52 -0
- package/dist/routes/public-branding.js.map +1 -0
- package/dist/routes/roles.d.ts +1 -1
- package/dist/routes/roles.d.ts.map +1 -1
- package/dist/routes/roles.js +63 -21
- package/dist/routes/roles.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +23 -12
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/two-factor.d.ts.map +1 -1
- package/dist/routes/two-factor.js +91 -38
- package/dist/routes/two-factor.js.map +1 -1
- package/dist/routes/user-roles.d.ts.map +1 -1
- package/dist/routes/user-roles.js +56 -9
- package/dist/routes/user-roles.js.map +1 -1
- package/dist/routes/users.d.ts +4 -8
- package/dist/routes/users.d.ts.map +1 -1
- package/dist/routes/users.js +108 -265
- package/dist/routes/users.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +53 -37
- package/dist/server.js.map +1 -1
- package/dist/two-factor/errors.d.ts +9 -0
- package/dist/two-factor/errors.d.ts.map +1 -1
- package/dist/two-factor/errors.js +12 -0
- package/dist/two-factor/errors.js.map +1 -1
- package/dist/two-factor/index.d.ts +5 -4
- package/dist/two-factor/index.d.ts.map +1 -1
- package/dist/two-factor/index.js +4 -4
- package/dist/two-factor/index.js.map +1 -1
- package/dist/two-factor/repository.d.ts +17 -6
- package/dist/two-factor/repository.d.ts.map +1 -1
- package/dist/two-factor/repository.js +37 -9
- package/dist/two-factor/repository.js.map +1 -1
- package/dist/two-factor/service.d.ts.map +1 -1
- package/dist/two-factor/service.js +26 -19
- package/dist/two-factor/service.js.map +1 -1
- package/dist/two-factor/totp.d.ts +13 -2
- package/dist/two-factor/totp.d.ts.map +1 -1
- package/dist/two-factor/totp.js +20 -7
- package/dist/two-factor/totp.js.map +1 -1
- package/dist/two-factor/types.d.ts +3 -0
- package/dist/two-factor/types.d.ts.map +1 -1
- package/dist/two-factor/types.js +5 -0
- package/dist/two-factor/types.js.map +1 -1
- package/dist/users/gdpr.d.ts +2 -40
- package/dist/users/gdpr.d.ts.map +1 -1
- package/dist/users/gdpr.js +3 -110
- package/dist/users/gdpr.js.map +1 -1
- package/dist/users/index.d.ts +5 -4
- package/dist/users/index.d.ts.map +1 -1
- package/dist/users/index.js +5 -4
- package/dist/users/index.js.map +1 -1
- package/dist/users/repository.d.ts +49 -3
- package/dist/users/repository.d.ts.map +1 -1
- package/dist/users/repository.js +169 -18
- package/dist/users/repository.js.map +1 -1
- package/dist/users/service.d.ts +19 -51
- package/dist/users/service.d.ts.map +1 -1
- package/dist/users/service.js +72 -122
- package/dist/users/service.js.map +1 -1
- package/dist/users/types.d.ts +4 -5
- package/dist/users/types.d.ts.map +1 -1
- package/dist/users/types.js.map +1 -1
- package/dist/users/validators.d.ts +28 -0
- package/dist/users/validators.d.ts.map +1 -0
- package/dist/users/validators.js +25 -0
- package/dist/users/validators.js.map +1 -0
- package/locales/default/en/errors.json +2 -1
- package/migrations/024_application_client_correction.sql +39 -0
- package/migrations/025_record_deletion_lifecycle.sql +26 -0
- package/migrations/026_simplify_user_lifecycle.sql +33 -0
- package/migrations/027_branding_asset_size_limits.sql +19 -0
- package/migrations/028_totp_replay_protection.sql +17 -0
- package/migrations/029_public_session_identifiers.sql +16 -0
- package/migrations/030_global_configuration_catalog.sql +39 -0
- package/migrations/031_monotonic_updated_at.sql +28 -0
- package/migrations/032_client_require_consent.sql +15 -0
- package/package.json +2 -2
- package/dist/lib/data-import-plan.d.ts +0 -50
- package/dist/lib/data-import-plan.d.ts.map +0 -1
- package/dist/lib/data-import-plan.js +0 -235
- package/dist/lib/data-import-plan.js.map +0 -1
- package/dist/lib/data-import.d.ts +0 -276
- package/dist/lib/data-import.d.ts.map +0 -1
- package/dist/lib/data-import.js +0 -1694
- package/dist/lib/data-import.js.map +0 -1
package/dist/lib/data-import.js
DELETED
|
@@ -1,1694 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Data import service.
|
|
3
|
-
*
|
|
4
|
-
* Imports configuration data from a JSON manifest with three modes:
|
|
5
|
-
* - merge: skip existing (match by slug), create new
|
|
6
|
-
* - overwrite: update existing, create new
|
|
7
|
-
* - dry-run: show what would change without applying
|
|
8
|
-
*
|
|
9
|
-
* All changes are applied in a single PostgreSQL transaction for atomicity.
|
|
10
|
-
* Entities are processed in 12 phases in dependency order:
|
|
11
|
-
* 1. Organizations (+branding, 2FA), 2. Applications, 3. Clients + secrets,
|
|
12
|
-
* 4. Roles, 5. Permissions, 6. Claim definitions, 7. Role-permission mappings,
|
|
13
|
-
* 8. Application modules, 9. Users, 10. User-role assignments,
|
|
14
|
-
* 11. User claim values, 12. System config overrides.
|
|
15
|
-
*
|
|
16
|
-
* Authentication material is never accepted. New users rely on the normal account-recovery
|
|
17
|
-
* boundary to establish credentials after provisioning.
|
|
18
|
-
*
|
|
19
|
-
* @module data-import
|
|
20
|
-
* @see 07-import-export-invitation.md
|
|
21
|
-
*/
|
|
22
|
-
import { createHash, randomUUID } from 'crypto';
|
|
23
|
-
import { z } from 'zod';
|
|
24
|
-
import { getPool } from './database.js';
|
|
25
|
-
import { logger } from './logger.js';
|
|
26
|
-
import { buildImportManifestPlan, isProhibitedImportConfigKey, requireResolvedImportDependencies, } from './data-import-plan.js';
|
|
27
|
-
import { generateClientId, generateSecret, hashSecret, sha256Secret } from '../clients/crypto.js';
|
|
28
|
-
import { LOGIN_METHODS } from '../clients/types.js';
|
|
29
|
-
import { validateRedirectUris } from '../clients/validators.js';
|
|
30
|
-
import { validateSlug as validateOrganizationSlug } from '../organizations/slugs.js';
|
|
31
|
-
import { validateSlug as validateApplicationSlug } from '../applications/slugs.js';
|
|
32
|
-
import { validatePermissionSlug, validateRoleSlug } from '../rbac/slugs.js';
|
|
33
|
-
import { validateClaimName, validateClaimValue } from '../custom-claims/validators.js';
|
|
34
|
-
import { writeAuditLogInTransaction } from './audit-log.js';
|
|
35
|
-
/**
|
|
36
|
-
* Sanitized import failure which retains no manifest values or dependency diagnostics.
|
|
37
|
-
*/
|
|
38
|
-
export class ImportOperationError extends Error {
|
|
39
|
-
/** Stable public error discriminator. */
|
|
40
|
-
code;
|
|
41
|
-
/** Server-created identifier used to correlate private operational diagnostics. */
|
|
42
|
-
correlationId;
|
|
43
|
-
/** Recommended public HTTP status. */
|
|
44
|
-
status;
|
|
45
|
-
/**
|
|
46
|
-
* Create a minimal import failure.
|
|
47
|
-
*
|
|
48
|
-
* @param code - Stable public failure category.
|
|
49
|
-
* @param status - Recommended HTTP status for the route boundary.
|
|
50
|
-
* @param correlationId - Optional existing server correlation identifier.
|
|
51
|
-
*/
|
|
52
|
-
constructor(code, status, correlationId) {
|
|
53
|
-
super('Import request could not be completed');
|
|
54
|
-
this.name = 'ImportOperationError';
|
|
55
|
-
this.code = code;
|
|
56
|
-
this.status = status;
|
|
57
|
-
this.correlationId = correlationId ?? randomUUID();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// ============================================================================
|
|
61
|
-
// Manifest schema — validates the import payload structure
|
|
62
|
-
// ============================================================================
|
|
63
|
-
/** Current manifest version */
|
|
64
|
-
const MANIFEST_VERSION = '1.0';
|
|
65
|
-
const organizationSchema = z
|
|
66
|
-
.object({
|
|
67
|
-
name: z.string().min(1).max(255),
|
|
68
|
-
slug: z.string().refine((slug) => validateOrganizationSlug(slug).isValid),
|
|
69
|
-
default_login_methods: z.array(z.enum(LOGIN_METHODS)).min(1).optional(),
|
|
70
|
-
default_locale: z.string().max(10).optional().nullable(),
|
|
71
|
-
// Organization policy and presentation fields.
|
|
72
|
-
two_factor_policy: z
|
|
73
|
-
.enum(['optional', 'required_email', 'required_totp', 'required_any'])
|
|
74
|
-
.optional(),
|
|
75
|
-
branding_primary_color: z.string().max(20).optional(),
|
|
76
|
-
branding_company_name: z.string().max(255).optional(),
|
|
77
|
-
branding_custom_css: z.string().max(10000).optional(),
|
|
78
|
-
branding_logo_url: z.string().max(2048).optional(),
|
|
79
|
-
branding_favicon_url: z.string().max(2048).optional(),
|
|
80
|
-
})
|
|
81
|
-
.strict();
|
|
82
|
-
const applicationSchema = z
|
|
83
|
-
.object({
|
|
84
|
-
name: z.string().min(1).max(255),
|
|
85
|
-
slug: z.string().refine((slug) => validateApplicationSlug(slug).isValid),
|
|
86
|
-
organization_slug: z.string().refine((slug) => validateOrganizationSlug(slug).isValid),
|
|
87
|
-
description: z.string().max(1000).optional().nullable(),
|
|
88
|
-
})
|
|
89
|
-
.strict();
|
|
90
|
-
const clientSchema = z
|
|
91
|
-
.object({
|
|
92
|
-
client_name: z.string().min(1).max(255),
|
|
93
|
-
application_slug: z.string().refine((slug) => validateApplicationSlug(slug).isValid),
|
|
94
|
-
organization_slug: z.string().refine((slug) => validateOrganizationSlug(slug).isValid),
|
|
95
|
-
client_type: z.enum(['confidential', 'public']),
|
|
96
|
-
application_type: z.enum(['web', 'native', 'spa']).optional(),
|
|
97
|
-
grant_types: z
|
|
98
|
-
.array(z.enum(['authorization_code', 'refresh_token', 'client_credentials']))
|
|
99
|
-
.min(1)
|
|
100
|
-
.optional(),
|
|
101
|
-
redirect_uris: z.array(z.string()).min(1).max(10).optional(),
|
|
102
|
-
response_types: z.array(z.literal('code')).min(1).max(1).optional(),
|
|
103
|
-
scope: z.string().optional(),
|
|
104
|
-
login_methods: z.array(z.enum(LOGIN_METHODS)).min(1).optional().nullable(),
|
|
105
|
-
token_endpoint_auth_method: z
|
|
106
|
-
.enum(['client_secret_basic', 'client_secret_post', 'none'])
|
|
107
|
-
.optional(),
|
|
108
|
-
// Additional OIDC client fields matching the database schema.
|
|
109
|
-
post_logout_redirect_uris: z.array(z.string().url()).max(10).optional(),
|
|
110
|
-
allowed_origins: z.array(z.string().url()).max(10).optional(),
|
|
111
|
-
require_pkce: z.boolean().optional(),
|
|
112
|
-
// Secret metadata is flattened from the nested provisioning document.
|
|
113
|
-
secret_label: z.string().max(255).optional().nullable(),
|
|
114
|
-
secret_expires_at: z.string().datetime().optional().nullable(),
|
|
115
|
-
})
|
|
116
|
-
.strict()
|
|
117
|
-
.superRefine((value, context) => {
|
|
118
|
-
if (value.redirect_uris !== undefined &&
|
|
119
|
-
!validateRedirectUris(value.redirect_uris, false).isValid) {
|
|
120
|
-
context.addIssue({
|
|
121
|
-
code: 'custom',
|
|
122
|
-
message: 'Invalid redirect URI',
|
|
123
|
-
path: ['redirect_uris'],
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (value.post_logout_redirect_uris?.some((uri) => !validateRedirectUris([uri], false).isValid)) {
|
|
127
|
-
context.addIssue({
|
|
128
|
-
code: 'custom',
|
|
129
|
-
message: 'Invalid post-logout redirect URI',
|
|
130
|
-
path: ['post_logout_redirect_uris'],
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
if (value.allowed_origins?.some((origin) => {
|
|
134
|
-
const parsed = new URL(origin);
|
|
135
|
-
return (parsed.pathname !== '/' ||
|
|
136
|
-
parsed.search !== '' ||
|
|
137
|
-
parsed.hash !== '' ||
|
|
138
|
-
origin.includes('*'));
|
|
139
|
-
})) {
|
|
140
|
-
context.addIssue({
|
|
141
|
-
code: 'custom',
|
|
142
|
-
message: 'Invalid allowed origin',
|
|
143
|
-
path: ['allowed_origins'],
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
const authMethod = value.token_endpoint_auth_method ??
|
|
147
|
-
(value.client_type === 'public' ? 'none' : 'client_secret_post');
|
|
148
|
-
if (value.client_type === 'public' && (authMethod !== 'none' || value.require_pkce === false)) {
|
|
149
|
-
context.addIssue({
|
|
150
|
-
code: 'custom',
|
|
151
|
-
message: 'Public clients require PKCE and no client secret authentication',
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
if (value.client_type === 'public' && value.grant_types?.includes('client_credentials')) {
|
|
155
|
-
context.addIssue({ code: 'custom', message: 'Public clients cannot use client credentials' });
|
|
156
|
-
}
|
|
157
|
-
if (value.client_type === 'confidential' && authMethod === 'none') {
|
|
158
|
-
context.addIssue({ code: 'custom', message: 'Confidential clients require authentication' });
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
const roleSchema = z
|
|
162
|
-
.object({
|
|
163
|
-
name: z.string().min(1).max(255),
|
|
164
|
-
slug: z.string().refine(validateRoleSlug),
|
|
165
|
-
application_slug: z.string().min(1),
|
|
166
|
-
organization_slug: z.string().min(1),
|
|
167
|
-
description: z.string().max(1000).optional().nullable(),
|
|
168
|
-
})
|
|
169
|
-
.strict();
|
|
170
|
-
const permissionSchema = z
|
|
171
|
-
.object({
|
|
172
|
-
name: z.string().min(1).max(255),
|
|
173
|
-
slug: z.string().refine(validatePermissionSlug),
|
|
174
|
-
application_slug: z.string().min(1),
|
|
175
|
-
organization_slug: z.string().min(1),
|
|
176
|
-
description: z.string().max(1000).optional().nullable(),
|
|
177
|
-
})
|
|
178
|
-
.strict();
|
|
179
|
-
const claimDefinitionSchema = z
|
|
180
|
-
.object({
|
|
181
|
-
name: z.string().min(1).max(255),
|
|
182
|
-
slug: z.string().refine((slug) => validateClaimName(slug).valid),
|
|
183
|
-
application_slug: z.string().min(1),
|
|
184
|
-
organization_slug: z.string().min(1),
|
|
185
|
-
claim_type: z.enum(['string', 'number', 'boolean', 'json']),
|
|
186
|
-
description: z.string().max(1000).optional().nullable(),
|
|
187
|
-
})
|
|
188
|
-
.strict();
|
|
189
|
-
/**
|
|
190
|
-
* Schema for role-permission mappings.
|
|
191
|
-
* Links roles to permissions after both are created during import.
|
|
192
|
-
* Uses slug-based references resolved against the appMap.
|
|
193
|
-
*/
|
|
194
|
-
const rolePermissionMappingSchema = z
|
|
195
|
-
.object({
|
|
196
|
-
role_slug: z.string().refine(validateRoleSlug),
|
|
197
|
-
permission_slugs: z.array(z.string().refine(validatePermissionSlug)).min(1),
|
|
198
|
-
application_slug: z.string().refine((slug) => validateApplicationSlug(slug).isValid),
|
|
199
|
-
organization_slug: z.string().refine((slug) => validateOrganizationSlug(slug).isValid),
|
|
200
|
-
})
|
|
201
|
-
.strict();
|
|
202
|
-
/** User input in the flat manifest format. */
|
|
203
|
-
const userSchema = z
|
|
204
|
-
.object({
|
|
205
|
-
email: z.string().min(1).email(),
|
|
206
|
-
organization_slug: z.string().min(1),
|
|
207
|
-
given_name: z.string().max(255).optional(),
|
|
208
|
-
family_name: z.string().max(255).optional(),
|
|
209
|
-
locale: z.string().max(10).optional(),
|
|
210
|
-
status: z.enum(['active', 'inactive']).optional(),
|
|
211
|
-
email_verified: z.boolean().optional(),
|
|
212
|
-
})
|
|
213
|
-
.strict();
|
|
214
|
-
/** Application-module input. */
|
|
215
|
-
const applicationModuleSchema = z
|
|
216
|
-
.object({
|
|
217
|
-
name: z.string().min(1).max(255),
|
|
218
|
-
slug: z.string().min(1).max(63),
|
|
219
|
-
application_slug: z.string().min(1),
|
|
220
|
-
organization_slug: z.string().min(1),
|
|
221
|
-
description: z.string().max(1000).optional(),
|
|
222
|
-
status: z.enum(['active', 'inactive']).optional(),
|
|
223
|
-
})
|
|
224
|
-
.strict();
|
|
225
|
-
/** User-role assignment input. */
|
|
226
|
-
const userRoleAssignmentSchema = z
|
|
227
|
-
.object({
|
|
228
|
-
email: z.string().min(1).email(),
|
|
229
|
-
organization_slug: z.string().min(1),
|
|
230
|
-
application_slug: z.string().min(1),
|
|
231
|
-
role_slug: z.string().min(1),
|
|
232
|
-
})
|
|
233
|
-
.strict();
|
|
234
|
-
/** User claim-value input. */
|
|
235
|
-
const userClaimValueSchema = z
|
|
236
|
-
.object({
|
|
237
|
-
email: z.string().min(1).email(),
|
|
238
|
-
organization_slug: z.string().min(1),
|
|
239
|
-
application_slug: z.string().min(1),
|
|
240
|
-
claim_slug: z.string().min(1),
|
|
241
|
-
value: z.unknown(),
|
|
242
|
-
})
|
|
243
|
-
.strict();
|
|
244
|
-
/** Add one issue for every repeated tenant-qualified natural key. */
|
|
245
|
-
function requireUniqueNaturalKeys(items, collection, keyFor, context) {
|
|
246
|
-
const firstIndexes = new Map();
|
|
247
|
-
for (const [index, item] of items.entries()) {
|
|
248
|
-
const key = keyFor(item);
|
|
249
|
-
if (firstIndexes.has(key)) {
|
|
250
|
-
context.addIssue({
|
|
251
|
-
code: 'custom',
|
|
252
|
-
message: `Duplicate natural key in ${collection}`,
|
|
253
|
-
path: [collection, index],
|
|
254
|
-
});
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
firstIndexes.set(key, index);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/** Join natural-key components without allowing ambiguous concatenation. */
|
|
261
|
-
function naturalKey(...parts) {
|
|
262
|
-
return parts.join('\u001f');
|
|
263
|
-
}
|
|
264
|
-
/** The full import manifest schema */
|
|
265
|
-
export const importManifestSchema = z
|
|
266
|
-
.object({
|
|
267
|
-
version: z.literal(MANIFEST_VERSION),
|
|
268
|
-
exportedAt: z.string().optional(),
|
|
269
|
-
organizations: z.array(organizationSchema).optional().default([]),
|
|
270
|
-
applications: z.array(applicationSchema).optional().default([]),
|
|
271
|
-
clients: z.array(clientSchema).optional().default([]),
|
|
272
|
-
roles: z.array(roleSchema).optional().default([]),
|
|
273
|
-
permissions: z.array(permissionSchema).optional().default([]),
|
|
274
|
-
claim_definitions: z.array(claimDefinitionSchema).optional().default([]),
|
|
275
|
-
// Role-permission mappings are applied after roles and permissions.
|
|
276
|
-
role_permission_mappings: z.array(rolePermissionMappingSchema).optional().default([]),
|
|
277
|
-
// System configuration overrides can update only existing keys.
|
|
278
|
-
config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
279
|
-
// User and application relationship collections.
|
|
280
|
-
users: z.array(userSchema).optional().default([]),
|
|
281
|
-
application_modules: z.array(applicationModuleSchema).optional().default([]),
|
|
282
|
-
user_role_assignments: z.array(userRoleAssignmentSchema).optional().default([]),
|
|
283
|
-
user_claim_values: z.array(userClaimValueSchema).optional().default([]),
|
|
284
|
-
})
|
|
285
|
-
.strict()
|
|
286
|
-
.superRefine((manifest, context) => {
|
|
287
|
-
requireUniqueNaturalKeys(manifest.organizations, 'organizations', (item) => item.slug, context);
|
|
288
|
-
requireUniqueNaturalKeys(manifest.applications, 'applications', (item) => item.slug, context);
|
|
289
|
-
requireUniqueNaturalKeys(manifest.clients, 'clients', (item) => naturalKey(item.organization_slug, item.application_slug, item.client_name), context);
|
|
290
|
-
requireUniqueNaturalKeys(manifest.roles, 'roles', (item) => naturalKey(item.application_slug, item.slug), context);
|
|
291
|
-
requireUniqueNaturalKeys(manifest.permissions, 'permissions', (item) => naturalKey(item.application_slug, item.slug), context);
|
|
292
|
-
requireUniqueNaturalKeys(manifest.claim_definitions, 'claim_definitions', (item) => naturalKey(item.application_slug, item.slug), context);
|
|
293
|
-
requireUniqueNaturalKeys(manifest.role_permission_mappings, 'role_permission_mappings', (item) => naturalKey(item.application_slug, item.role_slug), context);
|
|
294
|
-
requireUniqueNaturalKeys(manifest.application_modules, 'application_modules', (item) => naturalKey(item.application_slug, item.slug), context);
|
|
295
|
-
requireUniqueNaturalKeys(manifest.users, 'users', (item) => naturalKey(item.organization_slug, item.email.toLowerCase()), context);
|
|
296
|
-
requireUniqueNaturalKeys(manifest.user_role_assignments, 'user_role_assignments', (item) => naturalKey(item.organization_slug, item.application_slug, item.email.toLowerCase(), item.role_slug), context);
|
|
297
|
-
requireUniqueNaturalKeys(manifest.user_claim_values, 'user_claim_values', (item) => naturalKey(item.organization_slug, item.application_slug, item.email.toLowerCase(), item.claim_slug), context);
|
|
298
|
-
for (const configKey of Object.keys(manifest.config ?? {})) {
|
|
299
|
-
if (isProhibitedImportConfigKey(configKey)) {
|
|
300
|
-
context.addIssue({
|
|
301
|
-
code: 'custom',
|
|
302
|
-
message: 'Import configuration cannot contain authentication material',
|
|
303
|
-
path: ['config', configKey],
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
// ============================================================================
|
|
309
|
-
// Import service
|
|
310
|
-
// ============================================================================
|
|
311
|
-
/**
|
|
312
|
-
* Import configuration from a JSON manifest.
|
|
313
|
-
*
|
|
314
|
-
* Processes entities in dependency order within a single transaction:
|
|
315
|
-
* organizations → applications → clients → roles → permissions → claims.
|
|
316
|
-
*
|
|
317
|
-
* @param manifest - The validated import manifest
|
|
318
|
-
* @param mode - Import mode: merge, overwrite, or dry-run
|
|
319
|
-
* @param actorId - ID of the admin performing the import (for audit log)
|
|
320
|
-
* @returns Import result with created/updated/skipped/error counts
|
|
321
|
-
* @throws Error if manifest version is incompatible
|
|
322
|
-
*/
|
|
323
|
-
export async function importData(manifestInput, mode, actorId, organizationId) {
|
|
324
|
-
// Public routes parse first, while direct service callers receive the same closed/defaulted
|
|
325
|
-
// contract here so optional collections can never bypass prevalidation.
|
|
326
|
-
const manifest = importManifestSchema.parse(manifestInput);
|
|
327
|
-
// Validate manifest version
|
|
328
|
-
if (manifest.version !== MANIFEST_VERSION) {
|
|
329
|
-
throw new Error(`Unsupported manifest version: ${manifest.version}. Expected: ${MANIFEST_VERSION}`);
|
|
330
|
-
}
|
|
331
|
-
// Build the complete mutation-free dependency plan before obtaining a transaction client.
|
|
332
|
-
// Database-backed planning resolves its external dependencies in the execution task.
|
|
333
|
-
const manifestPlan = buildImportManifestPlan(manifest);
|
|
334
|
-
const result = {
|
|
335
|
-
mode,
|
|
336
|
-
created: [],
|
|
337
|
-
updated: [],
|
|
338
|
-
skipped: [],
|
|
339
|
-
errors: [],
|
|
340
|
-
credentials: [],
|
|
341
|
-
};
|
|
342
|
-
const pool = getPool();
|
|
343
|
-
const client = await pool.connect();
|
|
344
|
-
const acquiredClientLocks = [];
|
|
345
|
-
try {
|
|
346
|
-
const clientLockKeys = manifest.clients
|
|
347
|
-
.map((item) => naturalKey(item.organization_slug, item.application_slug, item.client_name))
|
|
348
|
-
.sort();
|
|
349
|
-
for (const lockKey of clientLockKeys) {
|
|
350
|
-
await client.query('SELECT pg_advisory_lock(hashtextextended($1, 0))', [lockKey]);
|
|
351
|
-
acquiredClientLocks.push(lockKey);
|
|
352
|
-
}
|
|
353
|
-
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
|
|
354
|
-
try {
|
|
355
|
-
await requireImportScope(client, manifest, organizationId);
|
|
356
|
-
await requireResolvedImportDependencies(client, manifestPlan);
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
360
|
-
}
|
|
361
|
-
// Slug-to-ID maps built during import for resolving foreign key references.
|
|
362
|
-
// Populated as entities are created/matched, used by dependent entities.
|
|
363
|
-
const orgMap = new Map(); // org slug → org ID
|
|
364
|
-
const appMap = new Map(); // "orgSlug:appSlug" → app ID
|
|
365
|
-
const roleMap = new Map();
|
|
366
|
-
const permissionMap = new Map();
|
|
367
|
-
const claimMap = new Map();
|
|
368
|
-
// A dry-run cannot persist newly declared parents, but dependent validation still needs stable
|
|
369
|
-
// transaction-local identities. UUID-shaped deterministic placeholders allow the normal
|
|
370
|
-
// parameterized lookup paths to run without ever matching durable rows.
|
|
371
|
-
if (mode === 'dry-run') {
|
|
372
|
-
for (const organization of manifest.organizations) {
|
|
373
|
-
orgMap.set(organization.slug, plannedEntityId('organization', organization.slug));
|
|
374
|
-
}
|
|
375
|
-
for (const application of manifest.applications) {
|
|
376
|
-
appMap.set(`${application.organization_slug}:${application.slug}`, plannedEntityId('application', `${application.organization_slug}:${application.slug}`));
|
|
377
|
-
}
|
|
378
|
-
for (const role of manifest.roles) {
|
|
379
|
-
roleMap.set(naturalKey(role.organization_slug, role.application_slug, role.slug), plannedEntityId('role', naturalKey(role.organization_slug, role.application_slug, role.slug)));
|
|
380
|
-
}
|
|
381
|
-
for (const permission of manifest.permissions) {
|
|
382
|
-
permissionMap.set(naturalKey(permission.organization_slug, permission.application_slug, permission.slug), plannedEntityId('permission', naturalKey(permission.organization_slug, permission.application_slug, permission.slug)));
|
|
383
|
-
}
|
|
384
|
-
for (const claim of manifest.claim_definitions) {
|
|
385
|
-
const claimKey = naturalKey(claim.organization_slug, claim.application_slug, claim.slug);
|
|
386
|
-
claimMap.set(claimKey, {
|
|
387
|
-
id: plannedEntityId('claim_definition', claimKey),
|
|
388
|
-
claimType: claim.claim_type,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// Organizations have no manifest dependencies.
|
|
393
|
-
for (const org of manifest.organizations ?? []) {
|
|
394
|
-
await processOrganization(client, org, mode, result, orgMap);
|
|
395
|
-
}
|
|
396
|
-
// Applications depend on organizations.
|
|
397
|
-
for (const app of manifest.applications ?? []) {
|
|
398
|
-
await processApplication(client, app, mode, result, appMap);
|
|
399
|
-
}
|
|
400
|
-
// Clients depend on organizations and applications.
|
|
401
|
-
for (const clientDef of manifest.clients ?? []) {
|
|
402
|
-
await processClient(client, clientDef, mode, result, orgMap, appMap);
|
|
403
|
-
}
|
|
404
|
-
// Roles depend on applications.
|
|
405
|
-
for (const role of manifest.roles ?? []) {
|
|
406
|
-
await processRole(client, role, mode, result, orgMap, appMap, roleMap);
|
|
407
|
-
}
|
|
408
|
-
// Permissions depend on applications.
|
|
409
|
-
for (const perm of manifest.permissions ?? []) {
|
|
410
|
-
await processPermission(client, perm, mode, result, orgMap, appMap, permissionMap);
|
|
411
|
-
}
|
|
412
|
-
// Claim definitions depend on applications.
|
|
413
|
-
for (const claim of manifest.claim_definitions ?? []) {
|
|
414
|
-
await processClaimDefinition(client, claim, mode, result, orgMap, appMap, claimMap);
|
|
415
|
-
}
|
|
416
|
-
// Role-permission mappings depend on roles and permissions.
|
|
417
|
-
for (const mapping of manifest.role_permission_mappings ?? []) {
|
|
418
|
-
await processRolePermissionMapping(client, mapping, mode, result, orgMap, appMap, roleMap, permissionMap);
|
|
419
|
-
}
|
|
420
|
-
// Application modules depend on applications.
|
|
421
|
-
for (const mod of manifest.application_modules ?? []) {
|
|
422
|
-
await processApplicationModule(client, mod, mode, result, orgMap, appMap);
|
|
423
|
-
}
|
|
424
|
-
// Users depend on organizations.
|
|
425
|
-
const userMap = new Map(); // "orgSlug:email" → user ID
|
|
426
|
-
if (mode === 'dry-run') {
|
|
427
|
-
for (const user of manifest.users) {
|
|
428
|
-
userMap.set(`${user.organization_slug}:${user.email}`, plannedEntityId('user', `${user.organization_slug}:${user.email.toLowerCase()}`));
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
for (const user of manifest.users ?? []) {
|
|
432
|
-
await processUser(client, user, mode, result, orgMap, userMap);
|
|
433
|
-
}
|
|
434
|
-
// User-role assignments depend on users and roles.
|
|
435
|
-
for (const assignment of manifest.user_role_assignments ?? []) {
|
|
436
|
-
await processUserRoleAssignment(client, assignment, mode, result, orgMap, appMap, userMap, roleMap);
|
|
437
|
-
}
|
|
438
|
-
// User claim values depend on users and claim definitions.
|
|
439
|
-
for (const claimValue of manifest.user_claim_values ?? []) {
|
|
440
|
-
await processUserClaimValue(client, claimValue, mode, result, orgMap, appMap, userMap, claimMap);
|
|
441
|
-
}
|
|
442
|
-
// System configuration overrides run after entity planning.
|
|
443
|
-
for (const [key, value] of Object.entries(manifest.config ?? {})) {
|
|
444
|
-
await processConfigOverride(client, key, value, mode, result);
|
|
445
|
-
}
|
|
446
|
-
if (result.errors.length > 0) {
|
|
447
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
448
|
-
}
|
|
449
|
-
// Dry-run rolls back the snapshot transaction even though every processor also avoids writes.
|
|
450
|
-
// The rollback is a defense in depth boundary which prevents a later processor regression from
|
|
451
|
-
// turning a preview into a mutation.
|
|
452
|
-
if (mode === 'dry-run') {
|
|
453
|
-
await client.query('ROLLBACK');
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
// Write audit log entry for the import
|
|
457
|
-
if (actorId) {
|
|
458
|
-
await writeAuditLogInTransaction(client, {
|
|
459
|
-
actorId,
|
|
460
|
-
eventType: 'admin.import',
|
|
461
|
-
eventCategory: 'admin',
|
|
462
|
-
metadata: {
|
|
463
|
-
mode,
|
|
464
|
-
manifestVersion: manifest.version,
|
|
465
|
-
manifestDigest: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
|
|
466
|
-
created: result.created.length,
|
|
467
|
-
updated: result.updated.length,
|
|
468
|
-
skipped: result.skipped.length,
|
|
469
|
-
},
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
await client.query('COMMIT');
|
|
473
|
-
}
|
|
474
|
-
const { errors: _errors, ...successfulResult } = result;
|
|
475
|
-
return successfulResult;
|
|
476
|
-
}
|
|
477
|
-
catch (err) {
|
|
478
|
-
try {
|
|
479
|
-
await client.query('ROLLBACK');
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
// The original failure remains authoritative. Connection cleanup happens in finally.
|
|
483
|
-
}
|
|
484
|
-
const failure = err instanceof ImportOperationError
|
|
485
|
-
? err
|
|
486
|
-
: new ImportOperationError('import_execution_failed', 503);
|
|
487
|
-
logger.error({ code: failure.code, correlationId: failure.correlationId, mode }, 'Import transaction failed');
|
|
488
|
-
throw failure;
|
|
489
|
-
}
|
|
490
|
-
finally {
|
|
491
|
-
await releaseImportClient(client, acquiredClientLocks);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Release session locks without revising the transaction's authoritative outcome.
|
|
496
|
-
*
|
|
497
|
-
* An unlock failure destroys the pooled connection, which makes PostgreSQL release every
|
|
498
|
-
* session-scoped lock. Cleanup is deliberately non-throwing because commit may already have made
|
|
499
|
-
* a one-time credential result durable and authoritative.
|
|
500
|
-
*/
|
|
501
|
-
async function releaseImportClient(client, acquiredLocks) {
|
|
502
|
-
try {
|
|
503
|
-
for (const lockKey of [...acquiredLocks].reverse()) {
|
|
504
|
-
await client.query('SELECT pg_advisory_unlock(hashtextextended($1, 0))', [lockKey]);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
try {
|
|
509
|
-
client.release(error instanceof Error ? error : new Error('Import lock cleanup failed'));
|
|
510
|
-
}
|
|
511
|
-
catch {
|
|
512
|
-
// Pool cleanup must never replace the committed result or the original transaction failure.
|
|
513
|
-
}
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
try {
|
|
517
|
-
client.release();
|
|
518
|
-
}
|
|
519
|
-
catch {
|
|
520
|
-
// Pool cleanup must never replace the committed result or the original transaction failure.
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* Require every tenant-qualified manifest entry to resolve to one requested organization.
|
|
525
|
-
* Global imports omit the boundary and retain the established super-administrator behavior.
|
|
526
|
-
*/
|
|
527
|
-
async function requireImportScope(client, manifest, organizationId) {
|
|
528
|
-
if (organizationId === undefined)
|
|
529
|
-
return;
|
|
530
|
-
const organization = await client.query('SELECT slug FROM organizations WHERE id = $1', [organizationId]);
|
|
531
|
-
const expectedSlug = organization.rows[0]?.slug;
|
|
532
|
-
if (!expectedSlug || Object.keys(manifest.config ?? {}).length > 0) {
|
|
533
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
534
|
-
}
|
|
535
|
-
const referencedSlugs = [
|
|
536
|
-
...manifest.organizations.map((item) => item.slug),
|
|
537
|
-
...manifest.applications.map((item) => item.organization_slug),
|
|
538
|
-
...manifest.clients.map((item) => item.organization_slug),
|
|
539
|
-
...manifest.roles.map((item) => item.organization_slug),
|
|
540
|
-
...manifest.permissions.map((item) => item.organization_slug),
|
|
541
|
-
...manifest.claim_definitions.map((item) => item.organization_slug),
|
|
542
|
-
...manifest.role_permission_mappings.map((item) => item.organization_slug),
|
|
543
|
-
...manifest.users.map((item) => item.organization_slug),
|
|
544
|
-
...manifest.application_modules.map((item) => item.organization_slug),
|
|
545
|
-
...manifest.user_role_assignments.map((item) => item.organization_slug),
|
|
546
|
-
...manifest.user_claim_values.map((item) => item.organization_slug),
|
|
547
|
-
];
|
|
548
|
-
if (referencedSlugs.some((slug) => slug !== expectedSlug)) {
|
|
549
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
550
|
-
}
|
|
551
|
-
const declaredApplications = new Set(manifest.applications
|
|
552
|
-
.filter((application) => application.organization_slug === expectedSlug)
|
|
553
|
-
.map((application) => application.slug));
|
|
554
|
-
for (const applicationSlug of declaredApplications) {
|
|
555
|
-
const existing = await client.query(`SELECT 1 FROM applications a
|
|
556
|
-
JOIN clients c ON c.application_id = a.id
|
|
557
|
-
WHERE a.slug = $1 AND c.organization_id = $2
|
|
558
|
-
LIMIT 1`, [applicationSlug, organizationId]);
|
|
559
|
-
const globallyExisting = await client.query('SELECT 1 FROM applications WHERE slug = $1', [
|
|
560
|
-
applicationSlug,
|
|
561
|
-
]);
|
|
562
|
-
if (globallyExisting.rowCount === 1 && existing.rowCount !== 1) {
|
|
563
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
const referencedApplications = new Set([
|
|
567
|
-
...manifest.clients.map((item) => item.application_slug),
|
|
568
|
-
...manifest.roles.map((item) => item.application_slug),
|
|
569
|
-
...manifest.permissions.map((item) => item.application_slug),
|
|
570
|
-
...manifest.claim_definitions.map((item) => item.application_slug),
|
|
571
|
-
...manifest.role_permission_mappings.map((item) => item.application_slug),
|
|
572
|
-
...manifest.application_modules.map((item) => item.application_slug),
|
|
573
|
-
...manifest.user_role_assignments.map((item) => item.application_slug),
|
|
574
|
-
...manifest.user_claim_values.map((item) => item.application_slug),
|
|
575
|
-
]);
|
|
576
|
-
for (const applicationSlug of referencedApplications) {
|
|
577
|
-
if (declaredApplications.has(applicationSlug))
|
|
578
|
-
continue;
|
|
579
|
-
const relationship = await client.query(`SELECT 1 FROM applications a
|
|
580
|
-
JOIN clients c ON c.application_id = a.id
|
|
581
|
-
WHERE a.slug = $1 AND c.organization_id = $2
|
|
582
|
-
LIMIT 1`, [applicationSlug, organizationId]);
|
|
583
|
-
if (relationship.rowCount !== 1) {
|
|
584
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
// ============================================================================
|
|
589
|
-
// Entity processors
|
|
590
|
-
// ============================================================================
|
|
591
|
-
/**
|
|
592
|
-
* Process a single organization from the manifest.
|
|
593
|
-
* Looks up by slug; creates, updates, or skips based on mode.
|
|
594
|
-
*/
|
|
595
|
-
async function processOrganization(client, org, mode, result, orgMap) {
|
|
596
|
-
try {
|
|
597
|
-
const { rows: existing } = await client.query(`SELECT id, name, default_locale, default_login_methods, two_factor_policy,
|
|
598
|
-
branding_primary_color, branding_company_name,
|
|
599
|
-
branding_custom_css, branding_logo_url, branding_favicon_url
|
|
600
|
-
FROM organizations WHERE slug = $1`, [org.slug]);
|
|
601
|
-
if (existing.length > 0) {
|
|
602
|
-
orgMap.set(org.slug, existing[0].id);
|
|
603
|
-
if (mode !== 'merge' &&
|
|
604
|
-
((org.default_login_methods !== undefined &&
|
|
605
|
-
JSON.stringify(existing[0].default_login_methods) !==
|
|
606
|
-
JSON.stringify(org.default_login_methods)) ||
|
|
607
|
-
(org.two_factor_policy !== undefined &&
|
|
608
|
-
existing[0].two_factor_policy !== org.two_factor_policy))) {
|
|
609
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
610
|
-
}
|
|
611
|
-
const changedFields = [
|
|
612
|
-
existing[0].name !== org.name ? 'name' : null,
|
|
613
|
-
existing[0].default_locale !== (org.default_locale ?? null) ? 'default_locale' : null,
|
|
614
|
-
existing[0].branding_primary_color !== (org.branding_primary_color ?? null)
|
|
615
|
-
? 'branding_primary_color'
|
|
616
|
-
: null,
|
|
617
|
-
existing[0].branding_company_name !== (org.branding_company_name ?? null)
|
|
618
|
-
? 'branding_company_name'
|
|
619
|
-
: null,
|
|
620
|
-
existing[0].branding_custom_css !== (org.branding_custom_css ?? null)
|
|
621
|
-
? 'branding_custom_css'
|
|
622
|
-
: null,
|
|
623
|
-
existing[0].branding_logo_url !== (org.branding_logo_url ?? null)
|
|
624
|
-
? 'branding_logo_url'
|
|
625
|
-
: null,
|
|
626
|
-
existing[0].branding_favicon_url !== (org.branding_favicon_url ?? null)
|
|
627
|
-
? 'branding_favicon_url'
|
|
628
|
-
: null,
|
|
629
|
-
].filter((field) => field !== null);
|
|
630
|
-
if (mode === 'merge' || changedFields.length === 0) {
|
|
631
|
-
result.skipped.push({
|
|
632
|
-
type: 'organization',
|
|
633
|
-
slug: org.slug,
|
|
634
|
-
reason: mode === 'merge' ? 'Already exists (merge mode)' : 'No mutable changes',
|
|
635
|
-
});
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
if (mode === 'dry-run') {
|
|
639
|
-
result.updated.push({
|
|
640
|
-
type: 'organization',
|
|
641
|
-
slug: org.slug,
|
|
642
|
-
name: org.name,
|
|
643
|
-
changes: changedFields,
|
|
644
|
-
});
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
await client.query(`UPDATE organizations
|
|
648
|
-
SET name = $1, default_locale = $2, branding_primary_color = $3,
|
|
649
|
-
branding_company_name = $4, branding_custom_css = $5, branding_logo_url = $6,
|
|
650
|
-
branding_favicon_url = $7, updated_at = NOW()
|
|
651
|
-
WHERE slug = $8`, [
|
|
652
|
-
org.name,
|
|
653
|
-
org.default_locale ?? null,
|
|
654
|
-
org.branding_primary_color ?? null,
|
|
655
|
-
org.branding_company_name ?? null,
|
|
656
|
-
org.branding_custom_css ?? null,
|
|
657
|
-
org.branding_logo_url ?? null,
|
|
658
|
-
org.branding_favicon_url ?? null,
|
|
659
|
-
org.slug,
|
|
660
|
-
]);
|
|
661
|
-
result.updated.push({
|
|
662
|
-
type: 'organization',
|
|
663
|
-
slug: org.slug,
|
|
664
|
-
name: org.name,
|
|
665
|
-
changes: changedFields,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
// Create new organization
|
|
670
|
-
if (mode === 'dry-run') {
|
|
671
|
-
result.created.push({ type: 'organization', slug: org.slug, name: org.name });
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
// Build INSERT dynamically: omit default_login_methods when not specified
|
|
675
|
-
// so the DB NOT NULL DEFAULT '{password,magic_link}' applies
|
|
676
|
-
const columns = ['name', 'slug', 'default_locale', 'status'];
|
|
677
|
-
const values = [
|
|
678
|
-
org.name,
|
|
679
|
-
org.slug,
|
|
680
|
-
org.default_locale ?? null,
|
|
681
|
-
'active',
|
|
682
|
-
];
|
|
683
|
-
if (org.default_login_methods) {
|
|
684
|
-
columns.splice(3, 0, 'default_login_methods');
|
|
685
|
-
values.splice(3, 0, org.default_login_methods);
|
|
686
|
-
}
|
|
687
|
-
// Creation may apply the admitted branding and two-factor policy fields.
|
|
688
|
-
if (org.two_factor_policy) {
|
|
689
|
-
columns.push('two_factor_policy');
|
|
690
|
-
values.push(org.two_factor_policy);
|
|
691
|
-
}
|
|
692
|
-
if (org.branding_primary_color) {
|
|
693
|
-
columns.push('branding_primary_color');
|
|
694
|
-
values.push(org.branding_primary_color);
|
|
695
|
-
}
|
|
696
|
-
if (org.branding_company_name) {
|
|
697
|
-
columns.push('branding_company_name');
|
|
698
|
-
values.push(org.branding_company_name);
|
|
699
|
-
}
|
|
700
|
-
if (org.branding_custom_css) {
|
|
701
|
-
columns.push('branding_custom_css');
|
|
702
|
-
values.push(org.branding_custom_css);
|
|
703
|
-
}
|
|
704
|
-
if (org.branding_logo_url) {
|
|
705
|
-
columns.push('branding_logo_url');
|
|
706
|
-
values.push(org.branding_logo_url);
|
|
707
|
-
}
|
|
708
|
-
if (org.branding_favicon_url) {
|
|
709
|
-
columns.push('branding_favicon_url');
|
|
710
|
-
values.push(org.branding_favicon_url);
|
|
711
|
-
}
|
|
712
|
-
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
|
713
|
-
const { rows } = await client.query(`INSERT INTO organizations (${columns.join(', ')}) VALUES (${placeholders}) RETURNING id`, values);
|
|
714
|
-
orgMap.set(org.slug, rows[0].id);
|
|
715
|
-
result.created.push({ type: 'organization', slug: org.slug, name: org.name });
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
catch (err) {
|
|
719
|
-
throw normalizeImportFailure(err);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Process a single application from the manifest.
|
|
724
|
-
* Applications are global (no org FK); matched by slug.
|
|
725
|
-
*/
|
|
726
|
-
async function processApplication(client, app, mode, result, appMap) {
|
|
727
|
-
try {
|
|
728
|
-
const { rows: existing } = await client.query('SELECT id, name, description FROM applications WHERE slug = $1', [app.slug]);
|
|
729
|
-
const mapKey = `${app.organization_slug}:${app.slug}`;
|
|
730
|
-
if (existing.length > 0) {
|
|
731
|
-
appMap.set(mapKey, existing[0].id);
|
|
732
|
-
const changedFields = [
|
|
733
|
-
existing[0].name !== app.name ? 'name' : null,
|
|
734
|
-
existing[0].description !== (app.description ?? null) ? 'description' : null,
|
|
735
|
-
].filter((field) => field !== null);
|
|
736
|
-
if (mode === 'merge' || changedFields.length === 0) {
|
|
737
|
-
result.skipped.push({
|
|
738
|
-
type: 'application',
|
|
739
|
-
slug: app.slug,
|
|
740
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
741
|
-
});
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
if (mode === 'dry-run') {
|
|
745
|
-
result.updated.push({
|
|
746
|
-
type: 'application',
|
|
747
|
-
slug: app.slug,
|
|
748
|
-
name: app.name,
|
|
749
|
-
changes: changedFields,
|
|
750
|
-
});
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
await client.query(`UPDATE applications SET name = $1, description = $2, updated_at = NOW()
|
|
754
|
-
WHERE id = $3`, [app.name, app.description ?? null, existing[0].id]);
|
|
755
|
-
result.updated.push({
|
|
756
|
-
type: 'application',
|
|
757
|
-
slug: app.slug,
|
|
758
|
-
name: app.name,
|
|
759
|
-
changes: changedFields,
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
if (mode === 'dry-run') {
|
|
764
|
-
result.created.push({ type: 'application', slug: app.slug, name: app.name });
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
const { rows } = await client.query(`INSERT INTO applications (name, slug, description, status)
|
|
768
|
-
VALUES ($1, $2, $3, 'active') RETURNING id`, [app.name, app.slug, app.description ?? null]);
|
|
769
|
-
appMap.set(mapKey, rows[0].id);
|
|
770
|
-
result.created.push({ type: 'application', slug: app.slug, name: app.name });
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
catch (err) {
|
|
774
|
-
throw normalizeImportFailure(err);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Process a single client from the manifest.
|
|
779
|
-
* Clients are matched by client_name within the application scope.
|
|
780
|
-
*/
|
|
781
|
-
async function processClient(client, clientDef, mode, result, orgMap, appMap) {
|
|
782
|
-
try {
|
|
783
|
-
const orgId = await resolveOrgId(client, clientDef.organization_slug, orgMap);
|
|
784
|
-
if (!orgId) {
|
|
785
|
-
result.errors.push({
|
|
786
|
-
type: 'client',
|
|
787
|
-
slug: clientDef.client_name,
|
|
788
|
-
error: `Parent organization '${clientDef.organization_slug}' not found`,
|
|
789
|
-
});
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
const appId = await resolveAppId(client, clientDef.organization_slug, clientDef.application_slug, orgMap, appMap);
|
|
793
|
-
if (!appId) {
|
|
794
|
-
result.errors.push({
|
|
795
|
-
type: 'client',
|
|
796
|
-
slug: clientDef.client_name,
|
|
797
|
-
error: `Parent application '${clientDef.application_slug}' not found`,
|
|
798
|
-
});
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
const { rows: existing } = await client.query(`SELECT id, client_id, client_type, application_type, grant_types, redirect_uris,
|
|
802
|
-
response_types, scope, login_methods, token_endpoint_auth_method,
|
|
803
|
-
post_logout_redirect_uris, allowed_origins, require_pkce
|
|
804
|
-
FROM clients
|
|
805
|
-
WHERE client_name = $1 AND application_id = $2 AND organization_id = $3`, [clientDef.client_name, appId, orgId]);
|
|
806
|
-
if (existing.length > 1) {
|
|
807
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
808
|
-
}
|
|
809
|
-
if (existing.length > 0) {
|
|
810
|
-
const expectedAuthMethod = clientDef.token_endpoint_auth_method ??
|
|
811
|
-
(clientDef.client_type === 'public' ? 'none' : 'client_secret_post');
|
|
812
|
-
const immutableMatches = existing[0].client_type === clientDef.client_type &&
|
|
813
|
-
existing[0].application_type === (clientDef.application_type ?? 'web') &&
|
|
814
|
-
JSON.stringify(existing[0].grant_types) ===
|
|
815
|
-
JSON.stringify(clientDef.grant_types ?? ['authorization_code']) &&
|
|
816
|
-
JSON.stringify(existing[0].redirect_uris) ===
|
|
817
|
-
JSON.stringify(clientDef.redirect_uris ?? []) &&
|
|
818
|
-
JSON.stringify(existing[0].response_types) ===
|
|
819
|
-
JSON.stringify(clientDef.response_types ?? ['code']) &&
|
|
820
|
-
existing[0].scope === (clientDef.scope ?? 'openid') &&
|
|
821
|
-
JSON.stringify(existing[0].login_methods) ===
|
|
822
|
-
JSON.stringify(clientDef.login_methods ?? null) &&
|
|
823
|
-
existing[0].token_endpoint_auth_method === expectedAuthMethod &&
|
|
824
|
-
JSON.stringify(existing[0].post_logout_redirect_uris) ===
|
|
825
|
-
JSON.stringify(clientDef.post_logout_redirect_uris ?? []) &&
|
|
826
|
-
JSON.stringify(existing[0].allowed_origins) ===
|
|
827
|
-
JSON.stringify(clientDef.allowed_origins ?? []) &&
|
|
828
|
-
existing[0].require_pkce === (clientDef.require_pkce ?? true);
|
|
829
|
-
if (mode !== 'merge' && !immutableMatches) {
|
|
830
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
831
|
-
}
|
|
832
|
-
result.skipped.push({
|
|
833
|
-
type: 'client',
|
|
834
|
-
slug: clientDef.client_name,
|
|
835
|
-
reason: 'Already exists; immutable client authority preserved',
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
if (mode === 'dry-run') {
|
|
840
|
-
result.created.push({
|
|
841
|
-
type: 'client',
|
|
842
|
-
slug: clientDef.client_name,
|
|
843
|
-
name: clientDef.client_name,
|
|
844
|
-
credentialWillBeGenerated: clientDef.client_type === 'confidential',
|
|
845
|
-
});
|
|
846
|
-
result.credentials.push({
|
|
847
|
-
clientName: clientDef.client_name,
|
|
848
|
-
clientType: clientDef.client_type,
|
|
849
|
-
credentialWillBeGenerated: clientDef.client_type === 'confidential',
|
|
850
|
-
});
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
// Generate base64url client_id using the standard crypto utility
|
|
854
|
-
const clientId = generateClientId();
|
|
855
|
-
// Derive token_endpoint_auth_method from client_type if not explicitly set
|
|
856
|
-
const authMethod = clientDef.token_endpoint_auth_method ??
|
|
857
|
-
(clientDef.client_type === 'public' ? 'none' : 'client_secret_post');
|
|
858
|
-
const { rows: insertedRows } = await client.query(`INSERT INTO clients (client_id, client_name, organization_id, application_id, client_type,
|
|
859
|
-
application_type, grant_types, redirect_uris, response_types, scope, login_methods,
|
|
860
|
-
token_endpoint_auth_method, post_logout_redirect_uris, allowed_origins, require_pkce, status)
|
|
861
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, 'active') RETURNING id`, [
|
|
862
|
-
clientId,
|
|
863
|
-
clientDef.client_name,
|
|
864
|
-
orgId,
|
|
865
|
-
appId,
|
|
866
|
-
clientDef.client_type,
|
|
867
|
-
clientDef.application_type ?? 'web',
|
|
868
|
-
clientDef.grant_types ?? ['authorization_code'],
|
|
869
|
-
clientDef.redirect_uris ?? [],
|
|
870
|
-
clientDef.response_types ?? ['code'],
|
|
871
|
-
clientDef.scope ?? 'openid',
|
|
872
|
-
clientDef.login_methods ?? null,
|
|
873
|
-
authMethod,
|
|
874
|
-
clientDef.post_logout_redirect_uris ?? [],
|
|
875
|
-
clientDef.allowed_origins ?? [],
|
|
876
|
-
clientDef.require_pkce ?? true,
|
|
877
|
-
]);
|
|
878
|
-
const clientDbId = insertedRows[0].id;
|
|
879
|
-
// Generate secret for confidential clients within the same transaction
|
|
880
|
-
let secretPlaintext;
|
|
881
|
-
let secretId;
|
|
882
|
-
if (clientDef.client_type === 'confidential') {
|
|
883
|
-
const plaintext = generateSecret();
|
|
884
|
-
const secretHash = await hashSecret(plaintext);
|
|
885
|
-
const secretSha256 = sha256Secret(plaintext);
|
|
886
|
-
const secretResult = await client.query(`INSERT INTO client_secrets (client_id, secret_hash, secret_sha256, label, expires_at)
|
|
887
|
-
VALUES ($1, $2, $3, $4, $5) RETURNING id`, [
|
|
888
|
-
clientDbId,
|
|
889
|
-
secretHash,
|
|
890
|
-
secretSha256,
|
|
891
|
-
clientDef.secret_label ?? null,
|
|
892
|
-
clientDef.secret_expires_at ?? null,
|
|
893
|
-
]);
|
|
894
|
-
secretPlaintext = plaintext;
|
|
895
|
-
secretId = secretResult.rows[0].id;
|
|
896
|
-
}
|
|
897
|
-
result.created.push({
|
|
898
|
-
type: 'client',
|
|
899
|
-
slug: clientDef.client_name,
|
|
900
|
-
name: clientDef.client_name,
|
|
901
|
-
});
|
|
902
|
-
// Report credentials — secret only present for newly created confidential clients
|
|
903
|
-
result.credentials.push({
|
|
904
|
-
clientName: clientDef.client_name,
|
|
905
|
-
clientId,
|
|
906
|
-
clientType: clientDef.client_type,
|
|
907
|
-
...(secretPlaintext ? { secretPlaintext } : {}),
|
|
908
|
-
...(secretId ? { secretId } : {}),
|
|
909
|
-
...(clientDef.secret_label ? { secretLabel: clientDef.secret_label } : {}),
|
|
910
|
-
...(clientDef.secret_expires_at ? { secretExpiresAt: clientDef.secret_expires_at } : {}),
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
catch (err) {
|
|
915
|
-
throw normalizeImportFailure(err);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
/**
|
|
919
|
-
* Process a single role from the manifest.
|
|
920
|
-
*/
|
|
921
|
-
async function processRole(client, role, mode, result, orgMap, appMap, roleMap) {
|
|
922
|
-
try {
|
|
923
|
-
const appId = await resolveAppId(client, role.organization_slug, role.application_slug, orgMap, appMap);
|
|
924
|
-
if (!appId) {
|
|
925
|
-
result.errors.push({
|
|
926
|
-
type: 'role',
|
|
927
|
-
slug: role.slug,
|
|
928
|
-
error: `Parent application '${role.application_slug}' not found`,
|
|
929
|
-
});
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
const { rows: existing } = await client.query('SELECT id, name, description FROM roles WHERE slug = $1 AND application_id = $2', [role.slug, appId]);
|
|
933
|
-
const roleKey = naturalKey(role.organization_slug, role.application_slug, role.slug);
|
|
934
|
-
if (existing.length > 0) {
|
|
935
|
-
roleMap.set(roleKey, existing[0].id);
|
|
936
|
-
const changedFields = [
|
|
937
|
-
existing[0].name !== role.name ? 'name' : null,
|
|
938
|
-
existing[0].description !== (role.description ?? null) ? 'description' : null,
|
|
939
|
-
].filter((field) => field !== null);
|
|
940
|
-
if (mode === 'merge' || changedFields.length === 0) {
|
|
941
|
-
result.skipped.push({
|
|
942
|
-
type: 'role',
|
|
943
|
-
slug: role.slug,
|
|
944
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
945
|
-
});
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
if (mode === 'dry-run') {
|
|
949
|
-
result.updated.push({
|
|
950
|
-
type: 'role',
|
|
951
|
-
slug: role.slug,
|
|
952
|
-
name: role.name,
|
|
953
|
-
changes: changedFields,
|
|
954
|
-
});
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
await client.query(`UPDATE roles SET name = $1, description = $2, updated_at = NOW() WHERE id = $3`, [role.name, role.description ?? null, existing[0].id]);
|
|
958
|
-
result.updated.push({
|
|
959
|
-
type: 'role',
|
|
960
|
-
slug: role.slug,
|
|
961
|
-
name: role.name,
|
|
962
|
-
changes: changedFields,
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
else {
|
|
966
|
-
if (mode === 'dry-run') {
|
|
967
|
-
result.created.push({ type: 'role', slug: role.slug, name: role.name });
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
const { rows } = await client.query(`INSERT INTO roles (name, slug, application_id, description)
|
|
971
|
-
VALUES ($1, $2, $3, $4) RETURNING id`, [role.name, role.slug, appId, role.description ?? null]);
|
|
972
|
-
roleMap.set(roleKey, rows[0].id);
|
|
973
|
-
result.created.push({ type: 'role', slug: role.slug, name: role.name });
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
catch (err) {
|
|
977
|
-
throw normalizeImportFailure(err);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* Process a single permission from the manifest.
|
|
982
|
-
*/
|
|
983
|
-
async function processPermission(client, perm, mode, result, orgMap, appMap, permissionMap) {
|
|
984
|
-
try {
|
|
985
|
-
const appId = await resolveAppId(client, perm.organization_slug, perm.application_slug, orgMap, appMap);
|
|
986
|
-
if (!appId) {
|
|
987
|
-
result.errors.push({
|
|
988
|
-
type: 'permission',
|
|
989
|
-
slug: perm.slug,
|
|
990
|
-
error: `Parent application '${perm.application_slug}' not found`,
|
|
991
|
-
});
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
const { rows: existing } = await client.query('SELECT id, name, description FROM permissions WHERE slug = $1 AND application_id = $2', [perm.slug, appId]);
|
|
995
|
-
const permissionKey = naturalKey(perm.organization_slug, perm.application_slug, perm.slug);
|
|
996
|
-
if (existing.length > 0) {
|
|
997
|
-
permissionMap.set(permissionKey, existing[0].id);
|
|
998
|
-
const changedFields = [
|
|
999
|
-
existing[0].name !== perm.name ? 'name' : null,
|
|
1000
|
-
existing[0].description !== (perm.description ?? null) ? 'description' : null,
|
|
1001
|
-
].filter((field) => field !== null);
|
|
1002
|
-
if (mode === 'merge' || changedFields.length === 0) {
|
|
1003
|
-
result.skipped.push({
|
|
1004
|
-
type: 'permission',
|
|
1005
|
-
slug: perm.slug,
|
|
1006
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
1007
|
-
});
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
if (mode === 'dry-run') {
|
|
1011
|
-
result.updated.push({
|
|
1012
|
-
type: 'permission',
|
|
1013
|
-
slug: perm.slug,
|
|
1014
|
-
name: perm.name,
|
|
1015
|
-
changes: changedFields,
|
|
1016
|
-
});
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
await client.query(`UPDATE permissions SET name = $1, description = $2 WHERE id = $3`, [
|
|
1020
|
-
perm.name,
|
|
1021
|
-
perm.description ?? null,
|
|
1022
|
-
existing[0].id,
|
|
1023
|
-
]);
|
|
1024
|
-
result.updated.push({
|
|
1025
|
-
type: 'permission',
|
|
1026
|
-
slug: perm.slug,
|
|
1027
|
-
name: perm.name,
|
|
1028
|
-
changes: changedFields,
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
else {
|
|
1032
|
-
if (mode === 'dry-run') {
|
|
1033
|
-
result.created.push({ type: 'permission', slug: perm.slug, name: perm.name });
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
const { rows } = await client.query(`INSERT INTO permissions (name, slug, application_id, description)
|
|
1037
|
-
VALUES ($1, $2, $3, $4) RETURNING id`, [perm.name, perm.slug, appId, perm.description ?? null]);
|
|
1038
|
-
permissionMap.set(permissionKey, rows[0].id);
|
|
1039
|
-
result.created.push({ type: 'permission', slug: perm.slug, name: perm.name });
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
catch (err) {
|
|
1043
|
-
throw normalizeImportFailure(err);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Process a single claim definition from the manifest.
|
|
1048
|
-
*/
|
|
1049
|
-
async function processClaimDefinition(client, claim, mode, result, orgMap, appMap, claimMap) {
|
|
1050
|
-
try {
|
|
1051
|
-
const appId = await resolveAppId(client, claim.organization_slug, claim.application_slug, orgMap, appMap);
|
|
1052
|
-
if (!appId) {
|
|
1053
|
-
result.errors.push({
|
|
1054
|
-
type: 'claim_definition',
|
|
1055
|
-
slug: claim.slug,
|
|
1056
|
-
error: `Parent application '${claim.application_slug}' not found`,
|
|
1057
|
-
});
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
// Table is custom_claim_definitions with claim_name column (unique per app)
|
|
1061
|
-
// The manifest slug maps to claim_name in the DB
|
|
1062
|
-
const { rows: existing } = await client.query(`SELECT id, description, claim_type FROM custom_claim_definitions
|
|
1063
|
-
WHERE claim_name = $1 AND application_id = $2`, [claim.slug, appId]);
|
|
1064
|
-
const claimKey = naturalKey(claim.organization_slug, claim.application_slug, claim.slug);
|
|
1065
|
-
if (existing.length > 0) {
|
|
1066
|
-
claimMap.set(claimKey, { id: existing[0].id, claimType: existing[0].claim_type });
|
|
1067
|
-
if (mode !== 'merge' && existing[0].claim_type !== claim.claim_type) {
|
|
1068
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
1069
|
-
}
|
|
1070
|
-
const changedFields = existing[0].description === (claim.description ?? null) ? [] : ['description'];
|
|
1071
|
-
if (mode === 'merge' || changedFields.length === 0) {
|
|
1072
|
-
result.skipped.push({
|
|
1073
|
-
type: 'claim_definition',
|
|
1074
|
-
slug: claim.slug,
|
|
1075
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
1076
|
-
});
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
if (mode === 'dry-run') {
|
|
1080
|
-
result.updated.push({
|
|
1081
|
-
type: 'claim_definition',
|
|
1082
|
-
slug: claim.slug,
|
|
1083
|
-
name: claim.name,
|
|
1084
|
-
changes: changedFields,
|
|
1085
|
-
});
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
await client.query(`UPDATE custom_claim_definitions SET description = $1, updated_at = NOW() WHERE id = $2`, [claim.description ?? null, existing[0].id]);
|
|
1089
|
-
result.updated.push({
|
|
1090
|
-
type: 'claim_definition',
|
|
1091
|
-
slug: claim.slug,
|
|
1092
|
-
name: claim.name,
|
|
1093
|
-
changes: changedFields,
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
else {
|
|
1097
|
-
if (mode === 'dry-run') {
|
|
1098
|
-
result.created.push({ type: 'claim_definition', slug: claim.slug, name: claim.name });
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
const { rows } = await client.query(`INSERT INTO custom_claim_definitions (claim_name, application_id, claim_type, description)
|
|
1102
|
-
VALUES ($1, $2, $3, $4) RETURNING id`, [claim.slug, appId, claim.claim_type, claim.description ?? null]);
|
|
1103
|
-
claimMap.set(claimKey, { id: rows[0].id, claimType: claim.claim_type });
|
|
1104
|
-
result.created.push({ type: 'claim_definition', slug: claim.slug, name: claim.name });
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
catch (err) {
|
|
1108
|
-
throw normalizeImportFailure(err);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
// ============================================================================
|
|
1112
|
-
// Relationship and system-configuration processors
|
|
1113
|
-
// ============================================================================
|
|
1114
|
-
/**
|
|
1115
|
-
* Process a single role-permission mapping from the manifest.
|
|
1116
|
-
*
|
|
1117
|
-
* Resolves the role and each permission by slug within the application,
|
|
1118
|
-
* then inserts the mapping rows. Uses ON CONFLICT DO NOTHING for
|
|
1119
|
-
* idempotent merge-mode behavior.
|
|
1120
|
-
*
|
|
1121
|
-
* @param client - Database client within the transaction
|
|
1122
|
-
* @param mapping - The role-permission mapping definition
|
|
1123
|
-
* @param mode - Import mode (merge, overwrite, dry-run)
|
|
1124
|
-
* @param result - Accumulator for import results
|
|
1125
|
-
* @param _orgMap - Organization slug→ID map (unused but kept for signature consistency)
|
|
1126
|
-
* @param appMap - Application "orgSlug:appSlug"→ID map
|
|
1127
|
-
*/
|
|
1128
|
-
async function processRolePermissionMapping(client, mapping, mode, result, _orgMap, appMap, roleMap, permissionMap) {
|
|
1129
|
-
try {
|
|
1130
|
-
// Resolve application ID from the composite key
|
|
1131
|
-
const appKey = `${mapping.organization_slug}:${mapping.application_slug}`;
|
|
1132
|
-
const appId = await resolveAppId(client, mapping.organization_slug, mapping.application_slug, _orgMap, appMap);
|
|
1133
|
-
if (!appId) {
|
|
1134
|
-
result.errors.push({
|
|
1135
|
-
type: 'role_permission_mapping',
|
|
1136
|
-
slug: mapping.role_slug,
|
|
1137
|
-
error: `Application not found: ${appKey}`,
|
|
1138
|
-
});
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
// Find role by slug within the application
|
|
1142
|
-
const roleKey = naturalKey(mapping.organization_slug, mapping.application_slug, mapping.role_slug);
|
|
1143
|
-
let roleId = roleMap.get(roleKey);
|
|
1144
|
-
if (!roleId) {
|
|
1145
|
-
const roleResult = await client.query('SELECT id FROM roles WHERE slug = $1 AND application_id = $2', [mapping.role_slug, appId]);
|
|
1146
|
-
roleId = roleResult.rows[0]?.id;
|
|
1147
|
-
if (roleId)
|
|
1148
|
-
roleMap.set(roleKey, roleId);
|
|
1149
|
-
}
|
|
1150
|
-
if (!roleId) {
|
|
1151
|
-
result.errors.push({
|
|
1152
|
-
type: 'role_permission_mapping',
|
|
1153
|
-
slug: mapping.role_slug,
|
|
1154
|
-
error: `Role not found: ${mapping.role_slug}`,
|
|
1155
|
-
});
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
// Process each permission in the mapping
|
|
1159
|
-
for (const permSlug of mapping.permission_slugs) {
|
|
1160
|
-
const permissionKey = naturalKey(mapping.organization_slug, mapping.application_slug, permSlug);
|
|
1161
|
-
let permId = permissionMap.get(permissionKey);
|
|
1162
|
-
if (!permId) {
|
|
1163
|
-
const permResult = await client.query('SELECT id FROM permissions WHERE slug = $1 AND application_id = $2', [permSlug, appId]);
|
|
1164
|
-
permId = permResult.rows[0]?.id;
|
|
1165
|
-
if (permId)
|
|
1166
|
-
permissionMap.set(permissionKey, permId);
|
|
1167
|
-
}
|
|
1168
|
-
if (!permId) {
|
|
1169
|
-
result.errors.push({
|
|
1170
|
-
type: 'role_permission_mapping',
|
|
1171
|
-
slug: `${mapping.role_slug}→${permSlug}`,
|
|
1172
|
-
error: `Permission not found: ${permSlug}`,
|
|
1173
|
-
});
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
if (mode === 'dry-run' &&
|
|
1177
|
-
(roleId === plannedEntityId('role', roleKey) ||
|
|
1178
|
-
permId === plannedEntityId('permission', permissionKey))) {
|
|
1179
|
-
result.created.push({
|
|
1180
|
-
type: 'role_permission_mapping',
|
|
1181
|
-
slug: `${mapping.role_slug}→${permSlug}`,
|
|
1182
|
-
name: `${mapping.role_slug} → ${permSlug}`,
|
|
1183
|
-
});
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
const existingMapping = await client.query('SELECT 1 FROM role_permissions WHERE role_id = $1 AND permission_id = $2', [roleId, permId]);
|
|
1187
|
-
if (existingMapping.rowCount !== 0) {
|
|
1188
|
-
result.skipped.push({
|
|
1189
|
-
type: 'role_permission_mapping',
|
|
1190
|
-
slug: `${mapping.role_slug}→${permSlug}`,
|
|
1191
|
-
reason: 'Relationship already exists',
|
|
1192
|
-
});
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
if (mode === 'dry-run') {
|
|
1196
|
-
result.created.push({
|
|
1197
|
-
type: 'role_permission_mapping',
|
|
1198
|
-
slug: `${mapping.role_slug}→${permSlug}`,
|
|
1199
|
-
name: `${mapping.role_slug} → ${permSlug}`,
|
|
1200
|
-
});
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
// Insert mapping with ON CONFLICT DO NOTHING for idempotent merge behavior
|
|
1204
|
-
await client.query(`INSERT INTO role_permissions (role_id, permission_id)
|
|
1205
|
-
VALUES ($1, $2)
|
|
1206
|
-
ON CONFLICT (role_id, permission_id) DO NOTHING`, [roleId, permId]);
|
|
1207
|
-
result.created.push({
|
|
1208
|
-
type: 'role_permission_mapping',
|
|
1209
|
-
slug: `${mapping.role_slug}→${permSlug}`,
|
|
1210
|
-
name: `${mapping.role_slug} → ${permSlug}`,
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
catch (err) {
|
|
1215
|
-
throw normalizeImportFailure(err);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
/**
|
|
1219
|
-
* Process a single system config override from the manifest.
|
|
1220
|
-
*
|
|
1221
|
-
* Only updates existing config keys — does not create new ones.
|
|
1222
|
-
* This prevents typos from silently creating invalid config entries.
|
|
1223
|
-
* Only keys seeded by migration 011 (or added by future migrations) can be set.
|
|
1224
|
-
*
|
|
1225
|
-
* @param client - Database client within the transaction
|
|
1226
|
-
* @param key - Config key to update
|
|
1227
|
-
* @param value - New value (string, number, or boolean — stored as JSONB)
|
|
1228
|
-
* @param mode - Import mode (merge, overwrite, dry-run)
|
|
1229
|
-
* @param result - Accumulator for import results
|
|
1230
|
-
*/
|
|
1231
|
-
async function processConfigOverride(client, key, value, mode, result) {
|
|
1232
|
-
try {
|
|
1233
|
-
// Check if config key exists — we only update, never create
|
|
1234
|
-
const existing = await client.query('SELECT id, value, value_type, is_sensitive FROM system_config WHERE key = $1', [key]);
|
|
1235
|
-
if (existing.rowCount === 0) {
|
|
1236
|
-
result.errors.push({
|
|
1237
|
-
type: 'config',
|
|
1238
|
-
slug: key,
|
|
1239
|
-
error: `Config key not found: ${key} (only existing keys can be updated)`,
|
|
1240
|
-
});
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if (existing.rows[0].is_sensitive === true ||
|
|
1244
|
-
!configValueMatchesType(value, existing.rows[0].value_type)) {
|
|
1245
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
1246
|
-
}
|
|
1247
|
-
const valueChanged = JSON.stringify(existing.rows[0].value) !== JSON.stringify(value);
|
|
1248
|
-
if (mode === 'merge' || !valueChanged) {
|
|
1249
|
-
result.skipped.push({
|
|
1250
|
-
type: 'config',
|
|
1251
|
-
slug: key,
|
|
1252
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
1253
|
-
});
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
if (mode === 'dry-run') {
|
|
1257
|
-
result.updated.push({
|
|
1258
|
-
type: 'config',
|
|
1259
|
-
slug: key,
|
|
1260
|
-
name: key,
|
|
1261
|
-
changes: ['value'],
|
|
1262
|
-
});
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
await client.query(`UPDATE system_config SET value = $1::jsonb, updated_at = NOW() WHERE key = $2`, [JSON.stringify(value), key]);
|
|
1266
|
-
result.updated.push({
|
|
1267
|
-
type: 'config',
|
|
1268
|
-
slug: key,
|
|
1269
|
-
name: key,
|
|
1270
|
-
changes: ['value'],
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
catch (err) {
|
|
1274
|
-
throw normalizeImportFailure(err);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
/** Validate a manifest value against the configuration row's persisted public type. */
|
|
1278
|
-
function configValueMatchesType(value, valueType) {
|
|
1279
|
-
switch (valueType) {
|
|
1280
|
-
case 'string':
|
|
1281
|
-
case 'duration':
|
|
1282
|
-
return typeof value === 'string';
|
|
1283
|
-
case 'number':
|
|
1284
|
-
return typeof value === 'number' && Number.isFinite(value);
|
|
1285
|
-
case 'boolean':
|
|
1286
|
-
return typeof value === 'boolean';
|
|
1287
|
-
case 'json':
|
|
1288
|
-
return true;
|
|
1289
|
-
default:
|
|
1290
|
-
return false;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
// ============================================================================
|
|
1294
|
-
// User, module, and assignment processors
|
|
1295
|
-
// ============================================================================
|
|
1296
|
-
/**
|
|
1297
|
-
* Process a single user from the manifest.
|
|
1298
|
-
* Users are matched by email and organization. Authentication secrets are never imported.
|
|
1299
|
-
*/
|
|
1300
|
-
async function processUser(client, user, mode, result, orgMap, userMap) {
|
|
1301
|
-
try {
|
|
1302
|
-
const orgId = await resolveOrgId(client, user.organization_slug, orgMap);
|
|
1303
|
-
if (!orgId) {
|
|
1304
|
-
result.errors.push({
|
|
1305
|
-
type: 'user',
|
|
1306
|
-
slug: user.email,
|
|
1307
|
-
error: `Parent organization '${user.organization_slug}' not found`,
|
|
1308
|
-
});
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
const { rows: existing } = await client.query(`SELECT id, given_name, family_name, locale, status, email_verified FROM users
|
|
1312
|
-
WHERE email = $1 AND organization_id = $2`, [user.email, orgId]);
|
|
1313
|
-
const userMapKey = `${user.organization_slug}:${user.email}`;
|
|
1314
|
-
if (existing.length > 0) {
|
|
1315
|
-
userMap.set(userMapKey, existing[0].id);
|
|
1316
|
-
if (mode !== 'merge' &&
|
|
1317
|
-
((user.status !== undefined && existing[0].status !== user.status) ||
|
|
1318
|
-
(user.email_verified !== undefined && existing[0].email_verified !== user.email_verified))) {
|
|
1319
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
1320
|
-
}
|
|
1321
|
-
const changedFields = [
|
|
1322
|
-
existing[0].given_name !== (user.given_name ?? null) ? 'given_name' : null,
|
|
1323
|
-
existing[0].family_name !== (user.family_name ?? null) ? 'family_name' : null,
|
|
1324
|
-
existing[0].locale !== (user.locale ?? null) ? 'locale' : null,
|
|
1325
|
-
].filter((field) => field !== null);
|
|
1326
|
-
if (mode === 'merge' || changedFields.length === 0) {
|
|
1327
|
-
result.skipped.push({
|
|
1328
|
-
type: 'user',
|
|
1329
|
-
slug: user.email,
|
|
1330
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
1331
|
-
});
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
if (mode === 'dry-run') {
|
|
1335
|
-
result.updated.push({
|
|
1336
|
-
type: 'user',
|
|
1337
|
-
slug: user.email,
|
|
1338
|
-
name: user.email,
|
|
1339
|
-
changes: changedFields,
|
|
1340
|
-
});
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
await client.query(`UPDATE users SET given_name = $1, family_name = $2, locale = $3,
|
|
1344
|
-
updated_at = NOW() WHERE id = $4`, [user.given_name ?? null, user.family_name ?? null, user.locale ?? null, existing[0].id]);
|
|
1345
|
-
result.updated.push({
|
|
1346
|
-
type: 'user',
|
|
1347
|
-
slug: user.email,
|
|
1348
|
-
name: user.email,
|
|
1349
|
-
changes: changedFields,
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
else {
|
|
1353
|
-
if (mode === 'dry-run') {
|
|
1354
|
-
result.created.push({ type: 'user', slug: user.email, name: user.email });
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
const { rows } = await client.query(`INSERT INTO users (email, organization_id, given_name, family_name, locale,
|
|
1358
|
-
email_verified, status)
|
|
1359
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, [
|
|
1360
|
-
user.email,
|
|
1361
|
-
orgId,
|
|
1362
|
-
user.given_name ?? null,
|
|
1363
|
-
user.family_name ?? null,
|
|
1364
|
-
user.locale ?? null,
|
|
1365
|
-
user.email_verified ?? false,
|
|
1366
|
-
user.status ?? 'active',
|
|
1367
|
-
]);
|
|
1368
|
-
userMap.set(userMapKey, rows[0].id);
|
|
1369
|
-
result.created.push({ type: 'user', slug: user.email, name: user.email });
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
catch (err) {
|
|
1373
|
-
throw normalizeImportFailure(err);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Process a single application module from the manifest.
|
|
1378
|
-
* Matched by slug + application.
|
|
1379
|
-
*/
|
|
1380
|
-
async function processApplicationModule(client, mod, mode, result, orgMap, appMap) {
|
|
1381
|
-
try {
|
|
1382
|
-
const appId = await resolveAppId(client, mod.organization_slug, mod.application_slug, orgMap, appMap);
|
|
1383
|
-
if (!appId) {
|
|
1384
|
-
result.errors.push({
|
|
1385
|
-
type: 'application_module',
|
|
1386
|
-
slug: mod.slug,
|
|
1387
|
-
error: `Parent application '${mod.application_slug}' not found`,
|
|
1388
|
-
});
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
const { rows: existing } = await client.query(`SELECT id, name, description, status FROM application_modules
|
|
1392
|
-
WHERE slug = $1 AND application_id = $2`, [mod.slug, appId]);
|
|
1393
|
-
if (existing.length > 0) {
|
|
1394
|
-
if (mode !== 'merge' && mod.status !== undefined && existing[0].status !== mod.status) {
|
|
1395
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
1396
|
-
}
|
|
1397
|
-
const changedFields = [
|
|
1398
|
-
existing[0].name !== mod.name ? 'name' : null,
|
|
1399
|
-
existing[0].description !== (mod.description ?? null) ? 'description' : null,
|
|
1400
|
-
].filter((field) => field !== null);
|
|
1401
|
-
if (mode === 'merge' || changedFields.length === 0) {
|
|
1402
|
-
result.skipped.push({
|
|
1403
|
-
type: 'application_module',
|
|
1404
|
-
slug: mod.slug,
|
|
1405
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
1406
|
-
});
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (mode === 'dry-run') {
|
|
1410
|
-
result.updated.push({
|
|
1411
|
-
type: 'application_module',
|
|
1412
|
-
slug: mod.slug,
|
|
1413
|
-
name: mod.name,
|
|
1414
|
-
changes: changedFields,
|
|
1415
|
-
});
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
await client.query(`UPDATE application_modules SET name = $1, description = $2, updated_at = NOW()
|
|
1419
|
-
WHERE id = $3`, [mod.name, mod.description ?? null, existing[0].id]);
|
|
1420
|
-
result.updated.push({
|
|
1421
|
-
type: 'application_module',
|
|
1422
|
-
slug: mod.slug,
|
|
1423
|
-
name: mod.name,
|
|
1424
|
-
changes: changedFields,
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
else {
|
|
1428
|
-
if (mode === 'dry-run') {
|
|
1429
|
-
result.created.push({ type: 'application_module', slug: mod.slug, name: mod.name });
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
await client.query(`INSERT INTO application_modules (name, slug, application_id, description, status)
|
|
1433
|
-
VALUES ($1, $2, $3, $4, $5)`, [mod.name, mod.slug, appId, mod.description ?? null, mod.status ?? 'active']);
|
|
1434
|
-
result.created.push({ type: 'application_module', slug: mod.slug, name: mod.name });
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
catch (err) {
|
|
1438
|
-
throw normalizeImportFailure(err);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
/**
|
|
1442
|
-
* Process a user-role assignment. Resolves user + role by slug/email.
|
|
1443
|
-
* Uses ON CONFLICT DO NOTHING for idempotent behavior.
|
|
1444
|
-
*/
|
|
1445
|
-
async function processUserRoleAssignment(client, assignment, mode, result, orgMap, appMap, userMap, roleMap) {
|
|
1446
|
-
try {
|
|
1447
|
-
const userMapKey = `${assignment.organization_slug}:${assignment.email}`;
|
|
1448
|
-
let userId = userMap.get(userMapKey);
|
|
1449
|
-
if (!userId) {
|
|
1450
|
-
const orgId = await resolveOrgId(client, assignment.organization_slug, orgMap);
|
|
1451
|
-
if (orgId) {
|
|
1452
|
-
const { rows } = await client.query('SELECT id FROM users WHERE email = $1 AND organization_id = $2', [assignment.email, orgId]);
|
|
1453
|
-
userId = rows[0]?.id;
|
|
1454
|
-
if (userId)
|
|
1455
|
-
userMap.set(userMapKey, userId);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
if (!userId) {
|
|
1459
|
-
result.errors.push({
|
|
1460
|
-
type: 'user_role_assignment',
|
|
1461
|
-
slug: `${assignment.email}→${assignment.role_slug}`,
|
|
1462
|
-
error: `User not found: ${assignment.email} in org ${assignment.organization_slug}`,
|
|
1463
|
-
});
|
|
1464
|
-
return;
|
|
1465
|
-
}
|
|
1466
|
-
const appId = await resolveAppId(client, assignment.organization_slug, assignment.application_slug, orgMap, appMap);
|
|
1467
|
-
if (!appId) {
|
|
1468
|
-
result.errors.push({
|
|
1469
|
-
type: 'user_role_assignment',
|
|
1470
|
-
slug: `${assignment.email}→${assignment.role_slug}`,
|
|
1471
|
-
error: `Application not found: ${assignment.application_slug}`,
|
|
1472
|
-
});
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
const roleKey = naturalKey(assignment.organization_slug, assignment.application_slug, assignment.role_slug);
|
|
1476
|
-
let roleId = roleMap.get(roleKey);
|
|
1477
|
-
if (!roleId) {
|
|
1478
|
-
const { rows } = await client.query('SELECT id FROM roles WHERE slug = $1 AND application_id = $2', [assignment.role_slug, appId]);
|
|
1479
|
-
roleId = rows[0]?.id;
|
|
1480
|
-
if (roleId)
|
|
1481
|
-
roleMap.set(roleKey, roleId);
|
|
1482
|
-
}
|
|
1483
|
-
if (!roleId) {
|
|
1484
|
-
result.errors.push({
|
|
1485
|
-
type: 'user_role_assignment',
|
|
1486
|
-
slug: `${assignment.email}→${assignment.role_slug}`,
|
|
1487
|
-
error: `Role not found: ${assignment.role_slug}`,
|
|
1488
|
-
});
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
if (mode === 'dry-run' &&
|
|
1492
|
-
(userId ===
|
|
1493
|
-
plannedEntityId('user', `${assignment.organization_slug}:${assignment.email.toLowerCase()}`) ||
|
|
1494
|
-
roleId === plannedEntityId('role', roleKey))) {
|
|
1495
|
-
result.created.push({
|
|
1496
|
-
type: 'user_role_assignment',
|
|
1497
|
-
slug: `${assignment.email}→${assignment.role_slug}`,
|
|
1498
|
-
name: `${assignment.email} → ${assignment.role_slug}`,
|
|
1499
|
-
});
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
const existingAssignment = await client.query('SELECT 1 FROM user_roles WHERE user_id = $1 AND role_id = $2', [userId, roleId]);
|
|
1503
|
-
if (existingAssignment.rowCount !== 0) {
|
|
1504
|
-
result.skipped.push({
|
|
1505
|
-
type: 'user_role_assignment',
|
|
1506
|
-
slug: `${assignment.email}→${assignment.role_slug}`,
|
|
1507
|
-
reason: 'Relationship already exists',
|
|
1508
|
-
});
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
if (mode === 'dry-run') {
|
|
1512
|
-
result.created.push({
|
|
1513
|
-
type: 'user_role_assignment',
|
|
1514
|
-
slug: `${assignment.email}→${assignment.role_slug}`,
|
|
1515
|
-
name: `${assignment.email} → ${assignment.role_slug}`,
|
|
1516
|
-
});
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
await client.query(`INSERT INTO user_roles (user_id, role_id, assigned_by)
|
|
1520
|
-
VALUES ($1, $2, $3)
|
|
1521
|
-
ON CONFLICT (user_id, role_id) DO NOTHING`, [userId, roleId, null]);
|
|
1522
|
-
result.created.push({
|
|
1523
|
-
type: 'user_role_assignment',
|
|
1524
|
-
slug: `${assignment.email}→${assignment.role_slug}`,
|
|
1525
|
-
name: `${assignment.email} → ${assignment.role_slug}`,
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
catch (err) {
|
|
1529
|
-
throw normalizeImportFailure(err);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
/**
|
|
1533
|
-
* Process a user claim value. Resolves user + claim definition by slug.
|
|
1534
|
-
* Uses ON CONFLICT DO UPDATE for upsert behavior.
|
|
1535
|
-
*/
|
|
1536
|
-
async function processUserClaimValue(client, claimValue, mode, result, orgMap, appMap, userMap, claimMap) {
|
|
1537
|
-
try {
|
|
1538
|
-
const userMapKey = `${claimValue.organization_slug}:${claimValue.email}`;
|
|
1539
|
-
let userId = userMap.get(userMapKey);
|
|
1540
|
-
if (!userId) {
|
|
1541
|
-
const orgId = await resolveOrgId(client, claimValue.organization_slug, orgMap);
|
|
1542
|
-
if (orgId) {
|
|
1543
|
-
const { rows } = await client.query('SELECT id FROM users WHERE email = $1 AND organization_id = $2', [claimValue.email, orgId]);
|
|
1544
|
-
userId = rows[0]?.id;
|
|
1545
|
-
if (userId)
|
|
1546
|
-
userMap.set(userMapKey, userId);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
if (!userId) {
|
|
1550
|
-
result.errors.push({
|
|
1551
|
-
type: 'user_claim_value',
|
|
1552
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1553
|
-
error: `User not found: ${claimValue.email} in org ${claimValue.organization_slug}`,
|
|
1554
|
-
});
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
const appId = await resolveAppId(client, claimValue.organization_slug, claimValue.application_slug, orgMap, appMap);
|
|
1558
|
-
if (!appId) {
|
|
1559
|
-
result.errors.push({
|
|
1560
|
-
type: 'user_claim_value',
|
|
1561
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1562
|
-
error: `Application not found: ${claimValue.application_slug}`,
|
|
1563
|
-
});
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
const claimKey = naturalKey(claimValue.organization_slug, claimValue.application_slug, claimValue.claim_slug);
|
|
1567
|
-
let claimIdentity = claimMap.get(claimKey);
|
|
1568
|
-
if (!claimIdentity) {
|
|
1569
|
-
const { rows } = await client.query('SELECT id, claim_type FROM custom_claim_definitions WHERE claim_name = $1 AND application_id = $2', [claimValue.claim_slug, appId]);
|
|
1570
|
-
const row = rows[0];
|
|
1571
|
-
if (row) {
|
|
1572
|
-
claimIdentity = { id: row.id, claimType: row.claim_type };
|
|
1573
|
-
claimMap.set(claimKey, claimIdentity);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
if (!claimIdentity) {
|
|
1577
|
-
result.errors.push({
|
|
1578
|
-
type: 'user_claim_value',
|
|
1579
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1580
|
-
error: `Claim definition not found: ${claimValue.claim_slug}`,
|
|
1581
|
-
});
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
if (!validateClaimValue(claimIdentity.claimType, claimValue.value).valid) {
|
|
1585
|
-
throw new ImportOperationError('import_plan_rejected', 409);
|
|
1586
|
-
}
|
|
1587
|
-
if (mode === 'dry-run' &&
|
|
1588
|
-
(userId ===
|
|
1589
|
-
plannedEntityId('user', `${claimValue.organization_slug}:${claimValue.email.toLowerCase()}`) ||
|
|
1590
|
-
claimIdentity.id === plannedEntityId('claim_definition', claimKey))) {
|
|
1591
|
-
result.created.push({
|
|
1592
|
-
type: 'user_claim_value',
|
|
1593
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1594
|
-
name: `${claimValue.email} → ${claimValue.claim_slug}`,
|
|
1595
|
-
});
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
const existingValue = await client.query('SELECT value FROM custom_claim_values WHERE user_id = $1 AND claim_id = $2', [userId, claimIdentity.id]);
|
|
1599
|
-
if (existingValue.rowCount !== 0) {
|
|
1600
|
-
const valueChanged = JSON.stringify(existingValue.rows[0].value) !== JSON.stringify(claimValue.value);
|
|
1601
|
-
if (mode === 'merge' || !valueChanged) {
|
|
1602
|
-
result.skipped.push({
|
|
1603
|
-
type: 'user_claim_value',
|
|
1604
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1605
|
-
reason: mode === 'merge' ? 'Already exists' : 'No mutable changes',
|
|
1606
|
-
});
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
if (mode === 'dry-run') {
|
|
1610
|
-
result.updated.push({
|
|
1611
|
-
type: 'user_claim_value',
|
|
1612
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1613
|
-
name: `${claimValue.email} → ${claimValue.claim_slug}`,
|
|
1614
|
-
changes: ['value'],
|
|
1615
|
-
});
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
await client.query('UPDATE custom_claim_values SET value = $1 WHERE user_id = $2 AND claim_id = $3', [JSON.stringify(claimValue.value), userId, claimIdentity.id]);
|
|
1619
|
-
result.updated.push({
|
|
1620
|
-
type: 'user_claim_value',
|
|
1621
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1622
|
-
name: `${claimValue.email} → ${claimValue.claim_slug}`,
|
|
1623
|
-
changes: ['value'],
|
|
1624
|
-
});
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
if (mode === 'dry-run') {
|
|
1628
|
-
result.created.push({
|
|
1629
|
-
type: 'user_claim_value',
|
|
1630
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1631
|
-
name: `${claimValue.email} → ${claimValue.claim_slug}`,
|
|
1632
|
-
});
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
await client.query('INSERT INTO custom_claim_values (user_id, claim_id, value) VALUES ($1, $2, $3)', [userId, claimIdentity.id, JSON.stringify(claimValue.value)]);
|
|
1636
|
-
result.created.push({
|
|
1637
|
-
type: 'user_claim_value',
|
|
1638
|
-
slug: `${claimValue.email}:${claimValue.claim_slug}`,
|
|
1639
|
-
name: `${claimValue.email} → ${claimValue.claim_slug}`,
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
catch (err) {
|
|
1643
|
-
throw normalizeImportFailure(err);
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
// ============================================================================
|
|
1647
|
-
// Helpers
|
|
1648
|
-
// ============================================================================
|
|
1649
|
-
/** Preserve closed import failures and collapse dependency diagnostics into one public category. */
|
|
1650
|
-
function normalizeImportFailure(error) {
|
|
1651
|
-
return error instanceof ImportOperationError
|
|
1652
|
-
? error
|
|
1653
|
-
: new ImportOperationError('import_execution_failed', 503);
|
|
1654
|
-
}
|
|
1655
|
-
/** Build a deterministic UUID-shaped identity that can never escape a rolled-back preview. */
|
|
1656
|
-
function plannedEntityId(kind, naturalKeyValue) {
|
|
1657
|
-
const digest = createHash('sha256')
|
|
1658
|
-
.update(`porta-import-preview\0${kind}\0${naturalKeyValue}`)
|
|
1659
|
-
.digest('hex');
|
|
1660
|
-
return `${digest.slice(0, 8)}-${digest.slice(8, 12)}-4${digest.slice(13, 16)}-8${digest.slice(17, 20)}-${digest.slice(20, 32)}`;
|
|
1661
|
-
}
|
|
1662
|
-
/**
|
|
1663
|
-
* Resolve an organization slug to its ID.
|
|
1664
|
-
* First checks the in-memory map (for entities created during this import),
|
|
1665
|
-
* then falls back to a database lookup (for pre-existing entities).
|
|
1666
|
-
*/
|
|
1667
|
-
async function resolveOrgId(client, slug, orgMap) {
|
|
1668
|
-
if (orgMap.has(slug))
|
|
1669
|
-
return orgMap.get(slug);
|
|
1670
|
-
const { rows } = await client.query('SELECT id FROM organizations WHERE slug = $1', [slug]);
|
|
1671
|
-
if (rows.length > 0) {
|
|
1672
|
-
orgMap.set(slug, rows[0].id);
|
|
1673
|
-
return rows[0].id;
|
|
1674
|
-
}
|
|
1675
|
-
return null;
|
|
1676
|
-
}
|
|
1677
|
-
/**
|
|
1678
|
-
* Resolve an application by org slug + app slug to its ID.
|
|
1679
|
-
*/
|
|
1680
|
-
async function resolveAppId(client, orgSlug, appSlug, orgMap, appMap) {
|
|
1681
|
-
const mapKey = `${orgSlug}:${appSlug}`;
|
|
1682
|
-
if (appMap.has(mapKey))
|
|
1683
|
-
return appMap.get(mapKey);
|
|
1684
|
-
const orgId = await resolveOrgId(client, orgSlug, orgMap);
|
|
1685
|
-
if (!orgId)
|
|
1686
|
-
return null;
|
|
1687
|
-
const { rows } = await client.query('SELECT id FROM applications WHERE slug = $1', [appSlug]);
|
|
1688
|
-
if (rows.length > 0) {
|
|
1689
|
-
appMap.set(mapKey, rows[0].id);
|
|
1690
|
-
return rows[0].id;
|
|
1691
|
-
}
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
//# sourceMappingURL=data-import.js.map
|