@wildo-ai/saas-backend-lib 1.1.12 → 1.1.13
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/base-classes/models/manual-controllers.backend.definition.d.ts.map +1 -1
- package/dist/esm/base-classes/models/manual-controllers.backend.definition.js +2 -0
- package/dist/esm/base-classes/models/manual-controllers.backend.definition.js.map +1 -1
- package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.d.ts +4 -0
- package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.d.ts.map +1 -1
- package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.js +20 -5
- package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.js.map +1 -1
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.d.ts +3 -0
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.d.ts.map +1 -1
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.js +10 -1
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.js.map +1 -1
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.d.ts +22 -0
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.d.ts.map +1 -1
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.js +25 -1
- package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.js.map +1 -1
- package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.d.ts +38 -3
- package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.d.ts.map +1 -1
- package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.js +68 -5
- package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.js.map +1 -1
- package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.d.ts +2 -2
- package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.js +6 -3
- package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.d.ts +83 -0
- package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.d.ts.map +1 -1
- package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.js +217 -14
- package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.js.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.d.ts +32 -0
- package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.d.ts.map +1 -0
- package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.js +36 -0
- package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.js.map +1 -0
- package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.d.ts +38 -0
- package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.d.ts.map +1 -0
- package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.js +83 -0
- package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.js.map +1 -0
- package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.d.ts +36 -0
- package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.d.ts.map +1 -0
- package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.js +51 -0
- package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.js.map +1 -0
- package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.d.ts +46 -0
- package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.d.ts.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.js +68 -0
- package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.js.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seal-store.backend.d.ts +27 -0
- package/dist/esm/compliance/approval-seals/approval-seal-store.backend.d.ts.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seal-store.backend.js +42 -3
- package/dist/esm/compliance/approval-seals/approval-seal-store.backend.js.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.d.ts +26 -1
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.d.ts.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.js +10 -0
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.js.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.d.ts +10 -1
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.d.ts.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.js +60 -28
- package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.js.map +1 -1
- package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.d.ts +90 -0
- package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.d.ts.map +1 -0
- package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.js +373 -0
- package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.js.map +1 -0
- package/dist/esm/compliance/approval-seals/decision-presentation.backend.d.ts +29 -2
- package/dist/esm/compliance/approval-seals/decision-presentation.backend.d.ts.map +1 -1
- package/dist/esm/compliance/approval-seals/decision-presentation.backend.js +40 -3
- package/dist/esm/compliance/approval-seals/decision-presentation.backend.js.map +1 -1
- package/dist/esm/compliance/electronic-signatures/pdf-signature-structure.backend.d.ts.map +1 -1
- package/dist/esm/compliance/electronic-signatures/pdf-signature-structure.backend.js +34 -6
- package/dist/esm/compliance/electronic-signatures/pdf-signature-structure.backend.js.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.d.ts +30 -6
- package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.d.ts.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.js +67 -22
- package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.js.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signature-request-poll.batch.backend.service.d.ts.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signature-request-poll.batch.backend.service.js +18 -1
- package/dist/esm/compliance/electronic-signatures/signature-request-poll.batch.backend.service.js.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.d.ts +66 -1
- package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.d.ts.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.js +161 -14
- package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.js.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signed-document-validation.backend.d.ts +47 -0
- package/dist/esm/compliance/electronic-signatures/signed-document-validation.backend.d.ts.map +1 -1
- package/dist/esm/compliance/electronic-signatures/signed-document-validation.backend.js.map +1 -1
- package/dist/esm/external-providers/services/content-safety.backend.service.d.ts +27 -2
- package/dist/esm/external-providers/services/content-safety.backend.service.d.ts.map +1 -1
- package/dist/esm/external-providers/services/content-safety.backend.service.js +46 -1
- package/dist/esm/external-providers/services/content-safety.backend.service.js.map +1 -1
- package/dist/esm/external-providers/services/embeddings.backend.service.d.ts.map +1 -1
- package/dist/esm/external-providers/services/embeddings.backend.service.js +2 -0
- package/dist/esm/external-providers/services/embeddings.backend.service.js.map +1 -1
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts +11 -0
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts.map +1 -1
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.js +23 -0
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.js.map +1 -1
- package/dist/esm/external-providers/services/provider-service-account-token.backend.d.ts +19 -1
- package/dist/esm/external-providers/services/provider-service-account-token.backend.d.ts.map +1 -1
- package/dist/esm/external-providers/services/provider-service-account-token.backend.js +41 -1
- package/dist/esm/external-providers/services/provider-service-account-token.backend.js.map +1 -1
- package/dist/esm/files/utils/file-parent-read-context.backend.d.ts +27 -2
- package/dist/esm/files/utils/file-parent-read-context.backend.d.ts.map +1 -1
- package/dist/esm/files/utils/file-parent-read-context.backend.js +23 -4
- package/dist/esm/files/utils/file-parent-read-context.backend.js.map +1 -1
- package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.d.ts +26 -0
- package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.d.ts.map +1 -1
- package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.js +100 -8
- package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.js.map +1 -1
- package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.d.ts +3 -1
- package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.d.ts.map +1 -1
- package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.js +8 -2
- package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.js.map +1 -1
- package/dist/esm/flows-actors/models/function-tools.backend.definitions.d.ts +43 -0
- package/dist/esm/flows-actors/models/function-tools.backend.definitions.d.ts.map +1 -1
- package/dist/esm/flows-actors/models/function-tools.backend.definitions.js +37 -0
- package/dist/esm/flows-actors/models/function-tools.backend.definitions.js.map +1 -1
- package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.d.ts +7 -0
- package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.d.ts.map +1 -1
- package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.js +8 -3
- package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.js.map +1 -1
- package/dist/esm/flows-actors/services/agents.backend.service.d.ts +31 -6
- package/dist/esm/flows-actors/services/agents.backend.service.d.ts.map +1 -1
- package/dist/esm/flows-actors/services/agents.backend.service.js +78 -30
- package/dist/esm/flows-actors/services/agents.backend.service.js.map +1 -1
- package/dist/esm/flows-actors/services/autonomous-agent-turn.backend.service.d.ts.map +1 -1
- package/dist/esm/flows-actors/services/autonomous-agent-turn.backend.service.js +4 -0
- package/dist/esm/flows-actors/services/autonomous-agent-turn.backend.service.js.map +1 -1
- package/dist/esm/flows-actors/services/conversational-execution.backend.service.d.ts +28 -2
- package/dist/esm/flows-actors/services/conversational-execution.backend.service.d.ts.map +1 -1
- package/dist/esm/flows-actors/services/conversational-execution.backend.service.js +132 -22
- package/dist/esm/flows-actors/services/conversational-execution.backend.service.js.map +1 -1
- package/dist/esm/flows-actors/services/flow-actor-executions.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/flows-actors/services/flow-actor-executions.custom-impl.backend.service.js +12 -8
- package/dist/esm/flows-actors/services/flow-actor-executions.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/flows-actors/services/flows-actors-registry.service.d.ts +5 -0
- package/dist/esm/flows-actors/services/flows-actors-registry.service.d.ts.map +1 -1
- package/dist/esm/flows-actors/services/flows-actors-registry.service.js +18 -2
- package/dist/esm/flows-actors/services/flows-actors-registry.service.js.map +1 -1
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts +33 -0
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts.map +1 -1
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js +58 -0
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.d.ts +12 -1
- package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.d.ts.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.js +23 -4
- package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.js.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.d.ts +12 -1
- package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.d.ts.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.js +55 -17
- package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.js.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-store.backend.service.d.ts +22 -1
- package/dist/esm/inbound/services/inbound-delivery-store.backend.service.d.ts.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-store.backend.service.js +52 -9
- package/dist/esm/inbound/services/inbound-delivery-store.backend.service.js.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.d.ts +79 -0
- package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.d.ts.map +1 -0
- package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.js +211 -0
- package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.js.map +1 -0
- package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.d.ts +2 -2
- package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.d.ts.map +1 -1
- package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.js +9 -1
- package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/inject/models/container.d.ts.map +1 -1
- package/dist/esm/inject/models/container.js +6 -0
- package/dist/esm/inject/models/container.js.map +1 -1
- package/dist/esm/inject/models/service-types.d.ts +5 -0
- package/dist/esm/inject/models/service-types.d.ts.map +1 -1
- package/dist/esm/inject/models/service-types.js +3 -0
- package/dist/esm/inject/models/service-types.js.map +1 -1
- package/dist/esm/monitoring/observability/models/observability.backend.schemas.d.ts +5 -0
- package/dist/esm/monitoring/observability/models/observability.backend.schemas.d.ts.map +1 -1
- package/dist/esm/monitoring/observability/models/observability.backend.schemas.js.map +1 -1
- package/dist/esm/monitoring/observability/services/metrics.service.d.ts +34 -1
- package/dist/esm/monitoring/observability/services/metrics.service.d.ts.map +1 -1
- package/dist/esm/monitoring/observability/services/metrics.service.js +49 -0
- package/dist/esm/monitoring/observability/services/metrics.service.js.map +1 -1
- package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.d.ts +6 -1
- package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.d.ts.map +1 -1
- package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.js +15 -4
- package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.js.map +1 -1
- package/dist/esm/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.custom-impl.backend.service.js +3 -1
- package/dist/esm/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/runtime/services/http-server.backend.service.d.ts +3 -1
- package/dist/esm/runtime/services/http-server.backend.service.d.ts.map +1 -1
- package/dist/esm/runtime/services/http-server.backend.service.js +11 -3
- package/dist/esm/runtime/services/http-server.backend.service.js.map +1 -1
- package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.d.ts +22 -0
- package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.d.ts.map +1 -1
- package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.js +54 -0
- package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.js.map +1 -1
- package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.d.ts +51 -0
- package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.d.ts.map +1 -1
- package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.js +87 -15
- package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.js.map +1 -1
- package/dist/esm/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.js +5 -2
- package/dist/esm/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/security/authentications/services/custom-impls/sso-config-domain-verification.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/security/authentications/services/custom-impls/sso-config-domain-verification.custom-impl.backend.service.js +65 -24
- package/dist/esm/security/authentications/services/custom-impls/sso-config-domain-verification.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/security/authorizations/super-admin-floor.backend.utils.d.ts +64 -1
- package/dist/esm/security/authorizations/super-admin-floor.backend.utils.d.ts.map +1 -1
- package/dist/esm/security/authorizations/super-admin-floor.backend.utils.js +84 -2
- package/dist/esm/security/authorizations/super-admin-floor.backend.utils.js.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.d.ts +14 -0
- package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.js +13 -5
- package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/users/services/anonymous-consent.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/users/services/anonymous-consent.custom-impl.backend.service.js +6 -2
- package/dist/esm/users/services/anonymous-consent.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.d.ts +7 -0
- package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.js +65 -20
- package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/users/services/user-role-administration.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/users/services/user-role-administration.custom-impl.backend.service.js +11 -4
- package/dist/esm/users/services/user-role-administration.custom-impl.backend.service.js.map +1 -1
- package/dist/esm/users/services/user-self-custom-implementations.backend.d.ts.map +1 -1
- package/dist/esm/users/services/user-self-custom-implementations.backend.js +4 -2
- package/dist/esm/users/services/user-self-custom-implementations.backend.js.map +1 -1
- package/dist/esm/users/services/user-super-admin-floor.custom-impl.backend.service.d.ts.map +1 -1
- package/dist/esm/users/services/user-super-admin-floor.custom-impl.backend.service.js +16 -0
- package/dist/esm/users/services/user-super-admin-floor.custom-impl.backend.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +10 -10
|
@@ -58,4 +58,58 @@ export function translateOversizedInboundBody(err, req, _res, next) {
|
|
|
58
58
|
}
|
|
59
59
|
next(err);
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The provider endpoints whose body may be larger than the global parsers read (#3395), by full route path: a vendor that
|
|
63
|
+
* pushes a whole email in its webhook (Postmark, Mailgun) declares a `maxBodyBytes` past the 10 MB the global JSON parser
|
|
64
|
+
* reads, and the delivery stores such a body outside its row. One instance per backend: the webhook controller declares
|
|
65
|
+
* the routes it mounts, and the body parser mounted before the global parsers reads them at request time.
|
|
66
|
+
*/
|
|
67
|
+
export class InboundLargeBodyRoutesBackendService {
|
|
68
|
+
limits = new Map();
|
|
69
|
+
/** The route at `fullPath` (`/api/v1/webhooks/{provider}/{endpoint}`) reads bodies up to `limitBytes`. */
|
|
70
|
+
declare(fullPath, limitBytes) {
|
|
71
|
+
this.limits.set(fullPath, limitBytes);
|
|
72
|
+
}
|
|
73
|
+
/** The body limit of the route a request arrived on, or undefined when the global parsers read it. */
|
|
74
|
+
limitFor(requestUrl) {
|
|
75
|
+
if (typeof requestUrl !== 'string')
|
|
76
|
+
return undefined;
|
|
77
|
+
return this.limits.get(requestUrl.split('?')[0]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the body of a large-body provider endpoint RAW, with that endpoint's own limit (#3395), before the global parsers
|
|
82
|
+
* (whose 10 MB limit would refuse it). Mounted on the webhook prefix; every other request passes through untouched. The
|
|
83
|
+
* bytes land in `req.rawBody` (verification needs them exactly), the body is marked parsed so the global parsers skip
|
|
84
|
+
* it, and a body over the endpoint's limit reaches the receiver as the same flag the global parsers' refusal sets, so it
|
|
85
|
+
* is answered with the receiver's audited 413. The limit is the endpoint's own, so the server never buffers more than the
|
|
86
|
+
* endpoint may accept.
|
|
87
|
+
*/
|
|
88
|
+
export function applyInboundLargeBodyProviderEndpoints(app, routes, basePath = MAIN_API_BASE_PATH) {
|
|
89
|
+
const parsers = new Map();
|
|
90
|
+
const parserFor = (limit) => {
|
|
91
|
+
let parser = parsers.get(limit);
|
|
92
|
+
if (!parser) {
|
|
93
|
+
parser = express.raw({ type: () => true, limit });
|
|
94
|
+
parsers.set(limit, parser);
|
|
95
|
+
}
|
|
96
|
+
return parser;
|
|
97
|
+
};
|
|
98
|
+
app.use(`${basePath}/webhooks/`, (req, res, next) => {
|
|
99
|
+
const limit = req.method === 'POST' ? routes.limitFor(req.originalUrl) : undefined;
|
|
100
|
+
if (limit === undefined) {
|
|
101
|
+
next();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
parserFor(limit)(req, res, (error) => {
|
|
105
|
+
if (error) {
|
|
106
|
+
next(error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (Buffer.isBuffer(req.body))
|
|
110
|
+
req.rawBody = req.body;
|
|
111
|
+
next();
|
|
112
|
+
});
|
|
113
|
+
}, translateOversizedInboundBody);
|
|
114
|
+
}
|
|
61
115
|
//# sourceMappingURL=inbound-endpoint-raw-body.backend.utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbound-endpoint-raw-body.backend.utils.js","sourceRoot":"","sources":["../../../../../src/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.ts"],"names":[],"mappings":"AACA,OAAO,OAAyE,MAAM,SAAS,CAAC;AAChG,OAAO,EAAE,yCAAyC,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEtG,OAAO,EAAE,mCAAmC,EAAE,MAAM,8EAA8E,CAAC;AAEnI;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sCAAsC,CAAC,GAAY,EAAE,WAAmB,kBAAkB;IACxG,GAAG,CAAC,GAAG,CACL,GAAG,QAAQ,aAAa,yCAAyC,GAAG,EACpE,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,mCAAmC,EAAE,CAAC,EAC7E,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;QACnD,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC;QACtD,IAAI,EAAE,CAAC;IACT,CAAC,EACD,6BAA6B,CAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAA2B;IAC5D,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QACxB,MAAM,GAAG,GAAI,GAAwB,CAAC,GAAG,CAAC;QAC1C,IAAI,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YAC5D,GAA0B,CAAC,OAAO,GAAG,GAAG,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,GAAY,EAAE,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC1G,IAAK,GAAiC,EAAE,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACpE,GAAG,CAAC,mBAAmB,GAAG,IAAI,CAAC;QAC9B,GAAqC,CAAC,KAAK,GAAG,IAAI,CAAC;QACpD,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,CAAC;AACZ,CAAC","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport express, { type Express, type NextFunction, type Request, type Response } from 'express';\nimport { INBOUND_APPLICATION_ENDPOINT_PROVIDER_REF, MAIN_API_BASE_PATH } from '@wildo-ai/saas-models';\n\nimport { MAX_APPLICATION_ENDPOINT_BODY_BYTES } from '~wildo-backend-lib/inbound/services/inbound-handler-registry.backend.service';\n\n/**\n * Application inbound endpoints (`POST {base}/webhooks/app/{endpointKey}`, #1596) take their body RAW.\n *\n * Mounted BEFORE the global `express.json`, the same way `applyOAuthBodyCap` is. Without it the global\n * parser answered a body that is not valid JSON with its own 400 before the receiver ran, so a malformed\n * delivery left no `INBOUND_DELIVERY_REJECTED` row, and the receiver's own parse-failure branch could never\n * be reached. The parser here keeps the exact bytes (`req.rawBody`, which verification needs anyway),\n * marks the body parsed so the global parser skips it, and leaves media type, size and JSON validity to\n * the receiver, which audits every refusal.\n *\n * The limit is the largest cap an endpoint may declare; each endpoint's own `maxBodyBytes` is enforced by\n * the receiver (413, audited). A body over THIS limit is refused by the parser itself; the error handler\n * below turns that refusal into a flag the receiver reads, so it answers the same audited 413 instead of the\n * parser's error reaching the generic handler as an unaudited 400.\n *\n * Known limit, kept deliberately: a body under this ceiling is buffered before the receiver compares it with\n * the endpoint's own, smaller cap. A declared `content-length` over that cap is still refused by the receiver\n * without reading anything further, and the ceiling bounds what an undeclared-length request can make the\n * server hold.\n */\nexport function applyInboundApplicationEndpointRawBody(app: Express, basePath: string = MAIN_API_BASE_PATH): void {\n app.use(\n `${basePath}/webhooks/${INBOUND_APPLICATION_ENDPOINT_PROVIDER_REF}/`,\n express.raw({ type: () => true, limit: MAX_APPLICATION_ENDPOINT_BODY_BYTES }),\n (req: Request, _res: Response, next: NextFunction) => {\n if (Buffer.isBuffer(req.body)) req.rawBody = req.body;\n next();\n },\n translateOversizedInboundBody,\n );\n}\n\n/**\n * The body parsers' `verify` hook that keeps the exact received bytes in `req.rawBody`, for requests under one of\n * `prefixes` only, so ordinary API traffic retains nothing past the parse.\n *\n * Given to BOTH global parsers, JSON and form (#2659): a provider endpoint's signature covers the bytes as they\n * arrived, and Slack's interactions and slash commands arrive `application/x-www-form-urlencoded`. A re-encoding of\n * the parsed fields would not byte-match what was signed.\n */\nexport function retainRawBodyUnder(prefixes: readonly string[]): (req: IncomingMessage, res: ServerResponse, buf: Buffer) => void {\n return (req, _res, buf) => {\n const url = (req as { url?: string }).url;\n if (url && prefixes.some((prefix) => url.startsWith(prefix))) {\n (req as unknown as Request).rawBody = buf;\n }\n };\n}\n\n/**\n * The raw parser's size refusal, handed to the receiver as a flag. Only `entity.too.large` is translated;\n * every other parser error keeps its usual path. `_body` is set so the global parsers do not read the\n * remaining stream a second time.\n */\nexport function translateOversizedInboundBody(err: unknown, req: Request, _res: Response, next: NextFunction): void {\n if ((err as { type?: unknown } | null)?.type === 'entity.too.large') {\n req.inboundBodyTooLarge = true;\n (req as Request & { _body?: boolean })._body = true;\n next();\n return;\n }\n next(err);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"inbound-endpoint-raw-body.backend.utils.js","sourceRoot":"","sources":["../../../../../src/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport express, { type Express, type NextFunction, type Request, type Response } from 'express';\nimport { INBOUND_APPLICATION_ENDPOINT_PROVIDER_REF, MAIN_API_BASE_PATH } from '@wildo-ai/saas-models';\n\nimport { MAX_APPLICATION_ENDPOINT_BODY_BYTES } from '~wildo-backend-lib/inbound/services/inbound-handler-registry.backend.service';\n\n/**\n * Application inbound endpoints (`POST {base}/webhooks/app/{endpointKey}`, #1596) take their body RAW.\n *\n * Mounted BEFORE the global `express.json`, the same way `applyOAuthBodyCap` is. Without it the global\n * parser answered a body that is not valid JSON with its own 400 before the receiver ran, so a malformed\n * delivery left no `INBOUND_DELIVERY_REJECTED` row, and the receiver's own parse-failure branch could never\n * be reached. The parser here keeps the exact bytes (`req.rawBody`, which verification needs anyway),\n * marks the body parsed so the global parser skips it, and leaves media type, size and JSON validity to\n * the receiver, which audits every refusal.\n *\n * The limit is the largest cap an endpoint may declare; each endpoint's own `maxBodyBytes` is enforced by\n * the receiver (413, audited). A body over THIS limit is refused by the parser itself; the error handler\n * below turns that refusal into a flag the receiver reads, so it answers the same audited 413 instead of the\n * parser's error reaching the generic handler as an unaudited 400.\n *\n * Known limit, kept deliberately: a body under this ceiling is buffered before the receiver compares it with\n * the endpoint's own, smaller cap. A declared `content-length` over that cap is still refused by the receiver\n * without reading anything further, and the ceiling bounds what an undeclared-length request can make the\n * server hold.\n */\nexport function applyInboundApplicationEndpointRawBody(app: Express, basePath: string = MAIN_API_BASE_PATH): void {\n app.use(\n `${basePath}/webhooks/${INBOUND_APPLICATION_ENDPOINT_PROVIDER_REF}/`,\n express.raw({ type: () => true, limit: MAX_APPLICATION_ENDPOINT_BODY_BYTES }),\n (req: Request, _res: Response, next: NextFunction) => {\n if (Buffer.isBuffer(req.body)) req.rawBody = req.body;\n next();\n },\n translateOversizedInboundBody,\n );\n}\n\n/**\n * The body parsers' `verify` hook that keeps the exact received bytes in `req.rawBody`, for requests under one of\n * `prefixes` only, so ordinary API traffic retains nothing past the parse.\n *\n * Given to BOTH global parsers, JSON and form (#2659): a provider endpoint's signature covers the bytes as they\n * arrived, and Slack's interactions and slash commands arrive `application/x-www-form-urlencoded`. A re-encoding of\n * the parsed fields would not byte-match what was signed.\n */\nexport function retainRawBodyUnder(prefixes: readonly string[]): (req: IncomingMessage, res: ServerResponse, buf: Buffer) => void {\n return (req, _res, buf) => {\n const url = (req as { url?: string }).url;\n if (url && prefixes.some((prefix) => url.startsWith(prefix))) {\n (req as unknown as Request).rawBody = buf;\n }\n };\n}\n\n/**\n * The raw parser's size refusal, handed to the receiver as a flag. Only `entity.too.large` is translated;\n * every other parser error keeps its usual path. `_body` is set so the global parsers do not read the\n * remaining stream a second time.\n */\nexport function translateOversizedInboundBody(err: unknown, req: Request, _res: Response, next: NextFunction): void {\n if ((err as { type?: unknown } | null)?.type === 'entity.too.large') {\n req.inboundBodyTooLarge = true;\n (req as Request & { _body?: boolean })._body = true;\n next();\n return;\n }\n next(err);\n}\n\n/**\n * The provider endpoints whose body may be larger than the global parsers read (#3395), by full route path: a vendor that\n * pushes a whole email in its webhook (Postmark, Mailgun) declares a `maxBodyBytes` past the 10 MB the global JSON parser\n * reads, and the delivery stores such a body outside its row. One instance per backend: the webhook controller declares\n * the routes it mounts, and the body parser mounted before the global parsers reads them at request time.\n */\nexport class InboundLargeBodyRoutesBackendService {\n private readonly limits = new Map<string, number>();\n\n /** The route at `fullPath` (`/api/v1/webhooks/{provider}/{endpoint}`) reads bodies up to `limitBytes`. */\n declare(fullPath: string, limitBytes: number): void {\n this.limits.set(fullPath, limitBytes);\n }\n\n /** The body limit of the route a request arrived on, or undefined when the global parsers read it. */\n limitFor(requestUrl: string | undefined): number | undefined {\n if (typeof requestUrl !== 'string') return undefined;\n return this.limits.get(requestUrl.split('?')[0]!);\n }\n}\n\n/**\n * Read the body of a large-body provider endpoint RAW, with that endpoint's own limit (#3395), before the global parsers\n * (whose 10 MB limit would refuse it). Mounted on the webhook prefix; every other request passes through untouched. The\n * bytes land in `req.rawBody` (verification needs them exactly), the body is marked parsed so the global parsers skip\n * it, and a body over the endpoint's limit reaches the receiver as the same flag the global parsers' refusal sets, so it\n * is answered with the receiver's audited 413. The limit is the endpoint's own, so the server never buffers more than the\n * endpoint may accept.\n */\nexport function applyInboundLargeBodyProviderEndpoints(app: Express, routes: Pick<InboundLargeBodyRoutesBackendService, 'limitFor'>, basePath: string = MAIN_API_BASE_PATH): void {\n const parsers = new Map<number, ReturnType<typeof express.raw>>();\n const parserFor = (limit: number) => {\n let parser = parsers.get(limit);\n if (!parser) {\n parser = express.raw({ type: () => true, limit });\n parsers.set(limit, parser);\n }\n return parser;\n };\n app.use(\n `${basePath}/webhooks/`,\n (req: Request, res: Response, next: NextFunction) => {\n const limit = req.method === 'POST' ? routes.limitFor(req.originalUrl) : undefined;\n if (limit === undefined) {\n next();\n return;\n }\n parserFor(limit)(req, res, (error?: unknown) => {\n if (error) {\n next(error);\n return;\n }\n if (Buffer.isBuffer(req.body)) req.rawBody = req.body;\n next();\n });\n },\n translateOversizedInboundBody,\n );\n}\n"]}
|
|
@@ -11,6 +11,7 @@ import { EmailBackendService } from '../../../../notifications/email/services/em
|
|
|
11
11
|
import { AuditLogsBackendService } from '../../../../compliance/audit-trails/services/audit-logs.backend.service';
|
|
12
12
|
import type { ExecutionContext } from '../../../../runtime/models/execution-context.backend.schemas';
|
|
13
13
|
import type { RecipientLanguageBackendService } from '../../../../notifications/services/recipient-language.backend.service';
|
|
14
|
+
import type { ServiceOptions } from '../../../../base-classes/models/repositories.backend.schemas';
|
|
14
15
|
export interface PasswordResetRequestParams {
|
|
15
16
|
email: string;
|
|
16
17
|
frontendUrl: string;
|
|
@@ -85,6 +86,17 @@ export declare class AuthPasswordResetBackendService {
|
|
|
85
86
|
* caller would imply nothing happened. The reset remains completable through a resend.
|
|
86
87
|
*/
|
|
87
88
|
private _mintResetTokenAndDispatchEmail;
|
|
89
|
+
/**
|
|
90
|
+
* The reset token itself, written through `transaction` when one is given (#2297): a forced reset mints it inside the
|
|
91
|
+
* operation's transaction, so a rolled-back attempt leaves no live link behind.
|
|
92
|
+
*/
|
|
93
|
+
private _mintResetToken;
|
|
94
|
+
/**
|
|
95
|
+
* Mail the reset link carrying `resetToken`'s RAW value. Never captured into an outbox (#2297): the value exists only
|
|
96
|
+
* in memory, and keeping it in a stored row would put a live credential at rest. Failure is logged, never thrown (see
|
|
97
|
+
* {@link _mintResetTokenAndDispatchEmail}).
|
|
98
|
+
*/
|
|
99
|
+
private _dispatchResetEmail;
|
|
88
100
|
/**
|
|
89
101
|
* ADMINISTRATOR-compelled credential reset — the privileged twin of
|
|
90
102
|
* {@link requestPasswordReset}.
|
|
@@ -118,6 +130,45 @@ export declare class AuthPasswordResetBackendService {
|
|
|
118
130
|
expiresAt: Date;
|
|
119
131
|
sessionsRevoked: boolean;
|
|
120
132
|
}>;
|
|
133
|
+
/**
|
|
134
|
+
* The first half of {@link forcePasswordResetByAdministrator} (#2297): resolve the account, refuse what may not be reset,
|
|
135
|
+
* and evict every live session. Runs OUTSIDE any database transaction, BEFORE the token is minted, exactly as the whole
|
|
136
|
+
* reset used to: a refusal or a failed revocation stops the reset with no link minted, and the revocation (Redis) is an
|
|
137
|
+
* effect a rolled-back attempt could not undo, so it never runs inside one. The `users | force_password_reset`
|
|
138
|
+
* operation runs it in its `beforeTransactionalCore` phase.
|
|
139
|
+
*
|
|
140
|
+
* Answers the account the second half mints for.
|
|
141
|
+
*/
|
|
142
|
+
revokeAccessForForcedPasswordReset(ec: ExecutionContext<any>, params: {
|
|
143
|
+
targetUserId: string;
|
|
144
|
+
}): Promise<{
|
|
145
|
+
_id: string;
|
|
146
|
+
email: string;
|
|
147
|
+
}>;
|
|
148
|
+
/**
|
|
149
|
+
* The second half of {@link forcePasswordResetByAdministrator} (#2297): mint the single-use reset token for `user`, whose
|
|
150
|
+
* sessions {@link revokeAccessForForcedPasswordReset} already evicted, then mail the link and record who forced it.
|
|
151
|
+
*
|
|
152
|
+
* With a transaction in `serviceOptions` the token is written THROUGH it, and the two effects outside the database run
|
|
153
|
+
* after the commit: a rolled-back attempt leaves no live link and sends no email, and a retried attempt mints once per
|
|
154
|
+
* attempt that commits. The record (`USER_PASSWORD_RESET_FORCED`) is a capturable recording, so an attempt the framework
|
|
155
|
+
* captures keeps it in the post-commit effect outbox and a crash between commit and drain loses no record. The email is
|
|
156
|
+
* deliberately NOT captured: it carries the token's raw value, which must never be stored (a lost email is recovered by
|
|
157
|
+
* forcing the reset again, which revokes the stale link's sessions and mints a fresh one).
|
|
158
|
+
*
|
|
159
|
+
* Decided by this repository's principles, not by asking: the record used to be awaited before the operation answered.
|
|
160
|
+
* `_createAuditLog` already isolates a store failure, so it never failed the operation; a record kept with the commit and
|
|
161
|
+
* written once afterwards is the stronger guarantee (the transactional-outbox shape #2297 settled for every audited write).
|
|
162
|
+
* Without a transaction both effects run inline, as before.
|
|
163
|
+
*/
|
|
164
|
+
issueForcedPasswordReset(ec: ExecutionContext<any>, params: Omit<AdministratorForcedPasswordResetParams, 'targetUserId'> & {
|
|
165
|
+
user: {
|
|
166
|
+
_id: string;
|
|
167
|
+
email: string;
|
|
168
|
+
};
|
|
169
|
+
}, serviceOptions?: ServiceOptions): Promise<{
|
|
170
|
+
expiresAt: Date;
|
|
171
|
+
}>;
|
|
121
172
|
/**
|
|
122
173
|
* Consumes token, validates policy, hashes, updates creds, revokes tokens, audits.
|
|
123
174
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-password-reset.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../src/security/authentications/services/auth-methods/auth-password-reset.backend.service.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAG1B,OAAO,EAAE,aAAa,EAAE,MAAM,8DAA8D,CAAC;AAC7F,OAAO,EAAE,0BAA0B,EAAE,MAAM,kEAAkE,CAAC;AAE9G,OAAO,EAAE,uBAAuB,EAAE,MAAM,sEAAsE,CAAC;AAE/G,OAAO,EAAE,6BAA6B,EAAE,MAAM,yFAAyF,CAAC;AACxI,OAAO,EAAE,oCAAoC,EAAE,MAAM,+FAA+F,CAAC;AACrJ,OAAO,EAAE,6BAA6B,EAAE,MAAM,wFAAwF,CAAC;AACvI,OAAO,EAAE,4BAA4B,EAAE,MAAM,6FAA6F,CAAC;AAI3I,OAAO,EAAE,gCAAgC,EAAE,MAAM,iGAAiG,CAAC;AACnJ,OAAO,EACL,mBAAmB,EACpB,MAAM,uEAAuE,CAAC;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,gFAAgF,CAAC;AAkBzH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qEAAqE,CAAC;AAC5G,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,8EAA8E,CAAC;
|
|
1
|
+
{"version":3,"file":"auth-password-reset.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../src/security/authentications/services/auth-methods/auth-password-reset.backend.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,6 +30,8 @@ import { BrandAssetComposition, BrandAssetPurpose, SecurityAlertType } from '@wi
|
|
|
30
30
|
import { sendSecurityAlert } from '../../../../security/authentications/services/utils/authenticator-added-notice.backend.utils.js';
|
|
31
31
|
import { DurationUnit } from '@wildo-ai/zod-decorators';
|
|
32
32
|
import { AuthResponseMessageRef } from '@wildo-ai/presets-components-models';
|
|
33
|
+
import { registerTransactionPostCommitEffect } from '../../../../databases/models/transaction.shared.schemas.js';
|
|
34
|
+
import { emitAuthorityChangeAuditWhenDurable } from '../../../../organizations/services/authority-change-audit.backend.utils.js';
|
|
33
35
|
/**
|
|
34
36
|
* Lifetime of a password-reset token, in hours.
|
|
35
37
|
*
|
|
@@ -133,7 +135,16 @@ let AuthPasswordResetBackendService = class AuthPasswordResetBackendService {
|
|
|
133
135
|
* caller would imply nothing happened. The reset remains completable through a resend.
|
|
134
136
|
*/
|
|
135
137
|
async _mintResetTokenAndDispatchEmail(ec, user, frontendUrl) {
|
|
136
|
-
const resetToken = await this.
|
|
138
|
+
const resetToken = await this._mintResetToken(user);
|
|
139
|
+
await this._dispatchResetEmail(user, frontendUrl, resetToken);
|
|
140
|
+
return { expiresAt: resetToken.expiresAt };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The reset token itself, written through `transaction` when one is given (#2297): a forced reset mints it inside the
|
|
144
|
+
* operation's transaction, so a rolled-back attempt leaves no live link behind.
|
|
145
|
+
*/
|
|
146
|
+
async _mintResetToken(user, transaction) {
|
|
147
|
+
return this.consumableTokenService.createToken({
|
|
137
148
|
tokenType: CoreConsumableTokenTypes.PASSWORD_RESET,
|
|
138
149
|
consumptionMode: ConsumableToken_ConsumptionMode.SINGLE_USE,
|
|
139
150
|
expiresIn: { value: PASSWORD_RESET_TOKEN_TTL_HOURS, unit: DurationUnit.HOURS },
|
|
@@ -141,7 +152,14 @@ let AuthPasswordResetBackendService = class AuthPasswordResetBackendService {
|
|
|
141
152
|
resourceIdentifier: CoreResourceType.USERS,
|
|
142
153
|
relatedId: user._id,
|
|
143
154
|
metadata: { email: user.email },
|
|
144
|
-
});
|
|
155
|
+
}, transaction);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Mail the reset link carrying `resetToken`'s RAW value. Never captured into an outbox (#2297): the value exists only
|
|
159
|
+
* in memory, and keeping it in a stored row would put a live credential at rest. Failure is logged, never thrown (see
|
|
160
|
+
* {@link _mintResetTokenAndDispatchEmail}).
|
|
161
|
+
*/
|
|
162
|
+
async _dispatchResetEmail(user, frontendUrl, resetToken) {
|
|
145
163
|
const resetUrl = `${frontendUrl}/password-lost?token=${encodeURIComponent(resetToken.token)}`;
|
|
146
164
|
try {
|
|
147
165
|
const appIdentity = this.appConfigService.config.appIdentity;
|
|
@@ -198,7 +216,6 @@ let AuthPasswordResetBackendService = class AuthPasswordResetBackendService {
|
|
|
198
216
|
...projectCaughtErrorLogFields(emailError),
|
|
199
217
|
});
|
|
200
218
|
}
|
|
201
|
-
return { expiresAt: resetToken.expiresAt };
|
|
202
219
|
}
|
|
203
220
|
/**
|
|
204
221
|
* ADMINISTRATOR-compelled credential reset — the privileged twin of
|
|
@@ -230,7 +247,21 @@ let AuthPasswordResetBackendService = class AuthPasswordResetBackendService {
|
|
|
230
247
|
* intervention is authorized, attributable, justified, and evicts existing access.
|
|
231
248
|
*/
|
|
232
249
|
async forcePasswordResetByAdministrator(ec, params) {
|
|
233
|
-
const
|
|
250
|
+
const user = await this.revokeAccessForForcedPasswordReset(ec, { targetUserId: params.targetUserId });
|
|
251
|
+
const { expiresAt } = await this.issueForcedPasswordReset(ec, { ...params, user });
|
|
252
|
+
return { expiresAt, sessionsRevoked: true };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The first half of {@link forcePasswordResetByAdministrator} (#2297): resolve the account, refuse what may not be reset,
|
|
256
|
+
* and evict every live session. Runs OUTSIDE any database transaction, BEFORE the token is minted, exactly as the whole
|
|
257
|
+
* reset used to: a refusal or a failed revocation stops the reset with no link minted, and the revocation (Redis) is an
|
|
258
|
+
* effect a rolled-back attempt could not undo, so it never runs inside one. The `users | force_password_reset`
|
|
259
|
+
* operation runs it in its `beforeTransactionalCore` phase.
|
|
260
|
+
*
|
|
261
|
+
* Answers the account the second half mints for.
|
|
262
|
+
*/
|
|
263
|
+
async revokeAccessForForcedPasswordReset(ec, params) {
|
|
264
|
+
const { targetUserId } = params;
|
|
234
265
|
const user = await this.userLookup.findUserById(ec, targetUserId);
|
|
235
266
|
if (!user) {
|
|
236
267
|
throw this.errorBuilder.buildError(ErrorType.NOT_FOUND, ec, {
|
|
@@ -251,17 +282,58 @@ let AuthPasswordResetBackendService = class AuthPasswordResetBackendService {
|
|
|
251
282
|
// disables, and the sessions would already be gone: the credential belongs to the directory.
|
|
252
283
|
await this.userLookup.assertCredentialMethodEnabledForUser(ec, user._id, AuthMethod.PASSWORD);
|
|
253
284
|
await this.tokenIssuer.invalidateAllTokensForUser(user._id);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
285
|
+
return { _id: String(user._id), email: user.email };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The second half of {@link forcePasswordResetByAdministrator} (#2297): mint the single-use reset token for `user`, whose
|
|
289
|
+
* sessions {@link revokeAccessForForcedPasswordReset} already evicted, then mail the link and record who forced it.
|
|
290
|
+
*
|
|
291
|
+
* With a transaction in `serviceOptions` the token is written THROUGH it, and the two effects outside the database run
|
|
292
|
+
* after the commit: a rolled-back attempt leaves no live link and sends no email, and a retried attempt mints once per
|
|
293
|
+
* attempt that commits. The record (`USER_PASSWORD_RESET_FORCED`) is a capturable recording, so an attempt the framework
|
|
294
|
+
* captures keeps it in the post-commit effect outbox and a crash between commit and drain loses no record. The email is
|
|
295
|
+
* deliberately NOT captured: it carries the token's raw value, which must never be stored (a lost email is recovered by
|
|
296
|
+
* forcing the reset again, which revokes the stale link's sessions and mints a fresh one).
|
|
297
|
+
*
|
|
298
|
+
* Decided by this repository's principles, not by asking: the record used to be awaited before the operation answered.
|
|
299
|
+
* `_createAuditLog` already isolates a store failure, so it never failed the operation; a record kept with the commit and
|
|
300
|
+
* written once afterwards is the stronger guarantee (the transactional-outbox shape #2297 settled for every audited write).
|
|
301
|
+
* Without a transaction both effects run inline, as before.
|
|
302
|
+
*/
|
|
303
|
+
async issueForcedPasswordReset(ec, params, serviceOptions) {
|
|
304
|
+
const { user, actorUserId, reason, frontendUrl, ipAddress, userAgent } = params;
|
|
305
|
+
const transaction = serviceOptions?.transaction;
|
|
306
|
+
const resetToken = await this._mintResetToken(user, transaction);
|
|
307
|
+
const sendEmail = () => this._dispatchResetEmail(user, frontendUrl, resetToken);
|
|
308
|
+
if (transaction) {
|
|
309
|
+
registerTransactionPostCommitEffect(transaction, {
|
|
310
|
+
key: `forced-password-reset-email:${user._id}:${transaction.id}`,
|
|
311
|
+
execute: sendEmail,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
await sendEmail();
|
|
316
|
+
}
|
|
317
|
+
await emitAuthorityChangeAuditWhenDurable({
|
|
318
|
+
serviceOptions,
|
|
319
|
+
effectKey: `forced-password-reset-record:${user._id}`,
|
|
320
|
+
logger: this.logger,
|
|
321
|
+
failureContext: { targetUserId: user._id, actorUserId, eventType: 'USER_PASSWORD_RESET_FORCED' },
|
|
322
|
+
recording: {
|
|
323
|
+
auditLogs: () => this.auditLogsService,
|
|
324
|
+
write: (auditLogs) => auditLogs.logUserPasswordResetForced({
|
|
325
|
+
targetUserId: user._id,
|
|
326
|
+
targetEmail: user.email,
|
|
327
|
+
actorUserId,
|
|
328
|
+
reason,
|
|
329
|
+
sessionsRevoked: true,
|
|
330
|
+
ipAddress,
|
|
331
|
+
userAgent,
|
|
332
|
+
}, ec),
|
|
333
|
+
subjectId: user._id,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
return { expiresAt: resetToken.expiresAt };
|
|
265
337
|
}
|
|
266
338
|
/**
|
|
267
339
|
* Consumes token, validates policy, hashes, updates creds, revokes tokens, audits.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-password-reset.backend.service.js","sourceRoot":"","sources":["../../../../../../src/security/authentications/services/auth-methods/auth-password-reset.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { injectable, inject } from 'inversify';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport { LoggerService } from '~wildo-backend-lib/monitoring/logger/services/logger.service';\nimport { ErrorBuilderBackendService } from '~wildo-backend-lib/errors/services/error-builder.backend.service';\nimport { describeCaughtError, projectCaughtErrorLogFields } from '~wildo-backend-lib/errors/utils/error-log-level-classification.backend.utils';\nimport { AppConfigurationService } from '~wildo-backend-lib/config/services/app-configuration.backend.service';\nimport { resolveRecipientLanguage } from '~wildo-backend-lib/config/services/app-configuration.backend.utils';\nimport { ConsumableTokenBackendService } from '~wildo-backend-lib/security/consumable-tokens/services/consumable-token.backend.service';\nimport { AuthCredentialVerifierBackendService } from '~wildo-backend-lib/security/authentications/services/auth-credential-verifier.backend.service';\nimport { AuthTokenIssuerBackendService } from '~wildo-backend-lib/security/authentications/services/auth-token-issuer.backend.service';\nimport { AuthUserLookupBackendService } from '~wildo-backend-lib/security/authentications/services/utils/auth-user-lookup.backend.service';\nimport {\n isCredentialMethodEnabledByPolicy,\n} from '~wildo-backend-lib/security/authentications/services/utils/auth-policy-resolution.backend.utils';\nimport { AuthResponseHelperBackendService } from '~wildo-backend-lib/security/authentications/services/utils/auth-response-helper.backend.service';\nimport {\n EmailBackendService,\n} from '~wildo-backend-lib/notifications/email/services/email.backend.service';\nimport { AuditLogsBackendService } from '~wildo-backend-lib/compliance/audit-trails/services/audit-logs.backend.service';\nimport {\n applicationDisplayName,\n AuthMethod,\n EmailSubmissionOutcome,\n CoreConsumableTokenTypes,\n CoreResourceType,\n ConsumableToken_ConsumptionMode,\n ErrorType,\n ErrorCustomMessageReference,\n EmailTemplateType,\n SystemEmailTemplateRef,\n UserStatus,\n} from '@wildo-ai/saas-models';\nimport { BrandAssetComposition, BrandAssetPurpose, SecurityAlertType } from '@wildo-ai/saas-models';\nimport { sendSecurityAlert } from '~wildo-backend-lib/security/authentications/services/utils/authenticator-added-notice.backend.utils';\nimport { DurationUnit } from '@wildo-ai/zod-decorators';\nimport { AuthResponseMessageRef } from '@wildo-ai/presets-components-models';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport type { RecipientLanguageBackendService } from '~wildo-backend-lib/notifications/services/recipient-language.backend.service';\n\nexport interface PasswordResetRequestParams {\n email: string;\n frontendUrl: string;\n ipAddress: string;\n userAgent: string;\n}\n\nexport interface PasswordResetCompleteParams {\n token: string;\n newPassword: string;\n ipAddress: string;\n userAgent: string;\n}\n\n/**\n * Input for {@link AuthPasswordResetBackendService.forcePasswordResetByAdministrator}.\n *\n * `actorUserId` is supplied by the caller from the AUTHENTICATED execution context, never from\n * request input — it is the attribution anchor of the resulting audit record.\n */\nexport interface AdministratorForcedPasswordResetParams {\n /** The account whose credential is being reset. */\n targetUserId: string;\n /** The administrator performing the reset, read from the execution context. */\n actorUserId: string;\n /** The operator's mandatory justification, recorded in the audit trail. */\n reason: string;\n /** Public base URL of the END-USER frontend, so the emailed link lands on the user's app. */\n frontendUrl: string;\n /**\n * Transport metadata, optional: the resource-operation entry point receives an\n * `ExecutionContext`, which carries identity but no request transport detail.\n */\n ipAddress?: string;\n userAgent?: string;\n}\n\n/**\n * Lifetime of a password-reset token, in hours.\n *\n * A framework policy constant rather than a per-call argument on either entry point. The\n * administrator-forced path originally declared a caller-supplied `resetExpirationHours`\n * (1–168); an operator able to widen the window could leave a week-long credential-recovery\n * path parked on an account, which is a persistence mechanism rather than a recovery one.\n */\nconst PASSWORD_RESET_TOKEN_TTL_HOURS = 1;\n\n/**\n * Handles password reset request and completion.\n * Extracted from AuthenticationControllerBackendService.\n */\n@injectable()\nexport class AuthPasswordResetBackendService {\n private readonly logDebug: (message: string, context?: Record<string, unknown>) => void;\n\n constructor(\n @inject(SAAS_SERVICE_TYPES.Logger) private readonly logger: LoggerService,\n @inject(SAAS_SERVICE_TYPES.ErrorBuilderService) private readonly errorBuilder: ErrorBuilderBackendService,\n @inject(SAAS_SERVICE_TYPES.AppConfigService) private readonly appConfigService: AppConfigurationService,\n @inject(SAAS_SERVICE_TYPES.ConsumableTokenService) private readonly consumableTokenService: ConsumableTokenBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthCredentialVerifier) private readonly credentialVerifier: AuthCredentialVerifierBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthTokenIssuer) private readonly tokenIssuer: AuthTokenIssuerBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthUserLookupService) private readonly userLookup: AuthUserLookupBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthResponseHelperService) private readonly authResponseHelper: AuthResponseHelperBackendService,\n @inject(SAAS_SERVICE_TYPES.EmailService) private readonly emailService: EmailBackendService,\n @inject(SAAS_SERVICE_TYPES.AuditLogsService) private readonly auditLogsService: AuditLogsBackendService,\n // Writes a person's own emails in their language (#1799 review M3).\n @inject(SAAS_SERVICE_TYPES.RecipientLanguageService) private readonly recipientLanguage: RecipientLanguageBackendService,\n ) {\n this.logDebug = this.logger.debugByType('AuthPasswordReset');\n this.logger.setDebugTypeLogging('AuthPasswordReset', false);\n }\n\n /**\n * Always returns a success response (anti-enumeration).\n * Creates token, builds URL, sends email, audits — only when user exists.\n */\n async requestPasswordReset(\n ec: ExecutionContext<any>,\n params: PasswordResetRequestParams,\n ): Promise<{ messageRef: string; message: string }> {\n const { email, frontendUrl, ipAddress, userAgent } = params;\n\n this.logDebug('Password reset requested', { email });\n\n const successResponse = this.authResponseHelper.getAuthResponse(AuthResponseMessageRef.PASSWORD_RESET_REQUEST_SUCCESS);\n\n try {\n const user = await this.userLookup.findAnyUserByEmail(ec, email);\n\n if (!user) {\n this.logDebug('Password reset: user not found, returning success to prevent enumeration', { email });\n this.auditLogsService.logUserPasswordResetRequested({\n email, ipAddress, userAgent,\n }).catch(() => {});\n return successResponse;\n }\n\n // F9 fix: Don't send reset email to suspended/deactivated users.\n // Return generic success to maintain anti-enumeration.\n if (user.status !== UserStatus.ACTIVE && user.status !== UserStatus.PENDING_VERIFICATION) {\n this.logDebug('Password reset: user not in active status, returning success to prevent enumeration', {\n email, status: user.status,\n });\n return successResponse;\n }\n\n // #1156: no reset link for a password the effective policy disables. It could not be completed, and completing it\n // would re-create the credential AUTH-24 cleared. The same success response as every other no-send branch, so the\n // answer says nothing about the account.\n // An account whose user type cannot be read has no policy to answer from, so it is refused like every other\n // credential-establishing path refuses it, silently here.\n const userType = user.userTypes?.[0];\n const policy = userType ? await this.userLookup.resolveEffectiveAuthPolicy(ec, user._id, userType) : null;\n if (!userType || !isCredentialMethodEnabledByPolicy(policy, AuthMethod.PASSWORD)) {\n this.logDebug('Password reset: password not enabled for this account, no email sent', { userId: user._id });\n return successResponse;\n }\n\n await this._mintResetTokenAndDispatchEmail(ec, user, frontendUrl);\n\n this.auditLogsService.logUserPasswordResetRequested({\n email, ipAddress, userAgent,\n }).catch(() => {});\n } catch (error) {\n this.logger.error('Unexpected error during password reset request', {\n ...projectCaughtErrorLogFields(error),\n });\n }\n\n return successResponse;\n }\n\n /**\n * Mint a single-use PASSWORD_RESET token for `user` and dispatch the system reset email.\n *\n * The one place a reset token is minted and mailed. Both entry points share it — the\n * self-service `requestPasswordReset` and the administrator-compelled\n * `forcePasswordResetByAdministrator` — so the TTL, the link shape, the template, and the\n * locale-resolution fallback chain cannot drift between them. Only the surrounding policy\n * differs (anti-enumeration and audit event on one side; authorization, session revocation\n * and actor-attributed audit on the other).\n *\n * Email dispatch failure is logged, never thrown: the token is already valid, and failing the\n * caller would imply nothing happened. The reset remains completable through a resend.\n */\n private async _mintResetTokenAndDispatchEmail(\n ec: ExecutionContext<any>,\n user: { _id: string; email: string },\n frontendUrl: string,\n ): Promise<{ expiresAt: Date }> {\n const resetToken = await this.consumableTokenService.createToken({\n tokenType: CoreConsumableTokenTypes.PASSWORD_RESET,\n consumptionMode: ConsumableToken_ConsumptionMode.SINGLE_USE,\n expiresIn: { value: PASSWORD_RESET_TOKEN_TTL_HOURS, unit: DurationUnit.HOURS },\n userId: user._id,\n resourceIdentifier: CoreResourceType.USERS,\n relatedId: user._id,\n metadata: { email: user.email },\n });\n\n const resetUrl = `${frontendUrl}/password-lost?token=${encodeURIComponent(resetToken.token)}`;\n\n try {\n const appIdentity = this.appConfigService.config.appIdentity;\n\n const emailResult = await this.emailService.send({\n submissionId: String(resetToken._id),\n templateRef: SystemEmailTemplateRef.PASSWORD_RESET,\n // The same choice every per-person email makes (#1799). Read through the internal door: the person\n // asking for a reset is not signed in, so this request's context could not read their preferences.\n locale: await this.recipientLanguage.forUser(user._id),\n type: EmailTemplateType.TRANSACTIONAL,\n to: user.email,\n context: {\n currentObject: { email: user.email, userId: user._id },\n objectContext: {\n primaryScopeContext: {\n appName: applicationDisplayName(appIdentity),\n applicationId: this.appConfigService.applicationId,\n logoUrl: this.appConfigService.resolveBrandLogoAbsoluteUrl({\n composition: BrandAssetComposition.LOCKUP,\n purpose: BrandAssetPurpose.EMAIL,\n }) ?? undefined,\n },\n },\n userContext: { userId: user._id, email: user.email },\n additionalContext: {\n resetUrl,\n tokenValue: resetToken.token,\n expiresInMinutes: PASSWORD_RESET_TOKEN_TTL_HOURS * 60,\n },\n },\n });\n if (\n !emailResult.success\n || (emailResult.outcome !== EmailSubmissionOutcome.PROVIDER_ACCEPTED\n && emailResult.outcome !== EmailSubmissionOutcome.LOCAL_PREVIEW)\n ) {\n this.logger.error('Password reset email provider did not confirm acceptance', {\n userId: user._id,\n submissionId: emailResult.submissionId,\n outcome: emailResult.outcome,\n error: emailResult.error,\n });\n } else {\n this.logDebug('Password reset email submission completed', {\n outcome: emailResult.outcome,\n userId: user._id,\n submissionId: emailResult.submissionId,\n providerMessageIds: emailResult.providerMessageIds,\n });\n }\n } catch (emailError) {\n this.logger.error('Failed to send password reset email', {\n userId: user._id,\n ...projectCaughtErrorLogFields(emailError),\n });\n }\n\n return { expiresAt: resetToken.expiresAt };\n }\n\n /**\n * ADMINISTRATOR-compelled credential reset — the privileged twin of\n * {@link requestPasswordReset}.\n *\n * Backs the `users | force_password_reset` resource operation. Four deliberate differences\n * from the self-service path:\n *\n * 1. **No anti-enumeration.** The target is addressed by id by a caller already authorized to\n * read the row, so a missing user is a genuine `404` rather than a fabricated success.\n * 2. **The reset link is NEVER returned to the caller.** It is mailed to the account owner and\n * nothing else. Handing an administrator a link that sets someone's password would let them\n * then authenticate AS that person, unnoticed and — since no actor claim exists on a session\n * JWT — indistinguishable downstream from the real user. That is an account-takeover\n * primitive, not a support tool, which is why the operation's response carries only\n * `expiresAt`.\n * 3. **Active sessions are revoked.** The verb exists for suspected compromise; leaving the\n * attacker's live session working until the victim happens to complete the reset would make\n * the response protective in name only. Revocation happens BEFORE the token is minted so a\n * failure there aborts the whole operation rather than leaving a half-done reset.\n * 4. **The audit event names the actor.** `USER_PASSWORD_RESET_FORCED`, not the self-service\n * event, which has no actor field and would credit the account owner with the admin's act.\n *\n * The TTL is the framework's policy constant, deliberately not a caller-supplied value: an\n * operator who could widen the window could park a long-lived credential-recovery path on an\n * account.\n *\n * assurance-control: WILDO.IDENTITY.CREDENTIAL_LIFECYCLE — privileged credential\n * intervention is authorized, attributable, justified, and evicts existing access.\n */\n async forcePasswordResetByAdministrator(\n ec: ExecutionContext<any>,\n params: AdministratorForcedPasswordResetParams,\n ): Promise<{ expiresAt: Date; sessionsRevoked: boolean }> {\n const { targetUserId, actorUserId, reason, frontendUrl, ipAddress, userAgent } = params;\n\n const user = await this.userLookup.findUserById(ec, targetUserId);\n if (!user) {\n throw this.errorBuilder.buildError(ErrorType.NOT_FOUND, ec, {\n customMessageReference: ErrorCustomMessageReference.NOT_FOUND,\n context: { reason: 'User not found for forced password reset', targetUserId },\n });\n }\n\n // A soft-deleted identity has no credential path worth resetting, and mailing a reset link\n // to a closed account is its own disclosure. ACTIVE is not required: forcing a reset on a\n // SUSPENDED or PENDING_VERIFICATION account is a legitimate recovery action.\n if (user.status === UserStatus.DELETED) {\n throw this.errorBuilder.buildError(ErrorType.CONFLICT, ec, {\n customMessageReference: ErrorCustomMessageReference.CONFLICT,\n context: { reason: 'Cannot force a password reset on a deleted account', targetUserId },\n });\n }\n\n // #1156: refused before any side effect. The link could never be completed for a password the effective policy\n // disables, and the sessions would already be gone: the credential belongs to the directory.\n await this.userLookup.assertCredentialMethodEnabledForUser(ec, user._id, AuthMethod.PASSWORD);\n\n await this.tokenIssuer.invalidateAllTokensForUser(user._id);\n const { expiresAt } = await this._mintResetTokenAndDispatchEmail(ec, user, frontendUrl);\n\n await this.auditLogsService.logUserPasswordResetForced({\n targetUserId: user._id,\n targetEmail: user.email,\n actorUserId,\n reason,\n sessionsRevoked: true,\n ipAddress,\n userAgent,\n }, ec);\n\n return { expiresAt, sessionsRevoked: true };\n }\n\n /**\n * Consumes token, validates policy, hashes, updates creds, revokes tokens, audits.\n */\n async completePasswordReset(\n ec: ExecutionContext<any>,\n params: PasswordResetCompleteParams,\n ): Promise<{ messageRef: string; message: string }> {\n const { token, newPassword, ipAddress, userAgent } = params;\n\n this.logDebug('Password reset completion attempted');\n\n let consumedToken;\n try {\n consumedToken = await this.consumableTokenService.consumeToken(token);\n } catch {\n throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n });\n }\n\n if (consumedToken.tokenType !== CoreConsumableTokenTypes.PASSWORD_RESET) {\n throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n });\n }\n\n const userId = consumedToken.userId || consumedToken.relatedId;\n if (!userId) {\n throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n });\n }\n\n // #1156: a link minted before the block took effect must not re-create the password, and a subject whose user or\n // user type cannot be read is refused rather than written. The token is already spent, which is right: it could never\n // be completed.\n const { policy: userTypePolicy, userEmail } = await this.userLookup.assertCredentialMethodEnabledForUser(ec, userId, AuthMethod.PASSWORD);\n const passwordPolicy = userTypePolicy?.passwordPolicy;\n\n const policyValidation = this.credentialVerifier.validatePasswordPolicy(newPassword, passwordPolicy, { email: userEmail });\n if (!policyValidation.valid) {\n throw this.errorBuilder.buildError(ErrorType.VALIDATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.VALIDATION_PASSWORD_POLICY_VIOLATION,\n context: { violations: policyValidation.violations, minLength: passwordPolicy?.minLength, maxLength: passwordPolicy?.maxLength },\n });\n }\n\n const newPasswordHash = await this.credentialVerifier.hashPassword(newPassword, this.appConfigService.config.auth.passwordHashing);\n await this.tokenIssuer.invalidateAllTokensForUser(userId);\n await this.credentialVerifier.updatePasswordHash(ec, userId, newPasswordHash);\n await this.consumableTokenService.revokeTokensByRelatedId(userId, CoreConsumableTokenTypes.PASSWORD_RESET);\n\n this.auditLogsService.logUserPasswordResetCompleted({\n userId, ipAddress, userAgent,\n }, ec).catch(() => {});\n\n /*\n * Tell the account holder their password was changed through recovery. A reset is the path a\n * mailbox takeover uses, so this goes to the same address the link did and is a signal for the owner\n * rather than a defence against that case — it is the notice for every other one: a reset they did\n * not start, from a device they do not recognise. Best-effort, after the new password is stored.\n */\n await sendSecurityAlert(\n { emailService: this.emailService, appConfigService: this.appConfigService, logger: this.logger, recipientLanguage: this.recipientLanguage },\n { userId, email: userEmail, alertType: SecurityAlertType.PASSWORD_CHANGED, ipAddress },\n );\n\n this.logDebug('Password reset completed successfully', { userId });\n\n return this.authResponseHelper.getAuthResponse(AuthResponseMessageRef.PASSWORD_RESET_COMPLETE_SUCCESS);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auth-password-reset.backend.service.js","sourceRoot":"","sources":["../../../../../../src/security/authentications/services/auth-methods/auth-password-reset.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { injectable, inject } from 'inversify';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport { LoggerService } from '~wildo-backend-lib/monitoring/logger/services/logger.service';\nimport { ErrorBuilderBackendService } from '~wildo-backend-lib/errors/services/error-builder.backend.service';\nimport { describeCaughtError, projectCaughtErrorLogFields } from '~wildo-backend-lib/errors/utils/error-log-level-classification.backend.utils';\nimport { AppConfigurationService } from '~wildo-backend-lib/config/services/app-configuration.backend.service';\nimport { resolveRecipientLanguage } from '~wildo-backend-lib/config/services/app-configuration.backend.utils';\nimport { ConsumableTokenBackendService } from '~wildo-backend-lib/security/consumable-tokens/services/consumable-token.backend.service';\nimport { AuthCredentialVerifierBackendService } from '~wildo-backend-lib/security/authentications/services/auth-credential-verifier.backend.service';\nimport { AuthTokenIssuerBackendService } from '~wildo-backend-lib/security/authentications/services/auth-token-issuer.backend.service';\nimport { AuthUserLookupBackendService } from '~wildo-backend-lib/security/authentications/services/utils/auth-user-lookup.backend.service';\nimport {\n isCredentialMethodEnabledByPolicy,\n} from '~wildo-backend-lib/security/authentications/services/utils/auth-policy-resolution.backend.utils';\nimport { AuthResponseHelperBackendService } from '~wildo-backend-lib/security/authentications/services/utils/auth-response-helper.backend.service';\nimport {\n EmailBackendService,\n} from '~wildo-backend-lib/notifications/email/services/email.backend.service';\nimport { AuditLogsBackendService } from '~wildo-backend-lib/compliance/audit-trails/services/audit-logs.backend.service';\nimport {\n applicationDisplayName,\n AuthMethod,\n EmailSubmissionOutcome,\n CoreConsumableTokenTypes,\n CoreResourceType,\n ConsumableToken_ConsumptionMode,\n ErrorType,\n ErrorCustomMessageReference,\n EmailTemplateType,\n SystemEmailTemplateRef,\n UserStatus,\n} from '@wildo-ai/saas-models';\nimport { BrandAssetComposition, BrandAssetPurpose, SecurityAlertType } from '@wildo-ai/saas-models';\nimport { sendSecurityAlert } from '~wildo-backend-lib/security/authentications/services/utils/authenticator-added-notice.backend.utils';\nimport { DurationUnit } from '@wildo-ai/zod-decorators';\nimport { AuthResponseMessageRef } from '@wildo-ai/presets-components-models';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport type { RecipientLanguageBackendService } from '~wildo-backend-lib/notifications/services/recipient-language.backend.service';\nimport type { ServiceOptions } from '~wildo-backend-lib/base-classes/models/repositories.backend.schemas';\nimport { registerTransactionPostCommitEffect, type TransactionContext } from '~wildo-backend-lib/databases/models/transaction.shared.schemas';\nimport { emitAuthorityChangeAuditWhenDurable } from '~wildo-backend-lib/organizations/services/authority-change-audit.backend.utils';\n\nexport interface PasswordResetRequestParams {\n email: string;\n frontendUrl: string;\n ipAddress: string;\n userAgent: string;\n}\n\nexport interface PasswordResetCompleteParams {\n token: string;\n newPassword: string;\n ipAddress: string;\n userAgent: string;\n}\n\n/**\n * Input for {@link AuthPasswordResetBackendService.forcePasswordResetByAdministrator}.\n *\n * `actorUserId` is supplied by the caller from the AUTHENTICATED execution context, never from\n * request input — it is the attribution anchor of the resulting audit record.\n */\nexport interface AdministratorForcedPasswordResetParams {\n /** The account whose credential is being reset. */\n targetUserId: string;\n /** The administrator performing the reset, read from the execution context. */\n actorUserId: string;\n /** The operator's mandatory justification, recorded in the audit trail. */\n reason: string;\n /** Public base URL of the END-USER frontend, so the emailed link lands on the user's app. */\n frontendUrl: string;\n /**\n * Transport metadata, optional: the resource-operation entry point receives an\n * `ExecutionContext`, which carries identity but no request transport detail.\n */\n ipAddress?: string;\n userAgent?: string;\n}\n\n/**\n * Lifetime of a password-reset token, in hours.\n *\n * A framework policy constant rather than a per-call argument on either entry point. The\n * administrator-forced path originally declared a caller-supplied `resetExpirationHours`\n * (1–168); an operator able to widen the window could leave a week-long credential-recovery\n * path parked on an account, which is a persistence mechanism rather than a recovery one.\n */\nconst PASSWORD_RESET_TOKEN_TTL_HOURS = 1;\n\n/**\n * Handles password reset request and completion.\n * Extracted from AuthenticationControllerBackendService.\n */\n@injectable()\nexport class AuthPasswordResetBackendService {\n private readonly logDebug: (message: string, context?: Record<string, unknown>) => void;\n\n constructor(\n @inject(SAAS_SERVICE_TYPES.Logger) private readonly logger: LoggerService,\n @inject(SAAS_SERVICE_TYPES.ErrorBuilderService) private readonly errorBuilder: ErrorBuilderBackendService,\n @inject(SAAS_SERVICE_TYPES.AppConfigService) private readonly appConfigService: AppConfigurationService,\n @inject(SAAS_SERVICE_TYPES.ConsumableTokenService) private readonly consumableTokenService: ConsumableTokenBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthCredentialVerifier) private readonly credentialVerifier: AuthCredentialVerifierBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthTokenIssuer) private readonly tokenIssuer: AuthTokenIssuerBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthUserLookupService) private readonly userLookup: AuthUserLookupBackendService,\n @inject(SAAS_SERVICE_TYPES.AuthResponseHelperService) private readonly authResponseHelper: AuthResponseHelperBackendService,\n @inject(SAAS_SERVICE_TYPES.EmailService) private readonly emailService: EmailBackendService,\n @inject(SAAS_SERVICE_TYPES.AuditLogsService) private readonly auditLogsService: AuditLogsBackendService,\n // Writes a person's own emails in their language (#1799 review M3).\n @inject(SAAS_SERVICE_TYPES.RecipientLanguageService) private readonly recipientLanguage: RecipientLanguageBackendService,\n ) {\n this.logDebug = this.logger.debugByType('AuthPasswordReset');\n this.logger.setDebugTypeLogging('AuthPasswordReset', false);\n }\n\n /**\n * Always returns a success response (anti-enumeration).\n * Creates token, builds URL, sends email, audits — only when user exists.\n */\n async requestPasswordReset(\n ec: ExecutionContext<any>,\n params: PasswordResetRequestParams,\n ): Promise<{ messageRef: string; message: string }> {\n const { email, frontendUrl, ipAddress, userAgent } = params;\n\n this.logDebug('Password reset requested', { email });\n\n const successResponse = this.authResponseHelper.getAuthResponse(AuthResponseMessageRef.PASSWORD_RESET_REQUEST_SUCCESS);\n\n try {\n const user = await this.userLookup.findAnyUserByEmail(ec, email);\n\n if (!user) {\n this.logDebug('Password reset: user not found, returning success to prevent enumeration', { email });\n this.auditLogsService.logUserPasswordResetRequested({\n email, ipAddress, userAgent,\n }).catch(() => {});\n return successResponse;\n }\n\n // F9 fix: Don't send reset email to suspended/deactivated users.\n // Return generic success to maintain anti-enumeration.\n if (user.status !== UserStatus.ACTIVE && user.status !== UserStatus.PENDING_VERIFICATION) {\n this.logDebug('Password reset: user not in active status, returning success to prevent enumeration', {\n email, status: user.status,\n });\n return successResponse;\n }\n\n // #1156: no reset link for a password the effective policy disables. It could not be completed, and completing it\n // would re-create the credential AUTH-24 cleared. The same success response as every other no-send branch, so the\n // answer says nothing about the account.\n // An account whose user type cannot be read has no policy to answer from, so it is refused like every other\n // credential-establishing path refuses it, silently here.\n const userType = user.userTypes?.[0];\n const policy = userType ? await this.userLookup.resolveEffectiveAuthPolicy(ec, user._id, userType) : null;\n if (!userType || !isCredentialMethodEnabledByPolicy(policy, AuthMethod.PASSWORD)) {\n this.logDebug('Password reset: password not enabled for this account, no email sent', { userId: user._id });\n return successResponse;\n }\n\n await this._mintResetTokenAndDispatchEmail(ec, user, frontendUrl);\n\n this.auditLogsService.logUserPasswordResetRequested({\n email, ipAddress, userAgent,\n }).catch(() => {});\n } catch (error) {\n this.logger.error('Unexpected error during password reset request', {\n ...projectCaughtErrorLogFields(error),\n });\n }\n\n return successResponse;\n }\n\n /**\n * Mint a single-use PASSWORD_RESET token for `user` and dispatch the system reset email.\n *\n * The one place a reset token is minted and mailed. Both entry points share it — the\n * self-service `requestPasswordReset` and the administrator-compelled\n * `forcePasswordResetByAdministrator` — so the TTL, the link shape, the template, and the\n * locale-resolution fallback chain cannot drift between them. Only the surrounding policy\n * differs (anti-enumeration and audit event on one side; authorization, session revocation\n * and actor-attributed audit on the other).\n *\n * Email dispatch failure is logged, never thrown: the token is already valid, and failing the\n * caller would imply nothing happened. The reset remains completable through a resend.\n */\n private async _mintResetTokenAndDispatchEmail(\n ec: ExecutionContext<any>,\n user: { _id: string; email: string },\n frontendUrl: string,\n ): Promise<{ expiresAt: Date }> {\n const resetToken = await this._mintResetToken(user);\n await this._dispatchResetEmail(user, frontendUrl, resetToken);\n return { expiresAt: resetToken.expiresAt };\n }\n\n /**\n * The reset token itself, written through `transaction` when one is given (#2297): a forced reset mints it inside the\n * operation's transaction, so a rolled-back attempt leaves no live link behind.\n */\n private async _mintResetToken(\n user: { _id: string; email: string },\n transaction?: TransactionContext,\n ): Promise<{ _id: unknown; token: string; expiresAt: Date }> {\n return this.consumableTokenService.createToken({\n tokenType: CoreConsumableTokenTypes.PASSWORD_RESET,\n consumptionMode: ConsumableToken_ConsumptionMode.SINGLE_USE,\n expiresIn: { value: PASSWORD_RESET_TOKEN_TTL_HOURS, unit: DurationUnit.HOURS },\n userId: user._id,\n resourceIdentifier: CoreResourceType.USERS,\n relatedId: user._id,\n metadata: { email: user.email },\n }, transaction);\n }\n\n /**\n * Mail the reset link carrying `resetToken`'s RAW value. Never captured into an outbox (#2297): the value exists only\n * in memory, and keeping it in a stored row would put a live credential at rest. Failure is logged, never thrown (see\n * {@link _mintResetTokenAndDispatchEmail}).\n */\n private async _dispatchResetEmail(\n user: { _id: string; email: string },\n frontendUrl: string,\n resetToken: { _id: unknown; token: string },\n ): Promise<void> {\n const resetUrl = `${frontendUrl}/password-lost?token=${encodeURIComponent(resetToken.token)}`;\n\n try {\n const appIdentity = this.appConfigService.config.appIdentity;\n\n const emailResult = await this.emailService.send({\n submissionId: String(resetToken._id),\n templateRef: SystemEmailTemplateRef.PASSWORD_RESET,\n // The same choice every per-person email makes (#1799). Read through the internal door: the person\n // asking for a reset is not signed in, so this request's context could not read their preferences.\n locale: await this.recipientLanguage.forUser(user._id),\n type: EmailTemplateType.TRANSACTIONAL,\n to: user.email,\n context: {\n currentObject: { email: user.email, userId: user._id },\n objectContext: {\n primaryScopeContext: {\n appName: applicationDisplayName(appIdentity),\n applicationId: this.appConfigService.applicationId,\n logoUrl: this.appConfigService.resolveBrandLogoAbsoluteUrl({\n composition: BrandAssetComposition.LOCKUP,\n purpose: BrandAssetPurpose.EMAIL,\n }) ?? undefined,\n },\n },\n userContext: { userId: user._id, email: user.email },\n additionalContext: {\n resetUrl,\n tokenValue: resetToken.token,\n expiresInMinutes: PASSWORD_RESET_TOKEN_TTL_HOURS * 60,\n },\n },\n });\n if (\n !emailResult.success\n || (emailResult.outcome !== EmailSubmissionOutcome.PROVIDER_ACCEPTED\n && emailResult.outcome !== EmailSubmissionOutcome.LOCAL_PREVIEW)\n ) {\n this.logger.error('Password reset email provider did not confirm acceptance', {\n userId: user._id,\n submissionId: emailResult.submissionId,\n outcome: emailResult.outcome,\n error: emailResult.error,\n });\n } else {\n this.logDebug('Password reset email submission completed', {\n outcome: emailResult.outcome,\n userId: user._id,\n submissionId: emailResult.submissionId,\n providerMessageIds: emailResult.providerMessageIds,\n });\n }\n } catch (emailError) {\n this.logger.error('Failed to send password reset email', {\n userId: user._id,\n ...projectCaughtErrorLogFields(emailError),\n });\n }\n }\n\n /**\n * ADMINISTRATOR-compelled credential reset — the privileged twin of\n * {@link requestPasswordReset}.\n *\n * Backs the `users | force_password_reset` resource operation. Four deliberate differences\n * from the self-service path:\n *\n * 1. **No anti-enumeration.** The target is addressed by id by a caller already authorized to\n * read the row, so a missing user is a genuine `404` rather than a fabricated success.\n * 2. **The reset link is NEVER returned to the caller.** It is mailed to the account owner and\n * nothing else. Handing an administrator a link that sets someone's password would let them\n * then authenticate AS that person, unnoticed and — since no actor claim exists on a session\n * JWT — indistinguishable downstream from the real user. That is an account-takeover\n * primitive, not a support tool, which is why the operation's response carries only\n * `expiresAt`.\n * 3. **Active sessions are revoked.** The verb exists for suspected compromise; leaving the\n * attacker's live session working until the victim happens to complete the reset would make\n * the response protective in name only. Revocation happens BEFORE the token is minted so a\n * failure there aborts the whole operation rather than leaving a half-done reset.\n * 4. **The audit event names the actor.** `USER_PASSWORD_RESET_FORCED`, not the self-service\n * event, which has no actor field and would credit the account owner with the admin's act.\n *\n * The TTL is the framework's policy constant, deliberately not a caller-supplied value: an\n * operator who could widen the window could park a long-lived credential-recovery path on an\n * account.\n *\n * assurance-control: WILDO.IDENTITY.CREDENTIAL_LIFECYCLE — privileged credential\n * intervention is authorized, attributable, justified, and evicts existing access.\n */\n async forcePasswordResetByAdministrator(\n ec: ExecutionContext<any>,\n params: AdministratorForcedPasswordResetParams,\n ): Promise<{ expiresAt: Date; sessionsRevoked: boolean }> {\n const user = await this.revokeAccessForForcedPasswordReset(ec, { targetUserId: params.targetUserId });\n const { expiresAt } = await this.issueForcedPasswordReset(ec, { ...params, user });\n return { expiresAt, sessionsRevoked: true };\n }\n\n /**\n * The first half of {@link forcePasswordResetByAdministrator} (#2297): resolve the account, refuse what may not be reset,\n * and evict every live session. Runs OUTSIDE any database transaction, BEFORE the token is minted, exactly as the whole\n * reset used to: a refusal or a failed revocation stops the reset with no link minted, and the revocation (Redis) is an\n * effect a rolled-back attempt could not undo, so it never runs inside one. The `users | force_password_reset`\n * operation runs it in its `beforeTransactionalCore` phase.\n *\n * Answers the account the second half mints for.\n */\n async revokeAccessForForcedPasswordReset(\n ec: ExecutionContext<any>,\n params: { targetUserId: string },\n ): Promise<{ _id: string; email: string }> {\n const { targetUserId } = params;\n\n const user = await this.userLookup.findUserById(ec, targetUserId);\n if (!user) {\n throw this.errorBuilder.buildError(ErrorType.NOT_FOUND, ec, {\n customMessageReference: ErrorCustomMessageReference.NOT_FOUND,\n context: { reason: 'User not found for forced password reset', targetUserId },\n });\n }\n\n // A soft-deleted identity has no credential path worth resetting, and mailing a reset link\n // to a closed account is its own disclosure. ACTIVE is not required: forcing a reset on a\n // SUSPENDED or PENDING_VERIFICATION account is a legitimate recovery action.\n if (user.status === UserStatus.DELETED) {\n throw this.errorBuilder.buildError(ErrorType.CONFLICT, ec, {\n customMessageReference: ErrorCustomMessageReference.CONFLICT,\n context: { reason: 'Cannot force a password reset on a deleted account', targetUserId },\n });\n }\n\n // #1156: refused before any side effect. The link could never be completed for a password the effective policy\n // disables, and the sessions would already be gone: the credential belongs to the directory.\n await this.userLookup.assertCredentialMethodEnabledForUser(ec, user._id, AuthMethod.PASSWORD);\n\n await this.tokenIssuer.invalidateAllTokensForUser(user._id);\n return { _id: String(user._id), email: user.email };\n }\n\n /**\n * The second half of {@link forcePasswordResetByAdministrator} (#2297): mint the single-use reset token for `user`, whose\n * sessions {@link revokeAccessForForcedPasswordReset} already evicted, then mail the link and record who forced it.\n *\n * With a transaction in `serviceOptions` the token is written THROUGH it, and the two effects outside the database run\n * after the commit: a rolled-back attempt leaves no live link and sends no email, and a retried attempt mints once per\n * attempt that commits. The record (`USER_PASSWORD_RESET_FORCED`) is a capturable recording, so an attempt the framework\n * captures keeps it in the post-commit effect outbox and a crash between commit and drain loses no record. The email is\n * deliberately NOT captured: it carries the token's raw value, which must never be stored (a lost email is recovered by\n * forcing the reset again, which revokes the stale link's sessions and mints a fresh one).\n *\n * Decided by this repository's principles, not by asking: the record used to be awaited before the operation answered.\n * `_createAuditLog` already isolates a store failure, so it never failed the operation; a record kept with the commit and\n * written once afterwards is the stronger guarantee (the transactional-outbox shape #2297 settled for every audited write).\n * Without a transaction both effects run inline, as before.\n */\n async issueForcedPasswordReset(\n ec: ExecutionContext<any>,\n params: Omit<AdministratorForcedPasswordResetParams, 'targetUserId'> & { user: { _id: string; email: string } },\n serviceOptions?: ServiceOptions,\n ): Promise<{ expiresAt: Date }> {\n const { user, actorUserId, reason, frontendUrl, ipAddress, userAgent } = params;\n const transaction = serviceOptions?.transaction;\n\n const resetToken = await this._mintResetToken(user, transaction);\n const sendEmail = () => this._dispatchResetEmail(user, frontendUrl, resetToken);\n if (transaction) {\n registerTransactionPostCommitEffect(transaction, {\n key: `forced-password-reset-email:${user._id}:${transaction.id}`,\n execute: sendEmail,\n });\n } else {\n await sendEmail();\n }\n\n await emitAuthorityChangeAuditWhenDurable({\n serviceOptions,\n effectKey: `forced-password-reset-record:${user._id}`,\n logger: this.logger,\n failureContext: { targetUserId: user._id, actorUserId, eventType: 'USER_PASSWORD_RESET_FORCED' },\n recording: {\n auditLogs: () => this.auditLogsService,\n write: (auditLogs) => auditLogs.logUserPasswordResetForced({\n targetUserId: user._id,\n targetEmail: user.email,\n actorUserId,\n reason,\n sessionsRevoked: true,\n ipAddress,\n userAgent,\n }, ec),\n subjectId: user._id,\n },\n });\n\n return { expiresAt: resetToken.expiresAt };\n }\n\n /**\n * Consumes token, validates policy, hashes, updates creds, revokes tokens, audits.\n */\n async completePasswordReset(\n ec: ExecutionContext<any>,\n params: PasswordResetCompleteParams,\n ): Promise<{ messageRef: string; message: string }> {\n const { token, newPassword, ipAddress, userAgent } = params;\n\n this.logDebug('Password reset completion attempted');\n\n let consumedToken;\n try {\n consumedToken = await this.consumableTokenService.consumeToken(token);\n } catch {\n throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n });\n }\n\n if (consumedToken.tokenType !== CoreConsumableTokenTypes.PASSWORD_RESET) {\n throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n });\n }\n\n const userId = consumedToken.userId || consumedToken.relatedId;\n if (!userId) {\n throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n });\n }\n\n // #1156: a link minted before the block took effect must not re-create the password, and a subject whose user or\n // user type cannot be read is refused rather than written. The token is already spent, which is right: it could never\n // be completed.\n const { policy: userTypePolicy, userEmail } = await this.userLookup.assertCredentialMethodEnabledForUser(ec, userId, AuthMethod.PASSWORD);\n const passwordPolicy = userTypePolicy?.passwordPolicy;\n\n const policyValidation = this.credentialVerifier.validatePasswordPolicy(newPassword, passwordPolicy, { email: userEmail });\n if (!policyValidation.valid) {\n throw this.errorBuilder.buildError(ErrorType.VALIDATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.VALIDATION_PASSWORD_POLICY_VIOLATION,\n context: { violations: policyValidation.violations, minLength: passwordPolicy?.minLength, maxLength: passwordPolicy?.maxLength },\n });\n }\n\n const newPasswordHash = await this.credentialVerifier.hashPassword(newPassword, this.appConfigService.config.auth.passwordHashing);\n await this.tokenIssuer.invalidateAllTokensForUser(userId);\n await this.credentialVerifier.updatePasswordHash(ec, userId, newPasswordHash);\n await this.consumableTokenService.revokeTokensByRelatedId(userId, CoreConsumableTokenTypes.PASSWORD_RESET);\n\n this.auditLogsService.logUserPasswordResetCompleted({\n userId, ipAddress, userAgent,\n }, ec).catch(() => {});\n\n /*\n * Tell the account holder their password was changed through recovery. A reset is the path a\n * mailbox takeover uses, so this goes to the same address the link did and is a signal for the owner\n * rather than a defence against that case — it is the notice for every other one: a reset they did\n * not start, from a device they do not recognise. Best-effort, after the new password is stored.\n */\n await sendSecurityAlert(\n { emailService: this.emailService, appConfigService: this.appConfigService, logger: this.logger, recipientLanguage: this.recipientLanguage },\n { userId, email: userEmail, alertType: SecurityAlertType.PASSWORD_CHANGED, ipAddress },\n );\n\n this.logDebug('Password reset completed successfully', { userId });\n\n return this.authResponseHelper.getAuthResponse(AuthResponseMessageRef.PASSWORD_RESET_COMPLETE_SUCCESS);\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"siem-delivery-dlq.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../src/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAmBH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAmMpH,wBAAgB,iCAAiC,IAAI,0CAA0C,EAAE,CAgGhG;AAMD,wBAAgB,8BAA8B,IAAI,0CAA0C,EAAE,CAsG7F;AAMD,wBAAgB,8BAA8B,IAAI,0CAA0C,EAAE,
|
|
1
|
+
{"version":3,"file":"siem-delivery-dlq.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../src/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAmBH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAmMpH,wBAAgB,iCAAiC,IAAI,0CAA0C,EAAE,CAgGhG;AAMD,wBAAgB,8BAA8B,IAAI,0CAA0C,EAAE,CAsG7F;AAMD,wBAAgB,8BAA8B,IAAI,0CAA0C,EAAE,CA6C7F"}
|
|
@@ -387,8 +387,11 @@ export function createSiemDlqPurgeAllFactories() {
|
|
|
387
387
|
isOperationDefault: true,
|
|
388
388
|
},
|
|
389
389
|
requestDtoSchema: SiemDeliveryDlq_PurgeAllRequestDto,
|
|
390
|
-
// #2297: the delete is the whole operation and it goes through utils.serviceOptions. It
|
|
391
|
-
//
|
|
390
|
+
// #2297: the delete is the whole operation and it goes through utils.serviceOptions. It stays best effort as an
|
|
391
|
+
// unbounded multi-row write: the HOOK selects the whole scope, so the framework's per-transaction chunking (wave 4,
|
|
392
|
+
// `MULTI_ROW_WRITE_ROWS_PER_TRANSACTION`) cannot reach it, and a `maximumRowsWrittenPerAttempt` would make "purge
|
|
393
|
+
// all" purge a page. Bounding it means changing what the operation answers (a partial purge reporting what
|
|
394
|
+
// remains, or a background job), not declaring a number here.
|
|
392
395
|
hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,
|
|
393
396
|
handlers: {
|
|
394
397
|
replaceCoreOperations: async (_id, _input, executionContext, _operationPath, utils) => {
|