Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
11 kB ยท 318 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319import { createInsertSchema, createSelectSchema } from "drizzle-zod";import { z } from "zod";
import { auditLog } from "./audit_log";
/** * Runtime schema + discriminated-union type for every audit action * services emit. Paired with `emitAudit` in `@openstatus/services`, * which calls `AuditEntrySchema.parse(...)` before writing the row. * * Every entity gets at most three verbs: `create`, `update`, `delete`. * Operation-specific semantics (acknowledge, resolve, accept, revoke, * rename, bulk-set) collapse into whichever of the three they * structurally are; readers recover the specific intent by inspecting * `changed_fields` on the row. * * `metadata` is reserved for side-channel context that is *not* * derivable from the entity snapshot โ e.g. `clonedFromMonitorId`, * `statusReportId` on a status-report update. Fields that change on the * entity itself belong in `before`/`after`. * * Adding a new emit site requires adding a variant here โ there is no * escape hatch. If a new operation isn't a create/update/delete, it * probably shouldn't be in the audit log. */
const metadataSchema = z.record(z.string(), z.unknown());const snapshotSchema = z.record(z.string(), z.unknown());
// Small helper so every variant reads the same โ keeps the discriminated// union legible when skimming.function action<A extends string, E extends string, I extends z.ZodTypeAny>( action: A, entityType: E, entityIdSchema: I, opts: { optionalMetadata?: boolean } = {},) { return z.object({ action: z.literal(action), entityType: z.literal(entityType), entityId: entityIdSchema, ...(opts.optionalMetadata ? { metadata: metadataSchema.optional() } : {}), });}
const intId = z.number().int();
const monitorActions = [ action("monitor.create", "monitor", intId, { optionalMetadata: true }), action("monitor.update", "monitor", intId), action("monitor.delete", "monitor", intId),] as const;
const pageActions = [ action("page.create", "page", intId, { optionalMetadata: true }), action("page.update", "page", intId), action("page.delete", "page", intId),] as const;
const pageComponentActions = [ action("page_component.create", "page_component", intId, { optionalMetadata: true, }), action("page_component.update", "page_component", intId), action("page_component.delete", "page_component", intId),] as const;
const pageComponentGroupActions = [ action("page_component_group.create", "page_component_group", intId, { optionalMetadata: true, }), action("page_component_group.update", "page_component_group", intId), action("page_component_group.delete", "page_component_group", intId),] as const;
const pageSubscriberActions = [ action("page_subscriber.create", "page_subscriber", intId, { optionalMetadata: true, }), action("page_subscriber.update", "page_subscriber", intId, { optionalMetadata: true, }), action("page_subscriber.delete", "page_subscriber", intId, { optionalMetadata: true, }),] as const;
const apiKeyActions = [ action("api_key.create", "api_key", intId, { optionalMetadata: true }), action("api_key.delete", "api_key", intId),] as const;
// `delete` covers every revocation path: manual, re-consent, member removal// and account deletion. `metadata` carries client name and scope on create.const oauthGrantActions = [ action("oauth_grant.create", "oauth_grant", intId, { optionalMetadata: true, }), action("oauth_grant.delete", "oauth_grant", intId, { optionalMetadata: true, }),] as const;
const notificationActions = [ action("notification.create", "notification", intId), action("notification.update", "notification", intId), action("notification.delete", "notification", intId),] as const;
const userActions = [action("user.delete", "user", intId)] as const;
const workspaceActions = [ action("workspace.update", "workspace", intId, { optionalMetadata: true }),] as const;
const maintenanceActions = [ action("maintenance.create", "maintenance", intId, { optionalMetadata: true, }), action("maintenance.update", "maintenance", intId), action("maintenance.delete", "maintenance", intId),] as const;
const monitorIncidentActions = [ action("monitor_incident.update", "monitor_incident", intId), action("monitor_incident.delete", "monitor_incident", intId),] as const;
const incidentActions = [ action("incident.create", "incident", intId, { optionalMetadata: true }), action("incident.update", "incident", intId, { optionalMetadata: true }), action("incident.delete", "incident", intId),] as const;
// Append-only timeline: events are never updated or deleted on their own.const incidentEventActions = [ action("incident_event.create", "incident_event", intId, { optionalMetadata: true, }),] as const;
const incidentPostmortemActions = [ action("incident_postmortem.create", "incident_postmortem", intId, { optionalMetadata: true, }), action("incident_postmortem.update", "incident_postmortem", intId, { optionalMetadata: true, }),] as const;
const slackUserActions = [ action("slack_user.create", "slack_user", intId, { optionalMetadata: true }), action("slack_user.delete", "slack_user", intId, { optionalMetadata: true }),] as const;
const statusReportActions = [ action("status_report.create", "status_report", intId, { optionalMetadata: true, }), action("status_report.update", "status_report", intId), action("status_report.delete", "status_report", intId),] as const;
const statusReportUpdateActions = [ action("status_report_update.create", "status_report_update", intId, { optionalMetadata: true, }), action("status_report_update.update", "status_report_update", intId, { optionalMetadata: true, }), action("status_report_update.delete", "status_report_update", intId, { optionalMetadata: true, }),] as const;
const invitationActions = [ action("invitation.create", "invitation", intId, { optionalMetadata: true }), action("invitation.update", "invitation", intId), action("invitation.delete", "invitation", intId),] as const;
// Several entities below carry only a subset of the create/update/delete// triplet. This is intentional, not an oversight:// - `member`: invitation-driven joins are recorded as `invitation.update`// (the acceptance stamp), so `member.create` covers only the paths that// mint a membership with no invitation behind it โ today, SSO JIT// provisioning.// - `monitor_incident`: rows originate from the checker pipeline, not user// mutations.// When those write paths migrate to the service layer, add the missing// verbs alongside.const memberActions = [ // `entityId` is the affected user's id. Member rows have no surrogate key โ // they're (userId, workspaceId) composite โ so the user id is the most // useful identifier to fix on in the audit log. action("member.create", "member", intId, { optionalMetadata: true }), action("member.delete", "member", intId),] as const;
const integrationActions = [ action("integration.create", "integration", intId, { optionalMetadata: true, }), action("integration.update", "integration", intId), action("integration.delete", "integration", intId),] as const;
const monitorTagActions = [ action("monitor_tag.create", "monitor_tag", intId, { optionalMetadata: true, }), action("monitor_tag.update", "monitor_tag", intId), action("monitor_tag.delete", "monitor_tag", intId),] as const;
const privateLocationActions = [ action("private_location.create", "private_location", intId, { optionalMetadata: true, }), action("private_location.update", "private_location", intId), action("private_location.delete", "private_location", intId),] as const;
// `entityId` is the workspace id โ SSO config has no surrogate key of its own.// Verified-domain rows fold into the same entity, distinguished by// `metadata.domain`; `delete` therefore means "a domain was dropped", never// "SSO was turned off" (that's an `update` on the `ssoEnabled` switch).const ssoActions = [ action("workspace_sso.create", "workspace_sso", intId, { optionalMetadata: true, }), action("workspace_sso.update", "workspace_sso", intId, { optionalMetadata: true, }), action("workspace_sso.delete", "workspace_sso", intId, { optionalMetadata: true, }),] as const;
export const auditActionSchema = z.discriminatedUnion("action", [ ...monitorActions, ...pageActions, ...pageComponentActions, ...pageComponentGroupActions, ...pageSubscriberActions, ...apiKeyActions, ...oauthGrantActions, ...notificationActions, ...userActions, ...workspaceActions, ...maintenanceActions, ...monitorIncidentActions, ...incidentActions, ...incidentEventActions, ...incidentPostmortemActions, ...slackUserActions, ...statusReportActions, ...statusReportUpdateActions, ...invitationActions, ...memberActions, ...integrationActions, ...monitorTagActions, ...privateLocationActions, ...ssoActions,]);
/** * Audit-entry input schema โ the action/entityType/entityId+metadata * discriminant plus optional `before`/`after` snapshots. * * Derived from `auditActionSchema` via `.and()` so the variant list * stays single-sourced โ adding a new action here would otherwise * require two parallel edits. * * The `before`/`after` snapshots are intentionally loose at runtime * (`Record<string, unknown>`): their shapes mirror the underlying * entity tables and binding them to this schema would force a cycle * with every column definition. Shape *presence* is enforced in * TypeScript via the `AuditEntry` type below, keyed off the action * verb suffix. */export const auditEntrySchema = auditActionSchema.and( z.object({ before: snapshotSchema.optional(), after: snapshotSchema.optional(), }),);
export const insertAuditLogSchema = createInsertSchema(auditLog);export const selectAuditLogSchema = createSelectSchema(auditLog);
export type AuditAction = z.infer<typeof auditActionSchema>;
type Snapshot = Record<string, unknown>;
/** * Snapshot presence is derived from the action's verb suffix: * * - `*.create` โ `after` required, `before` forbidden. * - `*.delete` โ `before` required, `after` forbidden. * - `*.update` โ both `before` and `after` required. * * Enforced at the `emitAudit` call site via the parameter type; Zod * stays loose at runtime so the schema file doesn't need to know each * entity's column list. */export type AuditEntry = AuditAction extends infer A ? A extends { action: `${string}.create` } ? A & { after: Snapshot; before?: never } : A extends { action: `${string}.delete` } ? A & { before: Snapshot; after?: never } : A extends { action: `${string}.update` } ? A & { before: Snapshot; after: Snapshot } : A & { before?: Snapshot; after?: Snapshot } : never;
export type AuditActionName = AuditAction["action"];export type AuditEntityType = AuditAction["entityType"];