feat: enforce WP6-03 external service hard stops
Dada P0-A isolated Windows CI / validate-and-package (push) Successful in 9m34s
Dada P0-A isolated Windows CI / validate-and-package (push) Successful in 9m34s
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
serializeAuditSummary,
|
||||
} from "./audit-policy.js";
|
||||
import type { ResendAdapter } from "./resend-adapter.js";
|
||||
import { ExternalServiceUsage } from "./external-service-usage.js";
|
||||
import {
|
||||
RegistrationError,
|
||||
type RegistrationErrorReason,
|
||||
@@ -226,6 +227,7 @@ function constantTimeTextEqual(left: string, right: string) {
|
||||
|
||||
export class RegistrationService {
|
||||
readonly database: BetterSqlite3.Database;
|
||||
readonly serviceUsage: ExternalServiceUsage;
|
||||
readonly options: Required<Pick<RegistrationServiceOptions, "clock" | "codeGenerator" | "inviteCodeGenerator">> & RegistrationServiceOptions;
|
||||
private adminAllowlistHashes = new Set<string>();
|
||||
private privacyPurgeActive = false;
|
||||
@@ -254,6 +256,7 @@ export class RegistrationService {
|
||||
this.database.function("dada_allow_retention_purge", { deterministic: false }, () => 0);
|
||||
this.database.function("dada_retention_purge_now", { deterministic: false }, () => 0);
|
||||
this.migrate();
|
||||
this.serviceUsage = new ExternalServiceUsage({ database: this.database, clock: this.options.clock });
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -299,6 +302,7 @@ export class RegistrationService {
|
||||
if (existing) throw new RegistrationError("AUTH_ENTRY_REJECTED", "registration_login_required");
|
||||
this.assertChallengeSendAllowed(email, "register", "registration", now);
|
||||
this.recordRateSend(email, "registration", now);
|
||||
this.serviceUsage.claimResendWithinTransaction(now);
|
||||
|
||||
this.database.prepare(`
|
||||
INSERT INTO email_challenges (
|
||||
@@ -328,6 +332,7 @@ export class RegistrationService {
|
||||
try {
|
||||
await this.options.resend.sendVerificationCode({ challengeId, code, email, purpose: "register" });
|
||||
} catch {
|
||||
this.serviceUsage.markProviderFailure({ serviceId: "resend_email", reason: "provider_unavailable", now });
|
||||
this.runImmediate("registration_send_compensation", () => {
|
||||
this.database.prepare("DELETE FROM email_challenges WHERE challenge_id = ? AND consumed_at IS NULL").run(challengeId);
|
||||
return { outcome: "committed", value: undefined };
|
||||
@@ -355,6 +360,7 @@ export class RegistrationService {
|
||||
if (user.role !== "user") throw new RegistrationError("AUTH_ENTRY_REJECTED", "login_admin_required");
|
||||
this.assertChallengeSendAllowed(email, "login", clientKey, now);
|
||||
this.recordRateSend(email, clientKey, now);
|
||||
this.serviceUsage.claimResendWithinTransaction(now);
|
||||
this.database.prepare(`
|
||||
INSERT INTO email_challenges (
|
||||
challenge_id, email, invite_id, code_hmac, purpose, expires_at,
|
||||
@@ -382,6 +388,7 @@ export class RegistrationService {
|
||||
try {
|
||||
await this.options.resend.sendVerificationCode({ challengeId, code, email, purpose: "login" });
|
||||
} catch {
|
||||
this.serviceUsage.markProviderFailure({ serviceId: "resend_email", reason: "provider_unavailable", now });
|
||||
this.runImmediate("registration_send_compensation", () => {
|
||||
this.database.prepare("DELETE FROM email_challenges WHERE challenge_id = ? AND consumed_at IS NULL").run(challengeId);
|
||||
return { outcome: "committed", value: undefined };
|
||||
@@ -768,6 +775,7 @@ export class RegistrationService {
|
||||
}
|
||||
this.assertChallengeSendAllowed(email, "admin_login", clientKey, now);
|
||||
this.recordRateSend(email, clientKey, now);
|
||||
this.serviceUsage.claimResendWithinTransaction(now);
|
||||
this.database.prepare(`
|
||||
INSERT INTO email_challenges (
|
||||
challenge_id, email, invite_id, code_hmac, purpose, expires_at,
|
||||
@@ -795,6 +803,7 @@ export class RegistrationService {
|
||||
try {
|
||||
await this.options.resend.sendVerificationCode({ challengeId, code, email, purpose: "admin_login" });
|
||||
} catch {
|
||||
this.serviceUsage.markProviderFailure({ serviceId: "resend_email", reason: "provider_unavailable", now });
|
||||
this.runImmediate("registration_send_compensation", () => {
|
||||
this.database.prepare("DELETE FROM email_challenges WHERE challenge_id = ? AND consumed_at IS NULL").run(challengeId);
|
||||
this.recordAdminLoginRejection("service_unavailable", now);
|
||||
@@ -1097,6 +1106,7 @@ export class RegistrationService {
|
||||
now + resendDelayMilliseconds,
|
||||
now,
|
||||
);
|
||||
this.serviceUsage.claimResendWithinTransaction(now);
|
||||
return {
|
||||
outcome: "committed",
|
||||
value: {
|
||||
@@ -1116,6 +1126,7 @@ export class RegistrationService {
|
||||
purpose: "account_delete",
|
||||
});
|
||||
} catch {
|
||||
this.serviceUsage.markProviderFailure({ serviceId: "resend_email", reason: "provider_unavailable", now });
|
||||
this.runImmediate("registration_send_compensation", () => {
|
||||
this.database.prepare("DELETE FROM account_deletion_challenges WHERE deletion_id = ? AND consumed_at IS NULL").run(deletionId);
|
||||
return { outcome: "committed", value: undefined };
|
||||
|
||||
Reference in New Issue
Block a user