diff --git a/packages/errors/__tests__/parse.test.ts b/packages/errors/__tests__/parse.test.ts index da301fd44e..7af662c9dc 100644 --- a/packages/errors/__tests__/parse.test.ts +++ b/packages/errors/__tests__/parse.test.ts @@ -226,11 +226,10 @@ describe('generated registry (full constructive-db audit)', () => { // require_step_up() raises one code per factor so the client knows which // re-verification to prompt for; a humanized code would prompt for the // wrong one, so each needs its own copy. - // STEP_UP_REQUIRED_PASSWORD_OR_MFA is deliberately absent: constructive-db - // split it into the per-factor codes below (require_step_up.sql). const factors = [ 'STEP_UP_REQUIRED_PASSWORD', 'STEP_UP_REQUIRED_MFA', + 'STEP_UP_REQUIRED_PASSWORD_OR_MFA', 'STEP_UP_REQUIRED_FRESH_AUTH' ]; for (const code of factors) { @@ -238,8 +237,8 @@ describe('generated registry (full constructive-db audit)', () => { expect(generatedRegistry[code].http).toBe(403); expect(format(code)).not.toMatch(/^Step up required/); } - expect(format('STEP_UP_REQUIRED_MFA')).not.toBe( - format('STEP_UP_REQUIRED_PASSWORD') + expect(new Set(factors.map((code) => format(code))).size).toBe( + factors.length ); // An unrecognized posture fails closed rather than passing the mutation. expect(classify('STEP_UP_INVALID_TYPE')).toBe('public'); diff --git a/packages/errors/scripts/db-error-inventory.json b/packages/errors/scripts/db-error-inventory.json index cc8a2ee768..52ac883359 100644 --- a/packages/errors/scripts/db-error-inventory.json +++ b/packages/errors/scripts/db-error-inventory.json @@ -5825,6 +5825,13 @@ "n_generated": 3, "class": "public" }, + "STEP_UP_REQUIRED_PASSWORD_OR_MFA": { + "count": 2, + "dynamic": false, + "sample": "STEP_UP_REQUIRED_PASSWORD_OR_MFA", + "n_source": 1, + "n_generated": 1 + }, "STORAGE_API_NOT_PROVISIONED": { "count": 2, "dynamic": false, diff --git a/packages/errors/scripts/generate-registry.py b/packages/errors/scripts/generate-registry.py index 59ad7b84ef..4f136e1eb7 100644 --- a/packages/errors/scripts/generate-registry.py +++ b/packages/errors/scripts/generate-registry.py @@ -74,7 +74,8 @@ def add_copy(code, msg): 'STEP_UP_REQUIRED_FRESH_AUTH': 'Please verify your identity to continue.', 'STEP_UP_REQUIRED_PASSWORD_OR_MFA': - 'Please verify your identity to continue.', + 'Please re-enter your password or enter a code from your ' + 'authenticator app to continue.', 'STEP_UP_INVALID_TYPE': 'This action requires verification that is not configured correctly. ' 'Please contact support.', diff --git a/packages/errors/src/generated/registry.generated.ts b/packages/errors/src/generated/registry.generated.ts index babfb07571..063e20a511 100644 --- a/packages/errors/src/generated/registry.generated.ts +++ b/packages/errors/src/generated/registry.generated.ts @@ -2,7 +2,7 @@ /** * GENERATED FILE — DO NOT EDIT BY HAND. * - * Source of truth: the constructive-db error audit (817 distinct codes + * Source of truth: the constructive-db error audit (818 distinct codes * raised via EXCEPTION/THROW across deploy sources + generated output). * Regenerate with `python3 scripts/generate-registry.py` (see README.md). * @@ -10,7 +10,7 @@ * codes carry their raw message (with %-args rendered as {{argN}}). Curated * entries in `registry.ts` override anything here (typed context + refined copy). * - * Counts: 817 total, 598 public, 219 internal. + * Counts: 818 total, 599 public, 219 internal. */ import { defineError, type DefinedError } from '../define'; import type { ErrorContext } from '../types'; @@ -763,6 +763,7 @@ export const generatedRegistry: Record> = { 'STEP_UP_REQUIRED_FRESH_AUTH': defineError({ code: 'STEP_UP_REQUIRED_FRESH_AUTH', class: 'public', http: 403, message: 'Please verify your identity to continue.' }), 'STEP_UP_REQUIRED_MFA': defineError({ code: 'STEP_UP_REQUIRED_MFA', class: 'public', http: 403, message: 'Please enter a code from your authenticator app to continue.' }), 'STEP_UP_REQUIRED_PASSWORD': defineError({ code: 'STEP_UP_REQUIRED_PASSWORD', class: 'public', http: 403, message: 'Please re-enter your password to continue.' }), + 'STEP_UP_REQUIRED_PASSWORD_OR_MFA': defineError({ code: 'STEP_UP_REQUIRED_PASSWORD_OR_MFA', class: 'public', http: 403, message: 'Please re-enter your password or enter a code from your authenticator app to continue.' }), 'STORAGE_API_NOT_PROVISIONED': defineError({ code: 'STORAGE_API_NOT_PROVISIONED', class: 'public', http: 400, message: 'Storage api not provisioned.' }), 'STORAGE_DESTINATION_REQUIRES_TEMP': defineError({ code: 'STORAGE_DESTINATION_REQUIRES_TEMP', class: 'internal', http: 500, message: 'Storage destination requires temp.' }), 'STORAGE_FILE_BUCKET_IMMUTABLE': defineError({ code: 'STORAGE_FILE_BUCKET_IMMUTABLE', class: 'internal', http: 500, message: 'Storage file bucket immutable.' }), @@ -1584,6 +1585,7 @@ export const GENERATED_CODE_META: Record = { 'STEP_UP_REQUIRED_FRESH_AUTH': { class: 'public', dynamic: false, generatedOnly: false }, 'STEP_UP_REQUIRED_MFA': { class: 'public', dynamic: false, generatedOnly: false }, 'STEP_UP_REQUIRED_PASSWORD': { class: 'public', dynamic: false, generatedOnly: false }, + 'STEP_UP_REQUIRED_PASSWORD_OR_MFA': { class: 'public', dynamic: false, generatedOnly: false }, 'STORAGE_API_NOT_PROVISIONED': { class: 'public', dynamic: false, generatedOnly: false }, 'STORAGE_DESTINATION_REQUIRES_TEMP': { class: 'internal', dynamic: false, generatedOnly: false }, 'STORAGE_FILE_BUCKET_IMMUTABLE': { class: 'internal', dynamic: false, generatedOnly: false }, @@ -1665,4 +1667,4 @@ export const GENERATED_CODE_META: Record = { }; /** Total number of codes collected from constructive-db. */ -export const GENERATED_CODE_COUNT = 817; +export const GENERATED_CODE_COUNT = 818; diff --git a/packages/node-type-registry/src/blueprint-types.generated.ts b/packages/node-type-registry/src/blueprint-types.generated.ts index 39b8908023..ff5a878666 100644 --- a/packages/node-type-registry/src/blueprint-types.generated.ts +++ b/packages/node-type-registry/src/blueprint-types.generated.ts @@ -349,7 +349,7 @@ export interface DataLockParams { /* How a guarded verb is stopped while locked. step_up requires recent strong verification (needs a provisioned user_auth_module); block refuses the verb outright with ROW_LOCKED until unlocked. */ enforcement?: 'step_up' | 'block'; /* Verification method satisfying the step-up requirement, for the guarded verbs in step_up mode and for clearing the lock */ - step_up_type?: 'password' | 'mfa' | 'fresh_auth'; + step_up_type?: 'password' | 'mfa' | 'fresh_auth' | 'password_or_mfa'; /* Require step-up to change the lock column itself, so a locked row cannot be quietly unlocked and then deleted. Redundant (and therefore skipped) in step_up mode when UPDATE is already guarded. */ guard_unlock?: boolean; /* For a guarded UPDATE, restrict the guard to changes touching these columns. Empty guards the whole row. */ @@ -565,7 +565,7 @@ export interface EventTrackerParams { ; /** Attaches a BEFORE trigger that calls require_step_up() to enforce recent strong verification (password, MFA, or identity-provider assertion) before allowing mutations. Requires a provisioned sessions_module (with app_settings_auth) for the target database. The step_up_window is read from app_settings_auth at runtime (default 30 minutes). Supports compound conditions (AND/OR/NOT), watch_fields (fire only when specific fields change), and simple condition_field/condition_value leaf conditions. */ export interface GuardStepUpParams { - /* Which verification method satisfies the step-up requirement (password_or_mfa is the legacy spelling of fresh_auth) */ + /* Which recent proof satisfies the step-up requirement: password (password re-entry), mfa (second factor), password_or_mfa (either factor; an SSO sign-in alone does not count), or fresh_auth (any recent sign-in or re-verification, including SSO, magic link and OTP) */ step_up_type?: 'password' | 'mfa' | 'fresh_auth' | 'password_or_mfa'; /* Which DML events require step-up verification */ events?: ('INSERT' | 'UPDATE' | 'DELETE')[]; diff --git a/packages/node-type-registry/src/data/data-lock.ts b/packages/node-type-registry/src/data/data-lock.ts index f9530682b4..8d18a1c9d4 100644 --- a/packages/node-type-registry/src/data/data-lock.ts +++ b/packages/node-type-registry/src/data/data-lock.ts @@ -45,7 +45,7 @@ export const DataLock: NodeTypeDefinition = { }, step_up_type: { type: 'string', - enum: ['password', 'mfa', 'fresh_auth'], + enum: ['password', 'mfa', 'fresh_auth', 'password_or_mfa'], description: 'Verification method satisfying the step-up requirement, for the ' + 'guarded verbs in step_up mode and for clearing the lock', diff --git a/packages/node-type-registry/src/guard/step-up.ts b/packages/node-type-registry/src/guard/step-up.ts index 5e4d9fa71b..b21d4bbdcf 100644 --- a/packages/node-type-registry/src/guard/step-up.ts +++ b/packages/node-type-registry/src/guard/step-up.ts @@ -22,8 +22,10 @@ export const GuardStepUp: NodeTypeDefinition = { type: 'string', enum: ['password', 'mfa', 'fresh_auth', 'password_or_mfa'], description: - 'Which verification method satisfies the step-up requirement ' + - '(password_or_mfa is the legacy spelling of fresh_auth)', + 'Which recent proof satisfies the step-up requirement: password ' + + '(password re-entry), mfa (second factor), password_or_mfa (either ' + + 'factor; an SSO sign-in alone does not count), or fresh_auth (any ' + + 'recent sign-in or re-verification, including SSO, magic link and OTP)', default: 'fresh_auth', }, events: {