diff --git a/listener/src/api/events-server.test.ts b/listener/src/api/events-server.test.ts index cf50e11f..c51a68a8 100644 --- a/listener/src/api/events-server.test.ts +++ b/listener/src/api/events-server.test.ts @@ -268,8 +268,7 @@ describe('POST /api/webhooks', () => { expect(status).toBe(401); expect((body as any).code).toBe('AUTH_INVALID_SIGNATURE'); - expect((body as any).success).toBe(true); - expect((body as any).data.status).toBe('accepted'); + expect((body as any).success).toBe(false); }); it('rejects a timestamp-bound signature when the timestamp header is removed (anti-replay)', async () => { @@ -412,13 +411,13 @@ describe('POST /api/webhooks', () => { const payload = JSON.stringify({ event: 'test' }); server = await startServer({ ...BASE_OPTIONS, webhookSecrets: secrets }); - await makePostRequest(server, '/api/webhooks', payload, { + const { body } = await makePostRequest(server, '/api/webhooks', payload, { 'X-Webhook-Key-Id': 'key-1', }); expect(logger.warn).toHaveBeenCalled(); expect((body as any).success).toBe(false); - expect((body as any).error.message).toBe('Unknown key-id'); + expect((body as any).error.message).toBe('Missing signature header'); }); it('returns 404 for POST to other paths', async () => { diff --git a/listener/src/api/events-server.ts b/listener/src/api/events-server.ts index 0587f583..18569499 100644 --- a/listener/src/api/events-server.ts +++ b/listener/src/api/events-server.ts @@ -12,20 +12,11 @@ import { handleTemplateRoutes } from './template-routes'; import { sendOk, sendErr, sendJson, ErrorCode } from '../utils/response'; import { handleApiError, ApiError } from './error-handler'; import { applyRequestContext } from '../utils/request-id'; -import { TemplateService } from '../services/template-service'; -import { handleTemplateRoutes } from './template-routes'; import { NotificationHistoryService } from '../services/notification-history'; import { SearchSuggestionService } from '../services/search-suggestion'; import { NotificationSearchService } from '../services/notification-search-service'; -import { - verifySignature, - extractSignature, - extractKeyId, - getSecretForKey, - collectRawBody, - extractTimestamp, - verifyWebhookRequest, -} from '../services/webhook-verifier'; +import { collectRawBody, verifyWebhookRequest } from '../services/webhook-verifier'; +import { IdempotencyKeyService, IdempotencyKeyReuseError } from '../services/idempotency-key-service'; import { WebhookSecret, RateLimitConfig, ContractConfig } from '../types'; import { RateLimiter } from './rate-limiter'; import { getDatabase } from '../database/database'; @@ -47,7 +38,7 @@ import { serializeAuditRecord, serializeTemplate, } from './template-api'; -import { CreateNotificationTemplateInputOld } from '../types/notification-template'; +import { CreateNotificationTemplateInput } from '../types/notification-template'; import { BatchValidationService } from '../services/batch-validation-service'; import { handleArchiveRequest } from './archive-api'; import { ArchiveStore } from '../services/archive-store'; @@ -68,6 +59,8 @@ export interface EventsServerOptions { webhookSecrets?: WebhookSecret[]; apiKeys?: Array<{ key: string; name?: string }>; notificationAPI?: NotificationAPI | null; + /** Idempotency-Key replay protection for mutating endpoints (webhooks, schedule). */ + idempotencyService?: IdempotencyKeyService | null; templateService?: NotificationTemplateService | null; /** Scheduler-scoped template service, used to render templates for scheduled notifications. */ schedulerTemplateService?: TemplateService | null; @@ -628,25 +621,16 @@ export function createEventsServer(options: EventsServerOptions): http.Server { // POST /api/webhooks if (req.method === 'POST' && url.pathname === '/api/webhooks') { const idempotencyKey = IdempotencyKeyService.extractKey(req.headers) ?? undefined; + + const writeAuthFailure = (statusCode: number, message: string, code: string): void => { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: { code, message }, code })); + }; + collectRawBody(req).then(async (rawBody) => { const sourceIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || (req.socket?.remoteAddress as string | undefined); - collectRawBody(req).then((rawBody) => { - const signatureHeader = extractSignature(req.headers); - const keyId = extractKeyId(req.headers); - - if (!signatureHeader) { - logger.warn('Webhook missing signature header', { requestId, correlationId }); - sendErr(res, 401, 'Missing signature header', ErrorCode.UNAUTHORIZED); - return; - } - - if (!keyId) { - logger.warn('Webhook missing key-id header', { requestId, correlationId }); - sendErr(res, 401, 'Missing key-id header', ErrorCode.UNAUTHORIZED); - return; - } const secrets = options.webhookSecrets ?? []; const maxAgeSeconds = options.signatureExpirationSeconds ?? 300; @@ -662,11 +646,12 @@ export function createEventsServer(options: EventsServerOptions): http.Server { }); if (!auth.authenticated) { - res.writeHead(auth.statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: auth.message, code: auth.errorCode })); - if (!secret) { - logger.warn('Webhook unknown key-id', { requestId, correlationId, keyId }); - sendErr(res, 401, 'Unknown key-id', ErrorCode.UNAUTHORIZED); + logger.warn('Webhook authentication failed', { + requestId, + correlationId, + code: auth.errorCode, + }); + writeAuthFailure(auth.statusCode, auth.message, auth.errorCode); return; } @@ -710,38 +695,16 @@ export function createEventsServer(options: EventsServerOptions): http.Server { logger.warn('Webhook rejected: idempotency key reused with different body', { requestId, correlationId, idempotencyKey, }); - res.writeHead(err.statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: err.message, code: err.code })); - if (timestampHeader) { - const timestamp = Array.isArray(timestampHeader) ? timestampHeader[0] : timestampHeader; - if (!isTimestampValid(timestamp, maxAgeSeconds)) { - logger.warn('Webhook request signature expired', { requestId, correlationId, keyId, timestamp }); - sendErr(res, 401, 'Request signature expired', ErrorCode.UNAUTHORIZED); + writeAuthFailure(err.statusCode, err.message, err.code); return; } - throw err; + logger.error('Failed to process webhook', { requestId, correlationId, error: err }); + sendErr(res, 500, 'Internal server error', ErrorCode.INTERNAL_ERROR); } }).catch((err) => { - if (err instanceof IdempotencyKeyReuseError) { - res.writeHead(err.statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: err.message, code: err.code })); - return; - } logger.error('Failed to read webhook body', { requestId, correlationId, error: err instanceof Error ? err.message : String(err) }); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to read request body', code: 'BODY_READ_FAILED' })); - - if (!verifySignature(rawBody, signatureHeader, secret)) { - logger.warn('Webhook invalid signature', { requestId, correlationId, keyId }); - sendErr(res, 401, 'Invalid signature', ErrorCode.UNAUTHORIZED); - return; - } - - logger.info('Webhook received and verified', { requestId, correlationId, keyId }); - sendOk(res, 202, { status: 'accepted' }); - }).catch((err) => { - logger.error('Failed to read webhook body', { requestId, correlationId, error: err }); - sendErr(res, 400, 'Failed to read request body', ErrorCode.BAD_REQUEST); }); return; } @@ -833,7 +796,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { if (!data.executeAt || !data.payload || !data.targetRecipient) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing required fields: executeAt, payload, targetRecipient', code: 'MISSING_FIELDS' })); - sendErr(res, 400, 'Missing required fields: executeAt, payload, targetRecipient', ErrorCode.BAD_REQUEST); return; } @@ -841,7 +803,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { if (isNaN(executeAt.getTime())) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'executeAt is not a valid date', code: 'INVALID_DATE' })); - sendErr(res, 400, 'executeAt is not a valid date', ErrorCode.BAD_REQUEST); return; } @@ -894,15 +855,7 @@ export function createEventsServer(options: EventsServerOptions): http.Server { res.end(JSON.stringify({ error: error.message, code: error.code })); return; } - logger.error('Failed to schedule notification', { - error: error instanceof Error ? error.message : String(error), - requestId, correlationId, - }); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: (error as Error).message, code: 'SCHEDULE_FAILED' })); - sendOk(res, 201, { id: notificationId }); - logger.info('Notification scheduled via API', { requestId, correlationId, notificationId, executeAt: data.executeAt }); - } catch (error) { + const anyError = error as any; if (anyError?.name === 'PayloadTooLargeError') { logger.warn('Payload too large', { @@ -912,11 +865,17 @@ export function createEventsServer(options: EventsServerOptions): http.Server { payloadSizeBytes: anyError.payloadSizeBytes, maxSizeBytes: anyError.maxSizeBytes, }); - sendErr(res, 413, anyError.message, ErrorCode.PAYLOAD_TOO_LARGE); + res.writeHead(413, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: anyError.message, code: 'PAYLOAD_TOO_LARGE' })); return; } - logger.error('Failed to schedule notification', { error, requestId, correlationId }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); + + logger.error('Failed to schedule notification', { + error: error instanceof Error ? error.message : String(error), + requestId, correlationId, + }); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (error as Error).message, code: 'SCHEDULE_FAILED' })); } }); return; @@ -989,40 +948,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { sendOk(res, 200, { failures: monitor.listFailures(limit), count: monitor.listFailures(limit).length }); return; } - if (req.method === 'GET' && url.pathname === '/api/schedule/execution-metrics') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - options.notificationAPI.getExecutionMetrics() - .then((metrics) => { - sendOk(res, 200, metrics); - }) - .catch((error) => { - logger.error('Failed to get execution metrics', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } - - // GET /api/schedule/retry-distribution - if (req.method === 'GET' && url.pathname === '/api/schedule/retry-distribution') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - options.notificationAPI.getRetryDistribution() - .then((distribution) => { - sendOk(res, 200, distribution); - }) - .catch((error) => { - logger.error('Failed to get retry distribution', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } // GET /api/schedule/retry-statistics if (req.method === 'GET' && url.pathname === '/api/schedule/retry-statistics') { @@ -1140,7 +1065,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { .then((result) => { sendJson(res, 200, result); logger.info('GET /api/notifications/history complete', { - requestId, total: result.total, durationMs: Date.now() - startTime, requestId, correlationId, total: result.total, @@ -1185,7 +1109,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { sortBy, }); - notificationSearchService.search({ q, sender, txHash, eventId, status, type, startDate, endDate, limit, offset }) notificationSearchService.search({ q, sender, @@ -1384,7 +1307,7 @@ export function createEventsServer(options: EventsServerOptions): http.Server { req.on('end', () => { void (async () => { try { - const parsed = JSON.parse(body) as CreateNotificationTemplateInputOld; + const parsed = JSON.parse(body) as CreateNotificationTemplateInput; if (!parsed?.id || !parsed?.name || !parsed?.type || !parsed?.body) { sendErr(res, 400, 'Invalid body: id, name, type, and body are required', ErrorCode.BAD_REQUEST); return; diff --git a/listener/src/api/template-routes.ts b/listener/src/api/template-routes.ts index b30e71c8..97376f64 100644 --- a/listener/src/api/template-routes.ts +++ b/listener/src/api/template-routes.ts @@ -112,13 +112,6 @@ export async function handleCreateTemplate(ctx: TemplateRouteContext): Promise + const processableEvents = events.filter((event: StellarSDK.rpc.Api.EventResponse) => this.shouldProcessEvent(event, contractConfig, requestId) ); diff --git a/listener/src/services/notification-api.test.ts b/listener/src/services/notification-api.test.ts index feb5e388..90d2933c 100644 --- a/listener/src/services/notification-api.test.ts +++ b/listener/src/services/notification-api.test.ts @@ -1,3 +1,4 @@ +import { jest } from '@jest/globals'; import { NotificationAPI } from './notification-api'; import { ScheduledNotificationRepository } from './scheduled-notification-repository'; import { NotificationType } from '../types/scheduled-notification'; @@ -5,7 +6,7 @@ import { ValidationError } from '../utils/validation'; function makeRepository(): jest.Mocked> { return { - create: jest.fn().mockResolvedValue(1), + create: jest.fn<() => Promise>().mockResolvedValue(1), }; } @@ -35,7 +36,12 @@ describe('NotificationAPI.scheduleNotification', () => { const input = baseInput(); const id = await api.scheduleNotification(input); expect(id).toBe(1); - expect(repository.create).toHaveBeenCalledWith(input, undefined); + // scheduleNotification() stamps the payload with the current protocol + // version (ensureNotificationVersion) before handing it to the repository. + expect(repository.create).toHaveBeenCalledWith( + { ...input, payload: { ...input.payload, version: 1 } }, + undefined, + ); }); it('rejects a missing executeAt', async () => { @@ -124,25 +130,26 @@ describe('NotificationAPI.scheduleNotification', () => { const input = { ...baseInput(), priority: 999 }; await expect(api.scheduleNotification(input)).rejects.toThrow(); expect(repository.create).not.toHaveBeenCalled(); -import { jest, describe, it, expect, beforeEach } from '@jest/globals'; -import { NotificationAPI } from './notification-api'; + }); +}); + import { PayloadTooLargeError, DEFAULT_MAX_PAYLOAD_SIZE_BYTES } from '../utils/payload-size-validator'; -import { NotificationType } from '../types/scheduled-notification'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -function futureDate(offsetMs = 60_000): Date { - return new Date(Date.now() + offsetMs); -} - -/** Return a payload whose JSON representation is exactly `targetBytes` bytes. */ -function payloadOfExactBytes(targetBytes: number): Record { - const overhead = Buffer.byteLength(JSON.stringify({ data: '' }), 'utf8'); // '{"data":""}' = 11 +/** + * Return a payload whose JSON representation is exactly `targetBytes` bytes + * *after* scheduleNotification() stamps it with the protocol version (#see + * ensureNotificationVersion) — the fixture already carries `version` so the + * stamping step is a no-op and doesn't grow the payload past the boundary. + */ +function payloadOfExactBytes(targetBytes: number): Record { + const overhead = Buffer.byteLength(JSON.stringify({ data: '', version: 1 }), 'utf8'); const fillLength = targetBytes - overhead; if (fillLength < 0) throw new Error(`targetBytes ${targetBytes} too small for wrapper`); - return { data: 'x'.repeat(fillLength) }; + return { data: 'x'.repeat(fillLength), version: 1 }; } // --------------------------------------------------------------------------- diff --git a/listener/src/services/notification-api.ts b/listener/src/services/notification-api.ts index be659483..82b4304e 100644 --- a/listener/src/services/notification-api.ts +++ b/listener/src/services/notification-api.ts @@ -28,17 +28,8 @@ import { buildRetryStatisticsPayload } from './retry-statistics'; * Includes support for idempotent request handling */ export class NotificationAPI { - private readonly maxPayloadSizeBytes: number; - - constructor( - private repository: ScheduledNotificationRepository, - private idempotencyService?: IdempotencyKeyService, - maxPayloadSizeBytes: number = DEFAULT_MAX_PAYLOAD_SIZE_BYTES - ) { - this.maxPayloadSizeBytes = maxPayloadSizeBytes; /** Maximum allowed serialised payload size in bytes. */ - readonly maxPayloadSizeBytes: number; - private readonly maxPayloadSizeBytes: number = DEFAULT_MAX_PAYLOAD_SIZE_BYTES; + readonly maxPayloadSizeBytes: number = DEFAULT_MAX_PAYLOAD_SIZE_BYTES; constructor( private repository: ScheduledNotificationRepository, @@ -118,7 +109,7 @@ export class NotificationAPI { validateNotificationMetadata(input.metadata ?? null); // Validate payload size BEFORE any storage or heavy processing operations. - // validatePayloadSize(input.payload, this.maxPayloadSizeBytes); + validatePayloadSize(input.payload, this.maxPayloadSizeBytes); logger.info('Scheduling new notification', { requestId, diff --git a/listener/src/services/notification-template-cache.ts b/listener/src/services/notification-template-cache.ts index 6c15abfd..3cb500ed 100644 --- a/listener/src/services/notification-template-cache.ts +++ b/listener/src/services/notification-template-cache.ts @@ -150,7 +150,10 @@ export class NotificationTemplateCache { * @notice Check if a template is currently cached */ has(templateId: string): boolean { - return this.cache.has(templateId); + // @types/node-cache (stub, v4.1) predates the real library's `.has()` + // method, so the cast is required even though it exists at runtime + // (see src/__mocks__/node-cache.ts, used in tests via moduleNameMapper). + return (this.cache as unknown as { has(key: string): boolean }).has(templateId); } } diff --git a/listener/src/services/notification-template-repository.ts b/listener/src/services/notification-template-repository.ts index 831b1d3e..36e6e5b7 100644 --- a/listener/src/services/notification-template-repository.ts +++ b/listener/src/services/notification-template-repository.ts @@ -4,11 +4,8 @@ import { CreateNotificationTemplateInput, AuditedNotificationTemplate, AuditedNotificationTemplateRow, - CreateNotificationTemplateInputOld, - NotificationTemplateOld, - NotificationTemplateRowOld, TemplateAuditRecord, - UpdateNotificationTemplateInputOld, + UpdateNotificationTemplateInput, } from '../types/notification-template'; import { TemplateAuditTrail } from './template-audit-trail'; import { NotificationTemplateCache } from './notification-template-cache'; @@ -38,7 +35,6 @@ export class NotificationTemplateRepository { ) {} async create(input: CreateNotificationTemplateInput): Promise { - async create(input: CreateNotificationTemplateInputOld): Promise { this.validateTemplateInput(input.id, input.name, input.body); const now = new Date(); @@ -72,8 +68,6 @@ export class NotificationTemplateRepository { async getById(templateId: string): Promise { const row = await this.db.get( - async getById(templateId: string): Promise { - const row = await this.db.get( 'SELECT * FROM notification_templates WHERE id = ?', [templateId], ); @@ -82,10 +76,9 @@ export class NotificationTemplateRepository { async update( templateId: string, - input: UpdateNotificationTemplateInputOld, + input: UpdateNotificationTemplateInput, actor: string, ): Promise { - ): Promise { const trimmedActor = actor?.trim(); if (!trimmedActor) { throw new TemplateValidationError('Actor is required for template updates'); @@ -101,7 +94,6 @@ export class NotificationTemplateRepository { this.validateTemplateInput(templateId, nextName, nextBody); const updated: AuditedNotificationTemplate = { - const updated: NotificationTemplateOld = { ...existing, ...input, name: nextName, @@ -162,21 +154,11 @@ export class NotificationTemplateRepository { 'SELECT * FROM notification_templates', ); return rows.map(row => this.rowToModel(row)); - async listAll(): Promise { - const rows = await this.db.all( - async getAll(): Promise { - const rows = await this.db.all( - 'SELECT * FROM notification_templates', - ); - return rows.map(row => this.rowToModel(row)); } - async listAll(): Promise { - const rows = await this.db.all( - async listAll(): Promise { - const rows = await this.db.all( + async listAll(): Promise { + const rows = await this.db.all( 'SELECT * FROM notification_templates ORDER BY created_at DESC', - [], ); return rows.map((row) => this.rowToModel(row)); } @@ -210,15 +192,12 @@ export class NotificationTemplateRepository { private hasTemplateChanges( previous: AuditedNotificationTemplate, next: AuditedNotificationTemplate, - previous: NotificationTemplateOld, - next: NotificationTemplateOld, ): boolean { return JSON.stringify(this.snapshotForComparison(previous)) !== JSON.stringify(this.snapshotForComparison(next)); } private snapshotForComparison(template: AuditedNotificationTemplate): Record { - private snapshotForComparison(template: NotificationTemplateOld): Record { return { id: template.id, name: template.name, @@ -232,7 +211,6 @@ export class NotificationTemplateRepository { } private rowToModel(row: AuditedNotificationTemplateRow): AuditedNotificationTemplate { - private rowToModel(row: NotificationTemplateRowOld): NotificationTemplateOld { return { id: row.id, name: row.name, diff --git a/listener/src/services/notification-template-service.ts b/listener/src/services/notification-template-service.ts index 28f9ca05..d1bd5a79 100644 --- a/listener/src/services/notification-template-service.ts +++ b/listener/src/services/notification-template-service.ts @@ -1,10 +1,8 @@ import { CreateNotificationTemplateInput, AuditedNotificationTemplate, - CreateNotificationTemplateInputOld, - NotificationTemplateOld, TemplateAuditRecord, - UpdateNotificationTemplateInputOld, + UpdateNotificationTemplateInput, } from '../types/notification-template'; import { NotificationTemplateRepository } from './notification-template-repository'; import { getTemplateCache, NotificationTemplateCache } from './notification-template-cache'; @@ -27,14 +25,12 @@ export class NotificationTemplateService { ) {} async create(input: CreateNotificationTemplateInput): Promise { - async create(input: CreateNotificationTemplateInputOld): Promise { const template = await this.repository.create(input); this.cache.set(String(template.id ?? ''), template); return template; } async listAll(): Promise { - async listAll(): Promise { return this.repository.listAll(); } @@ -49,7 +45,6 @@ export class NotificationTemplateService { */ renderTemplate( template: AuditedNotificationTemplate, - template: NotificationTemplateOld, variables: Record, ): { subject?: string; body: string } { const declared = template.variables ?? []; @@ -71,24 +66,18 @@ export class NotificationTemplateService { } async getById(templateId: string): Promise { - async getById(templateId: string): Promise { return this.cache.getOrLoad(templateId, () => this.repository.getById(templateId)); } async update( templateId: string, - input: UpdateNotificationTemplateInputOld, + input: UpdateNotificationTemplateInput, actor: string, ): Promise { return this.repository.update(templateId, input, actor); } async getAll(): Promise { - ): Promise { - return this.repository.update(templateId, input, actor); - } - - async getAll(): Promise { return this.repository.getAll(); } diff --git a/listener/src/services/payload-validation.integration.test.ts b/listener/src/services/payload-validation.integration.test.ts index c0779fa2..e459b698 100644 --- a/listener/src/services/payload-validation.integration.test.ts +++ b/listener/src/services/payload-validation.integration.test.ts @@ -30,12 +30,17 @@ jest.mock('../store/preference-store', () => ({ // Helpers // --------------------------------------------------------------------------- -/** Return a payload whose JSON representation is exactly `targetBytes` bytes. */ -function payloadOfExactBytes(targetBytes: number): Record { - const overhead = Buffer.byteLength(JSON.stringify({ data: '' }), 'utf8'); +/** + * Return a payload whose JSON representation is exactly `targetBytes` bytes + * *after* scheduleNotification() stamps it with the protocol version (see + * ensureNotificationVersion) — the fixture already carries `version` so the + * stamping step is a no-op and doesn't grow the payload past the boundary. + */ +function payloadOfExactBytes(targetBytes: number): Record { + const overhead = Buffer.byteLength(JSON.stringify({ data: '', version: 1 }), 'utf8'); const fillLength = targetBytes - overhead; if (fillLength < 0) throw new Error(`targetBytes ${targetBytes} too small`); - return { data: 'x'.repeat(fillLength) }; + return { data: 'x'.repeat(fillLength), version: 1 }; } function futureIso(offsetMs = 60_000): string { diff --git a/listener/src/services/scheduled-notification-repository.ts b/listener/src/services/scheduled-notification-repository.ts index 41d9a9c0..bfbc0a02 100644 --- a/listener/src/services/scheduled-notification-repository.ts +++ b/listener/src/services/scheduled-notification-repository.ts @@ -811,7 +811,6 @@ export class ScheduledNotificationRepository { return { id: row.id, - payload: row.payload, payload: decompressPayload(row.payload), payloadHash: row.payload_hash, notificationType: row.notification_type as any, diff --git a/listener/src/types/notification-template.ts b/listener/src/types/notification-template.ts index 6743d2db..3e8fe732 100644 --- a/listener/src/types/notification-template.ts +++ b/listener/src/types/notification-template.ts @@ -98,7 +98,6 @@ export interface ChannelNotificationTemplateRow { } export interface AuditedNotificationTemplate { -export interface NotificationTemplateOld { id: string; name: string; type: string;