Skip to content

Commit 161fbb6

Browse files
vitorbariclaude
andauthored
feat(api): add back action kind, reject on inbound definitions (#379)
## Summary Adds `back` to the action `kind` enum on `step-action.yaml` so the engine can emit it on rendered step responses for back-navigation (ADR 022). The domain validator rejects any declared action with `kind: back`, since flow authors must never declare it themselves. Contract + validator only — no engine behavior changes. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 64fd731 commit 161fbb6

8 files changed

Lines changed: 64 additions & 10 deletions

File tree

api/generated/oas_json_gen.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/generated/oas_schemas_gen.go

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/generated/oas_validators_gen.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/openapi/components/flows/step-action.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ properties:
1111
example: submit
1212
kind:
1313
type: string
14-
enum: [submit, passkey, passkey_register, navigate]
14+
enum: [submit, passkey, passkey_register, navigate, back]
1515
example: submit
1616
description: |
1717
Classifies how the engine handles this action:
@@ -21,7 +21,9 @@ properties:
2121
- `passkey_register`: issue a WebAuthn registration challenge; the matching
2222
transition fires once the returned attestation verifies.
2323
- `navigate`: route through the transition without running the input
24-
pipeline. Used for back-navigation and similar pure-routing actions.
24+
pipeline. Used for pure-routing actions declared in the flow definition.
25+
- `back`: return the user to the previous step. Surfaced by the engine
26+
when going back is available.
2527
primary:
2628
type: boolean
2729
default: false

internal/domain/flow_definition.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,14 @@ const (
9696
// verifies.
9797
FlowActionKindPasskeyRegister
9898
// FlowActionKindNavigate routes through the transition without running
99-
// the input pipeline. Used for back-navigation and similar pure-routing
100-
// actions where the submitted fields are irrelevant.
99+
// the input pipeline. Used for pure-routing actions declared in the flow
100+
// definition where the submitted fields are irrelevant.
101101
FlowActionKindNavigate
102+
// FlowActionKindBack pops the previous step from runtime history and
103+
// re-renders it. Injected by the engine on rendered step responses when
104+
// history is non-empty and the current step is non-terminal; rejected by
105+
// the validator on inbound flow definitions — flow authors never declare it.
106+
FlowActionKindBack
102107
)
103108

104109
// FlowDefinition is a customer-configured directed graph of authentication steps.

internal/domain/flow_definition_validator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ func validateSteps(steps []FlowDefinitionStep, userSchema *jsonschema.Schema) er
126126
return ErrFlowDefinitionInvalid(fmt.Sprintf(
127127
"step %q: action %q has no kind", step.Name, a.Name), nil)
128128
}
129+
if a.Kind == FlowActionKindBack {
130+
return ErrFlowDefinitionInvalid(fmt.Sprintf(
131+
"step %q: action %q has kind=back, which is engine-injected and cannot be declared", step.Name, a.Name), nil)
132+
}
129133
actionNames[a.Name] = struct{}{}
130134
}
131135

internal/domain/flow_definition_validator_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,3 +1247,27 @@ func TestValidator_MissingActionKindRejected(t *testing.T) {
12471247
require.Error(t, err)
12481248
assert.Contains(t, errorDetails(t, err), `action "submit" has no kind`)
12491249
}
1250+
1251+
// TestValidator_DeclaredBackKindRejected guards against authors declaring
1252+
// `kind: back` directly on a flow definition. The engine injects back on
1253+
// rendered responses; declaring it on the definition would let an author
1254+
// override the engine's reversibility rules and is not permitted.
1255+
func TestValidator_DeclaredBackKindRejected(t *testing.T) {
1256+
schema := mustSchema(t, userSchemaIDAndPassword)
1257+
def := domain.FlowDefinition{
1258+
ProjectID: "p", Name: "f", SchemaVersion: "1",
1259+
UserSchema: "https://tenant.com/schemas/idpw-user.json",
1260+
Purposes: map[domain.FlowDefinitionPurpose]string{domain.FlowDefinitionPurposeLogin: "step"},
1261+
Steps: []domain.FlowDefinitionStep{
1262+
{
1263+
Name: "step", Fields: []string{"email"},
1264+
Actions: []domain.FlowStepAction{{Name: "back", Kind: domain.FlowActionKindBack}},
1265+
Transitions: map[string]domain.FlowStepTransition{"back": {Target: "done"}},
1266+
},
1267+
{Name: "done", Complete: gu.Ptr(domain.FlowStepCompleteShow)},
1268+
},
1269+
}
1270+
_, err := domain.ValidateFlowDefinition(schema, def)
1271+
require.Error(t, err)
1272+
assert.Contains(t, errorDetails(t, err), `action "back" has kind=back, which is engine-injected and cannot be declared`)
1273+
}

internal/domain/flowactionkind_enumer.go

Lines changed: 8 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)