# Audit Trail on Tawa

## The Short Version

```typescript
import { Septor } from '@insureco/septor'

const septor = new Septor({
  apiUrl: process.env.SEPTOR_URL,
  namespace: process.env.SERVICE_NAME || 'my-service',
})

// Fire-and-forget — NEVER await septor on the critical path
septor.emit('payment.created', {
  entityId: orgSlug,
  data: { amount, referenceId: paymentId },
  metadata: { who: userId },
}).catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))
```

## Setup

`SEPTOR_URL` is auto-injected by the builder for every deployment, but it is a
**direct** in-cluster URL and Septor's NetworkPolicy only admits namespaces that
carry the `tawa.pro/direct-dep.septor=true` label. The builder applies that label
from your declared dependencies, so you **MUST declare septor in
`internalDependencies`**:

```yaml
spec:
  internalDependencies:
    - service: septor
```

Without this, the namespace is never labeled, every Septor call is dropped by the
NetworkPolicy and hangs until it times out (`connect ETIMEDOUT`), and the audit
trail is silently lost (emits are fire-and-forget). The builder may warn
"septor is auto-injected — remove it"; **ignore that warning and keep the
declaration** — it is what applies the NetworkPolicy label.

## Authentication (REQUIRED — SDK ≥ 1.4.0)

Septor verifies a **Bio-ID token** on every emit/query. The SDK (≥ 1.4.0) handles
this automatically: when no `apiKey` is passed, it mints a `client_credentials`
token from `BIO_CLIENT_ID` / `BIO_CLIENT_SECRET` / `BIO_ID_URL` and refreshes it
ahead of expiry. **No per-app token code is needed.**

**This means your service MUST have Bio-ID credentials.** Declare `spec.auth` in
`catalog-info.yaml` so the builder injects `BIO_CLIENT_ID` / `BIO_CLIENT_SECRET`:

```yaml
spec:
  auth:
    mode: sso            # or: service-only (no user login, creds still injected)
```

Without `spec.auth`, those vars are absent and **every Septor emit fails with
`Authorization header required`** (the audit trail is silently lost since emits
are fire-and-forget). See https://tawa.insureco.io/reference/convention-septor

> Pin `@insureco/septor` to `^1.4.0`. Earlier versions do not authenticate and
> will be rejected by Septor.

## Fire-and-Forget (REQUIRED)

A Septor outage must NEVER break your service. Always fire-and-forget:

```typescript
// ✅ CORRECT
septor.emit('payment.created', { ... })
  .catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))

// ❌ WRONG: blocking your user on an audit write
await septor.emit('payment.created', { ... })
```

## What MUST Be Septor-Wired

| Category | Required Events |
|----------|----------------|
| Payments | `payment.created`, `payment.completed`, `payment.failed`, `payment.refunded` |
| Policies | `policy.bound`, `policy.endorsed`, `policy.cancelled`, `policy.renewed` |
| Compliance | `ofac.cleared`, `ofac.flagged`, `kyc.verified`, `kyc.failed` |
| Authorization | `user.login`, `permission.granted`, `permission.revoked` |
| Data Access | `record.accessed`, `report.exported`, `data.modified` |

**Automatically written by the platform (no code needed):**
- Deploy events (builder writes these)
- Gas events (Janus writes these)
- Job/cron events (iec-queue/iec-cron write these)
- Credential rotations (builder writes these)

## Event Naming

Use `{resource}.{action}` dot notation, lowercase:

```
payment.created       ✅
policy.bound          ✅
ofac.screen.cleared   ✅  (nested resource.sub.action is OK)

payment-created       ❌  (hyphens — use dots)
createPayment         ❌  (camelCase)
PAYMENT_CREATED       ❌  (uppercase)
```

## Querying the Audit Trail

```typescript
const { data } = await septor.query({
  entityId: orgSlug,
  eventType: 'payment.created',  // optional filter
  from: '2024-01-01',
  to: '2024-01-31',
  limit: '100',
})

for (const event of data.events) {
  console.log(event.eventType, event.createdAt, event.metadata.who)
}
```

## Verifying Chain Integrity

```typescript
// Run in background jobs, not request handlers — this is O(n)
const { data } = await septor.verify(orgSlug)
if (!data.valid) {
  logger.error({ brokenAt: data.brokenAt }, 'Audit chain broken!')
}
```

## Key Facts

- Each event gets a cryptographic hash linked to the previous — tamper-evident chain
- `entityId` is the primary index — use a stable identifier (`orgSlug` or `userId`)
- Events are immutable — once written, cannot be modified or deleted
- Each service sees only its own events (namespace scoping)
- Missing `.catch()` is a compliance gap — failures must be logged

## What NOT to Do

```typescript
// ❌ WRONG: blocking on audit write
await septor.emit(...)

// ❌ WRONG: silent failure
septor.emit(...).catch(() => {})  // swallows the error — compliance gap

// ❌ WRONG: vague event type
septor.emit('event.happened', ...)

// ✅ CORRECT
septor.emit('payment.refunded', {
  entityId: orgSlug,
  data: { amount, reason, refundId },
  metadata: { who: userId, why: 'Customer requested refund' },
}).catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))
```
