Skip to content

Invariants Checklist & PR Gates

Purpose: Release-blocking requirements that can be statically and dynamically verified. Credit: Invariant definitions from GPT5.2 review.


Invariants Checklist

These are release-blockers. Every PR that touches vault-core must pass.

Classification and Downgrade Safety

ID Invariant Verification Method
INV-01 Every encrypted field/document is tagged with encryption_class ∈ {A,B,C} at creation; no "unknown/nullable" paths Schema constraint + NOT NULL + CHECK constraint
INV-02 No class downgrade paths exist by default (C→A prohibited; B→A and C→B require explicit typed confirmation + waiting period + audit) Code review + integration test
INV-03 Metadata may be Class A even if content is B/C, but the boundary must be explicit (separate fields/classes) Schema review + field naming convention

Class C: Zero-Knowledge Means Zero-Knowledge

ID Invariant Verification Method
INV-04 Server code contains no Class C plaintext operations: no decrypt, no PDF gen, no content search, no content in notifications, no "verifyContent" Static analysis scan
INV-05 Server releases ciphertext only for Class C; executor access delivers ciphertext blob + metadata only Integration test assertion
INV-06 Any attempt to perform a forbidden Class C operation is blocked and produces an audit event (encryption_class_violation_attempt) Runtime middleware + integration test

Class B: Escrowable, But Only Via Explicit Release

ID Invariant Verification Method
INV-07 Client encrypts before upload; server does not casually decrypt B content Code path analysis
INV-08 Every server-side Class B decryption is only inside executeTrigger() (or designated release handler), never elsewhere Static analysis + code review
INV-09 Each Class B decryption emits an audit event with: reason, triggerId, actor/system attribution, jurisdiction/policy context Integration test assertion

Key Management Correctness

ID Invariant Verification Method
INV-10 Class B uses "random master key + password-derived wrapping key"; escrow splits the master key, not the wrapping key Unit test + code review
INV-11 Password change re-wraps the same master key; escrow shares remain valid Integration test
INV-12 Class C uses envelope encryption (random DEK + password-derived KEK); password change re-wraps, no bulk re-encrypt Unit test + integration test

Trigger State Machine & Timing

ID Invariant Verification Method
INV-13 armed → triggered requires 2+ signals (single missed check-in only sends reminder) State machine unit tests
INV-14 Challenge and abort windows are enforced: no premature transitions Timing integration tests
INV-15 Abort works in armed, triggered, pending_execution; does not work once executing begins State machine tests

Audit Trail Integrity

ID Invariant Verification Method
INV-16 Every state transition and side effect has an audit entry; sequence numbers have no gaps; hash chain validates Audit verification tests
INV-17 Executor cannot access vault content before release; access links are time-limited Authorization integration tests

PR Gate Implementation

Gate 1: Static Scan (CI)

Fail CI if server code contains forbidden Class C operations:

// Forbidden patterns for Class C documents
const FORBIDDEN_CLASS_C_PATTERNS = [
  /decrypt\s*\(/,           // No decryption
  /generatePDF\s*\(/,       // No server-side PDF
  /searchContent\s*\(/,     // No content search
  /verifyContent\s*\(/,     // No content verification
  /includeContent\s*\(/,    // No content in notifications
];

// Static scan should fail if these appear in paths that could touch Class C

Implementation:

# Example grep-based scan (replace with AST-based for production)
grep -r "decrypt(" src/server/ --include="*.ts" | grep -v "// Class A only"

Gate 2: Runtime Enforcement

The enforceEncryptionClass() middleware must be exercised:

// lib/security/middleware.ts
export async function enforceEncryptionClass(
  document: Document,
  operation: string,
  context: OperationContext
) {
  const rules = ENCRYPTION_CLASS_RULES[document.encryption_class];

  if (!rules.allowedOperations.includes(operation)) {
    // Log violation attempt
    await auditLog.record({
      type: 'encryption_class_violation_attempt',
      documentId: document.id,
      encryptionClass: document.encryption_class,
      attemptedOperation: operation,
      actor: context.actor,
      blocked: true,
    });

    throw new EncryptionClassViolationError(
      `Operation '${operation}' not allowed for Class ${document.encryption_class}`
    );
  }
}

Gate 3: Integration Tests

Run North Star acceptance test twice (B + C variants):

describe('North Star Acceptance Test', () => {
  describe.each(['B', 'C'])('Class %s Document', (encryptionClass) => {
    // ... test steps 1-6 ...

    test('Step 5: Execution delivers correct content type', async () => {
      if (encryptionClass === 'C') {
        // Class C: ciphertext only
        const download = await executor.downloadDocument(documentId);
        expect(download.contentType).toBe('application/octet-stream');
        expect(download.content).toEqual(originalCiphertext);

        // Verify no decryption events
        const audit = await getAuditLog(documentId);
        expect(audit).not.toContainEventType('class_b_decryption');
        expect(audit).not.toContainEventType('generate_pdf');
        expect(audit).toContainEventType('class_c_document_released');
      } else {
        // Class B: plaintext or recovered key material
        const download = await executor.downloadDocument(documentId);
        // Assert based on Phase 0.5 promise (plaintext package or ciphertext + key)

        // Verify decryption event exists
        const audit = await getAuditLog(documentId);
        expect(audit).toContainEventType('class_b_decryption');
        expect(audit.find(e => e.type === 'class_b_decryption')).toMatchObject({
          reason: 'trigger_execution',
          triggerId: expect.any(String),
        });
      }
    });
  });
});

Negative Assertions

Add to test harness:

NEG-01: Class C Zero-Knowledge Verification

test('NEG-01: Class C has no server-side content operations', async () => {
  // After full C-variant test run
  const audit = await getFullAuditLog(vaultId);

  // No decryption events for C documents
  const cDocEvents = audit.filter(e => e.documentClass === 'C');
  expect(cDocEvents).not.toContainEventType('class_b_decryption');
  expect(cDocEvents).not.toContainEventType('generate_pdf');
  expect(cDocEvents).not.toContainEventType('content_search');
  expect(cDocEvents).not.toContainEventType('content_preview');
});

NEG-02: Pre-Release Access Denied

test('NEG-02: Executor access denied before release', async () => {
  // Setup: trigger armed but not executed
  const { triggerId, documentId } = await setupArmedTrigger();

  // Executor attempts access
  const response = await executor.attemptAccess(documentId);
  expect(response.status).toBe(403);

  // Verify denial is audited
  const audit = await getAuditLog(documentId);
  expect(audit).toContainEventType('access_denied');
});

Audit Event Types

CANONICAL REGISTRY: 13-event-schema.md is the single source of truth for event definitions.

This section lists event types for quick reference. For payload schemas and sequence mapping, see 13-event-schema.md.

Trigger Lifecycle Events

  • trigger_created
  • trigger_armed
  • trigger_updated
  • trigger_disarmed
  • check_in_recorded
  • check_in_missed
  • reminder_sent
  • reminder_acknowledged
  • reminder_unacknowledged
  • secondary_signal_confirmed
  • trigger_fired
  • challenge_window_started
  • challenge_window_elapsed
  • abort_requested
  • trigger_aborted
  • execution_started
  • access_granted
  • executor_notified
  • execution_completed
  • trigger_released
  • trigger_finalized

Document Events

  • document_uploaded
  • document_downloaded
  • document_deleted
  • document_version_created

Vault Events

  • vault_created
  • vault_updated
  • vault_deleted
  • executor_assigned
  • executor_removed

Encryption Events

  • master_key_unwrapped
  • class_b_decryption
  • class_c_document_released
  • encryption_class_violation_attempt

Access Events

  • access_link_generated
  • access_link_used
  • access_link_expired
  • access_denied

Recovery Events

  • recovery_requested
  • recovery_share_submitted
  • recovery_completed
  • recovery_cancelled

This document defines the invariants that must hold for the system to be trustworthy. All PRs must pass these gates.