From 5aab2f9ccca8c1d81c2c33d1cdfff558411b9524 Mon Sep 17 00:00:00 2001 From: AJ Beck Date: Sun, 27 Sep 2026 15:28:11 +0100 Subject: [PATCH] refactor!: move action-header selection to the actionheader package Move edge.ActionHeader to the standard-library-only actionheader package as Selector, with NewSelector as its constructor. Native HTTP consumers no longer link the Lambda adapter and aws-lambda-go events just to select an action. Parsing behavior and sentinel meanings are unchanged; error strings now carry the actionheader prefix. BREAKING CHANGE: edge.ActionHeader, edge.NewActionHeader and edge.ErrAction* are now actionheader.Selector, actionheader.NewSelector and actionheader.ErrAction*. --- AGENTS.md | 4 +- README.md | 5 +- ...mple_test.go => action_composition_test.go | 44 ++--------- action_gateway_test.go | 18 +++-- action.go => actionheader/actionheader.go | 50 +++++++++---- .../actionheader_test.go | 74 +++++++++---------- actionheader/example_test.go | 59 +++++++++++++++ authn/authorization_test.go | 4 +- authn/example_test.go | 4 +- authn/gateway_test.go | 3 +- authz/example_test.go | 4 +- docs/authn.md | 2 +- docs/plan.md | 2 +- docs/support.md | 2 +- examples/dispatcher/internal/orders/app.go | 4 +- testdata/consumer/consumer_test.go.txt | 3 +- 16 files changed, 166 insertions(+), 116 deletions(-) rename action_example_test.go => action_composition_test.go (70%) rename action.go => actionheader/actionheader.go (60%) rename action_test.go => actionheader/actionheader_test.go (67%) create mode 100644 actionheader/example_test.go diff --git a/AGENTS.md b/AGENTS.md index 1564a27..e75d044 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,9 @@ Keep accepted direction, proposed API contracts and implemented behavior clearly ## Repository map -- Root Go files: raw/typed Lambda adapters, Gateway event decoding, HTTP request translation, buffered/streaming responses, gateway identity and action headers. +- Root Go files: raw/typed Lambda adapters, Gateway event decoding, HTTP request translation, buffered/streaming responses and gateway identity. +- `actionheader/`: strict single-value action selection from a request header; + standard library only. - `identity/`: immutable callers, owned claims and context transport; standard library only, with no AWS dependencies. - `authn/`: credential selection, HTTP authentication middleware, Cognito token verification and JWKS caching. diff --git a/README.md b/README.md index 94653c7..c9e744a 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,12 @@ no live API Gateway streaming test has run. ## Header-selected actions -`edge.NewActionHeader("Action")` creates an immutable selector whose `Parse(r.Header)` method returns an action string or an error. +`actionheader.NewSelector("Action")` creates an immutable selector whose `Parse(r.Header)` method returns an action string or an error. +The `actionheader` package uses only the standard library, so native HTTP servers do not link the Lambda adapter. Configure the header name explicitly. Selection preserves case, trims outer spaces/tabs, and requires a nonempty HTTP token. Repeated values and comma-bearing values are rejected, including Gateway payload 2.0's comma-combined duplicates. -Use `errors.Is` with `edge.ErrActionMissing`, `edge.ErrActionAmbiguous`, or `edge.ErrActionInvalid` to distinguish request-input failures. +Use `errors.Is` with `actionheader.ErrActionMissing`, `actionheader.ErrActionAmbiguous`, or `actionheader.ErrActionInvalid` to distinguish request-input failures. Use ordinary `http.Handler` middleware to process this and other custom headers. The consumer owns its action registry, metadata, authorization and HTTP error responses. diff --git a/action_example_test.go b/action_composition_test.go similarity index 70% rename from action_example_test.go rename to action_composition_test.go index 3e56ba4..e104903 100644 --- a/action_example_test.go +++ b/action_composition_test.go @@ -2,19 +2,19 @@ package edge_test import ( "context" - "fmt" "net/http" "net/http/httptest" "slices" "testing" "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" ) -// These are consumer-owned middleware and metadata, not edge API types. +// These are consumer-owned middleware and metadata, not edge or actionheader API types. type actionKey struct{} -func processActionHeaders(selector edge.ActionHeader, next http.Handler) http.Handler { +func processActionHeaders(selector actionheader.Selector, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { action, err := selector.Parse(r.Header) if err != nil { @@ -26,42 +26,8 @@ func processActionHeaders(selector edge.ActionHeader, next http.Handler) http.Ha }) } -func ExampleActionHeader() { - selector, err := edge.NewActionHeader("Action") - if err != nil { - fmt.Println(err) - return - } - dispatcher := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - action, ok := r.Context().Value(actionKey{}).(string) - if !ok { - http.Error(w, "missing action metadata", http.StatusInternalServerError) - return - } - // This example operation is public. - // Protected operations must authorize this same selection before executing or publishing streaming output. - switch action { - case "service.ping": - fmt.Fprintln(w, "pong") - default: - http.Error(w, "unknown action", http.StatusBadRequest) - } - }) - // The composed http.Handler also goes directly to edge.New. - handler := processActionHeaders(selector, dispatcher) - r := httptest.NewRequest(http.MethodPost, "/", nil) - r.Header.Set("Action", "service.ping") - w := httptest.NewRecorder() - handler.ServeHTTP(w, r) - fmt.Println(w.Code) - fmt.Print(w.Body.String()) - // Output: - // 200 - // pong -} - -func TestActionHeaderMiddlewareComposition(t *testing.T) { - selector, err := edge.NewActionHeader("Action") +func TestActionSelectorMiddlewareComposition(t *testing.T) { + selector, err := actionheader.NewSelector("Action") if err != nil { t.Fatal(err) } diff --git a/action_gateway_test.go b/action_gateway_test.go index a89a6c0..22bc0b8 100644 --- a/action_gateway_test.go +++ b/action_gateway_test.go @@ -8,10 +8,12 @@ import ( "net/http/httptest" "strings" "testing" + + "github.com/asteroid-computing/go-lambda-edge/actionheader" ) -func TestActionHeaderAcrossRequestFormats(t *testing.T) { - selector, err := NewActionHeader("Action") +func TestActionSelectorAcrossRequestFormats(t *testing.T) { + selector, err := actionheader.NewSelector("Action") if err != nil { t.Fatal(err) } @@ -35,12 +37,12 @@ func TestActionHeaderAcrossRequestFormats(t *testing.T) { }{ {name: "single", values: []string{"Orders.Create"}, want: "Orders.Create"}, {name: "outer_whitespace", values: []string{" \tOrders.Create\t "}, want: "Orders.Create"}, - {name: "missing", wantErr: ErrActionMissing}, - {name: "empty", values: []string{""}, wantErr: ErrActionInvalid}, - {name: "invalid", values: []string{"orders/create"}, wantErr: ErrActionInvalid}, - {name: "duplicates", values: []string{"read", "delete"}, wantErr: ErrActionAmbiguous}, - {name: "identical_duplicates", values: []string{"read", "read"}, wantErr: ErrActionAmbiguous}, - {name: "literal_comma", values: []string{"read,delete"}, wantErr: ErrActionAmbiguous}, + {name: "missing", wantErr: actionheader.ErrActionMissing}, + {name: "empty", values: []string{""}, wantErr: actionheader.ErrActionInvalid}, + {name: "invalid", values: []string{"orders/create"}, wantErr: actionheader.ErrActionInvalid}, + {name: "duplicates", values: []string{"read", "delete"}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "identical_duplicates", values: []string{"read", "read"}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "literal_comma", values: []string{"read,delete"}, wantErr: actionheader.ErrActionAmbiguous}, } { t.Run(tt.name, func(t *testing.T) { for _, format := range []string{"typed_v1", "raw_rest", "raw_http_v1", "typed_v2", "raw_http_v2", "native_http"} { diff --git a/action.go b/actionheader/actionheader.go similarity index 60% rename from action.go rename to actionheader/actionheader.go index a579f90..4eccef2 100644 --- a/action.go +++ b/actionheader/actionheader.go @@ -1,4 +1,7 @@ -package edge +// Package actionheader selects one application action from a configured HTTP request header. +// It uses only the standard library, so native HTTP servers and Lambda adapters can share the same selection rules. +// Consumers own action metadata, registry lookup, authorization and HTTP responses. +package actionheader import ( "errors" @@ -9,35 +12,35 @@ import ( var ( // ErrActionMissing means the configured header has no represented values. // An absent header and a nil or empty value slice are all missing. - ErrActionMissing = errors.New("edge: missing action") + ErrActionMissing = errors.New("actionheader: missing action") // ErrActionAmbiguous means the header has multiple represented values or a value containing a comma. // Gateway payload 2.0 combines duplicates with commas; // a literal comma cannot be distinguished from combined values. - ErrActionAmbiguous = errors.New("edge: ambiguous action") + ErrActionAmbiguous = errors.New("actionheader: ambiguous action") // ErrActionInvalid means the single action value is empty after trimming outer spaces and tabs, or does not satisfy the HTTP token grammar. - ErrActionInvalid = errors.New("edge: invalid action") + ErrActionInvalid = errors.New("actionheader: invalid action") ) -// ActionHeader selects an action from a configured request header. -// Construct it with [NewActionHeader]; +// Selector selects an action from a configured request header. +// Construct it with [NewSelector]; // its zero value is unconfigured. -// An ActionHeader is immutable, safe to copy, and safe for concurrent use. -type ActionHeader struct { +// A Selector is immutable, safe to copy, and safe for concurrent use. +type Selector struct { name string } -// NewActionHeader validates name as an HTTP field name and returns a selector. +// NewSelector validates name as an HTTP field name and returns a selector. // The name is required and matched case-insensitively; // no default is inferred. // On error, the returned selector is the zero value. // Configuration errors are separate from [ErrActionMissing], [ErrActionAmbiguous] and [ErrActionInvalid]. -func NewActionHeader(name string) (ActionHeader, error) { +func NewSelector(name string) (Selector, error) { if !validToken(name) { - return ActionHeader{}, errors.New("edge: invalid action header name") + return Selector{}, errors.New("actionheader: invalid header name") } - return ActionHeader{name: http.CanonicalHeaderKey(name)}, nil + return Selector{name: http.CanonicalHeaderKey(name)}, nil } // Parse returns the single action in headers, trimming only outer SP/HTAB and preserving case. @@ -54,16 +57,16 @@ func NewActionHeader(name string) (ActionHeader, error) { // Parse does not modify headers or write an HTTP response. // Callers must not mutate headers concurrently with Parse. // Select once and use that same action for authorization and execution. -func (h ActionHeader) Parse(headers http.Header) (string, error) { - if h.name == "" { - return "", errors.New("edge: unconfigured action header") +func (s Selector) Parse(headers http.Header) (string, error) { + if s.name == "" { + return "", errors.New("actionheader: unconfigured selector") } var value string found := false for name, values := range headers { // CanonicalHeaderKey folds only valid ASCII field names. // In particular, Unicode case equivalents must not alias the configured HTTP name. - if http.CanonicalHeaderKey(name) != h.name || len(values) == 0 { + if http.CanonicalHeaderKey(name) != s.name || len(values) == 0 { continue } if found || len(values) > 1 { @@ -83,3 +86,18 @@ func (h ActionHeader) Parse(headers http.Header) (string, error) { } return value, nil } + +// validToken reports whether s is a nonempty HTTP token (RFC 9110 §5.6.2). +func validToken(s string) bool { + if s == "" { + return false + } + for i := 0; i < len(s); i++ { + c := s[i] + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsRune("!#$%&'*+-.^_`|~", rune(c)) { + continue + } + return false + } + return true +} diff --git a/action_test.go b/actionheader/actionheader_test.go similarity index 67% rename from action_test.go rename to actionheader/actionheader_test.go index f7f3c89..5afaec0 100644 --- a/action_test.go +++ b/actionheader/actionheader_test.go @@ -1,4 +1,4 @@ -package edge_test +package actionheader_test import ( "errors" @@ -8,11 +8,11 @@ import ( "strings" "testing" - "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" ) -func TestActionHeaderParse(t *testing.T) { - selector, err := edge.NewActionHeader("aCtIoN") +func TestSelectorParse(t *testing.T) { + selector, err := actionheader.NewSelector("aCtIoN") if err != nil { t.Fatal(err) } @@ -22,34 +22,34 @@ func TestActionHeaderParse(t *testing.T) { want string wantErr error }{ - {name: "nil", wantErr: edge.ErrActionMissing}, - {name: "unrelated", headers: http.Header{"Else": {"orders.create"}}, wantErr: edge.ErrActionMissing}, - {name: "nil_slice", headers: http.Header{"Action": nil}, wantErr: edge.ErrActionMissing}, - {name: "empty_slice", headers: http.Header{"Action": {}}, wantErr: edge.ErrActionMissing}, - {name: "empty", headers: http.Header{"Action": {""}}, wantErr: edge.ErrActionInvalid}, - {name: "whitespace", headers: http.Header{"Action": {" \t "}}, wantErr: edge.ErrActionInvalid}, + {name: "nil", wantErr: actionheader.ErrActionMissing}, + {name: "unrelated", headers: http.Header{"Else": {"orders.create"}}, wantErr: actionheader.ErrActionMissing}, + {name: "nil_slice", headers: http.Header{"Action": nil}, wantErr: actionheader.ErrActionMissing}, + {name: "empty_slice", headers: http.Header{"Action": {}}, wantErr: actionheader.ErrActionMissing}, + {name: "empty", headers: http.Header{"Action": {""}}, wantErr: actionheader.ErrActionInvalid}, + {name: "whitespace", headers: http.Header{"Action": {" \t "}}, wantErr: actionheader.ErrActionInvalid}, {name: "trim_and_preserve_case", headers: http.Header{"Action": {" \tOrders.Create\t "}}, want: "Orders.Create"}, {name: "direct_assignment", headers: http.Header{"aCtIoN": {"orders.create"}}, want: "orders.create"}, {name: "empty_alias", headers: http.Header{"ACTION": {}, "action": {"orders.create"}}, want: "orders.create"}, - {name: "conflict", headers: http.Header{"Action": {"read", "delete"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "identical", headers: http.Header{"Action": {"read", "read"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "case_aliases", headers: http.Header{"Action": {"read"}, "ACTION": {"delete"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "identical_aliases", headers: http.Header{"Action": {"read"}, "action": {"read"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "invalid_plus_alias", headers: http.Header{"Action": {""}, "action": {"read"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "multiple_empty", headers: http.Header{"Action": {"", ""}}, wantErr: edge.ErrActionAmbiguous}, - {name: "joined", headers: http.Header{"Action": {"read,delete"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "identical_joined", headers: http.Header{"Action": {"read,read"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "comma_before_invalid", headers: http.Header{"Action": {",\r\n"}}, wantErr: edge.ErrActionAmbiguous}, - {name: "space_in_token", headers: http.Header{"Action": {"orders create"}}, wantErr: edge.ErrActionInvalid}, - {name: "slash", headers: http.Header{"Action": {"orders/create"}}, wantErr: edge.ErrActionInvalid}, - {name: "colon", headers: http.Header{"Action": {"orders:create"}}, wantErr: edge.ErrActionInvalid}, - {name: "quoted", headers: http.Header{"Action": {`"read"`}}, wantErr: edge.ErrActionInvalid}, - {name: "unicode_space", headers: http.Header{"Action": {"\u00a0read"}}, wantErr: edge.ErrActionInvalid}, - {name: "unicode", headers: http.Header{"Action": {"créer"}}, wantErr: edge.ErrActionInvalid}, - {name: "invalid_utf8", headers: http.Header{"Action": {"\xff"}}, wantErr: edge.ErrActionInvalid}, - {name: "newline", headers: http.Header{"Action": {"read\n"}}, wantErr: edge.ErrActionInvalid}, - {name: "nul", headers: http.Header{"Action": {"read\x00"}}, wantErr: edge.ErrActionInvalid}, - {name: "del", headers: http.Header{"Action": {"read\x7f"}}, wantErr: edge.ErrActionInvalid}, + {name: "conflict", headers: http.Header{"Action": {"read", "delete"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "identical", headers: http.Header{"Action": {"read", "read"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "case_aliases", headers: http.Header{"Action": {"read"}, "ACTION": {"delete"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "identical_aliases", headers: http.Header{"Action": {"read"}, "action": {"read"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "invalid_plus_alias", headers: http.Header{"Action": {""}, "action": {"read"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "multiple_empty", headers: http.Header{"Action": {"", ""}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "joined", headers: http.Header{"Action": {"read,delete"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "identical_joined", headers: http.Header{"Action": {"read,read"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "comma_before_invalid", headers: http.Header{"Action": {",\r\n"}}, wantErr: actionheader.ErrActionAmbiguous}, + {name: "space_in_token", headers: http.Header{"Action": {"orders create"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "slash", headers: http.Header{"Action": {"orders/create"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "colon", headers: http.Header{"Action": {"orders:create"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "quoted", headers: http.Header{"Action": {`"read"`}}, wantErr: actionheader.ErrActionInvalid}, + {name: "unicode_space", headers: http.Header{"Action": {"\u00a0read"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "unicode", headers: http.Header{"Action": {"créer"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "invalid_utf8", headers: http.Header{"Action": {"\xff"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "newline", headers: http.Header{"Action": {"read\n"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "nul", headers: http.Header{"Action": {"read\x00"}}, wantErr: actionheader.ErrActionInvalid}, + {name: "del", headers: http.Header{"Action": {"read\x7f"}}, wantErr: actionheader.ErrActionInvalid}, {name: "all_token_punctuation", headers: http.Header{"Action": {"!#$%&'*+-.^_`|~AZaz09"}}, want: "!#$%&'*+-.^_`|~AZaz09"}, } { t.Run(tt.name, func(t *testing.T) { @@ -69,25 +69,25 @@ func TestActionHeaderParse(t *testing.T) { } } -func TestActionHeaderConfiguration(t *testing.T) { +func TestSelectorConfiguration(t *testing.T) { for _, name := range []string{"", " Action", "Action ", "Action:Name", "Action\r\n", "Àction", "Action\xff"} { t.Run(fmt.Sprintf("%q", name), func(t *testing.T) { - h, err := edge.NewActionHeader(name) - if err == nil || h != (edge.ActionHeader{}) { - t.Fatalf("NewActionHeader(%q) = %v, %v; want zero selector and error", name, h, err) + h, err := actionheader.NewSelector(name) + if err == nil || h != (actionheader.Selector{}) { + t.Fatalf("NewSelector(%q) = %v, %v; want zero selector and error", name, h, err) } _, parseErr := h.Parse(http.Header{"Action": {"read"}}) if parseErr == nil { t.Fatal("zero selector Parse succeeded") } - for _, category := range []error{edge.ErrActionMissing, edge.ErrActionAmbiguous, edge.ErrActionInvalid} { + for _, category := range []error{actionheader.ErrActionMissing, actionheader.ErrActionAmbiguous, actionheader.ErrActionInvalid} { if errors.Is(err, category) || errors.Is(parseErr, category) { t.Errorf("configuration error matched input category %v", category) } } }) } - selector, err := edge.NewActionHeader("Task") + selector, err := actionheader.NewSelector("Task") if err != nil { t.Fatal(err) } @@ -98,8 +98,8 @@ func TestActionHeaderConfiguration(t *testing.T) { } } -func TestActionHeaderErrorsDoNotDiscloseValues(t *testing.T) { - selector, err := edge.NewActionHeader("Action") +func TestSelectorErrorsDoNotDiscloseValues(t *testing.T) { + selector, err := actionheader.NewSelector("Action") if err != nil { t.Fatal(err) } diff --git a/actionheader/example_test.go b/actionheader/example_test.go new file mode 100644 index 0000000..9d2967a --- /dev/null +++ b/actionheader/example_test.go @@ -0,0 +1,59 @@ +package actionheader_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + + "github.com/asteroid-computing/go-lambda-edge/actionheader" +) + +// These are consumer-owned middleware and metadata, not actionheader API types. +type actionKey struct{} + +func processActionHeaders(selector actionheader.Selector, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + action, err := selector.Parse(r.Header) + if err != nil { + http.Error(w, "invalid action selection", http.StatusBadRequest) + return + } + ctx := context.WithValue(r.Context(), actionKey{}, action) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func ExampleSelector() { + selector, err := actionheader.NewSelector("Action") + if err != nil { + fmt.Println(err) + return + } + dispatcher := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + action, ok := r.Context().Value(actionKey{}).(string) + if !ok { + http.Error(w, "missing action metadata", http.StatusInternalServerError) + return + } + // This example operation is public. + // Protected operations must authorize this same selection before executing or publishing streaming output. + switch action { + case "service.ping": + fmt.Fprintln(w, "pong") + default: + http.Error(w, "unknown action", http.StatusBadRequest) + } + }) + // The composed http.Handler serves native HTTP directly or goes to an edge adapter unchanged. + handler := processActionHeaders(selector, dispatcher) + r := httptest.NewRequest(http.MethodPost, "/", nil) + r.Header.Set("Action", "service.ping") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + fmt.Println(w.Code) + fmt.Print(w.Body.String()) + // Output: + // 200 + // pong +} diff --git a/authn/authorization_test.go b/authn/authorization_test.go index e1740e8..95afade 100644 --- a/authn/authorization_test.go +++ b/authn/authorization_test.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "testing" - "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" "github.com/asteroid-computing/go-lambda-edge/authn" "github.com/asteroid-computing/go-lambda-edge/authz" "github.com/asteroid-computing/go-lambda-edge/identity" @@ -25,7 +25,7 @@ func TestAuthorizationAcrossTransports(t *testing.T) { return jwksResponse(keys), nil }))) a := authenticator(t, authn.Config{Bearer: verifier.Verify, IAMProof: mapProofErrors(proofVerifier)}) - selector, err := edge.NewActionHeader("Action") + selector, err := actionheader.NewSelector("Action") if err != nil { t.Fatal(err) } diff --git a/authn/example_test.go b/authn/example_test.go index b552f37..db3b808 100644 --- a/authn/example_test.go +++ b/authn/example_test.go @@ -8,7 +8,7 @@ import ( "net/http" "net/http/httptest" - "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" "github.com/asteroid-computing/go-lambda-edge/authn" "github.com/asteroid-computing/go-lambda-edge/iamproof" "github.com/asteroid-computing/go-lambda-edge/identity" @@ -45,7 +45,7 @@ func ExampleAuthenticator_Handler() { fmt.Println(err) return } - selector, err := edge.NewActionHeader("Action") + selector, err := actionheader.NewSelector("Action") if err != nil { fmt.Println(err) return diff --git a/authn/gateway_test.go b/authn/gateway_test.go index c092f0b..b110ed2 100644 --- a/authn/gateway_test.go +++ b/authn/gateway_test.go @@ -18,6 +18,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" "github.com/asteroid-computing/go-lambda-edge/authn" "github.com/asteroid-computing/go-lambda-edge/authz" "github.com/asteroid-computing/go-lambda-edge/iamproof" @@ -71,7 +72,7 @@ func TestAuthenticationAcrossTransports(t *testing.T) { key := signingKey(t) jwt := signedToken(t, key, "integration", accessClaims()) keys := keySet(t, publicJWK(key, "integration")) - selector, err := edge.NewActionHeader("Action") + selector, err := actionheader.NewSelector("Action") if err != nil { t.Fatal(err) } diff --git a/authz/example_test.go b/authz/example_test.go index 233ce18..5b9c37a 100644 --- a/authz/example_test.go +++ b/authz/example_test.go @@ -8,7 +8,7 @@ import ( "net/http" "net/http/httptest" - "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" "github.com/asteroid-computing/go-lambda-edge/authn" "github.com/asteroid-computing/go-lambda-edge/authz" "github.com/asteroid-computing/go-lambda-edge/iamproof" @@ -27,7 +27,7 @@ type grantStore interface { // This example has one fixed resource; // a real application validates/resolves its target before calling Authorize and keeps that same object/version for execution, using a transaction if needed. func orderHandler(authentication *authn.Authenticator, grants grantStore, issuer string) (http.Handler, error) { - selector, err := edge.NewActionHeader("Action") + selector, err := actionheader.NewSelector("Action") if err != nil { return nil, err } diff --git a/docs/authn.md b/docs/authn.md index 18a200d..4c121f0 100644 --- a/docs/authn.md +++ b/docs/authn.md @@ -131,7 +131,7 @@ Their synthetic Bearer fixture is explicitly not a production token verifier. ## Action dispatch and deployment -Select an action once using edge.ActionHeader or your own header processor. +Select an action once using actionheader.Selector or your own header processor. Authorize that caller for that exact selection, then execute it. Both IAM and JWT callers can reach the same dispatcher. Successful IAM authentication does not permit every AWS identity to execute your actions; diff --git a/docs/plan.md b/docs/plan.md index aab0596..86fdaa9 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -376,7 +376,7 @@ The owner approved E1–E6 and rejected E7 on 2026-09-27, with one stacked PR pe Behavior, error semantics and trust boundaries do not change; these are placement, naming, typing and documentation changes made before the first release. -- [ ] E1: move the action-header selector to the standard-library-only `actionheader` package as `Selector`/`NewSelector`. +- [x] E1: move the action-header selector to the standard-library-only `actionheader` package as `Selector`/`NewSelector`. - [ ] E2: rename the moved sentinels to `ErrMissing`, `ErrAmbiguous` and `ErrInvalid`. - [ ] E3: rename `identity.WithCaller` to `identity.NewContext`. - [ ] E4: type the `InvocationError` operation and limit names with exported constants. diff --git a/docs/support.md b/docs/support.md index e9fa081..fb19cc7 100644 --- a/docs/support.md +++ b/docs/support.md @@ -10,7 +10,7 @@ The dispatcher's internal application and commands are examples. | Buffered HTTP API 1.0 | Raw version 1.0; typed HandleV1 | Shared SDK type cannot identify API product | | Buffered HTTP API 2.0 | Raw version 2.0; typed HandleV2 | Combined headers/query values cannot be recovered; separate cookies | | REST streaming | NewStreaming, raw Handle, typed HandleV1 | Local reader/SDK tests pass; AWS deployment qualification open | -| Custom headers/actions | Ordinary HTTP middleware, ActionHeader | Strict selection tested across transports | +| Custom headers/actions | Ordinary HTTP middleware, actionheader.Selector | Strict selection tested across transports | | Gateway identity | Opt-in native IAM/JWT/Cognito mapping | Synthetic recognition/fidelity tests; no credential re-verification | | Local OAuth | Cognito RS256 access-token verifier | Local signatures, independent vector, cache/rotation/outage tests; no live Cognito run | | Local IAM | Signed GetCallerIdentity proof generator and verifier | Local protocol/HTTP contracts; no live STS run; not raw execute-api SigV4 validation | diff --git a/examples/dispatcher/internal/orders/app.go b/examples/dispatcher/internal/orders/app.go index 170a918..3d3d507 100644 --- a/examples/dispatcher/internal/orders/app.go +++ b/examples/dispatcher/internal/orders/app.go @@ -9,7 +9,7 @@ import ( "net/http" "strings" - "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" "github.com/asteroid-computing/go-lambda-edge/authn" "github.com/asteroid-computing/go-lambda-edge/authz" "github.com/asteroid-computing/go-lambda-edge/iamproof" @@ -78,7 +78,7 @@ func New(cfg Config) (http.Handler, error) { if err != nil { return nil, err } - selector, err := edge.NewActionHeader("Action") + selector, err := actionheader.NewSelector("Action") if err != nil { return nil, err } diff --git a/testdata/consumer/consumer_test.go.txt b/testdata/consumer/consumer_test.go.txt index 678091b..2919ce5 100644 --- a/testdata/consumer/consumer_test.go.txt +++ b/testdata/consumer/consumer_test.go.txt @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-lambda-go/lambda" "github.com/asteroid-computing/go-lambda-edge" + "github.com/asteroid-computing/go-lambda-edge/actionheader" "github.com/asteroid-computing/go-lambda-edge/authn" "github.com/asteroid-computing/go-lambda-edge/authz" "github.com/asteroid-computing/go-lambda-edge/iamproof" @@ -34,7 +35,7 @@ func TestPublicComposition(t *testing.T) { if err != nil { t.Fatal(err) } - selector, err := edge.NewActionHeader("Action") + selector, err := actionheader.NewSelector("Action") if err != nil { t.Fatal(err) }