benjivers/notes Public Public: anyone can read this; nobody can change it.

Files 9965609c46dc660e7b5b52f91658bb8a67c37294
89 lines · 2.7 KB · ~791 tok Raw History
1
/**
2
 * The audit trail: every attempt to change something, as one row — who,
3
 * what (a resource and a verb from the contract), on which row, when, and
4
 * whether they were let. Refusals are rows too: a stranger knocking on a
5
 * door that stays shut is exactly what an owner wants to read later.
6
 *
7
 * Recording never fails a request. A row that cannot be written becomes a
8
 * line in the log, with everything the row would have said, and the
9
 * request goes on; the log is where the forge keeps a run's output.
10
 *
11
 * Reads are not recorded (a page view is not an event), and neither is a
12
 * request refused for coming too fast: a flood must not become the history,
13
 * or push the real history out.
14
 */
15
16
import type { Database } from "bun:sqlite";
17
import type { Who } from "./who.ts";
18
19
export interface AuditRow {
20
  readonly id: number;
21
  readonly at: string;
22
  readonly who_id: string | null;
23
  readonly who_name: string | null;
24
  readonly role: string;
25
  readonly resource: string;
26
  readonly verb: string;
27
  readonly target: string | null;
28
  readonly allowed: 0 | 1;
29
  /** `ok`, or the failure: `signin_required`, `forbidden`, `invalid`, `not_found`, `cross_site`. */
30
  readonly outcome: string;
31
}
32
33
export interface Attempt {
34
  readonly who: Who;
35
  readonly resource: string;
36
  readonly verb: string;
37
  readonly target?: string | number | undefined;
38
  readonly outcome: string;
39
}
40
41
/** The newest rows kept; older ones go as new ones come. */
42
export const KEEP = 50_000;
43
44
/** Was it let? Only the door says no; a bad body or a missing row got past it. */
45
export const allowed = (outcome: string): boolean =>
46
  outcome !== "signin_required" &&
47
  outcome !== "forbidden" &&
48
  outcome !== "cross_site";
49
50
export function record(db: Database, attempt: Attempt): void {
51
  const { who, resource, verb, target, outcome } = attempt;
52
  const row = {
53
    who_id: who.role === "anon" ? null : who.id,
54
    who_name: who.role === "anon" ? null : who.name,
55
    role: who.role,
56
    resource,
57
    verb,
58
    target: target === undefined ? null : String(target),
59
    allowed: allowed(outcome) ? 1 : 0,
60
    outcome,
61
  };
62
  try {
63
    const { lastInsertRowid } = db
64
      .query(
65
        "INSERT INTO audit (who_id, who_name, role, resource, verb, target, allowed, outcome) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
66
      )
67
      .run(
68
        row.who_id,
69
        row.who_name,
70
        row.role,
71
        row.resource,
72
        row.verb,
73
        row.target,
74
        row.allowed,
75
        row.outcome,
76
      );
77
    db.query("DELETE FROM audit WHERE id <= ?1").run(
78
      Number(lastInsertRowid) - KEEP,
79
    );
80
  } catch (error) {
81
    console.error(
82
      JSON.stringify({
83
        audit: row,
84
        at: new Date().toISOString(),
85
        unrecorded: String(error),
86
      }),
87
    );
88
  }
89
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts