feat: enforce WP6-03 external service hard stops
Dada P0-A isolated Windows CI / validate-and-package (push) Successful in 9m34s

This commit is contained in:
suyx
2026-08-04 02:17:21 +08:00
parent 19212cc1b6
commit 70754ec12b
9 changed files with 1572 additions and 1 deletions
+11
View File
@@ -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 };