feat: establish auth listing and image vertical slices

Includes MFA-capable authentication, session and CSRF security, password recovery, listing lifecycle and ownership rules, inventory image metadata, local ImageStorage, migrations, tests, and implementation status documentation. Verification: npm test (62 passing) and integration suite (14 passing).
This commit is contained in:
2026-09-08 17:32:03 -04:00
commit c728016dec
128 changed files with 9788 additions and 0 deletions
@@ -0,0 +1,201 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { hashPassword, verifyPassword } from '../../src/modules/auth/domain/password.js';
import { createAuthenticationService } from '../../src/modules/auth/services/authentication-service.js';
test('verifies a password hash without accepting a different password', async () => {
const passwordHash = await hashPassword('correct-password');
assert.equal(await verifyPassword('correct-password', passwordHash), true);
assert.equal(await verifyPassword('incorrect-password', passwordHash), false);
});
test('creates only a hashed session token after valid credentials', async () => {
let createdSession;
const auditEvents = [];
const service = createAuthenticationService({
now: () => new Date('2026-09-07T00:00:00.000Z'),
passwordVerifier: async (password) => password === 'correct-password',
sessionRepository: {
async create(session) {
createdSession = session;
},
},
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
userRepository: {
async findAuthenticationUserByEmail() {
return {
id: 'user-id',
sellerId: 'seller-id',
role: 'ADMIN',
passwordHash: 'stored-hash',
};
},
},
});
const result = await service.login({
email: 'admin@example.test',
password: 'correct-password',
});
assert.notEqual(createdSession.sessionTokenHash, result.sessionToken);
assert.notEqual(createdSession.csrfTokenHash, result.csrfToken);
assert.equal(createdSession.userId, 'user-id');
assert.equal(result.principal.sellerId, 'seller-id');
assert.deepEqual(auditEvents, [
{ userId: 'user-id', event: 'LOGIN_SUCCESS', ipAddress: null, userAgent: null },
{ userId: 'user-id', event: 'SESSION_CREATED', ipAddress: null, userAgent: null },
]);
});
test('does not create a session for invalid credentials', async () => {
let sessionCreated = false;
const auditEvents = [];
const service = createAuthenticationService({
passwordVerifier: async () => false,
sessionRepository: {
async create() {
sessionCreated = true;
},
},
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
userRepository: {
async findAuthenticationUserByEmail() {
return { id: 'user-id', passwordHash: 'stored-hash' };
},
},
});
await assert.rejects(
() => service.login({ email: 'admin@example.test', password: 'incorrect-password' }),
(error) => error.code === 'UNAUTHORIZED',
);
assert.equal(sessionCreated, false);
assert.deepEqual(auditEvents, [{ event: 'LOGIN_FAILURE', ipAddress: null, userAgent: null }]);
});
test('does not create a session until enabled MFA verifies the login code', async () => {
let sessionCreated = false;
let receivedMfaInput;
const service = createAuthenticationService({
mfaService: {
async verifyLogin(input) {
receivedMfaInput = input;
const error = new Error('MFA code is invalid.');
error.code = 'MFA_CODE_INVALID';
throw error;
},
},
passwordVerifier: async () => true,
sessionRepository: { async create() { sessionCreated = true; } },
userRepository: {
async findAuthenticationUserByEmail() {
return {
id: 'user-id',
sellerId: 'seller-id',
role: 'ADMIN',
passwordHash: 'stored-hash',
};
},
},
});
await assert.rejects(
() => service.login({ email: 'admin@example.test', password: 'correct-password' }),
(error) => error.code === 'MFA_CODE_INVALID',
);
assert.deepEqual(receivedMfaInput, { userId: 'user-id', code: undefined });
assert.equal(sessionCreated, false);
});
test('rejects an unsafe request without the session-bound CSRF token', async () => {
const service = createAuthenticationService({
now: () => new Date('2026-09-07T00:00:00.000Z'),
sessionRepository: {
async findActiveAuthenticationSession() {
return { csrfTokenHash: 'different-token-hash' };
},
},
});
await assert.rejects(
() => service.authorizeUnsafeRequest('session-token', 'csrf-token'),
(error) => error.code === 'FORBIDDEN',
);
});
test('lists and revokes only sessions owned by the authenticated user', async () => {
let revokedSession;
let revokedOthers;
const auditEvents = [];
const service = createAuthenticationService({
now: () => new Date('2026-09-07T00:00:00.000Z'),
sessionRepository: {
async findActiveAuthenticationSession() {
return { id: 'current-session', userId: 'user-id', sellerId: 'seller-id', role: 'ADMIN' };
},
async listActiveByUserId(userId) {
assert.equal(userId, 'user-id');
return [{ id: 'current-session' }, { id: 'other-session' }];
},
async revokeByIdAndUserId(id, userId) {
revokedSession = { id, userId };
return true;
},
async revokeOtherByUserId(userId, currentSessionId) {
revokedOthers = { userId, currentSessionId };
},
},
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
});
const sessions = await service.listActiveSessions('session-token');
const revoked = await service.revokeSession('session-token', 'other-session');
await service.revokeOtherSessions('session-token');
assert.equal(sessions.currentSessionId, 'current-session');
assert.deepEqual(revokedSession, { id: 'other-session', userId: 'user-id' });
assert.equal(revoked.currentSessionRevoked, false);
assert.deepEqual(revokedOthers, { userId: 'user-id', currentSessionId: 'current-session' });
assert.deepEqual(auditEvents, [
{
userId: 'user-id',
event: 'SESSION_REVOKED',
metadata: { targetSessionId: 'other-session' },
},
{
userId: 'user-id',
event: 'SESSION_REVOKED',
metadata: { scope: 'OTHER_SESSIONS' },
},
]);
});
test('renews stale active session activity without updating recent activity', async () => {
let touchedAt;
const currentTime = new Date('2026-09-07T01:00:00.000Z');
const service = createAuthenticationService({
now: () => currentTime,
sessionRepository: {
async findActiveAuthenticationSession() {
return {
id: 'current-session',
userId: 'user-id',
sellerId: 'seller-id',
role: 'ADMIN',
lastActivityAt: new Date('2026-09-07T00:50:00.000Z'),
};
},
async touchById(id, activityTime) {
touchedAt = { id, activityTime };
},
},
});
await service.authenticate('session-token');
assert.deepEqual(touchedAt, { id: 'current-session', activityTime: currentTime });
});
@@ -0,0 +1,48 @@
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createImageStorageContract } from '../../src/infrastructure/images/image-storage.js';
import { createLocalImageStorage } from '../../src/infrastructure/images/local-image-storage.js';
test('local image storage round-trips and deletes binary data', async () => {
const root = await mkdtemp(path.join(os.tmpdir(), 'pokemon-images-'));
const storage = createLocalImageStorage(root);
try {
await storage.put({ key: 'inventory/item-1/front.jpg', data: Buffer.from('image-bytes') });
assert.deepEqual(await storage.get('inventory/item-1/front.jpg'), Buffer.from('image-bytes'));
await storage.delete('inventory/item-1/front.jpg');
await assert.rejects(() => storage.get('inventory/item-1/front.jpg'), { code: 'ENOENT' });
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('local image storage rejects traversal and duplicate writes', async () => {
const root = await mkdtemp(path.join(os.tmpdir(), 'pokemon-images-'));
const storage = createLocalImageStorage(root);
try {
await assert.rejects(
() => storage.put({ key: '../outside.jpg', data: Buffer.from('image-bytes') }),
/escapes the storage root/,
);
await storage.put({ key: 'same.jpg', data: Buffer.from('first') });
await assert.rejects(
() => storage.put({ key: 'same.jpg', data: Buffer.from('second') }),
{ code: 'EEXIST' },
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('image storage contracts reject incomplete adapters', () => {
assert.throws(
() => createImageStorageContract({ put() {} }),
/must implement get\(\)/,
);
});
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createInitialAdminService } from '../../src/modules/auth/services/initial-admin-service.js';
test('provisions a normalized administrator with a linked seller', async () => {
let provisionedInput;
const service = createInitialAdminService({
passwordHasher: async (password) => `hashed:${password}`,
userRepository: {
async createInitialAdmin(input) {
provisionedInput = input;
return {
user: { id: 'user-id', email: input.email },
seller: { id: 'seller-id' },
};
},
},
});
const administrator = await service.provision({
email: ' ADMIN@EXAMPLE.TEST ',
password: 'local-password',
sellerName: ' Catalog Owner ',
});
assert.deepEqual(administrator, {
userId: 'user-id',
sellerId: 'seller-id',
email: 'admin@example.test',
});
assert.deepEqual(provisionedInput, {
email: 'admin@example.test',
passwordHash: 'hashed:local-password',
sellerName: 'Catalog Owner',
});
});
test('rejects provisioning after an administrator already exists', async () => {
const service = createInitialAdminService({
passwordHasher: async () => 'password-hash',
userRepository: { async createInitialAdmin() { return null; } },
});
await assert.rejects(
() =>
service.provision({
email: 'admin@example.test',
password: 'local-password',
sellerName: 'Catalog Owner',
}),
(error) => error.code === 'INITIAL_ADMIN_ALREADY_EXISTS',
);
});
@@ -0,0 +1,49 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createInventoryItemService } from '../../src/modules/inventory/services/inventory-item-service.js';
function createService({ product, condition }) {
return createInventoryItemService({
productRepository: { async findById() { return product; } },
conditionRepository: { async findById() { return condition; } },
inventoryItemRepository: { async create(inventoryItem) { return inventoryItem; } },
});
}
test('creates an inventory item when its condition belongs to the product category', async () => {
const service = createService({
product: { id: 'product-id', sellerId: 'seller-id', categoryId: 'cards' },
condition: { id: 'condition-id', categoryId: 'cards' },
});
const inventoryItem = await service.create({
productId: 'product-id',
conditionId: 'condition-id',
sellerId: 'seller-id',
notes: ' Near mint ',
});
assert.deepEqual(inventoryItem, {
productId: 'product-id',
conditionId: 'condition-id',
notes: 'Near mint',
});
});
test('rejects conditions from another category', async () => {
const service = createService({
product: { id: 'product-id', sellerId: 'seller-id', categoryId: 'cards' },
condition: { id: 'condition-id', categoryId: 'merchandise' },
});
await assert.rejects(
() =>
service.create({
productId: 'product-id',
conditionId: 'condition-id',
sellerId: 'seller-id',
}),
(error) => error.code === 'CONDITION_CATEGORY_MISMATCH',
);
});
@@ -0,0 +1,158 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createListingService } from '../../src/modules/listings/services/listing-service.js';
test('creates a draft listing only after the listing publish requirements are met', async () => {
const created = [];
const service = createListingService({
listingRepository: {
async create(listing) {
const record = { id: 'listing-1', ...listing };
created.push(record);
return record;
},
},
inventoryImageRepository: {
async countByInventoryItemId() {
return 2;
},
},
});
const listing = await service.create({
inventoryItemId: 'inventory-item-id',
productName: ' Charizard ex ',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
notes: ' Near mint ',
});
assert.deepEqual(listing, {
id: 'listing-1',
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
notes: 'Near mint',
});
await assert.rejects(
() =>
service.create({
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 0,
imageCount: 2,
}),
(error) => error.code === 'LISTING_REQUIREMENTS_NOT_MET',
);
assert.equal(created.length, 1);
});
test('publishes an owned draft only when its persisted requirements are met', async () => {
const updated = [];
const service = createListingService({
listingRepository: {
async findByIdForSeller() {
return {
id: 'listing-1',
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
};
},
async updateStatus(id, status) {
updated.push({ id, status });
return { id, status };
},
},
});
assert.deepEqual(
await service.transition({
listingId: 'listing-1',
sellerId: 'seller-1',
nextStatus: 'AVAILABLE',
}),
{ id: 'listing-1', status: 'AVAILABLE' },
);
assert.deepEqual(updated, [{ id: 'listing-1', status: 'AVAILABLE' }]);
});
test('does not transition a listing owned by another seller', async () => {
const service = createListingService({
listingRepository: {
async findByIdForSeller() {
return null;
},
},
});
await assert.rejects(
() => service.transition({ listingId: 'listing-1', sellerId: 'seller-2', nextStatus: 'AVAILABLE' }),
(error) => error.code === 'LISTING_NOT_FOUND' && error.statusCode === 404,
);
});
test('does not publish a listing for sold or archived inventory', async () => {
for (const inventoryStatus of ['SOLD', 'ARCHIVED']) {
const service = createListingService({
listingRepository: {
async findByIdForSeller() {
return {
id: 'listing-1',
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
inventoryStatus,
status: 'DRAFT',
};
},
async updateStatus() {
throw new Error('A blocked listing must not be updated.');
},
},
});
await assert.rejects(
() => service.transition({ listingId: 'listing-1', sellerId: 'seller-1', nextStatus: 'AVAILABLE' }),
(error) => error.code === 'LISTING_INVENTORY_UNAVAILABLE' && error.statusCode === 409,
);
}
});
test('lists and retrieves only seller-owned listings', async () => {
const ownedListing = { id: 'listing-1', status: 'DRAFT' };
const service = createListingService({
listingRepository: {
async listBySellerId(sellerId) {
assert.equal(sellerId, 'seller-1');
return [ownedListing];
},
async findByIdForSeller(listingId, sellerId) {
assert.equal(listingId, 'listing-1');
assert.equal(sellerId, 'seller-1');
return ownedListing;
},
},
});
assert.deepEqual(await service.list({ sellerId: 'seller-1' }), [ownedListing]);
assert.deepEqual(await service.get({ listingId: 'listing-1', sellerId: 'seller-1' }), ownedListing);
});
@@ -0,0 +1,126 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createSecretProtector } from '../../src/modules/auth/domain/secret-protection.js';
import { createMfaService } from '../../src/modules/auth/services/mfa-service.js';
function createService({ method, isCodeValid = true } = {}) {
const createdMethods = [];
const auditEvents = [];
const repository = {
async create(input) {
const createdMethod = { id: 'method-id', isEnabled: false, ...input };
createdMethods.push(createdMethod);
return createdMethod;
},
async findActiveByIdAndUserId() {
return method ?? createdMethods[0] ?? null;
},
async enable(id) {
return { id, name: 'Authenticator', type: 'TOTP', isEnabled: true };
},
async disable(id) {
return { id, isEnabled: false };
},
};
const service = createMfaService({
mfaMethodRepository: repository,
now: () => new Date('2026-09-07T00:00:00.000Z'),
secretProtector: {
encrypt(secret) { return `encrypted:${secret}`; },
decrypt(ciphertext) { return ciphertext.replace('encrypted:', ''); },
},
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
totpEnrollment: () => ({ secret: 'TOTPSECRET', provisioningUri: 'otpauth://test' }),
totpVerifier: () => isCodeValid,
});
return { auditEvents, createdMethods, service };
}
test('encrypts the TOTP secret and returns the provisioning URI only during enrollment', async () => {
const { createdMethods, service } = createService();
const enrollment = await service.enroll({ userId: 'user-id', name: 'Authenticator' });
assert.deepEqual(enrollment, { id: 'method-id', provisioningUri: 'otpauth://test' });
assert.equal(createdMethods[0].secretCiphertext, 'encrypted:TOTPSECRET');
assert.equal(createdMethods[0].isEnabled, false);
assert.equal('secret' in enrollment, false);
});
test('encrypts MFA secrets with authenticated encryption', () => {
const protector = createSecretProtector(Buffer.alloc(32, 7).toString('base64'));
const ciphertext = protector.encrypt('TOTPSECRET');
assert.notEqual(ciphertext, 'TOTPSECRET');
assert.equal(protector.decrypt(ciphertext), 'TOTPSECRET');
});
test('enables an owned MFA method only after a valid code and records enrollment', async () => {
const { auditEvents, service } = createService({
method: {
id: 'method-id',
userId: 'user-id',
type: 'TOTP',
name: 'Authenticator',
secretCiphertext: 'encrypted:TOTPSECRET',
isEnabled: false,
},
});
const method = await service.verifyEnrollment({
userId: 'user-id',
methodId: 'method-id',
code: '123456',
});
assert.deepEqual(method, { id: 'method-id', name: 'Authenticator', type: 'TOTP' });
assert.deepEqual(auditEvents, [{ userId: 'user-id', event: 'MFA_ENROLLED' }]);
});
test('rejects invalid MFA codes without enabling or disabling a method', async () => {
const { auditEvents, service } = createService({
isCodeValid: false,
method: {
id: 'method-id',
userId: 'user-id',
type: 'TOTP',
name: 'Authenticator',
secretCiphertext: 'encrypted:TOTPSECRET',
isEnabled: true,
},
});
await assert.rejects(
() => service.disable({ userId: 'user-id', methodId: 'method-id', code: '000000' }),
(error) => error.code === 'MFA_CODE_INVALID',
);
assert.deepEqual(auditEvents, [{ userId: 'user-id', event: 'MFA_FAILURE' }]);
});
test('requires a valid TOTP code when an enabled MFA method exists at login', async () => {
let markedMethod;
const repository = {
async listEnabledByUserId(userId) {
assert.equal(userId, 'user-id');
return [{ id: 'method-id', secretCiphertext: 'encrypted:TOTPSECRET' }];
},
async markUsed(id) {
markedMethod = id;
},
};
const service = createMfaService({
mfaMethodRepository: repository,
secretProtector: { decrypt() { return 'TOTPSECRET'; } },
totpVerifier: ({ code }) => code === '123456',
});
await assert.rejects(
() => service.verifyLogin({ userId: 'user-id', code: undefined }),
(error) => error.code === 'MFA_CODE_INVALID',
);
await service.verifyLogin({ userId: 'user-id', code: '123456' });
assert.equal(markedMethod, 'method-id');
});
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createPasswordRecoveryService } from '../../src/modules/auth/services/password-recovery-service.js';
test('does not issue a token for an unknown email while recording a generic request', async () => {
let resetIssued = false;
const auditEvents = [];
const service = createPasswordRecoveryService({
passwordResetRepository: { async issue() { resetIssued = true; } },
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
userRepository: { async findAuthenticationUserByEmail() { return null; } },
});
await service.requestReset({ email: 'unknown@example.test' });
assert.equal(resetIssued, false);
assert.deepEqual(auditEvents, [
{ event: 'PASSWORD_RESET_REQUESTED', ipAddress: null, userAgent: null },
]);
});
test('delivers a reset token without persisting the raw token', async () => {
let issuedReset;
let deliveredReset;
const service = createPasswordRecoveryService({
now: () => new Date('2026-09-07T00:00:00.000Z'),
passwordResetNotifier: { async send(reset) { deliveredReset = reset; } },
passwordResetRepository: { async issue(reset) { issuedReset = reset; } },
userRepository: {
async findAuthenticationUserByEmail() {
return { id: 'user-id', email: 'admin@example.test' };
},
},
});
await service.requestReset({ email: 'admin@example.test' });
assert.equal(issuedReset.userId, 'user-id');
assert.notEqual(issuedReset.tokenHash, deliveredReset.token);
assert.equal(deliveredReset.email, 'admin@example.test');
assert.equal(deliveredReset.expiresAt.toISOString(), '2026-09-07T01:00:00.000Z');
});
test('consumes a reset token once and audits a successful password reset', async () => {
let consumedReset;
const auditEvents = [];
const service = createPasswordRecoveryService({
now: () => new Date('2026-09-07T00:00:00.000Z'),
passwordHasher: async (password) => `hashed:${password}`,
passwordResetRepository: {
async consume(reset) {
consumedReset = reset;
return 'user-id';
},
},
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
});
await service.resetPassword({ token: 'a-valid-reset-token', password: 'new-password-value' });
assert.equal(consumedReset.passwordHash, 'hashed:new-password-value');
assert.notEqual(consumedReset.tokenHash, 'a-valid-reset-token');
assert.deepEqual(auditEvents, [
{ userId: 'user-id', event: 'PASSWORD_RESET', ipAddress: null, userAgent: null },
]);
});
test('rejects expired or previously used reset tokens', async () => {
const service = createPasswordRecoveryService({
passwordHasher: async () => 'new-password-hash',
passwordResetRepository: { async consume() { return null; } },
});
await assert.rejects(
() => service.resetPassword({ token: 'invalid-reset-token', password: 'new-password-value' }),
(error) => error.code === 'PASSWORD_RESET_TOKEN_INVALID',
);
});
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createProductService } from '../../src/modules/products/services/product-service.js';
test('creates seller-scoped slugs and increments duplicate names', async () => {
const createdProducts = [];
const productService = createProductService({
productRepository: {
async create(product) {
createdProducts.push(product);
return product;
},
async findBySellerIdAndSlug(sellerId, slug) {
return (
createdProducts.find(
(product) => product.sellerId === sellerId && product.slug === slug,
) ?? null
);
},
},
});
const input = {
name: 'Pikachu Plush',
categoryId: 'category-id',
sellerId: 'seller-id',
};
await productService.create(input);
const secondProduct = await productService.create(input);
assert.equal(secondProduct.slug, 'pikachu-plush-2');
});