API reference¶
ConnectRPC serves sink.proto, registry.proto and query.proto as snake_case JSON with RFC 3339 timestamps. An empty search has no items key.
| Process | Serves |
|---|---|
Receiver (audit-writer) |
SinkService, RegistryService (0053) |
Query service (audit-query) |
QueryService |
Write¶
audit.v1.SinkService/Write takes a batch with a delivery, from workloads and adapters, never browsers. The response gives the accepted count, each Rejection with a reason, and the Durability. A structural refusal, such as an unknown catalogue version, refuses the whole batch. Decided in 0054 and 0059.
Catalogue delivery |
On the wire | The call returns |
|---|---|---|
block |
DELIVERY_BLOCK |
When the records are durable at the next hop |
async (default) |
DELIVERY_ASYNC |
At once; the record waits in the emitter's bounded queue |
durability |
Means |
|---|---|
DURABILITY_ARCHIVED |
The records are in the archive's bucket; the writer's own put reports this |
DURABILITY_QUEUED |
A replicated queue holds them; the JetStream and SQS publishers report this after every acknowledgement |
DURABILITY_LOGGED |
The process wrote them to its log |
DURABILITY_UNSPECIFIED |
The hop did not say; weaker than all others |
A higher value survives more. A hop reports its next hop's answer; a wrapper never reports more.
| Go call | Effect |
|---|---|
Guarantees() |
A sink declares the best it will ever report |
sink.Require(s, min) |
Refuses at start-up a chain that cannot give min |
sink.Guard(s, min) |
Does that, and fails any write whose acknowledgement is weaker at run time |
Client.Expecting |
States what a Connect client's far side gives, which it cannot learn |
DELIVERY_OUTBOX and DELIVERY_BEST_EFFORT are deprecated and never produced.
Catalogue registration¶
The receiver serves audit.v1.RegistryService on Write's port.
| Call | Behaviour |
|---|---|
RegisterCatalogue |
Called at start-up with the catalogue document and its extension schemas. An empty problems list means registered. A malformed catalogue returns its findings and the application does not start. The first use of a version copies it into the archive beside its records |
GetCatalogue, ListCatalogues |
Answer what this installation has been told. The list is short: an installation admits one application |
Query¶
audit.v1.QueryService/Search:
{
"profile": "security",
"filter": [
{
"occurred_at": {"between": {"from": "2026-09-01T00:00:00Z", "to": "2026-09-17T00:00:00Z"}},
"action": {"prefix": "wallet.credential."},
"outcome": {"in": {"values": ["failure", "denied"]}},
"targets": {"in": {"values": [{"type": "credential"}]}}
},
{ "actor_id": {"equal": "…"} }
],
"sort": [{"field": "FIELD_OCCURRED_AT", "order": "ORDER_DESC"}, {"field": "FIELD_ID", "order": "ORDER_ASC"}],
"limit": 100
}
Response:
{
"items": [ … ],
"query": { "profile": "security", "filter": [ … ], "sort": [ … ], "limit": 100 },
"cursors": { "self": "…", "first": "…", "prev": "…", "next": "…" }
}
first restarts the same question. There is no last. The service refuses operators not built.
| Type | Operators | Built |
|---|---|---|
| string | equal, not_equal, in, not_in, prefix | yes |
| string | is_null, is_not_null | no |
| time | between, greater_than, greater_than_or_equal, less_than, less_than_or_equal | yes, each as a half-open range |
| integer | equal | yes |
| integer | not_equal, in, not_in, between, comparisons | no |
| targets | in, not_in (an empty id matches the type) | yes |
| attributes | equal, not_equal, key_is_null, key_is_not_null | no |
| data path | string, integer or time predicate on a filterable property | yes |
A predicate on id takes whole UUIDs; anything else is invalid_argument. Get of a non-UUID is not_found.
Facets counts values of fields under the same filter:
{"profile": "security", "filter": [ … ], "fields": ["action", "outcome"], "limit_per_field": 10}
→ {"facets": [{"field": "action", "values": [{"value": "shop.order.placed", "count": "42"}]}]}
Get returns one record and its copy's location. digest_id and verified_at stay empty until seals (0061) set them:
{"profile": "security", "id": "0199b100-…"}
→ {"record": { … },
"provenance": {"object_key": "records/security/acme/2026/09/17/10/01K5…", "line": "3",
"digest_id": "", "verified_at": ""}}
Export starts a job. GetExport polls it and returns an expiring signed link:
{"profile": "security", "filter": [ … ], "format": "FORMAT_NDJSON"}
→ {"job_id": "x-…", "state": "EXPORT_STATE_PENDING"}
{"job_id": "x-…"}
→ {"state": "EXPORT_STATE_READY", "url": "https://…", "expires_at": "…", "records": "1204"}
| Limit | Value |
|---|---|
filter |
4 terms |
sort |
4 terms |
in |
100 values |
limit |
1000; a larger value is cut to it |
| Export | 100000 records; a larger match fails the job, asking to narrow the filter |
| Export recording | audit.export.requested is recorded before any read; if it cannot be, the export does not start |
| Refused reads | audit.search, audit.facets, audit.get and audit.pseudonym.resolved are recorded as failures; the same caller refused the same way on a profile is recorded once a minute |
| Collecting | Only the issuer and subject that asked for the job collect it, and only while they still hold the export grant on the profile. Each link issued is recorded (audit.export.completed) before it is returned; if it cannot be, no link is issued |
| CSV | A cell that begins with =, +, -, @, a tab or a carriage return is written with a leading ', so a spreadsheet does not run it |
A grant's window narrows a wider request. 64-bit integers (count, line, records) are JSON strings.
Access¶
audit.v1.QueryService/Access lists each profile a grant names with the caller's operations, tenants and period. It reads no record and is not recorded. Profiles with no effective operation are not listed. AuditView calls it when the host passes no profiles.
{}
→ {"profiles": [
{"profile": "security", "operations": ["search", "facets", "get", "tail", "export"],
"all_tenants": true},
{"profile": "history", "operations": ["search", "get"], "tenants": ["acme"],
"from": "2026-07-01T00:00:00Z"}
]}
Resolve¶
audit.v1.QueryService/Resolve maps a pseudonym to its identity: {"profile": "security", "tenant_id": "acme", "pseudonym": "ps_…"} returns {"identifier": "…"}. Name the profile that carried it.
| Rule | Detail |
|---|---|
| No key provider | unimplemented. This is the default (0055) |
| Offered when | The service has the keys: keys and archive in audit-query's configuration (a local adapter with keys.rootFile, or transit with a login that may decrypt) |
| Permission | The resolve operation on the profile, from an explicit rule only. No read grant or group name implies it. The tenant must be one the grant covers |
| Audit | audit.pseudonym.resolved is recorded and confirmed before the identity returns. If the trail cannot take it, nothing resolves. It names the pseudonym and the rule, never the identity |
| Scope | Only actor and subject pseudonyms. The writer seals each identifier under the tenant's key (identity/ in the archive) |
| Erasure | Destroying the key makes resolve fail with failed_precondition |
x-audit-sensitive: hmac |
Findable, never readable |
Errors¶
| Connect code | When |
|---|---|
invalid_argument |
A malformed filter, or a cursor from a different query |
permission_denied |
The grant excludes the profile, the operation or the tenant |
resource_exhausted |
A published limit or an export cap exceeded |
failed_precondition |
Resolve of a pseudonym whose tenant key was destroyed; retrying never helps |
not_found |
No such record, and also a record outside the grant, so a caller cannot learn it exists |
unimplemented |
Resolve with no key provider; export with no bucket configured |
unavailable |
The searcher is down. Also the default for anything unrecognised |