@wildo-ai/saas-models 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/applications/applications-context.schemas.d.ts +2 -0
- package/dist/esm/applications/applications-context.schemas.d.ts.map +1 -1
- package/dist/esm/billing/billing-account.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/billing/billing-account.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/billing/billing-account.shared.resources-config.schemas.js +36 -0
- package/dist/esm/billing/billing-account.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/billing/billing-account.shared.schemas.d.ts.map +1 -1
- package/dist/esm/billing/billing-account.shared.schemas.js +8 -1
- package/dist/esm/billing/billing-account.shared.schemas.js.map +1 -1
- package/dist/esm/billing/billing-types.shared.schemas.d.ts +2 -2
- package/dist/esm/billing/credit-pool.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/billing/credit-pool.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/billing/credit-pool.shared.resources-config.schemas.js +39 -0
- package/dist/esm/billing/credit-pool.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/billing/credit-pool.shared.schemas.d.ts +6 -6
- package/dist/esm/billing/credit-pool.shared.schemas.d.ts.map +1 -1
- package/dist/esm/billing/credit-pool.shared.schemas.js +8 -4
- package/dist/esm/billing/credit-pool.shared.schemas.js.map +1 -1
- package/dist/esm/billing/invoice-ref.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/billing/invoice-ref.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/billing/invoice-ref.shared.resources-config.schemas.js +35 -0
- package/dist/esm/billing/invoice-ref.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/billing/invoice-ref.shared.schemas.d.ts.map +1 -1
- package/dist/esm/billing/invoice-ref.shared.schemas.js +8 -3
- package/dist/esm/billing/invoice-ref.shared.schemas.js.map +1 -1
- package/dist/esm/billing/subscription.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/billing/subscription.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/billing/subscription.shared.resources-config.schemas.js +39 -0
- package/dist/esm/billing/subscription.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/billing/subscription.shared.schemas.d.ts +4 -4
- package/dist/esm/billing/subscription.shared.schemas.d.ts.map +1 -1
- package/dist/esm/billing/subscription.shared.schemas.js +6 -2
- package/dist/esm/billing/subscription.shared.schemas.js.map +1 -1
- package/dist/esm/billing/usage-record.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/billing/usage-record.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/billing/usage-record.shared.resources-config.schemas.js +39 -0
- package/dist/esm/billing/usage-record.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/billing/usage-record.shared.schemas.d.ts +4 -4
- package/dist/esm/billing/usage-record.shared.schemas.d.ts.map +1 -1
- package/dist/esm/billing/usage-record.shared.schemas.js +7 -3
- package/dist/esm/billing/usage-record.shared.schemas.js.map +1 -1
- package/dist/esm/compliance/audit-trails/audit-logs.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/compliance/audit-trails/audit-logs.shared.resources-config.schemas.js +35 -2
- package/dist/esm/compliance/audit-trails/audit-logs.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/compliance/audit-trails/auditable-events.shared.schema.d.ts +61 -0
- package/dist/esm/compliance/audit-trails/auditable-events.shared.schema.d.ts.map +1 -1
- package/dist/esm/compliance/audit-trails/auditable-events.shared.schema.js +58 -0
- package/dist/esm/compliance/audit-trails/auditable-events.shared.schema.js.map +1 -1
- package/dist/esm/compliance/audit-trails/security-audit-event-envelope.shared.schema.d.ts.map +1 -1
- package/dist/esm/compliance/audit-trails/security-audit-event-envelope.shared.schema.js +14 -0
- package/dist/esm/compliance/audit-trails/security-audit-event-envelope.shared.schema.js.map +1 -1
- package/dist/esm/compliance/privacy/impersonalization-scrub.shared.utils.d.ts +49 -0
- package/dist/esm/compliance/privacy/impersonalization-scrub.shared.utils.d.ts.map +1 -1
- package/dist/esm/compliance/privacy/impersonalization-scrub.shared.utils.js +124 -1
- package/dist/esm/compliance/privacy/impersonalization-scrub.shared.utils.js.map +1 -1
- package/dist/esm/compliance/privacy/impersonalization.shared.schemas.d.ts +11 -3
- package/dist/esm/compliance/privacy/impersonalization.shared.schemas.d.ts.map +1 -1
- package/dist/esm/compliance/privacy/impersonalization.shared.schemas.js +11 -3
- package/dist/esm/compliance/privacy/impersonalization.shared.schemas.js.map +1 -1
- package/dist/esm/compliance/privacy/operating-jurisdictions.shared.schemas.d.ts +93 -0
- package/dist/esm/compliance/privacy/operating-jurisdictions.shared.schemas.d.ts.map +1 -0
- package/dist/esm/compliance/privacy/operating-jurisdictions.shared.schemas.js +116 -0
- package/dist/esm/compliance/privacy/operating-jurisdictions.shared.schemas.js.map +1 -0
- package/dist/esm/compliance/privacy/operator-compliance-identity.shared.schemas.d.ts +67 -12
- package/dist/esm/compliance/privacy/operator-compliance-identity.shared.schemas.d.ts.map +1 -1
- package/dist/esm/compliance/privacy/operator-compliance-identity.shared.schemas.js +95 -8
- package/dist/esm/compliance/privacy/operator-compliance-identity.shared.schemas.js.map +1 -1
- package/dist/esm/compliance/privacy/redaction.shared.schemas.d.ts +21 -1
- package/dist/esm/compliance/privacy/redaction.shared.schemas.d.ts.map +1 -1
- package/dist/esm/compliance/privacy/redaction.shared.schemas.js +20 -0
- package/dist/esm/compliance/privacy/redaction.shared.schemas.js.map +1 -1
- package/dist/esm/config/app-configuration-shared.shared.schemas.d.ts +2 -0
- package/dist/esm/config/app-configuration-shared.shared.schemas.d.ts.map +1 -1
- package/dist/esm/config/app-configuration-shared.shared.schemas.js +29 -0
- package/dist/esm/config/app-configuration-shared.shared.schemas.js.map +1 -1
- package/dist/esm/config/frontend/navigation.shared.schemas.d.ts +33 -0
- package/dist/esm/config/frontend/navigation.shared.schemas.d.ts.map +1 -1
- package/dist/esm/config/frontend/navigation.shared.schemas.js +32 -0
- package/dist/esm/config/frontend/navigation.shared.schemas.js.map +1 -1
- package/dist/esm/errors/errors.custom-message-ref.shared.definitions.d.ts +12 -1
- package/dist/esm/errors/errors.custom-message-ref.shared.definitions.d.ts.map +1 -1
- package/dist/esm/errors/errors.custom-message-ref.shared.definitions.js +12 -1
- package/dist/esm/errors/errors.custom-message-ref.shared.definitions.js.map +1 -1
- package/dist/esm/external-data/external-data-pipeline.shared.schemas.d.ts +13 -0
- package/dist/esm/external-data/external-data-pipeline.shared.schemas.d.ts.map +1 -1
- package/dist/esm/external-data/external-data-pipeline.shared.schemas.js.map +1 -1
- package/dist/esm/external-providers/engine-capabilities.shared.schemas.d.ts +6 -6
- package/dist/esm/external-providers/engine-capabilities.shared.schemas.d.ts.map +1 -1
- package/dist/esm/external-providers/engine-capabilities.shared.schemas.js +26 -6
- package/dist/esm/external-providers/engine-capabilities.shared.schemas.js.map +1 -1
- package/dist/esm/external-providers/engine-capability-resolution-mode.shared.d.ts.map +1 -1
- package/dist/esm/external-providers/engine-capability-resolution-mode.shared.js +0 -2
- package/dist/esm/external-providers/engine-capability-resolution-mode.shared.js.map +1 -1
- package/dist/esm/external-providers/frontend-bootstrap.shared.schemas.d.ts +1 -0
- package/dist/esm/external-providers/frontend-bootstrap.shared.schemas.d.ts.map +1 -1
- package/dist/esm/external-providers/provider-capability.shared.schemas.d.ts +4 -3
- package/dist/esm/external-providers/provider-capability.shared.schemas.d.ts.map +1 -1
- package/dist/esm/external-providers/provider-capability.shared.schemas.js +4 -3
- package/dist/esm/external-providers/provider-capability.shared.schemas.js.map +1 -1
- package/dist/esm/features/user-features.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/features/user-features.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/features/user-features.shared.resources-config.schemas.js +48 -0
- package/dist/esm/features/user-features.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/files/file-upload-grant.shared.schemas.d.ts +144 -0
- package/dist/esm/files/file-upload-grant.shared.schemas.d.ts.map +1 -0
- package/dist/esm/files/file-upload-grant.shared.schemas.js +136 -0
- package/dist/esm/files/file-upload-grant.shared.schemas.js.map +1 -0
- package/dist/esm/files/files.shared.schemas.d.ts +45 -4
- package/dist/esm/files/files.shared.schemas.d.ts.map +1 -1
- package/dist/esm/files/files.shared.schemas.js +48 -4
- package/dist/esm/files/files.shared.schemas.js.map +1 -1
- package/dist/esm/flows-actors/a2a-push-delivery-log.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/flows-actors/a2a-push-delivery-log.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/flows-actors/a2a-push-delivery-log.shared.resources-config.schemas.js +33 -0
- package/dist/esm/flows-actors/a2a-push-delivery-log.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/flows-actors/a2a-push-delivery-log.shared.schemas.d.ts.map +1 -1
- package/dist/esm/flows-actors/a2a-push-delivery-log.shared.schemas.js +6 -2
- package/dist/esm/flows-actors/a2a-push-delivery-log.shared.schemas.js.map +1 -1
- package/dist/esm/flows-actors/a2a-token-budget-state.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/flows-actors/a2a-token-budget-state.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/flows-actors/a2a-token-budget-state.shared.resources-config.schemas.js +53 -0
- package/dist/esm/flows-actors/a2a-token-budget-state.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/flows-actors/flows-actors-execution.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/flows-actors/flows-actors-execution.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/flows-actors/flows-actors-execution.shared.resources-config.schemas.js +43 -0
- package/dist/esm/flows-actors/flows-actors-execution.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/flows-actors/flows-actors-execution.shared.schemas.d.ts.map +1 -1
- package/dist/esm/flows-actors/flows-actors-execution.shared.schemas.js +8 -3
- package/dist/esm/flows-actors/flows-actors-execution.shared.schemas.js.map +1 -1
- package/dist/esm/flows-actors/flows-actors-task.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/flows-actors/flows-actors-task.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/flows-actors/flows-actors-task.shared.resources-config.schemas.js +41 -0
- package/dist/esm/flows-actors/flows-actors-task.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/flows-actors/flows-actors-task.shared.schemas.d.ts +0 -7
- package/dist/esm/flows-actors/flows-actors-task.shared.schemas.d.ts.map +1 -1
- package/dist/esm/flows-actors/flows-actors-task.shared.schemas.js +23 -13
- package/dist/esm/flows-actors/flows-actors-task.shared.schemas.js.map +1 -1
- package/dist/esm/guidance/guidance-state.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/guidance/guidance-state.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/guidance/guidance-state.shared.resources-config.schemas.js +51 -0
- package/dist/esm/guidance/guidance-state.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/guidance/guidance-state.shared.schemas.d.ts.map +1 -1
- package/dist/esm/guidance/guidance-state.shared.schemas.js +8 -2
- package/dist/esm/guidance/guidance-state.shared.schemas.js.map +1 -1
- package/dist/esm/guidance/lifecycle-state.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/guidance/lifecycle-state.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/guidance/lifecycle-state.shared.resources-config.schemas.js +51 -0
- package/dist/esm/guidance/lifecycle-state.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/guidance/lifecycle-state.shared.schemas.d.ts.map +1 -1
- package/dist/esm/guidance/lifecycle-state.shared.schemas.js +28 -3
- package/dist/esm/guidance/lifecycle-state.shared.schemas.js.map +1 -1
- package/dist/esm/guidance/progression-state.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/guidance/progression-state.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/guidance/progression-state.shared.resources-config.schemas.js +51 -0
- package/dist/esm/guidance/progression-state.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/guidance/progression-state.shared.schemas.d.ts.map +1 -1
- package/dist/esm/guidance/progression-state.shared.schemas.js +18 -4
- package/dist/esm/guidance/progression-state.shared.schemas.js.map +1 -1
- package/dist/esm/http-api-binding/http-api-binding.shared.schemas.d.ts +37 -3
- package/dist/esm/http-api-binding/http-api-binding.shared.schemas.d.ts.map +1 -1
- package/dist/esm/http-api-binding/http-api-binding.shared.schemas.js +29 -3
- package/dist/esm/http-api-binding/http-api-binding.shared.schemas.js.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/notifications/m2m/m2m-webhook-delivery-contract.shared.d.ts +70 -0
- package/dist/esm/notifications/m2m/m2m-webhook-delivery-contract.shared.d.ts.map +1 -0
- package/dist/esm/notifications/m2m/m2m-webhook-delivery-contract.shared.js +71 -0
- package/dist/esm/notifications/m2m/m2m-webhook-delivery-contract.shared.js.map +1 -0
- package/dist/esm/notifications/notification-badges.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/notifications/notification-badges.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/notifications/notification-badges.shared.resources-config.schemas.js +36 -0
- package/dist/esm/notifications/notification-badges.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/organizations/organization-members.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/organizations/organization-members.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/organizations/organization-members.shared.resources-config.schemas.js +44 -0
- package/dist/esm/organizations/organization-members.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/organizations/organization-members.shared.schemas.d.ts.map +1 -1
- package/dist/esm/organizations/organization-members.shared.schemas.js +15 -4
- package/dist/esm/organizations/organization-members.shared.schemas.js.map +1 -1
- package/dist/esm/organizations/organizations.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/organizations/organizations.shared.resources-config.schemas.js +18 -10
- package/dist/esm/organizations/organizations.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/queue/jobs.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/queue/jobs.shared.resources-config.schemas.js +17 -0
- package/dist/esm/queue/jobs.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/requests/websocket.shared.schemas.d.ts +59 -0
- package/dist/esm/requests/websocket.shared.schemas.d.ts.map +1 -1
- package/dist/esm/requests/websocket.shared.schemas.js +103 -0
- package/dist/esm/requests/websocket.shared.schemas.js.map +1 -1
- package/dist/esm/resources/collection-query-parameters.shared.d.ts +22 -0
- package/dist/esm/resources/collection-query-parameters.shared.d.ts.map +1 -0
- package/dist/esm/resources/collection-query-parameters.shared.js +22 -0
- package/dist/esm/resources/collection-query-parameters.shared.js.map +1 -0
- package/dist/esm/resources/resources-config.shared.factory.d.ts.map +1 -1
- package/dist/esm/resources/resources-config.shared.factory.js +86 -13
- package/dist/esm/resources/resources-config.shared.factory.js.map +1 -1
- package/dist/esm/resources/resources-config.shared.schemas.d.ts +162 -41
- package/dist/esm/resources/resources-config.shared.schemas.d.ts.map +1 -1
- package/dist/esm/resources/resources-config.shared.schemas.js +109 -20
- package/dist/esm/resources/resources-config.shared.schemas.js.map +1 -1
- package/dist/esm/resources/utils/resources-config-operations.dtos-builder.utils.d.ts.map +1 -1
- package/dist/esm/resources/utils/resources-config-operations.dtos-builder.utils.js +21 -6
- package/dist/esm/resources/utils/resources-config-operations.dtos-builder.utils.js.map +1 -1
- package/dist/esm/security/authentications/authentication.shared.schemas.d.ts +57 -0
- package/dist/esm/security/authentications/authentication.shared.schemas.d.ts.map +1 -1
- package/dist/esm/security/authentications/authentication.shared.schemas.js +64 -0
- package/dist/esm/security/authentications/authentication.shared.schemas.js.map +1 -1
- package/dist/esm/security/authentications/consumable-token.shared.schemas.d.ts +17 -1
- package/dist/esm/security/authentications/consumable-token.shared.schemas.d.ts.map +1 -1
- package/dist/esm/security/authentications/consumable-token.shared.schemas.js +16 -0
- package/dist/esm/security/authentications/consumable-token.shared.schemas.js.map +1 -1
- package/dist/esm/security/authentications/mfa-backup-code-contract.shared.d.ts +28 -0
- package/dist/esm/security/authentications/mfa-backup-code-contract.shared.d.ts.map +1 -0
- package/dist/esm/security/authentications/mfa-backup-code-contract.shared.js +28 -0
- package/dist/esm/security/authentications/mfa-backup-code-contract.shared.js.map +1 -0
- package/dist/esm/security/authentications/siem/siem-delivery-reliability-contract.shared.d.ts +21 -0
- package/dist/esm/security/authentications/siem/siem-delivery-reliability-contract.shared.d.ts.map +1 -0
- package/dist/esm/security/authentications/siem/siem-delivery-reliability-contract.shared.js +21 -0
- package/dist/esm/security/authentications/siem/siem-delivery-reliability-contract.shared.js.map +1 -0
- package/dist/esm/security/authorizations/platform-access-grants.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/security/authorizations/platform-access-grants.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/security/authorizations/platform-access-grants.shared.resources-config.schemas.js +43 -0
- package/dist/esm/security/authorizations/platform-access-grants.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/security/authorizations/platform-access-grants.shared.schemas.d.ts +0 -8
- package/dist/esm/security/authorizations/platform-access-grants.shared.schemas.d.ts.map +1 -1
- package/dist/esm/security/authorizations/platform-access-grants.shared.schemas.js +13 -4
- package/dist/esm/security/authorizations/platform-access-grants.shared.schemas.js.map +1 -1
- package/dist/esm/security/authorizations/roles.shared.schemas.d.ts +16 -1
- package/dist/esm/security/authorizations/roles.shared.schemas.d.ts.map +1 -1
- package/dist/esm/security/authorizations/roles.shared.schemas.js +16 -1
- package/dist/esm/security/authorizations/roles.shared.schemas.js.map +1 -1
- package/dist/esm/security/authorizations/roles.shared.utils.d.ts +54 -0
- package/dist/esm/security/authorizations/roles.shared.utils.d.ts.map +1 -1
- package/dist/esm/security/authorizations/roles.shared.utils.js +59 -0
- package/dist/esm/security/authorizations/roles.shared.utils.js.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-clients.shared.schemas.d.ts +61 -0
- package/dist/esm/security/oauth-clients/oauth-clients.shared.schemas.d.ts.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-clients.shared.schemas.js +13 -1
- package/dist/esm/security/oauth-clients/oauth-clients.shared.schemas.js.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-consent-grants.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/security/oauth-clients/oauth-consent-grants.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-consent-grants.shared.resources-config.schemas.js +35 -0
- package/dist/esm/security/oauth-clients/oauth-consent-grants.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-consent-grants.shared.schemas.d.ts.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-consent-grants.shared.schemas.js +7 -1
- package/dist/esm/security/oauth-clients/oauth-consent-grants.shared.schemas.js.map +1 -1
- package/dist/esm/users/inbound-contacts.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/users/inbound-contacts.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/users/inbound-contacts.shared.resources-config.schemas.js +34 -0
- package/dist/esm/users/inbound-contacts.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/users/inbound-contacts.shared.schemas.d.ts.map +1 -1
- package/dist/esm/users/inbound-contacts.shared.schemas.js +7 -4
- package/dist/esm/users/inbound-contacts.shared.schemas.js.map +1 -1
- package/dist/esm/users/user-credentials.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/users/user-credentials.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-credentials.shared.resources-config.schemas.js +37 -0
- package/dist/esm/users/user-credentials.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/users/user-credentials.shared.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-credentials.shared.schemas.js +17 -2
- package/dist/esm/users/user-credentials.shared.schemas.js.map +1 -1
- package/dist/esm/users/user-identity-links.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/users/user-identity-links.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-identity-links.shared.resources-config.schemas.js +36 -0
- package/dist/esm/users/user-identity-links.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/users/user-identity-links.shared.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-identity-links.shared.schemas.js +15 -5
- package/dist/esm/users/user-identity-links.shared.schemas.js.map +1 -1
- package/dist/esm/users/user-preferences.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/users/user-preferences.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-preferences.shared.resources-config.schemas.js +53 -0
- package/dist/esm/users/user-preferences.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/users/user-profiles.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/users/user-profiles.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-profiles.shared.resources-config.schemas.js +62 -0
- package/dist/esm/users/user-profiles.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/users/user-self-preferences.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/users/user-self-preferences.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-self-preferences.shared.resources-config.schemas.js +53 -0
- package/dist/esm/users/user-self-preferences.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/users/user-self-profiles.shared.resources-config.schemas.d.ts +1 -1
- package/dist/esm/users/user-self-profiles.shared.resources-config.schemas.d.ts.map +1 -1
- package/dist/esm/users/user-self-profiles.shared.resources-config.schemas.js +62 -0
- package/dist/esm/users/user-self-profiles.shared.resources-config.schemas.js.map +1 -1
- package/dist/esm/users/users.shared.schemas.d.ts +19 -0
- package/dist/esm/users/users.shared.schemas.d.ts.map +1 -1
- package/dist/esm/users/users.shared.schemas.js +41 -16
- package/dist/esm/users/users.shared.schemas.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -6
- package/dist/esm/.builder.pid +0 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-access-grants.shared.resources-config.schemas.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/platform-access-grants.shared.resources-config.schemas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Platform Access Grants — resource configuration.\n *\n * The operation set is unusual and the asymmetry is the point: the PLATFORM requests, the TENANT\n * decides. No verb lets one side do the other's job.\n *\n * requestAccess — platform-tier, crosses the tenant boundary\n * approve / deny — the TARGET tenant's own administrators, never platform-tier\n * revoke — either side, because either may end an elevation early\n *\n * See `platform-access-grants.shared.schemas.ts` for why the resource exists at all (rungs 4–5 of\n * the operator-access ladder in `.claude/rules/administrative-continuity-floors.md`) and\n * `.claude/plans/platform-access-grants-jit-elevation.md` for the remaining steps.\n */\n\nimport { z } from 'zod';\nimport { PlatformAccessGrantSchema, PlatformAccessGrantStatus, PlatformAccessGrants_Operations } from './platform-access-grants.shared.schemas';\nimport { CoreResourceOperation, CoreResourceType, coreResourceShared_FieldIdentifier } from '../../resources/resources.shared.core.definitions';\nimport {\n ResourceOperationRiskLevel,\n ResourceOperationVariantType,\n type ResourceRelationship,\n} from '../../resources/resources.shared.schemas';\nimport { createResourceConfiguration_Initialization } from '../../resources/resources-config.shared.factory';\nimport { ResourceSystemAccessMode } from '../../resources/resources-config.shared.schemas';\nimport { CoreUserNotificationTarget, CoreUserNotificationChannel } from '../../notifications/notifications.shared.schemas';\nimport { CORE_APP_ROLES, CORE_ORG_ROLES } from './roles.shared.schemas';\n\n\ntype PlatformAccessGrantsCoreOperations =\n | CoreResourceOperation.READ\n | CoreResourceOperation.LIST\n | CoreResourceOperation.COUNT\n | CoreResourceOperation.DELETE;\n\nexport const platformAccessGrantsResourceConfiguration_InitializationFactory = (resourcesRelationships: ResourceRelationship[]) =>\n createResourceConfiguration_Initialization<\n typeof PlatformAccessGrants_Operations,\n PlatformAccessGrantsCoreOperations,\n typeof PlatformAccessGrantSchema\n >({\n mainSchema: PlatformAccessGrantSchema,\n resourceIdentifier: CoreResourceType.PLATFORM_ACCESS_GRANTS,\n resourceFieldIdentifier: coreResourceShared_FieldIdentifier[CoreResourceType.PLATFORM_ACCESS_GRANTS],\n resourceRelationships: resourcesRelationships,\n isSystemResource: true,\n\n /**\n * `read`/`list` are ALLOWED for the trusted path because the authorization layer itself has to\n * resolve a caller's usable grant before admitting a crossing, and it does so on a system context\n * — the caller is by definition not a member of the tenant whose grants are being read.\n *\n * No trusted `create`/`update`: every transition belongs to a custom implementation that applies\n * the tenant policy and the continuity-floor measurement. A system door around those would be a\n * second way to mint an elevation, which is the one thing this resource exists to prevent.\n */\n systemAccessPolicy: {\n read: ResourceSystemAccessMode.ALLOWED,\n list: ResourceSystemAccessMode.ALLOWED,\n\n /**\n * Subject-export disclosure — FORBIDDEN, and the declaration is REQUIRED rather than optional.\n *\n * `operatorUserId` and `approvedByUserId` are foreign keys back to USERS, so\n * `classifySubjectExportDisposition` puts these rows on a user's Art. 15 graph as\n * `INCLUDED_REFERENCES_SUBJECT` — personal data CONCERNING the subject even though the subject\n * does not own it. An included resource carrying NO declaration REFUSES the entire export, so\n * omitting this would trade a frontend boot failure for a broken disclosure right.\n *\n * FORBIDDEN for the reason AUDIT_LOGS is: this is a security/operational monitoring record, and\n * handing the monitoring trail to the person it monitors is self-defeating. It is also not only\n * about them — a grant names the TENANT's approver and carries the tenant's decision reason, so\n * disclosing it to the operator would expose another data subject's decision (Art. 15(4)).\n * FORBIDDEN rows are left OUT of the bundle and RECORDED with that reason, never silently\n * dropped, so the report still answers \"why is this missing?\".\n */\n exportSubject: ResourceSystemAccessMode.FORBIDDEN,\n },\n\n coreOperations: [\n CoreResourceOperation.READ,\n CoreResourceOperation.LIST,\n CoreResourceOperation.COUNT,\n // Present for the tenant-purge cascade ONLY — see the operation's configuration below.\n CoreResourceOperation.DELETE,\n ],\n\n operationsConfiguration: {\n [CoreResourceOperation.READ]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n // Both sides read the same row through the same door: a tenant admin sees elevations\n // raised against their tenant (the consent record is worthless if the consenting party\n // cannot see it), and the platform sees the ones it raised. The ORGANIZATIONS scope does\n // the tenant separation; the platform reaches it through the cross-tenant admission on\n // the verbs below.\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.LOW,\n },\n ],\n },\n\n [CoreResourceOperation.LIST]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.LOW,\n },\n ],\n },\n\n [CoreResourceOperation.COUNT]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.LOW,\n },\n ],\n },\n\n /**\n * PURGE ONLY — there is deliberately no `API_CALL` variant, so no client can delete a grant.\n *\n * The verb exists because grants are a COMPOSITION child of ORGANIZATIONS with\n * `onParentDelete` enabled, and a cascade can only call an operation the child actually\n * declares. Without it the registry's composition-cascade-deletability guard fails — correctly:\n * the cascade would have been declared and then silently unable to run, leaving grant rows\n * behind after their tenant was purged.\n *\n * Route-less is the whole point of the shape. `INTERNAL_CALL` mirrors `organizations.delete`'s\n * own default: the tenant purge may remove these rows, and nobody else may. Erasing a consent\n * record through an API would let either party rewrite the history of an access decision, which\n * is exactly what this resource exists to make impossible.\n */\n [CoreResourceOperation.DELETE]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.INTERNAL_CALL,\n isDefault: true,\n roles: [],\n riskLevel: ResourceOperationRiskLevel.CRITICAL,\n },\n ],\n },\n\n /**\n * The bootstrap verb, and the ONE operation here that must stay reachable without an existing\n * grant — otherwise the mechanism cannot start: an operator would need an elevation to request\n * an elevation. It therefore declares `admitsCrossTenantPlatformAdministration` and must be\n * explicitly exempted when the admission predicate later starts requiring a usable grant\n * (Step 4 of the plan). That exemption is load-bearing and gets its own pin.\n *\n * What keeps the exemption safe is that requesting is not accessing: this verb writes a row in\n * `PENDING_APPROVAL` (or `ACTIVE` when the tenant has not opted into approval) and touches\n * nothing else in the tenant. The blast radius of an abused request is a notification and an\n * audit row naming the operator.\n *\n * assurance-control: WILDO.ACCESS.CROSS_TENANT_ADMINISTRATION — elevation is requested, never assumed.\n * assurance-control: WILDO.ACCESS.PRIVILEGE_EVIDENCE\n */\n [PlatformAccessGrants_Operations.REQUEST_ACCESS]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN],\n riskLevel: ResourceOperationRiskLevel.CRITICAL,\n resourceOperationLike: CoreResourceOperation.CREATE,\n admitsCrossTenantPlatformAdministration: true,\n /**\n * `organizationId` deliberately does NOT appear in this body DTO. The organization is\n * already the mandatory primary-scope path segment and the execution-context creator\n * validates that URL value before dispatch. Requiring it again in JSON made clients\n * author the same security-critical target twice and created an avoidable mismatch case.\n *\n * WHERE AN OPERATOR GETS the path id is still a deliberate product decision. There is\n * no in-product surface that lists organizations the operator does not belong to:\n * `organizations | LIST` is confined to the caller's own memberships. The id therefore\n * arrives with the customer support case, where it is also present in support URLs,\n * organization notifications and the audit trail.\n *\n * Do not widen `organizations | LIST` to make discovery convenient: a browsable\n * directory is the customer list and would restore the standing cross-tenant\n * visibility this workflow exists to avoid.\n */\n requestDto: z.object({\n justification: z.string().min(1).max(1000),\n /** Minutes. Clamped server-side to the 4h ceiling; a longer ask is not refused. */\n requestedDurationMinutes: z.number().int().positive().optional(),\n }),\n },\n ],\n userNotifications: [\n {\n // The tenant learns immediately that an operator asked for access — before it happens,\n // not after. Under the approval posture this notification IS the request; without it,\n // opting in would give a tenant a decision it never hears about.\n target: CoreUserNotificationTarget.ORGANIZATION_USERS,\n channel: CoreUserNotificationChannel.EMAIL,\n },\n ],\n },\n\n /**\n * The tenant's decision. Gated on the TENANT's own roles and deliberately NOT reachable by the\n * platform: an operator who could approve their own request would make the whole Lockbox\n * posture ceremonial. Note the absence of `admitsCrossTenantPlatformAdministration` here — that\n * absence is the control, so do not add it \"for symmetry\".\n */\n [PlatformAccessGrants_Operations.APPROVE]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.HIGH,\n resourceOperationLike: CoreResourceOperation.UPDATE,\n requestDto: z.object({\n decisionReason: z.string().max(1000).optional(),\n }),\n // Only a request still awaiting a decision can be decided. Fails closed server-side, so a\n // replayed approval cannot revive a denied, revoked or expired grant.\n enabledCondition: ({ currentObject }) => currentObject.status === PlatformAccessGrantStatus.PENDING_APPROVAL,\n },\n ],\n },\n\n [PlatformAccessGrants_Operations.DENY]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.MEDIUM,\n resourceOperationLike: CoreResourceOperation.UPDATE,\n requestDto: z.object({\n decisionReason: z.string().max(1000).optional(),\n }),\n enabledCondition: ({ currentObject }) => currentObject.status === PlatformAccessGrantStatus.PENDING_APPROVAL,\n },\n ],\n },\n\n /**\n * Ending an elevation early. Reachable by BOTH sides, which is why it carries the cross-tenant\n * admission while `approve`/`deny` do not: a tenant revokes access it granted, and an operator\n * closes out their own session rather than leaving a live window open until it lapses.\n *\n * Revoking is monotonic — it only ever removes authority — so admitting the platform side costs\n * nothing the reversibility test would object to.\n */\n [PlatformAccessGrants_Operations.REVOKE]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.MEDIUM,\n resourceOperationLike: CoreResourceOperation.UPDATE,\n admitsCrossTenantPlatformAdministration: true,\n requestDto: z.object({\n decisionReason: z.string().max(1000).optional(),\n }),\n enabledCondition: ({ currentObject }) =>\n currentObject.status === PlatformAccessGrantStatus.PENDING_APPROVAL\n || currentObject.status === PlatformAccessGrantStatus.ACTIVE,\n },\n ],\n },\n },\n // NOTE: the ORGANIZATIONS primary scope is NOT declared here. It is derived from the registry\n // relationship carrying `isPrimaryScope: true` (ORGANIZATIONS → PLATFORM_ACCESS_GRANTS), which is\n // the single place scope is expressed for every resource. Declaring it twice would let the two\n // drift, and the relationship is the one the authorizer actually reads.\n });\n"]}
|
|
1
|
+
{"version":3,"file":"platform-access-grants.shared.resources-config.schemas.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/platform-access-grants.shared.resources-config.schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,+BAA+B,EAAE,MAAM,yCAAyC,CAAC;AAChJ,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,kCAAkC,EAAE,MAAM,mDAAmD,CAAC;AAChJ,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,GAE7B,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,0CAA0C,EAAE,MAAM,iDAAiD,CAAC;AAC7G,OAAO,EAAE,wBAAwB,EAAE,MAAM,iDAAiD,CAAC;AAC3F,OAAO,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,kDAAkD,CAAC;AAC3H,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,2DAA2D,CAAC;AAW1H,MAAM,CAAC,MAAM,+DAA+D,GAAG,CAAC,sBAA8C,EAAE,EAAE,CAChI,0CAA0C,CAIxC;IACA,UAAU,EAAE,yBAAyB;IACrC,kBAAkB,EAAE,gBAAgB,CAAC,sBAAsB;IAC3D,uBAAuB,EAAE,kCAAkC,CAAC,gBAAgB,CAAC,sBAAsB,CAAC;IACpG,qBAAqB,EAAE,sBAAsB;IAC7C,gBAAgB,EAAE,IAAI;IAEtB;;;;;;;;OAQG;IACH,kBAAkB,EAAE;QAClB,IAAI,EAAE,wBAAwB,CAAC,OAAO;QACtC,IAAI,EAAE,wBAAwB,CAAC,OAAO;QAEtC;;;;;;;;;;;;;;;WAeG;QACH,aAAa,EAAE,wBAAwB,CAAC,SAAS;KAClD;IAED;;;;;;;;;;;;;;OAcG;IACH,eAAe,EAAE;QACf,IAAI,EAAE,oBAAoB,CAAC,wBAAwB;KACpD;IAED;;;OAGG;IACH,iBAAiB,EAAE,EAAE,IAAI,EAAE,uBAAuB,CAAC,YAAY,EAAE;IAEjE,cAAc,EAAE;QACd,qBAAqB,CAAC,IAAI;QAC1B,qBAAqB,CAAC,IAAI;QAC1B,qBAAqB,CAAC,KAAK;QAC3B,oEAAoE;QACpE,qBAAqB,CAAC,WAAW;QACjC,uFAAuF;QACvF,qBAAqB,CAAC,MAAM;KAC7B;IAED,uBAAuB,EAAE;QAEvB;;;;;;;;WAQG;QAEH,CAAC,qBAAqB,CAAC,WAAW,CAAC,EAAE;YAEnC,QAAQ,EAAE,CAAC;oBAET,WAAW,EAAE,4BAA4B,CAAC,aAAa;oBAEvD,SAAS,EAAE,IAAI;oBAEf,KAAK,EAAE,CAAC,cAAc,CAAC,qBAAqB,CAAC;oBAE7C,SAAS,EAAE,0BAA0B,CAAC,QAAQ;iBAE/C,CAAC;SAEH;QAED,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE;YAC5B,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,QAAQ;oBAClD,SAAS,EAAE,IAAI;oBACf,qFAAqF;oBACrF,uFAAuF;oBACvF,yFAAyF;oBACzF,uFAAuF;oBACvF,mBAAmB;oBACnB,KAAK,EAAE,CAAC,cAAc,CAAC,SAAS,CAAC;oBACjC,SAAS,EAAE,0BAA0B,CAAC,GAAG;iBAC1C;aACF;SACF;QAED,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE;YAC5B,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,QAAQ;oBAClD,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,CAAC,cAAc,CAAC,SAAS,CAAC;oBACjC,SAAS,EAAE,0BAA0B,CAAC,GAAG;iBAC1C;aACF;SACF;QAED,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE;YAC7B,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,QAAQ;oBAClD,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,CAAC,cAAc,CAAC,SAAS,CAAC;oBACjC,SAAS,EAAE,0BAA0B,CAAC,GAAG;iBAC1C;aACF;SACF;QAED;;;;;;;;;;;;;WAaG;QACH,CAAC,qBAAqB,CAAC,MAAM,CAAC,EAAE;YAC9B,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,aAAa;oBACvD,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,EAAE;oBACT,SAAS,EAAE,0BAA0B,CAAC,QAAQ;iBAC/C;aACF;SACF;QAED;;;;;;;;;;;;;;WAcG;QACH,CAAC,+BAA+B,CAAC,cAAc,CAAC,EAAE;YAChD,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,QAAQ;oBAClD,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,CAAC,cAAc,CAAC,qBAAqB,CAAC;oBAC7C,SAAS,EAAE,0BAA0B,CAAC,QAAQ;oBAC9C,qBAAqB,EAAE,qBAAqB,CAAC,MAAM;oBACnD,uCAAuC,EAAE,IAAI;oBAC7C;;;;;;;;;;;;;;;uBAeG;oBACH,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC;wBACnB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;wBAC1C,mFAAmF;wBACnF,wBAAwB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;qBACjE,CAAC;iBACH;aACF;YACD,iBAAiB,EAAE;gBACjB;oBACE,uFAAuF;oBACvF,sFAAsF;oBACtF,iEAAiE;oBACjE,MAAM,EAAE,0BAA0B,CAAC,kBAAkB;oBACrD,OAAO,EAAE,2BAA2B,CAAC,KAAK;iBAC3C;aACF;SACF;QAED;;;;;WAKG;QACH,CAAC,+BAA+B,CAAC,OAAO,CAAC,EAAE;YACzC,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,QAAQ;oBAClD,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,CAAC,cAAc,CAAC,SAAS,CAAC;oBACjC,SAAS,EAAE,0BAA0B,CAAC,IAAI;oBAC1C,qBAAqB,EAAE,qBAAqB,CAAC,MAAM;oBACnD,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC;wBACnB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;qBAChD,CAAC;oBACF,0FAA0F;oBAC1F,sEAAsE;oBACtE,gBAAgB,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,KAAK,yBAAyB,CAAC,gBAAgB;iBAC7G;aACF;SACF;QAED,CAAC,+BAA+B,CAAC,IAAI,CAAC,EAAE;YACtC,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,QAAQ;oBAClD,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,CAAC,cAAc,CAAC,SAAS,CAAC;oBACjC,SAAS,EAAE,0BAA0B,CAAC,MAAM;oBAC5C,qBAAqB,EAAE,qBAAqB,CAAC,MAAM;oBACnD,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC;wBACnB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;qBAChD,CAAC;oBACF,gBAAgB,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,KAAK,yBAAyB,CAAC,gBAAgB;iBAC7G;aACF;SACF;QAED;;;;;;;WAOG;QACH,CAAC,+BAA+B,CAAC,MAAM,CAAC,EAAE;YACxC,QAAQ,EAAE;gBACR;oBACE,WAAW,EAAE,4BAA4B,CAAC,QAAQ;oBAClD,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,CAAC,cAAc,CAAC,SAAS,CAAC;oBACjC,SAAS,EAAE,0BAA0B,CAAC,MAAM;oBAC5C,qBAAqB,EAAE,qBAAqB,CAAC,MAAM;oBACnD,uCAAuC,EAAE,IAAI;oBAC7C,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC;wBACnB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;qBAChD,CAAC;oBACF,gBAAgB,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,CACtC,aAAa,CAAC,MAAM,KAAK,yBAAyB,CAAC,gBAAgB;2BAChE,aAAa,CAAC,MAAM,KAAK,yBAAyB,CAAC,MAAM;iBAC/D;aACF;SACF;KACF;IACD,8FAA8F;IAC9F,kGAAkG;IAClG,+FAA+F;IAC/F,wEAAwE;CACzE,CAAC,CAAC","sourcesContent":["/**\n * Platform Access Grants — resource configuration.\n *\n * The operation set is unusual and the asymmetry is the point: the PLATFORM requests, the TENANT\n * decides. No verb lets one side do the other's job.\n *\n * requestAccess — platform-tier, crosses the tenant boundary\n * approve / deny — the TARGET tenant's own administrators, never platform-tier\n * revoke — either side, because either may end an elevation early\n *\n * See `platform-access-grants.shared.schemas.ts` for why the resource exists at all (rungs 4–5 of\n * the operator-access ladder in `.claude/rules/administrative-continuity-floors.md`) and\n * `.claude/plans/platform-access-grants-jit-elevation.md` for the remaining steps.\n */\n\nimport { z } from 'zod';\nimport { PlatformAccessGrantSchema, PlatformAccessGrantStatus, PlatformAccessGrants_Operations } from './platform-access-grants.shared.schemas';\nimport { CoreResourceOperation, CoreResourceType, coreResourceShared_FieldIdentifier } from '../../resources/resources.shared.core.definitions';\nimport {\n ResourceOperationRiskLevel,\n ResourceOperationVariantType,\n type ResourceRelationship,\n} from '../../resources/resources.shared.schemas';\nimport { createResourceConfiguration_Initialization } from '../../resources/resources-config.shared.factory';\nimport { ResourceSystemAccessMode } from '../../resources/resources-config.shared.schemas';\nimport { CoreUserNotificationTarget, CoreUserNotificationChannel } from '../../notifications/notifications.shared.schemas';\nimport { CORE_APP_ROLES, CORE_ORG_ROLES } from './roles.shared.schemas';\nimport { ErasureRetentionMode, ResourceDataSubjectKind } from '../../compliance/privacy/impersonalization.shared.schemas';\n\n\ntype PlatformAccessGrantsCoreOperations =\n | CoreResourceOperation.READ\n | CoreResourceOperation.LIST\n | CoreResourceOperation.COUNT\n | CoreResourceOperation.DELETE\n // Route-less; `retentionPolicy` requires READ + LIST + UPDATE_MANY.\n | CoreResourceOperation.UPDATE_MANY;\n\nexport const platformAccessGrantsResourceConfiguration_InitializationFactory = (resourcesRelationships: ResourceRelationship[]) =>\n createResourceConfiguration_Initialization<\n typeof PlatformAccessGrants_Operations,\n PlatformAccessGrantsCoreOperations,\n typeof PlatformAccessGrantSchema\n >({\n mainSchema: PlatformAccessGrantSchema,\n resourceIdentifier: CoreResourceType.PLATFORM_ACCESS_GRANTS,\n resourceFieldIdentifier: coreResourceShared_FieldIdentifier[CoreResourceType.PLATFORM_ACCESS_GRANTS],\n resourceRelationships: resourcesRelationships,\n isSystemResource: true,\n\n /**\n * `read`/`list` are ALLOWED for the trusted path because the authorization layer itself has to\n * resolve a caller's usable grant before admitting a crossing, and it does so on a system context\n * — the caller is by definition not a member of the tenant whose grants are being read.\n *\n * No trusted `create`/`update`: every transition belongs to a custom implementation that applies\n * the tenant policy and the continuity-floor measurement. A system door around those would be a\n * second way to mint an elevation, which is the one thing this resource exists to prevent.\n */\n systemAccessPolicy: {\n read: ResourceSystemAccessMode.ALLOWED,\n list: ResourceSystemAccessMode.ALLOWED,\n\n /**\n * Subject-export disclosure — FORBIDDEN, and the declaration is REQUIRED rather than optional.\n *\n * `operatorUserId` and `approvedByUserId` are foreign keys back to USERS, so\n * `classifySubjectExportDisposition` puts these rows on a user's Art. 15 graph as\n * `INCLUDED_REFERENCES_SUBJECT` — personal data CONCERNING the subject even though the subject\n * does not own it. An included resource carrying NO declaration REFUSES the entire export, so\n * omitting this would trade a frontend boot failure for a broken disclosure right.\n *\n * FORBIDDEN for the reason AUDIT_LOGS is: this is a security/operational monitoring record, and\n * handing the monitoring trail to the person it monitors is self-defeating. It is also not only\n * about them — a grant names the TENANT's approver and carries the tenant's decision reason, so\n * disclosing it to the operator would expose another data subject's decision (Art. 15(4)).\n * FORBIDDEN rows are left OUT of the bundle and RECORDED with that reason, never silently\n * dropped, so the report still answers \"why is this missing?\".\n */\n exportSubject: ResourceSystemAccessMode.FORBIDDEN,\n },\n\n /**\n * Subject erasure RETAINS this row and scrubs its free text.\n *\n * Note this is a DIFFERENT question from the `exportSubject: FORBIDDEN` above, and the two answers\n * differ deliberately. That one asks whether the monitored operator may READ the monitoring trail —\n * no. This one asks what erasure DOES to the row, and nothing here says erasure must not reach it.\n *\n * The grant survives because its security value is WHO reached WHICH tenant, WHEN, and under whose\n * approval — none of which is scrubbed. What does not survive is `justification` and `decisionReason`:\n * operator- and approver-written prose, and the surface most likely to name a third party in passing.\n * That makes the erasure narrow rather than destructive of the record.\n *\n * Contrast `auditLogs`, which is exempt outright: it is the immutable ledger of what happened,\n * including this erasure, so scrubbing it would destroy the evidence the erasure was performed.\n */\n retentionPolicy: {\n mode: ErasureRetentionMode.RETAIN_AND_IMPERSONALIZE,\n },\n\n /**\n * Erasing one of these rows revokes NO session. The grant is a record ABOUT an operator's access, not\n * the operator; their session ends through the `USERS` row, which is `SELF_PRINCIPAL`.\n */\n dataSubjectPolicy: { kind: ResourceDataSubjectKind.NO_PRINCIPAL },\n\n coreOperations: [\n CoreResourceOperation.READ,\n CoreResourceOperation.LIST,\n CoreResourceOperation.COUNT,\n // Route-less; `retentionPolicy` requires READ + LIST + UPDATE_MANY.\n CoreResourceOperation.UPDATE_MANY,\n // Present for the tenant-purge cascade ONLY — see the operation's configuration below.\n CoreResourceOperation.DELETE,\n ],\n\n operationsConfiguration: {\n\n /*\n\n * Route-less retention plumbing — INTERNAL_CALL registers no route and must not be REPOSITORY_ONLY: the\n\n * retention primitives resolve through `getServiceOperationPathDefault`, which matches only\n\n * INTERNAL_CALL / API_CALL. CRITICAL — it is the verb the impersonalize writer uses to stamp markers.\n\n */\n\n [CoreResourceOperation.UPDATE_MANY]: {\n\n variants: [{\n\n variantType: ResourceOperationVariantType.INTERNAL_CALL,\n\n isDefault: true,\n\n roles: [CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN],\n\n riskLevel: ResourceOperationRiskLevel.CRITICAL,\n\n }],\n\n },\n\n [CoreResourceOperation.READ]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n // Both sides read the same row through the same door: a tenant admin sees elevations\n // raised against their tenant (the consent record is worthless if the consenting party\n // cannot see it), and the platform sees the ones it raised. The ORGANIZATIONS scope does\n // the tenant separation; the platform reaches it through the cross-tenant admission on\n // the verbs below.\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.LOW,\n },\n ],\n },\n\n [CoreResourceOperation.LIST]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.LOW,\n },\n ],\n },\n\n [CoreResourceOperation.COUNT]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.LOW,\n },\n ],\n },\n\n /**\n * PURGE ONLY — there is deliberately no `API_CALL` variant, so no client can delete a grant.\n *\n * The verb exists because grants are a COMPOSITION child of ORGANIZATIONS with\n * `onParentDelete` enabled, and a cascade can only call an operation the child actually\n * declares. Without it the registry's composition-cascade-deletability guard fails — correctly:\n * the cascade would have been declared and then silently unable to run, leaving grant rows\n * behind after their tenant was purged.\n *\n * Route-less is the whole point of the shape. `INTERNAL_CALL` mirrors `organizations.delete`'s\n * own default: the tenant purge may remove these rows, and nobody else may. Erasing a consent\n * record through an API would let either party rewrite the history of an access decision, which\n * is exactly what this resource exists to make impossible.\n */\n [CoreResourceOperation.DELETE]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.INTERNAL_CALL,\n isDefault: true,\n roles: [],\n riskLevel: ResourceOperationRiskLevel.CRITICAL,\n },\n ],\n },\n\n /**\n * The bootstrap verb, and the ONE operation here that must stay reachable without an existing\n * grant — otherwise the mechanism cannot start: an operator would need an elevation to request\n * an elevation. It therefore declares `admitsCrossTenantPlatformAdministration` and must be\n * explicitly exempted when the admission predicate later starts requiring a usable grant\n * (Step 4 of the plan). That exemption is load-bearing and gets its own pin.\n *\n * What keeps the exemption safe is that requesting is not accessing: this verb writes a row in\n * `PENDING_APPROVAL` (or `ACTIVE` when the tenant has not opted into approval) and touches\n * nothing else in the tenant. The blast radius of an abused request is a notification and an\n * audit row naming the operator.\n *\n * assurance-control: WILDO.ACCESS.CROSS_TENANT_ADMINISTRATION — elevation is requested, never assumed.\n * assurance-control: WILDO.ACCESS.PRIVILEGE_EVIDENCE\n */\n [PlatformAccessGrants_Operations.REQUEST_ACCESS]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN],\n riskLevel: ResourceOperationRiskLevel.CRITICAL,\n resourceOperationLike: CoreResourceOperation.CREATE,\n admitsCrossTenantPlatformAdministration: true,\n /**\n * `organizationId` deliberately does NOT appear in this body DTO. The organization is\n * already the mandatory primary-scope path segment and the execution-context creator\n * validates that URL value before dispatch. Requiring it again in JSON made clients\n * author the same security-critical target twice and created an avoidable mismatch case.\n *\n * WHERE AN OPERATOR GETS the path id is still a deliberate product decision. There is\n * no in-product surface that lists organizations the operator does not belong to:\n * `organizations | LIST` is confined to the caller's own memberships. The id therefore\n * arrives with the customer support case, where it is also present in support URLs,\n * organization notifications and the audit trail.\n *\n * Do not widen `organizations | LIST` to make discovery convenient: a browsable\n * directory is the customer list and would restore the standing cross-tenant\n * visibility this workflow exists to avoid.\n */\n requestDto: z.object({\n justification: z.string().min(1).max(1000),\n /** Minutes. Clamped server-side to the 4h ceiling; a longer ask is not refused. */\n requestedDurationMinutes: z.number().int().positive().optional(),\n }),\n },\n ],\n userNotifications: [\n {\n // The tenant learns immediately that an operator asked for access — before it happens,\n // not after. Under the approval posture this notification IS the request; without it,\n // opting in would give a tenant a decision it never hears about.\n target: CoreUserNotificationTarget.ORGANIZATION_USERS,\n channel: CoreUserNotificationChannel.EMAIL,\n },\n ],\n },\n\n /**\n * The tenant's decision. Gated on the TENANT's own roles and deliberately NOT reachable by the\n * platform: an operator who could approve their own request would make the whole Lockbox\n * posture ceremonial. Note the absence of `admitsCrossTenantPlatformAdministration` here — that\n * absence is the control, so do not add it \"for symmetry\".\n */\n [PlatformAccessGrants_Operations.APPROVE]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.HIGH,\n resourceOperationLike: CoreResourceOperation.UPDATE,\n requestDto: z.object({\n decisionReason: z.string().max(1000).optional(),\n }),\n // Only a request still awaiting a decision can be decided. Fails closed server-side, so a\n // replayed approval cannot revive a denied, revoked or expired grant.\n enabledCondition: ({ currentObject }) => currentObject.status === PlatformAccessGrantStatus.PENDING_APPROVAL,\n },\n ],\n },\n\n [PlatformAccessGrants_Operations.DENY]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.MEDIUM,\n resourceOperationLike: CoreResourceOperation.UPDATE,\n requestDto: z.object({\n decisionReason: z.string().max(1000).optional(),\n }),\n enabledCondition: ({ currentObject }) => currentObject.status === PlatformAccessGrantStatus.PENDING_APPROVAL,\n },\n ],\n },\n\n /**\n * Ending an elevation early. Reachable by BOTH sides, which is why it carries the cross-tenant\n * admission while `approve`/`deny` do not: a tenant revokes access it granted, and an operator\n * closes out their own session rather than leaving a live window open until it lapses.\n *\n * Revoking is monotonic — it only ever removes authority — so admitting the platform side costs\n * nothing the reversibility test would object to.\n */\n [PlatformAccessGrants_Operations.REVOKE]: {\n variants: [\n {\n variantType: ResourceOperationVariantType.API_CALL,\n isDefault: true,\n roles: [CORE_ORG_ROLES.ORG_ADMIN],\n riskLevel: ResourceOperationRiskLevel.MEDIUM,\n resourceOperationLike: CoreResourceOperation.UPDATE,\n admitsCrossTenantPlatformAdministration: true,\n requestDto: z.object({\n decisionReason: z.string().max(1000).optional(),\n }),\n enabledCondition: ({ currentObject }) =>\n currentObject.status === PlatformAccessGrantStatus.PENDING_APPROVAL\n || currentObject.status === PlatformAccessGrantStatus.ACTIVE,\n },\n ],\n },\n },\n // NOTE: the ORGANIZATIONS primary scope is NOT declared here. It is derived from the registry\n // relationship carrying `isPrimaryScope: true` (ORGANIZATIONS → PLATFORM_ACCESS_GRANTS), which is\n // the single place scope is expressed for every resource. Declaring it twice would let the two\n // drift, and the relationship is the one the authorizer actually reads.\n });\n"]}
|
|
@@ -94,14 +94,6 @@ export declare enum PlatformAccessGrantAutoApprovalReason {
|
|
|
94
94
|
*/
|
|
95
95
|
TENANT_CANNOT_APPROVE = "tenant_cannot_approve"
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
98
|
-
* The maximum life of a grant, and the default when a request names none.
|
|
99
|
-
*
|
|
100
|
-
* Four hours is a working session, not a working week: long enough that an operator is not
|
|
101
|
-
* re-requesting mid-incident, short enough that a forgotten grant is not a standing privilege by
|
|
102
|
-
* another name. Callers may request LESS; the ceiling is enforced server-side so a client cannot
|
|
103
|
-
* mint a long-lived elevation by asking for one.
|
|
104
|
-
*/
|
|
105
97
|
export declare const PLATFORM_ACCESS_GRANT_DEFAULT_DURATION_MINUTES = 240;
|
|
106
98
|
export declare const PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES = 240;
|
|
107
99
|
/** Statuses from which a grant can still be withdrawn. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-access-grants.shared.schemas.d.ts","sourceRoot":"","sources":["../../../../../src/security/authorizations/platform-access-grants.shared.schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"platform-access-grants.shared.schemas.d.ts","sourceRoot":"","sources":["../../../../../src/security/authorizations/platform-access-grants.shared.schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;GAKG;AACH,oBAAY,yBAAyB;IACnC,wFAAwF;IACxF,gBAAgB,qBAAqB;IACrC,yFAAyF;IACzF,MAAM,WAAW;IACjB,gGAAgG;IAChG,MAAM,WAAW;IACjB;;;;;OAKG;IACH,OAAO,YAAY;IACnB,2EAA2E;IAC3E,OAAO,YAAY;CACpB;AAED;;;;;;;GAOG;AACH,oBAAY,sCAAsC;IAChD,SAAS,cAAc;IACvB,QAAQ,aAAa;IACrB,MAAM,WAAW;IACjB,OAAO,YAAY;CACpB;AAED,yGAAyG;AACzG,oBAAY,qCAAqC;IAC/C,kGAAkG;IAClG,4BAA4B,iCAAiC;IAC7D;;;;;OAKG;IACH,qBAAqB,0BAA0B;CAChD;AAaD,eAAO,MAAM,8CAA8C,MAAM,CAAC;AAClE,eAAO,MAAM,8CAA8C,MAAM,CAAC;AAElE,0DAA0D;AAC1D,eAAO,MAAM,wCAAwC,EAAE,SAAS,yBAAyB,EAGxF,CAAC;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;iBAiFpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,IAAI,CAAC,mBAAmB,EAAE,QAAQ,GAAG,WAAW,CAAC,GAAG,SAAS,EACpE,GAAG,EAAE,IAAI,GACR,OAAO,CAMT;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,wBAAwB,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAM9G;AAED;;;;;;;;;;GAUG;AACH;;;;;;;;GAQG;AACH,oBAAY,+BAA+B;IACzC,cAAc,kBAAkB;IAChC,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB"}
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
*/
|
|
47
47
|
import { z } from 'zod';
|
|
48
48
|
import { PersonalDataCategory } from '../../compliance/privacy/personal-data-category.shared.schemas.js';
|
|
49
|
+
import { RedactionType } from '../../compliance/privacy/redaction.shared.schemas.js';
|
|
50
|
+
import { addImpersonalizeWith } from '@wildo-ai/zod-decorators';
|
|
49
51
|
/**
|
|
50
52
|
* Lifecycle of one elevation request.
|
|
51
53
|
*
|
|
@@ -106,6 +108,8 @@ export var PlatformAccessGrantAutoApprovalReason;
|
|
|
106
108
|
* another name. Callers may request LESS; the ceiling is enforced server-side so a client cannot
|
|
107
109
|
* mint a long-lived elevation by asking for one.
|
|
108
110
|
*/
|
|
111
|
+
/** Fixed replacement for grant free-text that cannot be cleared (a `.min(1)` field). */
|
|
112
|
+
const ERASED_GRANT_TEXT = '[erased]';
|
|
109
113
|
export const PLATFORM_ACCESS_GRANT_DEFAULT_DURATION_MINUTES = 240;
|
|
110
114
|
export const PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES = 240;
|
|
111
115
|
/** Statuses from which a grant can still be withdrawn. */
|
|
@@ -135,8 +139,12 @@ export const PlatformAccessGrantSchema = z.object({
|
|
|
135
139
|
// USER_CONTENT — free prose an OPERATOR typed, about why they need to reach a customer tenant.
|
|
136
140
|
// The data subject here is the operator, not the tenant's users; a notice covering staff must
|
|
137
141
|
// enumerate it, and it is exactly the free-text surface that can name a third party in passing.
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
// MASK on erasure, not REMOVE: `.min(1)` makes this non-clearable, so a null would be rejected by the
|
|
143
|
+
// strict update-DTO parse the impersonalize writer runs. A fixed non-empty string is accepted.
|
|
144
|
+
// It must be scrubbed at all because it is free text an operator wrote about why they needed
|
|
145
|
+
// access — exactly the surface that names a third party in passing.
|
|
146
|
+
justification: addImpersonalizeWith(z.string().min(1).max(1000).isSummaryField()
|
|
147
|
+
.dataCategories(PersonalDataCategory.USER_CONTENT), RedactionType.MASK, { maskValue: ERASED_GRANT_TEXT }),
|
|
140
148
|
status: z.enum(PlatformAccessGrantStatus)
|
|
141
149
|
.systemEnum('PlatformAccessGrantStatus')
|
|
142
150
|
.default(PlatformAccessGrantStatus.PENDING_APPROVAL)
|
|
@@ -169,8 +177,9 @@ export const PlatformAccessGrantSchema = z.object({
|
|
|
169
177
|
.excludeFromUpdate()
|
|
170
178
|
.isAuditEvidence(),
|
|
171
179
|
/** The tenant's reason for refusing, or the revoker's reason for withdrawing. */
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
// REMOVE — free text again, and clearable (`.optional()`).
|
|
181
|
+
decisionReason: addImpersonalizeWith(z.string().max(1000).optional().excludeFromCreate().excludeFromUpdate()
|
|
182
|
+
.dataCategories(PersonalDataCategory.USER_CONTENT), RedactionType.REMOVE),
|
|
174
183
|
createdAt: z.date().optional(),
|
|
175
184
|
updatedAt: z.date().optional(),
|
|
176
185
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-access-grants.shared.schemas.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/platform-access-grants.shared.schemas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Platform Access Grants — time-bound, justified, optionally tenant-approved elevation that lets a\n * platform operator cross the tenant boundary.\n *\n * ## Where this sits\n *\n * Wildo's answer to \"may a platform super-administrator administer any tenant?\" is YES, as at most\n * B2B platforms. The question this resource answers is the narrower one that follows: *on what\n * terms?* The progression, recorded in `.claude/rules/administrative-continuity-floors.md`:\n *\n * ambient+unaudited → ambient+audited → declared, deny-by-default → **JIT/time-bound (here)**\n * → customer-approved (here, when the tenant opts in)\n *\n * Declaring a crossing per operation variant (`admitsCrossTenantPlatformAdministration`) removed\n * AMBIENT authority: a crossing must now be asked for in code, reviewed, and specified. It did not\n * remove STANDING authority — every platform administrator still held every declared crossing,\n * permanently, over every tenant. That is the shape zero-standing-privilege programmes exist to\n * eliminate, and what NIST SP 800-53 AC-2 / AC-6(1) mean by least privilege: privileged access is\n * requested, scoped, time-boxed, and expires on its own.\n *\n * A grant is therefore required IN ADDITION to the variant's declaration. The declaration says\n * \"this verb MAY cross\"; the grant says \"this operator MAY cross into THIS tenant, until THIS time,\n * for THIS reason\". Neither is sufficient alone.\n *\n * ## The tenant-approval half (Lockbox)\n *\n * Microsoft Customer Lockbox and Google Access Approval let the CUSTOMER approve provider access\n * before it happens. Wildo models that as {@link PlatformAccessGrantStatus.PENDING_APPROVAL}, gated\n * per tenant so it is an opt-in posture rather than a deployment-wide one — the same shape those\n * products ship, and for the same reason: a tenant that has not staffed an approver must not be\n * unable to receive support.\n *\n * ## ⚠️ The interaction that is easy to get wrong\n *\n * Tenant approval and the administrative-continuity floors are in direct tension. Approval requires\n * an administrator; the floors exist precisely because a tenant can end up WITHOUT one. Requiring a\n * stranded tenant to approve its own repair is circular — the tenant cannot approve, so the repair\n * cannot happen, so the tenant stays stranded.\n *\n * This is resolved by measurement, not by exception: when the target tenant has no administratively\n * usable owner, the grant is auto-approved with\n * {@link PlatformAccessGrantAutoApprovalReason.TENANT_CANNOT_APPROVE} and recorded loudly. The\n * condition is evaluated with the SAME predicate the owner floor uses to refuse writes, so the two\n * can never disagree about whether a tenant can administer itself. An operator cannot manufacture\n * the condition either: stranding a tenant is exactly what the floors refuse.\n */\n\nimport { z } from 'zod';\nimport { PersonalDataCategory } from '../../compliance/privacy/personal-data-category.shared.schemas';\n\n/**\n * Lifecycle of one elevation request.\n *\n * A closed vocabulary with one named runtime source: schemas, guards, fixtures and comparisons all\n * derive from this enum rather than repeating literals.\n */\nexport enum PlatformAccessGrantStatus {\n /** Awaiting a decision from an administrator of the TARGET tenant (Lockbox posture). */\n PENDING_APPROVAL = 'pending_approval',\n /** Usable now. The admission predicate accepts this status, and only while unexpired. */\n ACTIVE = 'active',\n /** The tenant refused. Terminal — a new request must be raised rather than this one retried. */\n DENIED = 'denied',\n /**\n * The window elapsed. Terminal, and reached WITHOUT a writer: the admission predicate compares\n * `expiresAt` against now, so an expired grant stops working whether or not any sweeper has\n * relabelled it. A background transition to this status is bookkeeping for humans and reporting,\n * never the control.\n */\n EXPIRED = 'expired',\n /** Withdrawn before expiry, by the tenant or by the platform. Terminal. */\n REVOKED = 'revoked',\n}\n\n/**\n * Which step of a grant's life an audit row records.\n *\n * Distinct from {@link PlatformAccessGrantStatus}: a status is where the grant IS, a transition is\n * what just happened to it. They differ where it matters — an auto-approved grant lands in `ACTIVE`\n * through `REQUESTED`, never through `APPROVED`, and collapsing the two would erase exactly the\n * distinction a customer audit asks about.\n */\nexport enum PlatformAccessGrantLifecycleTransition {\n REQUESTED = 'requested',\n APPROVED = 'approved',\n DENIED = 'denied',\n REVOKED = 'revoked',\n}\n\n/** Why a grant became {@link PlatformAccessGrantStatus.ACTIVE} without a tenant administrator acting. */\nexport enum PlatformAccessGrantAutoApprovalReason {\n /** The tenant has not opted into approval. The default posture, matching Lockbox being opt-in. */\n TENANT_APPROVAL_NOT_REQUIRED = 'tenant_approval_not_required',\n /**\n * The tenant has NO administratively usable owner, so no principal exists who could approve.\n * Evaluated with the owner floor's own predicate — see this file's header for why the alternative\n * (refusing) is circular. Recorded at high severity: this is the one path where operator access\n * proceeds against a tenant that never consented and never could.\n */\n TENANT_CANNOT_APPROVE = 'tenant_cannot_approve',\n}\n\n/**\n * The maximum life of a grant, and the default when a request names none.\n *\n * Four hours is a working session, not a working week: long enough that an operator is not\n * re-requesting mid-incident, short enough that a forgotten grant is not a standing privilege by\n * another name. Callers may request LESS; the ceiling is enforced server-side so a client cannot\n * mint a long-lived elevation by asking for one.\n */\nexport const PLATFORM_ACCESS_GRANT_DEFAULT_DURATION_MINUTES = 240;\nexport const PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES = 240;\n\n/** Statuses from which a grant can still be withdrawn. */\nexport const PLATFORM_ACCESS_GRANT_REVOCABLE_STATUSES: readonly PlatformAccessGrantStatus[] = [\n PlatformAccessGrantStatus.PENDING_APPROVAL,\n PlatformAccessGrantStatus.ACTIVE,\n];\n\nexport const PlatformAccessGrantSchema = z.object({\n _id: z.string().min(1).isPrimaryKey().isSummaryField(),\n\n /**\n * The TARGET tenant — the one being reached into, never the operator's own.\n *\n * This is the scope anchor: the grant is an organization-scoped row so the tenant can see, approve\n * and revoke elevations raised against it. A grant the tenant cannot see would defeat the point of\n * modelling consent at all.\n */\n organizationId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate().isAuditEvidence(),\n\n /** The operator who requested the elevation. Server-authored from the session, never client-supplied. */\n operatorUserId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromCreate().excludeFromUpdate().isAuditEvidence(),\n\n /**\n * Why the operator needs access. REQUIRED, and deliberately not optional anywhere in the flow.\n *\n * An elevation record without a reason answers \"who and when\" but not \"why\", which is the only\n * question a reviewer or a customer actually asks. `AC-2(7)` wants privileged assignment monitored;\n * an unexplained monitored assignment is a log line, not a control.\n */\n // USER_CONTENT — free prose an OPERATOR typed, about why they need to reach a customer tenant.\n // The data subject here is the operator, not the tenant's users; a notice covering staff must\n // enumerate it, and it is exactly the free-text surface that can name a third party in passing.\n justification: z.string().min(1).max(1000).isSummaryField()\n .dataCategories(PersonalDataCategory.USER_CONTENT),\n\n status: z.enum(PlatformAccessGrantStatus)\n .systemEnum('PlatformAccessGrantStatus')\n .default(PlatformAccessGrantStatus.PENDING_APPROVAL)\n .isDBIndexed()\n .isSummaryField()\n .excludeFromCreate()\n .excludeFromUpdate()\n .isAuditEvidence(),\n\n /**\n * When the elevation stops working.\n *\n * Server-authored from the requested duration, clamped to\n * {@link PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES}. The admission predicate reads THIS field\n * rather than trusting `status`, so expiry needs no sweeper to take effect.\n */\n expiresAt: z.date().isDBIndexed().isSummaryField().excludeFromCreate().excludeFromUpdate().isAuditEvidence(),\n\n /** Set when a tenant administrator approved. Absent on an auto-approved grant — see {@link autoApprovalReason}. */\n approvedByUserId: z.string().min(1).optional().isForeignKey().excludeFromCreate().excludeFromUpdate().isAuditEvidence(),\n approvedAt: z.date().optional().excludeFromCreate().excludeFromUpdate(),\n\n /**\n * Present exactly when the grant became ACTIVE with no tenant administrator acting.\n *\n * Its absence on an ACTIVE grant is therefore meaningful: a human in the tenant approved it. That\n * distinction is what a customer audit needs and what a single `approved: true` boolean would erase.\n */\n autoApprovalReason: z.enum(PlatformAccessGrantAutoApprovalReason)\n .systemEnum('PlatformAccessGrantAutoApprovalReason')\n .optional()\n .excludeFromCreate()\n .excludeFromUpdate()\n .isAuditEvidence(),\n\n /** The tenant's reason for refusing, or the revoker's reason for withdrawing. */\n decisionReason: z.string().max(1000).optional().excludeFromCreate().excludeFromUpdate()\n .dataCategories(PersonalDataCategory.USER_CONTENT),\n\n createdAt: z.date().optional(),\n updatedAt: z.date().optional(),\n});\n\nexport type PlatformAccessGrant = z.infer<typeof PlatformAccessGrantSchema>;\n\n/**\n * Is this grant usable RIGHT NOW?\n *\n * The single predicate every consumer must use — the admission check, the tenant's UI, and any\n * report. Two independent conditions, and the expiry half is why this is a function rather than a\n * status comparison: a grant whose window elapsed is unusable the moment it elapses, with no writer\n * involved. Reading `status === ACTIVE` alone would honour a stale row for as long as no sweeper had\n * run, turning a time-bound elevation back into a standing one.\n *\n * `now` is injected so callers can evaluate a grant at a decision time they already fixed, and so\n * this stays testable without clock control.\n */\nexport function isPlatformAccessGrantUsable(\n grant: Pick<PlatformAccessGrant, 'status' | 'expiresAt'> | undefined,\n now: Date,\n): boolean {\n if (!grant) return false;\n if (grant.status !== PlatformAccessGrantStatus.ACTIVE) return false;\n return grant.expiresAt instanceof Date\n ? grant.expiresAt.getTime() > now.getTime()\n : new Date(grant.expiresAt).getTime() > now.getTime();\n}\n\n/**\n * Clamp a requested duration to the server-side ceiling.\n *\n * Applied to every request path, including trusted ones. A caller asking for a longer window is not\n * refused — being told \"no, ask again with a smaller number\" during an incident is friction with no\n * security benefit, since the ceiling is what actually binds. They simply get the ceiling.\n */\nexport function resolvePlatformAccessGrantExpiry(requestedDurationMinutes: number | undefined, now: Date): Date {\n const requested = requestedDurationMinutes && requestedDurationMinutes > 0\n ? requestedDurationMinutes\n : PLATFORM_ACCESS_GRANT_DEFAULT_DURATION_MINUTES;\n const minutes = Math.min(requested, PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES);\n return new Date(now.getTime() + minutes * 60_000);\n}\n\n/**\n * NOTE ON PLACEMENT — this enum lives with the VOCABULARY, not with the resource configuration,\n * and moving it back would reintroduce a real defect.\n *\n * The authorization layer needs `REQUEST_ACCESS` by name (it is the bootstrap verb, exempt from the\n * grant requirement it bootstraps). Importing that value from the resources-config module dragged\n * resource-configuration evaluation into the authorizer's module graph, and schema construction that\n * runs before the Zod decorators are initialised loses decorator metadata — which surfaced, far from\n * here, as the ephemeral-disclosure catalogue losing an operation-level declaration. A vocabulary has\n * no such side effects.\n */\n/**\n * Custom verbs. Ordinary CREATE/UPDATE/DELETE are deliberately NOT exposed:\n *\n * - CREATE would let a caller author `status`, `expiresAt` and `operatorUserId` directly, which are\n * precisely the fields whose server authorship IS the control.\n * - UPDATE would be a second, ungated path to the status transitions `approve`/`deny`/`revoke` own.\n * - DELETE exists but has NO client door — see its configuration below. An elevation either side could\n * erase is not evidence; the terminal statuses (DENIED / EXPIRED / REVOKED) are how a grant ends.\n */\nexport enum PlatformAccessGrants_Operations {\n REQUEST_ACCESS = 'requestAccess',\n APPROVE = 'approve',\n DENY = 'deny',\n REVOKE = 'revoke',\n}\n"]}
|
|
1
|
+
{"version":3,"file":"platform-access-grants.shared.schemas.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/platform-access-grants.shared.schemas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Platform Access Grants — time-bound, justified, optionally tenant-approved elevation that lets a\n * platform operator cross the tenant boundary.\n *\n * ## Where this sits\n *\n * Wildo's answer to \"may a platform super-administrator administer any tenant?\" is YES, as at most\n * B2B platforms. The question this resource answers is the narrower one that follows: *on what\n * terms?* The progression, recorded in `.claude/rules/administrative-continuity-floors.md`:\n *\n * ambient+unaudited → ambient+audited → declared, deny-by-default → **JIT/time-bound (here)**\n * → customer-approved (here, when the tenant opts in)\n *\n * Declaring a crossing per operation variant (`admitsCrossTenantPlatformAdministration`) removed\n * AMBIENT authority: a crossing must now be asked for in code, reviewed, and specified. It did not\n * remove STANDING authority — every platform administrator still held every declared crossing,\n * permanently, over every tenant. That is the shape zero-standing-privilege programmes exist to\n * eliminate, and what NIST SP 800-53 AC-2 / AC-6(1) mean by least privilege: privileged access is\n * requested, scoped, time-boxed, and expires on its own.\n *\n * A grant is therefore required IN ADDITION to the variant's declaration. The declaration says\n * \"this verb MAY cross\"; the grant says \"this operator MAY cross into THIS tenant, until THIS time,\n * for THIS reason\". Neither is sufficient alone.\n *\n * ## The tenant-approval half (Lockbox)\n *\n * Microsoft Customer Lockbox and Google Access Approval let the CUSTOMER approve provider access\n * before it happens. Wildo models that as {@link PlatformAccessGrantStatus.PENDING_APPROVAL}, gated\n * per tenant so it is an opt-in posture rather than a deployment-wide one — the same shape those\n * products ship, and for the same reason: a tenant that has not staffed an approver must not be\n * unable to receive support.\n *\n * ## ⚠️ The interaction that is easy to get wrong\n *\n * Tenant approval and the administrative-continuity floors are in direct tension. Approval requires\n * an administrator; the floors exist precisely because a tenant can end up WITHOUT one. Requiring a\n * stranded tenant to approve its own repair is circular — the tenant cannot approve, so the repair\n * cannot happen, so the tenant stays stranded.\n *\n * This is resolved by measurement, not by exception: when the target tenant has no administratively\n * usable owner, the grant is auto-approved with\n * {@link PlatformAccessGrantAutoApprovalReason.TENANT_CANNOT_APPROVE} and recorded loudly. The\n * condition is evaluated with the SAME predicate the owner floor uses to refuse writes, so the two\n * can never disagree about whether a tenant can administer itself. An operator cannot manufacture\n * the condition either: stranding a tenant is exactly what the floors refuse.\n */\n\nimport { z } from 'zod';\nimport { PersonalDataCategory } from '../../compliance/privacy/personal-data-category.shared.schemas';\nimport { RedactionType } from '../../compliance/privacy/redaction.shared.schemas';\nimport { addImpersonalizeWith } from '@wildo-ai/zod-decorators';\n\n/**\n * Lifecycle of one elevation request.\n *\n * A closed vocabulary with one named runtime source: schemas, guards, fixtures and comparisons all\n * derive from this enum rather than repeating literals.\n */\nexport enum PlatformAccessGrantStatus {\n /** Awaiting a decision from an administrator of the TARGET tenant (Lockbox posture). */\n PENDING_APPROVAL = 'pending_approval',\n /** Usable now. The admission predicate accepts this status, and only while unexpired. */\n ACTIVE = 'active',\n /** The tenant refused. Terminal — a new request must be raised rather than this one retried. */\n DENIED = 'denied',\n /**\n * The window elapsed. Terminal, and reached WITHOUT a writer: the admission predicate compares\n * `expiresAt` against now, so an expired grant stops working whether or not any sweeper has\n * relabelled it. A background transition to this status is bookkeeping for humans and reporting,\n * never the control.\n */\n EXPIRED = 'expired',\n /** Withdrawn before expiry, by the tenant or by the platform. Terminal. */\n REVOKED = 'revoked',\n}\n\n/**\n * Which step of a grant's life an audit row records.\n *\n * Distinct from {@link PlatformAccessGrantStatus}: a status is where the grant IS, a transition is\n * what just happened to it. They differ where it matters — an auto-approved grant lands in `ACTIVE`\n * through `REQUESTED`, never through `APPROVED`, and collapsing the two would erase exactly the\n * distinction a customer audit asks about.\n */\nexport enum PlatformAccessGrantLifecycleTransition {\n REQUESTED = 'requested',\n APPROVED = 'approved',\n DENIED = 'denied',\n REVOKED = 'revoked',\n}\n\n/** Why a grant became {@link PlatformAccessGrantStatus.ACTIVE} without a tenant administrator acting. */\nexport enum PlatformAccessGrantAutoApprovalReason {\n /** The tenant has not opted into approval. The default posture, matching Lockbox being opt-in. */\n TENANT_APPROVAL_NOT_REQUIRED = 'tenant_approval_not_required',\n /**\n * The tenant has NO administratively usable owner, so no principal exists who could approve.\n * Evaluated with the owner floor's own predicate — see this file's header for why the alternative\n * (refusing) is circular. Recorded at high severity: this is the one path where operator access\n * proceeds against a tenant that never consented and never could.\n */\n TENANT_CANNOT_APPROVE = 'tenant_cannot_approve',\n}\n\n/**\n * The maximum life of a grant, and the default when a request names none.\n *\n * Four hours is a working session, not a working week: long enough that an operator is not\n * re-requesting mid-incident, short enough that a forgotten grant is not a standing privilege by\n * another name. Callers may request LESS; the ceiling is enforced server-side so a client cannot\n * mint a long-lived elevation by asking for one.\n */\n/** Fixed replacement for grant free-text that cannot be cleared (a `.min(1)` field). */\nconst ERASED_GRANT_TEXT = '[erased]';\n\nexport const PLATFORM_ACCESS_GRANT_DEFAULT_DURATION_MINUTES = 240;\nexport const PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES = 240;\n\n/** Statuses from which a grant can still be withdrawn. */\nexport const PLATFORM_ACCESS_GRANT_REVOCABLE_STATUSES: readonly PlatformAccessGrantStatus[] = [\n PlatformAccessGrantStatus.PENDING_APPROVAL,\n PlatformAccessGrantStatus.ACTIVE,\n];\n\nexport const PlatformAccessGrantSchema = z.object({\n _id: z.string().min(1).isPrimaryKey().isSummaryField(),\n\n /**\n * The TARGET tenant — the one being reached into, never the operator's own.\n *\n * This is the scope anchor: the grant is an organization-scoped row so the tenant can see, approve\n * and revoke elevations raised against it. A grant the tenant cannot see would defeat the point of\n * modelling consent at all.\n */\n organizationId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate().isAuditEvidence(),\n\n /** The operator who requested the elevation. Server-authored from the session, never client-supplied. */\n operatorUserId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromCreate().excludeFromUpdate().isAuditEvidence(),\n\n /**\n * Why the operator needs access. REQUIRED, and deliberately not optional anywhere in the flow.\n *\n * An elevation record without a reason answers \"who and when\" but not \"why\", which is the only\n * question a reviewer or a customer actually asks. `AC-2(7)` wants privileged assignment monitored;\n * an unexplained monitored assignment is a log line, not a control.\n */\n // USER_CONTENT — free prose an OPERATOR typed, about why they need to reach a customer tenant.\n // The data subject here is the operator, not the tenant's users; a notice covering staff must\n // enumerate it, and it is exactly the free-text surface that can name a third party in passing.\n // MASK on erasure, not REMOVE: `.min(1)` makes this non-clearable, so a null would be rejected by the\n // strict update-DTO parse the impersonalize writer runs. A fixed non-empty string is accepted.\n // It must be scrubbed at all because it is free text an operator wrote about why they needed\n // access — exactly the surface that names a third party in passing.\n justification: addImpersonalizeWith(\n z.string().min(1).max(1000).isSummaryField()\n .dataCategories(PersonalDataCategory.USER_CONTENT),\n RedactionType.MASK,\n { maskValue: ERASED_GRANT_TEXT },\n ),\n\n status: z.enum(PlatformAccessGrantStatus)\n .systemEnum('PlatformAccessGrantStatus')\n .default(PlatformAccessGrantStatus.PENDING_APPROVAL)\n .isDBIndexed()\n .isSummaryField()\n .excludeFromCreate()\n .excludeFromUpdate()\n .isAuditEvidence(),\n\n /**\n * When the elevation stops working.\n *\n * Server-authored from the requested duration, clamped to\n * {@link PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES}. The admission predicate reads THIS field\n * rather than trusting `status`, so expiry needs no sweeper to take effect.\n */\n expiresAt: z.date().isDBIndexed().isSummaryField().excludeFromCreate().excludeFromUpdate().isAuditEvidence(),\n\n /** Set when a tenant administrator approved. Absent on an auto-approved grant — see {@link autoApprovalReason}. */\n approvedByUserId: z.string().min(1).optional().isForeignKey().excludeFromCreate().excludeFromUpdate().isAuditEvidence(),\n approvedAt: z.date().optional().excludeFromCreate().excludeFromUpdate(),\n\n /**\n * Present exactly when the grant became ACTIVE with no tenant administrator acting.\n *\n * Its absence on an ACTIVE grant is therefore meaningful: a human in the tenant approved it. That\n * distinction is what a customer audit needs and what a single `approved: true` boolean would erase.\n */\n autoApprovalReason: z.enum(PlatformAccessGrantAutoApprovalReason)\n .systemEnum('PlatformAccessGrantAutoApprovalReason')\n .optional()\n .excludeFromCreate()\n .excludeFromUpdate()\n .isAuditEvidence(),\n\n /** The tenant's reason for refusing, or the revoker's reason for withdrawing. */\n // REMOVE — free text again, and clearable (`.optional()`).\n decisionReason: addImpersonalizeWith(\n z.string().max(1000).optional().excludeFromCreate().excludeFromUpdate()\n .dataCategories(PersonalDataCategory.USER_CONTENT),\n RedactionType.REMOVE,\n ),\n\n createdAt: z.date().optional(),\n updatedAt: z.date().optional(),\n});\n\nexport type PlatformAccessGrant = z.infer<typeof PlatformAccessGrantSchema>;\n\n/**\n * Is this grant usable RIGHT NOW?\n *\n * The single predicate every consumer must use — the admission check, the tenant's UI, and any\n * report. Two independent conditions, and the expiry half is why this is a function rather than a\n * status comparison: a grant whose window elapsed is unusable the moment it elapses, with no writer\n * involved. Reading `status === ACTIVE` alone would honour a stale row for as long as no sweeper had\n * run, turning a time-bound elevation back into a standing one.\n *\n * `now` is injected so callers can evaluate a grant at a decision time they already fixed, and so\n * this stays testable without clock control.\n */\nexport function isPlatformAccessGrantUsable(\n grant: Pick<PlatformAccessGrant, 'status' | 'expiresAt'> | undefined,\n now: Date,\n): boolean {\n if (!grant) return false;\n if (grant.status !== PlatformAccessGrantStatus.ACTIVE) return false;\n return grant.expiresAt instanceof Date\n ? grant.expiresAt.getTime() > now.getTime()\n : new Date(grant.expiresAt).getTime() > now.getTime();\n}\n\n/**\n * Clamp a requested duration to the server-side ceiling.\n *\n * Applied to every request path, including trusted ones. A caller asking for a longer window is not\n * refused — being told \"no, ask again with a smaller number\" during an incident is friction with no\n * security benefit, since the ceiling is what actually binds. They simply get the ceiling.\n */\nexport function resolvePlatformAccessGrantExpiry(requestedDurationMinutes: number | undefined, now: Date): Date {\n const requested = requestedDurationMinutes && requestedDurationMinutes > 0\n ? requestedDurationMinutes\n : PLATFORM_ACCESS_GRANT_DEFAULT_DURATION_MINUTES;\n const minutes = Math.min(requested, PLATFORM_ACCESS_GRANT_MAXIMUM_DURATION_MINUTES);\n return new Date(now.getTime() + minutes * 60_000);\n}\n\n/**\n * NOTE ON PLACEMENT — this enum lives with the VOCABULARY, not with the resource configuration,\n * and moving it back would reintroduce a real defect.\n *\n * The authorization layer needs `REQUEST_ACCESS` by name (it is the bootstrap verb, exempt from the\n * grant requirement it bootstraps). Importing that value from the resources-config module dragged\n * resource-configuration evaluation into the authorizer's module graph, and schema construction that\n * runs before the Zod decorators are initialised loses decorator metadata — which surfaced, far from\n * here, as the ephemeral-disclosure catalogue losing an operation-level declaration. A vocabulary has\n * no such side effects.\n */\n/**\n * Custom verbs. Ordinary CREATE/UPDATE/DELETE are deliberately NOT exposed:\n *\n * - CREATE would let a caller author `status`, `expiresAt` and `operatorUserId` directly, which are\n * precisely the fields whose server authorship IS the control.\n * - UPDATE would be a second, ungated path to the status transitions `approve`/`deny`/`revoke` own.\n * - DELETE exists but has NO client door — see its configuration below. An elevation either side could\n * erase is not evidence; the terminal statuses (DENIED / EXPIRED / REVOKED) are how a grant ends.\n */\nexport enum PlatformAccessGrants_Operations {\n REQUEST_ACCESS = 'requestAccess',\n APPROVE = 'approve',\n DENY = 'deny',\n REVOKE = 'revoke',\n}\n"]}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ResourcePrimaryScope } from '../../resources/resources.shared.schemas';
|
|
3
3
|
export declare enum CORE_APP_ROLES {
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Open access: an operation declaring `APP_PUBLIC` requires NO credential, and imposes no role
|
|
6
|
+
* requirement on one that is presented.
|
|
7
|
+
*
|
|
8
|
+
* Not a role anybody is granted — a SENTINEL on the operation's `roles`, which is why the two
|
|
9
|
+
* halves above have to be stated together. "Accessible without auth" was the whole comment until
|
|
10
|
+
* 2026-08-31, and it was read as *reachable only when unauthenticated*: every role gate then
|
|
11
|
+
* refused a credentialed caller, so a public price list, catalogue or i18n bundle answered 200 to
|
|
12
|
+
* an anonymous probe and 403 to a logged-in customer (#153). Authenticating must never take access
|
|
13
|
+
* away.
|
|
14
|
+
*
|
|
15
|
+
* `requiredRolesDeclarePublicAccess` in `roles.shared.utils.ts` is the runtime source of this
|
|
16
|
+
* meaning; every gate consults it rather than re-deriving the test. Scope, tenancy and the
|
|
17
|
+
* repository's contextual filter are untouched by the declaration — it says no role is required to
|
|
18
|
+
* ASK, never that no scope confines the answer.
|
|
19
|
+
*/
|
|
5
20
|
APP_PUBLIC = "APP_PUBLIC",
|
|
6
21
|
APP_ADMIN_SUPER_ADMIN = "APP_ADMIN_SUPER_ADMIN",
|
|
7
22
|
APP_ADMIN_BILLING_MANAGER = "APP_ADMIN_BILLING_MANAGER",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roles.shared.schemas.d.ts","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.schemas.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAC;AAEhF,oBAAY,cAAc;IACxB
|
|
1
|
+
{"version":3,"file":"roles.shared.schemas.d.ts","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.schemas.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAC;AAEhF,oBAAY,cAAc;IACxB;;;;;;;;;;;;;;;OAeG;IACH,UAAU,eAAe;IACzB,qBAAqB,0BAA0B;IAC/C,yBAAyB,8BAA8B;IACvD,0BAA0B,+BAA+B;IACzD,gBAAgB,qBAAqB;IACrC,QAAQ,aAAa;IACrB,iGAAiG;IACjG,aAAa,kBAAkB;CAChC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qCAAqC,EAAE,SAAS,cAAc,EAKzE,CAAC;AAEH,oBAAY,cAAc;IACxB,SAAS,cAAc;IACvB,SAAS,cAAc;IACvB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,UAAU,eAAe;CAC1B;AAGD,eAAO,MAAM,eAAe,2FAG1B,CAAC;AAEH,eAAO,MAAM,WAAW,wGAItB,CAAC;AAEH,eAAO,MAAM,cAAc,sEAGzB,CAAC;AAEH,eAAO,MAAM,cAAc,sEAGzB,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;iBAK/B,CAAC;AAGH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,wBAAwB;;;;;kBAYpC,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAChD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AACxD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
|
@@ -2,7 +2,22 @@ import { z } from 'zod';
|
|
|
2
2
|
import { ResourcePrimaryScope } from '../../resources/resources.shared.schemas.js';
|
|
3
3
|
export var CORE_APP_ROLES;
|
|
4
4
|
(function (CORE_APP_ROLES) {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Open access: an operation declaring `APP_PUBLIC` requires NO credential, and imposes no role
|
|
7
|
+
* requirement on one that is presented.
|
|
8
|
+
*
|
|
9
|
+
* Not a role anybody is granted — a SENTINEL on the operation's `roles`, which is why the two
|
|
10
|
+
* halves above have to be stated together. "Accessible without auth" was the whole comment until
|
|
11
|
+
* 2026-08-31, and it was read as *reachable only when unauthenticated*: every role gate then
|
|
12
|
+
* refused a credentialed caller, so a public price list, catalogue or i18n bundle answered 200 to
|
|
13
|
+
* an anonymous probe and 403 to a logged-in customer (#153). Authenticating must never take access
|
|
14
|
+
* away.
|
|
15
|
+
*
|
|
16
|
+
* `requiredRolesDeclarePublicAccess` in `roles.shared.utils.ts` is the runtime source of this
|
|
17
|
+
* meaning; every gate consults it rather than re-deriving the test. Scope, tenancy and the
|
|
18
|
+
* repository's contextual filter are untouched by the declaration — it says no role is required to
|
|
19
|
+
* ASK, never that no scope confines the answer.
|
|
20
|
+
*/
|
|
6
21
|
CORE_APP_ROLES["APP_PUBLIC"] = "APP_PUBLIC";
|
|
7
22
|
CORE_APP_ROLES["APP_ADMIN_SUPER_ADMIN"] = "APP_ADMIN_SUPER_ADMIN";
|
|
8
23
|
CORE_APP_ROLES["APP_ADMIN_BILLING_MANAGER"] = "APP_ADMIN_BILLING_MANAGER";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roles.shared.schemas.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.schemas.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAC;AAEhF,MAAM,CAAN,IAAY,
|
|
1
|
+
{"version":3,"file":"roles.shared.schemas.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.schemas.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAC;AAEhF,MAAM,CAAN,IAAY,cAyBX;AAzBD,WAAY,cAAc;IACxB;;;;;;;;;;;;;;;OAeG;IACH,2CAAyB,CAAA;IACzB,iEAA+C,CAAA;IAC/C,yEAAuD,CAAA;IACvD,2EAAyD,CAAA;IACzD,uDAAqC,CAAA;IACrC,uCAAqB,CAAA;IACrB,iGAAiG;IACjG,iDAA+B,CAAA;AACjC,CAAC,EAzBW,cAAc,KAAd,cAAc,QAyBzB;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qCAAqC,GAA8B,MAAM,CAAC,MAAM,CAAC;IAC5F,cAAc,CAAC,qBAAqB;IACpC,cAAc,CAAC,yBAAyB;IACxC,cAAc,CAAC,0BAA0B;IACzC,cAAc,CAAC,gBAAgB;CAChC,CAAC,CAAC;AAEH,MAAM,CAAN,IAAY,cAMX;AAND,WAAY,cAAc;IACxB,yCAAuB,CAAA;IACvB,yCAAuB,CAAA;IACvB,yCAAuB,CAAA;IACvB,6CAA2B,CAAA;IAC3B,2CAAyB,CAAA;AAC3B,CAAC,EANW,cAAc,KAAd,cAAc,QAMzB;AAGD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC;IACrC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;IACtB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;CACvB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC;IACjC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;IACtB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;IACtB,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC;IACpC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;IACtB,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC;IACpC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;IACtB,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,WAAW,CAAC,QAAQ,EAAE;IACnC,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACxC,mBAAmB,EAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC;CACnD,CAAC,CAAC;AAKH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC,MAAM,CACvF,CAAC,MAAM,EAAE,EAAE;IACT,yCAAyC;IACzC,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;QACrC,IAAI,CAAC;YACH,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CACF,CAAC","sourcesContent":["\nimport { z } from 'zod';\nimport { ResourcePrimaryScope } from '../../resources/resources.shared.schemas';\n\nexport enum CORE_APP_ROLES {\n /**\n * Open access: an operation declaring `APP_PUBLIC` requires NO credential, and imposes no role\n * requirement on one that is presented.\n *\n * Not a role anybody is granted — a SENTINEL on the operation's `roles`, which is why the two\n * halves above have to be stated together. \"Accessible without auth\" was the whole comment until\n * 2026-08-31, and it was read as *reachable only when unauthenticated*: every role gate then\n * refused a credentialed caller, so a public price list, catalogue or i18n bundle answered 200 to\n * an anonymous probe and 403 to a logged-in customer (#153). Authenticating must never take access\n * away.\n *\n * `requiredRolesDeclarePublicAccess` in `roles.shared.utils.ts` is the runtime source of this\n * meaning; every gate consults it rather than re-deriving the test. Scope, tenancy and the\n * repository's contextual filter are untouched by the declaration — it says no role is required to\n * ASK, never that no scope confines the answer.\n */\n APP_PUBLIC = 'APP_PUBLIC',\n APP_ADMIN_SUPER_ADMIN = 'APP_ADMIN_SUPER_ADMIN',\n APP_ADMIN_BILLING_MANAGER = 'APP_ADMIN_BILLING_MANAGER',\n APP_ADMIN_MARKETING_EDITOR = 'APP_ADMIN_MARKETING_EDITOR',\n APP_ADMIN_VIEWER = 'APP_ADMIN_VIEWER',\n APP_USER = 'APP_USER',\n /** Anonymous session access. Operations with APP_ANONYMOUS are accessible to anonymous users. */\n APP_ANONYMOUS = 'APP_ANONYMOUS',\n}\n\n/**\n * Authorities that administer the generated application itself.\n *\n * This named subset is the canonical boundary used by consumer-facing\n * products such as the API reference. It prevents each downstream consumer\n * from guessing administrative meaning from `APP_ADMIN_` spelling or copying\n * an independent list that drifts when the role vocabulary evolves.\n */\nexport const CORE_APPLICATION_ADMINISTRATION_ROLES: readonly CORE_APP_ROLES[] = Object.freeze([\n CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN,\n CORE_APP_ROLES.APP_ADMIN_BILLING_MANAGER,\n CORE_APP_ROLES.APP_ADMIN_MARKETING_EDITOR,\n CORE_APP_ROLES.APP_ADMIN_VIEWER,\n]);\n\nexport enum CORE_ORG_ROLES {\n ORG_GUEST = 'ORG_GUEST',\n ORG_OWNER = 'ORG_OWNER',\n ORG_ADMIN = 'ORG_ADMIN',\n ORG_MANAGER = 'ORG_MANAGER',\n ORG_MEMBER = 'ORG_MEMBER',\n}\n\n\nexport const CoreRolesSchema = z.union([\n z.enum(CORE_APP_ROLES),\n z.enum(CORE_ORG_ROLES),\n]);\n\nexport const RolesSchema = z.union([\n z.enum(CORE_APP_ROLES),\n z.enum(CORE_ORG_ROLES),\n z.string().min(1),\n]);\n\nexport const AppRolesSchema = z.union([\n z.enum(CORE_APP_ROLES),\n z.string().min(1),\n]);\n\nexport const OrgRolesSchema = z.union([\n z.enum(CORE_ORG_ROLES),\n z.string().min(1),\n]);\n\nexport const RoleDefinitionSchema = z.object({\n role: RolesSchema,\n inheritFrom: RolesSchema.optional(),\n isSystemRole: z.boolean().default(false),\n relatedPrimaryScope : z.enum(ResourcePrimaryScope),\n});\n\n\nexport type RoleDefinition = z.infer<typeof RoleDefinitionSchema>;\n\nexport const RolesConfigurationSchema = z.record(z.string(), RoleDefinitionSchema).refine(\n (record) => {\n // Validate that all keys are valid roles\n return Object.keys(record).every(key => {\n try {\n RolesSchema.parse(key);\n return true;\n } catch {\n return false;\n }\n });\n }\n);\n\nexport type Roles = z.infer<typeof RolesSchema>;\nexport type CoreRoles = z.infer<typeof CoreRolesSchema>;\nexport type AppRoles = z.infer<typeof AppRolesSchema>;\nexport type OrgRoles = z.infer<typeof OrgRolesSchema>;\nexport type RolesConfiguration = z.infer<typeof RolesConfigurationSchema>;\n"]}
|
|
@@ -41,6 +41,60 @@ import { type RolesConfiguration } from './roles.shared.schemas';
|
|
|
41
41
|
* @returns the role + its inherited ancestors, nearest-first; empty if unknown
|
|
42
42
|
*/
|
|
43
43
|
export declare function computeRoleHierarchy(roleKey: string, rolesConfig: RolesConfiguration): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Does this required-role set declare PUBLIC access — imposing no positive role requirement on ANY
|
|
46
|
+
* caller, credentialed or not?
|
|
47
|
+
*
|
|
48
|
+
* ## The semantic this settles, and why the alternative reading was wrong
|
|
49
|
+
*
|
|
50
|
+
* `APP_PUBLIC` can be read two ways, and for a long time different layers of the framework read it
|
|
51
|
+
* differently:
|
|
52
|
+
*
|
|
53
|
+
* - *no credential REQUIRED* — reachable when nobody authenticated, and gated on roles otherwise;
|
|
54
|
+
* - *no credential EXAMINED* — reachable by everyone, whether or not they authenticated.
|
|
55
|
+
*
|
|
56
|
+
* The second is the contract, and the first was a defect. Under the first reading, an operation
|
|
57
|
+
* declaring `APP_PUBLIC` answered 200 to an anonymous probe and **403 to the same request carrying a
|
|
58
|
+
* valid token**, because `APP_PUBLIC` is not a role anybody is GRANTED — so a credentialed caller
|
|
59
|
+
* fell past the public carve-out to an ordinary role gate that nothing in their grant set could
|
|
60
|
+
* satisfy. Authenticating REDUCED access below what anonymity gave, on every public surface an
|
|
61
|
+
* application can build: a price list, a public catalogue, an i18n bundle served as a resource.
|
|
62
|
+
*
|
|
63
|
+
* Three of the four surfaces that consume `roles` already implemented the second reading, which is
|
|
64
|
+
* what makes this the framework's settled contract rather than a preference:
|
|
65
|
+
*
|
|
66
|
+
* | surface | reading |
|
|
67
|
+
* |---|---|
|
|
68
|
+
* | frontend action visibility ({@link userRolesSatisfyRequiredRoles}) | no positive requirement |
|
|
69
|
+
* | OpenAPI generation — `APP_PUBLIC` emits `security: []` | no authentication required |
|
|
70
|
+
* | variant rights ranking ({@link computeRolePrivilegeRank}) | ranks `0`, the floor |
|
|
71
|
+
* | backend role gate (before this predicate existed) | **credentialed callers refused** |
|
|
72
|
+
*
|
|
73
|
+
* An OpenAPI `security: []` says the endpoint imposes no security requirement. A client that sends a
|
|
74
|
+
* bearer token to such an endpoint is conformant, and refusing it contradicts the published
|
|
75
|
+
* contract. And independently of any document: **authenticating must never take access away.**
|
|
76
|
+
*
|
|
77
|
+
* ## What it does NOT relax
|
|
78
|
+
*
|
|
79
|
+
* Only the ROLE question. Tenancy, parent-scope and FK-path gates run unchanged, and the
|
|
80
|
+
* repository's contextual filter still confines every row the caller then reaches. A public
|
|
81
|
+
* declaration says "no role is required to ask"; it never says "no scope confines the answer".
|
|
82
|
+
*
|
|
83
|
+
* ## Why `APP_ANONYMOUS` is deliberately NOT here
|
|
84
|
+
*
|
|
85
|
+
* {@link OPEN_REQUIRED_ROLES} — the DISPLAY-side set — holds both sentinels, because that gate only
|
|
86
|
+
* decides whether to render a button and is documented to fail OPEN on uncertainty. This predicate
|
|
87
|
+
* is an ADMISSION rule, and the two sentinels are not interchangeable for admission:
|
|
88
|
+
* `APP_ANONYMOUS` is a role an anonymous session genuinely HOLDS (the execution-context creator
|
|
89
|
+
* grants it), so an anonymous caller already satisfies the ordinary gate and needs no carve-out —
|
|
90
|
+
* whereas nobody is ever granted `APP_PUBLIC`. Widening admission to `APP_ANONYMOUS` would therefore
|
|
91
|
+
* change who reaches anonymous-scoped operations without fixing anything, which is the wrong trade
|
|
92
|
+
* for a gate. Keep the two questions separate.
|
|
93
|
+
*
|
|
94
|
+
* @param requiredRoles - an operation's declared `roles` (OR semantics)
|
|
95
|
+
* @returns `true` when the declaration admits every caller, role-wise
|
|
96
|
+
*/
|
|
97
|
+
export declare function requiredRolesDeclarePublicAccess(requiredRoles: readonly string[] | undefined): boolean;
|
|
44
98
|
/** Parameters for {@link userRolesSatisfyRequiredRoles}. */
|
|
45
99
|
export interface UserRolesSatisfyRequiredRolesParams {
|
|
46
100
|
/** The operation's required roles (OR semantics). Empty/undefined ⇒ unrestricted. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roles.shared.utils.d.ts","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAkC,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAMjG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,GAAG,MAAM,EAAE,CAQ/F;
|
|
1
|
+
{"version":3,"file":"roles.shared.utils.d.ts","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAkC,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAMjG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,GAAG,MAAM,EAAE,CAQ/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,wBAAgB,gCAAgC,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAEtG;AAeD,4DAA4D;AAC5D,MAAM,WAAW,mCAAmC;IAClD,qFAAqF;IACrF,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7C,wEAAwE;IACxE,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,0GAA0G;IAC1G,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,mFAAmF;IACnF,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC,+EAA+E;IAC/E,cAAc,CAAC,EAAE,kBAAkB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mCAAmC,GAAG,OAAO,CAoClG;AAsCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,cAAc,GAAE,kBAAmD,EACnE,cAAc,GAAE,kBAA+C,GAC9D,MAAM,CAMR;AAED,sFAAsF;AACtF,MAAM,WAAW,qBAAqB;IACpC,8FAA8F;IAC9F,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,cAAc,CAAC,EAAE,kBAAkB,EACnC,cAAc,CAAC,EAAE,kBAAkB,GAClC,MAAM,CAIR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,qBAAqB,EACzE,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,cAAc,CAAC,EAAE,kBAAkB,EACnC,cAAc,CAAC,EAAE,kBAAkB,GAClC,CAAC,EAAE,CAQL"}
|
|
@@ -50,10 +50,69 @@ export function computeRoleHierarchy(roleKey, rolesConfig) {
|
|
|
50
50
|
}
|
|
51
51
|
return hierarchy;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Does this required-role set declare PUBLIC access — imposing no positive role requirement on ANY
|
|
55
|
+
* caller, credentialed or not?
|
|
56
|
+
*
|
|
57
|
+
* ## The semantic this settles, and why the alternative reading was wrong
|
|
58
|
+
*
|
|
59
|
+
* `APP_PUBLIC` can be read two ways, and for a long time different layers of the framework read it
|
|
60
|
+
* differently:
|
|
61
|
+
*
|
|
62
|
+
* - *no credential REQUIRED* — reachable when nobody authenticated, and gated on roles otherwise;
|
|
63
|
+
* - *no credential EXAMINED* — reachable by everyone, whether or not they authenticated.
|
|
64
|
+
*
|
|
65
|
+
* The second is the contract, and the first was a defect. Under the first reading, an operation
|
|
66
|
+
* declaring `APP_PUBLIC` answered 200 to an anonymous probe and **403 to the same request carrying a
|
|
67
|
+
* valid token**, because `APP_PUBLIC` is not a role anybody is GRANTED — so a credentialed caller
|
|
68
|
+
* fell past the public carve-out to an ordinary role gate that nothing in their grant set could
|
|
69
|
+
* satisfy. Authenticating REDUCED access below what anonymity gave, on every public surface an
|
|
70
|
+
* application can build: a price list, a public catalogue, an i18n bundle served as a resource.
|
|
71
|
+
*
|
|
72
|
+
* Three of the four surfaces that consume `roles` already implemented the second reading, which is
|
|
73
|
+
* what makes this the framework's settled contract rather than a preference:
|
|
74
|
+
*
|
|
75
|
+
* | surface | reading |
|
|
76
|
+
* |---|---|
|
|
77
|
+
* | frontend action visibility ({@link userRolesSatisfyRequiredRoles}) | no positive requirement |
|
|
78
|
+
* | OpenAPI generation — `APP_PUBLIC` emits `security: []` | no authentication required |
|
|
79
|
+
* | variant rights ranking ({@link computeRolePrivilegeRank}) | ranks `0`, the floor |
|
|
80
|
+
* | backend role gate (before this predicate existed) | **credentialed callers refused** |
|
|
81
|
+
*
|
|
82
|
+
* An OpenAPI `security: []` says the endpoint imposes no security requirement. A client that sends a
|
|
83
|
+
* bearer token to such an endpoint is conformant, and refusing it contradicts the published
|
|
84
|
+
* contract. And independently of any document: **authenticating must never take access away.**
|
|
85
|
+
*
|
|
86
|
+
* ## What it does NOT relax
|
|
87
|
+
*
|
|
88
|
+
* Only the ROLE question. Tenancy, parent-scope and FK-path gates run unchanged, and the
|
|
89
|
+
* repository's contextual filter still confines every row the caller then reaches. A public
|
|
90
|
+
* declaration says "no role is required to ask"; it never says "no scope confines the answer".
|
|
91
|
+
*
|
|
92
|
+
* ## Why `APP_ANONYMOUS` is deliberately NOT here
|
|
93
|
+
*
|
|
94
|
+
* {@link OPEN_REQUIRED_ROLES} — the DISPLAY-side set — holds both sentinels, because that gate only
|
|
95
|
+
* decides whether to render a button and is documented to fail OPEN on uncertainty. This predicate
|
|
96
|
+
* is an ADMISSION rule, and the two sentinels are not interchangeable for admission:
|
|
97
|
+
* `APP_ANONYMOUS` is a role an anonymous session genuinely HOLDS (the execution-context creator
|
|
98
|
+
* grants it), so an anonymous caller already satisfies the ordinary gate and needs no carve-out —
|
|
99
|
+
* whereas nobody is ever granted `APP_PUBLIC`. Widening admission to `APP_ANONYMOUS` would therefore
|
|
100
|
+
* change who reaches anonymous-scoped operations without fixing anything, which is the wrong trade
|
|
101
|
+
* for a gate. Keep the two questions separate.
|
|
102
|
+
*
|
|
103
|
+
* @param requiredRoles - an operation's declared `roles` (OR semantics)
|
|
104
|
+
* @returns `true` when the declaration admits every caller, role-wise
|
|
105
|
+
*/
|
|
106
|
+
export function requiredRolesDeclarePublicAccess(requiredRoles) {
|
|
107
|
+
return Array.isArray(requiredRoles) && requiredRoles.includes(CORE_APP_ROLES.APP_PUBLIC);
|
|
108
|
+
}
|
|
53
109
|
/**
|
|
54
110
|
* Required-role sentinels that impose NO positive role requirement — an operation
|
|
55
111
|
* gated on public / anonymous access must never be hidden from a viewer. Treated
|
|
56
112
|
* as always-satisfied.
|
|
113
|
+
*
|
|
114
|
+
* DISPLAY-side and deliberately WIDER than {@link requiredRolesDeclarePublicAccess}; see that
|
|
115
|
+
* predicate's JSDoc for why the admission rule must not adopt `APP_ANONYMOUS`.
|
|
57
116
|
*/
|
|
58
117
|
const OPEN_REQUIRED_ROLES = new Set([
|
|
59
118
|
CORE_APP_ROLES.APP_PUBLIC,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roles.shared.utils.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Role-resolution utilities — pure, shared between backend authorization and\n * frontend display gating.\n *\n * The framework authorizes operations against a role **hierarchy**: a user\n * holding a high role (e.g. `ORG_OWNER`) implicitly satisfies every role it\n * inherits from (`ORG_OWNER → ORG_ADMIN → ORG_MANAGER → ORG_MEMBER → ORG_GUEST`).\n * `AuthorizationsBackendService` enforces this on every request; these helpers\n * expose the SAME hierarchy walk so the frontend can decide which operation\n * actions to even surface — without duplicating (and drifting from) the\n * algorithm.\n *\n * **Why this exists on the frontend.** The resource registry the browser holds\n * carries every operation a resource declares, each with its `roles` requirement,\n * but the frontend historically never consulted `operation.roles` when building\n * the action bar — so admin-only / super-admin-only actions rendered for every\n * viewer and only 403'd on click. The backend stays the security authority; this\n * gate is a display optimisation that hides buttons the user provably cannot use.\n *\n * @see roles.shared.defaults.ts — the default app/org role configurations\n * @see roles.shared.schemas.ts — `Roles`, `RolesConfiguration`, `RoleDefinition`\n * @see AuthorizationsBackendService.getRoleHierarchy / validateOperationRoles —\n * the backend twin this mirrors. If the hierarchy walk changes there, change it\n * here too (the role *configuration* is already shared via the constants below,\n * so only the algorithm can drift).\n */\n\nimport { CORE_APP_ROLES, CORE_ORG_ROLES, type RolesConfiguration } from './roles.shared.schemas';\nimport {\n DEFAULT_APP_ROLE_CONFIGURATION,\n DEFAULT_ORGANIZATION_ROLES,\n} from './roles.shared.defaults';\n\n/**\n * Walk a role's inheritance chain within a single role configuration, returning\n * the role itself plus every role it (transitively) inherits from, nearest-first.\n *\n * Mirrors `AuthorizationsBackendService.getRoleHierarchy`: starts at `roleKey`\n * and follows `inheritFrom` until a role is absent from the config. A role that\n * is **not present** in `rolesConfig` yields an EMPTY chain (it contributes\n * nothing), exactly like the backend — callers that must not over-restrict on\n * unknown (app-custom) roles handle that case explicitly (see\n * {@link userRolesSatisfyRequiredRoles}).\n *\n * @param roleKey - the assigned role to expand\n * @param rolesConfig - the role configuration owning the hierarchy (app or org)\n * @returns the role + its inherited ancestors, nearest-first; empty if unknown\n */\nexport function computeRoleHierarchy(roleKey: string, rolesConfig: RolesConfiguration): string[] {\n const hierarchy: string[] = [];\n let current: string | undefined = roleKey;\n while (current && rolesConfig[current]) {\n hierarchy.push(current);\n current = rolesConfig[current].inheritFrom;\n }\n return hierarchy;\n}\n\n/**\n * Required-role sentinels that impose NO positive role requirement — an operation\n * gated on public / anonymous access must never be hidden from a viewer. Treated\n * as always-satisfied.\n */\nconst OPEN_REQUIRED_ROLES: ReadonlySet<string> = new Set<string>([\n CORE_APP_ROLES.APP_PUBLIC,\n CORE_APP_ROLES.APP_ANONYMOUS,\n]);\n\n/** Parameters for {@link userRolesSatisfyRequiredRoles}. */\nexport interface UserRolesSatisfyRequiredRolesParams {\n /** The operation's required roles (OR semantics). Empty/undefined ⇒ unrestricted. */\n requiredRoles: readonly string[] | undefined;\n /** The user's APPLICATION-scope roles (e.g. `permissions.appLevel`). */\n userAppRoles: readonly string[];\n /** The user's ORGANIZATIONS-scope roles for the CURRENT org (e.g. `permissions.organizations[orgId]`). */\n userOrgRoles: readonly string[];\n /** App role configuration (defaults to {@link DEFAULT_APP_ROLE_CONFIGURATION}). */\n appRolesConfig?: RolesConfiguration;\n /** Org role configuration (defaults to {@link DEFAULT_ORGANIZATION_ROLES}). */\n orgRolesConfig?: RolesConfiguration;\n}\n\n/**\n * Decide whether a user's effective roles satisfy an operation's required roles,\n * honouring the role hierarchy — the shared answer to \"can this user perform this\n * operation, role-wise?\".\n *\n * This is the SAME OR-with-hierarchy decision the backend enforces\n * (`validateOperationRoles`): the user passes if ANY required role lies in the\n * inheritance closure of ANY role they hold (app roles expanded against the app\n * config, org roles against the org config).\n *\n * **Conservative on the frontend / display side.** It gates which action buttons\n * are even shown; the backend remains the security authority. It therefore FAILS\n * OPEN on uncertainty so it can never hide a button the user is actually allowed\n * to use:\n * - no/empty `requiredRoles` ⇒ `true` (unrestricted operation).\n * - a required role that is a public/anonymous sentinel ⇒ `true`.\n * - if the user holds a role UNKNOWN to the supplied config (an app-custom role\n * whose inheritance the default config cannot resolve) and the known-role\n * expansion did not already satisfy the requirement ⇒ `true` (do not hide —\n * the custom role might inherit a required role). For core-role-only users\n * (the common case, and the whole settings hub) the decision is exact.\n *\n * @returns `true` if the operation should be considered role-permitted (shown).\n */\nexport function userRolesSatisfyRequiredRoles(params: UserRolesSatisfyRequiredRolesParams): boolean {\n const {\n requiredRoles,\n userAppRoles,\n userOrgRoles,\n appRolesConfig = DEFAULT_APP_ROLE_CONFIGURATION,\n orgRolesConfig = DEFAULT_ORGANIZATION_ROLES,\n } = params;\n\n // Unrestricted operation — always permitted.\n if (!requiredRoles || requiredRoles.length === 0) return true;\n\n // A public/anonymous gate imposes no positive requirement.\n if (requiredRoles.some(role => OPEN_REQUIRED_ROLES.has(role))) return true;\n\n // Expand the user's roles through their respective hierarchies.\n const satisfiedRoles = new Set<string>();\n let hasUnresolvableUserRole = false;\n\n for (const role of userAppRoles) {\n const hierarchy = computeRoleHierarchy(role, appRolesConfig);\n if (hierarchy.length === 0) hasUnresolvableUserRole = true;\n for (const inherited of hierarchy) satisfiedRoles.add(inherited);\n }\n for (const role of userOrgRoles) {\n const hierarchy = computeRoleHierarchy(role, orgRolesConfig);\n if (hierarchy.length === 0) hasUnresolvableUserRole = true;\n for (const inherited of hierarchy) satisfiedRoles.add(inherited);\n }\n\n // Direct OR-with-hierarchy satisfaction.\n if (requiredRoles.some(role => satisfiedRoles.has(role))) return true;\n\n // Fail open if the user holds a role the supplied config can't expand — its\n // (unknown) inheritance might cover a required role. Never hide on uncertainty.\n return hasUnresolvableUserRole;\n}\n\n// ════════════════════════════════════════════════════════════════════════════\n// Variant selection by rights (\"show only the highest-right-level variant\")\n// ════════════════════════════════════════════════════════════════════════════\n\n/**\n * Privilege **tier** of a core role — the product ordering between role families.\n * A variant gated on a higher-tier role represents a \"higher right level\".\n *\n * 3 — platform/app admin (`APP_ADMIN_*`)\n * 2 — organisation roles (`ORG_*`)\n * 1 — authenticated user (`APP_USER`)\n * 0 — public / anonymous\n *\n * This is the one **cross-scope** decision the ranking needs: app-admin outranks\n * org, which outranks the plain authenticated user. Within a tier, the role\n * hierarchy depth breaks ties (a deeper role inherits more, so it is higher).\n */\nconst ROLE_PRIVILEGE_TIER: Readonly<Record<string, number>> = {\n [CORE_APP_ROLES.APP_PUBLIC]: 0,\n [CORE_APP_ROLES.APP_ANONYMOUS]: 0,\n [CORE_APP_ROLES.APP_USER]: 1,\n [CORE_ORG_ROLES.ORG_GUEST]: 2,\n [CORE_ORG_ROLES.ORG_MEMBER]: 2,\n [CORE_ORG_ROLES.ORG_MANAGER]: 2,\n [CORE_ORG_ROLES.ORG_ADMIN]: 2,\n [CORE_ORG_ROLES.ORG_OWNER]: 2,\n [CORE_APP_ROLES.APP_ADMIN_VIEWER]: 3,\n [CORE_APP_ROLES.APP_ADMIN_MARKETING_EDITOR]: 3,\n [CORE_APP_ROLES.APP_ADMIN_BILLING_MANAGER]: 3,\n [CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN]: 3,\n};\n\n/** Tier used for a role unknown to the default config (an app-custom role). */\nconst UNKNOWN_ROLE_TIER = 2;\nconst TIER_MULTIPLIER = 100;\n\n/**\n * Numeric privilege rank of a single role: `tier * 100 + hierarchyDepth`.\n *\n * Higher = more privileged. Derived from {@link ROLE_PRIVILEGE_TIER} (cross-tier\n * ordering) plus the role's hierarchy depth within its config (intra-tier\n * ordering), so e.g. `ORG_OWNER` (205) > `ORG_MEMBER` (202), and\n * `APP_ADMIN_SUPER_ADMIN` (304) outranks every `ORG_*`. Unknown custom roles fall\n * back to the org tier with depth 1 — exact for core-role apps (the common case),\n * conservative otherwise.\n *\n * Public / anonymous gates impose no positive requirement, so they rank `0` — the\n * floor, equal to a truly unrestricted variant (see {@link computeVariantRightsRank}).\n *\n * NOTE: the intra-tier tiebreak uses hierarchy DEPTH, which is a faithful proxy\n * for privilege only when inheritance is (near-)linear, as it is in the default\n * configs. A branching custom `RolesConfiguration` could give a shallow-but-powerful\n * role a lower depth than a deep-but-weak one and mis-order them; thread an explicit\n * rank (or extend this) if such a config is introduced.\n */\nexport function computeRolePrivilegeRank(\n role: string,\n appRolesConfig: RolesConfiguration = DEFAULT_APP_ROLE_CONFIGURATION,\n orgRolesConfig: RolesConfiguration = DEFAULT_ORGANIZATION_ROLES,\n): number {\n if (OPEN_REQUIRED_ROLES.has(role)) return 0;\n const tier = ROLE_PRIVILEGE_TIER[role] ?? UNKNOWN_ROLE_TIER;\n const config = appRolesConfig[role] ? appRolesConfig : orgRolesConfig[role] ? orgRolesConfig : undefined;\n const depth = config ? computeRoleHierarchy(role, config).length : 1;\n return tier * TIER_MULTIPLIER + depth;\n}\n\n/** Minimal shape this module needs from an operation variant to rank it by rights. */\nexport interface RightsRankableVariant {\n /** The roles that may perform this variant (OR semantics). Empty/undefined = unrestricted. */\n roles?: readonly string[];\n}\n\n/**\n * The rights rank a variant represents = the privilege of its most-privileged\n * required role. An unrestricted variant (no `roles`) ranks `0` — the lowest gate.\n */\nexport function computeVariantRightsRank(\n variant: RightsRankableVariant,\n appRolesConfig?: RolesConfiguration,\n orgRolesConfig?: RolesConfiguration,\n): number {\n const roles = variant.roles ?? [];\n if (roles.length === 0) return 0;\n return Math.max(...roles.map(role => computeRolePrivilegeRank(role, appRolesConfig, orgRolesConfig)));\n}\n\n/**\n * Collapse a set of operation variants to **only the highest-right-level ones**.\n *\n * The framework principle: variants of an operation exist to give different\n * behaviour according to rights, so a surface should show only the variant with\n * the highest right level the user qualifies for — not the lower-rights ones too.\n * (A super-admin who is also an org member otherwise sees both the member READ and\n * the super-admin READ; this keeps only the super-admin one.)\n *\n * **Precondition:** pass variants the user already QUALIFIES for (i.e. post\n * role-gating via {@link userRolesSatisfyRequiredRoles}). This selector then keeps\n * the subset with the maximum {@link computeVariantRightsRank}. Variants that\n * **tie** at the top rank (equal rights — e.g. context/variantKey variants that\n * are not a rights distinction) are all returned, so the caller's existing\n * default/sub-group logic still handles non-rights multiplicity unchanged.\n *\n * Used globally wherever variants surface as user actions; composite / execution\n * views deliberately bypass this (they may show all variants, for now) by not\n * routing through the collapsing path.\n */\nexport function selectHighestRightsVariants<T extends RightsRankableVariant>(\n variants: readonly T[],\n appRolesConfig?: RolesConfiguration,\n orgRolesConfig?: RolesConfiguration,\n): T[] {\n if (variants.length <= 1) return [...variants];\n const ranked = variants.map(variant => ({\n variant,\n rank: computeVariantRightsRank(variant, appRolesConfig, orgRolesConfig),\n }));\n const maxRank = Math.max(...ranked.map(entry => entry.rank));\n return ranked.filter(entry => entry.rank === maxRank).map(entry => entry.variant);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"roles.shared.utils.js","sourceRoot":"","sources":["../../../../../src/security/authorizations/roles.shared.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Role-resolution utilities — pure, shared between backend authorization and\n * frontend display gating.\n *\n * The framework authorizes operations against a role **hierarchy**: a user\n * holding a high role (e.g. `ORG_OWNER`) implicitly satisfies every role it\n * inherits from (`ORG_OWNER → ORG_ADMIN → ORG_MANAGER → ORG_MEMBER → ORG_GUEST`).\n * `AuthorizationsBackendService` enforces this on every request; these helpers\n * expose the SAME hierarchy walk so the frontend can decide which operation\n * actions to even surface — without duplicating (and drifting from) the\n * algorithm.\n *\n * **Why this exists on the frontend.** The resource registry the browser holds\n * carries every operation a resource declares, each with its `roles` requirement,\n * but the frontend historically never consulted `operation.roles` when building\n * the action bar — so admin-only / super-admin-only actions rendered for every\n * viewer and only 403'd on click. The backend stays the security authority; this\n * gate is a display optimisation that hides buttons the user provably cannot use.\n *\n * @see roles.shared.defaults.ts — the default app/org role configurations\n * @see roles.shared.schemas.ts — `Roles`, `RolesConfiguration`, `RoleDefinition`\n * @see AuthorizationsBackendService.getRoleHierarchy / validateOperationRoles —\n * the backend twin this mirrors. If the hierarchy walk changes there, change it\n * here too (the role *configuration* is already shared via the constants below,\n * so only the algorithm can drift).\n */\n\nimport { CORE_APP_ROLES, CORE_ORG_ROLES, type RolesConfiguration } from './roles.shared.schemas';\nimport {\n DEFAULT_APP_ROLE_CONFIGURATION,\n DEFAULT_ORGANIZATION_ROLES,\n} from './roles.shared.defaults';\n\n/**\n * Walk a role's inheritance chain within a single role configuration, returning\n * the role itself plus every role it (transitively) inherits from, nearest-first.\n *\n * Mirrors `AuthorizationsBackendService.getRoleHierarchy`: starts at `roleKey`\n * and follows `inheritFrom` until a role is absent from the config. A role that\n * is **not present** in `rolesConfig` yields an EMPTY chain (it contributes\n * nothing), exactly like the backend — callers that must not over-restrict on\n * unknown (app-custom) roles handle that case explicitly (see\n * {@link userRolesSatisfyRequiredRoles}).\n *\n * @param roleKey - the assigned role to expand\n * @param rolesConfig - the role configuration owning the hierarchy (app or org)\n * @returns the role + its inherited ancestors, nearest-first; empty if unknown\n */\nexport function computeRoleHierarchy(roleKey: string, rolesConfig: RolesConfiguration): string[] {\n const hierarchy: string[] = [];\n let current: string | undefined = roleKey;\n while (current && rolesConfig[current]) {\n hierarchy.push(current);\n current = rolesConfig[current].inheritFrom;\n }\n return hierarchy;\n}\n\n/**\n * Does this required-role set declare PUBLIC access — imposing no positive role requirement on ANY\n * caller, credentialed or not?\n *\n * ## The semantic this settles, and why the alternative reading was wrong\n *\n * `APP_PUBLIC` can be read two ways, and for a long time different layers of the framework read it\n * differently:\n *\n * - *no credential REQUIRED* — reachable when nobody authenticated, and gated on roles otherwise;\n * - *no credential EXAMINED* — reachable by everyone, whether or not they authenticated.\n *\n * The second is the contract, and the first was a defect. Under the first reading, an operation\n * declaring `APP_PUBLIC` answered 200 to an anonymous probe and **403 to the same request carrying a\n * valid token**, because `APP_PUBLIC` is not a role anybody is GRANTED — so a credentialed caller\n * fell past the public carve-out to an ordinary role gate that nothing in their grant set could\n * satisfy. Authenticating REDUCED access below what anonymity gave, on every public surface an\n * application can build: a price list, a public catalogue, an i18n bundle served as a resource.\n *\n * Three of the four surfaces that consume `roles` already implemented the second reading, which is\n * what makes this the framework's settled contract rather than a preference:\n *\n * | surface | reading |\n * |---|---|\n * | frontend action visibility ({@link userRolesSatisfyRequiredRoles}) | no positive requirement |\n * | OpenAPI generation — `APP_PUBLIC` emits `security: []` | no authentication required |\n * | variant rights ranking ({@link computeRolePrivilegeRank}) | ranks `0`, the floor |\n * | backend role gate (before this predicate existed) | **credentialed callers refused** |\n *\n * An OpenAPI `security: []` says the endpoint imposes no security requirement. A client that sends a\n * bearer token to such an endpoint is conformant, and refusing it contradicts the published\n * contract. And independently of any document: **authenticating must never take access away.**\n *\n * ## What it does NOT relax\n *\n * Only the ROLE question. Tenancy, parent-scope and FK-path gates run unchanged, and the\n * repository's contextual filter still confines every row the caller then reaches. A public\n * declaration says \"no role is required to ask\"; it never says \"no scope confines the answer\".\n *\n * ## Why `APP_ANONYMOUS` is deliberately NOT here\n *\n * {@link OPEN_REQUIRED_ROLES} — the DISPLAY-side set — holds both sentinels, because that gate only\n * decides whether to render a button and is documented to fail OPEN on uncertainty. This predicate\n * is an ADMISSION rule, and the two sentinels are not interchangeable for admission:\n * `APP_ANONYMOUS` is a role an anonymous session genuinely HOLDS (the execution-context creator\n * grants it), so an anonymous caller already satisfies the ordinary gate and needs no carve-out —\n * whereas nobody is ever granted `APP_PUBLIC`. Widening admission to `APP_ANONYMOUS` would therefore\n * change who reaches anonymous-scoped operations without fixing anything, which is the wrong trade\n * for a gate. Keep the two questions separate.\n *\n * @param requiredRoles - an operation's declared `roles` (OR semantics)\n * @returns `true` when the declaration admits every caller, role-wise\n */\nexport function requiredRolesDeclarePublicAccess(requiredRoles: readonly string[] | undefined): boolean {\n return Array.isArray(requiredRoles) && requiredRoles.includes(CORE_APP_ROLES.APP_PUBLIC);\n}\n\n/**\n * Required-role sentinels that impose NO positive role requirement — an operation\n * gated on public / anonymous access must never be hidden from a viewer. Treated\n * as always-satisfied.\n *\n * DISPLAY-side and deliberately WIDER than {@link requiredRolesDeclarePublicAccess}; see that\n * predicate's JSDoc for why the admission rule must not adopt `APP_ANONYMOUS`.\n */\nconst OPEN_REQUIRED_ROLES: ReadonlySet<string> = new Set<string>([\n CORE_APP_ROLES.APP_PUBLIC,\n CORE_APP_ROLES.APP_ANONYMOUS,\n]);\n\n/** Parameters for {@link userRolesSatisfyRequiredRoles}. */\nexport interface UserRolesSatisfyRequiredRolesParams {\n /** The operation's required roles (OR semantics). Empty/undefined ⇒ unrestricted. */\n requiredRoles: readonly string[] | undefined;\n /** The user's APPLICATION-scope roles (e.g. `permissions.appLevel`). */\n userAppRoles: readonly string[];\n /** The user's ORGANIZATIONS-scope roles for the CURRENT org (e.g. `permissions.organizations[orgId]`). */\n userOrgRoles: readonly string[];\n /** App role configuration (defaults to {@link DEFAULT_APP_ROLE_CONFIGURATION}). */\n appRolesConfig?: RolesConfiguration;\n /** Org role configuration (defaults to {@link DEFAULT_ORGANIZATION_ROLES}). */\n orgRolesConfig?: RolesConfiguration;\n}\n\n/**\n * Decide whether a user's effective roles satisfy an operation's required roles,\n * honouring the role hierarchy — the shared answer to \"can this user perform this\n * operation, role-wise?\".\n *\n * This is the SAME OR-with-hierarchy decision the backend enforces\n * (`validateOperationRoles`): the user passes if ANY required role lies in the\n * inheritance closure of ANY role they hold (app roles expanded against the app\n * config, org roles against the org config).\n *\n * **Conservative on the frontend / display side.** It gates which action buttons\n * are even shown; the backend remains the security authority. It therefore FAILS\n * OPEN on uncertainty so it can never hide a button the user is actually allowed\n * to use:\n * - no/empty `requiredRoles` ⇒ `true` (unrestricted operation).\n * - a required role that is a public/anonymous sentinel ⇒ `true`.\n * - if the user holds a role UNKNOWN to the supplied config (an app-custom role\n * whose inheritance the default config cannot resolve) and the known-role\n * expansion did not already satisfy the requirement ⇒ `true` (do not hide —\n * the custom role might inherit a required role). For core-role-only users\n * (the common case, and the whole settings hub) the decision is exact.\n *\n * @returns `true` if the operation should be considered role-permitted (shown).\n */\nexport function userRolesSatisfyRequiredRoles(params: UserRolesSatisfyRequiredRolesParams): boolean {\n const {\n requiredRoles,\n userAppRoles,\n userOrgRoles,\n appRolesConfig = DEFAULT_APP_ROLE_CONFIGURATION,\n orgRolesConfig = DEFAULT_ORGANIZATION_ROLES,\n } = params;\n\n // Unrestricted operation — always permitted.\n if (!requiredRoles || requiredRoles.length === 0) return true;\n\n // A public/anonymous gate imposes no positive requirement.\n if (requiredRoles.some(role => OPEN_REQUIRED_ROLES.has(role))) return true;\n\n // Expand the user's roles through their respective hierarchies.\n const satisfiedRoles = new Set<string>();\n let hasUnresolvableUserRole = false;\n\n for (const role of userAppRoles) {\n const hierarchy = computeRoleHierarchy(role, appRolesConfig);\n if (hierarchy.length === 0) hasUnresolvableUserRole = true;\n for (const inherited of hierarchy) satisfiedRoles.add(inherited);\n }\n for (const role of userOrgRoles) {\n const hierarchy = computeRoleHierarchy(role, orgRolesConfig);\n if (hierarchy.length === 0) hasUnresolvableUserRole = true;\n for (const inherited of hierarchy) satisfiedRoles.add(inherited);\n }\n\n // Direct OR-with-hierarchy satisfaction.\n if (requiredRoles.some(role => satisfiedRoles.has(role))) return true;\n\n // Fail open if the user holds a role the supplied config can't expand — its\n // (unknown) inheritance might cover a required role. Never hide on uncertainty.\n return hasUnresolvableUserRole;\n}\n\n// ════════════════════════════════════════════════════════════════════════════\n// Variant selection by rights (\"show only the highest-right-level variant\")\n// ════════════════════════════════════════════════════════════════════════════\n\n/**\n * Privilege **tier** of a core role — the product ordering between role families.\n * A variant gated on a higher-tier role represents a \"higher right level\".\n *\n * 3 — platform/app admin (`APP_ADMIN_*`)\n * 2 — organisation roles (`ORG_*`)\n * 1 — authenticated user (`APP_USER`)\n * 0 — public / anonymous\n *\n * This is the one **cross-scope** decision the ranking needs: app-admin outranks\n * org, which outranks the plain authenticated user. Within a tier, the role\n * hierarchy depth breaks ties (a deeper role inherits more, so it is higher).\n */\nconst ROLE_PRIVILEGE_TIER: Readonly<Record<string, number>> = {\n [CORE_APP_ROLES.APP_PUBLIC]: 0,\n [CORE_APP_ROLES.APP_ANONYMOUS]: 0,\n [CORE_APP_ROLES.APP_USER]: 1,\n [CORE_ORG_ROLES.ORG_GUEST]: 2,\n [CORE_ORG_ROLES.ORG_MEMBER]: 2,\n [CORE_ORG_ROLES.ORG_MANAGER]: 2,\n [CORE_ORG_ROLES.ORG_ADMIN]: 2,\n [CORE_ORG_ROLES.ORG_OWNER]: 2,\n [CORE_APP_ROLES.APP_ADMIN_VIEWER]: 3,\n [CORE_APP_ROLES.APP_ADMIN_MARKETING_EDITOR]: 3,\n [CORE_APP_ROLES.APP_ADMIN_BILLING_MANAGER]: 3,\n [CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN]: 3,\n};\n\n/** Tier used for a role unknown to the default config (an app-custom role). */\nconst UNKNOWN_ROLE_TIER = 2;\nconst TIER_MULTIPLIER = 100;\n\n/**\n * Numeric privilege rank of a single role: `tier * 100 + hierarchyDepth`.\n *\n * Higher = more privileged. Derived from {@link ROLE_PRIVILEGE_TIER} (cross-tier\n * ordering) plus the role's hierarchy depth within its config (intra-tier\n * ordering), so e.g. `ORG_OWNER` (205) > `ORG_MEMBER` (202), and\n * `APP_ADMIN_SUPER_ADMIN` (304) outranks every `ORG_*`. Unknown custom roles fall\n * back to the org tier with depth 1 — exact for core-role apps (the common case),\n * conservative otherwise.\n *\n * Public / anonymous gates impose no positive requirement, so they rank `0` — the\n * floor, equal to a truly unrestricted variant (see {@link computeVariantRightsRank}).\n *\n * NOTE: the intra-tier tiebreak uses hierarchy DEPTH, which is a faithful proxy\n * for privilege only when inheritance is (near-)linear, as it is in the default\n * configs. A branching custom `RolesConfiguration` could give a shallow-but-powerful\n * role a lower depth than a deep-but-weak one and mis-order them; thread an explicit\n * rank (or extend this) if such a config is introduced.\n */\nexport function computeRolePrivilegeRank(\n role: string,\n appRolesConfig: RolesConfiguration = DEFAULT_APP_ROLE_CONFIGURATION,\n orgRolesConfig: RolesConfiguration = DEFAULT_ORGANIZATION_ROLES,\n): number {\n if (OPEN_REQUIRED_ROLES.has(role)) return 0;\n const tier = ROLE_PRIVILEGE_TIER[role] ?? UNKNOWN_ROLE_TIER;\n const config = appRolesConfig[role] ? appRolesConfig : orgRolesConfig[role] ? orgRolesConfig : undefined;\n const depth = config ? computeRoleHierarchy(role, config).length : 1;\n return tier * TIER_MULTIPLIER + depth;\n}\n\n/** Minimal shape this module needs from an operation variant to rank it by rights. */\nexport interface RightsRankableVariant {\n /** The roles that may perform this variant (OR semantics). Empty/undefined = unrestricted. */\n roles?: readonly string[];\n}\n\n/**\n * The rights rank a variant represents = the privilege of its most-privileged\n * required role. An unrestricted variant (no `roles`) ranks `0` — the lowest gate.\n */\nexport function computeVariantRightsRank(\n variant: RightsRankableVariant,\n appRolesConfig?: RolesConfiguration,\n orgRolesConfig?: RolesConfiguration,\n): number {\n const roles = variant.roles ?? [];\n if (roles.length === 0) return 0;\n return Math.max(...roles.map(role => computeRolePrivilegeRank(role, appRolesConfig, orgRolesConfig)));\n}\n\n/**\n * Collapse a set of operation variants to **only the highest-right-level ones**.\n *\n * The framework principle: variants of an operation exist to give different\n * behaviour according to rights, so a surface should show only the variant with\n * the highest right level the user qualifies for — not the lower-rights ones too.\n * (A super-admin who is also an org member otherwise sees both the member READ and\n * the super-admin READ; this keeps only the super-admin one.)\n *\n * **Precondition:** pass variants the user already QUALIFIES for (i.e. post\n * role-gating via {@link userRolesSatisfyRequiredRoles}). This selector then keeps\n * the subset with the maximum {@link computeVariantRightsRank}. Variants that\n * **tie** at the top rank (equal rights — e.g. context/variantKey variants that\n * are not a rights distinction) are all returned, so the caller's existing\n * default/sub-group logic still handles non-rights multiplicity unchanged.\n *\n * Used globally wherever variants surface as user actions; composite / execution\n * views deliberately bypass this (they may show all variants, for now) by not\n * routing through the collapsing path.\n */\nexport function selectHighestRightsVariants<T extends RightsRankableVariant>(\n variants: readonly T[],\n appRolesConfig?: RolesConfiguration,\n orgRolesConfig?: RolesConfiguration,\n): T[] {\n if (variants.length <= 1) return [...variants];\n const ranked = variants.map(variant => ({\n variant,\n rank: computeVariantRightsRank(variant, appRolesConfig, orgRolesConfig),\n }));\n const maxRank = Math.max(...ranked.map(entry => entry.rank));\n return ranked.filter(entry => entry.rank === maxRank).map(entry => entry.variant);\n}\n"]}
|