@12-apps/audit

中文文档
5.11.0发布于 2026/10/30MITES Module构建来源已验证近 30 天下载 3.3万

Generic, portable action audit. Framework-free core (the action/resource vocabulary and the deny-by-default diff redaction), host-mounted backend surface (./server: the transactional writer, the per-request actor context, the created_by/updated_by stamp a

原始使用文档
文档目录8 节
  1. What it is for
  2. The vocabulary
  3. The backend half
  4. Endpoints
  5. The permission it gates with
  6. The frontend half
  7. The database
  8. What this does NOT protect against

@12-apps/audit

Action audit as a package: an append-only "who did what" trail, its transactional writer, the per-request actor context behind it, the two Prisma extensions that enforce it, the retention sweep, the tenant-scoped listing endpoint and the viewer that reads it.

The vocabulary is yours. This package ships no actions, no resource types, no field allowlists and no product language — you declare them and pass them in. See ADOPTING.md for the migration table if you are coming from 1.x.

Two halves, one factory each.

// backend
import { createApiAudit } from '@12-apps/audit/server';
import { auditRouter } from '@12-apps/audit/hono';

// frontend
import { createWebAudit } from '@12-apps/audit/react';

What it is for

An audit trail is only worth having if it cannot be edited afterwards, and only defensible if it names the right person. This package is those two properties, made mechanical:

  • Immutable. The entry is written INSIDE the caller's transaction, so "it happened" and "it was logged" commit or roll back together — never fire-and-forget. Afterwards the model refuses update / upsert / delete at the client layer.
  • Correctly attributed. An entry carries a PAIR of identities: the real human whose credentials authorized the write, and — when a session is impersonating — who the screen claimed to be. One field cannot answer both questions, and collapsing them is not recoverable on an append-only table.

The vocabulary

One value describes what may be audited, what a row may say, and what a human reads. Both halves take the same object.

import { defineAuditVocabulary } from '@12-apps/audit';

export const AUDIT = defineAuditVocabulary({
  actions: {
    'post.publish': { label: 'Post published' },
    'comment.hide': { label: 'Comment hidden' },
  },
  resources: {
    post: { label: 'Post', fields: ['title', 'state', 'publishedAt'] },
    comment: { label: 'Comment', fields: ['state', 'reason'] },
  },
});

fields is a deny-by-default allowlist: a field the diff carries but the vocabulary does not name is dropped, so a caller passing a whole database row cannot leak PII into an append-only table. The flip side is that an omission is invisible at the write site and shows up as a hollow entry — list every field each writer of a shared resource type emits.

The factory refuses, at assembly, everything that would make the vocabulary unsafe later: an empty axis, an empty fields, a blank label, a blank or whitespace-padded id or field name, a duplicate field, an integer-like id. Every one of those is a fail-open, explained in ADOPTING.md §5.

The backend half

const audit = auditRouter({
  db: () => getPrismaClient(),               // the seam: one owned model
  vocabulary: AUDIT,                         // what may be audited
  trackedModels: ['Post', 'Comment'],        // created_by / updated_by
  retention: { floorDays: 365 },
  directory: { getUsers, listActors },       // who a user id is (optional)
  messages: { forbidden: 'No access.' },     // your product's copy
  resolveActor: async (request) => {         // WHO is calling
    const session = await auth(request.raw);
    if (!session) return null;
    const { tenantId, permissions } = await resolveTenant(request.params.tenantSlug, session);
    // Return null — the 401 — rather than an actor with no tenant. An `undefined`
    // tenantId is a cross-tenant read waiting to happen, so the package refuses it
    // with a 500 (ADOPTING.md rule 1).
    if (!tenantId) return null;
    return {
      tenantId,
      userId: session.user.id,
      permissions,
      role: session.role,
      scope: tenantId,
      onBehalfOfUserId: session.impersonating?.subjectUserId ?? null,
    };
  },
});

app.use('*', audit.actorContext);              // stamp every request
app.route('/api/admin/:tenantSlug', audit.router);

What you get back:

routesframework-neutral descriptors, in mount order (./hono adapts them)
write(tx, entry)the transactional writer — call it inside your transaction
extendPrismaClient(client)both extensions, applied once at client construction
withActorContext(req, run)the actor-context middleware (actorContext in ./hono)
retentionpurgeExpired() and purgeTenantWindow(tenant, since, cutoff)
storethe tenant-scoped read, for a host surface that lists the same rows
vocabularythe vocabulary in force — labels and validation

Endpoints

MethodPath
GET/audit-logsthe tenant's trail, newest first — { data, pagination }
GET/audit-logs/actorsthe viewer's actor-filter options — { data }

Filters: q (resource id contains, ≤200 chars), action_in, resourceType_in, actorUserId, resourceId, from/to (inclusive YYYY-MM-DD), sort, page, pageSize. The paging numbers are config (pagination), defaulting to 20 / 100 / 10 000, and they are published as bounded integers rather than as bare strings. Unknown filter values, an unservable sort and malformed dates are 400 — never accepted and ignored; a denial is { error } at the top level with 401 / 403.

The listing's order is total: created_at DESC, id DESC by default, or ASC, ASC for ?sort=createdAt:asc — created_at is the one axis a trail has, and reading it forwards is how an incident is reconstructed. The tie-break follows the direction, or the two views would not be reverses of each other. An audit trail is written in bursts and created_at is timestamp(3), so ties are ordinary — and SQL guarantees nothing about the order of rows a sort cannot distinguish, which with skip/take means a reader sees one entry twice and never sees another. The tie-break is part of the seam (AUDIT_LOG_ORDER_BY), so a non-Prisma implementation maps it rather than reinventing it.

There is no write endpoint. Entries are written by the mutations themselves, so a POST here would only be a way to forge history.

The permission it gates with

audit:read is exported as AUDIT_READ_PERMISSION, because this package owns the endpoint it guards. Compose it into your catalog, or spell it differently with gatePermissions.read — the constant is the default, not a requirement.

The frontend half

const { page: AuditLog } = createWebAudit({
  apiBase: '/api/admin/my-store',
  vocabulary: AUDIT,
  labels: { title: 'History' },   // your product's words
});

The whole SCREEN, on the shared DataViews grid: the dashboard header with its [i] and its export, the search box, the action / resource / actor filter pills, the period range, the sortable date column, column visibility, saved views, the trail with its diff summary and the server-mode pager — and the impersonation pair rendered as one line naming both people. The vocabulary is shared with the backend half, so an action that exists is an action the viewer can label.

Being the SAME grid the host's other lists use is the point: an operator moving between Pedidos, Produtos and the trail learns one filter bar. Two host-side requirements come with it — DataViewsCopyProvider at your root (the grid's own words are yours, and it refuses to invent them), and a table wrapper if you want saved views, because persistence is a backend a package cannot invent. See ADOPTING.md §4b.

Copy defaults to English and dates to the runtime's own locale. Both are fallbacks rather than recommendations — a package cannot know your market, and 1.x defaulted to one application's.

A host whose readers speak Portuguese passes the shipped pack rather than retyping it: PT_BR_AUDIT_LABELS (./react) to labels, PT_BR_AUDIT_MESSAGES (./server) to messages. Each has an EN_US_* twin and a tag-keyed map (AUDIT_LABELS, AUDIT_MESSAGES) — ADOPTING.md §4.

The database

The package owns one model (prisma/audit.prisma) and its migration. A host copies the partial in with the package's own script:

node node_modules/@12-apps/audit/scripts/sync-audit-schema.mjs [<host-schema-dir>]

The migration is replay-safe (CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS), so a host that already has an audit_logs table adopts it with no baselining and no prisma migrate resolve --applied.

What this does NOT protect against

Stated plainly, because a guard whose limits are undocumented gets trusted past them:

  • Raw SQL bypasses the append-only guard. $executeRaw* / $queryRaw* never reach a model delegate, so anyone holding the client can DELETE FROM audit_logs. That is deliberate — it is the only way the retention sweep can run — and it means the guard stops the ACCIDENT, not a determined operator. Guarantees at that level are database privileges or a trigger, and the host owns both.
  • Another connection is unaffected. psql, a migration, a second client built without the extensions.
  • The host still owns authorization. The package gates on the permission ids the host resolved; it does not compute them, and it will read nothing for an actor that carries none.

See ADOPTING.md for the full contract, the impersonation pair's design, the assembly refusals and the checklist.

版本与发布记录

@12-apps/audit 共发布 20 个版本,首次发布于 2026/08/13;过去 12 个月发布了 20 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
5.11.0latest2026/10/308 天前
5.10.02026/10/2711 天前
5.9.02026/10/2514 天前
5.8.02026/09/271 个月前
5.7.12026/08/262 个月前
5.7.02026/08/262 个月前

关于 @12-apps/audit 的常见问题

@12-apps/audit 是什么?

Generic, portable action audit. Framework-free core (the action/resource vocabulary and the deny-by-default diff redaction), host-mounted backend surface (./server: the transactional writer, the per-request actor context, the created_by/updated_by stamp a

如何安装 @12-apps/audit?

在项目目录运行 npm install @12-apps/audit,或使用 pnpm add / yarn add / bun add。

@12-apps/audit 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/12-apps__audit 是否存在。

@12-apps/audit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@12-apps/audit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@12-apps/audit 可以商用吗?

@12-apps/audit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@12-apps/audit 的下载量有多少?

根据 npm 官方下载统计,@12-apps/audit 在 2026/10/04 – 2026/11/02 这 30 天内被下载 33,353 次,日均约 1,112 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@12-apps/audit 还在维护吗?

最新版本 5.11.0 发布于 2026/10/30(8 天前);至今共发布 20 个版本,其中过去 12 个月发布了 20 个;首次发布于 2026/08/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。