export const PRODUCT_NOTIFICATION_CONTRACT_VERSION = 1 as const; export const PRODUCT_NOTIFICATION_EVENT_CATALOG = [ { id: 'play.cron.succeeded', label: 'Cron success', description: 'A scheduled Play run completed successfully.', source: 'cron', outcome: 'succeeded', defaultEnabled: true, }, { id: 'play.cron.failed', label: 'Cron failure', description: 'A scheduled Play could not start or its run reached a failed terminal state.', source: 'cron', outcome: 'failed', defaultEnabled: true, }, { id: 'play.webhook.succeeded', label: 'Webhook success', description: 'An accepted webhook-triggered Play completed successfully.', source: 'webhook', outcome: 'succeeded', defaultEnabled: true, }, { id: 'play.webhook.failed', label: 'Webhook failure', description: 'An accepted webhook-triggered Play reached a failed terminal state.', source: 'webhook', outcome: 'failed', defaultEnabled: true, }, ] as const; export type ProductNotificationEventDefinition = (typeof PRODUCT_NOTIFICATION_EVENT_CATALOG)[number]; export type ProductNotificationEventType = ProductNotificationEventDefinition['id']; export type ProductNotificationSource = ProductNotificationEventDefinition['source']; export type ProductNotificationOutcome = ProductNotificationEventDefinition['outcome']; const PRODUCT_NOTIFICATION_EVENT_TYPE_SET = new Set( PRODUCT_NOTIFICATION_EVENT_CATALOG.map((event) => event.id), ); export function isProductNotificationEventType( value: unknown, ): value is ProductNotificationEventType { return ( typeof value === 'string' && PRODUCT_NOTIFICATION_EVENT_TYPE_SET.has(value) ); } export function getProductNotificationEventDefinition( eventType: string, ): ProductNotificationEventDefinition | null { return ( PRODUCT_NOTIFICATION_EVENT_CATALOG.find( (definition) => definition.id === eventType, ) ?? null ); } export function defaultProductNotificationEventTypes(): ProductNotificationEventType[] { return PRODUCT_NOTIFICATION_EVENT_CATALOG.filter( (definition) => definition.defaultEnabled, ).map((definition) => definition.id); } export const PRODUCT_NOTIFICATION_DESTINATION_KINDS = ['slack'] as const; /** * The legacy settings API predates named notification rules. Its shortcuts * operate on this deterministic compatibility rule when more than one Slack * notification exists. */ export const LEGACY_SLACK_NOTIFICATION_NAME = 'Slack notifications'; export const PRODUCT_NOTIFICATION_LEGACY_SELECTION_ERROR_CODE = 'PRODUCT_NOTIFICATION_LEGACY_SELECTION_AMBIGUOUS'; export type LegacySlackNotificationDestination = { kind: string; name: string; archivedAt?: number; }; export class ProductNotificationLegacySelectionError extends Error { constructor(message: string) { super(message); this.name = 'ProductNotificationLegacySelectionError'; } } export function isProductNotificationLegacySelectionError( error: unknown, ): boolean { if (!error || typeof error !== 'object' || !('data' in error)) return false; const data = (error as { data?: unknown }).data; return ( typeof data === 'object' && data !== null && (data as { code?: unknown }).code === PRODUCT_NOTIFICATION_LEGACY_SELECTION_ERROR_CODE ); } /** * Resolve the singleton target for the legacy settings API without ever * silently choosing one of several named notification rules. */ export function resolveLegacySlackNotification< T extends LegacySlackNotificationDestination, >(destinations: readonly T[]): T | null { const slackDestinations = destinations.filter( (destination) => destination.kind === 'slack' && destination.archivedAt === undefined, ); const canonical = slackDestinations.filter( (destination) => destination.name === LEGACY_SLACK_NOTIFICATION_NAME, ); if (canonical.length === 1) return canonical[0]!; if (canonical.length > 1) { throw new ProductNotificationLegacySelectionError( `More than one \"${LEGACY_SLACK_NOTIFICATION_NAME}\" Slack notification exists. Use the named notification API to choose one.`, ); } if (slackDestinations.length <= 1) return slackDestinations[0] ?? null; throw new ProductNotificationLegacySelectionError( 'More than one Slack notification is configured. Use the named notification API to choose one.', ); } export const PRODUCT_NOTIFICATION_SLACK_OAUTH_SCOPES = [ 'channels:read', 'chat:write', 'groups:read', 'im:write', ] as const; export type ProductNotificationDestinationKind = (typeof PRODUCT_NOTIFICATION_DESTINATION_KINDS)[number]; export const PRODUCT_NOTIFICATION_SLACK_TARGET_KINDS = [ 'channel', 'member', ] as const; export type ProductNotificationSlackTargetKind = (typeof PRODUCT_NOTIFICATION_SLACK_TARGET_KINDS)[number]; /** Slack member IDs begin with U (or W for Enterprise Grid workspaces). */ export function isProductNotificationSlackMemberId( value: unknown, ): value is string { return typeof value === 'string' && /^[UW][A-Z0-9]{8,}$/.test(value.trim()); } export const PRODUCT_NOTIFICATION_DELIVERY_STATES = [ 'pending', 'delivering', 'delivered', 'retry_scheduled', 'dead_lettered', 'expired', 'suppressed', ] as const; export type ProductNotificationDeliveryState = (typeof PRODUCT_NOTIFICATION_DELIVERY_STATES)[number]; /** Immediate send plus four bounded retries. */ export const PRODUCT_NOTIFICATION_RETRY_DELAYS_MS = [ 0, 60_000, 5 * 60_000, 15 * 60_000, 60 * 60_000, ] as const; export const PRODUCT_NOTIFICATION_MAX_ATTEMPTS = PRODUCT_NOTIFICATION_RETRY_DELAYS_MS.length; /** A claimed delivery must finish within this window or a recovery attempt may claim it. */ export const PRODUCT_NOTIFICATION_DELIVERY_LEASE_MS = 2 * 60_000; export const PRODUCT_NOTIFICATION_PENDING_LIMIT_PER_DESTINATION = 100; export const PRODUCT_NOTIFICATION_DLQ_REPLAY_LIMIT = 25; export const PRODUCT_NOTIFICATION_SUCCESS_TTL_MS = 15 * 60_000; export const PRODUCT_NOTIFICATION_FAILURE_TTL_MS = 24 * 60 * 60_000; export function productNotificationEventTypeFor(input: { source: ProductNotificationSource; outcome: ProductNotificationOutcome; }): ProductNotificationEventType { return `play.${input.source}.${input.outcome}` as ProductNotificationEventType; } export function productNotificationEventExpiresAt(input: { eventType: ProductNotificationEventType; occurredAt: number; }): number { const definition = getProductNotificationEventDefinition(input.eventType); return ( input.occurredAt + (definition?.outcome === 'succeeded' ? PRODUCT_NOTIFICATION_SUCCESS_TTL_MS : PRODUCT_NOTIFICATION_FAILURE_TTL_MS) ); }