You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 7604fcc
Browse filesBrowse the repository at this point in the historyBrowse files
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>
`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.
12
18
13
19
## Method
14
20
21
+
The slot's entire surface is one method:
22
+
15
23
```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:
- 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
0 commit comments