Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/wfctl/plugin_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func downloadURL(url string) ([]byte, error) {
func verifyChecksum(data []byte, expected string) error {
h := sha256.Sum256(data)
got := hex.EncodeToString(h[:])
if got != strings.ToLower(expected) {
if !strings.EqualFold(got, expected) {
return fmt.Errorf("checksum mismatch: got %s, want %s", got, expected)
}
return nil
Expand Down
8 changes: 4 additions & 4 deletions example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/CrisisTextLine/modular/modules/auth v0.4.0 // indirect
github.com/CrisisTextLine/modular/modules/eventbus v1.6.0 // indirect
github.com/CrisisTextLine/modular/modules/eventbus/v2 v2.0.0 // indirect
github.com/DataDog/datadog-go/v5 v5.4.0 // indirect
github.com/GoCodeAlone/yaegi v0.17.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
Expand Down Expand Up @@ -105,16 +105,16 @@ require (
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nats-io/nats.go v1.46.1 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
Expand Down
28 changes: 20 additions & 8 deletions example/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ github.com/CrisisTextLine/modular/modules/auth v0.4.0 h1:sP7CYgdJPz88M1PrfOz2knw
github.com/CrisisTextLine/modular/modules/auth v0.4.0/go.mod h1:0DnUawpxdFCka4BjMhmXubQIjkF4VRwRdN6c64Jbvvo=
github.com/CrisisTextLine/modular/modules/cache v0.4.0 h1:vlPXAsucSM1M0RsPly9cWyODouMLQMUwhW/wltQZHZk=
github.com/CrisisTextLine/modular/modules/cache v0.4.0/go.mod h1:4irZOGXxUlgJqAnWlpMyPC3C1tM/f5145/wMThYnAsY=
github.com/CrisisTextLine/modular/modules/eventbus v1.6.0 h1:40H5/mrhPw3Jzi9wntg2//7YVAC4gIgCZJX7sBhXDmU=
github.com/CrisisTextLine/modular/modules/eventbus v1.6.0/go.mod h1:I1tGf3DmadwyMP2NE2m6XHYl9ebXB9wBc/KZLywTR4c=
github.com/CrisisTextLine/modular/modules/eventbus v1.7.0 h1:SSeu7rjuECDgFa+iNyndn94YPQxffHxJgfR7U4psz6E=
github.com/CrisisTextLine/modular/modules/eventbus v1.7.0/go.mod h1:I1tGf3DmadwyMP2NE2m6XHYl9ebXB9wBc/KZLywTR4c=
github.com/CrisisTextLine/modular/modules/eventbus/v2 v2.0.0 h1:bDNWBparvVzXnhLxjFPJ9MDg7N4NUnNOjfn56G/CwGU=
github.com/CrisisTextLine/modular/modules/eventbus/v2 v2.0.0/go.mod h1:5DmacIYrhhiN18i2OHyAVBiNKbN2jHuEv2UJoRToMg0=
github.com/CrisisTextLine/modular/modules/reverseproxy/v2 v2.2.0 h1:SUJEPA61IbjdUwKdSembQTbX9rKz5v4vmyr/cbvb4tY=
github.com/CrisisTextLine/modular/modules/reverseproxy/v2 v2.2.0/go.mod h1:/jVQz+0c/OSm0KcLElNAQueI5BoLd48l1KHV4Np+RO8=
github.com/CrisisTextLine/modular/modules/scheduler v0.4.0 h1:PDYAD+hL7E6mM7YJey9ag1dnTTcJwsepoylxfZY8trw=
Expand All @@ -53,6 +55,8 @@ github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CB
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM=
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
Expand Down Expand Up @@ -184,6 +188,8 @@ github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig=
github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
Expand Down Expand Up @@ -261,8 +267,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand All @@ -276,6 +282,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
Expand All @@ -295,10 +303,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/nats-io/nats.go v1.46.1 h1:bqQ2ZcxVd2lpYI97xYASeRTY3I5boe/IVmuUDPitHfo=
github.com/nats-io/nats.go v1.46.1/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts=
github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
Expand Down
11 changes: 9 additions & 2 deletions module/http_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import (
"github.com/CrisisTextLine/modular"
)

// contextKey is a private type for context keys defined in this package,
// preventing collisions with keys defined in other packages.
type contextKey string

// paramsKey is the context key used to store extracted path parameters.
const paramsKey contextKey = "params"

// pathParamRe matches {name} or {name...} segments in a URL path template.
var pathParamRe = regexp.MustCompile(`\{(\w+)(?:\.\.\.)?}`)

Expand Down Expand Up @@ -170,9 +177,9 @@ func (r *StandardHTTPRouter) rebuildMuxLocked() {
mux := http.NewServeMux()
for _, route := range r.routes {
mux.HandleFunc(fmt.Sprintf("%s %s", route.Method, route.Path), func(w http.ResponseWriter, r *http.Request) {
// Inject path params into context so triggers can read them via r.Context().Value("params").
// Inject path params into context so triggers can read them via r.Context().Value(paramsKey).
if params := extractRouteParams(route.Path, r); len(params) > 0 {
r = r.WithContext(context.WithValue(r.Context(), "params", params))
r = r.WithContext(context.WithValue(r.Context(), paramsKey, params))
}

// Create handler chain with middleware
Expand Down
2 changes: 1 addition & 1 deletion module/http_trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func (t *HTTPTrigger) createHandler(route HTTPTriggerRoute) HTTPHandler {
handlerFn := func(w http.ResponseWriter, r *http.Request) {
// Extract path parameters from the context (would have been set by the router)
params := make(map[string]string)
if routeParams, ok := r.Context().Value("params").(map[string]string); ok {
if routeParams, ok := r.Context().Value(paramsKey).(map[string]string); ok {
params = routeParams
}

Expand Down
2 changes: 2 additions & 0 deletions ui/src/components/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export default function LoginPage() {
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password"
required
autoComplete={mode === 'signin' ? 'current-password' : 'new-password'}
style={inputStyle}
/>
</div>
Expand All @@ -184,6 +185,7 @@ export default function LoginPage() {
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm password"
required
autoComplete="new-password"
style={inputStyle}
/>
</div>
Expand Down
2 changes: 2 additions & 0 deletions ui/src/components/auth/SetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export default function SetupWizard() {
onChange={(e) => setPassword(e.target.value)}
placeholder="Min 6 characters"
required
autoComplete="new-password"
style={inputStyle}
/>
</div>
Expand All @@ -130,6 +131,7 @@ export default function SetupWizard() {
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm password"
required
autoComplete="new-password"
style={inputStyle}
/>
</div>
Expand Down
95 changes: 90 additions & 5 deletions ui/src/store/authStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ function mockFetchFailure(message: string, status = 401) {
ok: false,
status,
statusText: 'Unauthorized',
text: () => Promise.resolve(message),
json: () => Promise.resolve({ error: message }),
text: () => Promise.resolve(JSON.stringify({ error: message })),
});
}

Expand Down Expand Up @@ -122,7 +123,7 @@ describe('authStore', () => {
expect(state.user?.email).toBe('test@example.com');
});

it('sets error on failure', async () => {
it('maps "Invalid credentials" to friendly error message', async () => {
globalThis.fetch = mockFetchFailure('Invalid credentials');

await act(async () => {
Expand All @@ -131,7 +132,7 @@ describe('authStore', () => {

const state = useAuthStore.getState();
expect(state.isAuthenticated).toBe(false);
expect(state.error).toBe('Invalid credentials');
expect(state.error).toBe('Invalid email or password');
expect(state.isLoading).toBe(false);
});

Expand Down Expand Up @@ -200,18 +201,102 @@ describe('authStore', () => {
expect(state.user?.display_name).toBe('New User');
});

it('sets error on registration failure', async () => {
it('maps "Email already exists" to friendly error message', async () => {
globalThis.fetch = mockFetchFailure('Email already exists', 409);

await act(async () => {
await useAuthStore.getState().register('dup@example.com', 'pw', 'Dup');
});

expect(useAuthStore.getState().error).toBe('Email already exists');
expect(useAuthStore.getState().error).toBe('An account with this email already exists');
expect(useAuthStore.getState().isAuthenticated).toBe(false);
});
});

describe('login error transformation', () => {
function setupLoginFailure(errorMessage: string) {
globalThis.fetch = mockFetchFailure(errorMessage);
}

it.each([
'invalid credentials',
'Invalid credentials',
'invalid password',
'invalid user',
'unauthorized',
'Unauthorized',
'HTTP 401',
])('maps "%s" to "Invalid email or password"', async (msg) => {
setupLoginFailure(msg);
await act(async () => {
await useAuthStore.getState().login('a@b.com', 'pw');
});
expect(useAuthStore.getState().error).toBe('Invalid email or password');
});

it.each([
'Internal server error',
'request timeout',
'Error 4010',
'Code: 14015',
'something went wrong',
])('passes through "%s" unchanged', async (msg) => {
setupLoginFailure(msg);
await act(async () => {
await useAuthStore.getState().login('a@b.com', 'pw');
});
expect(useAuthStore.getState().error).toBe(msg);
});

it('does not match partial status codes like "4010"', async () => {
setupLoginFailure('Error 4010');
await act(async () => {
await useAuthStore.getState().login('a@b.com', 'pw');
});
expect(useAuthStore.getState().error).toBe('Error 4010');
});

it('does not match partial status codes like "14015"', async () => {
setupLoginFailure('Code: 14015');
await act(async () => {
await useAuthStore.getState().login('a@b.com', 'pw');
});
expect(useAuthStore.getState().error).toBe('Code: 14015');
});
});

describe('register error transformation', () => {
function setupRegisterFailure(errorMessage: string) {
globalThis.fetch = mockFetchFailure(errorMessage, 409);
}

it.each([
'email already exists',
'Email already exists',
'already registered',
'Already registered',
'duplicate email address',
])('maps "%s" to friendly message', async (msg) => {
setupRegisterFailure(msg);
await act(async () => {
await useAuthStore.getState().register('a@b.com', 'pw', 'Test');
});
expect(useAuthStore.getState().error).toBe('An account with this email already exists');
});

it.each([
'password too short',
'Internal server error',
'invalid email format',
])('passes through "%s" unchanged', async (msg) => {
setupRegisterFailure(msg);
await act(async () => {
await useAuthStore.getState().register('a@b.com', 'pw', 'Test');
});
expect(useAuthStore.getState().error).toBe(msg);
});
});

describe('logout', () => {
it('clears token, user, and sets isAuthenticated false', () => {
useAuthStore.setState({
Expand Down
18 changes: 10 additions & 8 deletions ui/src/store/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,11 @@ const useAuthStore = create<AuthStore>((set, get) => ({
scheduleRefresh(data.expires_in, get().refreshAuth);
await get().loadUser();
} catch (err) {
set({
isLoading: false,
error: err instanceof Error ? err.message : 'Login failed',
});
const raw = err instanceof Error ? err.message : 'Login failed';
const friendly = /(invalid.*(cred|password|user)|unauthorized|\b401\b)/i.test(raw)
? 'Invalid email or password'
: raw;
set({ isLoading: false, error: friendly });
}
},

Expand All @@ -128,10 +129,11 @@ const useAuthStore = create<AuthStore>((set, get) => ({
scheduleRefresh(data.expires_in, get().refreshAuth);
await get().loadUser();
} catch (err) {
set({
isLoading: false,
error: err instanceof Error ? err.message : 'Registration failed',
});
const raw = err instanceof Error ? err.message : 'Registration failed';
const friendly = /(already\s*(exists|registered)|duplicate.*email)/i.test(raw)
? 'An account with this email already exists'
: raw;
set({ isLoading: false, error: friendly });
}
},

Expand Down
Loading