Skip to content
Open
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
39 changes: 39 additions & 0 deletions sep10-auth/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
58 changes: 58 additions & 0 deletions sep10-auth/test/middleware-options.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});