@flowdular/sdk 0.2.4 → 0.3.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/.ai/agents/sandbox/business-manager.md +5 -1
- package/.ai/blueprints/author-spec/README.md +1 -1
- package/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
- package/.ai/blueprints/author-spec/steps.yaml +5 -5
- package/.ai/blueprints/author-spec/templates/module.yaml +99 -3
- package/.ai/blueprints/edit-module/gates.yaml +4 -0
- package/.ai/blueprints/edit-module/required-files.yaml +9 -0
- package/.ai/blueprints/new-module/gates.yaml +4 -0
- package/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
- package/.ai/blueprints/release/gates.yaml +4 -0
- package/.ai/platform-capabilities.md +128 -0
- package/.ai/policies/capabilities.yaml +100 -0
- package/.ai/policies/path-ownership.yaml +5 -2
- package/.ai/policies/task-budgets.yaml +5 -3
- package/.ai/references/catalog/module.json +4 -4
- package/.ai/references/catalog/package.json +2 -2
- package/.ai/references/catalog/spec/module.yaml +5 -3
- package/.ai/references/catalog/src/platform.ts +2 -0
- package/.ai/references/catalog/src/services/catalog-service.ts +89 -1
- package/.ai/references/catalog/src/services/data-classes.ts +47 -0
- package/.ai/references/catalog/src/services/database-repository.ts +98 -1
- package/.ai/references/catalog/src/services/repository.ts +22 -1
- package/.ai/references/catalog/tests/data-classes.test.ts +157 -0
- package/.ai/references/catalog.provenance.json +12 -10
- package/.ai/skills/README.md +10 -0
- package/.ai/skills/agent-tool-design/SKILL.md +1 -2
- package/.ai/skills/auth-security-review/SKILL.md +1 -1
- package/.ai/skills/business-agent-design/SKILL.md +0 -1
- package/.ai/skills/deploy-operate/SKILL.md +114 -0
- package/.ai/skills/module-new/SKILL.md +29 -3
- package/.ai/skills/module-update/SKILL.md +9 -3
- package/.ai/skills/perf-audit/SKILL.md +0 -1
- package/.ai/skills/release-eject-pr/SKILL.md +0 -1
- package/.ai/skills/spec-interview/SKILL.md +120 -0
- package/.ai/skills/test-hardening/SKILL.md +1 -0
- package/.ai/skills/ux-design/SKILL.md +34 -3
- package/.ai/skills/variables/SKILL.md +0 -2
- package/.ai/skills/workflow-development/SKILL.md +0 -1
- package/AGENTS.md +4 -0
- package/docs/agent-contract.md +2 -2
- package/docs/design-system.md +185 -31
- package/modules/access/migrations/0001_access_core.down.sql +1 -0
- package/modules/access/migrations/0001_access_core.up.sql +28 -0
- package/modules/access/migrations/README.md +3 -0
- package/modules/access/module.json +33 -0
- package/modules/access/package.json +43 -0
- package/modules/access/spec/module.yaml +225 -0
- package/modules/access/src/acl/permissions.ts +6 -0
- package/modules/access/src/api/endpoints.ts +244 -0
- package/modules/access/src/client/ActivityView.tsrx +245 -0
- package/modules/access/src/client/AttestationsView.tsrx +176 -0
- package/modules/access/src/client/ReviewView.tsrx +586 -0
- package/modules/access/src/client/api.ts +132 -0
- package/modules/access/src/client/contribution.tsrx +41 -0
- package/modules/access/src/client/index.ts +41 -0
- package/modules/access/src/client/navigation.ts +57 -0
- package/modules/access/src/client/presentation.ts +69 -0
- package/modules/access/src/client/state.ts +72 -0
- package/modules/access/src/domain/types.ts +189 -0
- package/modules/access/src/index.ts +38 -0
- package/modules/access/src/platform.ts +60 -0
- package/modules/access/src/server/index.ts +42 -0
- package/modules/access/src/server/runtime.ts +105 -0
- package/modules/access/src/services/access-service.ts +298 -0
- package/modules/access/src/services/audit-window.ts +78 -0
- package/modules/access/src/services/auth-directory.ts +72 -0
- package/modules/access/src/services/changes.ts +91 -0
- package/modules/access/src/services/data-classes.ts +28 -0
- package/modules/access/src/services/database-repository.ts +185 -0
- package/modules/access/src/services/directory.ts +102 -0
- package/modules/access/src/services/index.ts +33 -0
- package/modules/access/src/services/list-exports.ts +150 -0
- package/modules/access/src/services/migration.ts +54 -0
- package/modules/access/src/services/repository.ts +30 -0
- package/modules/access/src/services/review.ts +178 -0
- package/modules/access/translations/en.json +155 -0
- package/modules/access/translations/pl.json +155 -0
- package/modules/agents/migrations/0022_credential_rotation_inventory.down.sql +2 -0
- package/modules/agents/migrations/0022_credential_rotation_inventory.up.sql +7 -0
- package/modules/agents/migrations/0023_agents_retention_indexes.down.sql +2 -0
- package/modules/agents/migrations/0023_agents_retention_indexes.up.sql +9 -0
- package/modules/agents/migrations/0024_agent_meter_refusals.down.sql +3 -0
- package/modules/agents/migrations/0024_agent_meter_refusals.up.sql +22 -0
- package/modules/agents/migrations/0025_agent_list_indexes.down.sql +3 -0
- package/modules/agents/migrations/0025_agent_list_indexes.up.sql +11 -0
- package/modules/agents/module.json +30 -4
- package/modules/agents/package.json +2 -1
- package/modules/agents/spec/module.yaml +63 -5
- package/modules/agents/src/api/endpoints.ts +291 -33
- package/modules/agents/src/cli/commands.json +11 -0
- package/modules/agents/src/cli/index.ts +45 -0
- package/modules/agents/src/client/AgentDashboardWidget.tsrx +19 -11
- package/modules/agents/src/client/AgentDefinitionList.tsrx +53 -10
- package/modules/agents/src/client/AgentPlaygroundView.tsrx +4 -4
- package/modules/agents/src/client/AgentProceduresView.tsrx +2 -2
- package/modules/agents/src/client/AgentRunsView.tsrx +143 -40
- package/modules/agents/src/client/AgentsView.tsrx +109 -33
- package/modules/agents/src/client/api.ts +99 -21
- package/modules/agents/src/client/state.ts +97 -0
- package/modules/agents/src/domain/types.ts +44 -4
- package/modules/agents/src/platform.ts +42 -0
- package/modules/agents/src/server/action-execution.ts +201 -106
- package/modules/agents/src/server/runtime.ts +32 -0
- package/modules/agents/src/services/agent-service.ts +158 -30
- package/modules/agents/src/services/credential-rotation.ts +185 -0
- package/modules/agents/src/services/credential-vault.ts +71 -46
- package/modules/agents/src/services/data-classes.ts +176 -0
- package/modules/agents/src/services/database-repository.ts +393 -101
- package/modules/agents/src/services/metering.ts +111 -0
- package/modules/agents/src/services/migration.ts +109 -0
- package/modules/agents/src/services/module-agent-preflight.ts +17 -9
- package/modules/agents/src/services/notifications.ts +98 -0
- package/modules/agents/src/services/provider-repository.ts +9 -18
- package/modules/agents/src/services/provider-service.ts +1 -9
- package/modules/agents/src/services/reports.ts +107 -0
- package/modules/agents/src/services/repository.ts +86 -6
- package/modules/agents/src/services/usage-service.ts +2 -0
- package/modules/agents/src/services/worker.ts +114 -4
- package/modules/agents/src/settings.ts +14 -3
- package/modules/agents/translations/en.json +14 -4
- package/modules/agents/translations/pl.json +14 -4
- package/modules/approvals/migrations/0001_approvals_core.down.sql +3 -0
- package/modules/approvals/migrations/0001_approvals_core.up.sql +78 -0
- package/modules/approvals/migrations/0002_approvals_expiry_routing_role.down.sql +2 -0
- package/modules/approvals/migrations/0002_approvals_expiry_routing_role.up.sql +19 -0
- package/modules/approvals/migrations/0003_approvals_retention_indexes.down.sql +6 -0
- package/modules/approvals/migrations/0003_approvals_retention_indexes.up.sql +14 -0
- package/modules/approvals/migrations/0004_approvals_inbox_keyset_indexes.down.sql +5 -0
- package/modules/approvals/migrations/0004_approvals_inbox_keyset_indexes.up.sql +12 -0
- package/modules/approvals/migrations/README.md +3 -0
- package/modules/approvals/module.json +34 -0
- package/modules/approvals/package.json +43 -0
- package/modules/approvals/spec/module.yaml +318 -0
- package/modules/approvals/src/acl/permissions.ts +7 -0
- package/modules/approvals/src/api/endpoints.ts +457 -0
- package/modules/approvals/src/client/ApprovalRequestDrawer.tsrx +138 -0
- package/modules/approvals/src/client/ApprovalsView.tsrx +562 -0
- package/modules/approvals/src/client/PendingCountWidget.tsrx +52 -0
- package/modules/approvals/src/client/api.ts +203 -0
- package/modules/approvals/src/client/approvals.css +51 -0
- package/modules/approvals/src/client/contribution.tsrx +44 -0
- package/modules/approvals/src/client/index.ts +26 -0
- package/modules/approvals/src/client/navigation.ts +39 -0
- package/modules/approvals/src/client/pending-count.ts +26 -0
- package/modules/approvals/src/client/presentation.ts +52 -0
- package/modules/approvals/src/client/state.ts +141 -0
- package/modules/approvals/src/domain/capability.ts +95 -0
- package/modules/approvals/src/domain/types.ts +154 -0
- package/modules/approvals/src/index.ts +52 -0
- package/modules/approvals/src/platform.ts +128 -0
- package/modules/approvals/src/server/index.ts +42 -0
- package/modules/approvals/src/server/runtime.ts +181 -0
- package/modules/approvals/src/services/approvals-service.ts +557 -0
- package/modules/approvals/src/services/callbacks.ts +75 -0
- package/modules/approvals/src/services/data-classes.ts +167 -0
- package/modules/approvals/src/services/database-repository.ts +724 -0
- package/modules/approvals/src/services/eligibility.ts +121 -0
- package/modules/approvals/src/services/expiry-runner.ts +81 -0
- package/modules/approvals/src/services/index.ts +31 -0
- package/modules/approvals/src/services/migration.ts +222 -0
- package/modules/approvals/src/services/notifications.ts +95 -0
- package/modules/approvals/src/services/repository.ts +161 -0
- package/modules/approvals/src/services/service-error.ts +70 -0
- package/modules/approvals/src/settings.ts +102 -0
- package/modules/approvals/translations/en.json +104 -0
- package/modules/approvals/translations/pl.json +104 -0
- package/modules/audit/migrations/0001_audit_core.down.sql +8 -0
- package/modules/audit/migrations/0001_audit_core.up.sql +107 -0
- package/modules/audit/migrations/0002_audit_sweep_routing_role.down.sql +2 -0
- package/modules/audit/migrations/0002_audit_sweep_routing_role.up.sql +21 -0
- package/modules/audit/migrations/0003_audit_export_request.down.sql +12 -0
- package/modules/audit/migrations/0003_audit_export_request.up.sql +40 -0
- package/modules/audit/migrations/0004_audit_hold_seal_erasure.down.sql +17 -0
- package/modules/audit/migrations/0004_audit_hold_seal_erasure.up.sql +192 -0
- package/modules/audit/migrations/0005_audit_event_format.down.sql +17 -0
- package/modules/audit/migrations/0005_audit_event_format.up.sql +60 -0
- package/modules/audit/migrations/0006_audit_own_class_walks.down.sql +3 -0
- package/modules/audit/migrations/0006_audit_own_class_walks.up.sql +14 -0
- package/modules/audit/migrations/README.md +3 -0
- package/modules/audit/module.json +32 -0
- package/modules/audit/package.json +49 -0
- package/modules/audit/spec/module.yaml +809 -0
- package/modules/audit/src/acl/permissions.ts +7 -0
- package/modules/audit/src/api/endpoints.ts +315 -0
- package/modules/audit/src/cli/commands.json +95 -0
- package/modules/audit/src/cli/index.ts +658 -0
- package/modules/audit/src/client/DataClassesView.tsrx +420 -0
- package/modules/audit/src/client/ExportsView.tsrx +256 -0
- package/modules/audit/src/client/HoldsView.tsrx +604 -0
- package/modules/audit/src/client/SweepsView.tsrx +251 -0
- package/modules/audit/src/client/api.ts +185 -0
- package/modules/audit/src/client/contribution.tsrx +52 -0
- package/modules/audit/src/client/index.ts +23 -0
- package/modules/audit/src/client/navigation.ts +74 -0
- package/modules/audit/src/client/presentation.ts +105 -0
- package/modules/audit/src/client/state.ts +86 -0
- package/modules/audit/src/domain/data-classes.ts +16 -0
- package/modules/audit/src/domain/types.ts +339 -0
- package/modules/audit/src/index.ts +76 -0
- package/modules/audit/src/platform.ts +44 -0
- package/modules/audit/src/server/index.ts +177 -0
- package/modules/audit/src/server/runtime.ts +303 -0
- package/modules/audit/src/services/anchor-key.ts +143 -0
- package/modules/audit/src/services/anchor-rotation.ts +147 -0
- package/modules/audit/src/services/audit-runners.ts +278 -0
- package/modules/audit/src/services/backup-guard.ts +95 -0
- package/modules/audit/src/services/database-repository.ts +1946 -0
- package/modules/audit/src/services/declared-classes.ts +64 -0
- package/modules/audit/src/services/erasure-port.ts +126 -0
- package/modules/audit/src/services/erasure-service.ts +770 -0
- package/modules/audit/src/services/export-directory.ts +106 -0
- package/modules/audit/src/services/export-service.ts +714 -0
- package/modules/audit/src/services/hold-service.ts +295 -0
- package/modules/audit/src/services/index.ts +17 -0
- package/modules/audit/src/services/migration.ts +658 -0
- package/modules/audit/src/services/own-classes.ts +318 -0
- package/modules/audit/src/services/platform-version.ts +23 -0
- package/modules/audit/src/services/repository.ts +495 -0
- package/modules/audit/src/services/retention-service.ts +191 -0
- package/modules/audit/src/services/seal-service.ts +769 -0
- package/modules/audit/src/services/service-error.ts +61 -0
- package/modules/audit/src/services/subject-keys.ts +90 -0
- package/modules/audit/src/services/sweep-service.ts +298 -0
- package/modules/audit/src/services/zip.ts +217 -0
- package/modules/audit/src/settings.ts +69 -0
- package/modules/audit/translations/en.json +210 -0
- package/modules/audit/translations/pl.json +210 -0
- package/modules/auth/README.md +77 -29
- package/modules/auth/migrations/0016_external_identities.up.sql +16 -0
- package/modules/auth/migrations/0017_mfa_key_id.up.sql +8 -0
- package/modules/auth/migrations/0018_retire_bundled_module_scopes.up.sql +29 -0
- package/modules/auth/migrations/0019_notifications_member_scopes.up.sql +44 -0
- package/modules/auth/migrations/0020_identity_providers.up.sql +61 -0
- package/modules/auth/migrations/0021_membership_status.up.sql +19 -0
- package/modules/auth/migrations/0022_auth_provider_scopes.up.sql +45 -0
- package/modules/auth/migrations/0023_enterprise_module_scopes.down.sql +19 -0
- package/modules/auth/migrations/0023_enterprise_module_scopes.up.sql +71 -0
- package/modules/auth/migrations/0024_approvals_member_scopes.down.sql +22 -0
- package/modules/auth/migrations/0024_approvals_member_scopes.up.sql +48 -0
- package/modules/auth/migrations/0025_audit_holds_owner_scope.down.sql +21 -0
- package/modules/auth/migrations/0025_audit_holds_owner_scope.up.sql +47 -0
- package/modules/auth/migrations/0026_auth_export_keyset_indexes.down.sql +6 -0
- package/modules/auth/migrations/0026_auth_export_keyset_indexes.up.sql +15 -0
- package/modules/auth/migrations/0027_workflow_automation_profile_scopes.down.sql +34 -0
- package/modules/auth/migrations/0027_workflow_automation_profile_scopes.up.sql +72 -0
- package/modules/auth/migrations/0028_auth_member_search_prefix_indexes.down.sql +6 -0
- package/modules/auth/migrations/0028_auth_member_search_prefix_indexes.up.sql +21 -0
- package/modules/auth/migrations/0029_reports_exports_access_scopes.down.sql +22 -0
- package/modules/auth/migrations/0029_reports_exports_access_scopes.up.sql +56 -0
- package/modules/auth/migrations/0030_auth_membership_keyset_index.down.sql +4 -0
- package/modules/auth/migrations/0030_auth_membership_keyset_index.up.sql +10 -0
- package/modules/auth/migrations/0031_membership_scope_backfills_under_rls.down.sql +4 -0
- package/modules/auth/migrations/0031_membership_scope_backfills_under_rls.up.sql +31 -0
- package/modules/auth/migrations/0032_audit_actor_service.down.sql +9 -0
- package/modules/auth/migrations/0032_audit_actor_service.up.sql +10 -0
- package/modules/auth/migrations/0033_auth_account_keyset_indexes.down.sql +5 -0
- package/modules/auth/migrations/0033_auth_account_keyset_indexes.up.sql +23 -0
- package/modules/auth/module.json +3 -3
- package/modules/auth/package.json +2 -1
- package/modules/auth/spec/module.yaml +372 -3
- package/modules/auth/src/acl/scopes.ts +50 -6
- package/modules/auth/src/cli/commands.json +11 -0
- package/modules/auth/src/cli/greenfield.ts +2 -2
- package/modules/auth/src/cli/index.ts +14 -0
- package/modules/auth/src/cli/secrets.ts +114 -0
- package/modules/auth/src/client/AuthenticationCore.tsrx +61 -8
- package/modules/auth/src/client/api.ts +72 -6
- package/modules/auth/src/client/contribution.tsrx +23 -0
- package/modules/auth/src/client/index.ts +6 -0
- package/modules/auth/src/client/providers/IdentityProviderForm.tsrx +191 -0
- package/modules/auth/src/client/providers/IdentityProvidersView.tsrx +358 -0
- package/modules/auth/src/client/providers/api.ts +128 -0
- package/modules/auth/src/client/providers/provider-columns.tsrx +60 -0
- package/modules/auth/src/client/providers/state.ts +35 -0
- package/modules/auth/src/client/security/SecurityView.tsrx +9 -1
- package/modules/auth/src/client/state.ts +15 -1
- package/modules/auth/src/domain/types.ts +55 -0
- package/modules/auth/src/index.ts +11 -1
- package/modules/auth/src/middleware/authentication.ts +4 -0
- package/modules/auth/src/server/endpoints.ts +467 -171
- package/modules/auth/src/server/http.ts +3 -2
- package/modules/auth/src/server/index.ts +22 -1
- package/modules/auth/src/server/membership-endpoints.ts +62 -0
- package/modules/auth/src/server/mfa-enforcement.ts +156 -0
- package/modules/auth/src/server/oidc.ts +389 -0
- package/modules/auth/src/server/provider-endpoints.ts +246 -0
- package/modules/auth/src/server/provider-host.ts +77 -0
- package/modules/auth/src/server/runtime.ts +231 -55
- package/modules/auth/src/services/auth-service-error.ts +7 -2
- package/modules/auth/src/services/auth-service.ts +948 -51
- package/modules/auth/src/services/data-classes.ts +236 -0
- package/modules/auth/src/services/database-repository.ts +853 -71
- package/modules/auth/src/services/identity-provider-service.ts +571 -0
- package/modules/auth/src/services/mail-delivery.ts +16 -16
- package/modules/auth/src/services/mail-message.ts +54 -0
- package/modules/auth/src/services/mail-port.ts +24 -0
- package/modules/auth/src/services/mail-smtp.ts +85 -0
- package/modules/auth/src/services/mfa-rotation.ts +144 -0
- package/modules/auth/src/services/migration.ts +810 -0
- package/modules/auth/src/services/password.ts +14 -0
- package/modules/auth/src/services/provider-secret-rotation.ts +169 -0
- package/modules/auth/src/services/provider-secrets.ts +136 -0
- package/modules/auth/src/services/repository.ts +321 -5
- package/modules/auth/src/services/session-sweep-runner.ts +73 -0
- package/modules/auth/src/services/settings-store.ts +10 -83
- package/modules/auth/src/services/totp.ts +88 -62
- package/modules/auth/src/services/validation.ts +194 -7
- package/modules/auth/src/settings.ts +18 -0
- package/modules/auth/translations/en.json +84 -1
- package/modules/auth/translations/pl.json +84 -1
- package/modules/automations/migrations/0005_secret_rotation_inventory.down.sql +1 -0
- package/modules/automations/migrations/0005_secret_rotation_inventory.up.sql +6 -0
- package/modules/automations/migrations/0006_automations_list_sort_indexes.down.sql +4 -0
- package/modules/automations/migrations/0006_automations_list_sort_indexes.up.sql +11 -0
- package/modules/automations/module.json +24 -6
- package/modules/automations/package.json +4 -1
- package/modules/automations/spec/module.yaml +81 -7
- package/modules/automations/src/api/endpoints.ts +202 -28
- package/modules/automations/src/cli/commands.json +17 -0
- package/modules/automations/src/cli/index.ts +114 -0
- package/modules/automations/src/client/AutomationScheduleForm.tsrx +77 -19
- package/modules/automations/src/client/AutomationSchedulesView.tsrx +144 -40
- package/modules/automations/src/client/AutomationTriggersView.tsrx +131 -32
- package/modules/automations/src/client/api.ts +62 -19
- package/modules/automations/src/client/presentation.ts +20 -2
- package/modules/automations/src/client/state.ts +84 -0
- package/modules/automations/src/domain/cadence.ts +99 -3
- package/modules/automations/src/domain/cron.ts +366 -0
- package/modules/automations/src/domain/time-zone.ts +61 -0
- package/modules/automations/src/platform.ts +38 -4
- package/modules/automations/src/server/index.ts +11 -0
- package/modules/automations/src/server/runtime.ts +46 -22
- package/modules/{automations-workflows-integration/src/server/adapter.ts → automations/src/server/workflow-target.ts} +13 -14
- package/modules/{automations-workflows-integration/src/server/tools.ts → automations/src/server/workflow-tools.ts} +0 -0
- package/modules/automations/src/services/data-classes.ts +62 -0
- package/modules/automations/src/services/database-repository.ts +152 -38
- package/modules/automations/src/services/migration.ts +119 -9
- package/modules/automations/src/services/repository.ts +52 -1
- package/modules/automations/src/services/schedule-runner.ts +84 -0
- package/modules/automations/src/services/schedule-service.ts +152 -26
- package/modules/automations/src/services/secret-rotation.ts +176 -0
- package/modules/automations/src/services/secret-vault.ts +68 -41
- package/modules/automations/src/services/trigger-service.ts +13 -10
- package/modules/automations/translations/en.json +15 -2
- package/modules/automations/translations/pl.json +15 -2
- package/modules/connectors/migrations/0001_connectors_core.down.sql +3 -0
- package/modules/connectors/migrations/0001_connectors_core.up.sql +72 -0
- package/modules/connectors/migrations/0002_connectors_call_keys.down.sql +3 -0
- package/modules/connectors/migrations/0002_connectors_call_keys.up.sql +18 -0
- package/modules/connectors/migrations/README.md +3 -0
- package/modules/connectors/module.json +28 -0
- package/modules/{automations-workflows-integration → connectors}/package.json +27 -26
- package/modules/connectors/spec/module.yaml +447 -0
- package/modules/connectors/src/acl/permissions.ts +6 -0
- package/modules/connectors/src/agent/tools.ts +180 -0
- package/modules/connectors/src/api/endpoints.ts +572 -0
- package/modules/connectors/src/client/ConnectorCallsView.tsrx +358 -0
- package/modules/connectors/src/client/ConnectorForm.tsrx +424 -0
- package/modules/connectors/src/client/ConnectorsView.tsrx +711 -0
- package/modules/connectors/src/client/api.ts +281 -0
- package/modules/connectors/src/client/consent.ts +48 -0
- package/modules/connectors/src/client/contribution.tsrx +37 -0
- package/modules/connectors/src/client/index.ts +22 -0
- package/modules/connectors/src/client/navigation.ts +42 -0
- package/modules/connectors/src/client/presentation.ts +38 -0
- package/modules/connectors/src/client/state.ts +175 -0
- package/modules/connectors/src/domain/calls.ts +70 -0
- package/modules/connectors/src/domain/definitions.ts +190 -0
- package/modules/connectors/src/domain/http-json.ts +68 -0
- package/modules/connectors/src/domain/types.ts +185 -0
- package/modules/connectors/src/index.ts +64 -0
- package/modules/connectors/src/platform.ts +56 -0
- package/modules/connectors/src/server/index.ts +66 -0
- package/modules/connectors/src/server/runtime.ts +163 -0
- package/modules/connectors/src/services/call-service.ts +903 -0
- package/modules/connectors/src/services/connectors-service.ts +653 -0
- package/modules/connectors/src/services/credential-vault.ts +188 -0
- package/modules/connectors/src/services/data-classes.ts +158 -0
- package/modules/connectors/src/services/database-repository.ts +897 -0
- package/modules/connectors/src/services/egress.ts +261 -0
- package/modules/connectors/src/services/index.ts +19 -0
- package/modules/connectors/src/services/migration.ts +166 -0
- package/modules/connectors/src/services/repository.ts +175 -0
- package/modules/connectors/src/services/service-error.ts +46 -0
- package/modules/connectors/src/settings.ts +69 -0
- package/modules/connectors/translations/en.json +191 -0
- package/modules/connectors/translations/pl.json +191 -0
- package/modules/directory/migrations/0001_directory_core.down.sql +5 -0
- package/modules/directory/migrations/0001_directory_core.up.sql +111 -0
- package/modules/directory/migrations/README.md +3 -0
- package/modules/directory/module.json +27 -0
- package/modules/directory/package.json +46 -0
- package/modules/directory/spec/module.yaml +351 -0
- package/modules/directory/src/acl/permissions.ts +7 -0
- package/modules/directory/src/api/endpoints.ts +478 -0
- package/modules/directory/src/api/scim-endpoints.ts +516 -0
- package/modules/directory/src/client/GroupMappingsView.tsrx +430 -0
- package/modules/directory/src/client/ProvisioningLogView.tsrx +248 -0
- package/modules/directory/src/client/ScimTokensView.tsrx +587 -0
- package/modules/directory/src/client/api.ts +222 -0
- package/modules/directory/src/client/contribution.tsrx +46 -0
- package/modules/directory/src/client/index.ts +22 -0
- package/modules/directory/src/client/navigation.ts +57 -0
- package/modules/directory/src/client/presentation.ts +143 -0
- package/modules/directory/src/client/state.ts +173 -0
- package/modules/directory/src/domain/scim.ts +346 -0
- package/modules/directory/src/domain/types.ts +166 -0
- package/modules/directory/src/index.ts +44 -0
- package/modules/directory/src/platform.ts +43 -0
- package/modules/directory/src/server/index.ts +17 -0
- package/modules/directory/src/server/runtime.ts +124 -0
- package/modules/directory/src/services/auth-port.ts +127 -0
- package/modules/directory/src/services/data-classes.ts +41 -0
- package/modules/directory/src/services/database-repository.ts +1067 -0
- package/modules/directory/src/services/directory-service.ts +311 -0
- package/modules/directory/src/services/index.ts +43 -0
- package/modules/directory/src/services/migration.ts +146 -0
- package/modules/directory/src/services/provisioning-service.ts +1420 -0
- package/modules/directory/src/services/rate-limiter.ts +103 -0
- package/modules/directory/src/services/repository.ts +208 -0
- package/modules/directory/src/services/service-error.ts +42 -0
- package/modules/directory/src/services/token-service.ts +215 -0
- package/modules/directory/src/settings.ts +86 -0
- package/modules/directory/translations/en.json +185 -0
- package/modules/directory/translations/pl.json +185 -0
- package/modules/documents/migrations/0001_documents_core.down.sql +1 -0
- package/modules/documents/migrations/0001_documents_core.up.sql +35 -0
- package/modules/documents/migrations/0002_documents_files_page.down.sql +1 -0
- package/modules/documents/migrations/0002_documents_files_page.up.sql +6 -0
- package/modules/documents/migrations/README.md +3 -0
- package/modules/documents/module.json +28 -0
- package/modules/documents/package.json +49 -0
- package/modules/documents/spec/module.yaml +247 -0
- package/modules/documents/src/acl/permissions.ts +6 -0
- package/modules/documents/src/api/endpoints.ts +401 -0
- package/modules/documents/src/client/DocumentUploader.tsrx +148 -0
- package/modules/documents/src/client/DocumentsView.tsrx +540 -0
- package/modules/documents/src/client/api.ts +209 -0
- package/modules/documents/src/client/contribution.tsrx +29 -0
- package/modules/documents/src/client/download.ts +70 -0
- package/modules/documents/src/client/index.ts +22 -0
- package/modules/documents/src/client/navigation.ts +25 -0
- package/modules/documents/src/client/presentation.ts +110 -0
- package/modules/documents/src/client/state.ts +117 -0
- package/modules/documents/src/domain/attachments.ts +84 -0
- package/modules/documents/src/domain/types.ts +80 -0
- package/modules/documents/src/index.ts +46 -0
- package/modules/documents/src/platform.ts +61 -0
- package/modules/documents/src/server/index.ts +30 -0
- package/modules/documents/src/server/runtime.ts +107 -0
- package/modules/documents/src/services/attachments.ts +39 -0
- package/modules/documents/src/services/data-classes.ts +25 -0
- package/modules/documents/src/services/database-repository.ts +259 -0
- package/modules/documents/src/services/documents-service.ts +586 -0
- package/modules/documents/src/services/index.ts +18 -0
- package/modules/documents/src/services/migration.ts +82 -0
- package/modules/documents/src/services/repository.ts +48 -0
- package/modules/documents/src/settings.ts +107 -0
- package/modules/documents/translations/en.json +111 -0
- package/modules/documents/translations/pl.json +111 -0
- package/modules/exports/migrations/0001_exports_core.down.sql +2 -0
- package/modules/exports/migrations/0001_exports_core.up.sql +28 -0
- package/modules/exports/migrations/0002_exports_job_routing_role.down.sql +4 -0
- package/modules/exports/migrations/0002_exports_job_routing_role.up.sql +21 -0
- package/modules/exports/migrations/README.md +3 -0
- package/modules/exports/module.json +28 -0
- package/modules/exports/package.json +44 -0
- package/modules/exports/spec/module.yaml +229 -0
- package/modules/exports/src/acl/permissions.ts +6 -0
- package/modules/exports/src/api/endpoints.ts +211 -0
- package/modules/exports/src/client/ExportsView.tsrx +327 -0
- package/modules/exports/src/client/api.ts +133 -0
- package/modules/exports/src/client/contribution.tsrx +31 -0
- package/modules/exports/src/client/index.ts +20 -0
- package/modules/exports/src/client/navigation.ts +25 -0
- package/modules/exports/src/client/presentation.ts +53 -0
- package/modules/exports/src/client/state.ts +56 -0
- package/modules/exports/src/domain/lists.ts +29 -0
- package/modules/exports/src/domain/types.ts +120 -0
- package/modules/exports/src/index.ts +41 -0
- package/modules/exports/src/platform.ts +80 -0
- package/modules/exports/src/server/index.ts +51 -0
- package/modules/exports/src/server/runtime.ts +160 -0
- package/modules/exports/src/services/data-classes.ts +56 -0
- package/modules/exports/src/services/database-repository.ts +405 -0
- package/modules/exports/src/services/export-runner.ts +123 -0
- package/modules/exports/src/services/export-service.ts +356 -0
- package/modules/exports/src/services/index.ts +6 -0
- package/modules/exports/src/services/list-registry.ts +105 -0
- package/modules/exports/src/services/migration.ts +89 -0
- package/modules/exports/src/services/repository.ts +97 -0
- package/modules/exports/src/settings.ts +70 -0
- package/modules/exports/translations/en.json +69 -0
- package/modules/exports/translations/pl.json +69 -0
- package/modules/import/migrations/0001_import_core.down.sql +6 -0
- package/modules/import/migrations/0001_import_core.up.sql +61 -0
- package/modules/import/migrations/0002_import_job_routing_role.down.sql +4 -0
- package/modules/import/migrations/0002_import_job_routing_role.up.sql +20 -0
- package/modules/import/migrations/0003_import_jobs_export_index.down.sql +1 -0
- package/modules/import/migrations/0003_import_jobs_export_index.up.sql +7 -0
- package/modules/import/migrations/0004_import_jobs_traceparent.down.sql +1 -0
- package/modules/import/migrations/0004_import_jobs_traceparent.up.sql +7 -0
- package/modules/import/migrations/README.md +3 -0
- package/modules/import/module.json +37 -0
- package/modules/import/package.json +50 -0
- package/modules/import/spec/module.yaml +335 -0
- package/modules/import/src/acl/permissions.ts +6 -0
- package/modules/import/src/api/endpoints.ts +400 -0
- package/modules/import/src/client/ImportJobDrawer.tsrx +229 -0
- package/modules/import/src/client/ImportsView.tsrx +455 -0
- package/modules/import/src/client/NewImportForm.tsrx +274 -0
- package/modules/import/src/client/api.ts +243 -0
- package/modules/import/src/client/contribution.tsrx +29 -0
- package/modules/import/src/client/index.ts +23 -0
- package/modules/import/src/client/navigation.ts +25 -0
- package/modules/import/src/client/presentation.ts +59 -0
- package/modules/import/src/client/state.ts +43 -0
- package/modules/import/src/domain/csv.ts +258 -0
- package/modules/import/src/domain/ports.ts +157 -0
- package/modules/import/src/domain/types.ts +179 -0
- package/modules/import/src/index.ts +58 -0
- package/modules/import/src/platform.ts +60 -0
- package/modules/import/src/server/index.ts +49 -0
- package/modules/import/src/server/runtime.ts +158 -0
- package/modules/import/src/services/csv-source.ts +121 -0
- package/modules/import/src/services/data-classes.ts +63 -0
- package/modules/import/src/services/database-repository.ts +780 -0
- package/modules/import/src/services/import-runner.ts +127 -0
- package/modules/import/src/services/import-service.ts +872 -0
- package/modules/import/src/services/index.ts +6 -0
- package/modules/import/src/services/migration.ts +156 -0
- package/modules/import/src/services/port-registry.ts +158 -0
- package/modules/import/src/services/repository.ts +147 -0
- package/modules/import/src/settings.ts +70 -0
- package/modules/import/translations/en.json +175 -0
- package/modules/import/translations/pl.json +175 -0
- package/modules/metering/migrations/0001_metering_core.down.sql +6 -0
- package/modules/metering/migrations/0001_metering_core.up.sql +119 -0
- package/modules/metering/migrations/0002_metering_bucket_export.down.sql +3 -0
- package/modules/metering/migrations/0002_metering_bucket_export.up.sql +5 -0
- package/modules/metering/migrations/README.md +3 -0
- package/modules/metering/module.json +46 -0
- package/modules/metering/package.json +50 -0
- package/modules/metering/spec/module.yaml +269 -0
- package/modules/metering/src/acl/permissions.ts +5 -0
- package/modules/metering/src/api/endpoints.ts +104 -0
- package/modules/metering/src/cli/commands.json +28 -0
- package/modules/metering/src/cli/index.ts +246 -0
- package/modules/metering/src/client/LimitsView.tsrx +170 -0
- package/modules/metering/src/client/UsageSummaryWidget.tsrx +63 -0
- package/modules/metering/src/client/UsageView.tsrx +205 -0
- package/modules/metering/src/client/api.ts +91 -0
- package/modules/metering/src/client/contribution.tsrx +42 -0
- package/modules/metering/src/client/index.ts +26 -0
- package/modules/metering/src/client/navigation.ts +54 -0
- package/modules/metering/src/client/presentation.ts +63 -0
- package/modules/metering/src/client/state.ts +31 -0
- package/modules/metering/src/domain/meters.ts +100 -0
- package/modules/metering/src/domain/types.ts +71 -0
- package/modules/metering/src/index.ts +51 -0
- package/modules/metering/src/platform.ts +84 -0
- package/modules/metering/src/server/index.ts +40 -0
- package/modules/metering/src/server/runtime.ts +165 -0
- package/modules/metering/src/services/data-classes.ts +34 -0
- package/modules/metering/src/services/database-repository.ts +574 -0
- package/modules/metering/src/services/index.ts +9 -0
- package/modules/metering/src/services/meter-registry.ts +145 -0
- package/modules/metering/src/services/metering-service.ts +441 -0
- package/modules/metering/src/services/migration.ts +183 -0
- package/modules/metering/src/services/notifications.ts +75 -0
- package/modules/metering/src/services/reports.ts +105 -0
- package/modules/metering/src/services/repository.ts +132 -0
- package/modules/metering/src/services/service-error.ts +46 -0
- package/modules/metering/src/settings.ts +47 -0
- package/modules/metering/translations/en.json +68 -0
- package/modules/metering/translations/pl.json +68 -0
- package/modules/notifications/migrations/0001_notifications_core.down.sql +1 -0
- package/modules/notifications/migrations/0001_notifications_core.up.sql +26 -0
- package/modules/notifications/migrations/0002_notifications_preferences.down.sql +1 -0
- package/modules/notifications/migrations/0002_notifications_preferences.up.sql +17 -0
- package/modules/notifications/migrations/0003_notifications_webhook_subscriptions.down.sql +1 -0
- package/modules/notifications/migrations/0003_notifications_webhook_subscriptions.up.sql +30 -0
- package/modules/notifications/migrations/0004_notifications_deliveries.down.sql +1 -0
- package/modules/notifications/migrations/0004_notifications_deliveries.up.sql +35 -0
- package/modules/notifications/migrations/0005_notifications_delivery_routing_role.down.sql +2 -0
- package/modules/notifications/migrations/0005_notifications_delivery_routing_role.up.sql +17 -0
- package/modules/notifications/migrations/0006_notifications_delivery_title.down.sql +1 -0
- package/modules/notifications/migrations/0006_notifications_delivery_title.up.sql +11 -0
- package/modules/notifications/migrations/0007_notifications_secret_rotation_inventory.down.sql +2 -0
- package/modules/notifications/migrations/0007_notifications_secret_rotation_inventory.up.sql +19 -0
- package/modules/notifications/migrations/0008_notifications_delivery_claim.down.sql +8 -0
- package/modules/notifications/migrations/0008_notifications_delivery_claim.up.sql +19 -0
- package/modules/notifications/migrations/0009_notifications_kind_approvals.down.sql +16 -0
- package/modules/notifications/migrations/0009_notifications_kind_approvals.up.sql +18 -0
- package/modules/notifications/migrations/0010_notifications_kind_meter_threshold.down.sql +16 -0
- package/modules/notifications/migrations/0010_notifications_kind_meter_threshold.up.sql +18 -0
- package/modules/notifications/migrations/0011_notifications_retention_keyset.down.sql +5 -0
- package/modules/notifications/migrations/0011_notifications_retention_keyset.up.sql +15 -0
- package/modules/notifications/migrations/0012_notifications_email_channel.down.sql +12 -0
- package/modules/notifications/migrations/0012_notifications_email_channel.up.sql +38 -0
- package/modules/notifications/migrations/0013_notifications_list_pages.down.sql +5 -0
- package/modules/notifications/migrations/0013_notifications_list_pages.up.sql +13 -0
- package/modules/notifications/migrations/README.md +3 -0
- package/modules/notifications/module.json +32 -0
- package/modules/notifications/package.json +49 -0
- package/modules/notifications/spec/module.yaml +718 -0
- package/modules/notifications/src/acl/permissions.ts +11 -0
- package/modules/notifications/src/api/endpoints.ts +814 -0
- package/modules/notifications/src/cli/commands.json +17 -0
- package/modules/notifications/src/cli/index.ts +126 -0
- package/modules/notifications/src/client/DeliveriesView.tsrx +483 -0
- package/modules/notifications/src/client/FactList.tsrx +24 -0
- package/modules/notifications/src/client/NotificationPreferencesView.tsrx +240 -0
- package/modules/notifications/src/client/NotificationsInboxView.tsrx +592 -0
- package/modules/notifications/src/client/UnreadCountWidget.tsrx +62 -0
- package/modules/notifications/src/client/WebhookForm.tsrx +297 -0
- package/modules/notifications/src/client/WebhooksView.tsrx +637 -0
- package/modules/notifications/src/client/api.ts +382 -0
- package/modules/notifications/src/client/contribution.tsrx +83 -0
- package/modules/notifications/src/client/inbox.ts +41 -0
- package/modules/notifications/src/client/index.ts +29 -0
- package/modules/notifications/src/client/navigation.ts +92 -0
- package/modules/notifications/src/client/notifications.css +70 -0
- package/modules/notifications/src/client/presentation.ts +81 -0
- package/modules/notifications/src/client/state.ts +246 -0
- package/modules/notifications/src/client/unread-count.ts +26 -0
- package/modules/notifications/src/domain/locale.ts +47 -0
- package/modules/notifications/src/domain/publish.ts +46 -0
- package/modules/notifications/src/domain/types.ts +186 -0
- package/modules/notifications/src/index.ts +62 -0
- package/modules/notifications/src/platform.ts +85 -0
- package/modules/notifications/src/server/index.ts +82 -0
- package/modules/notifications/src/server/runtime.ts +251 -0
- package/modules/notifications/src/services/data-classes.ts +139 -0
- package/modules/notifications/src/services/database-repository.ts +1334 -0
- package/modules/notifications/src/services/delivery-payload.ts +93 -0
- package/modules/notifications/src/services/delivery-runner.ts +170 -0
- package/modules/notifications/src/services/delivery-service.ts +811 -0
- package/modules/notifications/src/services/egress.ts +239 -0
- package/modules/notifications/src/services/email-channel.ts +93 -0
- package/modules/notifications/src/services/index.ts +23 -0
- package/modules/notifications/src/services/migration.ts +572 -0
- package/modules/notifications/src/services/notifications-service.ts +297 -0
- package/modules/notifications/src/services/paging.ts +60 -0
- package/modules/notifications/src/services/publish-service.ts +133 -0
- package/modules/notifications/src/services/repository.ts +342 -0
- package/modules/notifications/src/services/secret-rotation.ts +177 -0
- package/modules/notifications/src/services/secret-vault.ts +168 -0
- package/modules/notifications/src/services/service-error.ts +67 -0
- package/modules/notifications/src/services/signature.ts +81 -0
- package/modules/notifications/src/services/webhook-service.ts +307 -0
- package/modules/notifications/src/settings.ts +130 -0
- package/modules/notifications/translations/en.json +255 -0
- package/modules/notifications/translations/pl.json +255 -0
- package/modules/profile/module.json +3 -3
- package/modules/profile/package.json +1 -1
- package/modules/profile/spec/module.yaml +3 -3
- package/modules/profile/src/services/database-repository.ts +3 -13
- package/modules/reports/module.json +28 -0
- package/modules/reports/package.json +42 -0
- package/modules/reports/spec/module.yaml +231 -0
- package/modules/reports/src/acl/permissions.ts +5 -0
- package/modules/reports/src/api/endpoints.ts +72 -0
- package/modules/reports/src/client/ReportsSummaryWidget.tsrx +52 -0
- package/modules/reports/src/client/ReportsView.tsrx +246 -0
- package/modules/reports/src/client/api.ts +65 -0
- package/modules/reports/src/client/contribution.tsrx +36 -0
- package/modules/reports/src/client/index.ts +18 -0
- package/modules/reports/src/client/navigation.ts +39 -0
- package/modules/reports/src/client/presentation.ts +123 -0
- package/modules/reports/src/client/state.ts +28 -0
- package/modules/reports/src/domain/providers.ts +115 -0
- package/modules/reports/src/domain/types.ts +44 -0
- package/modules/reports/src/index.ts +47 -0
- package/modules/reports/src/platform.ts +37 -0
- package/modules/reports/src/server/index.ts +21 -0
- package/modules/reports/src/server/runtime.ts +40 -0
- package/modules/reports/src/services/index.ts +17 -0
- package/modules/reports/src/services/provider-registry.ts +144 -0
- package/modules/reports/src/services/range.ts +65 -0
- package/modules/reports/src/services/reports-service.ts +316 -0
- package/modules/reports/src/services/service-error.ts +26 -0
- package/modules/reports/src/settings.ts +49 -0
- package/modules/reports/translations/en.json +35 -0
- package/modules/reports/translations/pl.json +35 -0
- package/modules/sandbox/module.json +4 -4
- package/modules/sandbox/package.json +1 -1
- package/modules/sandbox/spec/module.yaml +3 -3
- package/modules/sandbox/src/services/database-repository.ts +14 -23
- package/modules/sandbox/src/services/directory.ts +16 -0
- package/modules/sandbox/src/services/sandbox-service.ts +13 -11
- package/modules/search/migrations/0001_search_core.down.sql +1 -0
- package/modules/search/migrations/0001_search_core.up.sql +22 -0
- package/modules/search/migrations/0002_search_recent_export.down.sql +3 -0
- package/modules/search/migrations/0002_search_recent_export.up.sql +5 -0
- package/modules/search/migrations/README.md +3 -0
- package/modules/search/module.json +28 -0
- package/modules/search/package.json +43 -0
- package/modules/search/spec/module.yaml +177 -0
- package/modules/search/src/acl/permissions.ts +5 -0
- package/modules/search/src/api/endpoints.ts +221 -0
- package/modules/search/src/client/SearchView.tsrx +346 -0
- package/modules/search/src/client/api.ts +157 -0
- package/modules/search/src/client/contribution.tsrx +31 -0
- package/modules/search/src/client/index.ts +17 -0
- package/modules/search/src/client/navigation.ts +97 -0
- package/modules/search/src/client/search.css +56 -0
- package/modules/search/src/client/state.ts +113 -0
- package/modules/search/src/domain/data-classes.ts +73 -0
- package/modules/search/src/domain/providers.ts +88 -0
- package/modules/search/src/domain/types.ts +56 -0
- package/modules/search/src/index.ts +46 -0
- package/modules/search/src/platform.ts +42 -0
- package/modules/search/src/server/index.ts +25 -0
- package/modules/search/src/server/runtime.ts +123 -0
- package/modules/search/src/services/database-repository.ts +163 -0
- package/modules/search/src/services/index.ts +21 -0
- package/modules/search/src/services/migration.ts +65 -0
- package/modules/search/src/services/provider-registry.ts +137 -0
- package/modules/search/src/services/repository.ts +43 -0
- package/modules/search/src/services/search-service.ts +442 -0
- package/modules/search/src/services/service-error.ts +35 -0
- package/modules/search/src/settings.ts +68 -0
- package/modules/search/translations/en.json +45 -0
- package/modules/search/translations/pl.json +45 -0
- package/modules/system/module.json +2 -2
- package/modules/system/package.json +1 -1
- package/modules/system/spec/module.yaml +30 -3
- package/modules/system/src/client/FlagsPanel.tsrx +127 -0
- package/modules/system/src/client/ModuleSettingRow.tsrx +17 -7
- package/modules/system/src/client/ModuleSettingsSection.tsrx +10 -71
- package/modules/system/src/client/ModulesView.tsrx +91 -65
- package/modules/system/src/client/flags.ts +35 -0
- package/modules/system/src/client/index.ts +8 -0
- package/modules/system/src/client/navigation.ts +8 -0
- package/modules/system/src/client/settings-state.ts +159 -0
- package/modules/system/src/client/state.ts +2 -0
- package/modules/system/src/domain/time-zone.ts +105 -0
- package/modules/system/src/index.ts +12 -0
- package/modules/system/src/platform.ts +5 -1
- package/modules/system/src/server/endpoints.ts +72 -21
- package/modules/system/src/settings.ts +28 -0
- package/modules/system/translations/en.json +17 -0
- package/modules/system/translations/pl.json +17 -0
- package/modules/users/module.json +30 -4
- package/modules/users/package.json +4 -1
- package/modules/users/spec/module.yaml +290 -7
- package/modules/users/src/api/endpoints.ts +228 -15
- package/modules/users/src/client/InviteMemberForm.tsrx +2 -2
- package/modules/users/src/client/MemberDrawer.tsrx +142 -26
- package/modules/users/src/client/UserAccountForm.tsrx +2 -2
- package/modules/users/src/client/UsersView.tsrx +432 -71
- package/modules/users/src/client/api.ts +106 -12
- package/modules/users/src/client/contribution.tsrx +5 -0
- package/modules/users/src/client/member-columns.tsrx +22 -9
- package/modules/users/src/client/state.ts +172 -7
- package/modules/users/src/domain/lists.ts +9 -0
- package/modules/users/src/index.ts +11 -0
- package/modules/users/src/platform.ts +50 -1
- package/modules/users/src/services/member-export.ts +51 -0
- package/modules/users/src/services/member-import.ts +249 -0
- package/modules/users/src/services/member-search.ts +145 -0
- package/modules/users/src/services/users-service.ts +117 -4
- package/modules/users/translations/en.json +57 -8
- package/modules/users/translations/pl.json +57 -8
- package/modules/workflows/migrations/0004_payload_rotation_inventory.up.sql +17 -0
- package/modules/workflows/migrations/0005_workflows_human_approval.up.sql +28 -0
- package/modules/workflows/migrations/0006_workflows_retention_indexes.up.sql +11 -0
- package/modules/workflows/migrations/0007_workflows_erasure_subject_indexes.up.sql +17 -0
- package/modules/workflows/migrations/0008_workflows_subject_account.up.sql +24 -0
- package/modules/workflows/migrations/0009_workflows_definition_list_indexes.up.sql +9 -0
- package/modules/workflows/module.json +34 -6
- package/modules/workflows/package.json +2 -1
- package/modules/workflows/spec/module.yaml +26 -9
- package/modules/workflows/src/api/endpoints.ts +131 -25
- package/modules/workflows/src/cli/commands.json +17 -0
- package/modules/workflows/src/cli/index.ts +114 -0
- package/modules/workflows/src/client/WorkflowExecutionHistory.tsrx +36 -27
- package/modules/workflows/src/client/WorkflowInspector.tsrx +116 -0
- package/modules/workflows/src/client/WorkflowNodePalette.tsrx +1 -0
- package/modules/workflows/src/client/WorkflowsView.tsrx +236 -99
- package/modules/workflows/src/client/api.ts +54 -34
- package/modules/workflows/src/client/canvas-model.ts +12 -0
- package/modules/workflows/src/client/state.ts +95 -0
- package/modules/workflows/src/domain/events.ts +25 -11
- package/modules/workflows/src/domain/graph.ts +135 -1
- package/modules/workflows/src/domain/types.ts +91 -4
- package/modules/workflows/src/platform.ts +12 -0
- package/modules/workflows/src/server/runtime.ts +78 -5
- package/modules/workflows/src/services/approvals.ts +62 -0
- package/modules/workflows/src/services/cursors.ts +37 -0
- package/modules/workflows/src/services/data-classes.ts +232 -0
- package/modules/workflows/src/services/database-repository.ts +660 -171
- package/modules/workflows/src/services/migration.ts +240 -0
- package/modules/workflows/src/services/notifications.ts +98 -0
- package/modules/workflows/src/services/payload-codec.ts +51 -28
- package/modules/workflows/src/services/payload-rotation.ts +171 -0
- package/modules/workflows/src/services/repository.ts +123 -8
- package/modules/workflows/src/services/simulation.ts +7 -0
- package/modules/workflows/src/services/worker.ts +446 -25
- package/modules/workflows/src/services/workflows-service.ts +264 -103
- package/modules/workflows/translations/en.json +12 -4
- package/modules/workflows/translations/pl.json +12 -4
- package/modules.json +48 -4
- package/package.json +64 -6
- package/packages/client/package.json +1 -0
- package/packages/client/src/ApplicationShell.tsrx +43 -0
- package/packages/client/src/contributions.ts +55 -0
- package/packages/client/src/i18n/locales/en.json +5 -0
- package/packages/client/src/i18n/locales/pl.json +5 -0
- package/packages/client/src/index.ts +13 -1
- package/packages/client/src/routing.ts +22 -0
- package/packages/client/src/shell/CommandPalette.tsrx +223 -11
- package/packages/client/src/shell/command-search.ts +126 -0
- package/packages/client/src/shell/command.ts +30 -0
- package/packages/client/src/shell/navigation.ts +12 -0
- package/packages/client/src/shell/palette-keys.ts +27 -0
- package/packages/client/src/shell/shell.css +7 -0
- package/packages/contracts/schemas/module-spec.schema.json +287 -1
- package/packages/contracts/schemas/module.schema.json +25 -0
- package/packages/contracts/src/index.ts +144 -1
- package/packages/database/src/backup.ts +179 -0
- package/packages/database/src/decoders.ts +25 -0
- package/packages/database/src/index.ts +17 -0
- package/packages/database/src/record-history.ts +5 -14
- package/packages/harness/src/index.ts +2 -0
- package/packages/harness/src/runtime.ts +218 -1
- package/packages/harness/src/tool-adapters.ts +19 -1
- package/packages/kernel/src/acl.ts +179 -0
- package/packages/kernel/src/capability-registry.ts +69 -2
- package/packages/kernel/src/data-class-registry.ts +354 -0
- package/packages/kernel/src/index.ts +65 -4
- package/packages/kernel/src/keyring.ts +208 -0
- package/packages/kernel/src/module-compatibility.ts +35 -4
- package/packages/kernel/src/module-registry.ts +49 -2
- package/packages/kernel/src/module-settings.ts +189 -27
- package/packages/server/package.json +2 -1
- package/packages/server/src/composition.ts +38 -0
- package/packages/server/src/endpoint.ts +104 -47
- package/packages/server/src/export/csv.ts +34 -0
- package/packages/server/src/export/definition.ts +346 -0
- package/packages/server/src/export/index.ts +61 -0
- package/packages/server/src/export/run.ts +116 -0
- package/packages/server/src/index.ts +155 -0
- package/packages/server/src/jobs/index.ts +93 -0
- package/packages/server/src/jobs/runner.ts +486 -0
- package/packages/server/src/log.ts +291 -0
- package/packages/server/src/mail/config.ts +184 -0
- package/packages/server/src/mail/contracts.ts +251 -0
- package/packages/server/src/mail/index.ts +31 -0
- package/packages/server/src/mail/port.ts +101 -0
- package/packages/server/src/mail/smtp.ts +191 -0
- package/packages/server/src/mail/template.ts +105 -0
- package/packages/server/src/metrics.ts +447 -0
- package/packages/server/src/pagination.ts +246 -0
- package/packages/server/src/trace/context.ts +103 -0
- package/packages/server/src/trace/egress.ts +66 -0
- package/packages/server/src/trace/error-sink.ts +296 -0
- package/packages/server/src/trace/exporter.ts +295 -0
- package/packages/server/src/trace/index.ts +60 -0
- package/packages/server/src/trace/job-sink.ts +172 -0
- package/packages/server/src/trace/tracer.ts +338 -0
- package/packages/server/src/web.ts +5 -1
- package/packages/storage/package.json +40 -0
- package/packages/storage/src/config.ts +189 -0
- package/packages/storage/src/content-type.ts +124 -0
- package/packages/storage/src/contracts.ts +112 -0
- package/packages/storage/src/envelope.ts +189 -0
- package/packages/storage/src/index.ts +43 -0
- package/packages/storage/src/local.ts +75 -0
- package/packages/storage/src/port.ts +251 -0
- package/packages/storage/src/read-token.ts +130 -0
- package/packages/storage/src/s3.ts +144 -0
- package/packages/storage/src/scanner.ts +22 -0
- package/packages/storage/src/sigv4.ts +104 -0
- package/packages/storage/src/store.ts +13 -0
- package/packages/ui/package.json +1 -0
- package/packages/ui/src/components/Button.tsrx +3 -0
- package/packages/ui/src/components/ConfirmDialog.tsrx +13 -2
- package/packages/ui/src/components/DateField.tsrx +92 -0
- package/packages/ui/src/components/DatePicker.tsrx +525 -0
- package/packages/ui/src/components/DateRangeField.tsrx +93 -0
- package/packages/ui/src/components/Drawer.tsrx +10 -1
- package/packages/ui/src/components/FileUpload.tsrx +158 -0
- package/packages/ui/src/components/Pagination.tsrx +130 -0
- package/packages/ui/src/components/Select.tsrx +78 -0
- package/packages/ui/src/components/Table.tsrx +498 -81
- package/packages/ui/src/components/Tabs.tsrx +53 -0
- package/packages/ui/src/components/ToastHost.tsrx +48 -0
- package/packages/ui/src/components/calendar.ts +243 -0
- package/packages/ui/src/components/date-field.ts +62 -0
- package/packages/ui/src/components/file-upload.ts +49 -0
- package/packages/ui/src/components/focus-trap.ts +62 -0
- package/packages/ui/src/components/pagination.ts +68 -0
- package/packages/ui/src/components/table-sorting.ts +49 -0
- package/packages/ui/src/components/tabs.ts +61 -0
- package/packages/ui/src/components/toast-store.ts +116 -0
- package/packages/ui/src/icons/Icon.tsrx +5 -0
- package/packages/ui/src/index.ts +45 -0
- package/packages/ui/src/styles/components.css +598 -0
- package/modules/automations-workflows-integration/module.json +0 -27
- package/modules/automations-workflows-integration/spec/module.yaml +0 -93
- package/modules/automations-workflows-integration/src/index.ts +0 -14
- package/modules/automations-workflows-integration/src/platform.ts +0 -24
- package/modules/automations-workflows-integration/src/server/index.ts +0 -11
- package/modules/automations-workflows-integration/translations/en.json +0 -21
- package/modules/automations-workflows-integration/translations/pl.json +0 -21
- package/modules/workflows/src/services/cursor-codec.ts +0 -62
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
serviceActor,
|
|
4
|
+
userActor,
|
|
5
|
+
type Actor,
|
|
6
|
+
type ModuleSettingChange,
|
|
7
|
+
type UserActor,
|
|
8
|
+
} from '@flowdular/sdk/kernel';
|
|
9
|
+
import type { ErrorSink, ModuleMetrics } from '@flowdular/sdk/server';
|
|
3
10
|
import { AUTH_SCOPES, MEMBER_SCOPES, OWNER_SCOPES } from '../acl/scopes.ts';
|
|
4
11
|
import type {
|
|
5
12
|
ApiTokenRecord,
|
|
@@ -11,8 +18,10 @@ import type {
|
|
|
11
18
|
CreateApiTokenInput,
|
|
12
19
|
CreateRoleInput,
|
|
13
20
|
CreateTenantMemberInput,
|
|
21
|
+
CreateTenantMemberWithoutPasswordInput,
|
|
14
22
|
IssuedApiToken,
|
|
15
23
|
IssuedSession,
|
|
24
|
+
MembershipStatus,
|
|
16
25
|
SessionSummary,
|
|
17
26
|
SignInInput,
|
|
18
27
|
SignUpInput,
|
|
@@ -25,9 +34,20 @@ import {
|
|
|
25
34
|
DEFAULT_SESSION_TTL_HOURS,
|
|
26
35
|
} from '../settings.ts';
|
|
27
36
|
import { AuthServiceError } from './auth-service-error.ts';
|
|
37
|
+
import {
|
|
38
|
+
IdentityProviderService,
|
|
39
|
+
PROVIDER_AUDIT_ACTIONS,
|
|
40
|
+
type OidcDiscoveryPort,
|
|
41
|
+
} from './identity-provider-service.ts';
|
|
42
|
+
import {
|
|
43
|
+
createProviderSecretVault,
|
|
44
|
+
type ProviderSecretVault,
|
|
45
|
+
} from './provider-secrets.ts';
|
|
28
46
|
import {
|
|
29
47
|
DEFAULT_PASSWORD_HASH_OPTIONS,
|
|
30
48
|
hashPassword,
|
|
49
|
+
passwordIsSet,
|
|
50
|
+
UNUSABLE_PASSWORD_HASH,
|
|
31
51
|
verifyPassword,
|
|
32
52
|
type PasswordHashOptions,
|
|
33
53
|
} from './password.ts';
|
|
@@ -38,20 +58,26 @@ import {
|
|
|
38
58
|
type AccountCredential,
|
|
39
59
|
type AuditActorEvent,
|
|
40
60
|
type AuthRepository,
|
|
61
|
+
type ExternalIdentityPage,
|
|
41
62
|
type TenantMember,
|
|
63
|
+
type TenantMemberKeyset,
|
|
64
|
+
type TenantMemberPage,
|
|
65
|
+
type TenantMemberSort,
|
|
66
|
+
type TenantMemberSortedPage,
|
|
42
67
|
type TenantSummary,
|
|
43
68
|
} from './repository.ts';
|
|
44
69
|
import type { AuthMailDelivery } from './mail-delivery.ts';
|
|
45
70
|
import {
|
|
71
|
+
createMfaSecretVault,
|
|
46
72
|
createTotpSecret,
|
|
47
|
-
decryptMfaSecret,
|
|
48
|
-
encryptMfaSecret,
|
|
49
73
|
verifyTotp,
|
|
74
|
+
type MfaSecretVault,
|
|
50
75
|
} from './totp.ts';
|
|
51
76
|
import {
|
|
52
77
|
assertPasswordPolicy,
|
|
53
78
|
normalizeEmail,
|
|
54
79
|
validateCreateTenantMember,
|
|
80
|
+
validateCreateTenantMemberWithoutPassword,
|
|
55
81
|
validateDisplayName,
|
|
56
82
|
validateEmailAddress,
|
|
57
83
|
validateRoleDescription,
|
|
@@ -84,10 +110,37 @@ export interface AuthServiceOptions {
|
|
|
84
110
|
readonly now?: () => number;
|
|
85
111
|
/** AES-256-GCM key used only to protect enrolled TOTP secrets at rest. */
|
|
86
112
|
readonly mfaEncryptionKey?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Keys a rotation has not finished retiring. They open stored factors that
|
|
115
|
+
* were sealed before the rotation, and nothing is ever written with them.
|
|
116
|
+
*/
|
|
117
|
+
readonly mfaPreviousEncryptionKeys?: readonly string[];
|
|
118
|
+
/**
|
|
119
|
+
* Verifies an issuer through its discovery document when a workspace saves
|
|
120
|
+
* a provider. The server layer installs the HTTP adapter; without one, a
|
|
121
|
+
* save is refused instead of trusting an unverified issuer.
|
|
122
|
+
*/
|
|
123
|
+
readonly oidcDiscovery?: OidcDiscoveryPort;
|
|
124
|
+
/**
|
|
125
|
+
* Drops the ID token verifier's cached key set for a provider whose row
|
|
126
|
+
* changed. The server layer installs the verifier it serves with; a
|
|
127
|
+
* composition without one caches nothing to invalidate.
|
|
128
|
+
*/
|
|
129
|
+
readonly forgetProviderKeys?: (providerId: string) => void;
|
|
87
130
|
/** Deployment-owned delivery adapter. auth.core never selects an email vendor. */
|
|
88
131
|
readonly mailDelivery?: AuthMailDelivery;
|
|
132
|
+
/**
|
|
133
|
+
* The locale a workspace's messages are worded in. The server resolves it
|
|
134
|
+
* from the workspace's defaultLocale setting; without one every message is
|
|
135
|
+
* English.
|
|
136
|
+
*/
|
|
137
|
+
readonly mailLocale?: (tenantId: string) => Promise<string> | string;
|
|
89
138
|
/** Public origin used to form opaque, one-time delivery links. */
|
|
90
139
|
readonly publicBaseUrl?: string;
|
|
140
|
+
/** Counts the audit writes that failed; a failed write never fails its operation. */
|
|
141
|
+
readonly metrics?: ModuleMetrics;
|
|
142
|
+
/** Receives a failed audit write as a name and a module, never as the driver's words. */
|
|
143
|
+
readonly errorSink?: ErrorSink;
|
|
91
144
|
}
|
|
92
145
|
|
|
93
146
|
export interface MfaChallenge extends IssuedSession {
|
|
@@ -199,6 +252,34 @@ export interface SignInContext {
|
|
|
199
252
|
readonly address?: string | null;
|
|
200
253
|
}
|
|
201
254
|
|
|
255
|
+
/** What a sign-in through a tenant-owned provider needs about the workspace. */
|
|
256
|
+
export interface TenantProviderSignIn {
|
|
257
|
+
readonly tenantId: string;
|
|
258
|
+
readonly jitEnabled: boolean;
|
|
259
|
+
readonly allowedDomains: readonly string[];
|
|
260
|
+
readonly jitRole: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* A workspace saving a provider verifies its issuer through discovery, which is
|
|
264
|
+
an outbound request the server layer owns. Without that adapter the save is
|
|
265
|
+
refused rather than trusting the issuer a request supplied. */
|
|
266
|
+
function discoveryUnavailable(): AuthServiceError {
|
|
267
|
+
return new AuthServiceError(
|
|
268
|
+
'PROVIDER_DISCOVERY_UNAVAILABLE',
|
|
269
|
+
'Issuer discovery is not available in this composition.',
|
|
270
|
+
503,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* A provisioned account has only the address the provider verified. The local
|
|
275
|
+
part is the readable half of it and is what the member sees until they
|
|
276
|
+
change it. */
|
|
277
|
+
function displayNameFromEmail(email: string): string {
|
|
278
|
+
const local = email.slice(0, email.lastIndexOf('@'));
|
|
279
|
+
const candidate = local.length >= 2 ? local : email;
|
|
280
|
+
return candidate.slice(0, 80);
|
|
281
|
+
}
|
|
282
|
+
|
|
202
283
|
export function hashSessionToken(token: string): string {
|
|
203
284
|
return createHash('sha256').update(token, 'utf8').digest('base64url');
|
|
204
285
|
}
|
|
@@ -221,6 +302,50 @@ const OPERATOR_SETUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
221
302
|
const MFA_CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
222
303
|
const RECOVERY_CODE_COUNT = 10;
|
|
223
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Addresses one `findTenantMembersByEmail` call may name. A bulk caller splits
|
|
307
|
+
* a larger batch, so the statement never carries an unbounded list and a
|
|
308
|
+
* workspace of unknown size is never listed to answer a handful of addresses.
|
|
309
|
+
*/
|
|
310
|
+
export const TENANT_MEMBER_LOOKUP_LIMIT = 500;
|
|
311
|
+
|
|
312
|
+
/** Members one `searchTenantMembers` call may answer. */
|
|
313
|
+
export const TENANT_MEMBER_SEARCH_LIMIT = 500;
|
|
314
|
+
|
|
315
|
+
/** Members one paged `listTenantMembers` call may answer. */
|
|
316
|
+
export const TENANT_MEMBER_PAGE_LIMIT = 500;
|
|
317
|
+
|
|
318
|
+
/** Sort keys `listTenantMembersSorted` accepts. */
|
|
319
|
+
export const TENANT_MEMBER_SORTS: readonly TenantMemberSort[] = [
|
|
320
|
+
'displayName',
|
|
321
|
+
'email',
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
/** Characters a sorted page's keyset value may carry; a display name or an address is shorter. */
|
|
325
|
+
const TENANT_MEMBER_KEYSET_LENGTH = 512;
|
|
326
|
+
|
|
327
|
+
export interface TenantMemberSortedInput {
|
|
328
|
+
readonly sort: TenantMemberSort;
|
|
329
|
+
readonly direction: 'asc' | 'desc';
|
|
330
|
+
readonly limit: number;
|
|
331
|
+
/** A prefix of the display name or the address, folded like a search term; empty narrows nothing. */
|
|
332
|
+
readonly query?: string | null;
|
|
333
|
+
readonly membershipStatus?: MembershipStatus | null;
|
|
334
|
+
readonly after?: TenantMemberKeyset | null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Bindings one `listExternalIdentities` call may answer. */
|
|
338
|
+
export const EXTERNAL_IDENTITY_PAGE_LIMIT = 500;
|
|
339
|
+
|
|
340
|
+
/** Characters a search term may carry; a longer one matches nothing useful. */
|
|
341
|
+
export const TENANT_MEMBER_SEARCH_TERM_LENGTH = 200;
|
|
342
|
+
|
|
343
|
+
/* The LIKE wildcards, so a term a person typed is matched literally. The
|
|
344
|
+
statements that use it declare ESCAPE '\'. */
|
|
345
|
+
function escapeLikeTerm(term: string): string {
|
|
346
|
+
return term.replace(/[\\%_]/g, (character) => `\\${character}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
224
349
|
export const AUDIT_ACTIONS = Object.freeze({
|
|
225
350
|
signInSucceeded: 'auth.sign-in.succeeded',
|
|
226
351
|
signInFailed: 'auth.sign-in.failed',
|
|
@@ -237,11 +362,20 @@ export const AUDIT_ACTIONS = Object.freeze({
|
|
|
237
362
|
mfaEnrolled: 'auth.mfa.enrolled',
|
|
238
363
|
mfaConfirmed: 'auth.mfa.confirmed',
|
|
239
364
|
mfaChallengeSucceeded: 'auth.mfa.challenge-succeeded',
|
|
365
|
+
mfaReset: 'auth.mfa.reset',
|
|
240
366
|
tenantRenamed: 'auth.tenant.renamed',
|
|
241
367
|
workspaceProvisioned: 'auth.workspace.provisioned',
|
|
242
368
|
memberCreated: 'users.member.created',
|
|
243
369
|
memberUpdated: 'users.member.updated',
|
|
244
370
|
memberStatus: 'users.member.status',
|
|
371
|
+
memberProvisioned: 'auth.member.provisioned',
|
|
372
|
+
jitRefused: 'auth.jit.refused',
|
|
373
|
+
membershipStatus: 'auth.membership.status',
|
|
374
|
+
providerCreated: PROVIDER_AUDIT_ACTIONS.created,
|
|
375
|
+
providerUpdated: PROVIDER_AUDIT_ACTIONS.updated,
|
|
376
|
+
providerStatusChanged: PROVIDER_AUDIT_ACTIONS.status,
|
|
377
|
+
providerSecretRotated: PROVIDER_AUDIT_ACTIONS.secretRotated,
|
|
378
|
+
providerDeleted: PROVIDER_AUDIT_ACTIONS.deleted,
|
|
245
379
|
memberRemoved: 'users.member.removed',
|
|
246
380
|
memberPasswordReset: 'users.member.password-reset',
|
|
247
381
|
memberScopes: 'users.member.scopes',
|
|
@@ -250,16 +384,44 @@ export const AUDIT_ACTIONS = Object.freeze({
|
|
|
250
384
|
roleUpdated: 'auth.role.updated',
|
|
251
385
|
roleDeleted: 'auth.role.deleted',
|
|
252
386
|
settingsUpdated: 'settings.updated',
|
|
387
|
+
settingsFlagChanged: 'settings.flag.changed',
|
|
253
388
|
});
|
|
254
389
|
|
|
255
390
|
export const AUDIT_ACTION_LIST = Object.freeze(Object.values(AUDIT_ACTIONS));
|
|
256
391
|
|
|
392
|
+
/* The actor an audit row records. A kernel service actor always names its
|
|
393
|
+
configuring user; a provider row stores none, so the trail admits a service
|
|
394
|
+
actor whose configuring user is unknown and persists it as null. */
|
|
395
|
+
interface ServiceAuditActor {
|
|
396
|
+
readonly kind: 'service';
|
|
397
|
+
readonly id: string;
|
|
398
|
+
readonly label: string;
|
|
399
|
+
readonly configuredBy: UserActor | null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
type AuditActor = Actor | ServiceAuditActor;
|
|
403
|
+
|
|
257
404
|
/* An operator command runs from a deployment shell and belongs to no account.
|
|
258
|
-
The
|
|
259
|
-
|
|
260
|
-
filter can search for. */
|
|
405
|
+
The shell is the service and the person at it, whose identifier carries the
|
|
406
|
+
cli: prefix the label filter can search for, is who configured it. */
|
|
261
407
|
function operatorActor(operator: string): Actor {
|
|
262
|
-
return {
|
|
408
|
+
return serviceActor({
|
|
409
|
+
serviceId: operator,
|
|
410
|
+
label: operator,
|
|
411
|
+
configuredBy: { kind: 'user', id: operator, label: operator },
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* A refused just-in-time sign-in belongs to no account of this workspace, so
|
|
416
|
+
the provider that asserted the identity is the actor and the address it
|
|
417
|
+
reported never reaches the trail. */
|
|
418
|
+
function providerActor(provider: string): ServiceAuditActor {
|
|
419
|
+
return {
|
|
420
|
+
kind: 'service',
|
|
421
|
+
id: `oidc:${provider}`,
|
|
422
|
+
label: `oidc:${provider}`,
|
|
423
|
+
configuredBy: null,
|
|
424
|
+
};
|
|
263
425
|
}
|
|
264
426
|
|
|
265
427
|
function member(account: AccountCredential, createdAt: number): TenantMember {
|
|
@@ -270,6 +432,7 @@ function member(account: AccountCredential, createdAt: number): TenantMember {
|
|
|
270
432
|
role: account.role,
|
|
271
433
|
roleId: account.roleId,
|
|
272
434
|
status: account.status,
|
|
435
|
+
membershipStatus: account.membershipStatus,
|
|
273
436
|
scopes: account.scopes,
|
|
274
437
|
passwordChangeRequired: account.passwordChangeRequired,
|
|
275
438
|
createdAt,
|
|
@@ -282,8 +445,17 @@ export class AuthService {
|
|
|
282
445
|
readonly #passwordHash: PasswordHashOptions;
|
|
283
446
|
readonly #now: () => number;
|
|
284
447
|
readonly #mfaEncryptionKey: string | undefined;
|
|
448
|
+
readonly #mfaPreviousEncryptionKeys: readonly string[];
|
|
449
|
+
#mfaVault: MfaSecretVault | undefined;
|
|
450
|
+
#providerSecretVault: ProviderSecretVault | undefined;
|
|
451
|
+
#identityProviders: IdentityProviderService | undefined;
|
|
452
|
+
readonly #oidcDiscovery: OidcDiscoveryPort;
|
|
453
|
+
readonly #forgetProviderKeys: (providerId: string) => void;
|
|
285
454
|
readonly #mailDelivery: AuthMailDelivery | undefined;
|
|
455
|
+
readonly #mailLocale: (tenantId: string) => Promise<string> | string;
|
|
286
456
|
readonly #publicBaseUrl: string;
|
|
457
|
+
readonly #metrics: ModuleMetrics | undefined;
|
|
458
|
+
readonly #errorSink: ErrorSink | undefined;
|
|
287
459
|
|
|
288
460
|
constructor(repository: AuthRepository, options: AuthServiceOptions = {}) {
|
|
289
461
|
this.#repository = repository;
|
|
@@ -297,11 +469,18 @@ export class AuthService {
|
|
|
297
469
|
this.#passwordHash = options.passwordHash ?? DEFAULT_PASSWORD_HASH_OPTIONS;
|
|
298
470
|
this.#now = options.now ?? Date.now;
|
|
299
471
|
this.#mfaEncryptionKey = options.mfaEncryptionKey;
|
|
472
|
+
this.#mfaPreviousEncryptionKeys = options.mfaPreviousEncryptionKeys ?? [];
|
|
473
|
+
this.#oidcDiscovery =
|
|
474
|
+
options.oidcDiscovery ?? (() => Promise.reject(discoveryUnavailable()));
|
|
475
|
+
this.#forgetProviderKeys = options.forgetProviderKeys ?? (() => undefined);
|
|
300
476
|
this.#mailDelivery = options.mailDelivery;
|
|
477
|
+
this.#mailLocale = options.mailLocale ?? (() => 'en');
|
|
301
478
|
this.#publicBaseUrl = (options.publicBaseUrl ?? 'http://localhost').replace(
|
|
302
479
|
/\/$/,
|
|
303
480
|
'',
|
|
304
481
|
);
|
|
482
|
+
this.#metrics = options.metrics;
|
|
483
|
+
this.#errorSink = options.errorSink;
|
|
305
484
|
}
|
|
306
485
|
|
|
307
486
|
get policy(): AuthPolicy {
|
|
@@ -312,7 +491,7 @@ export class AuthService {
|
|
|
312
491
|
describe; a failing write is reported and swallowed. */
|
|
313
492
|
async #audit(
|
|
314
493
|
tenantId: string,
|
|
315
|
-
actor:
|
|
494
|
+
actor: AuditActor,
|
|
316
495
|
action: string,
|
|
317
496
|
subjectType: string,
|
|
318
497
|
subjectId: string,
|
|
@@ -325,6 +504,7 @@ export class AuthService {
|
|
|
325
504
|
actorLabel: actor.label,
|
|
326
505
|
actorKind: actor.kind,
|
|
327
506
|
actorRunId: actor.kind === 'agent' ? actor.runId : null,
|
|
507
|
+
configuredBy: actor.kind === 'service' ? actor.configuredBy : null,
|
|
328
508
|
action,
|
|
329
509
|
subjectType,
|
|
330
510
|
subjectId,
|
|
@@ -336,6 +516,13 @@ export class AuthService {
|
|
|
336
516
|
bound values from the operation being audited. */
|
|
337
517
|
void error;
|
|
338
518
|
console.error('[auth.core] audit write failed');
|
|
519
|
+
this.#metrics?.counter('audit_write_failures_total', { action });
|
|
520
|
+
this.#errorSink?.report({
|
|
521
|
+
at: this.#now(),
|
|
522
|
+
name: 'AuditWriteFailed',
|
|
523
|
+
module: 'auth.core',
|
|
524
|
+
message: `Audit write failed for ${action}.`,
|
|
525
|
+
});
|
|
339
526
|
}
|
|
340
527
|
}
|
|
341
528
|
|
|
@@ -463,12 +650,22 @@ export class AuthService {
|
|
|
463
650
|
423,
|
|
464
651
|
);
|
|
465
652
|
}
|
|
466
|
-
const account = await this.#
|
|
653
|
+
const account = await this.#signInAccount(input);
|
|
467
654
|
if (!account) {
|
|
468
655
|
await hashPassword(input.password, this.#passwordHash);
|
|
469
656
|
await this.#recordFailure(input.email, null, context);
|
|
470
657
|
throw this.#invalidCredentials();
|
|
471
658
|
}
|
|
659
|
+
/* An account created without a password answers exactly as an unknown
|
|
660
|
+
address does, down to burning the same derivation: a distinct code here
|
|
661
|
+
would tell an attacker which addresses are registered and not yet
|
|
662
|
+
activated, which is the set most worth attacking. PASSWORD_NOT_SET is
|
|
663
|
+
reported where the caller is already authenticated as the account. */
|
|
664
|
+
if (!passwordIsSet(account.passwordHash)) {
|
|
665
|
+
await hashPassword(input.password, this.#passwordHash);
|
|
666
|
+
await this.#recordFailure(input.email, account, context);
|
|
667
|
+
throw this.#invalidCredentials();
|
|
668
|
+
}
|
|
472
669
|
const valid = await verifyPassword(input.password, account.passwordHash);
|
|
473
670
|
if (!valid || account.status !== 'active') {
|
|
474
671
|
await this.#recordFailure(input.email, account, context);
|
|
@@ -490,6 +687,26 @@ export class AuthService {
|
|
|
490
687
|
return issued;
|
|
491
688
|
}
|
|
492
689
|
|
|
690
|
+
/* The workspace the sign-in screen names is the one the session opens in:
|
|
691
|
+
the membership has to exist there and be active. An unknown workspace and
|
|
692
|
+
a membership the account does not hold answer as a wrong password does,
|
|
693
|
+
so the form never reports which workspaces hold an address. Without a
|
|
694
|
+
named workspace the oldest membership answers, exactly as before. */
|
|
695
|
+
async #signInAccount(input: SignInInput): Promise<AccountCredential | null> {
|
|
696
|
+
if (input.workspace === undefined) {
|
|
697
|
+
return this.#repository.findAccountByEmail(input.email);
|
|
698
|
+
}
|
|
699
|
+
const tenant = await this.findTenant(input.workspace);
|
|
700
|
+
if (!tenant) return null;
|
|
701
|
+
const identity = await this.#repository.findAccountIdentity(input.email);
|
|
702
|
+
if (!identity) return null;
|
|
703
|
+
const membership = await this.#repository.findAccountMembership(
|
|
704
|
+
identity.accountId,
|
|
705
|
+
tenant.tenantId,
|
|
706
|
+
);
|
|
707
|
+
return membership?.membershipStatus === 'active' ? membership : null;
|
|
708
|
+
}
|
|
709
|
+
|
|
493
710
|
async #issueMfaChallenge(
|
|
494
711
|
account: AccountCredential,
|
|
495
712
|
now = this.#now(),
|
|
@@ -557,8 +774,232 @@ export class AuthService {
|
|
|
557
774
|
);
|
|
558
775
|
}
|
|
559
776
|
|
|
560
|
-
|
|
561
|
-
|
|
777
|
+
listTenantMembers(tenantId: string): Promise<readonly TenantMember[]>;
|
|
778
|
+
listTenantMembers(
|
|
779
|
+
tenantId: string,
|
|
780
|
+
page: { readonly cursor?: string | null; readonly limit: number },
|
|
781
|
+
): Promise<TenantMemberPage>;
|
|
782
|
+
/**
|
|
783
|
+
* The members of one workspace. The unbounded call answers the whole roll
|
|
784
|
+
* ordered by display name and stays what every caller holding a workspace of
|
|
785
|
+
* known size already reads. The paged call walks the same members by account
|
|
786
|
+
* id, one bounded page and the cursor of the next, for a caller that must
|
|
787
|
+
* not read a workspace of unknown size in one statement.
|
|
788
|
+
*/
|
|
789
|
+
async listTenantMembers(
|
|
790
|
+
tenantId: string,
|
|
791
|
+
page?: { readonly cursor?: string | null; readonly limit: number },
|
|
792
|
+
): Promise<readonly TenantMember[] | TenantMemberPage> {
|
|
793
|
+
if (page === undefined) return this.#repository.listTenantMembers(tenantId);
|
|
794
|
+
const limit = this.#pageLimit(page.limit, TENANT_MEMBER_PAGE_LIMIT);
|
|
795
|
+
const members = await this.#repository.listTenantMembersPage(
|
|
796
|
+
this.#identifier(tenantId, 'tenantId'),
|
|
797
|
+
page.cursor ?? '',
|
|
798
|
+
limit + 1,
|
|
799
|
+
);
|
|
800
|
+
const answered = members.slice(0, limit);
|
|
801
|
+
const last = answered[answered.length - 1];
|
|
802
|
+
return {
|
|
803
|
+
members: answered,
|
|
804
|
+
nextCursor: members.length > limit && last ? last.accountId : null,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* One page of the workspace's members in the order a screen shows them,
|
|
810
|
+
* by display name or by address, ascending or descending, narrowed by a
|
|
811
|
+
* prefix the way the search narrows and by membership status, all in SQL.
|
|
812
|
+
* The page answers the keyset of its last member when it is full; the
|
|
813
|
+
* caller carries that keyset back as `after` to continue. The paged walk
|
|
814
|
+
* by account id stays for the callers that must not lose a member to a
|
|
815
|
+
* rename mid-walk; this read is for a screen, which shows one page at a time.
|
|
816
|
+
*/
|
|
817
|
+
async listTenantMembersSorted(
|
|
818
|
+
tenantId: string,
|
|
819
|
+
input: TenantMemberSortedInput,
|
|
820
|
+
): Promise<TenantMemberSortedPage> {
|
|
821
|
+
if (!TENANT_MEMBER_SORTS.includes(input.sort)) {
|
|
822
|
+
throw new AuthServiceError(
|
|
823
|
+
'INVALID_INPUT',
|
|
824
|
+
`Sort must be one of ${TENANT_MEMBER_SORTS.join(', ')}.`,
|
|
825
|
+
400,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
if (input.direction !== 'asc' && input.direction !== 'desc') {
|
|
829
|
+
throw new AuthServiceError(
|
|
830
|
+
'INVALID_INPUT',
|
|
831
|
+
'Direction must be asc or desc.',
|
|
832
|
+
400,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const status = input.membershipStatus ?? null;
|
|
836
|
+
if (status !== null && status !== 'active' && status !== 'disabled') {
|
|
837
|
+
throw new AuthServiceError(
|
|
838
|
+
'INVALID_INPUT',
|
|
839
|
+
'Membership status must be active or disabled.',
|
|
840
|
+
400,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const term = normalizeEmail(input.query ?? '');
|
|
844
|
+
if (term.length > TENANT_MEMBER_SEARCH_TERM_LENGTH) {
|
|
845
|
+
throw new AuthServiceError(
|
|
846
|
+
'INVALID_INPUT',
|
|
847
|
+
`Search term must contain at most ${TENANT_MEMBER_SEARCH_TERM_LENGTH} characters.`,
|
|
848
|
+
400,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const after = input.after ?? null;
|
|
852
|
+
if (
|
|
853
|
+
after !== null &&
|
|
854
|
+
(typeof after.sortValue !== 'string' ||
|
|
855
|
+
after.sortValue.length > TENANT_MEMBER_KEYSET_LENGTH)
|
|
856
|
+
) {
|
|
857
|
+
throw new AuthServiceError(
|
|
858
|
+
'INVALID_INPUT',
|
|
859
|
+
'The page keyset is not valid.',
|
|
860
|
+
400,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
return this.#repository.listTenantMembersSorted(
|
|
864
|
+
this.#identifier(tenantId, 'tenantId'),
|
|
865
|
+
{
|
|
866
|
+
sort: input.sort,
|
|
867
|
+
direction: input.direction,
|
|
868
|
+
limit: this.#pageLimit(input.limit, TENANT_MEMBER_PAGE_LIMIT),
|
|
869
|
+
term: term.length === 0 ? null : escapeLikeTerm(term),
|
|
870
|
+
membershipStatus: status,
|
|
871
|
+
after:
|
|
872
|
+
after === null
|
|
873
|
+
? null
|
|
874
|
+
: {
|
|
875
|
+
sortValue: after.sortValue,
|
|
876
|
+
accountId: this.#identifier(after.accountId, 'accountId'),
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** How many members the workspace holds; the count is an index range, so a screen may show it. */
|
|
883
|
+
countTenantMembers(tenantId: string): Promise<number> {
|
|
884
|
+
return this.#repository.countTenantMembers(
|
|
885
|
+
this.#identifier(tenantId, 'tenantId'),
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The identity bindings this workspace's own providers assert about its
|
|
891
|
+
* members, one bounded page per call. A binding names the account, the
|
|
892
|
+
* provider key, the subject the provider keeps stable and when it was first
|
|
893
|
+
* recorded; no token, secret, ciphertext or fingerprint is part of one, and
|
|
894
|
+
* neither is the address the provider reported. A binding a platform
|
|
895
|
+
* provider made carries no workspace and is not answered here. The
|
|
896
|
+
* permission a reader needs is the calling module's to enforce; this is
|
|
897
|
+
* scoped to the workspace it is given and nothing wider.
|
|
898
|
+
*/
|
|
899
|
+
async listExternalIdentities(
|
|
900
|
+
tenantId: string,
|
|
901
|
+
page: { readonly cursor?: string | null; readonly limit: number },
|
|
902
|
+
): Promise<ExternalIdentityPage> {
|
|
903
|
+
const limit = this.#pageLimit(page.limit, EXTERNAL_IDENTITY_PAGE_LIMIT);
|
|
904
|
+
/* A provider key carries no colon, so the first one separates the pair; a
|
|
905
|
+
cursor that is not one starts the walk over rather than half-reading a
|
|
906
|
+
position, the way a malformed audit cursor narrows nothing. */
|
|
907
|
+
const separator = page.cursor?.indexOf(':') ?? -1;
|
|
908
|
+
const after =
|
|
909
|
+
page.cursor && separator > 0
|
|
910
|
+
? {
|
|
911
|
+
provider: page.cursor.slice(0, separator),
|
|
912
|
+
subject: page.cursor.slice(separator + 1),
|
|
913
|
+
}
|
|
914
|
+
: { provider: '', subject: '' };
|
|
915
|
+
const identities = await this.#repository.listExternalIdentitiesPage(
|
|
916
|
+
this.#identifier(tenantId, 'tenantId'),
|
|
917
|
+
after,
|
|
918
|
+
limit + 1,
|
|
919
|
+
);
|
|
920
|
+
const answered = identities.slice(0, limit);
|
|
921
|
+
const last = answered[answered.length - 1];
|
|
922
|
+
return {
|
|
923
|
+
identities: answered,
|
|
924
|
+
nextCursor:
|
|
925
|
+
identities.length > limit && last
|
|
926
|
+
? `${last.provider}:${last.subject}`
|
|
927
|
+
: null,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* The members of one workspace holding any of these addresses. The natural
|
|
933
|
+
* key a bulk caller resolves per batch, answered by the database rather than
|
|
934
|
+
* by listing the workspace and matching in memory. Addresses are folded the
|
|
935
|
+
* way auth.core folds a stored address, so the caller passes what a person
|
|
936
|
+
* typed. An address the workspace does not hold is simply absent from the
|
|
937
|
+
* answer, and an account that exists in another workspace stays invisible
|
|
938
|
+
* here.
|
|
939
|
+
*/
|
|
940
|
+
async findTenantMembersByEmail(
|
|
941
|
+
tenantId: string,
|
|
942
|
+
emails: readonly string[],
|
|
943
|
+
): Promise<readonly TenantMember[]> {
|
|
944
|
+
if (emails.length > TENANT_MEMBER_LOOKUP_LIMIT) {
|
|
945
|
+
throw new AuthServiceError(
|
|
946
|
+
'INVALID_INPUT',
|
|
947
|
+
`At most ${TENANT_MEMBER_LOOKUP_LIMIT} addresses can be looked up at once.`,
|
|
948
|
+
400,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
const normalized = [
|
|
952
|
+
...new Set(emails.map(normalizeEmail).filter((email) => email !== '')),
|
|
953
|
+
];
|
|
954
|
+
return this.#repository.findTenantMembersByEmail(
|
|
955
|
+
this.#identifier(tenantId, 'tenantId'),
|
|
956
|
+
normalized,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Members of one workspace whose display name or address starts with the
|
|
962
|
+
* term, cut to `limit` in the database. The caller ranks what comes back;
|
|
963
|
+
* this only bounds what a search reads.
|
|
964
|
+
*/
|
|
965
|
+
async searchTenantMembers(
|
|
966
|
+
tenantId: string,
|
|
967
|
+
input: { readonly query: string; readonly limit: number },
|
|
968
|
+
): Promise<readonly TenantMember[]> {
|
|
969
|
+
const term = normalizeEmail(input.query);
|
|
970
|
+
if (term.length === 0 || term.length > TENANT_MEMBER_SEARCH_TERM_LENGTH) {
|
|
971
|
+
throw new AuthServiceError(
|
|
972
|
+
'INVALID_INPUT',
|
|
973
|
+
`Search term must contain 1 to ${TENANT_MEMBER_SEARCH_TERM_LENGTH} characters.`,
|
|
974
|
+
400,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
if (
|
|
978
|
+
!Number.isSafeInteger(input.limit) ||
|
|
979
|
+
input.limit < 1 ||
|
|
980
|
+
input.limit > TENANT_MEMBER_SEARCH_LIMIT
|
|
981
|
+
) {
|
|
982
|
+
throw new AuthServiceError(
|
|
983
|
+
'INVALID_INPUT',
|
|
984
|
+
`Limit must be 1 to ${TENANT_MEMBER_SEARCH_LIMIT}.`,
|
|
985
|
+
400,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
return this.#repository.searchTenantMembers(
|
|
989
|
+
this.#identifier(tenantId, 'tenantId'),
|
|
990
|
+
escapeLikeTerm(term),
|
|
991
|
+
input.limit,
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/* A dependent module that needs the role and scopes of one account asks for
|
|
996
|
+
that account: reading the whole workspace to find it costs the roll on
|
|
997
|
+
every decision a member takes. */
|
|
998
|
+
async findTenantMember(
|
|
999
|
+
tenantId: string,
|
|
1000
|
+
accountId: string,
|
|
1001
|
+
): Promise<TenantMember | null> {
|
|
1002
|
+
return this.#repository.findTenantMember(tenantId, accountId);
|
|
562
1003
|
}
|
|
563
1004
|
|
|
564
1005
|
/* Scopes are authorization metadata, not credentials. Dependent modules read
|
|
@@ -605,6 +1046,16 @@ export class AuthService {
|
|
|
605
1046
|
404,
|
|
606
1047
|
);
|
|
607
1048
|
}
|
|
1049
|
+
/* The caller is already authenticated as this account, so naming the cause
|
|
1050
|
+
reveals nothing it does not already know, and "the current password is
|
|
1051
|
+
incorrect" would be a dead end for a member who never had one. */
|
|
1052
|
+
if (!passwordIsSet(account.passwordHash)) {
|
|
1053
|
+
throw new AuthServiceError(
|
|
1054
|
+
'PASSWORD_NOT_SET',
|
|
1055
|
+
'This account has no password yet. Use the password reset link to set the first one.',
|
|
1056
|
+
409,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
608
1059
|
const valid = await verifyPassword(
|
|
609
1060
|
input.currentPassword,
|
|
610
1061
|
account.passwordHash,
|
|
@@ -616,7 +1067,11 @@ export class AuthService {
|
|
|
616
1067
|
401,
|
|
617
1068
|
);
|
|
618
1069
|
}
|
|
619
|
-
assertPasswordPolicy(
|
|
1070
|
+
assertPasswordPolicy(
|
|
1071
|
+
input.newPassword,
|
|
1072
|
+
this.#policy().passwordMinLength,
|
|
1073
|
+
account.email,
|
|
1074
|
+
);
|
|
620
1075
|
if (input.newPassword === input.currentPassword) {
|
|
621
1076
|
throw new AuthServiceError(
|
|
622
1077
|
'PASSWORD_UNCHANGED',
|
|
@@ -800,6 +1255,37 @@ export class AuthService {
|
|
|
800
1255
|
raw,
|
|
801
1256
|
this.#policy().passwordMinLength,
|
|
802
1257
|
);
|
|
1258
|
+
return this.#createTenantMember(
|
|
1259
|
+
input,
|
|
1260
|
+
actor,
|
|
1261
|
+
await hashPassword(input.password, this.#passwordHash),
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* A member who holds no password at all. The account stores the unusable
|
|
1267
|
+
* credential marker rather than a secret nobody keeps: no password can match
|
|
1268
|
+
* it, so a sign-in by password is refused, and the member reaches the
|
|
1269
|
+
* workspace through the reset flow, an administrative temporary password or
|
|
1270
|
+
* an external identity. A bulk creator calls this instead of drawing a random
|
|
1271
|
+
* secret it immediately throws away.
|
|
1272
|
+
*/
|
|
1273
|
+
async createTenantMemberWithoutPassword(
|
|
1274
|
+
raw: CreateTenantMemberWithoutPasswordInput,
|
|
1275
|
+
actor: AuthActor | null = null,
|
|
1276
|
+
): Promise<TenantMember> {
|
|
1277
|
+
return this.#createTenantMember(
|
|
1278
|
+
validateCreateTenantMemberWithoutPassword(raw),
|
|
1279
|
+
actor,
|
|
1280
|
+
UNUSABLE_PASSWORD_HASH,
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async #createTenantMember(
|
|
1285
|
+
input: CreateTenantMemberWithoutPasswordInput,
|
|
1286
|
+
actor: AuthActor | null,
|
|
1287
|
+
passwordHash: string,
|
|
1288
|
+
): Promise<TenantMember> {
|
|
803
1289
|
if (actor && actor.tenantId !== input.tenantId) {
|
|
804
1290
|
throw new AuthServiceError(
|
|
805
1291
|
'TENANT_ACCESS_DENIED',
|
|
@@ -815,7 +1301,6 @@ export class AuthService {
|
|
|
815
1301
|
: input.role === 'owner'
|
|
816
1302
|
? OWNER_SCOPES
|
|
817
1303
|
: MEMBER_SCOPES;
|
|
818
|
-
const passwordHash = await hashPassword(input.password, this.#passwordHash);
|
|
819
1304
|
try {
|
|
820
1305
|
const createdAt = this.#now();
|
|
821
1306
|
const account = await this.#repository.createAccountInTenant({
|
|
@@ -837,7 +1322,12 @@ export class AuthService {
|
|
|
837
1322
|
AUDIT_ACTIONS.memberCreated,
|
|
838
1323
|
'account',
|
|
839
1324
|
account.accountId,
|
|
840
|
-
|
|
1325
|
+
/* Only the credential-less creation is named, so the trail of an
|
|
1326
|
+
ordinary one keeps the shape it had and carries no word about
|
|
1327
|
+
a secret at all. */
|
|
1328
|
+
passwordHash === UNUSABLE_PASSWORD_HASH
|
|
1329
|
+
? { email: account.email, role: input.role, credential: 'none' }
|
|
1330
|
+
: { email: account.email, role: input.role },
|
|
841
1331
|
);
|
|
842
1332
|
}
|
|
843
1333
|
return member(account, createdAt);
|
|
@@ -882,13 +1372,20 @@ export class AuthService {
|
|
|
882
1372
|
return target;
|
|
883
1373
|
}
|
|
884
1374
|
|
|
1375
|
+
/* `counted` says whether the target is one of the active owners
|
|
1376
|
+
countActiveOwners still counts, which is what makes `<= 1` mean "the last
|
|
1377
|
+
one". A caller that changes the membership alone must not read the account
|
|
1378
|
+
block as the membership's own state, or disabling an owner whose
|
|
1379
|
+
membership is already disabled would answer LAST_OWNER instead of doing
|
|
1380
|
+
nothing. */
|
|
885
1381
|
async #assertOwnerRemains(
|
|
886
1382
|
tenantId: string,
|
|
887
1383
|
target: AccountCredential,
|
|
1384
|
+
counted = target.status === 'active',
|
|
888
1385
|
): Promise<void> {
|
|
889
1386
|
if (
|
|
890
1387
|
target.role === 'owner' &&
|
|
891
|
-
|
|
1388
|
+
counted &&
|
|
892
1389
|
(await this.#repository.countActiveOwners(tenantId)) <= 1
|
|
893
1390
|
) {
|
|
894
1391
|
throw new AuthServiceError(
|
|
@@ -989,7 +1486,11 @@ export class AuthService {
|
|
|
989
1486
|
const target = await this.#targetMember(actor, accountId, {
|
|
990
1487
|
allowSelf: false,
|
|
991
1488
|
});
|
|
992
|
-
assertPasswordPolicy(
|
|
1489
|
+
assertPasswordPolicy(
|
|
1490
|
+
temporaryPassword,
|
|
1491
|
+
this.#policy().passwordMinLength,
|
|
1492
|
+
target.email,
|
|
1493
|
+
);
|
|
993
1494
|
await this.#repository.updatePasswordHash(
|
|
994
1495
|
target.accountId,
|
|
995
1496
|
await hashPassword(temporaryPassword, this.#passwordHash),
|
|
@@ -1245,6 +1746,20 @@ export class AuthService {
|
|
|
1245
1746
|
409,
|
|
1246
1747
|
);
|
|
1247
1748
|
}
|
|
1749
|
+
/* A provider provisions into a role by key, and the key is all the row
|
|
1750
|
+
holds. Deleting the role behind it would leave every just-in-time
|
|
1751
|
+
sign-in through that provider refusing silently, so the provider is
|
|
1752
|
+
pointed elsewhere first. */
|
|
1753
|
+
const named = (
|
|
1754
|
+
await this.#repository.listIdentityProviders(actor.tenantId)
|
|
1755
|
+
).find((provider) => provider.jitRole === role.key);
|
|
1756
|
+
if (named) {
|
|
1757
|
+
throw new AuthServiceError(
|
|
1758
|
+
'ROLE_NAMED_BY_PROVIDER',
|
|
1759
|
+
`The identity provider ${named.key} provisions members into this role. Point it at another role first.`,
|
|
1760
|
+
409,
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1248
1763
|
await this.#repository.deleteRole(actor.tenantId, role.id);
|
|
1249
1764
|
await this.#audit(
|
|
1250
1765
|
actor.tenantId,
|
|
@@ -1320,17 +1835,25 @@ export class AuthService {
|
|
|
1320
1835
|
const normalized = normalizeEmail(email);
|
|
1321
1836
|
if (normalized.length > 254) return;
|
|
1322
1837
|
const account = await this.#repository.findAccountByEmail(normalized);
|
|
1323
|
-
if (!account
|
|
1838
|
+
if (!account) return;
|
|
1839
|
+
if (!this.#mailDelivery) {
|
|
1840
|
+
/* The client still gets the non-enumerating success, so the deployment
|
|
1841
|
+
log is the only place this dead end is visible. The address stays out
|
|
1842
|
+
of it. */
|
|
1843
|
+
console.warn(
|
|
1844
|
+
'[auth.core] mail transport is none; password reset for an existing account was not delivered',
|
|
1845
|
+
);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1324
1848
|
const reset = await this.#mintPasswordResetToken(
|
|
1325
1849
|
account.accountId,
|
|
1326
1850
|
PASSWORD_RESET_TTL_MS,
|
|
1327
1851
|
);
|
|
1328
1852
|
try {
|
|
1329
|
-
await this.#mailDelivery.send(
|
|
1330
|
-
to: account.email,
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
});
|
|
1853
|
+
await this.#mailDelivery.send(
|
|
1854
|
+
{ to: account.email, kind: 'password-reset', url: reset.url },
|
|
1855
|
+
await this.#mailLocale(account.tenantId),
|
|
1856
|
+
);
|
|
1334
1857
|
await this.#audit(
|
|
1335
1858
|
account.tenantId,
|
|
1336
1859
|
{ kind: 'user', id: account.accountId, label: account.email },
|
|
@@ -1352,20 +1875,36 @@ export class AuthService {
|
|
|
1352
1875
|
400,
|
|
1353
1876
|
);
|
|
1354
1877
|
}
|
|
1355
|
-
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1878
|
+
const tokenHash = hashSessionToken(token);
|
|
1879
|
+
const named = await this.#repository.findPasswordResetTokenAccount(
|
|
1880
|
+
tokenHash,
|
|
1358
1881
|
this.#now(),
|
|
1359
1882
|
);
|
|
1360
|
-
|
|
1883
|
+
const account = named
|
|
1884
|
+
? await this.#repository.findAccountCredentialById(named)
|
|
1885
|
+
: null;
|
|
1886
|
+
if (!account || account.status !== 'active') {
|
|
1361
1887
|
throw new AuthServiceError(
|
|
1362
1888
|
'RESET_TOKEN_INVALID',
|
|
1363
1889
|
'This password reset link is invalid or has expired.',
|
|
1364
1890
|
400,
|
|
1365
1891
|
);
|
|
1366
1892
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1893
|
+
/* The whole policy, including the address rule, runs on the account the
|
|
1894
|
+
link names before anything spends it: a refused password leaves the
|
|
1895
|
+
link usable instead of stranding the visitor with a dead one. */
|
|
1896
|
+
assertPasswordPolicy(
|
|
1897
|
+
password,
|
|
1898
|
+
this.#policy().passwordMinLength,
|
|
1899
|
+
account.email,
|
|
1900
|
+
);
|
|
1901
|
+
/* Claiming the token is still one statement, so two submissions of the
|
|
1902
|
+
same link cannot both change the password. */
|
|
1903
|
+
const accountId = await this.#repository.consumePasswordResetToken(
|
|
1904
|
+
tokenHash,
|
|
1905
|
+
this.#now(),
|
|
1906
|
+
);
|
|
1907
|
+
if (!accountId) {
|
|
1369
1908
|
throw new AuthServiceError(
|
|
1370
1909
|
'RESET_TOKEN_INVALID',
|
|
1371
1910
|
'This password reset link is invalid or has expired.',
|
|
@@ -1420,11 +1959,10 @@ export class AuthService {
|
|
|
1420
1959
|
createdBy: actor.accountId,
|
|
1421
1960
|
});
|
|
1422
1961
|
try {
|
|
1423
|
-
await this.#mailDelivery.send(
|
|
1424
|
-
to: normalized,
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
});
|
|
1962
|
+
await this.#mailDelivery.send(
|
|
1963
|
+
{ to: normalized, kind: 'tenant-invitation', url: invitation.url },
|
|
1964
|
+
await this.#mailLocale(actor.tenantId),
|
|
1965
|
+
);
|
|
1428
1966
|
} catch {
|
|
1429
1967
|
throw new AuthServiceError(
|
|
1430
1968
|
'MAIL_DELIVERY_FAILED',
|
|
@@ -1534,7 +2072,11 @@ export class AuthService {
|
|
|
1534
2072
|
const displayName = validateDisplayName(input.ownerDisplayName);
|
|
1535
2073
|
const email = validateEmailAddress(input.ownerEmail);
|
|
1536
2074
|
if (input.password !== undefined) {
|
|
1537
|
-
assertPasswordPolicy(
|
|
2075
|
+
assertPasswordPolicy(
|
|
2076
|
+
input.password,
|
|
2077
|
+
this.#policy().passwordMinLength,
|
|
2078
|
+
email,
|
|
2079
|
+
);
|
|
1538
2080
|
}
|
|
1539
2081
|
if (await this.#repository.isTenantSlugTaken(slug)) {
|
|
1540
2082
|
throw new AuthServiceError(
|
|
@@ -1786,6 +2328,45 @@ export class AuthService {
|
|
|
1786
2328
|
};
|
|
1787
2329
|
}
|
|
1788
2330
|
|
|
2331
|
+
/* Built on first use, so a deployment that configures no MFA key still
|
|
2332
|
+
constructs the service and an unusable key is refused where a factor is
|
|
2333
|
+
actually sealed or opened, with MFA_NOT_CONFIGURED. */
|
|
2334
|
+
#vault(): MfaSecretVault {
|
|
2335
|
+
return (this.#mfaVault ??= createMfaSecretVault(
|
|
2336
|
+
this.#mfaEncryptionKey,
|
|
2337
|
+
this.#mfaPreviousEncryptionKeys,
|
|
2338
|
+
));
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/* The same deployment keys under a provider-specific context, refused with
|
|
2342
|
+
PROVIDER_KEY_REQUIRED where a provider secret is actually sealed. */
|
|
2343
|
+
#providerVault(): ProviderSecretVault {
|
|
2344
|
+
return (this.#providerSecretVault ??= createProviderSecretVault(
|
|
2345
|
+
this.#mfaEncryptionKey,
|
|
2346
|
+
this.#mfaPreviousEncryptionKeys,
|
|
2347
|
+
));
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/** Tenant-owned identity providers of the workspaces this service serves. */
|
|
2351
|
+
get identityProviders(): IdentityProviderService {
|
|
2352
|
+
return (this.#identityProviders ??= new IdentityProviderService({
|
|
2353
|
+
repository: this.#repository,
|
|
2354
|
+
vault: () => this.#providerVault(),
|
|
2355
|
+
now: this.#now,
|
|
2356
|
+
discover: this.#oidcDiscovery,
|
|
2357
|
+
forgetKeys: (providerId) => this.#forgetProviderKeys(providerId),
|
|
2358
|
+
audit: (tenantId, actor, action, subjectType, subjectId, metadata) =>
|
|
2359
|
+
this.#audit(
|
|
2360
|
+
tenantId,
|
|
2361
|
+
actor,
|
|
2362
|
+
action,
|
|
2363
|
+
subjectType,
|
|
2364
|
+
subjectId,
|
|
2365
|
+
metadata ?? {},
|
|
2366
|
+
),
|
|
2367
|
+
}));
|
|
2368
|
+
}
|
|
2369
|
+
|
|
1789
2370
|
async enrollTotp(
|
|
1790
2371
|
accountId: string,
|
|
1791
2372
|
issuer = 'Flowdular',
|
|
@@ -1820,7 +2401,7 @@ export class AuthService {
|
|
|
1820
2401
|
const now = this.#now();
|
|
1821
2402
|
await this.#repository.upsertMfaTotp(
|
|
1822
2403
|
account.accountId,
|
|
1823
|
-
|
|
2404
|
+
this.#vault().seal(secret),
|
|
1824
2405
|
now,
|
|
1825
2406
|
);
|
|
1826
2407
|
await this.#repository.replaceMfaRecoveryCodes(
|
|
@@ -1860,6 +2441,31 @@ export class AuthService {
|
|
|
1860
2441
|
};
|
|
1861
2442
|
}
|
|
1862
2443
|
|
|
2444
|
+
/** Whether the account holds a confirmed second factor; one indexed read. */
|
|
2445
|
+
async hasConfirmedMfa(accountId: string): Promise<boolean> {
|
|
2446
|
+
return this.#repository.hasConfirmedMfaTotp(
|
|
2447
|
+
this.#identifier(accountId, 'accountId'),
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
/* An administrator clears a lost factor. The target is resolved inside the
|
|
2452
|
+
acting workspace, so an account that belongs only to another workspace
|
|
2453
|
+
answers exactly as an unknown one does. */
|
|
2454
|
+
async resetMemberMfa(actor: AuthActor, accountId: string): Promise<void> {
|
|
2455
|
+
const target = await this.#targetMember(actor, accountId, {
|
|
2456
|
+
allowSelf: false,
|
|
2457
|
+
});
|
|
2458
|
+
await this.#repository.deleteMfaEnrolment(target.accountId);
|
|
2459
|
+
await this.#audit(
|
|
2460
|
+
actor.tenantId,
|
|
2461
|
+
this.#actorOf(actor),
|
|
2462
|
+
AUDIT_ACTIONS.mfaReset,
|
|
2463
|
+
'account',
|
|
2464
|
+
target.accountId,
|
|
2465
|
+
{ email: target.email },
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
1863
2469
|
async confirmTotp(accountId: string, code: string): Promise<void> {
|
|
1864
2470
|
const account = await this.#repository.findAccountCredentialById(
|
|
1865
2471
|
this.#identifier(accountId, 'accountId'),
|
|
@@ -1871,7 +2477,7 @@ export class AuthService {
|
|
|
1871
2477
|
!account ||
|
|
1872
2478
|
!record ||
|
|
1873
2479
|
!verifyTotp(
|
|
1874
|
-
|
|
2480
|
+
this.#vault().open(record.secretCiphertext, record.keyId),
|
|
1875
2481
|
code,
|
|
1876
2482
|
this.#now(),
|
|
1877
2483
|
)
|
|
@@ -1926,7 +2532,7 @@ export class AuthService {
|
|
|
1926
2532
|
record !== null &&
|
|
1927
2533
|
((typeof code === 'string' &&
|
|
1928
2534
|
verifyTotp(
|
|
1929
|
-
|
|
2535
|
+
this.#vault().open(record.secretCiphertext, record.keyId),
|
|
1930
2536
|
code,
|
|
1931
2537
|
this.#now(),
|
|
1932
2538
|
)) ||
|
|
@@ -1955,14 +2561,54 @@ export class AuthService {
|
|
|
1955
2561
|
return issued;
|
|
1956
2562
|
}
|
|
1957
2563
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2564
|
+
/* The provider subject is the identity; the address it reports only
|
|
2565
|
+
bootstraps the first link. Resolving by subject afterwards is what keeps a
|
|
2566
|
+
reassigned address at the provider from reaching another account here. */
|
|
2567
|
+
async signInExternalIdentity(input: {
|
|
2568
|
+
readonly provider: string;
|
|
2569
|
+
readonly subject: string;
|
|
2570
|
+
readonly email: string;
|
|
2571
|
+
/**
|
|
2572
|
+
* Present when a tenant-owned provider asserted the identity: the binding
|
|
2573
|
+
* lives in that workspace, the session opens in that workspace, and
|
|
2574
|
+
* nothing the provider says reaches another one.
|
|
2575
|
+
*/
|
|
2576
|
+
readonly workspace?: TenantProviderSignIn;
|
|
2577
|
+
}): Promise<IssuedSession | MfaChallenge> {
|
|
2578
|
+
const provider = this.#identifier(input.provider, 'provider');
|
|
2579
|
+
const subject = this.#identifier(input.subject, 'subject');
|
|
2580
|
+
const workspace = input.workspace ?? null;
|
|
2581
|
+
const tenantId = workspace?.tenantId ?? null;
|
|
2582
|
+
const linkedAccountId = await this.#repository.findExternalIdentity(
|
|
2583
|
+
provider,
|
|
2584
|
+
subject,
|
|
2585
|
+
tenantId,
|
|
1963
2586
|
);
|
|
2587
|
+
const account = linkedAccountId
|
|
2588
|
+
? await this.#resolveLinkedAccount(linkedAccountId, tenantId)
|
|
2589
|
+
: await this.#resolveExternalAccount(input.email, provider, workspace);
|
|
1964
2590
|
if (!account || account.status !== 'active')
|
|
1965
2591
|
throw this.#invalidCredentials();
|
|
2592
|
+
if (
|
|
2593
|
+
!linkedAccountId &&
|
|
2594
|
+
(await this.#repository.findExternalIdentitySubject(
|
|
2595
|
+
provider,
|
|
2596
|
+
account.accountId,
|
|
2597
|
+
tenantId,
|
|
2598
|
+
)) !== null
|
|
2599
|
+
) {
|
|
2600
|
+
/* The account already answers to another subject at this provider. An
|
|
2601
|
+
address the provider reports never re-binds it, which is what a
|
|
2602
|
+
reused or transferred address would otherwise do. */
|
|
2603
|
+
throw this.#invalidCredentials();
|
|
2604
|
+
}
|
|
2605
|
+
await this.#repository.linkExternalIdentity({
|
|
2606
|
+
provider,
|
|
2607
|
+
subject,
|
|
2608
|
+
accountId: account.accountId,
|
|
2609
|
+
tenantId,
|
|
2610
|
+
now: this.#now(),
|
|
2611
|
+
});
|
|
1966
2612
|
if (
|
|
1967
2613
|
(await this.#repository.findMfaTotp(account.accountId))?.confirmedAt !=
|
|
1968
2614
|
null
|
|
@@ -1980,6 +2626,193 @@ export class AuthService {
|
|
|
1980
2626
|
return issued;
|
|
1981
2627
|
}
|
|
1982
2628
|
|
|
2629
|
+
/* A binding already made resolves the membership it was made in. A platform
|
|
2630
|
+
binding names no workspace and keeps resolving the account's own. */
|
|
2631
|
+
async #resolveLinkedAccount(
|
|
2632
|
+
accountId: string,
|
|
2633
|
+
tenantId: string | null,
|
|
2634
|
+
): Promise<AccountCredential | null> {
|
|
2635
|
+
if (!tenantId) {
|
|
2636
|
+
return this.#repository.findAccountCredentialById(accountId);
|
|
2637
|
+
}
|
|
2638
|
+
const membership = await this.#repository.findAccountMembership(
|
|
2639
|
+
accountId,
|
|
2640
|
+
tenantId,
|
|
2641
|
+
);
|
|
2642
|
+
return membership?.membershipStatus === 'active' ? membership : null;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
/**
|
|
2646
|
+
* The first sign-in through a provider, before any binding exists. The
|
|
2647
|
+
* verified address only bootstraps the link: for a platform provider it
|
|
2648
|
+
* resolves the account exactly as before, and for a workspace provider it
|
|
2649
|
+
* resolves the membership of that workspace, provisioning one when the
|
|
2650
|
+
* provider is configured to and the address is inside its domains.
|
|
2651
|
+
*/
|
|
2652
|
+
async #resolveExternalAccount(
|
|
2653
|
+
email: string,
|
|
2654
|
+
provider: string,
|
|
2655
|
+
workspace: TenantProviderSignIn | null,
|
|
2656
|
+
): Promise<AccountCredential | null> {
|
|
2657
|
+
const normalized = normalizeEmail(email);
|
|
2658
|
+
if (!workspace) return this.#repository.findAccountByEmail(normalized);
|
|
2659
|
+
const identity = await this.#repository.findAccountIdentity(normalized);
|
|
2660
|
+
if (identity && identity.status !== 'active') return null;
|
|
2661
|
+
if (identity) {
|
|
2662
|
+
const membership = await this.#repository.findAccountMembership(
|
|
2663
|
+
identity.accountId,
|
|
2664
|
+
workspace.tenantId,
|
|
2665
|
+
);
|
|
2666
|
+
if (membership) {
|
|
2667
|
+
return membership.membershipStatus === 'active' ? membership : null;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return this.#provisionExternalMember(
|
|
2671
|
+
normalized,
|
|
2672
|
+
identity?.accountId ?? null,
|
|
2673
|
+
provider,
|
|
2674
|
+
workspace,
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/* Just-in-time provisioning. Every refusal happens before the first write,
|
|
2679
|
+
so a provider that is not allowed to create members creates nothing. */
|
|
2680
|
+
async #provisionExternalMember(
|
|
2681
|
+
normalizedEmail: string,
|
|
2682
|
+
accountId: string | null,
|
|
2683
|
+
provider: string,
|
|
2684
|
+
workspace: TenantProviderSignIn,
|
|
2685
|
+
): Promise<AccountCredential | null> {
|
|
2686
|
+
if (!workspace.jitEnabled) return null;
|
|
2687
|
+
const domain = normalizedEmail.slice(normalizedEmail.lastIndexOf('@') + 1);
|
|
2688
|
+
if (!domain || !workspace.allowedDomains.includes(domain)) return null;
|
|
2689
|
+
/* A tenant-owned provider may not absorb an account other workspaces
|
|
2690
|
+
rely on. Reuse is limited to an account no workspace holds; anyone
|
|
2691
|
+
else is invited by this workspace instead. */
|
|
2692
|
+
if (accountId && (await this.#repository.countMemberships(accountId)) > 0) {
|
|
2693
|
+
await this.#audit(
|
|
2694
|
+
workspace.tenantId,
|
|
2695
|
+
providerActor(provider),
|
|
2696
|
+
AUDIT_ACTIONS.jitRefused,
|
|
2697
|
+
'sign-in',
|
|
2698
|
+
provider,
|
|
2699
|
+
{ provider, reason: 'account-has-other-membership' },
|
|
2700
|
+
);
|
|
2701
|
+
throw new AuthServiceError(
|
|
2702
|
+
'JIT_ACCOUNT_EXISTS',
|
|
2703
|
+
'The account already belongs to another workspace.',
|
|
2704
|
+
401,
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
const role = await this.#repository.findRoleByKey(
|
|
2708
|
+
workspace.tenantId,
|
|
2709
|
+
workspace.jitRole,
|
|
2710
|
+
);
|
|
2711
|
+
/* Saving a provider proves the role exists and deleting that role is
|
|
2712
|
+
refused, so reaching this means the row and the roles drifted apart.
|
|
2713
|
+
The refusal is recorded under its own reason rather than looking like
|
|
2714
|
+
a domain or account refusal. */
|
|
2715
|
+
if (!role) {
|
|
2716
|
+
await this.#audit(
|
|
2717
|
+
workspace.tenantId,
|
|
2718
|
+
providerActor(provider),
|
|
2719
|
+
AUDIT_ACTIONS.jitRefused,
|
|
2720
|
+
'sign-in',
|
|
2721
|
+
provider,
|
|
2722
|
+
{ provider, reason: 'role-missing', role: workspace.jitRole },
|
|
2723
|
+
);
|
|
2724
|
+
return null;
|
|
2725
|
+
}
|
|
2726
|
+
const createdAt = this.#now();
|
|
2727
|
+
const member = accountId
|
|
2728
|
+
? await this.#repository.createMembershipInTenant({
|
|
2729
|
+
accountId,
|
|
2730
|
+
tenantId: workspace.tenantId,
|
|
2731
|
+
role: role.key,
|
|
2732
|
+
roleId: role.id,
|
|
2733
|
+
scopes: role.scopes,
|
|
2734
|
+
createdAt,
|
|
2735
|
+
})
|
|
2736
|
+
: await this.#repository.createAccountInTenant({
|
|
2737
|
+
accountId: randomUUID(),
|
|
2738
|
+
tenantId: workspace.tenantId,
|
|
2739
|
+
email: normalizedEmail,
|
|
2740
|
+
normalizedEmail,
|
|
2741
|
+
/* The account signs in through the provider. A random hash nobody
|
|
2742
|
+
holds keeps the password path closed until a reset sets one. */
|
|
2743
|
+
passwordHash: await hashPassword(
|
|
2744
|
+
randomBytes(32).toString('base64url'),
|
|
2745
|
+
this.#passwordHash,
|
|
2746
|
+
),
|
|
2747
|
+
displayName: displayNameFromEmail(normalizedEmail),
|
|
2748
|
+
role: role.key,
|
|
2749
|
+
roleId: role.id,
|
|
2750
|
+
scopes: role.scopes,
|
|
2751
|
+
createdAt,
|
|
2752
|
+
});
|
|
2753
|
+
await this.#audit(
|
|
2754
|
+
workspace.tenantId,
|
|
2755
|
+
{ kind: 'user', id: member.accountId, label: member.email },
|
|
2756
|
+
AUDIT_ACTIONS.memberProvisioned,
|
|
2757
|
+
'account',
|
|
2758
|
+
member.accountId,
|
|
2759
|
+
{ provider, role: role.key },
|
|
2760
|
+
);
|
|
2761
|
+
return member;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
/**
|
|
2765
|
+
* The workspace's own status for one member. Disabling revokes that
|
|
2766
|
+
* membership's sessions and API tokens and refuses its sign-in; the other
|
|
2767
|
+
* workspaces of the same account are untouched, and the account status stays
|
|
2768
|
+
* the deployment operator's platform-level block.
|
|
2769
|
+
*/
|
|
2770
|
+
async setMembershipStatus(
|
|
2771
|
+
actor: AuthActor,
|
|
2772
|
+
accountId: string,
|
|
2773
|
+
status: MembershipStatus,
|
|
2774
|
+
): Promise<{
|
|
2775
|
+
readonly accountId: string;
|
|
2776
|
+
readonly status: MembershipStatus;
|
|
2777
|
+
}> {
|
|
2778
|
+
const target = await this.#targetMember(actor, accountId, {
|
|
2779
|
+
allowSelf: false,
|
|
2780
|
+
});
|
|
2781
|
+
if (status === 'disabled') {
|
|
2782
|
+
await this.#assertOwnerRemains(
|
|
2783
|
+
actor.tenantId,
|
|
2784
|
+
target,
|
|
2785
|
+
target.status === 'active' && target.membershipStatus === 'active',
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
await this.#repository.setMembershipStatus(
|
|
2789
|
+
target.accountId,
|
|
2790
|
+
actor.tenantId,
|
|
2791
|
+
status,
|
|
2792
|
+
);
|
|
2793
|
+
if (status === 'disabled') {
|
|
2794
|
+
await this.#repository.deleteMembershipSessions(
|
|
2795
|
+
target.accountId,
|
|
2796
|
+
actor.tenantId,
|
|
2797
|
+
);
|
|
2798
|
+
await this.#repository.revokeMembershipApiTokens(
|
|
2799
|
+
actor.tenantId,
|
|
2800
|
+
target.accountId,
|
|
2801
|
+
this.#now(),
|
|
2802
|
+
actor.accountId,
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
await this.#audit(
|
|
2806
|
+
actor.tenantId,
|
|
2807
|
+
this.#actorOf(actor),
|
|
2808
|
+
AUDIT_ACTIONS.membershipStatus,
|
|
2809
|
+
'account',
|
|
2810
|
+
target.accountId,
|
|
2811
|
+
{ status },
|
|
2812
|
+
);
|
|
2813
|
+
return { accountId: target.accountId, status };
|
|
2814
|
+
}
|
|
2815
|
+
|
|
1983
2816
|
async resolveSession(token: string | null): Promise<AuthSession | null> {
|
|
1984
2817
|
if (!token) return null;
|
|
1985
2818
|
return this.#repository.findSession(
|
|
@@ -2051,19 +2884,31 @@ export class AuthService {
|
|
|
2051
2884
|
return this.#repository.deleteExpiredSessions(this.#now());
|
|
2052
2885
|
}
|
|
2053
2886
|
|
|
2887
|
+
/**
|
|
2888
|
+
* The one audit row a committed settings change owes. A flag is recorded
|
|
2889
|
+
* under its own action with the values around the change, because an
|
|
2890
|
+
* operator reviewing a flag needs to see what it was turned to; every other
|
|
2891
|
+
* setting keeps the plain record, so no declared value ever reaches the
|
|
2892
|
+
* trail by the side door.
|
|
2893
|
+
*/
|
|
2054
2894
|
async recordSettingsUpdate(
|
|
2055
2895
|
actor: AuthActor,
|
|
2056
|
-
|
|
2057
|
-
key: string,
|
|
2058
|
-
cleared: boolean,
|
|
2896
|
+
change: ModuleSettingChange,
|
|
2059
2897
|
): Promise<void> {
|
|
2898
|
+
const flag = change.kind === 'flag';
|
|
2060
2899
|
await this.#audit(
|
|
2061
2900
|
actor.tenantId,
|
|
2062
2901
|
this.#actorOf(actor),
|
|
2063
|
-
AUDIT_ACTIONS.settingsUpdated,
|
|
2902
|
+
flag ? AUDIT_ACTIONS.settingsFlagChanged : AUDIT_ACTIONS.settingsUpdated,
|
|
2064
2903
|
'setting',
|
|
2065
|
-
`${moduleId}.${key}`,
|
|
2066
|
-
|
|
2904
|
+
`${change.moduleId}.${change.key}`,
|
|
2905
|
+
flag
|
|
2906
|
+
? {
|
|
2907
|
+
cleared: change.cleared,
|
|
2908
|
+
previous: change.previous,
|
|
2909
|
+
next: change.next,
|
|
2910
|
+
}
|
|
2911
|
+
: { cleared: change.cleared },
|
|
2067
2912
|
);
|
|
2068
2913
|
}
|
|
2069
2914
|
|
|
@@ -2072,9 +2917,22 @@ export class AuthService {
|
|
|
2072
2917
|
Math.max(1, Math.trunc(query.limit)),
|
|
2073
2918
|
MAX_AUDIT_PAGE,
|
|
2074
2919
|
);
|
|
2920
|
+
const from = this.#auditBound(query.from, 'from');
|
|
2921
|
+
const to = this.#auditBound(query.to, 'to');
|
|
2922
|
+
/* A window that ends before it starts would answer an empty page, which a
|
|
2923
|
+
caller cannot tell from a workspace with no events in the window. */
|
|
2924
|
+
if (from !== null && to !== null && from > to) {
|
|
2925
|
+
throw new AuthServiceError(
|
|
2926
|
+
'INVALID_INPUT',
|
|
2927
|
+
'from must not be later than to.',
|
|
2928
|
+
400,
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2075
2931
|
const events = await this.#repository.queryAudit({
|
|
2076
2932
|
...query,
|
|
2077
2933
|
tenantId: this.#identifier(query.tenantId, 'tenantId'),
|
|
2934
|
+
from,
|
|
2935
|
+
to,
|
|
2078
2936
|
limit: limit + 1,
|
|
2079
2937
|
});
|
|
2080
2938
|
const page = events.slice(0, limit);
|
|
@@ -2106,7 +2964,9 @@ export class AuthService {
|
|
|
2106
2964
|
current.principal.accountId,
|
|
2107
2965
|
tenantId,
|
|
2108
2966
|
);
|
|
2109
|
-
|
|
2967
|
+
/* A membership the workspace disabled is not a workspace this account may
|
|
2968
|
+
enter, and it answers exactly as one it never held. */
|
|
2969
|
+
if (!account || account.membershipStatus !== 'active') {
|
|
2110
2970
|
throw new AuthServiceError(
|
|
2111
2971
|
'TENANT_ACCESS_DENIED',
|
|
2112
2972
|
'The requested tenant is not available.',
|
|
@@ -2137,7 +2997,11 @@ export class AuthService {
|
|
|
2137
2997
|
accountId,
|
|
2138
2998
|
tenantId,
|
|
2139
2999
|
);
|
|
2140
|
-
if (
|
|
3000
|
+
if (
|
|
3001
|
+
!membership ||
|
|
3002
|
+
membership.status !== 'active' ||
|
|
3003
|
+
membership.membershipStatus !== 'active'
|
|
3004
|
+
) {
|
|
2141
3005
|
throw new AuthServiceError(
|
|
2142
3006
|
'ACCOUNT_NOT_FOUND',
|
|
2143
3007
|
'The account is not an active member of this workspace.',
|
|
@@ -2256,7 +3120,12 @@ export class AuthService {
|
|
|
2256
3120
|
record.accountId,
|
|
2257
3121
|
record.tenantId,
|
|
2258
3122
|
);
|
|
2259
|
-
if (
|
|
3123
|
+
if (
|
|
3124
|
+
!membership ||
|
|
3125
|
+
membership.status !== 'active' ||
|
|
3126
|
+
membership.membershipStatus !== 'active'
|
|
3127
|
+
)
|
|
3128
|
+
return null;
|
|
2260
3129
|
const held = new Set(membership.scopes);
|
|
2261
3130
|
const scopes = record.scopes.filter((scope) => held.has(scope));
|
|
2262
3131
|
if (scopes.length === 0) return null;
|
|
@@ -2277,6 +3146,34 @@ export class AuthService {
|
|
|
2277
3146
|
};
|
|
2278
3147
|
}
|
|
2279
3148
|
|
|
3149
|
+
/* One end of the audit window. An absent end is open; a bound that is not an
|
|
3150
|
+
epoch millisecond is a caller defect rather than an open end, because
|
|
3151
|
+
silently dropping it would answer a wider trail than was asked for. */
|
|
3152
|
+
#auditBound(value: number | null | undefined, field: string): number | null {
|
|
3153
|
+
if (value === null || value === undefined) return null;
|
|
3154
|
+
if (!Number.isSafeInteger(value)) {
|
|
3155
|
+
throw new AuthServiceError(
|
|
3156
|
+
'INVALID_INPUT',
|
|
3157
|
+
`${field} must be an epoch millisecond timestamp.`,
|
|
3158
|
+
400,
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
return value;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
/* The bound a paged read surface carries into SQL, so no caller of one can
|
|
3165
|
+
ask a workspace of unknown size for all of it. */
|
|
3166
|
+
#pageLimit(limit: number, max: number): number {
|
|
3167
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) {
|
|
3168
|
+
throw new AuthServiceError(
|
|
3169
|
+
'INVALID_INPUT',
|
|
3170
|
+
`Limit must be 1 to ${max}.`,
|
|
3171
|
+
400,
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
return limit;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
2280
3177
|
#identifier(value: string, field: string): string {
|
|
2281
3178
|
const normalized = value.trim();
|
|
2282
3179
|
if (normalized.length < 1 || normalized.length > 128) {
|