strapi-cms-audit-log 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1026 -0
  4. package/dist/admin/App-BLt4lqEM.js +1284 -0
  5. package/dist/admin/App-ou5hy99f.mjs +1266 -0
  6. package/dist/admin/en-B0rPiE2W.mjs +87 -0
  7. package/dist/admin/en-BMlJxq3g.js +87 -0
  8. package/dist/admin/index-BYi8OPTw.js +80 -0
  9. package/dist/admin/index-DdmY-p3Q.mjs +81 -0
  10. package/dist/admin/index.js +4 -0
  11. package/dist/admin/index.mjs +4 -0
  12. package/dist/admin/src/components/AuditLogFilters.d.ts +18 -0
  13. package/dist/admin/src/components/AuditLogTable.d.ts +19 -0
  14. package/dist/admin/src/components/ChangeViewer.d.ts +14 -0
  15. package/dist/admin/src/components/JsonViewer.d.ts +20 -0
  16. package/dist/admin/src/components/PluginIcon.d.ts +3 -0
  17. package/dist/admin/src/components/WidgetDiff.d.ts +19 -0
  18. package/dist/admin/src/hooks/useAuditLogs.d.ts +36 -0
  19. package/dist/admin/src/index.d.ts +3 -0
  20. package/dist/admin/src/pages/App.d.ts +11 -0
  21. package/dist/admin/src/pages/AuditLogDetails.d.ts +10 -0
  22. package/dist/admin/src/pages/AuditLogs.d.ts +3 -0
  23. package/dist/admin/src/permissions.d.ts +26 -0
  24. package/dist/admin/src/pluginId.d.ts +2 -0
  25. package/dist/admin/src/types.d.ts +91 -0
  26. package/dist/admin/src/utils/format.d.ts +41 -0
  27. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  28. package/dist/admin/src/utils/widgets.d.ts +85 -0
  29. package/dist/server/index.js +1856 -0
  30. package/dist/server/index.mjs +1856 -0
  31. package/dist/server/src/bootstrap.d.ts +14 -0
  32. package/dist/server/src/config/index.d.ts +52 -0
  33. package/dist/server/src/constants.d.ts +140 -0
  34. package/dist/server/src/content-types/audit-log/index.d.ts +86 -0
  35. package/dist/server/src/content-types/audit-log/schema.d.ts +141 -0
  36. package/dist/server/src/content-types/index.d.ts +88 -0
  37. package/dist/server/src/controllers/audit-log.d.ts +31 -0
  38. package/dist/server/src/controllers/index.d.ts +42 -0
  39. package/dist/server/src/destroy.d.ts +19 -0
  40. package/dist/server/src/index.d.ts +328 -0
  41. package/dist/server/src/register.d.ts +31 -0
  42. package/dist/server/src/routes/admin.d.ts +26 -0
  43. package/dist/server/src/routes/index.d.ts +19 -0
  44. package/dist/server/src/services/access.d.ts +51 -0
  45. package/dist/server/src/services/audit.d.ts +25 -0
  46. package/dist/server/src/services/config.d.ts +30 -0
  47. package/dist/server/src/services/context.d.ts +33 -0
  48. package/dist/server/src/services/diff.d.ts +48 -0
  49. package/dist/server/src/services/immutability.d.ts +30 -0
  50. package/dist/server/src/services/index.d.ts +137 -0
  51. package/dist/server/src/services/retention.d.ts +22 -0
  52. package/dist/server/src/services/security.d.ts +54 -0
  53. package/dist/server/src/services/snapshot.d.ts +46 -0
  54. package/dist/server/src/services/tracker.d.ts +39 -0
  55. package/dist/server/src/types/index.d.ts +214 -0
  56. package/dist/server/src/utils/json.d.ts +17 -0
  57. package/dist/server/src/utils/paths.d.ts +34 -0
  58. package/dist/server/src/utils/sanitize.d.ts +16 -0
  59. package/package.json +112 -0
@@ -0,0 +1,137 @@
1
+ declare const _default: {
2
+ access: ({ strapi }: {
3
+ strapi: import('@strapi/types/dist/core').Strapi;
4
+ }) => {
5
+ createMiddleware: () => (ctx: {
6
+ [x: string]: any;
7
+ }, next: () => Promise<unknown>) => Promise<void>;
8
+ register: () => void;
9
+ recordDenial: (ctx: {
10
+ [x: string]: any;
11
+ }) => Promise<void>;
12
+ sourceOf: (ctx: {
13
+ [x: string]: any;
14
+ }) => "admin" | "api" | "unknown";
15
+ isIgnoredPath: (path: string) => boolean;
16
+ };
17
+ audit: ({ strapi }: {
18
+ strapi: import('@strapi/types/dist/core').Strapi;
19
+ }) => {
20
+ record: (entry: import('..').AuditEntryInput) => Promise<void>;
21
+ write: (entry: import('..').AuditEntryInput) => Promise<void>;
22
+ flush: () => Promise<void>;
23
+ find: (query?: import('..').AuditLogQuery) => Promise<import('..').AuditLogListResult>;
24
+ findOne: (id: number) => Promise<import('..').AuditLog | null>;
25
+ deleteOne: (id: number) => Promise<import('..').AuditLog | null>;
26
+ deleteOlderThan: (date: Date) => Promise<number>;
27
+ getFilterOptions: () => Promise<import('..').AuditFilterOptions>;
28
+ buildWhere: (query: import('..').AuditLogQuery) => Record<string, unknown>;
29
+ parseSort: (sort: string | undefined) => Record<string, "asc" | "desc">;
30
+ };
31
+ config: ({ strapi }: {
32
+ strapi: import('@strapi/types/dist/core').Strapi;
33
+ }) => {
34
+ resolve: () => import('./config').ResolvedConfig;
35
+ isAuditedContentType: (uid: string) => boolean;
36
+ isAuditedAction: (action: import('..').AuditAction) => boolean;
37
+ isAuditedSecurityAction: (action: import('..').AuditSecurityAction) => boolean;
38
+ hasSecurityEvents: () => boolean;
39
+ enabledSecurityActions: () => import('..').AuditSecurityAction[];
40
+ getPublicConfig: () => import('..').AuditConfig;
41
+ clearCache: () => void;
42
+ };
43
+ context: ({ strapi }: {
44
+ strapi: import('@strapi/types/dist/core').Strapi;
45
+ }) => {
46
+ resolve: () => import('..').AuditContext;
47
+ runAs: <T>(override: import('./context').SourceOverride, callback: () => T) => T;
48
+ };
49
+ diff: ({ strapi }: {
50
+ strapi: import('@strapi/types/dist/core').Strapi;
51
+ }) => {
52
+ buildDiff: (before: unknown, after: unknown, options?: import('./diff').DiffOptions) => import('..').AuditChangeSet;
53
+ };
54
+ immutability: ({ strapi }: {
55
+ strapi: import('@strapi/types/dist/core').Strapi;
56
+ }) => {
57
+ createMiddleware: () => (ctx: {
58
+ uid: string;
59
+ action: string;
60
+ }, next: () => Promise<unknown>) => Promise<unknown>;
61
+ register: () => void;
62
+ };
63
+ retention: ({ strapi }: {
64
+ strapi: import('@strapi/types/dist/core').Strapi;
65
+ }) => {
66
+ cleanup: () => Promise<number>;
67
+ cutoffDate: (retentionDays: number, now?: Date) => Date;
68
+ register: () => void;
69
+ unregister: () => void;
70
+ };
71
+ security: ({ strapi }: {
72
+ strapi: import('@strapi/types/dist/core').Strapi;
73
+ }) => {
74
+ register: () => void;
75
+ unregister: () => void;
76
+ record: (action: import('..').AuditSecurityAction, subject: string, fields?: {
77
+ contentDocumentId?: string | null;
78
+ contentId?: string | null;
79
+ user?: {
80
+ [x: string]: any;
81
+ } | null;
82
+ before?: Record<string, unknown> | null;
83
+ after?: Record<string, unknown> | null;
84
+ metadata?: import('..').AuditMetadata | null;
85
+ outcome?: import('..').AuditOutcome;
86
+ }) => Promise<void>;
87
+ identify: (entity: {
88
+ [x: string]: any;
89
+ } | null | undefined) => Record<string, unknown> | null;
90
+ subjectOf: (payload: {
91
+ [x: string]: any;
92
+ }) => {
93
+ [x: string]: any;
94
+ } | null;
95
+ sourceOf: () => "admin" | "system";
96
+ entityHandler: (action: import('..').AuditSecurityAction, subject: string) => (payload: {
97
+ [x: string]: any;
98
+ }) => Promise<void>;
99
+ handlers: Record<string, (payload: {
100
+ [x: string]: any;
101
+ }) => Promise<void>>;
102
+ };
103
+ snapshot: ({ strapi }: {
104
+ strapi: import('@strapi/types/dist/core').Strapi;
105
+ }) => {
106
+ buildSnapshotQuery: (uid: string, keys: string[] | null, options: {
107
+ depth: number;
108
+ isIgnored?: (path: string) => boolean;
109
+ }) => import('./snapshot').SnapshotQuery;
110
+ buildComponentPopulate: (componentUid: string, depth: number) => Record<string, unknown> | true;
111
+ buildWhere: (uid: string, lookup: import('./snapshot').SnapshotLookup) => Record<string, unknown>;
112
+ fetchRows: (uid: string, lookup: import('./snapshot').SnapshotLookup, query: import('./snapshot').SnapshotQuery, depth: number) => Promise<{
113
+ [x: string]: any;
114
+ }[]>;
115
+ refineDynamicZones: (uid: string, rows: {
116
+ [x: string]: any;
117
+ }[], query: import('./snapshot').SnapshotQuery, depth: number) => Promise<{
118
+ [x: string]: any;
119
+ }[]>;
120
+ toSnapshot: (row: {
121
+ [x: string]: any;
122
+ } | null | undefined) => Record<string, unknown> | null;
123
+ hasDraftAndPublish: (uid: string) => boolean;
124
+ isLocalized: (uid: string) => boolean;
125
+ };
126
+ tracker: ({ strapi }: {
127
+ strapi: import('@strapi/types/dist/core').Strapi;
128
+ }) => {
129
+ createMiddleware: () => (ctx: import('./tracker').DocumentMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>;
130
+ register: () => void;
131
+ changedKeys: (uid: string, params: Record<string, any>) => string[] | null;
132
+ resultRows: (result: unknown) => {
133
+ [x: string]: any;
134
+ }[];
135
+ };
136
+ };
137
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import { Core } from '@strapi/strapi';
2
+ /**
3
+ * Scheduled deletion of expired audit records.
4
+ *
5
+ * Runs on `strapi.cron`, off the request path entirely. Cleaning up inside a
6
+ * content operation — the obvious shortcut — would put an unbounded `DELETE`
7
+ * over the largest table in the project directly in front of an editor pressing
8
+ * Save, and would do it once per write. Here it runs once a day, by default at
9
+ * 03:00 server time, and a slow pass delays nothing but itself.
10
+ *
11
+ * `retentionDays: 0` disables the job entirely: no cron entry is registered, so
12
+ * projects that keep audit history forever pay nothing for the feature.
13
+ */
14
+ declare const retentionService: ({ strapi }: {
15
+ strapi: Core.Strapi;
16
+ }) => {
17
+ cleanup: () => Promise<number>;
18
+ cutoffDate: (retentionDays: number, now?: Date) => Date;
19
+ register: () => void;
20
+ unregister: () => void;
21
+ };
22
+ export default retentionService;
@@ -0,0 +1,54 @@
1
+ import { Core } from '@strapi/strapi';
2
+ import { AuditMetadata, AuditOutcome, AuditSecurityAction } from '../types';
3
+ type Payload = Record<string, any>;
4
+ /**
5
+ * The half of the audit trail Strapi does not route through the Document
6
+ * Service: who signed in, who failed to, who signed out, and every change to the
7
+ * admin users, roles, permissions and media that govern the rest of it.
8
+ *
9
+ * ## Why `eventHub` and not more Document Service middleware
10
+ *
11
+ * None of these operations is a document operation. `@strapi/admin` writes admin
12
+ * users with `strapi.db.query('admin::user')` and `@strapi/upload` writes files
13
+ * with `strapi.db.query(FILE_MODEL_UID)` — both sit *below* the Document
14
+ * Service, so the tracker in `tracker.ts` never sees them and no amount of
15
+ * middleware would make it. Authentication is not a write at all.
16
+ *
17
+ * What Strapi does do is emit a named event for each one, on the same
18
+ * `strapi.eventHub` its own webhook system runs on. Those emissions are all in
19
+ * **Community Edition** — `server/src/controllers/authentication.ts` and
20
+ * `server/src/services/{user,role,permission}.ts` in `@strapi/admin`,
21
+ * `server/src/services/{upload,folder}.ts` in `@strapi/upload` — so nothing here
22
+ * needs an Enterprise licence, and nothing here patches Strapi's own code.
23
+ *
24
+ * ## Why a listener may never throw
25
+ *
26
+ * `eventHub.emit` awaits its subscribers in sequence *inside the operation that
27
+ * emitted*. A listener that threw on `admin.auth.success` would turn a correct
28
+ * password into a failed login. Every handler is therefore wrapped: an audit
29
+ * failure is logged and swallowed, and `failOnAuditError` is deliberately not
30
+ * honoured here. That option exists so a project can refuse to serve content it
31
+ * cannot audit, which is a very different proposition from locking every
32
+ * administrator out of the panel because the audit table is unreachable.
33
+ */
34
+ declare const securityService: ({ strapi }: {
35
+ strapi: Core.Strapi;
36
+ }) => {
37
+ register: () => void;
38
+ unregister: () => void;
39
+ record: (action: AuditSecurityAction, subject: string, fields?: {
40
+ contentDocumentId?: string | null;
41
+ contentId?: string | null;
42
+ user?: Payload | null;
43
+ before?: Record<string, unknown> | null;
44
+ after?: Record<string, unknown> | null;
45
+ metadata?: AuditMetadata | null;
46
+ outcome?: AuditOutcome;
47
+ }) => Promise<void>;
48
+ identify: (entity: Payload | null | undefined) => Record<string, unknown> | null;
49
+ subjectOf: (payload: Payload) => Payload | null;
50
+ sourceOf: () => "admin" | "system";
51
+ entityHandler: (action: AuditSecurityAction, subject: string) => (payload: Payload) => Promise<void>;
52
+ handlers: Record<string, (payload: Payload) => Promise<void>>;
53
+ };
54
+ export default securityService;
@@ -0,0 +1,46 @@
1
+ import { Core } from '@strapi/strapi';
2
+ type Row = Record<string, any>;
3
+ export interface SnapshotQuery {
4
+ select: string[];
5
+ populate?: Record<string, unknown>;
6
+ /** Dynamic-zone attributes present in the query, for the optional refinement pass. */
7
+ dynamicZones: string[];
8
+ }
9
+ export interface SnapshotLookup {
10
+ documentId: string;
11
+ locales?: string[] | null;
12
+ status?: 'draft' | 'published' | 'any';
13
+ }
14
+ /**
15
+ * Builds the narrowest query that can answer "what did these fields look like?".
16
+ *
17
+ * This is the heart of the plugin's performance story. Strapi's own
18
+ * `getDeepPopulate` walks the *schema* and asks for every relation, component
19
+ * and media field a type could ever hold; on a page content type with 173
20
+ * registered dynamic-zone widgets that is hundreds of joins to audit a
21
+ * two-field edit. Here:
22
+ *
23
+ * - the key set is the attributes the write actually touched, not the schema;
24
+ * - relations and media are reduced to identifying fields, so a relation to a
25
+ * 50-field document costs a handful of columns rather than all of them;
26
+ * - dynamic zones are populated with `true`, which makes Strapi read the join
27
+ * table first and then issue one query per component type *actually present*
28
+ * in the rows. Five widgets used out of 173 registered means five queries,
29
+ * and the 168 unused schemas are never touched.
30
+ */
31
+ declare const snapshotService: ({ strapi }: {
32
+ strapi: Core.Strapi;
33
+ }) => {
34
+ buildSnapshotQuery: (uid: string, keys: string[] | null, options: {
35
+ depth: number;
36
+ isIgnored?: (path: string) => boolean;
37
+ }) => SnapshotQuery;
38
+ buildComponentPopulate: (componentUid: string, depth: number) => Record<string, unknown> | true;
39
+ buildWhere: (uid: string, lookup: SnapshotLookup) => Record<string, unknown>;
40
+ fetchRows: (uid: string, lookup: SnapshotLookup, query: SnapshotQuery, depth: number) => Promise<Row[]>;
41
+ refineDynamicZones: (uid: string, rows: Row[], query: SnapshotQuery, depth: number) => Promise<Row[]>;
42
+ toSnapshot: (row: Row | null | undefined) => Record<string, unknown> | null;
43
+ hasDraftAndPublish: (uid: string) => boolean;
44
+ isLocalized: (uid: string) => boolean;
45
+ };
46
+ export default snapshotService;
@@ -0,0 +1,39 @@
1
+ import { Core } from '@strapi/strapi';
2
+ type Row = Record<string, any>;
3
+ export interface DocumentMiddlewareContext {
4
+ uid: string;
5
+ action: string;
6
+ contentType?: Record<string, any>;
7
+ params: Record<string, any>;
8
+ }
9
+ /**
10
+ * Global interception of content writes.
11
+ *
12
+ * Registered once with `strapi.documents.use()`, which is the Strapi v5
13
+ * replacement for per-content-type lifecycles and the reason a consuming
14
+ * application never writes a line of audit code. Three properties make this the
15
+ * right layer, and `db.lifecycles` the wrong one:
16
+ *
17
+ * 1. It sees the *semantic* action. `publish` and `unpublish` are ordinary row
18
+ * inserts and deletes to the database layer; only the Document Service knows
19
+ * they were a publish.
20
+ * 2. It runs *outside* the write's transaction (`middlewares.wrapObject` wraps
21
+ * the already-transaction-wrapped repository methods), so an audit failure
22
+ * can never roll back an editor's save, and an audit insert never holds a
23
+ * row lock open.
24
+ * 3. It sees `documentId` and `locale` as first-class parameters rather than
25
+ * having to reconstruct them from a row.
26
+ *
27
+ * Every operation the Content Manager, the REST/GraphQL content API and custom
28
+ * server code perform goes through this facade, so all three are covered by the
29
+ * one registration.
30
+ */
31
+ declare const trackerService: ({ strapi }: {
32
+ strapi: Core.Strapi;
33
+ }) => {
34
+ createMiddleware: () => (ctx: DocumentMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>;
35
+ register: () => void;
36
+ changedKeys: (uid: string, params: Record<string, any>) => string[] | null;
37
+ resultRows: (result: unknown) => Row[];
38
+ };
39
+ export default trackerService;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Public type surface of the plugin.
3
+ *
4
+ * Re-exported from `strapi-plugin-audit-log/strapi-server`, so a consuming
5
+ * application can type its own code against the same shapes the plugin uses:
6
+ *
7
+ * ```ts
8
+ * import type { AuditLog, AuditConfig } from 'strapi-plugin-audit-log/strapi-server';
9
+ * ```
10
+ *
11
+ * The admin panel keeps its own copies of the few shared unions in
12
+ * `admin/src/types.ts`. That is not an oversight: the admin and server bundles
13
+ * are compiled by separate Vite passes whose `.d.ts` emit is rooted at
14
+ * `admin/src` and `server/src` respectively, so a file imported across that
15
+ * boundary would be emitted outside its own root. See README → Architecture.
16
+ */
17
+ /** Operations the plugin can record. Extend via `AuditAction` unions downstream. */
18
+ export type AuditAction = 'create' | 'update' | 'delete' | 'publish' | 'unpublish';
19
+ /**
20
+ * Operations the plugin records that are not content writes.
21
+ *
22
+ * Produced by `strapi.eventHub` listeners and by the access middleware rather
23
+ * than by the Document Service. Namespaced with a dot so a reader can tell at a
24
+ * glance which family a row belongs to, and so a filter on `login.` prefixes
25
+ * groups the authentication trail without a second column.
26
+ */
27
+ export type AuditSecurityAction = 'login.success' | 'login.failed' | 'logout' | 'access.denied' | 'admin.user.create' | 'admin.user.update' | 'admin.user.delete' | 'admin.role.create' | 'admin.role.update' | 'admin.role.delete' | 'admin.permission.create' | 'admin.permission.update' | 'admin.permission.delete' | 'media.create' | 'media.update' | 'media.delete' | 'media-folder.create' | 'media-folder.update' | 'media-folder.delete';
28
+ /** Every action the plugin can write, across both families. */
29
+ export type AuditAnyAction = AuditAction | AuditSecurityAction;
30
+ /**
31
+ * Whether the recorded attempt succeeded.
32
+ *
33
+ * Every content write is a `success` — the middleware runs after the operation
34
+ * resolved, so a failed save produces no record at all. The value earns its
35
+ * column on the security side, where `login.failed` and `access.denied` are the
36
+ * rows a reviewer opens the log to find, and where "show me the failures" must
37
+ * be an indexed query rather than a scan of an action list.
38
+ */
39
+ export type AuditOutcome = 'success' | 'failure';
40
+ /**
41
+ * Free-form detail that only some actions have.
42
+ *
43
+ * A deliberately loose bag rather than a column each: the useful fields differ
44
+ * per action (`reason` and `provider` for a login, `method`/`path`/`statusCode`
45
+ * for a denial, `fileName` for an upload), and none of them is ever filtered or
46
+ * sorted on. Anything worth querying gets a real column instead.
47
+ */
48
+ export interface AuditMetadata {
49
+ [key: string]: unknown;
50
+ }
51
+ /** How security events are selected. `'*'` means every one of them. */
52
+ export type SecurityEventSelector = '*' | AuditSecurityAction[];
53
+ /**
54
+ * Where the operation came from.
55
+ *
56
+ * `admin` — an authenticated Strapi admin panel request
57
+ * `api` — the REST/GraphQL content API (API token or Users & Permissions user)
58
+ * `cron` — a scheduled task
59
+ * `migration` — a data migration or import script
60
+ * `system` — server-side code with no request context (bootstrap, listeners)
61
+ * `unknown` — a request context existed but could not be classified
62
+ */
63
+ export type AuditSource = 'admin' | 'api' | 'system' | 'cron' | 'migration' | 'unknown';
64
+ /** A single field-level change, keyed in {@link AuditChangeSet} by dotted path. */
65
+ export interface AuditChange {
66
+ from: unknown;
67
+ to: unknown;
68
+ }
69
+ /**
70
+ * Field-level diff of an operation.
71
+ *
72
+ * Keys are dotted paths into the document, with array members addressed by
73
+ * index: `title`, `seo.metaTitle`, `blocks[2].heading`.
74
+ */
75
+ export type AuditChangeSet = Record<string, AuditChange>;
76
+ /** Identity of the actor, snapshotted at write time so it survives user deletion. */
77
+ export interface AuditActor {
78
+ userId: string | null;
79
+ userEmail: string | null;
80
+ userName: string | null;
81
+ }
82
+ /** Request metadata, captured opportunistically — every field may be absent. */
83
+ export interface AuditRequestContext {
84
+ ipAddress: string | null;
85
+ userAgent: string | null;
86
+ requestId: string | null;
87
+ }
88
+ /** Everything the audit service needs to know about who/where, resolved once per operation. */
89
+ export interface AuditContext extends AuditActor, AuditRequestContext {
90
+ source: AuditSource;
91
+ }
92
+ /** A persisted audit record, as returned by the plugin's admin API. */
93
+ export interface AuditLog extends AuditContext {
94
+ id: number;
95
+ /** The audit record's own Strapi v5 document id — not the audited document's. */
96
+ documentId: string;
97
+ action: AuditAnyAction | string;
98
+ /** UID of the audited content type, e.g. `api::page.page`. */
99
+ contentType: string;
100
+ /** Human-readable name of the audited content type at the time of the write. */
101
+ contentTypeDisplayName: string | null;
102
+ /**
103
+ * The audited document's Strapi v5 document id.
104
+ *
105
+ * Not called `documentId`: Strapi v5 reserves that attribute name on every
106
+ * content type and throws at boot if a schema declares it.
107
+ */
108
+ contentDocumentId: string | null;
109
+ /** The audited entry's numeric database id, as a string. */
110
+ contentId: string | null;
111
+ locale: string | null;
112
+ changes: AuditChangeSet | null;
113
+ before: Record<string, unknown> | null;
114
+ after: Record<string, unknown> | null;
115
+ /** `failure` for a rejected login or a denied request; `success` for everything else. */
116
+ outcome: AuditOutcome | null;
117
+ /** Action-specific detail — see {@link AuditMetadata}. */
118
+ metadata: AuditMetadata | null;
119
+ createdAt: string;
120
+ updatedAt: string;
121
+ }
122
+ /** Selects which content types are audited. `'*'` means every content type. */
123
+ export type ContentTypeSelector = '*' | string[];
124
+ /** How the audit row is persisted relative to the content operation. */
125
+ export type AuditWriteMode = 'sync' | 'async';
126
+ /**
127
+ * Plugin configuration, after defaults have been resolved.
128
+ *
129
+ * See {@link AuditUserConfig} for the shape accepted in `config/plugins.ts`.
130
+ */
131
+ export interface AuditConfig {
132
+ actions: AuditAction[];
133
+ contentTypes: ContentTypeSelector;
134
+ ignoredContentTypes: string[];
135
+ ignoredFields: string[];
136
+ additionalIgnoredFields: string[];
137
+ ignoredChangeFields: string[];
138
+ storeBefore: boolean;
139
+ storeAfter: boolean;
140
+ storeChanges: boolean;
141
+ retentionDays: number;
142
+ retentionCron: string;
143
+ failOnAuditError: boolean;
144
+ writeMode: AuditWriteMode;
145
+ maxPopulateDepth: number;
146
+ maxSnapshotBytes: number;
147
+ auditSystemOperations: boolean;
148
+ /** Which security events to record. `'*'` is all of them; `[]` disables the listeners entirely. */
149
+ securityEvents: SecurityEventSelector;
150
+ /** Mirror every record to `strapi.log` as one line of structured JSON, for a SIEM to pick up off stdout. */
151
+ forwardToLogger: boolean;
152
+ /** Level the mirrored line is written at. */
153
+ forwardLogLevel: AuditLogLevel;
154
+ }
155
+ /** Levels `strapi.log` exposes that make sense for an audit mirror. */
156
+ export type AuditLogLevel = 'debug' | 'info' | 'warn' | 'error';
157
+ /** Configuration as written by a consumer — every key optional. */
158
+ export type AuditUserConfig = Partial<AuditConfig>;
159
+ /** Payload handed to the audit service for persistence. */
160
+ export interface AuditEntryInput extends AuditContext {
161
+ action: AuditAnyAction;
162
+ contentType: string;
163
+ contentTypeDisplayName: string | null;
164
+ contentDocumentId: string | null;
165
+ contentId: string | null;
166
+ locale: string | null;
167
+ changes: AuditChangeSet | null;
168
+ before: Record<string, unknown> | null;
169
+ after: Record<string, unknown> | null;
170
+ /** Defaults to `success` when omitted — see {@link AuditOutcome}. */
171
+ outcome?: AuditOutcome | null;
172
+ metadata?: AuditMetadata | null;
173
+ }
174
+ /** Filters accepted by the admin list endpoint. */
175
+ export interface AuditLogQuery {
176
+ page?: number;
177
+ pageSize?: number;
178
+ sort?: string;
179
+ action?: string | string[];
180
+ contentType?: string | string[];
181
+ userId?: string | string[];
182
+ locale?: string | string[];
183
+ source?: string | string[];
184
+ outcome?: string | string[];
185
+ contentDocumentId?: string;
186
+ dateFrom?: string;
187
+ dateTo?: string;
188
+ _q?: string;
189
+ }
190
+ /** Shape returned by `GET /audit-log/logs`. */
191
+ export interface AuditLogListResult {
192
+ results: AuditLog[];
193
+ pagination: {
194
+ page: number;
195
+ pageSize: number;
196
+ pageCount: number;
197
+ total: number;
198
+ };
199
+ }
200
+ /** Distinct values behind the admin list's filter dropdowns. */
201
+ export interface AuditFilterOptions {
202
+ contentTypes: Array<{
203
+ uid: string;
204
+ displayName: string;
205
+ }>;
206
+ users: Array<{
207
+ userId: string;
208
+ label: string;
209
+ }>;
210
+ locales: string[];
211
+ actions: string[];
212
+ sources: string[];
213
+ outcomes: string[];
214
+ }
@@ -0,0 +1,17 @@
1
+ /** Small JSON helpers shared by the diff engine and the audit service. */
2
+ export declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
3
+ /**
4
+ * Structural equality for values that came out of the database or off a request
5
+ * body — i.e. JSON-shaped, plus `Date`, which Strapi hands back for datetime
6
+ * columns while the incoming payload for the same field is an ISO string.
7
+ * Comparing those two with `===` would report a change on every single write.
8
+ */
9
+ export declare const isEqual: (a: unknown, b: unknown) => boolean;
10
+ /**
11
+ * Approximate byte size of a value once serialised.
12
+ *
13
+ * Approximate on purpose: this only ever feeds a "is this snapshot absurdly
14
+ * large" check, and stringifying a 50MB document twice to get an exact number
15
+ * would cost more than the check saves.
16
+ */
17
+ export declare const approximateJsonBytes: (value: unknown) => number;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Dotted-path matching for the ignore lists.
3
+ *
4
+ * A path is written the way the diff engine writes it — `seo.metaTitle`,
5
+ * `blocks[2].heading` — and a pattern may be:
6
+ *
7
+ * `password` leaf name; matches at any depth
8
+ * `seo.metaTitle` exact path, anchored at the document root
9
+ * `*token*` substring match within one segment
10
+ * `seo.*.token` `*` on its own matches exactly one whole segment
11
+ * `blocks.**.secret` `**` matches zero or more segments
12
+ *
13
+ * Array indices are stripped before matching, so `blocks[2].secret` is matched
14
+ * by `blocks.secret` — someone writing an ignore list should never have to think
15
+ * about which element of a repeatable component they are excluding.
16
+ *
17
+ * Matching is case-insensitive. Attribute names in Strapi are camelCase while
18
+ * the words worth redacting (`token`, `secret`, `password`) are written
19
+ * lowercase, so a case-sensitive `*token*` would sail straight past
20
+ * `internalToken`. For a redaction list, matching too widely costs a field in an
21
+ * audit record; matching too narrowly costs a secret stored forever.
22
+ */
23
+ /** `blocks[2].seo.title` -> `['blocks', 'seo', 'title']`, lowercased. */
24
+ export declare const toSegments: (path: string) => string[];
25
+ /**
26
+ * Compiles an ignore list into a predicate.
27
+ *
28
+ * Built once per operation rather than per field: a document with a large
29
+ * dynamic zone produces thousands of path tests, and re-parsing the patterns for
30
+ * each one is the difference between negligible and measurable.
31
+ */
32
+ export declare const createPathMatcher: (patterns: string[]) => ((path: string) => boolean);
33
+ /** Appends a key to a dotted path, using `[i]` notation for array members. */
34
+ export declare const joinPath: (base: string, key: string | number) => string;
@@ -0,0 +1,16 @@
1
+ import { createPathMatcher } from './paths';
2
+ export type PathMatcher = (path: string) => boolean;
3
+ /**
4
+ * Recursively strips every ignored path from a snapshot.
5
+ *
6
+ * Runs on `before` and `after` *before* they are handed to the diff engine, so a
7
+ * redacted field cannot leak into `changes` either. Values are dropped
8
+ * entirely rather than masked with a placeholder: a placeholder still tells a
9
+ * reader of the audit log that the field exists and changed, and for a password
10
+ * hash or an API key even that is more than an audit trail needs to say.
11
+ *
12
+ * Returns a new object; the input — which is a live row handed to us by the
13
+ * Document Service — is never mutated.
14
+ */
15
+ export declare const sanitizeValue: <T>(value: T, isIgnored: PathMatcher, basePath?: string) => T;
16
+ export { createPathMatcher };