Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/audit/lib/event.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// `id` and `version` are stamped at the emit boundary, not by the caller.

import type { AuditActor, AuditContext, AuditOutcome, AuditTarget, AuditTrailVersion } from '@nangohq/types';
import type { AuditActor, AuditContext, AuditEventKey, AuditOutcome, AuditTarget, AuditTrailVersion } from '@nangohq/types';

export type {
AuditActor,
AuditActorType,
AuditContext,
AuditEventKey,
AuditOutcome,
AuditTarget,
AuditTargetType,
Expand Down Expand Up @@ -152,6 +153,13 @@ export type AuditResourceAction =
| { resource: 'mfa'; action: 'enrolled' | 'enabled' | 'disabled' | 'recovery_regenerated' }
| { resource: 'mfa'; action: 'verified'; metadata?: MfaVerifiedMetadata };

type EmittedKey<T extends { resource: string; action: string }> = T extends unknown ? `${T['resource']}.${T['action']}` : never;

type EmittedButNotInVocabulary = Exclude<EmittedKey<AuditResourceAction>, AuditEventKey>;
type InVocabularyButNotEmitted = Exclude<AuditEventKey, EmittedKey<AuditResourceAction>>;

true satisfies [EmittedButNotInVocabulary, InVocabularyButNotEmitted] extends [never, never] ? true : never;

export type AuditEvent = AuditEventCommon & AuditResourceAction;

export type StoredAuditEvent = AuditEvent & { id: string; version: AuditTrailVersion };
8 changes: 6 additions & 2 deletions packages/server/lib/middleware/audit.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { RequestLocals } from '../utils/express.js';
import type { AuditActor, AuditContext, AuditEvent, AuditOutcome, AuditTarget, AuditTargetType, MfaVerifiedMetadata } from '@nangohq/audit';
import type {
AcceptInvite,
AuditAction,
AuditActionOf,
AuditPolicy,
AuditResource,
AuditScope,
Expand Down Expand Up @@ -80,7 +80,11 @@ type AuditRequest<TEndpoint extends Endpoint<any>> = Request<TEndpoint['Params']
type AuditableEndpoint = Endpoint<any> & { Audit: AuditPolicy };

const Audit = {
auditable: <R extends AuditResource, A extends AuditAction, S extends AuditScope>(policy: { resource: R; action: A; scope: S }): AuditPolicy<R, A, S> => ({
auditable: <R extends AuditResource, A extends AuditActionOf<R>, S extends AuditScope>(policy: {
resource: R;
action: A;
scope: S;
}): AuditPolicy<R, A, S> => ({
kind: 'audit',
...policy
})
Expand Down
75 changes: 20 additions & 55 deletions packages/types/lib/audit-trail/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,26 @@ export type AuditTrailVersion = '2026-07-16';
export type AuditActorType = 'user' | 'api_key' | 'system' | 'anonymous';
export type AuditOutcome = 'success' | 'failure' | 'denied';

export type AuditResource =
| 'connection'
| 'sync'
| 'function'
| 'integration'
| 'api_key'
| 'member'
| 'team'
| 'user'
| 'environment'
| 'app_auth'
| 'mfa'
| 'billing'
| 'audit_log';
interface AuditEventTable {
connection: 'created' | 'updated' | 'metadata_updated' | 'refreshed' | 'deleted';
sync: 'enabled' | 'disabled' | 'paused' | 'started' | 'triggered' | 'cancelled' | 'frequency_changed' | 'variant_created' | 'variant_deleted';
function: 'deployed' | 'upgraded' | 'deleted';
integration: 'created' | 'updated' | 'deleted';
api_key: 'created' | 'updated' | 'deleted';
member: 'invited' | 'invite_accepted' | 'invite_declined' | 'invite_revoked' | 'role_changed' | 'removed';
team: 'updated';
user: 'updated';
environment: 'created' | 'updated' | 'variables_changed' | 'webhook_urls_changed' | 'deleted';
app_auth: 'login' | 'logout' | 'signup' | 'password_changed' | 'password_reset';
mfa: 'enrolled' | 'enabled' | 'disabled' | 'verified' | 'recovery_regenerated';
billing: 'plan_changed' | 'trial_extended' | 'details_changed' | 'payment_method_added' | 'payment_method_removed';
}

export type AuditResource = keyof AuditEventTable;
export type AuditActionOf<R extends AuditResource> = AuditEventTable[R];
export type AuditAction = AuditActionOf<AuditResource>;

export type AuditAction =
| 'created'
| 'reauthorized'
| 'refreshed'
| 'updated'
| 'metadata_updated'
| 'deleted'
| 'paused'
| 'started'
| 'cancelled'
| 'enabled'
| 'disabled'
| 'frequency_changed'
| 'triggered'
| 'variant_created'
| 'variant_deleted'
| 'upgraded'
| 'deployed'
| 'invited'
| 'invite_accepted'
| 'invite_declined'
| 'invite_revoked'
| 'removed'
| 'role_changed'
| 'variables_changed'
| 'webhook_urls_changed'
| 'login'
| 'logout'
| 'signup'
| 'password_changed'
| 'password_reset'
| 'enrolled'
| 'recovery_regenerated'
| 'verified'
| 'plan_changed'
| 'trial_extended'
| 'details_changed'
| 'payment_method_added'
| 'payment_method_removed'
| 'exported';
export type AuditEventKey = { [R in AuditResource]: `${R}.${AuditEventTable[R]}` }[AuditResource];

export type AuditScope = 'account' | 'environment';

Expand Down Expand Up @@ -86,7 +51,7 @@ export interface AuditContext {
// decision — a customer endpoint cannot be added without consciously opting in or out. The policy's
// resource/action/scope are captured as type parameters so the endpoint's declaration and the
// middleware spec that services it are checked against each other by the compiler.
export interface AuditPolicy<R extends AuditResource = AuditResource, A extends AuditAction = AuditAction, S extends AuditScope = AuditScope> {
export interface AuditPolicy<R extends AuditResource = AuditResource, A extends AuditActionOf<R> = AuditActionOf<R>, S extends AuditScope = AuditScope> {
kind: 'audit';
resource: R;
action: A;
Expand Down
Loading