diff --git a/sep10-auth/src/middleware.ts b/sep10-auth/src/middleware.ts index 4f6502e..5f50a60 100644 --- a/sep10-auth/src/middleware.ts +++ b/sep10-auth/src/middleware.ts @@ -31,7 +31,46 @@ export interface Sep10MiddlewareOptions extends VerifyChallengeOptions { // typically verify once and issue a short-lived session JWT instead of // re-verifying the challenge transaction on every request; that is out of // scope for this package. +/** + * Reject options that cannot authenticate anything, at factory time. + * + * `verifyChallenge` would otherwise fail every single request at runtime with a + * per-request 401, which reads as "the client's challenge is bad" rather than + * "the server is misconfigured". Failing here surfaces it at wiring time, next + * to the call that is actually wrong. + * + * Only emptiness is checked - whether a `serverAccountId` is a real Stellar key, + * or a home domain is reachable, stays `verifyChallenge`'s business. + */ +function assertValidOptions(options: Sep10MiddlewareOptions): void { + const toList = (value: string | string[]): string[] => + Array.isArray(value) ? value : [value]; + const isBlank = (value: unknown): boolean => + typeof value !== 'string' || value.trim() === ''; + + if (isBlank(options.serverAccountId)) { + throw new TypeError('createSep10Middleware: serverAccountId must be a non-empty string'); + } + + for (const [name, value] of [ + ['homeDomains', options.homeDomains], + ['webAuthDomain', options.webAuthDomain], + ] as const) { + const entries = toList(value); + if (entries.length === 0) { + throw new TypeError(`createSep10Middleware: ${name} must not be empty`); + } + if (entries.some(isBlank)) { + throw new TypeError( + `createSep10Middleware: ${name} must not contain empty or blank entries`, + ); + } + } +} + export function createSep10Middleware(options: Sep10MiddlewareOptions): RequestHandler { + assertValidOptions(options); + const logger = options.logger ?? noopLogger; return async (req, res, next) => { diff --git a/sep10-auth/test/middleware-options.test.ts b/sep10-auth/test/middleware-options.test.ts new file mode 100644 index 0000000..99caec0 --- /dev/null +++ b/sep10-auth/test/middleware-options.test.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2026 stellar-compliance-kit + * SPDX-License-Identifier: MIT + */ + +import { createSep10Middleware } from '../src/middleware'; +import type { Sep10MiddlewareOptions } from '../src/middleware'; + +const valid: Sep10MiddlewareOptions = { + serverAccountId: 'GSERVERACCOUNTIDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + homeDomains: 'example.com', + webAuthDomain: 'example.com', +}; + +describe('createSep10Middleware - option validation at creation time', () => { + it('accepts valid options', () => { + expect(() => createSep10Middleware(valid)).not.toThrow(); + }); + + it('accepts array forms', () => { + expect(() => + createSep10Middleware({ + ...valid, + homeDomains: ['example.com', 'alt.example.com'], + webAuthDomain: ['example.com'], + }), + ).not.toThrow(); + }); + + it.each([ + ['an empty serverAccountId', { serverAccountId: '' }, /serverAccountId/], + ['a blank serverAccountId', { serverAccountId: ' ' }, /serverAccountId/], + ['an empty homeDomains array', { homeDomains: [] }, /homeDomains/], + ['an empty homeDomains string', { homeDomains: '' }, /homeDomains/], + ['a blank entry in homeDomains', { homeDomains: ['example.com', ' '] }, /homeDomains/], + ['an empty webAuthDomain array', { webAuthDomain: [] }, /webAuthDomain/], + ['an empty webAuthDomain string', { webAuthDomain: '' }, /webAuthDomain/], + ])('throws synchronously for %s', (_description, override, expected) => { + expect(() => + createSep10Middleware({ ...valid, ...override } as Sep10MiddlewareOptions), + ).toThrow(expected); + }); + + it('throws a TypeError, not a bare Error', () => { + expect(() => createSep10Middleware({ ...valid, serverAccountId: '' })).toThrow(TypeError); + }); + + it('names the factory in the message so the misconfigured call site is obvious', () => { + expect(() => createSep10Middleware({ ...valid, homeDomains: [] })).toThrow( + /^createSep10Middleware: /, + ); + }); + + it('does not validate the shape of a serverAccountId, only that it is present', () => { + // Whether it is a real Stellar key stays verifyChallenge's business. + expect(() => createSep10Middleware({ ...valid, serverAccountId: 'not-a-key' })).not.toThrow(); + }); +});