@wtfalch/audit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,6 +70,21 @@ const touched = await ledger.erase(tx, { subject: personId, pseudonym, email });
70
70
  uses: `tenant.invoice_paid` is out, `invoice.paid` is in. The core's
71
71
  namespaces are the trusted base's.
72
72
 
73
+ ## A writer for an embedding service
74
+
75
+ A service that wants a trail (a forum's moderation, a mail admin's writes) gets
76
+ a writer, never the signer. The host binds one per namespace:
77
+
78
+ ```ts
79
+ export const forumAudit = ledger.writer({ namespace: 'thread', handle: db });
80
+ createThreads({ db, gates, audit: forumAudit });
81
+ ```
82
+
83
+ The writer signs `thread.*` and refuses everything else; the actor is whoever
84
+ the service resolved; context, actor class and tenant default to what the
85
+ host bound. A caller passes its transaction as the second argument when the
86
+ row must commit with the change it records.
87
+
73
88
  ## A host's own columns
74
89
 
75
90
  A host that needs a column beside the ledger's (a team, a region) declares
package/dist/ledger.d.ts CHANGED
@@ -71,6 +71,55 @@ export interface Page {
71
71
  readonly id: number;
72
72
  } | null;
73
73
  }
74
+ /**
75
+ * What an embedding service hands a bound writer for one row. The namespace
76
+ * is fixed when the host binds the writer, so `action` must sit in it; the
77
+ * actor is whoever the service resolved (a forum's `who`, a mail admin's
78
+ * principal), never chosen by the ledger. Everything else is optional and
79
+ * defaults as `SignInput` does.
80
+ */
81
+ export interface WriterEvent {
82
+ readonly action: string;
83
+ readonly actor: {
84
+ readonly id: string;
85
+ readonly display: string;
86
+ readonly class?: string;
87
+ };
88
+ readonly target: {
89
+ readonly type: string;
90
+ readonly id: string;
91
+ };
92
+ readonly tenantId?: string | null;
93
+ readonly outcome?: string;
94
+ readonly reason?: string | null;
95
+ readonly reference?: string | null;
96
+ readonly before?: unknown;
97
+ readonly after?: unknown;
98
+ readonly subject?: {
99
+ readonly class: string;
100
+ readonly id: string;
101
+ } | null;
102
+ readonly request?: SignInput['request'];
103
+ readonly extra?: SignInput['extra'];
104
+ }
105
+ /**
106
+ * A writer the host bound to one namespace. `handle` is the caller's
107
+ * transaction when the row must commit with the change it records;
108
+ * otherwise the handle the host bound.
109
+ */
110
+ export type AuditWriter = (event: WriterEvent, handle?: Handle) => Promise<void>;
111
+ export interface WriterOptions {
112
+ /** The namespace the writer may sign in: `thread` admits `thread.pinned` and refuses `tenant.created`. */
113
+ readonly namespace: string;
114
+ /** The handle used when the caller passes none. */
115
+ readonly handle: Handle;
116
+ /** The context every row carries. Defaults to the vocabulary's first context. */
117
+ readonly context?: string;
118
+ /** The actor class when the event names none. Defaults to the vocabulary's first actor class. */
119
+ readonly actorClass?: string;
120
+ /** The tenant when the event names none. Defaults to null: a row on the estate log. */
121
+ readonly tenantId?: string | null;
122
+ }
74
123
  export interface EraseInput {
75
124
  readonly subject: string;
76
125
  readonly pseudonym: string;
@@ -105,5 +154,11 @@ export interface Ledger {
105
154
  erase(handle: Handle, input: EraseInput): Promise<number>;
106
155
  /** One tenant's rows, oldest first, for a tenant's export. Deterministic order; no secrets are in this table to omit. */
107
156
  exportRows(handle: Handle, tenantId: string): Promise<readonly AuditEventRow[]>;
157
+ /**
158
+ * A writer for one embedding service, bound to one namespace. This is what
159
+ * a host hands to `createThreads({ audit })` or a postmaster: it can write
160
+ * that namespace's events and nothing else, and it never sees `sign`.
161
+ */
162
+ writer(options: WriterOptions): AuditWriter;
108
163
  }
109
164
  export declare function createLedger(options: LedgerOptions): Ledger;
package/dist/ledger.js CHANGED
@@ -119,5 +119,40 @@ export function createLedger(options) {
119
119
  .where(eq(auditEvents.tenantId, tenantId))
120
120
  .orderBy(asc(auditEvents.occurredAt), asc(auditEvents.id));
121
121
  }
122
- return { vocabulary, tables: { events: auditEvents }, sign, page, erase, exportRows };
122
+ function writer(options) {
123
+ const { namespace } = options;
124
+ if (!/^[a-z][a-z0-9_]*$/.test(namespace)) {
125
+ throw new Error(`audit: "${namespace}" is not a namespace`);
126
+ }
127
+ const context = options.context ?? vocabulary.contexts[0];
128
+ const actorClass = options.actorClass ?? vocabulary.actorClasses[0];
129
+ if (context === undefined || actorClass === undefined) {
130
+ throw new Error('audit: the vocabulary has no context or actor class to default to');
131
+ }
132
+ return async (event, handle) => {
133
+ if (!event.action.startsWith(`${namespace}.`)) {
134
+ throw new Error(`audit: "${event.action}" is outside the namespace "${namespace}" this writer is bound to`);
135
+ }
136
+ await sign(handle ?? options.handle, {
137
+ action: event.action,
138
+ tenantId: event.tenantId === undefined ? (options.tenantId ?? null) : event.tenantId,
139
+ actor: {
140
+ class: event.actor.class ?? actorClass,
141
+ id: event.actor.id,
142
+ display: event.actor.display,
143
+ },
144
+ context,
145
+ target: event.target,
146
+ outcome: event.outcome,
147
+ reason: event.reason,
148
+ reference: event.reference,
149
+ before: event.before,
150
+ after: event.after,
151
+ subject: event.subject,
152
+ request: event.request,
153
+ extra: event.extra,
154
+ });
155
+ };
156
+ }
157
+ return { vocabulary, tables: { events: auditEvents }, sign, page, erase, exportRows, writer };
123
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/audit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The estate's append-only audit ledger: the table's shape and walls, erasure, export and readers, with a signer a host builds once inside its trusted base and never exports. Per-app Postgres, a host-supplied vocabulary of closed sets, no framework.",
5
5
  "repository": {
6
6
  "type": "git",