@devx-retailos/audit-log
v0.0.1
Published
Append-only who-did-what audit trail for retailOS. Non-blocking middleware captures actor, scope, operation, HTTP status, and payload snapshot across admin and store surfaces.
Keywords
Readme
@devx-retailos/audit-log
Append-only who-did-what audit trail for retailOS. Non-blocking middleware captures actor, scope, operation, HTTP status, and payload snapshot across admin and store surfaces.
Installation
pnpm add @devx-retailos/audit-logQuick start
// medusa-config.ts
import type { AuditRule } from "@devx-retailos/audit-log"
export default defineConfig({
plugins: [
{
resolve: "@devx-retailos/audit-log",
options: {
rules: [
{
method: "POST",
pathPattern: "/admin/retailos/orders",
module: "order",
operation_type: "create",
title: "Order created",
},
{
method: "GET",
pathPattern: "/admin/retailos/orders/:id",
module: "order",
operation_type: "read",
title: "Order viewed",
},
],
},
},
],
})How it works
The package registers a non-blocking middleware on every /admin/retailos/* and /store/retailos/* request. After the response is sent (res.finish), the middleware:
- Resolves the matching
AuditRulefrom the registry (by HTTP method + path pattern). - Reads actor context from
req.auth_contextand organization/store headers. - Redacts PII and secrets from the request body.
- Writes one
retailos_audit_log_entryrow — fire-and-forget, errors are swallowed.
If no rule matches, nothing is written. Routes not in the registry are invisible to the audit log.
Plugin options
| Option | Type | Default | Description |
|---|---|---|---|
| rules | AuditRule[] | [] | Rules to register at boot. |
| capturePayload | boolean | true for non-GET | Whether to snapshot the request body. |
| captureResponse | boolean | false | Whether to snapshot the response body. |
| maxBodyBytes | number | 100_000 | Byte cap on stored payload; truncated payloads include { _truncated: true, _original_bytes: N }. |
| redact | (body) => unknown | built-in | Override the PII redactor. |
| retentionDays | number | — | If set, pruneOlderThan uses this value when called from a scheduled job. |
| getActorContext | async (req) => AuditActorContext \| null | reads req.auth_context + headers | Custom actor resolver. |
| getLocationContext | async (req) => AuditLocationContext \| null | reads x-forwarded-for + user-agent | Custom location resolver. |
Audit rules
An AuditRule matches a request by method + path and controls what gets written:
interface AuditRule {
method: string // "GET" | "POST" | "PATCH" | "DELETE" | "*"
pathPattern: string | RegExp // "/admin/retailos/orders/:id" or a RegExp
module: string // e.g. "order"
operation_type: string // e.g. "create" | "read" | "update" | "delete"
title: string // short human-readable label
describe?: (req: MedusaRequest, responseBody: unknown) => string | undefined // optional dynamic description
}:param placeholders in string patterns match any single path segment. First-match wins.
Rules can also be registered at runtime:
const auditService = container.resolve(AUDIT_LOG_MODULE)
auditService.registerAuditRule({
method: "POST",
pathPattern: "/admin/retailos/returns",
module: "order",
operation_type: "return",
title: "Return initiated",
})API routes
All routes require the audit.read permission (grant it to the relevant RBAC role).
List entries
GET /admin/retailos/audit-logsQuery params: limit, offset, subject_id, subject_type, store_id, organization_id, module, source, operation_type, from (ISO date), to (ISO date), q (free-text on title/description/path).
Get single entry
GET /admin/retailos/audit-logs/:idExport CSV
GET /admin/retailos/audit-logs/export?from=2026-01-01&to=2026-06-30Requires audit.export permission. from and to are required. Capped at 10,000 rows.
Default PII redaction
The built-in defaultRedact strips values for keys matching (case-insensitive):
password, passwd, token, secret, api_key, apikey, authorization, auth, access_token, refresh_token, credit_card, card_number, card_no, cvv, cvc, expiry, ssn, otp, pin, email, phone, mobile, phone_number.
Redacted values are replaced with "[REDACTED]". The original object is never mutated.
Permissions
| Key | Description |
|---|---|
| audit.read | List and retrieve audit log entries |
| audit.export | Export entries as CSV |
Register via RBAC:
# Grant to a role (using the RBAC API)
POST /admin/retailos/roles/:roleId/permissions
{ "permission_id": "<perm_id>" }Immutability
Entries are append-only. updateAuditLogEntries and deleteAuditLogEntries on the service throw AuditLogImmutableError. The only sanctioned deletion path is auditService.pruneOlderThan(date), intended for scheduled retention jobs.
Error codes
| Code | Class | When |
|---|---|---|
| RETAILOS_AUDIT_IMMUTABLE | AuditLogImmutableError | update or delete attempted |
| RETAILOS_AUDIT_NOT_FOUND | AuditLogEntryNotFoundError | entry id does not exist |
| RETAILOS_AUDIT_EXPORT_TOO_LARGE | AuditLogExportTooLargeError | export exceeds 10,000 rows |
| RETAILOS_AUDIT_EXPORT_MISSING_DATE_RANGE | AuditLogExportMissingDateRangeError | export called without from/to |
