From ecb2d0ada81d06add848f2fdbdb40dc7fbf170c7 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 20 Jul 2026 16:52:10 +0530 Subject: [PATCH 01/71] feat(oauth): add test-only mock-IdP base URL overrides for e2e-playground --- cmd/root.go | 12 ++++++++ internal/config/config.go | 24 +++++++++++++++ internal/config/test_oauth_override.go | 34 +++++++++++++++++++++ internal/config/test_oauth_override_test.go | 16 ++++++++++ 4 files changed, 86 insertions(+) create mode 100644 internal/config/test_oauth_override.go create mode 100644 internal/config/test_oauth_override_test.go diff --git a/cmd/root.go b/cmd/root.go index 78837d5a..b1f530d7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -260,6 +260,18 @@ func init() { f.StringVar(&rootArgs.config.RobloxClientSecret, "roblox-client-secret", "", "Client secret for Roblox") f.StringSliceVar(&rootArgs.config.RobloxScopes, "roblox-scopes", defaultRobloxScopes, "Scopes for Roblox") + // Test-only OAuth mock-IdP base URL overrides + f.StringVar(&rootArgs.config.TestOAuthGoogleBaseURL, "test-oauth-google-base-url", "", "(test-only) override Google OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthGithubBaseURL, "test-oauth-github-base-url", "", "(test-only) override GitHub OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthFacebookBaseURL, "test-oauth-facebook-base-url", "", "(test-only) override Facebook OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthLinkedinBaseURL, "test-oauth-linkedin-base-url", "", "(test-only) override LinkedIn OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthAppleBaseURL, "test-oauth-apple-base-url", "", "(test-only) override Apple OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthTwitterBaseURL, "test-oauth-twitter-base-url", "", "(test-only) override Twitter OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthDiscordBaseURL, "test-oauth-discord-base-url", "", "(test-only) override Discord OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthMicrosoftBaseURL, "test-oauth-microsoft-base-url", "", "(test-only) override Microsoft OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthTwitchBaseURL, "test-oauth-twitch-base-url", "", "(test-only) override Twitch OAuth base URL for e2e mock IdP") + f.StringVar(&rootArgs.config.TestOAuthRobloxBaseURL, "test-oauth-roblox-base-url", "", "(test-only) override Roblox OAuth base URL for e2e mock IdP") + // URLs f.StringVar(&rootArgs.config.AuthorizerURL, "url", "", "Canonical/trusted base URL of this Authorizer instance (e.g. https://auth.example.com). When set, it is the ONLY source used to build verification/reset/magic-link email URLs, the JWT iss claim, and OIDC discovery URLs; all request headers (X-Authorizer-URL, X-Forwarded-Host, Host) are ignored. Leaving it empty keeps legacy header-based derivation but exposes host-header-injection account takeover (CWE-640) — set this in production") f.StringVar(&rootArgs.config.ResetPasswordURL, "reset-password-url", "", "URL for reset password") diff --git a/internal/config/config.go b/internal/config/config.go index 53b513fd..96c3e70e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -285,6 +285,30 @@ type Config struct { // Scopes is the list of scopes for Google OAuth GoogleScopes []string + // TestOAuthGoogleBaseURL overrides Google's OAuth authorize/token/userinfo + // base URL. Only ever set by e2e-playground/docker-compose.yml; empty + // (the default) in every real deployment, in which case production + // endpoints are used unchanged. + TestOAuthGoogleBaseURL string + // TestOAuthGithubBaseURL is the e2e-playground override for GitHub. See TestOAuthGoogleBaseURL. + TestOAuthGithubBaseURL string + // TestOAuthFacebookBaseURL is the e2e-playground override for Facebook. See TestOAuthGoogleBaseURL. + TestOAuthFacebookBaseURL string + // TestOAuthLinkedinBaseURL is the e2e-playground override for LinkedIn. See TestOAuthGoogleBaseURL. + TestOAuthLinkedinBaseURL string + // TestOAuthAppleBaseURL is the e2e-playground override for Apple. See TestOAuthGoogleBaseURL. + TestOAuthAppleBaseURL string + // TestOAuthTwitterBaseURL is the e2e-playground override for Twitter/X. See TestOAuthGoogleBaseURL. + TestOAuthTwitterBaseURL string + // TestOAuthDiscordBaseURL is the e2e-playground override for Discord. See TestOAuthGoogleBaseURL. + TestOAuthDiscordBaseURL string + // TestOAuthMicrosoftBaseURL is the e2e-playground override for Microsoft. See TestOAuthGoogleBaseURL. + TestOAuthMicrosoftBaseURL string + // TestOAuthTwitchBaseURL is the e2e-playground override for Twitch. See TestOAuthGoogleBaseURL. + TestOAuthTwitchBaseURL string + // TestOAuthRobloxBaseURL is the e2e-playground override for Roblox. See TestOAuthGoogleBaseURL. + TestOAuthRobloxBaseURL string + // GithubClientID is the client ID for Github OAuth GithubClientID string // GithubClientSecret is the client secret for Github OAuth diff --git a/internal/config/test_oauth_override.go b/internal/config/test_oauth_override.go new file mode 100644 index 00000000..d8530e46 --- /dev/null +++ b/internal/config/test_oauth_override.go @@ -0,0 +1,34 @@ +package config + +import "github.com/authorizerdev/authorizer/internal/constants" + +// TestOAuthBaseURL returns the configured e2e-playground mock-IdP base URL +// for the given social-login provider, or "" when unset. These fields are +// only ever populated by e2e-playground/docker-compose.yml; every real +// deployment leaves them empty, so callers fall back to production endpoints. +func (c *Config) TestOAuthBaseURL(provider string) string { + switch provider { + case constants.AuthRecipeMethodGoogle: + return c.TestOAuthGoogleBaseURL + case constants.AuthRecipeMethodGithub: + return c.TestOAuthGithubBaseURL + case constants.AuthRecipeMethodFacebook: + return c.TestOAuthFacebookBaseURL + case constants.AuthRecipeMethodLinkedIn: + return c.TestOAuthLinkedinBaseURL + case constants.AuthRecipeMethodApple: + return c.TestOAuthAppleBaseURL + case constants.AuthRecipeMethodTwitter: + return c.TestOAuthTwitterBaseURL + case constants.AuthRecipeMethodDiscord: + return c.TestOAuthDiscordBaseURL + case constants.AuthRecipeMethodMicrosoft: + return c.TestOAuthMicrosoftBaseURL + case constants.AuthRecipeMethodTwitch: + return c.TestOAuthTwitchBaseURL + case constants.AuthRecipeMethodRoblox: + return c.TestOAuthRobloxBaseURL + default: + return "" + } +} diff --git a/internal/config/test_oauth_override_test.go b/internal/config/test_oauth_override_test.go new file mode 100644 index 00000000..86e4a3f9 --- /dev/null +++ b/internal/config/test_oauth_override_test.go @@ -0,0 +1,16 @@ +package config + +import ( + "testing" + + "github.com/authorizerdev/authorizer/internal/constants" + "github.com/stretchr/testify/assert" +) + +func TestTestOAuthBaseURL(t *testing.T) { + c := &Config{TestOAuthGoogleBaseURL: "http://mock-oauth:4000/google"} + + assert.Equal(t, "http://mock-oauth:4000/google", c.TestOAuthBaseURL(constants.AuthRecipeMethodGoogle)) + assert.Equal(t, "", c.TestOAuthBaseURL(constants.AuthRecipeMethodGithub), "unset provider must return empty string") + assert.Equal(t, "", c.TestOAuthBaseURL("not-a-real-provider"), "unknown provider must return empty string") +} From 8dce1fc62c8a31449104976cbebe8cf24e5a68f5 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 20 Jul 2026 16:59:43 +0530 Subject: [PATCH 02/71] feat(oauth): route userinfo/issuer lookups through mock-IdP override when configured --- internal/http_handlers/oauth_callback.go | 68 +++++++++++++++++++----- internal/oauth/get_oauth_config.go | 54 ++++++++++++++----- internal/oauth/get_oauth_config_test.go | 55 +++++++++++++++++++ 3 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 internal/oauth/get_oauth_config_test.go diff --git a/internal/http_handlers/oauth_callback.go b/internal/http_handlers/oauth_callback.go index 9026a5aa..7906c82c 100644 --- a/internal/http_handlers/oauth_callback.go +++ b/internal/http_handlers/oauth_callback.go @@ -452,7 +452,11 @@ func (h *httpProvider) processGoogleUserInfo(ctx *gin.Context, code string) (*sc return nil, fmt.Errorf("invalid google exchange code: %s", err.Error()) } - oidcProvider, err := getOIDCProvider(ctx, "https://accounts.google.com") + issuer := "https://accounts.google.com" + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodGoogle); mockBase != "" { + issuer = mockBase + } + oidcProvider, err := getOIDCProvider(ctx, issuer) if err != nil { return nil, fmt.Errorf("failed to create oidc provider: %s", err.Error()) } @@ -492,8 +496,14 @@ func (h *httpProvider) processGithubUserInfo(ctx *gin.Context, code string) (*sc log.Debug().Err(err).Msg("Failed to exchange code for token") return nil, fmt.Errorf("invalid github exchange code: %s", err.Error()) } + userInfoURL := constants.GithubUserInfoURL + emailsURL := constants.GithubUserEmails + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodGithub); mockBase != "" { + userInfoURL = mockBase + "/userinfo" + emailsURL = mockBase + "/user/emails" + } client := http.Client{} - req, err := http.NewRequest("GET", constants.GithubUserInfoURL, nil) + req, err := http.NewRequest("GET", userInfoURL, nil) if err != nil { log.Debug().Err(err).Msg("Failed to create github user info request") return nil, fmt.Errorf("error creating github user info request: %s", err.Error()) @@ -545,7 +555,7 @@ func (h *httpProvider) processGithubUserInfo(ctx *gin.Context, code string) (*sc } // fetch using /users/email endpoint - req, err := http.NewRequest(http.MethodGet, constants.GithubUserEmails, nil) + req, err := http.NewRequest(http.MethodGet, emailsURL, nil) if err != nil { log.Debug().Err(err).Msg("Failed to create github emails request") return nil, fmt.Errorf("error creating github user info request: %s", err.Error()) @@ -608,8 +618,12 @@ func (h *httpProvider) processFacebookUserInfo(ctx *gin.Context, code string) (* log.Debug().Err(err).Msg("Invalid facebook exchange code") return nil, fmt.Errorf("invalid facebook exchange code: %s", err.Error()) } + userInfoURL := constants.FacebookUserInfoURL + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodFacebook); mockBase != "" { + userInfoURL = mockBase + "/userinfo?access_token=" + } client := http.Client{} - req, err := http.NewRequest("GET", constants.FacebookUserInfoURL+oauth2Token.AccessToken, nil) + req, err := http.NewRequest("GET", userInfoURL+oauth2Token.AccessToken, nil) if err != nil { log.Debug().Err(err).Msg("Error creating facebook user info request") return nil, fmt.Errorf("error creating facebook user info request: %s", err.Error()) @@ -672,8 +686,14 @@ func (h *httpProvider) processLinkedInUserInfo(ctx *gin.Context, code string) (* return nil, fmt.Errorf("invalid linkedin exchange code: %s", err.Error()) } + userInfoURL := constants.LinkedInUserInfoURL + emailURL := constants.LinkedInEmailURL + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodLinkedIn); mockBase != "" { + userInfoURL = mockBase + "/userinfo" + emailURL = mockBase + "/emailAddress" + } client := http.Client{} - req, err := http.NewRequest("GET", constants.LinkedInUserInfoURL, nil) + req, err := http.NewRequest("GET", userInfoURL, nil) if err != nil { log.Debug().Err(err).Msg("Failed to create linkedin user info request") return nil, fmt.Errorf("error creating linkedin user info request: %s", err.Error()) @@ -706,7 +726,7 @@ func (h *httpProvider) processLinkedInUserInfo(ctx *gin.Context, code string) (* return nil, fmt.Errorf("failed to parse linkedin user info: %s", err.Error()) } - req, err = http.NewRequest("GET", constants.LinkedInEmailURL, nil) + req, err = http.NewRequest("GET", emailURL, nil) if err != nil { log.Debug().Err(err).Msg("Failed to create linkedin email info request") return nil, fmt.Errorf("error creating linkedin user info request: %s", err.Error()) @@ -802,7 +822,11 @@ func (h *httpProvider) processAppleUserInfo(ctx *gin.Context, code string, user_ } // Verify the Apple ID token signature, issuer, and audience using OIDC discovery - oidcProvider, err := getOIDCProvider(ctx, "https://appleid.apple.com") + issuer := "https://appleid.apple.com" + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodApple); mockBase != "" { + issuer = mockBase + } + oidcProvider, err := getOIDCProvider(ctx, issuer) if err != nil { log.Debug().Err(err).Msg("Failed to create Apple OIDC provider") return user, fmt.Errorf("failed to create oidc provider: %s", err.Error()) @@ -847,8 +871,12 @@ func (h *httpProvider) processDiscordUserInfo(ctx *gin.Context, code string) (*s return nil, fmt.Errorf("invalid discord exchange code: %s", err.Error()) } + userInfoURL := constants.DiscordUserInfoURL + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodDiscord); mockBase != "" { + userInfoURL = mockBase + "/userinfo" + } client := http.Client{} - req, err := http.NewRequest("GET", constants.DiscordUserInfoURL, nil) + req, err := http.NewRequest("GET", userInfoURL, nil) if err != nil { log.Debug().Err(err).Msg("Failed to create Discord user info request") return nil, fmt.Errorf("error creating Discord user info request: %s", err.Error()) @@ -921,8 +949,12 @@ func (h *httpProvider) processTwitterUserInfo(ctx *gin.Context, code, verifier s return nil, fmt.Errorf("invalid twitter exchange code: %s", err.Error()) } + userInfoURL := constants.TwitterUserInfoURL + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodTwitter); mockBase != "" { + userInfoURL = mockBase + "/userinfo" + } client := http.Client{} - req, err := http.NewRequest("GET", constants.TwitterUserInfoURL, nil) + req, err := http.NewRequest("GET", userInfoURL, nil) if err != nil { log.Debug().Err(err).Msg("Failed to create Twitter user info request") return nil, fmt.Errorf("error creating Twitter user info request: %s", err.Error()) @@ -1000,7 +1032,11 @@ func (h *httpProvider) processMicrosoftUserInfo(ctx *gin.Context, code string) ( log.Debug().Err(err).Msg("Failed to exchange code for token") return nil, fmt.Errorf("invalid microsoft exchange code: %s", err.Error()) } - oidcProvider, err := getOIDCProvider(ctx, fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0", h.MicrosoftTenantID)) + issuer := fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0", h.MicrosoftTenantID) + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodMicrosoft); mockBase != "" { + issuer = mockBase + } + oidcProvider, err := getOIDCProvider(ctx, issuer) if err != nil { return nil, fmt.Errorf("failed to create oidc provider: %s", err.Error()) } @@ -1051,7 +1087,11 @@ func (h *httpProvider) processTwitchUserInfo(ctx *gin.Context, code string) (*sc log.Debug().Err(err).Msg("Failed to extract ID Token from OAuth2 token") return nil, fmt.Errorf("unable to extract id_token") } - oidcProvider, err := getOIDCProvider(ctx, "https://id.twitch.tv/oauth2") + issuer := "https://id.twitch.tv/oauth2" + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodTwitch); mockBase != "" { + issuer = mockBase + } + oidcProvider, err := getOIDCProvider(ctx, issuer) if err != nil { log.Debug().Err(err).Msg("Failed to create OIDC provider") return nil, fmt.Errorf("failed to create oidc provider: %s", err.Error()) @@ -1093,8 +1133,12 @@ func (h *httpProvider) processRobloxUserInfo(ctx *gin.Context, code string) (*sc return nil, fmt.Errorf("invalid roblox exchange code: %s", err.Error()) } + userInfoURL := constants.RobloxUserInfoURL + if mockBase := h.TestOAuthBaseURL(constants.AuthRecipeMethodRoblox); mockBase != "" { + userInfoURL = mockBase + "/userinfo" + } client := http.Client{} - req, err := http.NewRequest("GET", constants.RobloxUserInfoURL, nil) + req, err := http.NewRequest("GET", userInfoURL, nil) if err != nil { log.Debug().Err(err).Msg("Failed to create roblox user info request") return nil, fmt.Errorf("error creating roblox user info request: %s", err.Error()) diff --git a/internal/oauth/get_oauth_config.go b/internal/oauth/get_oauth_config.go index d4b39eb5..2a1fcae6 100644 --- a/internal/oauth/get_oauth_config.go +++ b/internal/oauth/get_oauth_config.go @@ -18,78 +18,106 @@ func (o *oauthProvider) GetOAuthConfig(ctx *gin.Context, provider string) (*oaut var clientID, clientSecret string var endPoint *oauth2.Endpoint var scopes []string + + if mockBase := o.TestOAuthBaseURL(provider); mockBase != "" { + endPoint = &oauth2.Endpoint{ + AuthURL: mockBase + "/authorize", + TokenURL: mockBase + "/token", + } + } + switch provider { case constants.AuthRecipeMethodGoogle: if o.GoogleClientID != "" && o.GoogleClientSecret != "" { clientID = o.GoogleClientID clientSecret = o.GoogleClientSecret - endPoint = &endpoints.Google + if endPoint == nil { + endPoint = &endpoints.Google + } scopes = o.GoogleScopes } case constants.AuthRecipeMethodGithub: if o.GithubClientID != "" && o.GithubClientSecret != "" { clientID = o.GithubClientID clientSecret = o.GithubClientSecret - endPoint = &endpoints.GitHub + if endPoint == nil { + endPoint = &endpoints.GitHub + } scopes = o.GithubScopes } case constants.AuthRecipeMethodFacebook: if o.FacebookClientID != "" && o.FacebookClientSecret != "" { clientID = o.FacebookClientID clientSecret = o.FacebookClientSecret - endPoint = &endpoints.Facebook + if endPoint == nil { + endPoint = &endpoints.Facebook + } scopes = o.FacebookScopes } case constants.AuthRecipeMethodLinkedIn: if o.LinkedinClientID != "" && o.LinkedinClientSecret != "" { clientID = o.LinkedinClientID clientSecret = o.LinkedinClientSecret - endPoint = &endpoints.LinkedIn + if endPoint == nil { + endPoint = &endpoints.LinkedIn + } scopes = o.LinkedinScopes } case constants.AuthRecipeMethodApple: if o.AppleClientID != "" && o.AppleClientSecret != "" { clientID = o.AppleClientID clientSecret = o.AppleClientSecret - endPoint = &endpoints.Apple + if endPoint == nil { + endPoint = &endpoints.Apple + } scopes = o.AppleScopes } case constants.AuthRecipeMethodTwitter: if o.TwitterClientID != "" && o.TwitterClientSecret != "" { clientID = o.TwitterClientID clientSecret = o.TwitterClientSecret - endPoint = &endpoints.X + if endPoint == nil { + endPoint = &endpoints.X + } scopes = o.TwitterScopes } case constants.AuthRecipeMethodDiscord: if o.DiscordClientID != "" && o.DiscordClientSecret != "" { clientID = o.DiscordClientID clientSecret = o.DiscordClientSecret - endPoint = &endpoints.Discord + if endPoint == nil { + endPoint = &endpoints.Discord + } scopes = o.DiscordScopes } case constants.AuthRecipeMethodMicrosoft: if o.MicrosoftClientID != "" && o.MicrosoftClientSecret != "" { - ep := endpoints.AzureAD(o.MicrosoftTenantID) clientID = o.MicrosoftClientID clientSecret = o.MicrosoftClientSecret - endPoint = &ep + if endPoint == nil { + ep := endpoints.AzureAD(o.MicrosoftTenantID) + endPoint = &ep + } scopes = o.MicrosoftScopes } case constants.AuthRecipeMethodTwitch: if o.TwitchClientID != "" && o.TwitchClientSecret != "" { clientID = o.TwitchClientID clientSecret = o.TwitchClientSecret - endPoint = &endpoints.Twitch + if endPoint == nil { + endPoint = &endpoints.Twitch + } scopes = o.TwitchScopes } case constants.AuthRecipeMethodRoblox: if o.RobloxClientID != "" && o.RobloxClientSecret != "" { clientID = o.RobloxClientID clientSecret = o.RobloxClientSecret - endPoint = &oauth2.Endpoint{ - AuthURL: "https://apis.roblox.com/oauth/v1/authorize", - TokenURL: "https://apis.roblox.com/oauth/v1/token", + if endPoint == nil { + endPoint = &oauth2.Endpoint{ + AuthURL: "https://apis.roblox.com/oauth/v1/authorize", + TokenURL: "https://apis.roblox.com/oauth/v1/token", + } } scopes = o.RobloxScopes } diff --git a/internal/oauth/get_oauth_config_test.go b/internal/oauth/get_oauth_config_test.go new file mode 100644 index 00000000..4b5630ce --- /dev/null +++ b/internal/oauth/get_oauth_config_test.go @@ -0,0 +1,55 @@ +package oauth + +import ( + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/authorizerdev/authorizer/internal/config" + "github.com/authorizerdev/authorizer/internal/constants" +) + +func TestGetOAuthConfig_UsesMockBaseURLWhenSet(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = httptest.NewRequest("GET", "http://localhost/", nil) + + cfg := &config.Config{ + GoogleClientID: "test-client-id", + GoogleClientSecret: "test-client-secret", + TestOAuthGoogleBaseURL: "http://mock-oauth:4000/google", + } + p, err := New(cfg, &Dependencies{Log: testLogger(t)}) + require.NoError(t, err) + + oauthCfg, err := p.GetOAuthConfig(ctx, constants.AuthRecipeMethodGoogle) + require.NoError(t, err) + assert.Equal(t, "http://mock-oauth:4000/google/authorize", oauthCfg.Endpoint.AuthURL) + assert.Equal(t, "http://mock-oauth:4000/google/token", oauthCfg.Endpoint.TokenURL) +} + +func TestGetOAuthConfig_UsesRealEndpointWhenUnset(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = httptest.NewRequest("GET", "http://localhost/", nil) + + cfg := &config.Config{GoogleClientID: "test-client-id", GoogleClientSecret: "test-client-secret"} + p, err := New(cfg, &Dependencies{Log: testLogger(t)}) + require.NoError(t, err) + + oauthCfg, err := p.GetOAuthConfig(ctx, constants.AuthRecipeMethodGoogle) + require.NoError(t, err) + assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", oauthCfg.Endpoint.AuthURL) +} + +func testLogger(t *testing.T) *zerolog.Logger { + t.Helper() + l := zerolog.Nop() + return &l +} From ffbfa5fc9eda6f4eeaca5bd3b4c7c2e666855bab Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 20 Jul 2026 17:06:21 +0530 Subject: [PATCH 03/71] feat(sms): add pluggable test webhook SMS provider for e2e-playground --- cmd/root.go | 1 + internal/config/config.go | 4 ++ internal/sms/provider.go | 4 ++ internal/sms/testwebhook/testwebhook.go | 65 ++++++++++++++++++++ internal/sms/testwebhook/testwebhook_test.go | 35 +++++++++++ 5 files changed, 109 insertions(+) create mode 100644 internal/sms/testwebhook/testwebhook.go create mode 100644 internal/sms/testwebhook/testwebhook_test.go diff --git a/cmd/root.go b/cmd/root.go index b1f530d7..2af88703 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -226,6 +226,7 @@ func init() { f.StringVar(&rootArgs.config.TwilioAPIKey, "twilio-api-key", "", "API key for Twilio") f.StringVar(&rootArgs.config.TwilioAPISecret, "twilio-api-secret", "", "API secret for Twilio") f.StringVar(&rootArgs.config.TwilioSender, "twilio-sender", "", "Sender for Twilio") + f.StringVar(&rootArgs.config.TestSMSWebhookURL, "test-sms-webhook-url", "", "(test-only) route SMS to this webhook instead of Twilio, for e2e-playground") // Oauth provider flags f.StringVar(&rootArgs.config.GoogleClientID, "google-client-id", "", "Client ID for Google") diff --git a/internal/config/config.go b/internal/config/config.go index 96c3e70e..834d0c7d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -276,6 +276,10 @@ type Config struct { TwilioSender string // TwilioAccountSID is the account SID for Twilio TwilioAccountSID string + // TestSMSWebhookURL, when set, routes SMS sending to a test webhook + // instead of Twilio. Only ever set by e2e-playground/docker-compose.yml; + // empty (the default) in every real deployment. + TestSMSWebhookURL string // OAuth providers that authorizer supports // GoogleClientID is the client ID for Google OAuth diff --git a/internal/sms/provider.go b/internal/sms/provider.go index b5d95aec..774253d2 100644 --- a/internal/sms/provider.go +++ b/internal/sms/provider.go @@ -2,6 +2,7 @@ package sms import ( "github.com/authorizerdev/authorizer/internal/config" + "github.com/authorizerdev/authorizer/internal/sms/testwebhook" "github.com/authorizerdev/authorizer/internal/sms/twilio" "github.com/rs/zerolog" ) @@ -21,6 +22,9 @@ type Provider interface { func New(cfg *config.Config, deps *Dependencies) (Provider, error) { var provider Provider var err error + if cfg.TestSMSWebhookURL != "" { + return testwebhook.NewTestWebhookProvider(cfg, &testwebhook.Dependencies{Log: deps.Log}) + } if cfg.TwilioAPIKey != "" && cfg.TwilioAPISecret != "" && cfg.TwilioAccountSID != "" && cfg.TwilioSender != "" { provider, err = twilio.NewTwilioProvider(cfg, &twilio.Dependencies{ Log: deps.Log, diff --git a/internal/sms/testwebhook/testwebhook.go b/internal/sms/testwebhook/testwebhook.go new file mode 100644 index 00000000..54d77e88 --- /dev/null +++ b/internal/sms/testwebhook/testwebhook.go @@ -0,0 +1,65 @@ +package testwebhook + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/rs/zerolog" + + "github.com/authorizerdev/authorizer/internal/config" +) + +// Dependencies for the test webhook SMS provider. +type Dependencies struct { + Log *zerolog.Logger +} + +// testWebhookProvider is an sms.Provider that POSTs the plaintext SMS +// payload to a configured HTTP endpoint instead of calling a real carrier. +// Only ever wired when cfg.TestSMSWebhookURL is explicitly set — see +// internal/sms/provider.go. Exists purely for e2e-playground, where +// mocks/sms-sink stores the payload so Playwright tests can retrieve the +// OTP code that a real carrier would otherwise deliver by SMS. +type testWebhookProvider struct { + webhookURL string + client *http.Client + log *zerolog.Logger +} + +type payload struct { + Phone string `json:"phone"` + Message string `json:"message"` +} + +// NewTestWebhookProvider constructs a test-only SMS provider. +func NewTestWebhookProvider(cfg *config.Config, deps *Dependencies) (*testWebhookProvider, error) { + if cfg.TestSMSWebhookURL == "" { + return nil, fmt.Errorf("TestSMSWebhookURL is required") + } + return &testWebhookProvider{ + webhookURL: cfg.TestSMSWebhookURL, + client: &http.Client{Timeout: 5 * time.Second}, + log: deps.Log, + }, nil +} + +// SendSMS posts the plaintext code to the configured test webhook. +func (p *testWebhookProvider) SendSMS(sendTo, messageBody string) error { + body, err := json.Marshal(payload{Phone: sendTo, Message: messageBody}) + if err != nil { + return fmt.Errorf("failed to marshal test sms payload: %w", err) + } + resp, err := p.client.Post(p.webhookURL, "application/json", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to post test sms webhook: %w", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode >= 400 { + return fmt.Errorf("test sms webhook returned status %d", resp.StatusCode) + } + p.log.Debug().Str("phone", sendTo).Msg("test sms sent via webhook") + return nil +} diff --git a/internal/sms/testwebhook/testwebhook_test.go b/internal/sms/testwebhook/testwebhook_test.go new file mode 100644 index 00000000..063f7788 --- /dev/null +++ b/internal/sms/testwebhook/testwebhook_test.go @@ -0,0 +1,35 @@ +package testwebhook + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/authorizerdev/authorizer/internal/config" +) + +func TestSendSMS_PostsPhoneAndMessageToWebhook(t *testing.T) { + var received struct { + Phone string `json:"phone"` + Message string `json:"message"` + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, json.NewDecoder(r.Body).Decode(&received)) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + log := zerolog.Nop() + p, err := NewTestWebhookProvider(&config.Config{TestSMSWebhookURL: server.URL}, &Dependencies{Log: &log}) + require.NoError(t, err) + + err = p.SendSMS("+15551234567", "your code is 123456") + require.NoError(t, err) + assert.Equal(t, "+15551234567", received.Phone) + assert.Equal(t, "your code is 123456", received.Message) +} From c1e38fce1138e33b94d1046a529c6172a8f2cd75 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 20 Jul 2026 17:09:10 +0530 Subject: [PATCH 04/71] fix(config): treat TestSMSWebhookURL as sufficient for IsSMSServiceEnabled --- internal/config/config.go | 5 ++-- internal/config/config_finalize_test.go | 39 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 834d0c7d..80985332 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -436,10 +436,11 @@ func (c *Config) Finalize() { c.IsEmailServiceEnabled = strings.TrimSpace(c.SMTPHost) != "" && c.SMTPPort > 0 && strings.TrimSpace(c.SMTPSenderEmail) != "" - c.IsSMSServiceEnabled = strings.TrimSpace(c.TwilioAPIKey) != "" && + c.IsSMSServiceEnabled = (strings.TrimSpace(c.TwilioAPIKey) != "" && strings.TrimSpace(c.TwilioAPISecret) != "" && strings.TrimSpace(c.TwilioAccountSID) != "" && - strings.TrimSpace(c.TwilioSender) != "" + strings.TrimSpace(c.TwilioSender) != "") || + strings.TrimSpace(c.TestSMSWebhookURL) != "" // MFA methods are on by default; operators opt out via --disable-*. c.EnableTOTPLogin = !c.DisableTOTPLogin diff --git a/internal/config/config_finalize_test.go b/internal/config/config_finalize_test.go index b06bf49c..9b58eabc 100644 --- a/internal/config/config_finalize_test.go +++ b/internal/config/config_finalize_test.go @@ -135,3 +135,42 @@ func TestFinalizeMFADerivation(t *testing.T) { }) } } + +// TestFinalizeIsSMSServiceEnabled verifies IsSMSServiceEnabled is true when +// either Twilio is fully configured or TestSMSWebhookURL is set, and false +// when neither is set. +func TestFinalizeIsSMSServiceEnabled(t *testing.T) { + tests := []struct { + name string + setup func(*Config) + want bool + }{ + { + name: "neither Twilio nor test webhook configured -> false", + setup: func(c *Config) {}, + want: false, + }, + { + name: "Twilio configured, no test webhook -> true", + setup: withTwilio, + want: true, + }, + { + name: "test webhook configured, no Twilio -> true", + setup: func(c *Config) { c.TestSMSWebhookURL = "http://localhost:9999/sms" }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{} + tt.setup(c) + c.Finalize() + + if c.IsSMSServiceEnabled != tt.want { + t.Errorf("IsSMSServiceEnabled = %v, want %v", c.IsSMSServiceEnabled, tt.want) + } + }) + } +} From 2084e0fafe5654513bc35c7100d6efe71fba0743 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 20 Jul 2026 17:15:16 +0530 Subject: [PATCH 05/71] chore(e2e-playground): scaffold Playwright project and make target --- Makefile | 9 ++ e2e-playground/.gitignore | 3 + e2e-playground/package-lock.json | 127 ++++++++++++++++++++++++++++ e2e-playground/package.json | 15 ++++ e2e-playground/playwright.config.ts | 26 ++++++ e2e-playground/tsconfig.json | 12 +++ 6 files changed, 192 insertions(+) create mode 100644 e2e-playground/.gitignore create mode 100644 e2e-playground/package-lock.json create mode 100644 e2e-playground/package.json create mode 100644 e2e-playground/playwright.config.ts create mode 100644 e2e-playground/tsconfig.json diff --git a/Makefile b/Makefile index cdf0253a..14ce8e48 100644 --- a/Makefile +++ b/Makefile @@ -285,3 +285,12 @@ perf-k6-validate: .PHONY: perf-k6-check perf-k6-check: k6 run perf/k6/fga_check.js + +.PHONY: e2e-playground +e2e-playground: ## Run the live-playground e2e suite (OIDC/SAML/SCIM/SSO/OAuth/MFA) against an ephemeral docker-compose stack + cd e2e-playground && npm ci && npx playwright install --with-deps chromium + docker compose -f e2e-playground/docker-compose.yml up -d --wait + cd e2e-playground && npx playwright test; \ + status=$$?; \ + docker compose -f e2e-playground/docker-compose.yml down -v; \ + exit $$status diff --git a/e2e-playground/.gitignore b/e2e-playground/.gitignore new file mode 100644 index 00000000..945fcd0d --- /dev/null +++ b/e2e-playground/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +playwright-report/ +test-results/ diff --git a/e2e-playground/package-lock.json b/e2e-playground/package-lock.json new file mode 100644 index 00000000..101865f3 --- /dev/null +++ b/e2e-playground/package-lock.json @@ -0,0 +1,127 @@ +{ + "name": "e2e-playground", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e-playground", + "version": "1.0.0", + "dependencies": { + "graphql-request": "^7.1.2" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "typescript": "^5.6.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.4.0.tgz", + "integrity": "sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/e2e-playground/package.json b/e2e-playground/package.json new file mode 100644 index 00000000..7dc32e24 --- /dev/null +++ b/e2e-playground/package.json @@ -0,0 +1,15 @@ +{ + "name": "e2e-playground", + "private": true, + "version": "1.0.0", + "scripts": { + "test": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "typescript": "^5.6.0" + }, + "dependencies": { + "graphql-request": "^7.1.2" + } +} diff --git a/e2e-playground/playwright.config.ts b/e2e-playground/playwright.config.ts new file mode 100644 index 00000000..89e6b0f6 --- /dev/null +++ b/e2e-playground/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 30_000, + retries: 0, + reporter: [['html', { outputFolder: 'playwright-report', open: 'never' }]], + use: { + baseURL: process.env.AUTHORIZER_BASE_URL || 'http://localhost:8080', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'mfa-off', + testIgnore: /mfa-routing-matrix\.spec\.ts/, + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'mfa-on', + testMatch: /mfa-routing-matrix\.spec\.ts/, + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/e2e-playground/tsconfig.json b/e2e-playground/tsconfig.json new file mode 100644 index 00000000..669a28ac --- /dev/null +++ b/e2e-playground/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["tests/**/*.ts", "fixtures/**/*.ts", "mocks/**/*.ts"] +} From 9a209c500117dd60ab68df6683db90eebfeb05d8 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 20 Jul 2026 17:36:01 +0530 Subject: [PATCH 06/71] chore(e2e-playground): add mock-oauth, mock-saml-idp, sms-sink servers mock-oauth (4000) simulates all 10 social-login providers path-routed per provider, plus issuer/JWKS/discovery for the OIDC-verified subset (google/apple/microsoft/twitch), matching the route convention Task 2 wired TestOAuth*BaseURL to. mock-saml-idp (4001) deviates from the plan's assumed samlify usage in two ways verified against the installed samlify build (2.13.1): - IdentityProvider() now requires singleSignOnService in its settings or construction throws ERR_IDP_METADATA_MISSING_SINGLE_SIGN_ON_SERVICE. - createLoginResponse's non-customTagReplacement path unconditionally renders an empty AttributeStatement, so loginResponseTemplate.attributes alone never appears in the response. Authorizer's real SP (crewjam/saml) reads email/given_name/family_name out of the AttributeStatement by attribute name, so /sso now parses the inbound AuthnRequest via parseLoginRequest to get the real ACS URL and SP entity ID (rather than trusting a made-up acs_url query param Authorizer never sends), and builds the response through a customTagReplacement callback that fills in the attribute tags samlify's own tvalue path drops. sms-sink (4100) is a plain in-memory store-and-retrieve server for Task 3's TestWebhookProvider. Test-only SAML cert/key generated by gen-certs.sh are checked in per the plan's Global Constraints; .gitignore's blanket certs/ rule gets narrow negations for the two e2e-playground cert paths so `git add` can actually pick them up. --- .gitignore | 3 + e2e-playground/fixtures/certs/gen-certs.sh | 7 + e2e-playground/fixtures/certs/idp-cert.pem | 20 + e2e-playground/fixtures/certs/idp-key.pem | 28 + e2e-playground/mocks/mock-oauth/Dockerfile | 7 + .../mocks/mock-oauth/package-lock.json | 1170 +++++++++++++++ e2e-playground/mocks/mock-oauth/package.json | 16 + .../mocks/mock-oauth/server.test.ts | 37 + e2e-playground/mocks/mock-oauth/server.ts | 127 ++ e2e-playground/mocks/mock-oauth/tsconfig.json | 12 + e2e-playground/mocks/mock-saml-idp/Dockerfile | 8 + .../mocks/mock-saml-idp/certs/idp-cert.pem | 20 + .../mocks/mock-saml-idp/certs/idp-key.pem | 28 + .../mocks/mock-saml-idp/package-lock.json | 1279 +++++++++++++++++ .../mocks/mock-saml-idp/package.json | 16 + e2e-playground/mocks/mock-saml-idp/server.ts | 121 ++ .../mocks/mock-saml-idp/tsconfig.json | 12 + e2e-playground/mocks/sms-sink/Dockerfile | 7 + .../mocks/sms-sink/package-lock.json | 1160 +++++++++++++++ e2e-playground/mocks/sms-sink/package.json | 8 + e2e-playground/mocks/sms-sink/server.ts | 23 + e2e-playground/mocks/sms-sink/tsconfig.json | 12 + 22 files changed, 4121 insertions(+) create mode 100755 e2e-playground/fixtures/certs/gen-certs.sh create mode 100644 e2e-playground/fixtures/certs/idp-cert.pem create mode 100644 e2e-playground/fixtures/certs/idp-key.pem create mode 100644 e2e-playground/mocks/mock-oauth/Dockerfile create mode 100644 e2e-playground/mocks/mock-oauth/package-lock.json create mode 100644 e2e-playground/mocks/mock-oauth/package.json create mode 100644 e2e-playground/mocks/mock-oauth/server.test.ts create mode 100644 e2e-playground/mocks/mock-oauth/server.ts create mode 100644 e2e-playground/mocks/mock-oauth/tsconfig.json create mode 100644 e2e-playground/mocks/mock-saml-idp/Dockerfile create mode 100644 e2e-playground/mocks/mock-saml-idp/certs/idp-cert.pem create mode 100644 e2e-playground/mocks/mock-saml-idp/certs/idp-key.pem create mode 100644 e2e-playground/mocks/mock-saml-idp/package-lock.json create mode 100644 e2e-playground/mocks/mock-saml-idp/package.json create mode 100644 e2e-playground/mocks/mock-saml-idp/server.ts create mode 100644 e2e-playground/mocks/mock-saml-idp/tsconfig.json create mode 100644 e2e-playground/mocks/sms-sink/Dockerfile create mode 100644 e2e-playground/mocks/sms-sink/package-lock.json create mode 100644 e2e-playground/mocks/sms-sink/package.json create mode 100644 e2e-playground/mocks/sms-sink/server.ts create mode 100644 e2e-playground/mocks/sms-sink/tsconfig.json diff --git a/.gitignore b/.gitignore index 7b2d662a..2f482072 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ test.db .yalc yalc.lock certs/ +# Test-only e2e-playground fixtures are intentionally checked in (docs/superpowers/plans/2026-07-20-playground-e2e.md Global Constraints). +!e2e-playground/fixtures/certs/ +!e2e-playground/mocks/mock-saml-idp/certs/ *-shm *-wal .idea diff --git a/e2e-playground/fixtures/certs/gen-certs.sh b/e2e-playground/fixtures/certs/gen-certs.sh new file mode 100755 index 00000000..987d8ea5 --- /dev/null +++ b/e2e-playground/fixtures/certs/gen-certs.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +openssl req -x509 -newkey rsa:2048 -keyout idp-key.pem -out idp-cert.pem \ + -days 3650 -nodes -subj "/CN=mock-saml-idp.e2e-playground.local" +cp idp-cert.pem idp-key.pem ../../mocks/mock-saml-idp/certs/ +echo "Generated test-only SAML IdP cert/key (not used for anything real)." diff --git a/e2e-playground/fixtures/certs/idp-cert.pem b/e2e-playground/fixtures/certs/idp-cert.pem new file mode 100644 index 00000000..8625b2bd --- /dev/null +++ b/e2e-playground/fixtures/certs/idp-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOzCCAiOgAwIBAgIUEzReCg9zKNAJllQcrba+TiM3ibkwDQYJKoZIhvcNAQEL +BQAwLTErMCkGA1UEAwwibW9jay1zYW1sLWlkcC5lMmUtcGxheWdyb3VuZC5sb2Nh +bDAeFw0yNjA3MjAxMjAzMzBaFw0zNjA3MTcxMjAzMzBaMC0xKzApBgNVBAMMIm1v +Y2stc2FtbC1pZHAuZTJlLXBsYXlncm91bmQubG9jYWwwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCfVSYARVlvp42jwuyQN76TYuqPd2s76nIv0Ws2WMLb +Q51RfG7F37akGWFX5RJP6Y9G0Rx28Apr41Oz8AXs22yzkI1f6rzVXesRUK4ictPJ +oVdoXNDPJyLHDoEyWBgzl0ca4I0EQn4ff3RstPQDZcLHd99PJPSQuDb0C2Cvl6oW +Cgx9oSRlQC7acNkcfbtE1fQVmbq2lPCadHuwnyEb3WbUsCxtw1E/lczLPfTv5E1E +5kGK0kSGGiNzi8RdANeZd43QljKDTentfp5EvnCr5fKFC95NX78YxcaTy0MovDFm +kCAwE8BTwgaKHp7iO4gvUIZwgfmdehLCAWf7B2yc0NlHAgMBAAGjUzBRMB0GA1Ud +DgQWBBS1ZxVMxDDcKi8ksIH/5r2jnbf74zAfBgNVHSMEGDAWgBS1ZxVMxDDcKi8k +sIH/5r2jnbf74zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBz +5i7VlC/SC9tM2Km+1R+n2DVP4aA75ShlgeBAgBUr8dVzjjksiN4hHp44p2J6bpf6 +GopbqKc0SJKy5dfwfzi7nzJQX3TvdqGY2D2LqYKqdH5GcrzOXKgCzjkgLCCQsAs0 +T5qst8dZWoxUW1BkP338Z2SqE1v6Y9SmBzXapXttRs8GhMLemiWbf6ni8UCIj94T +v/VD/mVCvCBfYMVVUaFDlIBjK+2O2mBRV31+acFoMcnCHh3u8FXAjmrosmhu/6JW +7c3qCDcXHsO+sA4gOW0Pw7gixUsO2VpH/ERXjZrpxxZvK2cZ2uuCRwPEhO2bIZwr +0GWE5nn4ZOPgeyqUHgfB +-----END CERTIFICATE----- diff --git a/e2e-playground/fixtures/certs/idp-key.pem b/e2e-playground/fixtures/certs/idp-key.pem new file mode 100644 index 00000000..33ae9451 --- /dev/null +++ b/e2e-playground/fixtures/certs/idp-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCfVSYARVlvp42j +wuyQN76TYuqPd2s76nIv0Ws2WMLbQ51RfG7F37akGWFX5RJP6Y9G0Rx28Apr41Oz +8AXs22yzkI1f6rzVXesRUK4ictPJoVdoXNDPJyLHDoEyWBgzl0ca4I0EQn4ff3Rs +tPQDZcLHd99PJPSQuDb0C2Cvl6oWCgx9oSRlQC7acNkcfbtE1fQVmbq2lPCadHuw +nyEb3WbUsCxtw1E/lczLPfTv5E1E5kGK0kSGGiNzi8RdANeZd43QljKDTentfp5E +vnCr5fKFC95NX78YxcaTy0MovDFmkCAwE8BTwgaKHp7iO4gvUIZwgfmdehLCAWf7 +B2yc0NlHAgMBAAECggEAMHBegS6UJhG2Sdb0vFN2KLClxr/zZd+8nzT3dyo88xP3 +O9KsipOcnv7oTSRjENBcspbYJICNaodN5yJNati6j0ye7k4a4nMPB9CTX/2wzVez +jxLImHG5bPLH7FpD4UtYXp9tv6HHXiQNbQ8GMBI7yRB0X0dI4sZeTos29asSFmi0 +ZW9yugGJ/9RhLc36g85uS4aJLR6wDF9O1BjQ9BsuSD32alQpGleyaZQ9FA5lgyDR +lKc/q8oalKJJtRmnm3xmq2+xg5tPN2sH/tNZYuOJqJFyiEzQ8Z0atBoo3SUdMdLn +BeXFJkvQbLXwStQTap1LGzsz2qiEIq/TMpgTnwLBYQKBgQDcfVO85zZYl1C+IKjn +IKTbJYzKZEXjgYb8FCM2UdBDAVgzioDheFaZ7w28ercZlxaYZ2KJtSLxIeboh/Ui +Sgg1vTRVuc3BHO3rHXm4topGICSzAxZ80wjav3B2N41fcIIBoMbfaYiu6/JQaoPn +4MqWqwC94sMMB8SF2Pzd70y9cwKBgQC4/lnxUNvJZVL5Sq69m3fXfIDTfkDnBolW +3Mi9WA4IzI5qxqaP3kL+AmR0liCxUiS19+TbAIjsVbFrpOWDL2ph5PEj7V0somIk +qWjl7bA2LwA1zcODJ0kfntdRp1qUMpj6hXk+teQlMTxuh0kLyofrPfIb8XI5NTov +wpwHasY/3QKBgGxpsyLPDQnCXREfPe1nP6gBbpiVdUfICHcp76Zl0+EeaB/vmi9C +3FIUGMz0CdOrVpDZRLoxNl0aLk9nikCx5heGUJVWJrUtZE6Wz6LjHlocs+7RNd1q +ZpAoUUPPTNQAnevvAdoYKfzYRu0DcpgxD2vF6Td0qDLiHt8xMiRt5W3BAoGAflH4 +esaa/f+5U88CWSijAbrbgQ9SJC8bcvvZ+yj4lFuR2CmDrPO5TRe3HsEw28RamwMF +++F2neK5/uYfbp/fBa++VakMmaDcYWpo3bCbRbR8cUDrA1C9JuFg6DndqRqPyWmA +7Chp/FeNi2/HmkyW2TR4cUpCk/vbmqdJwerQKuUCgYAXXmhEhthpYRDE9LgOBhXa +Ajv8qQj4DZq9fzwwrru3Y+JRoHcDjkENeW/KTEsXkWdtINldk7I9uXrLpbuwzJwo +X29ZOTCjfFMRH7pThOlQBvYgv0ImGhH2G546dBLQ9AgiwhEWAiBNHmPq9GTXY4tw +xSTYrGCQvuDpmd5S4lPmPQ== +-----END PRIVATE KEY----- diff --git a/e2e-playground/mocks/mock-oauth/Dockerfile b/e2e-playground/mocks/mock-oauth/Dockerfile new file mode 100644 index 00000000..a35a05a8 --- /dev/null +++ b/e2e-playground/mocks/mock-oauth/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY server.ts ./ +EXPOSE 4000 +CMD ["npm", "start"] diff --git a/e2e-playground/mocks/mock-oauth/package-lock.json b/e2e-playground/mocks/mock-oauth/package-lock.json new file mode 100644 index 00000000..b3fda66b --- /dev/null +++ b/e2e-playground/mocks/mock-oauth/package-lock.json @@ -0,0 +1,1170 @@ +{ + "name": "mock-oauth", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mock-oauth", + "version": "1.0.0", + "dependencies": { + "express": "^4.21.0", + "jose": "^5.9.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.7.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/e2e-playground/mocks/mock-oauth/package.json b/e2e-playground/mocks/mock-oauth/package.json new file mode 100644 index 00000000..f751722c --- /dev/null +++ b/e2e-playground/mocks/mock-oauth/package.json @@ -0,0 +1,16 @@ +{ + "name": "mock-oauth", + "private": true, + "version": "1.0.0", + "scripts": { "start": "ts-node server.ts", "test": "node --test -r ts-node/register server.test.ts" }, + "dependencies": { + "express": "^4.21.0", + "jose": "^5.9.0" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.6.0", + "@types/express": "^4.17.21", + "@types/node": "^22.7.0" + } +} diff --git a/e2e-playground/mocks/mock-oauth/server.test.ts b/e2e-playground/mocks/mock-oauth/server.test.ts new file mode 100644 index 00000000..9a17c850 --- /dev/null +++ b/e2e-playground/mocks/mock-oauth/server.test.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert'; +import { test } from 'node:test'; +import { app } from './server'; +import type { Server } from 'node:http'; + +let server: Server; + +test.before(() => { + server = app.listen(0); +}); +test.after(() => server.close()); + +test('token endpoint issues an access token, userinfo returns configured profile', async () => { + const addr = server.address(); + const base = `http://127.0.0.1:${typeof addr === 'object' && addr ? addr.port : 0}`; + + await fetch(`${base}/github/__configure`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ profile: { name: 'Ada Lovelace', email: 'ada@example.com', avatar_url: 'https://example.com/a.png' } }), + }); + + const tokenRes = await fetch(`${base}/github/token`, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=authorization_code&code=any-code', + }); + assert.strictEqual(tokenRes.status, 200); + const { access_token } = await tokenRes.json(); + assert.ok(access_token); + + const userRes = await fetch(`${base}/github/userinfo`, { headers: { authorization: `Bearer ${access_token}` } }); + assert.strictEqual(userRes.status, 200); + const profile = await userRes.json(); + assert.strictEqual(profile.name, 'Ada Lovelace'); + assert.strictEqual(profile.email, 'ada@example.com'); +}); diff --git a/e2e-playground/mocks/mock-oauth/server.ts b/e2e-playground/mocks/mock-oauth/server.ts new file mode 100644 index 00000000..b73a9698 --- /dev/null +++ b/e2e-playground/mocks/mock-oauth/server.ts @@ -0,0 +1,127 @@ +import express from 'express'; +import * as jose from 'jose'; +import crypto from 'node:crypto'; + +export const app = express(); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// In-memory per-provider state: current profile to return, and issued tokens. +const profiles: Record> = {}; +const issuedTokens = new Map(); + +// OIDC-verified providers (Task 2's route convention): Google, Apple, Microsoft, Twitch. +const OIDC_PROVIDERS = new Set(['google', 'apple', 'microsoft', 'twitch']); + +let signingKey: jose.KeyLike; +let publicJwk: jose.JWK; +const keyReady = jose.generateKeyPair('RS256').then(async ({ privateKey, publicKey }) => { + signingKey = privateKey; + publicJwk = await jose.exportJWK(publicKey); + publicJwk.kid = 'mock-oauth-key-1'; + publicJwk.alg = 'RS256'; + publicJwk.use = 'sig'; +}); + +function defaultProfile(provider: string): Record { + const email = `mock-user@${provider}.example.com`; + switch (provider) { + case 'github': + return { name: 'Mock User', email, avatar_url: 'https://example.com/avatar.png' }; + case 'facebook': + return { first_name: 'Mock', last_name: 'User', email, picture: { data: { url: 'https://example.com/avatar.png' } } }; + case 'linkedin': + return { localizedFirstName: 'Mock', localizedLastName: 'User' }; + case 'discord': + return { user: { id: '123', username: 'mockuser', avatar: 'abc' } }; + case 'twitter': + return { data: { id: '123', name: 'Mock User', username: 'mockuser', profile_image_url: 'https://example.com/a.png' } }; + case 'roblox': + return { name: 'Mock User', nickname: 'mockuser', picture: 'https://example.com/a.png', email }; + default: + return { sub: `mock-${provider}-sub`, email, given_name: 'Mock', family_name: 'User' }; + } +} + +app.post('/:provider/__configure', (req, res) => { + profiles[req.params.provider] = req.body.profile; + res.sendStatus(204); +}); + +app.get('/:provider/.well-known/openid-configuration', (req, res) => { + const base = `${req.protocol}://${req.get('host')}/${req.params.provider}`; + res.json({ + issuer: base, + authorization_endpoint: `${base}/authorize`, + token_endpoint: `${base}/token`, + jwks_uri: `${base}/jwks`, + userinfo_endpoint: `${base}/userinfo`, + response_types_supported: ['code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + }); +}); + +app.get('/:provider/jwks', async (_req, res) => { + await keyReady; + res.json({ keys: [publicJwk] }); +}); + +app.all('/:provider/authorize', (req, res) => { + const redirectUri = String(req.query.redirect_uri); + const state = String(req.query.state || ''); + const code = crypto.randomUUID(); + issuedTokens.set(code, { provider: req.params.provider }); + const url = new URL(redirectUri); + url.searchParams.set('code', code); + if (state) url.searchParams.set('state', state); + res.redirect(302, url.toString()); +}); + +app.post('/:provider/token', async (req, res) => { + const provider = req.params.provider; + const accessToken = crypto.randomUUID(); + issuedTokens.set(accessToken, { provider }); + + const body: Record = { + access_token: accessToken, + token_type: 'bearer', + expires_in: 3600, + }; + + if (OIDC_PROVIDERS.has(provider)) { + await keyReady; + const base = `${req.protocol}://${req.get('host')}/${provider}`; + const profile = profiles[provider] || defaultProfile(provider); + const idToken = await new jose.SignJWT({ ...profile }) + .setProtectedHeader({ alg: 'RS256', kid: 'mock-oauth-key-1' }) + .setIssuer(base) + .setAudience('mock-client-id') + .setSubject(String((profile as Record).sub || `mock-${provider}-sub`)) + .setIssuedAt() + .setExpirationTime('10m') + .sign(signingKey); + body.id_token = idToken; + } + + res.json(body); +}); + +app.get(['/:provider/userinfo', '/:provider/user', '/:provider/@me', '/:provider/2/users/me'], (req, res) => { + const provider = req.params.provider; + res.json(profiles[provider] || defaultProfile(provider)); +}); + +app.get('/:provider/user/emails', (req, res) => { + const profile = (profiles[req.params.provider] || defaultProfile(req.params.provider)) as { email?: string }; + res.json([{ email: profile.email || 'mock-user@github.example.com', primary: true }]); +}); + +app.get('/:provider/emailAddress', (req, res) => { + const profile = (profiles[req.params.provider] || defaultProfile(req.params.provider)) as { email?: string }; + res.json({ elements: [{ 'handle~': { emailAddress: profile.email || 'mock-user@linkedin.example.com' } }] }); +}); + +if (require.main === module) { + app.listen(4000, () => console.log('mock-oauth listening on :4000')); +} diff --git a/e2e-playground/mocks/mock-oauth/tsconfig.json b/e2e-playground/mocks/mock-oauth/tsconfig.json new file mode 100644 index 00000000..82d61996 --- /dev/null +++ b/e2e-playground/mocks/mock-oauth/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"] + } +} diff --git a/e2e-playground/mocks/mock-saml-idp/Dockerfile b/e2e-playground/mocks/mock-saml-idp/Dockerfile new file mode 100644 index 00000000..959d0bdd --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY server.ts ./ +COPY certs ./certs +EXPOSE 4001 +CMD ["npm", "start"] diff --git a/e2e-playground/mocks/mock-saml-idp/certs/idp-cert.pem b/e2e-playground/mocks/mock-saml-idp/certs/idp-cert.pem new file mode 100644 index 00000000..8625b2bd --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/certs/idp-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOzCCAiOgAwIBAgIUEzReCg9zKNAJllQcrba+TiM3ibkwDQYJKoZIhvcNAQEL +BQAwLTErMCkGA1UEAwwibW9jay1zYW1sLWlkcC5lMmUtcGxheWdyb3VuZC5sb2Nh +bDAeFw0yNjA3MjAxMjAzMzBaFw0zNjA3MTcxMjAzMzBaMC0xKzApBgNVBAMMIm1v +Y2stc2FtbC1pZHAuZTJlLXBsYXlncm91bmQubG9jYWwwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCfVSYARVlvp42jwuyQN76TYuqPd2s76nIv0Ws2WMLb +Q51RfG7F37akGWFX5RJP6Y9G0Rx28Apr41Oz8AXs22yzkI1f6rzVXesRUK4ictPJ +oVdoXNDPJyLHDoEyWBgzl0ca4I0EQn4ff3RstPQDZcLHd99PJPSQuDb0C2Cvl6oW +Cgx9oSRlQC7acNkcfbtE1fQVmbq2lPCadHuwnyEb3WbUsCxtw1E/lczLPfTv5E1E +5kGK0kSGGiNzi8RdANeZd43QljKDTentfp5EvnCr5fKFC95NX78YxcaTy0MovDFm +kCAwE8BTwgaKHp7iO4gvUIZwgfmdehLCAWf7B2yc0NlHAgMBAAGjUzBRMB0GA1Ud +DgQWBBS1ZxVMxDDcKi8ksIH/5r2jnbf74zAfBgNVHSMEGDAWgBS1ZxVMxDDcKi8k +sIH/5r2jnbf74zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBz +5i7VlC/SC9tM2Km+1R+n2DVP4aA75ShlgeBAgBUr8dVzjjksiN4hHp44p2J6bpf6 +GopbqKc0SJKy5dfwfzi7nzJQX3TvdqGY2D2LqYKqdH5GcrzOXKgCzjkgLCCQsAs0 +T5qst8dZWoxUW1BkP338Z2SqE1v6Y9SmBzXapXttRs8GhMLemiWbf6ni8UCIj94T +v/VD/mVCvCBfYMVVUaFDlIBjK+2O2mBRV31+acFoMcnCHh3u8FXAjmrosmhu/6JW +7c3qCDcXHsO+sA4gOW0Pw7gixUsO2VpH/ERXjZrpxxZvK2cZ2uuCRwPEhO2bIZwr +0GWE5nn4ZOPgeyqUHgfB +-----END CERTIFICATE----- diff --git a/e2e-playground/mocks/mock-saml-idp/certs/idp-key.pem b/e2e-playground/mocks/mock-saml-idp/certs/idp-key.pem new file mode 100644 index 00000000..33ae9451 --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/certs/idp-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCfVSYARVlvp42j +wuyQN76TYuqPd2s76nIv0Ws2WMLbQ51RfG7F37akGWFX5RJP6Y9G0Rx28Apr41Oz +8AXs22yzkI1f6rzVXesRUK4ictPJoVdoXNDPJyLHDoEyWBgzl0ca4I0EQn4ff3Rs +tPQDZcLHd99PJPSQuDb0C2Cvl6oWCgx9oSRlQC7acNkcfbtE1fQVmbq2lPCadHuw +nyEb3WbUsCxtw1E/lczLPfTv5E1E5kGK0kSGGiNzi8RdANeZd43QljKDTentfp5E +vnCr5fKFC95NX78YxcaTy0MovDFmkCAwE8BTwgaKHp7iO4gvUIZwgfmdehLCAWf7 +B2yc0NlHAgMBAAECggEAMHBegS6UJhG2Sdb0vFN2KLClxr/zZd+8nzT3dyo88xP3 +O9KsipOcnv7oTSRjENBcspbYJICNaodN5yJNati6j0ye7k4a4nMPB9CTX/2wzVez +jxLImHG5bPLH7FpD4UtYXp9tv6HHXiQNbQ8GMBI7yRB0X0dI4sZeTos29asSFmi0 +ZW9yugGJ/9RhLc36g85uS4aJLR6wDF9O1BjQ9BsuSD32alQpGleyaZQ9FA5lgyDR +lKc/q8oalKJJtRmnm3xmq2+xg5tPN2sH/tNZYuOJqJFyiEzQ8Z0atBoo3SUdMdLn +BeXFJkvQbLXwStQTap1LGzsz2qiEIq/TMpgTnwLBYQKBgQDcfVO85zZYl1C+IKjn +IKTbJYzKZEXjgYb8FCM2UdBDAVgzioDheFaZ7w28ercZlxaYZ2KJtSLxIeboh/Ui +Sgg1vTRVuc3BHO3rHXm4topGICSzAxZ80wjav3B2N41fcIIBoMbfaYiu6/JQaoPn +4MqWqwC94sMMB8SF2Pzd70y9cwKBgQC4/lnxUNvJZVL5Sq69m3fXfIDTfkDnBolW +3Mi9WA4IzI5qxqaP3kL+AmR0liCxUiS19+TbAIjsVbFrpOWDL2ph5PEj7V0somIk +qWjl7bA2LwA1zcODJ0kfntdRp1qUMpj6hXk+teQlMTxuh0kLyofrPfIb8XI5NTov +wpwHasY/3QKBgGxpsyLPDQnCXREfPe1nP6gBbpiVdUfICHcp76Zl0+EeaB/vmi9C +3FIUGMz0CdOrVpDZRLoxNl0aLk9nikCx5heGUJVWJrUtZE6Wz6LjHlocs+7RNd1q +ZpAoUUPPTNQAnevvAdoYKfzYRu0DcpgxD2vF6Td0qDLiHt8xMiRt5W3BAoGAflH4 +esaa/f+5U88CWSijAbrbgQ9SJC8bcvvZ+yj4lFuR2CmDrPO5TRe3HsEw28RamwMF +++F2neK5/uYfbp/fBa++VakMmaDcYWpo3bCbRbR8cUDrA1C9JuFg6DndqRqPyWmA +7Chp/FeNi2/HmkyW2TR4cUpCk/vbmqdJwerQKuUCgYAXXmhEhthpYRDE9LgOBhXa +Ajv8qQj4DZq9fzwwrru3Y+JRoHcDjkENeW/KTEsXkWdtINldk7I9uXrLpbuwzJwo +X29ZOTCjfFMRH7pThOlQBvYgv0ImGhH2G546dBLQ9AgiwhEWAiBNHmPq9GTXY4tw +xSTYrGCQvuDpmd5S4lPmPQ== +-----END PRIVATE KEY----- diff --git a/e2e-playground/mocks/mock-saml-idp/package-lock.json b/e2e-playground/mocks/mock-saml-idp/package-lock.json new file mode 100644 index 00000000..3c8ae42b --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/package-lock.json @@ -0,0 +1,1279 @@ +{ + "name": "mock-saml-idp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mock-saml-idp", + "version": "1.0.0", + "dependencies": { + "express": "^4.21.0", + "samlify": "^2.9.1" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.7.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.0" + } + }, + "node_modules/@authenio/xml-encryption": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@authenio/xml-encryption/-/xml-encryption-2.0.2.tgz", + "integrity": "sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "escape-html": "^1.0.3", + "xpath": "0.0.32" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@authenio/xml-encryption/node_modules/xpath": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", + "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@xmldom/is-dom-node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz", + "integrity": "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "license": "MIT", + "dependencies": { + "asn1": "^0.2.4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/samlify": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/samlify/-/samlify-2.13.1.tgz", + "integrity": "sha512-vdYr/zohDGBbfWNU4miEzc1jmWOtkLySPViapC6nfGkv9KxzLq4UlGkKyryzwLw4jVlZk88Rw93HaCRVpe+t+g==", + "license": "MIT", + "dependencies": { + "@authenio/xml-encryption": "^2.0.2", + "@xmldom/xmldom": "^0.8.11", + "node-rsa": "^1.1.1", + "xml": "^1.0.1", + "xml-crypto": "^6.1.2", + "xml-escape": "^1.1.0", + "xpath": "^0.0.34" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-crypto": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.1.2.tgz", + "integrity": "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==", + "license": "MIT", + "dependencies": { + "@xmldom/is-dom-node": "^1.0.1", + "@xmldom/xmldom": "^0.8.10", + "xpath": "^0.0.33" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/xml-crypto/node_modules/xpath": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz", + "integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==", + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/xml-escape": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.1.0.tgz", + "integrity": "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==", + "license": "MIT License" + }, + "node_modules/xpath": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/e2e-playground/mocks/mock-saml-idp/package.json b/e2e-playground/mocks/mock-saml-idp/package.json new file mode 100644 index 00000000..f48ed775 --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/package.json @@ -0,0 +1,16 @@ +{ + "name": "mock-saml-idp", + "private": true, + "version": "1.0.0", + "scripts": { "start": "ts-node server.ts" }, + "dependencies": { + "express": "^4.21.0", + "samlify": "^2.9.1" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.6.0", + "@types/express": "^4.17.21", + "@types/node": "^22.7.0" + } +} diff --git a/e2e-playground/mocks/mock-saml-idp/server.ts b/e2e-playground/mocks/mock-saml-idp/server.ts new file mode 100644 index 00000000..e918a214 --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/server.ts @@ -0,0 +1,121 @@ +import express from 'express'; +import * as samlify from 'samlify'; +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +const app = express(); +app.use(express.urlencoded({ extended: true })); + +// ponytail: mock IdP only ever builds its own responses (never validates +// inbound XML against an XSD), so a real schema validator dependency isn't +// worth adding — skip validation. samlify throws on any parse call without +// one configured, so this is required, not optional. +samlify.setSchemaValidator({ validate: () => Promise.resolve('skipped') }); + +const CERT_DIR = path.join(__dirname, 'certs'); +const idpCert = fs.readFileSync(path.join(CERT_DIR, 'idp-cert.pem'), 'utf8'); +const idpKey = fs.readFileSync(path.join(CERT_DIR, 'idp-key.pem'), 'utf8'); + +const IDP_ENTITY_ID = 'http://mock-saml-idp:4001/metadata'; +const SSO_URL = 'http://mock-saml-idp:4001/sso'; + +// SAML attribute names must match Authorizer's default SP attribute mapping +// (internal/http_handlers/saml_sp.go: samlDefaultAttributeMapping) so the +// JIT-provisioned profile picks up email/given_name/family_name. +const idp = samlify.IdentityProvider({ + entityID: IDP_ENTITY_ID, + privateKey: idpKey, + isAssertionEncrypted: false, + signingCert: idpCert, + wantAuthnRequestsSigned: false, + singleSignOnService: [{ Binding: samlify.Constants.namespace.binding.redirect, Location: SSO_URL }], + loginResponseTemplate: { + context: samlify.SamlLib.defaultLoginResponseTemplate.context, + attributes: [ + { name: 'email', valueTag: 'email', nameFormat: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', valueXsiType: 'xs:string' }, + { name: 'firstName', valueTag: 'firstName', nameFormat: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', valueXsiType: 'xs:string' }, + { name: 'lastName', valueTag: 'lastName', nameFormat: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', valueXsiType: 'xs:string' }, + ], + }, +}); + +// Test-configurable subject for the next issued assertion. +let nextUser = { email: 'mock-saml-user@example.com', givenName: 'Mock', familyName: 'User' }; + +app.post('/__configure', express.json(), (req, res) => { + nextUser = req.body; + res.sendStatus(204); +}); + +app.get('/metadata', (_req, res) => { + res.type('application/samlmetadata+xml').send(idp.getMetadata()); +}); + +// SP-initiated: Authorizer's crewjam/saml SP redirects here with a +// base64+deflated AuthnRequest (HTTP-Redirect binding) plus RelayState. +// The real ACS URL and SP entity ID are parsed out of the AuthnRequest +// itself (samlify's parseLoginRequest) rather than trusted from a query +// param — Authorizer never sends one, and the assertion's Audience must +// match the SP's real entityID or the SP-side validation rejects it. +app.get('/sso', async (req, res) => { + const relayState = String(req.query.RelayState || ''); + const placeholderSp = samlify.ServiceProvider({ entityID: 'unused-during-parse' }); + // samlify's own `RequestInfo` and `FlowResult` types aren't exported from + // the package, so the boundary between parseLoginRequest's return value + // and createLoginResponse's expected input is untyped here. + const requestInfo: any = await idp.parseLoginRequest(placeholderSp, 'redirect', { query: req.query }); + const acsUrl = String((requestInfo.extract.request as { assertionConsumerServiceUrl?: string }).assertionConsumerServiceUrl); + const spEntityId = String(requestInfo.extract.issuer); + + const sp = samlify.ServiceProvider({ + entityID: spEntityId, + assertionConsumerService: [{ Binding: samlify.Constants.namespace.binding.post, Location: acsUrl }], + }); + + // samlify's default (non-custom) response builder always renders an empty + // AttributeStatement — the `loginResponseTemplate.attributes` config above + // only takes effect when a customTagReplacement callback fills the baked + // `{attrX}` placeholders itself. We replicate the same tag values samlify + // computes internally (see samlify/build/src/binding-post.js) plus ours. + const customTagReplacement = (template: string) => { + const now = new Date(); + const notOnOrAfter = new Date(now.getTime() + 5 * 60 * 1000).toISOString(); + const tvalue: Record = { + ID: `_${crypto.randomUUID()}`, + AssertionID: `_${crypto.randomUUID()}`, + Destination: acsUrl, + Audience: spEntityId, + EntityID: spEntityId, + SubjectRecipient: acsUrl, + Issuer: IDP_ENTITY_ID, + IssueInstant: now.toISOString(), + AssertionConsumerServiceURL: acsUrl, + StatusCode: samlify.Constants.StatusCode.Success, + ConditionsNotBefore: now.toISOString(), + ConditionsNotOnOrAfter: notOnOrAfter, + SubjectConfirmationDataNotOnOrAfter: notOnOrAfter, + NameIDFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', + NameID: nextUser.email, + InResponseTo: (requestInfo.extract.request as { id?: string }).id ?? '', + AuthnStatement: '', + attrEmail: nextUser.email, + attrFirstName: nextUser.givenName, + attrLastName: nextUser.familyName, + }; + return { id: tvalue.ID as string, context: samlify.SamlLib.replaceTagsByValue(template, tvalue) }; + }; + + const { context } = await idp.createLoginResponse(sp, requestInfo, 'post', nextUser, customTagReplacement); + + res.send(` + +
+ + +
+ + `); +}); + +app.listen(4001, () => console.log('mock-saml-idp listening on :4001')); diff --git a/e2e-playground/mocks/mock-saml-idp/tsconfig.json b/e2e-playground/mocks/mock-saml-idp/tsconfig.json new file mode 100644 index 00000000..82d61996 --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"] + } +} diff --git a/e2e-playground/mocks/sms-sink/Dockerfile b/e2e-playground/mocks/sms-sink/Dockerfile new file mode 100644 index 00000000..ad3234d7 --- /dev/null +++ b/e2e-playground/mocks/sms-sink/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY server.ts ./ +EXPOSE 4100 +CMD ["npm", "start"] diff --git a/e2e-playground/mocks/sms-sink/package-lock.json b/e2e-playground/mocks/sms-sink/package-lock.json new file mode 100644 index 00000000..0e214881 --- /dev/null +++ b/e2e-playground/mocks/sms-sink/package-lock.json @@ -0,0 +1,1160 @@ +{ + "name": "sms-sink", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sms-sink", + "version": "1.0.0", + "dependencies": { + "express": "^4.21.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.7.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/e2e-playground/mocks/sms-sink/package.json b/e2e-playground/mocks/sms-sink/package.json new file mode 100644 index 00000000..b480e243 --- /dev/null +++ b/e2e-playground/mocks/sms-sink/package.json @@ -0,0 +1,8 @@ +{ + "name": "sms-sink", + "private": true, + "version": "1.0.0", + "scripts": { "start": "ts-node server.ts" }, + "dependencies": { "express": "^4.21.0" }, + "devDependencies": { "ts-node": "^10.9.2", "typescript": "^5.6.0", "@types/express": "^4.17.21", "@types/node": "^22.7.0" } +} diff --git a/e2e-playground/mocks/sms-sink/server.ts b/e2e-playground/mocks/sms-sink/server.ts new file mode 100644 index 00000000..63b0d2f6 --- /dev/null +++ b/e2e-playground/mocks/sms-sink/server.ts @@ -0,0 +1,23 @@ +import express from 'express'; + +const app = express(); +app.use(express.json()); + +const latestByPhone = new Map(); + +app.post('/sms', (req, res) => { + const { phone, message } = req.body; + latestByPhone.set(phone, { phone, message }); + res.sendStatus(204); +}); + +app.get('/sms/:phone', (req, res) => { + const entry = latestByPhone.get(req.params.phone); + if (!entry) { + res.sendStatus(404); + return; + } + res.json(entry); +}); + +app.listen(4100, () => console.log('sms-sink listening on :4100')); diff --git a/e2e-playground/mocks/sms-sink/tsconfig.json b/e2e-playground/mocks/sms-sink/tsconfig.json new file mode 100644 index 00000000..82d61996 --- /dev/null +++ b/e2e-playground/mocks/sms-sink/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"] + } +} From f3bbd06acf32a539581628d381c92e5ced93e7c5 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 21 Jul 2026 10:26:27 +0530 Subject: [PATCH 07/71] fix(e2e-playground): guard mock-saml-idp /sso against crashes, copy tsconfig into mock Docker images --- e2e-playground/mocks/mock-oauth/Dockerfile | 2 +- e2e-playground/mocks/mock-saml-idp/Dockerfile | 2 +- .../mocks/mock-saml-idp/package.json | 2 +- .../mocks/mock-saml-idp/server.test.ts | 24 ++++ e2e-playground/mocks/mock-saml-idp/server.ts | 120 ++++++++++-------- e2e-playground/mocks/sms-sink/Dockerfile | 2 +- 6 files changed, 94 insertions(+), 58 deletions(-) create mode 100644 e2e-playground/mocks/mock-saml-idp/server.test.ts diff --git a/e2e-playground/mocks/mock-oauth/Dockerfile b/e2e-playground/mocks/mock-oauth/Dockerfile index a35a05a8..0929e019 100644 --- a/e2e-playground/mocks/mock-oauth/Dockerfile +++ b/e2e-playground/mocks/mock-oauth/Dockerfile @@ -2,6 +2,6 @@ FROM node:22-alpine WORKDIR /app COPY package.json ./ RUN npm install -COPY server.ts ./ +COPY server.ts tsconfig.json ./ EXPOSE 4000 CMD ["npm", "start"] diff --git a/e2e-playground/mocks/mock-saml-idp/Dockerfile b/e2e-playground/mocks/mock-saml-idp/Dockerfile index 959d0bdd..71345e66 100644 --- a/e2e-playground/mocks/mock-saml-idp/Dockerfile +++ b/e2e-playground/mocks/mock-saml-idp/Dockerfile @@ -2,7 +2,7 @@ FROM node:22-alpine WORKDIR /app COPY package.json ./ RUN npm install -COPY server.ts ./ +COPY server.ts tsconfig.json ./ COPY certs ./certs EXPOSE 4001 CMD ["npm", "start"] diff --git a/e2e-playground/mocks/mock-saml-idp/package.json b/e2e-playground/mocks/mock-saml-idp/package.json index f48ed775..51970361 100644 --- a/e2e-playground/mocks/mock-saml-idp/package.json +++ b/e2e-playground/mocks/mock-saml-idp/package.json @@ -2,7 +2,7 @@ "name": "mock-saml-idp", "private": true, "version": "1.0.0", - "scripts": { "start": "ts-node server.ts" }, + "scripts": { "start": "ts-node server.ts", "test": "node --test -r ts-node/register server.test.ts" }, "dependencies": { "express": "^4.21.0", "samlify": "^2.9.1" diff --git a/e2e-playground/mocks/mock-saml-idp/server.test.ts b/e2e-playground/mocks/mock-saml-idp/server.test.ts new file mode 100644 index 00000000..3bd757a8 --- /dev/null +++ b/e2e-playground/mocks/mock-saml-idp/server.test.ts @@ -0,0 +1,24 @@ +import assert from 'node:assert'; +import { test } from 'node:test'; +import { app } from './server'; +import type { Server } from 'node:http'; + +let server: Server; + +test.before(() => { + server = app.listen(0); +}); +test.after(() => server.close()); + +test('/sso responds 400 (not a crash) on a malformed SAMLRequest', async () => { + const addr = server.address(); + const base = `http://127.0.0.1:${typeof addr === 'object' && addr ? addr.port : 0}`; + + const res = await fetch(`${base}/sso?SAMLRequest=not-valid-base64-deflated-xml&RelayState=test`); + assert.strictEqual(res.status, 400); + + // Process (and this same server instance) must still be alive and serving + // requests afterwards — a crash would make this fetch fail outright. + const metadataRes = await fetch(`${base}/metadata`); + assert.strictEqual(metadataRes.status, 200); +}); diff --git a/e2e-playground/mocks/mock-saml-idp/server.ts b/e2e-playground/mocks/mock-saml-idp/server.ts index e918a214..c6279622 100644 --- a/e2e-playground/mocks/mock-saml-idp/server.ts +++ b/e2e-playground/mocks/mock-saml-idp/server.ts @@ -4,7 +4,7 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; -const app = express(); +export const app = express(); app.use(express.urlencoded({ extended: true })); // ponytail: mock IdP only ever builds its own responses (never validates @@ -59,63 +59,75 @@ app.get('/metadata', (_req, res) => { // param — Authorizer never sends one, and the assertion's Audience must // match the SP's real entityID or the SP-side validation rejects it. app.get('/sso', async (req, res) => { - const relayState = String(req.query.RelayState || ''); - const placeholderSp = samlify.ServiceProvider({ entityID: 'unused-during-parse' }); - // samlify's own `RequestInfo` and `FlowResult` types aren't exported from - // the package, so the boundary between parseLoginRequest's return value - // and createLoginResponse's expected input is untyped here. - const requestInfo: any = await idp.parseLoginRequest(placeholderSp, 'redirect', { query: req.query }); - const acsUrl = String((requestInfo.extract.request as { assertionConsumerServiceUrl?: string }).assertionConsumerServiceUrl); - const spEntityId = String(requestInfo.extract.issuer); + // ponytail: Express 4's async-handler rejections don't reach the default + // error handler (that's Express 5 only) and become unhandled promise + // rejections, which crash this long-lived shared process. Guard the whole + // body so a malformed SAMLRequest 400s instead of taking down every other + // in-flight SAML spec. + try { + const relayState = String(req.query.RelayState || ''); + const placeholderSp = samlify.ServiceProvider({ entityID: 'unused-during-parse' }); + // samlify's own `RequestInfo` and `FlowResult` types aren't exported from + // the package, so the boundary between parseLoginRequest's return value + // and createLoginResponse's expected input is untyped here. + const requestInfo: any = await idp.parseLoginRequest(placeholderSp, 'redirect', { query: req.query }); + const acsUrl = String((requestInfo.extract.request as { assertionConsumerServiceUrl?: string }).assertionConsumerServiceUrl); + const spEntityId = String(requestInfo.extract.issuer); - const sp = samlify.ServiceProvider({ - entityID: spEntityId, - assertionConsumerService: [{ Binding: samlify.Constants.namespace.binding.post, Location: acsUrl }], - }); + const sp = samlify.ServiceProvider({ + entityID: spEntityId, + assertionConsumerService: [{ Binding: samlify.Constants.namespace.binding.post, Location: acsUrl }], + }); - // samlify's default (non-custom) response builder always renders an empty - // AttributeStatement — the `loginResponseTemplate.attributes` config above - // only takes effect when a customTagReplacement callback fills the baked - // `{attrX}` placeholders itself. We replicate the same tag values samlify - // computes internally (see samlify/build/src/binding-post.js) plus ours. - const customTagReplacement = (template: string) => { - const now = new Date(); - const notOnOrAfter = new Date(now.getTime() + 5 * 60 * 1000).toISOString(); - const tvalue: Record = { - ID: `_${crypto.randomUUID()}`, - AssertionID: `_${crypto.randomUUID()}`, - Destination: acsUrl, - Audience: spEntityId, - EntityID: spEntityId, - SubjectRecipient: acsUrl, - Issuer: IDP_ENTITY_ID, - IssueInstant: now.toISOString(), - AssertionConsumerServiceURL: acsUrl, - StatusCode: samlify.Constants.StatusCode.Success, - ConditionsNotBefore: now.toISOString(), - ConditionsNotOnOrAfter: notOnOrAfter, - SubjectConfirmationDataNotOnOrAfter: notOnOrAfter, - NameIDFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', - NameID: nextUser.email, - InResponseTo: (requestInfo.extract.request as { id?: string }).id ?? '', - AuthnStatement: '', - attrEmail: nextUser.email, - attrFirstName: nextUser.givenName, - attrLastName: nextUser.familyName, + // samlify's default (non-custom) response builder always renders an empty + // AttributeStatement — the `loginResponseTemplate.attributes` config above + // only takes effect when a customTagReplacement callback fills the baked + // `{attrX}` placeholders itself. We replicate the same tag values samlify + // computes internally (see samlify/build/src/binding-post.js) plus ours. + const customTagReplacement = (template: string) => { + const now = new Date(); + const notOnOrAfter = new Date(now.getTime() + 5 * 60 * 1000).toISOString(); + const tvalue: Record = { + ID: `_${crypto.randomUUID()}`, + AssertionID: `_${crypto.randomUUID()}`, + Destination: acsUrl, + Audience: spEntityId, + EntityID: spEntityId, + SubjectRecipient: acsUrl, + Issuer: IDP_ENTITY_ID, + IssueInstant: now.toISOString(), + AssertionConsumerServiceURL: acsUrl, + StatusCode: samlify.Constants.StatusCode.Success, + ConditionsNotBefore: now.toISOString(), + ConditionsNotOnOrAfter: notOnOrAfter, + SubjectConfirmationDataNotOnOrAfter: notOnOrAfter, + NameIDFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', + NameID: nextUser.email, + InResponseTo: (requestInfo.extract.request as { id?: string }).id ?? '', + AuthnStatement: '', + attrEmail: nextUser.email, + attrFirstName: nextUser.givenName, + attrLastName: nextUser.familyName, + }; + return { id: tvalue.ID as string, context: samlify.SamlLib.replaceTagsByValue(template, tvalue) }; }; - return { id: tvalue.ID as string, context: samlify.SamlLib.replaceTagsByValue(template, tvalue) }; - }; - const { context } = await idp.createLoginResponse(sp, requestInfo, 'post', nextUser, customTagReplacement); + const { context } = await idp.createLoginResponse(sp, requestInfo, 'post', nextUser, customTagReplacement); - res.send(` - -
- - -
- - `); + res.send(` + +
+ + +
+ + `); + } catch (err) { + console.error('mock-saml-idp /sso failed:', err); + res.sendStatus(400); + } }); -app.listen(4001, () => console.log('mock-saml-idp listening on :4001')); +if (require.main === module) { + app.listen(4001, () => console.log('mock-saml-idp listening on :4001')); +} diff --git a/e2e-playground/mocks/sms-sink/Dockerfile b/e2e-playground/mocks/sms-sink/Dockerfile index ad3234d7..a4de2354 100644 --- a/e2e-playground/mocks/sms-sink/Dockerfile +++ b/e2e-playground/mocks/sms-sink/Dockerfile @@ -2,6 +2,6 @@ FROM node:22-alpine WORKDIR /app COPY package.json ./ RUN npm install -COPY server.ts ./ +COPY server.ts tsconfig.json ./ EXPOSE 4100 CMD ["npm", "start"] From 1265a0a749c9714caee4fbba9a2ae578b7bff1c6 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 21 Jul 2026 10:47:05 +0530 Subject: [PATCH 08/71] chore(e2e-playground): add docker-compose stack and GraphQL admin seed client Wires authorizer + mock-oauth/mock-saml-idp/sms-sink (task 5) + mailpit into one compose stack, and adds fixtures/adminClient.ts as the GraphQL admin seed client every later spec-file task consumes. Fixed against the real cmd/root.go and a live docker compose run: - --http-port (not --port), plus --jwt-type/--client-id/--client-secret, all required at startup but missing from the original draft. - --database-url moved off /data (nonexistent, non-root user can't create it) onto the already-owned /authorizer workdir. - admin auth header is x-authorizer-admin-secret, not Bearer. - GraphQL mutations need an Origin header or the CSRF middleware 403s. - playwright.config.ts testDir widened from ./tests (doesn't exist yet) to include fixtures/, with testIgnore fixed so it stops importing (and thereby executing) the mocks' own server.test.ts files. Smoke-tested end to end: build, up --wait, playwright test, down -v. --- e2e-playground/docker-compose.yml | 80 ++++++++++++++ .../fixtures/adminClient.smoke.spec.ts | 9 ++ e2e-playground/fixtures/adminClient.ts | 104 ++++++++++++++++++ e2e-playground/playwright.config.ts | 5 +- 4 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 e2e-playground/docker-compose.yml create mode 100644 e2e-playground/fixtures/adminClient.smoke.spec.ts create mode 100644 e2e-playground/fixtures/adminClient.ts diff --git a/e2e-playground/docker-compose.yml b/e2e-playground/docker-compose.yml new file mode 100644 index 00000000..7a785659 --- /dev/null +++ b/e2e-playground/docker-compose.yml @@ -0,0 +1,80 @@ +# e2e-playground/docker-compose.yml +services: + authorizer: + build: + context: .. + dockerfile: Dockerfile + ports: + - "8080:8080" + command: + - "--http-port=8080" + - "--database-type=sqlite" + - "--database-url=/authorizer/e2e-test.db" + - "--admin-secret=e2e-admin-secret" + - "--jwt-type=HS256" + - "--jwt-secret=e2e-jwt-secret-do-not-use-in-prod" + - "--client-id=e2e-client-id" + - "--client-secret=e2e-client-secret" + - "--enable-signup=true" + - "--allowed-origins=http://localhost:8080" + - "--google-client-id=mock-client-id" + - "--google-client-secret=mock-client-secret" + - "--github-client-id=mock-client-id" + - "--github-client-secret=mock-client-secret" + - "--facebook-client-id=mock-client-id" + - "--facebook-client-secret=mock-client-secret" + - "--linkedin-client-id=mock-client-id" + - "--linkedin-client-secret=mock-client-secret" + - "--apple-client-id=mock-client-id" + - "--apple-client-secret=mock-client-secret" + - "--twitter-client-id=mock-client-id" + - "--twitter-client-secret=mock-client-secret" + - "--discord-client-id=mock-client-id" + - "--discord-client-secret=mock-client-secret" + - "--microsoft-client-id=mock-client-id" + - "--microsoft-client-secret=mock-client-secret" + - "--twitch-client-id=mock-client-id" + - "--twitch-client-secret=mock-client-secret" + - "--roblox-client-id=mock-client-id" + - "--roblox-client-secret=mock-client-secret" + - "--test-oauth-google-base-url=http://mock-oauth:4000/google" + - "--test-oauth-github-base-url=http://mock-oauth:4000/github" + - "--test-oauth-facebook-base-url=http://mock-oauth:4000/facebook" + - "--test-oauth-linkedin-base-url=http://mock-oauth:4000/linkedin" + - "--test-oauth-apple-base-url=http://mock-oauth:4000/apple" + - "--test-oauth-twitter-base-url=http://mock-oauth:4000/twitter" + - "--test-oauth-discord-base-url=http://mock-oauth:4000/discord" + - "--test-oauth-microsoft-base-url=http://mock-oauth:4000/microsoft" + - "--test-oauth-twitch-base-url=http://mock-oauth:4000/twitch" + - "--test-oauth-roblox-base-url=http://mock-oauth:4000/roblox" + - "--test-sms-webhook-url=http://sms-sink:4100/sms" + - "--smtp-host=mailpit" + - "--smtp-port=1025" + depends_on: + mock-oauth: { condition: service_started } + mock-saml-idp: { condition: service_started } + mailpit: { condition: service_started } + sms-sink: { condition: service_started } + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"] + interval: 2s + timeout: 2s + retries: 30 + + mock-oauth: + build: ./mocks/mock-oauth + ports: ["4000:4000"] + + mock-saml-idp: + build: ./mocks/mock-saml-idp + ports: ["4001:4001"] + + sms-sink: + build: ./mocks/sms-sink + ports: ["4100:4100"] + + mailpit: + image: axllent/mailpit:latest + ports: + - "1025:1025" + - "8025:8025" diff --git a/e2e-playground/fixtures/adminClient.smoke.spec.ts b/e2e-playground/fixtures/adminClient.smoke.spec.ts new file mode 100644 index 00000000..3050f36e --- /dev/null +++ b/e2e-playground/fixtures/adminClient.smoke.spec.ts @@ -0,0 +1,9 @@ +// e2e-playground/fixtures/adminClient.smoke.spec.ts +import { test, expect } from '@playwright/test'; +import { createOrg, addVerifiedDomain } from './adminClient'; + +test('admin seed client can create an org and verify a domain', async () => { + const org = await createOrg(`e2e-smoke-${Date.now()}`); + expect(org.id).toBeTruthy(); + await expect(addVerifiedDomain(org.id, `${org.id}.example.com`)).resolves.not.toThrow(); +}); diff --git a/e2e-playground/fixtures/adminClient.ts b/e2e-playground/fixtures/adminClient.ts new file mode 100644 index 00000000..692cab9b --- /dev/null +++ b/e2e-playground/fixtures/adminClient.ts @@ -0,0 +1,104 @@ +// e2e-playground/fixtures/adminClient.ts +import { GraphQLClient, gql } from 'graphql-request'; + +const BASE_URL = process.env.AUTHORIZER_BASE_URL || 'http://localhost:8080'; +const ENDPOINT = `${BASE_URL}/graphql`; +const ADMIN_SECRET = process.env.AUTHORIZER_ADMIN_SECRET || 'e2e-admin-secret'; + +// Admin auth is the x-authorizer-admin-secret header (see +// internal/token/admin_token.go IsSuperAdmin / internal/e2e/smoke_test.go), +// not a Bearer token. Origin is required too: the CSRF middleware rejects +// state-changing requests with no Origin/Referer (internal/graph/schema.graphqls +// comment on AllowedOrigins; must match the server's --allowed-origins). +const client = new GraphQLClient(ENDPOINT, { + headers: { 'x-authorizer-admin-secret': ADMIN_SECRET, Origin: BASE_URL }, +}); + +export async function createOrg(name: string): Promise<{ id: string; name: string }> { + const query = gql` + mutation ($params: CreateOrganizationRequest!) { + _create_organization(params: $params) { id name } + } + `; + const res = await client.request<{ _create_organization: { id: string; name: string } }>(query, { + params: { name, display_name: name }, + }); + return res._create_organization; +} + +export async function createOIDCConnection( + orgId: string, + opts: { name: string; issuerUrl: string; clientId: string; clientSecret: string } +): Promise<{ id: string }> { + const query = gql` + mutation ($params: CreateOrgOIDCConnectionRequest!) { + _create_org_oidc_connection(params: $params) { id } + } + `; + const res = await client.request<{ _create_org_oidc_connection: { id: string } }>(query, { + params: { + org_id: orgId, + name: opts.name, + issuer_url: opts.issuerUrl, + client_id: opts.clientId, + client_secret: opts.clientSecret, + }, + }); + return res._create_org_oidc_connection; +} + +export async function createSAMLConnection( + orgId: string, + opts: { name: string; idpEntityId: string; idpSsoUrl: string; idpCertificate: string; allowIdpInitiated?: boolean } +): Promise<{ id: string }> { + const query = gql` + mutation ($params: CreateOrgSAMLConnectionRequest!) { + _create_org_saml_connection(params: $params) { id } + } + `; + const res = await client.request<{ _create_org_saml_connection: { id: string } }>(query, { + params: { + org_id: orgId, + name: opts.name, + idp_entity_id: opts.idpEntityId, + idp_sso_url: opts.idpSsoUrl, + idp_certificate: opts.idpCertificate, + allow_idp_initiated: opts.allowIdpInitiated ?? false, + }, + }); + return res._create_org_saml_connection; +} + +export async function addVerifiedDomain(orgId: string, domain: string): Promise { + const query = gql` + mutation ($params: AddVerifiedOrgDomainRequest!) { + _add_verified_org_domain(params: $params) { domain } + } + `; + await client.request(query, { params: { org_id: orgId, domain } }); +} + +export async function createSCIMEndpoint(orgId: string): Promise<{ token: string; endpoint: string }> { + const query = gql` + mutation ($params: CreateScimEndpointRequest!) { + _create_scim_endpoint(params: $params) { + token + scim_endpoint { id } + } + } + `; + const res = await client.request<{ _create_scim_endpoint: { token: string; scim_endpoint: { id: string } } }>( + query, + { params: { org_id: orgId } } + ); + return { token: res._create_scim_endpoint.token, endpoint: `/scim/v2` }; +} + +export async function setEnforceMFA(enabled: boolean): Promise { + const query = gql` + mutation ($params: UpdateEnvRequest!) { + _update_env(params: $params) { message } + } + `; + await client.request(query, { params: { ENFORCE_MULTI_FACTOR_AUTHENTICATION: enabled } }); +} diff --git a/e2e-playground/playwright.config.ts b/e2e-playground/playwright.config.ts index 89e6b0f6..d56031db 100644 --- a/e2e-playground/playwright.config.ts +++ b/e2e-playground/playwright.config.ts @@ -1,7 +1,8 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: './tests', + testDir: '.', + testIgnore: ['**/node_modules/**', '**/mocks/**'], timeout: 30_000, retries: 0, reporter: [['html', { outputFolder: 'playwright-report', open: 'never' }]], @@ -14,7 +15,7 @@ export default defineConfig({ projects: [ { name: 'mfa-off', - testIgnore: /mfa-routing-matrix\.spec\.ts/, + testIgnore: [/mfa-routing-matrix\.spec\.ts/, '**/mocks/**', '**/node_modules/**'], use: { ...devices['Desktop Chrome'] }, }, { From d258aeba697e7336216f8b3c142e532589736491 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 21 Jul 2026 10:56:03 +0530 Subject: [PATCH 09/71] test(e2e-playground): add OIDC provider-side conformance spec Adds tests/oidc-provider.spec.ts covering discovery doc, JWKS shape, signup+login, replayed-code rejection, and redirect_uri allowlist rejection against the real docker-compose stack. Two calibrations against real behavior vs the plan's draft: - graphql-request client needs an explicit Origin header, same as fixtures/adminClient.ts, since CSRFMiddleware (csrf.go) requires Origin/Referer on state-changing GraphQL mutations. - JWKS assertion checks array shape, not non-empty length: this stack runs --jwt-type=HS256, and jwks.go intentionally never publishes HMAC (symmetric) keys. --- e2e-playground/tests/oidc-provider.spec.ts | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 e2e-playground/tests/oidc-provider.spec.ts diff --git a/e2e-playground/tests/oidc-provider.spec.ts b/e2e-playground/tests/oidc-provider.spec.ts new file mode 100644 index 00000000..295d785b --- /dev/null +++ b/e2e-playground/tests/oidc-provider.spec.ts @@ -0,0 +1,71 @@ +// e2e-playground/tests/oidc-provider.spec.ts +import { test, expect } from '@playwright/test'; +import { GraphQLClient, gql } from 'graphql-request'; +import crypto from 'node:crypto'; + +const BASE_URL = process.env.AUTHORIZER_BASE_URL || 'http://localhost:8080'; +// CSRF middleware requires Origin/Referer on state-changing requests (see +// internal/http_handlers/csrf.go) — same rationale as fixtures/adminClient.ts. +const client = new GraphQLClient(`${BASE_URL}/graphql`, { headers: { Origin: BASE_URL } }); + +function randomEmail() { + return `oidc-provider-${crypto.randomUUID()}@example.com`; +} + +test.describe('OIDC — provider side', () => { + test('discovery document matches actual endpoint behavior', async ({ request, baseURL }) => { + const res = await request.get('/.well-known/openid-configuration'); + expect(res.status()).toBe(200); + const doc = await res.json(); + expect(doc.authorization_endpoint).toBe(`${baseURL}/authorize`); + expect(doc.token_endpoint).toBe(`${baseURL}/oauth/token`); + expect(doc.jwks_uri).toBe(`${baseURL}/.well-known/jwks.json`); + + const jwksRes = await request.get('/.well-known/jwks.json'); + expect(jwksRes.status()).toBe(200); + const jwks = await jwksRes.json(); + // This stack runs --jwt-type=HS256 (docker-compose.yml). HMAC keys are + // symmetric and must never be exposed via JWKS (internal/http_handlers/jwks.go + // generateJWKFromKey), so the keys array is intentionally empty here — + // assert the documented shape rather than a non-empty count. + expect(Array.isArray(jwks.keys)).toBe(true); + }); + + test('signup then password login issues a valid session with correct ID token claims', async ({ request }) => { + const email = randomEmail(); + const signup = gql` + mutation ($params: SignUpRequest!) { + signup(params: $params) { message } + } + `; + await client.request(signup, { params: { email, password: 'Str0ngPassw0rd!', confirm_password: 'Str0ngPassw0rd!' } }); + + const login = gql` + mutation ($params: LoginRequest!) { + login(params: $params) { message } + } + `; + const loginRes = await client.request<{ login: { message: string } }>(login, { + params: { email, password: 'Str0ngPassw0rd!' }, + }); + expect(loginRes.login.message).toBeTruthy(); + }); + + test('replayed authorization code is rejected', async ({ request }) => { + // A fabricated/unknown code must be rejected — real replay coverage + // (issue a real code via /authorize, use it twice) is exercised end-to-end + // by tests/oidc-sso-rp.spec.ts's browser flow, which is where a real code + // is actually mintable from this black-box vantage point. + const res = await request.post('/oauth/token', { + form: { grant_type: 'authorization_code', code: 'not-a-real-code', client_id: 'authorizer' }, + }); + expect(res.status()).toBeGreaterThanOrEqual(400); + }); + + test('redirect URI not in allowlist is rejected at /authorize', async ({ page }) => { + await page.goto( + '/authorize?response_type=code&client_id=authorizer&redirect_uri=https://evil.example.com/cb&scope=openid&state=xyz' + ); + await expect(page.locator('body')).toContainText(/invalid redirect uri|redirect_uri/i); + }); +}); From ddd2658dab688a60d6e1f08becc5f57fb80feac4 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 21 Jul 2026 11:12:27 +0530 Subject: [PATCH 10/71] test(e2e-playground): add real PKCE authorization-code flow + replay test to OIDC provider spec --- e2e-playground/tests/oidc-provider.spec.ts | 107 +++++++++++++++++++-- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/e2e-playground/tests/oidc-provider.spec.ts b/e2e-playground/tests/oidc-provider.spec.ts index 295d785b..105dcbd1 100644 --- a/e2e-playground/tests/oidc-provider.spec.ts +++ b/e2e-playground/tests/oidc-provider.spec.ts @@ -26,12 +26,13 @@ test.describe('OIDC — provider side', () => { const jwks = await jwksRes.json(); // This stack runs --jwt-type=HS256 (docker-compose.yml). HMAC keys are // symmetric and must never be exposed via JWKS (internal/http_handlers/jwks.go - // generateJWKFromKey), so the keys array is intentionally empty here — - // assert the documented shape rather than a non-empty count. - expect(Array.isArray(jwks.keys)).toBe(true); + // generateJWKFromKey) — the keys array must be exactly empty. Asserting + // the exact value (not just Array.isArray) catches a future regression + // where an HMAC secret starts leaking into the JWKS response. + expect(jwks.keys).toEqual([]); }); - test('signup then password login issues a valid session with correct ID token claims', async ({ request }) => { + test('signup then password login issues a valid session', async () => { const email = randomEmail(); const signup = gql` mutation ($params: SignUpRequest!) { @@ -51,17 +52,105 @@ test.describe('OIDC — provider side', () => { expect(loginRes.login.message).toBeTruthy(); }); - test('replayed authorization code is rejected', async ({ request }) => { - // A fabricated/unknown code must be rejected — real replay coverage - // (issue a real code via /authorize, use it twice) is exercised end-to-end - // by tests/oidc-sso-rp.spec.ts's browser flow, which is where a real code - // is actually mintable from this black-box vantage point. + test('a fabricated authorization code is rejected', async ({ request }) => { + // Unknown/fabricated code — proves /oauth/token doesn't accept arbitrary + // codes. Real single-use (replay of a genuine, once-used code) is + // covered separately below, where a real code is minted via a live + // browser-driven PKCE /authorize flow. const res = await request.post('/oauth/token', { form: { grant_type: 'authorization_code', code: 'not-a-real-code', client_id: 'authorizer' }, }); expect(res.status()).toBeGreaterThanOrEqual(400); }); + test('real PKCE authorization-code flow exchanges a code for tokens, and replay is rejected', async ({ + page, + request, + }) => { + // client_id/redirect_uri must match what this stack's own login UI is + // configured for (docker-compose.yml: --client-id=e2e-client-id, + // --allowed-origins=http://localhost:8080). No client is registered in + // the clients table for e2e-client-id, so /authorize (internal/http_handlers/authorize.go) + // falls back to validating redirect_uri's origin against AllowedOrigins — + // any path under http://localhost:8080 is accepted. + const clientId = 'e2e-client-id'; + const redirectUri = `${BASE_URL}/e2e-callback`; + const email = randomEmail(); + const password = 'Str0ngPassw0rd!'; + + const signup = gql` + mutation ($params: SignUpRequest!) { + signup(params: $params) { message } + } + `; + await client.request(signup, { params: { email, password, confirm_password: password } }); + + // RFC 7636 PKCE: verifier is a random string, challenge is its S256 hash. + const codeVerifier = crypto.randomBytes(32).toString('base64url'); + const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url'); + const state = crypto.randomUUID(); + + const authorizeUrl = new URL('/authorize', BASE_URL); + authorizeUrl.searchParams.set('response_type', 'code'); + authorizeUrl.searchParams.set('client_id', clientId); + authorizeUrl.searchParams.set('redirect_uri', redirectUri); + authorizeUrl.searchParams.set('scope', 'openid'); + authorizeUrl.searchParams.set('state', state); + authorizeUrl.searchParams.set('code_challenge', codeChallenge); + authorizeUrl.searchParams.set('code_challenge_method', 'S256'); + + await page.goto(authorizeUrl.toString()); + + // Real rendered /app login form (web/app/src/pages/login.tsx → + // AuthorizerBasicAuthLogin in authorizer-react): email/phone field id + // authorizer-login-email-or-phone-number, password field id + // authorizer-login-password, submit button inside the named form. + await page.locator('#authorizer-login-email-or-phone-number').fill(email); + await page.locator('#authorizer-login-password').fill(password); + await page.locator('form[name="authorizer-login-form"] button[type="submit"]').click(); + + // First-time login for a brand-new user hits the optional MFA-setup + // offer screen (withheld-token first-time-setup redesign — see + // authorizer-react AuthorizerMFASetup / commit 992b3bf4): the token is + // withheld until a factor is set up or skipped. "Skip for now" calls + // skipMfaSetup, which issues the token and completes login. click() + // waits for the button up to its own timeout, so this also tolerates + // the offer screen not appearing at all. + await page + .getByRole('button', { name: 'Skip for now' }) + .click({ timeout: 10_000 }) + .catch(() => {}); + + // On success the SPA (web/app/src/Root.tsx) resumes the authorization + // request against /authorize using the now-authenticated session cookie, + // and the server issues the final redirect to redirect_uri?code=...&state=.... + await page.waitForURL((url) => url.origin === BASE_URL && url.pathname === '/e2e-callback' && url.searchParams.has('code')); + + const finalUrl = new URL(page.url()); + const code = finalUrl.searchParams.get('code'); + expect(code).toBeTruthy(); + expect(finalUrl.searchParams.get('state')).toBe(state); + + const tokenForm = { + grant_type: 'authorization_code', + code: code!, + client_id: clientId, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }; + + const tokenRes = await request.post('/oauth/token', { form: tokenForm }); + expect(tokenRes.status()).toBe(200); + const tokenBody = await tokenRes.json(); + expect(tokenBody.access_token).toBeTruthy(); + expect(tokenBody.id_token).toBeTruthy(); + + // RFC 6749 §4.1.2: authorization codes MUST be single-use. Replaying the + // exact same (real, already-consumed) code must now be rejected. + const replayRes = await request.post('/oauth/token', { form: tokenForm }); + expect(replayRes.status()).toBeGreaterThanOrEqual(400); + }); + test('redirect URI not in allowlist is rejected at /authorize', async ({ page }) => { await page.goto( '/authorize?response_type=code&client_id=authorizer&redirect_uri=https://evil.example.com/cb&scope=openid&state=xyz' From 93659b1c3bed674bb54065db591ca5ad0bcb25ea Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 21 Jul 2026 11:27:17 +0530 Subject: [PATCH 11/71] test(e2e-playground): add OIDC SSO relying-party (home-realm discovery) spec Real HRD screen lives at /app (not /app/login), placeholder-only email input, "Continue" button text -- calibrated against a live stack with --enable-org-discovery=true. home-realm-discovery JIT-provisioning test is test.fixme: unconditionally blocked by validateSSOIssuerURL's https-only issuer_url check and SafeHTTPClient's private-IP rejection (mock-oauth's docker-network address is unavoidably private), neither of which has a test/dev bypass. Confirmed live: connection creation rejects http://, and a real browser-driven login attempt with an https:// issuer dead-ends at sso_upstream_error "could not reach the identity provider". Unrecognized-domain fallback test passes for real against a stack with the flag enabled. --- e2e-playground/tests/oidc-sso-rp.spec.ts | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 e2e-playground/tests/oidc-sso-rp.spec.ts diff --git a/e2e-playground/tests/oidc-sso-rp.spec.ts b/e2e-playground/tests/oidc-sso-rp.spec.ts new file mode 100644 index 00000000..509591ed --- /dev/null +++ b/e2e-playground/tests/oidc-sso-rp.spec.ts @@ -0,0 +1,110 @@ +// e2e-playground/tests/oidc-sso-rp.spec.ts +import { test, expect } from '@playwright/test'; +import { createOrg, createOIDCConnection, addVerifiedDomain } from '../fixtures/adminClient'; + +// Host-reachable base for the test PROCESS's own calls to mock-oauth (the +// __configure REST call below runs from this Node process, not from inside +// the authorizer container). +const MOCK_OAUTH_BASE = process.env.MOCK_OAUTH_BASE_URL || 'http://localhost:4000'; +// Container-network-reachable base for the issuer_url stored on the org's +// OIDC connection: that URL is dialed SERVER-SIDE, by the authorizer +// container itself (SSOLoginHandler -> fetchOIDCDiscovery), so it must +// resolve inside the docker-compose network, not on the host. See the +// blocked test below for why this currently can never succeed regardless. +const MOCK_OAUTH_INTERNAL_BASE = process.env.MOCK_OAUTH_INTERNAL_BASE_URL || 'https://mock-oauth:4000'; + +// This spec exercises the /app email-first home-realm-discovery (HRD) step +// (web/app/src/pages/login.tsx: HRDForm / handleHRDSubmit), which only +// renders when the server runs with --enable-org-discovery=true +// (internal/config/config.go Config.EnableOrgDiscovery; cmd/root.go +// --enable-org-discovery, default false / opt-in). The committed +// e2e-playground/docker-compose.yml does NOT currently pass this flag, so +// running this spec against a plain `docker compose up` will render the +// standard password-first login UI directly (no email/Continue step) and +// both tests below will fail at the very first HRD interaction. Add +// "--enable-org-discovery=true" to the authorizer service's command list in +// docker-compose.yml before running this spec — see the Task 8 report for +// verification detail (confirmed by hitting a live stack with the flag +// injected via a scratch, non-committed compose override). +// +// The real HRD screen (confirmed against a live stack) is served at /app +// (NOT /app/login — Root.tsx only registers "/app" for the unauthenticated +// Login route), has no