@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 +15 -0
- package/dist/ledger.d.ts +55 -0
- package/dist/ledger.js +36 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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",
|