Skip to content

Commit 7604fcc

Browse files
claude[bot]claude
andauthored
docs(kernel): document services.audit as the slot it actually is (#9587)
The page described `record()` / `'set' | 'reset'`, which is `SettingsAuditSink` — a constructor-injected sink, never registered as or resolved from the kernel `audit` slot. The slot's only registrant is plugin-audit's `createAuthEventAuditSink`, whose surface is `recordAuthEvent(event)` with `action: 'login' | 'logout'`. Rewritten around the real slot, plus a disambiguation section naming the settings sink so the shape stays discoverable. Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja Co-authored-by: Claude <noreply@anthropic.com>
1 parent b057e53 commit 7604fcc

1 file changed

Lines changed: 86 additions & 12 deletions

File tree

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,100 @@
11
---
22
title: services.audit
3-
description: Audit write bridge currently used by settings/runtime integrations.
3+
description: Write ingress for audit events that are not CRUD — today, auth session events.
44
---
55

66
# `services.audit`
77

88
- **Stability:** `experimental`
9-
- **Canonical source:** `packages/services/service-settings/src/settings-service.types.ts`
9+
- **Canonical source:** `packages/plugins/plugin-audit/src/auth-event-audit.ts`
1010

11-
`services.audit` is currently a lightweight sink abstraction used by runtime services (for example `service-settings`) to emit append-only audit entries.
11+
`services.audit` is the kernel service slot that `@objectstack/plugin-audit` registers
12+
during `init()`. It is the **write ingress for audit events that are not CRUD** — today,
13+
auth session events — and every call appends one row to `sys_audit_log`.
14+
15+
Record-level `create` / `update` / `delete` rows do **not** travel through this slot. The
16+
same plugin writes those from ObjectQL lifecycle hooks, with no service call involved, so
17+
resolving this service is only ever necessary for events the CRUD lifecycle cannot see.
1218

1319
## Method
1420

21+
The slot's entire surface is one method:
22+
1523
```ts
16-
services.audit.record(entry: {
24+
services.audit.recordAuthEvent(event: {
25+
action: 'login' | 'logout';
26+
userId: string;
27+
sessionId?: string;
28+
organizationId?: string;
29+
ipAddress?: string;
30+
userAgent?: string;
31+
actor?: string;
32+
context?: Record<string, unknown>;
33+
}): Promise<void>
34+
```
35+
36+
- **`action`** is a closed literal union. Every `sys_audit_log` field is `readonly` and
37+
validation skips readonly/system fields, so the object's declared action vocabulary
38+
validates nothing in either direction — a misspelled action would be accepted silently.
39+
This union is the only structural protection an author gets, which is why the caller
40+
hands over an **event** (what happened) rather than a row (what to store).
41+
- **`userId`** is required and must be a real `sys_user` id — it lands on the strict
42+
`user_id` lookup.
43+
- **`sessionId`** lands on `record_id`, with `object_name` fixed to `sys_session`, so the
44+
ledger row is navigable to the session it describes.
45+
- **`organizationId`** is the session's active organization. It stamps the row's tenant
46+
columns, and an unstamped row is one that non-administrator members can never see
47+
through the security layer's predicate.
48+
- **`actor`** is the principal that *caused* the event when that is not the subject — an
49+
administrator's user id on an impersonation session. Omitted, the subject is recorded as
50+
having acted for themselves.
51+
- **`context`** is free-form and is serialized into `metadata`, e.g. the endpoint that
52+
produced the event (`/sign-in/email`, `/sign-out`).
53+
54+
## Notes
55+
56+
- This API is intentionally write-only. It is not a query API.
57+
- For querying audit history, use `services.data` against `sys_audit_log`/`sys_activity` objects.
58+
- The slot is registered in `init()`, while the data engine only resolves at
59+
`kernel:ready`. The sink therefore resolves its engine per call, so it is safe to hold a
60+
reference from the moment the plugin initializes.
61+
- `@objectstack/plugin-auth` is the only in-repo consumer. It resolves the slot lazily
62+
through a locally-declared structural surface and calls it from the session lifecycle,
63+
so neither package depends on the other.
64+
65+
## Typical Errors
66+
67+
`recordAuthEvent` **never throws**. An audit write must not turn a valid sign-in into an
68+
error, so every failure is caught, and callers do not need a surrounding `try`/`catch`.
69+
The consequences are worth knowing precisely, because none of them are visible from the
70+
caller's side:
71+
72+
- A failed ledger insert is reported at `error` level **once per process** — every later
73+
failure drops to `debug` — and the row is simply lost. Nothing retries it. The sign-in
74+
itself succeeded and returned 200, so the shipped `auth_events` list view and the
75+
system-overview widgets keep showing an empty, healthy-looking screen.
76+
- The call **silently no-ops** when no data engine resolves, or when `userId` is absent.
77+
A caller that passes an event without a subject records nothing and is told nothing.
78+
- The most common cause of a failing insert is a datasource split rather than a broken
79+
write: `sys_audit_log` carries an ADR-0057 `audit` lifecycle class, which routes it to a
80+
dedicated `telemetry` datasource whenever one is registered — `os dev` provisions one by
81+
default as a sibling SQLite file. A "no such table" here usually means the write ran
82+
against a different datasource than the one the table was created in.
83+
84+
## Not this: the settings audit sink
85+
86+
A second, unrelated audit shape shares the word *audit* and is easy to reach for by
87+
mistake. `SettingsAuditSink` — canonical source
88+
`packages/services/service-settings/src/settings-service.types.ts` — is a
89+
**constructor-injected** sink that `service-settings` calls after a settings write lands:
90+
91+
```ts
92+
record(entry: {
1793
namespace: string;
1894
key: string;
1995
scope: SpecifierScope;
2096
userId?: string;
97+
tenantId?: string;
2198
actor?: string;
2299
action: 'set' | 'reset';
23100
valueDigest: string;
@@ -26,11 +103,8 @@ services.audit.record(entry: {
26103
}): Promise<void> | void
27104
```
28105

29-
## Notes
30-
31-
- This API is intentionally write-only.
32-
- For querying audit history, use `services.data` against `sys_audit_log`/`sys_activity` objects.
33-
34-
## Typical Errors
35-
36-
Sink implementations should avoid throwing into user paths: the runtime caller (for example `service-settings`) awaits `record()` without a surrounding `try`/`catch`, so a throwing sink propagates its error and fails the caller's write.
106+
It is never registered as, or resolved from, this service slot, and the settings service
107+
wraps the call in its own `try`/`catch` so a throwing sink cannot fail the write it
108+
describes. Calling `getService('audit').record({ ... })` therefore fails with a
109+
`TypeError`: the resolved object has no `record`, and it accepts neither `'set'` nor
110+
`'reset'`.

0 commit comments

Comments
 (0)