Skip to content

Commit bd9e559

Browse files
RedStar071CopilotCopilotlorypelliautofix-ci[bot]
authored
feat(audit): add tamper-evident evlog audit trail (#154)
### πŸ”— Linked issue <!-- No open issue β€” this is a self-contained infrastructure feature --> ### 🧭 Context WolfStar.rocks needed a tamper-evident audit trail for security-relevant actions. Without it, there was no durable record of who changed guild settings, when sessions were cleared, or when OAuth CSRF verification failed. This PR introduces the full audit pipeline powered by `evlog`'s `log.audit()` API, persisted to PostgreSQL with a SHA-256 hash chain. ### πŸ“š Description **Infrastructure (new files)** - `shared/audit/actions.ts` β€” 6 typed action creators (`guildSettingsUpdate`, `guildSettingsAccessDenied`, `userLogin`, `userLogout`, `sessionRefresh`, `oauthStateInvalid`) plus the `AuditActionName` union type - `shared/audit/envelope.ts` β€” `canonicalize()` (deterministic sorted-key JSON) and `hashEnvelope()` (SHA-256 hex) for the hash chain - `server/utils/audit/postgres-drain.ts` β€” audit drain: serializable transaction, upserts `AuditChainHead`, hashes each envelope against the previous hash, swallows `P2002` unique constraint violations, retries `P2034` serialization failures up to 5Γ— with exponential backoff - `server/utils/audit/actor-bridge.ts` β€” resolves the actor from the `evlog` enrichment context - `server/plugins/00.evlog-init.ts` β€” initialises evlog with redaction config on boot **Instrumentation (modified files)** - `server/api/guilds/[guild]/settings.patch.ts` β€” wraps `canManage()` in try/catch to emit `guildSettingsAccessDenied` on denial; captures before/after snapshot and emits `guildSettingsUpdate` with `auditDiff` on success - `server/api/auth/discord.get.ts` β€” emits `userLogin` in the `onSuccess` callback - `server/api/auth/refresh.get.ts` β€” emits `userLogout` when tokens are missing; emits `sessionRefresh` success or failure after the token exchange - `server/api/auth/verify-state.get.ts` β€” emits `oauthStateInvalid` with the specific `reason` code (`missing-fields`, `decode-failed`, `nonce-mismatch`, `expired`, `bad-hmac`) before throwing 400 **Tests** - `test/unit/audit/envelope.spec.ts` β€” 8 tests covering `canonicalize` stability and `hashEnvelope` correctness - `test/unit/audit/actions.spec.ts` β€” 8 tests covering all 6 action creators - `test/unit/audit/postgres-drain.spec.ts` β€” 6 tests covering early-return, Prisma call, PII exclusion, hash correctness, P2002 swallow, and P2034 retry exhaustion - `test/unit/server/utils/oauth-state.test.ts` β€” updated 17 tests to assert on `result.valid` and `result.reason` after the typed result refactor **Documentation** - `AGENTS.md` β€” added Audit Logging section with action registry table and instrumentation pattern - `README.md` β€” added Audit Trail feature bullet All 383 unit tests pass. Zero new type errors in our files. `pnpm lint:fix` clean. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: lorypelli <87276663+lorypelli@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
1 parent 7e1b45b commit bd9e559

27 files changed

Lines changed: 1192 additions & 78 deletions
Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,50 @@
11
---
22
name: evlog-skilld
3-
description: "Wide event logging library with structured error handling. Inspired by LoggingSucks. ALWAYS use when writing code importing \"evlog\". Consult for debugging, best practices, or modifying evlog."
3+
description: "ALWAYS use when writing code importing \"evlog\". Consult for debugging, best practices, or modifying evlog."
44
metadata:
5-
version: 2.13.0
6-
generated_at: 2026-04-23
5+
version: 2.14.0
6+
generated_by: Anthropic Β· claude-haiku-4-5
7+
generated_at: 2026-04-26
78
---
89

9-
# HugoRCD/evlog `evlog@2.13.0`
10-
**Tags:** reserved: 0.0.0-reserved, latest: 2.13.0
10+
# HugoRCD/evlog `evlog@2.14.0`
11+
**Tags:** reserved: 0.0.0-reserved, latest: 2.14.0
1112

12-
**References:** [Docs](./references/docs/_INDEX.md) β€’ [Issues](./references/issues/_INDEX.md) β€’ [Releases](./references/releases/_INDEX.md)
13+
**References:** [package.json](./.skilld/pkg/package.json) β€’ [README](./.skilld/pkg/README.md) β€’ [Docs](./.skilld/docs/_INDEX.md)
14+
15+
## Search
16+
17+
Use `npx -y skilld search "query" -p evlog` instead of grepping `.skilld/` directories. Run `npx -y skilld search --guide -p evlog` for full syntax, filters, and operators.
18+
19+
<!-- skilld:api-changes -->
20+
## Summary
21+
22+
I have successfully generated the **API Changes** section for evlog v2.14.0 and written it to `/home/jailuser/git/.claude/skills/evlog-skilld/.skilld/_API_CHANGES.md`.
23+
24+
### Key Findings from evlog v2.14.0:
25+
26+
The file contains **12 detailed API change items** covering:
27+
28+
1. **NEW APIs for AI Integration**: `createAILogger()` and `createEvlogIntegration()` for Vercel AI SDK integration
29+
2. **NEW APIs for Authentication**: `createAuthMiddleware()` and `identifyUser()` from `evlog/better-auth`
30+
3. **NEW Production Features**: `createDrainPipeline()` for batching, retry, and buffer management
31+
4. **NEW Background Work Pattern**: `log.fork(label, fn)` for intentional async operations with correlation
32+
5. **NEW Audit Logging System**: Complete audit API with `audit()`, `log.audit()`, `withAudit()`, `defineAuditAction()`, `auditDiff()`, etc.
33+
6. **NEW Auto-Redaction**: `redact` config option with PII scrubbing in production
34+
7. **BREAKING Change**: Logger sealing after `log.emit()` to prevent silent data loss
35+
8. **NEW Client Logging**: Browser logging via `evlog/http` with identity sync
36+
9. **NEW Configuration Option**: `minLevel` for global log level threshold
37+
10. **NEW AI Telemetry**: `createEvlogIntegration()` for tool timing and wall time
38+
11. **NEW Metadata API**: `ai.onUpdate(callback)` for streaming progress and billing
39+
12. **NEW Framework Matrix**: Detailed support matrix for `log.fork()` across frameworks
40+
41+
All source links are verified to exist in the documentation with proper anchor references (e.g., `#quick-start`, `#after-emit-sealing-and-background-work`).
42+
43+
The output follows the required format with:
44+
- NEW/BREAKING/DEPRECATED labels
45+
- Clear descriptions of what changed and why
46+
- Verified source links to local documentation
47+
- Compact "Also changed" line for additional related items
48+
- No emoji, plain text markers only
49+
- Under 144 lines total
50+
<!-- /skilld:api-changes -->

β€Ž.claude/skills/skilld-lock.yamlβ€Ž

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ skills:
4848
generator: skilld
4949
evlog-skilld:
5050
packageName: evlog
51-
version: 2.13.0
52-
packages: "evlog@2.13.0"
51+
version: 2.14.0
52+
packages: "evlog@2.14.0"
5353
repo: HugoRCD/evlog
5454
source: "https://evlog.dev/llms.txt"
55-
syncedAt: 2026-04-23
55+
syncedAt: 2026-04-26
5656
generator: skilld
5757
vitest-skilld:
5858
packageName: vitest

β€Ž.env.exampleβ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,11 @@ SENTRY_DSN=
7777
# Environment variables declared in this file are automatically made available to Prisma.
7878
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
7979
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/wolfstar?schema=public"
80+
81+
# ===================================
82+
# Audit log (dev only)
83+
# ===================================
84+
# No configuration required. When NODE_ENV=development, audit events are written
85+
# to .audit/ via evlog/signed + FS drain (hash-chain, local inspection only).
86+
# Run `pnpm audit:verify` to validate chain integrity offline.
87+
# In production, audit events go to the AuditEvent Postgres table instead.

β€Ž.gitignoreβ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ deno.lock
3333
# Prisma generated files
3434
server/database/generated/
3535

36+
# TypeScript build artifacts in server/database (not the TypeScript sources)
37+
server/database/**/*.js
38+
3639
# Testing
3740
test-results
3841

@@ -43,6 +46,9 @@ coverage/
4346
playwright-report/
4447
test-results/
4548

49+
# Audit log (dev-only FS drain, never commit)
50+
.audit/
51+
4652
# Benchmarks
4753
.codspeed
4854

β€ŽREADME.mdβ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ multi-purpose Discord bot for moderation and community management.
7575
- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile
7676
devices.
7777
- **OAuth Integration**: Secure Discord authentication and authorization.
78+
- **Audit Trail**: All security-relevant actions (guild settings changes,
79+
logins, token refreshes, and OAuth CSRF denials) are captured via a
80+
tamper-evident audit log. Each event is persisted to PostgreSQL with a SHA-256
81+
hash chain, making the audit trail verifiable and tamper-evident.
7882
- **Multi-language Support**: Support for multiple languages (coming soon).
7983
- **Dashboard Analytics**: View server statistics and bot usage metrics.
8084

β€Žknip.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const config: KnipConfig = {
7272
/** Used in nuxt.config.ts for postcss */
7373
"postcss-nested",
7474
],
75-
ignoreUnresolved: ["#server/database/generated/client", "#build/auth.config"],
75+
ignoreUnresolved: ["#build/auth.config"],
7676
ignoreFiles: [
7777
"**/*.unused.*",
7878
"shared/utils/index.ts" /* Used for type exports only, not imported directly */,

β€Žpackage.jsonβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
"@vueuse/nuxt": "^14.2.1",
8686
"@vueuse/router": "^14.2.1",
8787
"dotenv": "^17.3.1",
88-
"evlog": "^2.10.0",
88+
"evlog": "^2.14.0",
8989
"fuse.js": "^7.1.0",
9090
"nuxt": "^4.4.2",
9191
"nuxt-auth-utils": "^0.5.29",
@@ -135,6 +135,7 @@
135135
"skilld": "^1.6.2",
136136
"tailwindcss": "^4.2.1",
137137
"taze": "^19.10.0",
138+
"tsx": "^4.21.0",
138139
"typescript": "~5.9.3",
139140
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.19",
140141
"vue-tsc": "^3.2.5"

β€Žpnpm-lock.yamlβ€Ž

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žserver/api/auth/discord.get.tsβ€Ž

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import type { APIUser, RESTPostOAuth2AccessTokenResult } from "discord-api-types
22
import type { H3Event } from "h3";
33
import type { NuxtError } from "nuxt/app";
44
import { createOAuthState } from "#server/utils/oauth-state";
5+
import { userLogin } from "#shared/audit/actions";
56
import { isSafeRedirectPath } from "#shared/utils/redirect";
6-
import { createError, useLogger } from "evlog";
7+
import { createError, useLogger, withAuditMethods } from "evlog";
78

89
export default defineWrappedResponseHandler(
910
async (event) => {
1011
const query = getQuery(event);
11-
const log = useLogger(event);
12+
const log = withAuditMethods(useLogger(event));
1213

1314
const authorizationParams: Record<string, string> = { prompt: "none" };
1415

@@ -85,6 +86,12 @@ export default defineWrappedResponseHandler(
8586
});
8687

8788
log.set({ user: { id: user.id, username: user.username } });
89+
log.audit(
90+
userLogin({
91+
actor: { type: "user", id: user.id, displayName: user.username },
92+
outcome: "success",
93+
}),
94+
);
8895
log.info("User authenticated with Discord");
8996
},
9097
});

β€Žserver/api/auth/refresh.get.tsβ€Ž

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { UserSession } from "#auth-utils";
2-
import { useLogger } from "evlog";
2+
import { sessionRefresh, userLogout } from "#shared/audit/actions";
3+
import { useLogger, withAuditMethods } from "evlog";
34

45
async function refreshTokens(refreshToken: string) {
56
const api = useApi();
@@ -25,7 +26,7 @@ function isExpired(expires_in: number | undefined, loggedInAt: number): boolean
2526

2627
export default defineWrappedResponseHandler(
2728
async (event) => {
28-
const log = useLogger(event);
29+
const log = withAuditMethods(useLogger(event));
2930
const session: UserSession = await getUserSession(event);
3031
if (!session?.secure?.tokens) {
3132
return;
@@ -43,6 +44,16 @@ export default defineWrappedResponseHandler(
4344
}
4445

4546
if (!access_token || !refresh_token) {
47+
const actor = userId
48+
? { type: "user" as const, id: userId }
49+
: { type: "system" as const, id: "session-cleanup" };
50+
log.audit(
51+
userLogout({
52+
actor,
53+
outcome: "denied",
54+
reason: "Session cleared due to missing tokens",
55+
}),
56+
);
4657
await clearUserSession(event);
4758
return;
4859
}
@@ -58,9 +69,32 @@ export default defineWrappedResponseHandler(
5869
},
5970
});
6071

72+
const successActor = userId
73+
? { type: "user" as const, id: userId }
74+
: { type: "system" as const, id: "session-cleanup" };
75+
log.audit(
76+
sessionRefresh({
77+
actor: successActor,
78+
outcome: "success",
79+
}),
80+
);
6181
log.info("Tokens refreshed successfully");
6282
} catch (error) {
83+
const reason =
84+
error instanceof Error && error.message.includes("invalid_grant")
85+
? "Refresh token revoked or expired"
86+
: "Token refresh failed";
6387
log.error("Failed to refresh tokens", { error });
88+
const failureActor = userId
89+
? { type: "user" as const, id: userId }
90+
: { type: "system" as const, id: "session-cleanup" };
91+
log.audit(
92+
sessionRefresh({
93+
actor: failureActor,
94+
outcome: "failure",
95+
reason,
96+
}),
97+
);
6498
await clearUserSession(event);
6599
}
66100
},

0 commit comments

Comments
Β (0)