PSA/shared/rmm/alerts/contracts.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

202 lines
6.8 KiB
TypeScript

import { z } from 'zod';
import type { Knex } from 'knex';
import type { RmmProvider } from '@alga-psa/types';
/** Severity scale every provider normalizes into (matches rmm_alerts.severity). */
export type NormalizedRmmAlertSeverity = 'critical' | 'major' | 'moderate' | 'minor' | 'none';
export const NORMALIZED_RMM_ALERT_SEVERITIES = ['critical', 'major', 'moderate', 'minor', 'none'] as const;
export type NormalizedRmmAlertEventKind = 'triggered' | 'reset' | 'acknowledged';
/**
* Provider-agnostic alert event. Webhook routes and the reconciliation poller
* map provider payloads into this shape and hand it to processRmmAlertEvent().
*/
export interface NormalizedRmmAlertEvent {
tenantId: string;
integrationId: string;
provider: RmmProvider;
kind: NormalizedRmmAlertEventKind;
externalAlertId: string;
externalDeviceId?: string | null;
/**
* Stable identity of the firing condition on a device, used for dedup
* (e.g. NinjaOne statusCode falling back to activityType). When absent the
* pipeline falls back to alertClass/activityType/sourceType.
*/
conditionIdentity?: string | null;
activityType?: string | null;
alertClass?: string | null;
sourceType?: string | null;
severity: NormalizedRmmAlertSeverity;
message?: string | null;
deviceName?: string | null;
externalOrganizationId?: string | null;
/** ISO timestamp of the provider-side occurrence. */
occurredAt: string;
/** Raw provider payload, persisted to rmm_alerts.metadata. */
raw: Record<string, unknown>;
}
/** Optional per-provider outbound surface. Providers without one are skipped. */
export interface RmmAlertOutboundAdapter {
resetAlert(args: {
tenantId: string;
integrationId: string;
externalAlertId: string;
}): Promise<void>;
}
const timeOfDayPattern = /^([01]\d|2[0-3]):[0-5]\d$/;
export const rmmMaintenanceWindowRecurrenceSchema = z
.object({
type: z.literal('weekly'),
/** Days of week, 0 = Sunday … 6 = Saturday. */
days: z.array(z.number().int().min(0).max(6)).min(1),
startTime: z.string().regex(timeOfDayPattern, 'startTime must be HH:mm'),
endTime: z.string().regex(timeOfDayPattern, 'endTime must be HH:mm'),
timezone: z.string().min(1),
})
.strict()
.superRefine((value, ctx) => {
if (value.startTime === value.endTime) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'startTime and endTime must differ (a window spanning a full day should use multiple days instead)',
});
}
try {
new Intl.DateTimeFormat('en-US', { timeZone: value.timezone });
} catch {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Unknown timezone: ${value.timezone}` });
}
});
export type RmmMaintenanceWindowRecurrence = z.infer<typeof rmmMaintenanceWindowRecurrenceSchema>;
export const rmmAlertRuleConditionsSchema = z
.object({
severities: z.array(z.enum(NORMALIZED_RMM_ALERT_SEVERITIES)).optional(),
activityTypes: z.array(z.string().min(1)).optional(),
alertClasses: z.array(z.string().min(1)).optional(),
sourceTypes: z.array(z.string().min(1)).optional(),
/** External organization IDs (provider-side). */
organizationIds: z.array(z.string().min(1)).optional(),
/** Regex tested against the alert message. Validated at save time. */
messagePattern: z
.string()
.min(1)
.optional()
.superRefine((pattern, ctx) => {
if (pattern === undefined) return;
try {
new RegExp(pattern);
} catch {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'messagePattern is not a valid regular expression' });
}
}),
/** Case-insensitive substrings matched against the alert message. */
keywords: z.array(z.string().min(1)).optional(),
})
.strict();
export type RmmAlertRuleConditions = z.infer<typeof rmmAlertRuleConditionsSchema>;
export const rmmAlertRuleActionsSchema = z
.object({
createTicket: z.boolean().default(true),
boardId: z.string().uuid().optional(),
/** priority_id override; severity mapping applies when absent. */
priorityOverride: z.string().uuid().optional(),
assignToUserId: z.string().uuid().optional(),
ticketTemplate: z
.object({
titleTemplate: z.string().min(1).optional(),
descriptionTemplate: z.string().min(1).optional(),
})
.strict()
.optional(),
/** Close the linked ticket (if untouched) when the alert resets. */
autoResolveTicket: z.boolean().default(false),
/** Status to close into; tenant's first is_closed status when absent. */
autoResolveStatusId: z.string().uuid().optional(),
/** Reset the alert in the RMM when the linked ticket is closed. */
resetAlertOnTicketClose: z.boolean().default(true),
notifyUserIds: z.array(z.string().uuid()).optional(),
})
.strict();
export type RmmAlertRuleActions = z.infer<typeof rmmAlertRuleActionsSchema>;
/** rmm_alert_rules row as read from the DB (conditions/actions already JSON). */
export interface RmmAlertRuleRow {
tenant: string;
rule_id: string;
integration_id: string;
name: string;
description?: string | null;
is_active: boolean;
priority_order: number;
conditions: RmmAlertRuleConditions;
actions: RmmAlertRuleActions;
}
/** rmm_maintenance_windows row as read from the DB. */
export interface RmmMaintenanceWindowRow {
tenant: string;
window_id: string;
integration_id?: string | null;
client_id?: string | null;
asset_id?: string | null;
name: string;
is_active: boolean;
starts_at?: string | Date | null;
ends_at?: string | Date | null;
recurrence?: RmmMaintenanceWindowRecurrence | null;
}
export type RmmAlertProcessingOutcome =
| 'suppressed'
| 'ticket_created'
| 'occurrence_appended'
| 'recorded_only'
| 'resolved'
| 'acknowledged'
| 'skipped';
export interface RmmAlertProcessingResult {
outcome: RmmAlertProcessingOutcome;
alertId?: string;
ticketId?: string | null;
matchedRuleId?: string | null;
suppressedByWindowId?: string | null;
/** Non-fatal issues (e.g. a rule skipped over a bad stored regex). */
warnings: string[];
}
/**
* Side effects the pipeline triggers after commit. Injected by callers so
* shared/ stays free of dependencies on the event-bus and notification
* packages; see buildRmmAlertPipelineDeps() in @alga-psa/integrations.
*/
export interface RmmAlertPipelineDeps {
publishWorkflowEvent?: (args: {
eventType: 'RMM_ALERT_TRIGGERED' | 'RMM_ALERT_RESOLVED';
tenantId: string;
payload: Record<string, unknown>;
}) => Promise<void>;
notifyUsers?: (args: {
tenantId: string;
userIds: string[];
alert: { alertId: string; message?: string | null; severity: string; assetId?: string | null; ticketId?: string | null };
}) => Promise<void>;
logger?: Pick<Console, 'info' | 'warn' | 'error'>;
}
export interface RmmAlertProcessingContext {
knex: Knex;
deps?: RmmAlertPipelineDeps;
}