strapi-cms-audit-log 1.1.0 → 1.2.1

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 (34) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +12 -12
  3. package/dist/admin/{App-BLt4lqEM.js → App-CmbG2k9E.js} +110 -100
  4. package/dist/admin/{App-ou5hy99f.mjs → App-jWvaMXIw.mjs} +113 -103
  5. package/dist/admin/{en-BMlJxq3g.js → en-Bs4tlklI.js} +11 -9
  6. package/dist/admin/{en-B0rPiE2W.mjs → en-C3Og1VyK.mjs} +11 -9
  7. package/dist/admin/{index-DdmY-p3Q.mjs → index-BIIMJgKU.mjs} +3 -5
  8. package/dist/admin/{index-BYi8OPTw.js → index-BsVOGSDf.js} +3 -5
  9. package/dist/admin/index.js +1 -1
  10. package/dist/admin/index.mjs +1 -1
  11. package/dist/admin/src/components/AuditLogTable.d.ts +1 -2
  12. package/dist/admin/src/components/IntegrityStatus.d.ts +13 -0
  13. package/dist/admin/src/hooks/useAuditLogs.d.ts +14 -3
  14. package/dist/admin/src/permissions.d.ts +0 -8
  15. package/dist/admin/src/types.d.ts +26 -2
  16. package/dist/server/index.js +321 -94
  17. package/dist/server/index.mjs +321 -94
  18. package/dist/server/src/config/index.d.ts +1 -0
  19. package/dist/server/src/constants.d.ts +24 -2
  20. package/dist/server/src/content-types/audit-log/index.d.ts +8 -0
  21. package/dist/server/src/content-types/audit-log/schema.d.ts +21 -0
  22. package/dist/server/src/content-types/index.d.ts +8 -0
  23. package/dist/server/src/controllers/audit-log.d.ts +5 -3
  24. package/dist/server/src/controllers/index.d.ts +4 -4
  25. package/dist/server/src/index.d.ts +36 -7
  26. package/dist/server/src/register.d.ts +7 -3
  27. package/dist/server/src/routes/admin.d.ts +8 -0
  28. package/dist/server/src/services/audit.d.ts +0 -1
  29. package/dist/server/src/services/correlation.d.ts +35 -0
  30. package/dist/server/src/services/immutability.d.ts +5 -5
  31. package/dist/server/src/services/index.d.ts +21 -1
  32. package/dist/server/src/services/integrity.d.ts +69 -0
  33. package/dist/server/src/types/index.d.ts +54 -4
  34. package/package.json +10 -6
@@ -1,11 +1,11 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { useFetchClient, Page, Layouts, BackButton, useNotification, useQueryParams, useRBAC, SearchInput, Pagination, ConfirmDialog } from "@strapi/strapi/admin";
2
+ import { useFetchClient, Page, Layouts, BackButton, useQueryParams, SearchInput, Pagination } from "@strapi/strapi/admin";
3
3
  import { useParams, useNavigate, Routes, Route } from "react-router-dom";
4
- import { g as getTranslation, P as PLUGIN_ID, a as PERMISSIONS } from "./index-DdmY-p3Q.mjs";
5
- import { Box, Flex, Typography, Button, Badge, Divider, Grid, Field, SingleSelect, SingleSelectOption, Combobox, ComboboxOption, TextInput, DatePicker, EmptyStateLayout, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, IconButton, Dialog } from "@strapi/design-system";
4
+ import { g as getTranslation, P as PLUGIN_ID, a as PERMISSIONS } from "./index-BIIMJgKU.mjs";
5
+ import { Box, Flex, Typography, Button, Badge, Divider, Grid, Field, SingleSelect, SingleSelectOption, Combobox, ComboboxOption, TextInput, DatePicker, EmptyStateLayout, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, IconButton } from "@strapi/design-system";
6
6
  import { useIntl } from "react-intl";
7
7
  import * as React from "react";
8
- import { CaretUp, CaretDown, Cross, Eye, Trash } from "@strapi/icons";
8
+ import { CaretUp, CaretDown, Cross, Eye } from "@strapi/icons";
9
9
  const ACTION_COLORS = {
10
10
  create: { background: "success100", text: "success600" },
11
11
  update: { background: "primary100", text: "primary600" },
@@ -33,7 +33,9 @@ const ACTION_COLORS = {
33
33
  "media.delete": { background: "danger100", text: "danger600" },
34
34
  "media-folder.create": { background: "success100", text: "success600" },
35
35
  "media-folder.update": { background: "primary100", text: "primary600" },
36
- "media-folder.delete": { background: "danger100", text: "danger600" }
36
+ "media-folder.delete": { background: "danger100", text: "danger600" },
37
+ // The log's own housekeeping. Neutral: expected, scheduled, and self-reported.
38
+ "retention.purge": { background: "neutral150", text: "neutral700" }
37
39
  };
38
40
  const actionColor = (action) => ACTION_COLORS[action] ?? { background: "neutral150", text: "neutral700" };
39
41
  const shortContentTypeName = (uid) => {
@@ -568,6 +570,30 @@ const useAuditFilterOptions = () => {
568
570
  }, [get]);
569
571
  return { options, isLoading };
570
572
  };
573
+ const useAuditIntegrity = () => {
574
+ const { get } = useFetchClient();
575
+ const [data, setData] = React.useState(null);
576
+ const [isLoading, setIsLoading] = React.useState(true);
577
+ const [error, setError] = React.useState(null);
578
+ React.useEffect(() => {
579
+ let cancelled = false;
580
+ const load = async () => {
581
+ try {
582
+ const { data: response } = await get(`/${PLUGIN_ID}/integrity`);
583
+ if (!cancelled) setData(response.data);
584
+ } catch (err) {
585
+ if (!cancelled) setError(message(err));
586
+ } finally {
587
+ if (!cancelled) setIsLoading(false);
588
+ }
589
+ };
590
+ void load();
591
+ return () => {
592
+ cancelled = true;
593
+ };
594
+ }, [get]);
595
+ return { data, isLoading, error };
596
+ };
571
597
  const useAuditLog = (id) => {
572
598
  const { get } = useFetchClient();
573
599
  const [data, setData] = React.useState(null);
@@ -602,15 +628,6 @@ const useAuditLog = (id) => {
602
628
  }, [get, id]);
603
629
  return { data, isLoading, error };
604
630
  };
605
- const useDeleteAuditLog = () => {
606
- const { del } = useFetchClient();
607
- return React.useCallback(
608
- async (id) => {
609
- await del(`/${PLUGIN_ID}/logs/${id}`);
610
- },
611
- [del]
612
- );
613
- };
614
631
  const MetaField = ({ label, value, mono = false }) => /* @__PURE__ */ jsxs(Grid.Item, { col: 4, s: 6, xs: 12, direction: "column", alignItems: "flex-start", gap: 1, children: [
615
632
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: label }),
616
633
  /* @__PURE__ */ jsx(
@@ -786,6 +803,25 @@ const AuditLogDetails = () => {
786
803
  value: log.userAgent,
787
804
  mono: true
788
805
  }
806
+ ),
807
+ /* @__PURE__ */ jsx(
808
+ MetaField,
809
+ {
810
+ label: formatMessage({ id: getTranslation("detail.hash"), defaultMessage: "Hash" }),
811
+ value: log.hash,
812
+ mono: true
813
+ }
814
+ ),
815
+ /* @__PURE__ */ jsx(
816
+ MetaField,
817
+ {
818
+ label: formatMessage({
819
+ id: getTranslation("detail.prevHash"),
820
+ defaultMessage: "Previous hash"
821
+ }),
822
+ value: log.prevHash,
823
+ mono: true
824
+ }
789
825
  )
790
826
  ] }) })
791
827
  ] }),
@@ -1009,7 +1045,6 @@ const AuditLogTable = ({
1009
1045
  sort,
1010
1046
  onSortChange,
1011
1047
  onView,
1012
- onDelete,
1013
1048
  isLoading
1014
1049
  }) => {
1015
1050
  const { formatMessage, locale } = useIntl();
@@ -1064,36 +1099,69 @@ const AuditLogTable = ({
1064
1099
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: log.locale ?? "-" }) }),
1065
1100
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: log.source }) }),
1066
1101
  /* @__PURE__ */ jsx(Td, { children: log.outcome === "failure" ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger600", children: log.outcome }) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: log.outcome ?? "success" }) }),
1067
- /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", onClick: (event) => event.stopPropagation(), children: [
1068
- /* @__PURE__ */ jsx(
1069
- IconButton,
1070
- {
1071
- label: formatMessage({
1072
- id: getTranslation("list.view"),
1073
- defaultMessage: "View details"
1074
- }),
1075
- variant: "ghost",
1076
- onClick: () => onView(log),
1077
- children: /* @__PURE__ */ jsx(Eye, {})
1078
- }
1079
- ),
1080
- onDelete ? /* @__PURE__ */ jsx(
1081
- IconButton,
1082
- {
1083
- label: formatMessage({
1084
- id: getTranslation("list.delete"),
1085
- defaultMessage: "Delete this record"
1086
- }),
1087
- variant: "ghost",
1088
- onClick: () => onDelete(log),
1089
- children: /* @__PURE__ */ jsx(Trash, {})
1090
- }
1091
- ) : null
1092
- ] }) })
1102
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Flex, { gap: 1, justifyContent: "flex-end", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx(
1103
+ IconButton,
1104
+ {
1105
+ label: formatMessage({
1106
+ id: getTranslation("list.view"),
1107
+ defaultMessage: "View details"
1108
+ }),
1109
+ variant: "ghost",
1110
+ onClick: () => onView(log),
1111
+ children: /* @__PURE__ */ jsx(Eye, {})
1112
+ }
1113
+ ) }) })
1093
1114
  ] }, log.id);
1094
1115
  }) })
1095
1116
  ] });
1096
1117
  };
1118
+ const IntegrityStatus = () => {
1119
+ const { formatMessage, locale } = useIntl();
1120
+ const { data, isLoading, error } = useAuditIntegrity();
1121
+ if (isLoading) {
1122
+ return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
1123
+ id: getTranslation("integrity.checking"),
1124
+ defaultMessage: "Verifying the audit chain…"
1125
+ }) });
1126
+ }
1127
+ if (error || !data) {
1128
+ return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning600", children: formatMessage(
1129
+ {
1130
+ id: getTranslation("integrity.unavailable"),
1131
+ defaultMessage: "Integrity check unavailable: {error}"
1132
+ },
1133
+ { error: error ?? "no report" }
1134
+ ) });
1135
+ }
1136
+ if (!data.ok) {
1137
+ return /* @__PURE__ */ jsxs(Box, { background: "danger100", hasRadius: true, padding: 3, borderColor: "danger200", borderWidth: "1px", borderStyle: "solid", children: [
1138
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, wrap: "wrap", children: [
1139
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger600", textColor: "neutral0", children: formatMessage({ id: getTranslation("integrity.broken"), defaultMessage: "CHAIN BROKEN" }) }),
1140
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger700", fontWeight: "bold", children: formatMessage(
1141
+ {
1142
+ id: getTranslation("integrity.brokenAt"),
1143
+ defaultMessage: "Record #{id}: {reason}"
1144
+ },
1145
+ { id: data.brokenAt?.id ?? "?", reason: data.brokenAt?.reason ?? "" }
1146
+ ) })
1147
+ ] }),
1148
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger700", children: formatMessage({
1149
+ id: getTranslation("integrity.brokenHint"),
1150
+ defaultMessage: "A record was modified, removed or inserted after it was written. Treat everything from this point as unverified and compare against the forwarded copy."
1151
+ }) }) })
1152
+ ] });
1153
+ }
1154
+ return /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, wrap: "wrap", children: [
1155
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: formatMessage({ id: getTranslation("integrity.ok"), defaultMessage: "Chain verified" }) }),
1156
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(
1157
+ {
1158
+ id: getTranslation("integrity.summary"),
1159
+ defaultMessage: "{hashed, plural, one {# linked record} other {# linked records}}{legacy, plural, =0 {} one { · # pre-chain record} other { · # pre-chain records}} · checked {when}"
1160
+ },
1161
+ { hashed: data.hashed, legacy: data.legacy, when: formatDate(data.verifiedAt, locale) }
1162
+ ) })
1163
+ ] });
1164
+ };
1097
1165
  const toSearchParams = (query) => {
1098
1166
  const params = new URLSearchParams();
1099
1167
  for (const [key, value] of Object.entries(query)) {
@@ -1105,17 +1173,10 @@ const toSearchParams = (query) => {
1105
1173
  const AuditLogs = () => {
1106
1174
  const { formatMessage } = useIntl();
1107
1175
  const navigate = useNavigate();
1108
- const { toggleNotification } = useNotification();
1109
1176
  const [{ query }, setQuery] = useQueryParams({ page: 1, pageSize: 20 });
1110
- const {
1111
- allowedActions: { canDelete },
1112
- isLoading: isLoadingPermissions
1113
- } = useRBAC(PERMISSIONS);
1114
1177
  const search = React.useMemo(() => toSearchParams(query), [query]);
1115
- const { data, pagination, isLoading, error, refresh } = useAuditLogs(search);
1178
+ const { data, pagination, isLoading, error } = useAuditLogs(search);
1116
1179
  const { options } = useAuditFilterOptions();
1117
- const deleteLog = useDeleteAuditLog();
1118
- const [pendingDelete, setPendingDelete] = React.useState(null);
1119
1180
  const updateQuery = React.useCallback(
1120
1181
  (patch) => {
1121
1182
  setQuery({ ...patch, page: 1 });
@@ -1139,33 +1200,6 @@ const AuditLogs = () => {
1139
1200
  "remove"
1140
1201
  );
1141
1202
  }, [setQuery]);
1142
- const confirmDelete = React.useCallback(async () => {
1143
- if (!pendingDelete) return;
1144
- try {
1145
- await deleteLog(pendingDelete.id);
1146
- toggleNotification({
1147
- type: "success",
1148
- message: formatMessage({
1149
- id: getTranslation("delete.success"),
1150
- defaultMessage: "Audit record deleted."
1151
- })
1152
- });
1153
- refresh();
1154
- } catch {
1155
- toggleNotification({
1156
- type: "danger",
1157
- message: formatMessage({
1158
- id: getTranslation("delete.error"),
1159
- defaultMessage: "Could not delete this audit record."
1160
- })
1161
- });
1162
- } finally {
1163
- setPendingDelete(null);
1164
- }
1165
- }, [deleteLog, formatMessage, pendingDelete, refresh, toggleNotification]);
1166
- if (isLoadingPermissions) {
1167
- return /* @__PURE__ */ jsx(Page.Loading, {});
1168
- }
1169
1203
  return /* @__PURE__ */ jsxs(Page.Main, { children: [
1170
1204
  /* @__PURE__ */ jsx(Page.Title, { children: formatMessage({ id: getTranslation("plugin.name"), defaultMessage: "Audit Logs" }) }),
1171
1205
  /* @__PURE__ */ jsx(
@@ -1196,6 +1230,7 @@ const AuditLogs = () => {
1196
1230
  }
1197
1231
  ),
1198
1232
  /* @__PURE__ */ jsxs(Layouts.Content, { children: [
1233
+ /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(IntegrityStatus, {}) }),
1199
1234
  /* @__PURE__ */ jsx(
1200
1235
  AuditLogFilters,
1201
1236
  {
@@ -1213,8 +1248,7 @@ const AuditLogs = () => {
1213
1248
  isLoading,
1214
1249
  sort: String(query.sort ?? "createdAt:desc"),
1215
1250
  onSortChange: (sort) => setQuery({ sort, page: 1 }),
1216
- onView: (log) => navigate(`${log.id}`),
1217
- onDelete: canDelete ? setPendingDelete : null
1251
+ onView: (log) => navigate(`${log.id}`)
1218
1252
  }
1219
1253
  ),
1220
1254
  /* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsx(
@@ -1229,31 +1263,7 @@ const AuditLogs = () => {
1229
1263
  ] })
1230
1264
  }
1231
1265
  ) })
1232
- ] }),
1233
- /* @__PURE__ */ jsx(
1234
- Dialog.Root,
1235
- {
1236
- open: pendingDelete !== null,
1237
- onOpenChange: (open) => {
1238
- if (!open) setPendingDelete(null);
1239
- },
1240
- children: /* @__PURE__ */ jsx(
1241
- ConfirmDialog,
1242
- {
1243
- title: formatMessage({
1244
- id: getTranslation("delete.title"),
1245
- defaultMessage: "Delete audit record"
1246
- }),
1247
- onConfirm: confirmDelete,
1248
- onCancel: () => setPendingDelete(null),
1249
- children: formatMessage({
1250
- id: getTranslation("delete.confirm"),
1251
- defaultMessage: "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?"
1252
- })
1253
- }
1254
- )
1255
- }
1256
- )
1266
+ ] })
1257
1267
  ] });
1258
1268
  };
1259
1269
  const App = () => /* @__PURE__ */ jsx(Page.Protect, { permissions: PERMISSIONS.read, children: /* @__PURE__ */ jsxs(Routes, { children: [
@@ -5,9 +5,8 @@ const en = {
5
5
  "plugin.description": "Track every change to your content and every security event.",
6
6
  "list.subtitle": "{total, plural, =0 {No records} one {# record} other {# records}} of content and security activity",
7
7
  "list.search": "Search by document id, user, content type or request id",
8
- "list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file.",
8
+ "list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file. Records cannot be deleted from this page.",
9
9
  "list.view": "View details",
10
- "list.delete": "Delete this record",
11
10
  "list.column.createdAt": "Date",
12
11
  "list.column.userName": "User",
13
12
  "list.column.action": "Action",
@@ -53,12 +52,6 @@ const en = {
53
52
  "changes.empty": "No field-level changes were recorded for this operation. This is expected for creates and deletes, and for updates that re-saved a document without modifying it.",
54
53
  "json.show": "Show ({count} lines)",
55
54
  "json.hide": "Hide",
56
- "delete.title": "Delete audit record",
57
- "delete.confirm": "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?",
58
- "delete.action": "Delete",
59
- "delete.cancel": "Cancel",
60
- "delete.success": "Audit record deleted.",
61
- "delete.error": "Could not delete this audit record.",
62
55
  "filters.outcome": "Outcome",
63
56
  "detail.outcome": "Outcome",
64
57
  "detail.metadata": "Event detail",
@@ -82,6 +75,15 @@ const en = {
82
75
  "widgets.status.added": "Added",
83
76
  "widgets.status.removed": "Removed",
84
77
  "widgets.status.unchanged": "Unchanged",
85
- "list.column.outcome": "Outcome"
78
+ "list.column.outcome": "Outcome",
79
+ "detail.hash": "Hash",
80
+ "detail.prevHash": "Previous hash",
81
+ "integrity.checking": "Verifying the audit chain…",
82
+ "integrity.unavailable": "Integrity check unavailable: {error}",
83
+ "integrity.ok": "Chain verified",
84
+ "integrity.summary": "{hashed, plural, one {# linked record} other {# linked records}}{legacy, plural, =0 {} one { · # pre-chain record} other { · # pre-chain records}} · checked {when}",
85
+ "integrity.broken": "CHAIN BROKEN",
86
+ "integrity.brokenAt": "Record #{id}: {reason}",
87
+ "integrity.brokenHint": "A record was modified, removed or inserted after it was written. Treat everything from this point as unverified and compare against the forwarded copy."
86
88
  };
87
89
  exports.default = en;
@@ -3,9 +3,8 @@ const en = {
3
3
  "plugin.description": "Track every change to your content and every security event.",
4
4
  "list.subtitle": "{total, plural, =0 {No records} one {# record} other {# records}} of content and security activity",
5
5
  "list.search": "Search by document id, user, content type or request id",
6
- "list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file.",
6
+ "list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file. Records cannot be deleted from this page.",
7
7
  "list.view": "View details",
8
- "list.delete": "Delete this record",
9
8
  "list.column.createdAt": "Date",
10
9
  "list.column.userName": "User",
11
10
  "list.column.action": "Action",
@@ -51,12 +50,6 @@ const en = {
51
50
  "changes.empty": "No field-level changes were recorded for this operation. This is expected for creates and deletes, and for updates that re-saved a document without modifying it.",
52
51
  "json.show": "Show ({count} lines)",
53
52
  "json.hide": "Hide",
54
- "delete.title": "Delete audit record",
55
- "delete.confirm": "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?",
56
- "delete.action": "Delete",
57
- "delete.cancel": "Cancel",
58
- "delete.success": "Audit record deleted.",
59
- "delete.error": "Could not delete this audit record.",
60
53
  "filters.outcome": "Outcome",
61
54
  "detail.outcome": "Outcome",
62
55
  "detail.metadata": "Event detail",
@@ -80,7 +73,16 @@ const en = {
80
73
  "widgets.status.added": "Added",
81
74
  "widgets.status.removed": "Removed",
82
75
  "widgets.status.unchanged": "Unchanged",
83
- "list.column.outcome": "Outcome"
76
+ "list.column.outcome": "Outcome",
77
+ "detail.hash": "Hash",
78
+ "detail.prevHash": "Previous hash",
79
+ "integrity.checking": "Verifying the audit chain…",
80
+ "integrity.unavailable": "Integrity check unavailable: {error}",
81
+ "integrity.ok": "Chain verified",
82
+ "integrity.summary": "{hashed, plural, one {# linked record} other {# linked records}}{legacy, plural, =0 {} one { · # pre-chain record} other { · # pre-chain records}} · checked {when}",
83
+ "integrity.broken": "CHAIN BROKEN",
84
+ "integrity.brokenAt": "Record #{id}: {reason}",
85
+ "integrity.brokenHint": "A record was modified, removed or inserted after it was written. Treat everything from this point as unverified and compare against the forwarded copy."
84
86
  };
85
87
  export {
86
88
  en as default
@@ -27,9 +27,7 @@ const entity = (action) => ({
27
27
  conditions: []
28
28
  });
29
29
  const PERMISSIONS = {
30
- read: [entity(`plugin::${PLUGIN_ID}.read`)],
31
- delete: [entity(`plugin::${PLUGIN_ID}.delete`)],
32
- settings: [entity(`plugin::${PLUGIN_ID}.settings`)]
30
+ read: [entity(`plugin::${PLUGIN_ID}.read`)]
33
31
  };
34
32
  const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
35
33
  const plugin = {
@@ -42,7 +40,7 @@ const plugin = {
42
40
  defaultMessage: "Audit Logs"
43
41
  },
44
42
  permissions: PERMISSIONS.read,
45
- Component: () => import("./App-ou5hy99f.mjs").then((mod) => ({ default: mod.App }))
43
+ Component: () => import("./App-jWvaMXIw.mjs").then((mod) => ({ default: mod.App }))
46
44
  });
47
45
  app.registerPlugin({
48
46
  id: PLUGIN_ID,
@@ -60,7 +58,7 @@ const plugin = {
60
58
  return Promise.all(
61
59
  locales.map(async (locale) => {
62
60
  try {
63
- const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-B0rPiE2W.mjs") }), `./translations/${locale}.json`, 3);
61
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-C3Og1VyK.mjs") }), `./translations/${locale}.json`, 3);
64
62
  const namespaced = {};
65
63
  for (const key of Object.keys(data)) {
66
64
  namespaced[getTranslation(key)] = data[key];
@@ -28,9 +28,7 @@ const entity = (action) => ({
28
28
  conditions: []
29
29
  });
30
30
  const PERMISSIONS = {
31
- read: [entity(`plugin::${PLUGIN_ID}.read`)],
32
- delete: [entity(`plugin::${PLUGIN_ID}.delete`)],
33
- settings: [entity(`plugin::${PLUGIN_ID}.settings`)]
31
+ read: [entity(`plugin::${PLUGIN_ID}.read`)]
34
32
  };
35
33
  const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
36
34
  const plugin = {
@@ -43,7 +41,7 @@ const plugin = {
43
41
  defaultMessage: "Audit Logs"
44
42
  },
45
43
  permissions: PERMISSIONS.read,
46
- Component: () => Promise.resolve().then(() => require("./App-BLt4lqEM.js")).then((mod) => ({ default: mod.App }))
44
+ Component: () => Promise.resolve().then(() => require("./App-CmbG2k9E.js")).then((mod) => ({ default: mod.App }))
47
45
  });
48
46
  app.registerPlugin({
49
47
  id: PLUGIN_ID,
@@ -61,7 +59,7 @@ const plugin = {
61
59
  return Promise.all(
62
60
  locales.map(async (locale) => {
63
61
  try {
64
- const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-BMlJxq3g.js")) }), `./translations/${locale}.json`, 3);
62
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-Bs4tlklI.js")) }), `./translations/${locale}.json`, 3);
65
63
  const namespaced = {};
66
64
  for (const key of Object.keys(data)) {
67
65
  namespaced[getTranslation(key)] = data[key];
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const index = require("./index-BYi8OPTw.js");
3
+ const index = require("./index-BsVOGSDf.js");
4
4
  exports.default = index.plugin;
@@ -1,4 +1,4 @@
1
- import { p } from "./index-DdmY-p3Q.mjs";
1
+ import { p } from "./index-BIIMJgKU.mjs";
2
2
  export {
3
3
  p as default
4
4
  };
@@ -5,7 +5,6 @@ interface AuditLogTableProps {
5
5
  sort: string;
6
6
  onSortChange: (sort: string) => void;
7
7
  onView: (log: AuditLog) => void;
8
- onDelete: ((log: AuditLog) => void) | null;
9
8
  isLoading: boolean;
10
9
  }
11
10
  /**
@@ -15,5 +14,5 @@ interface AuditLogTableProps {
15
14
  * `Array.sort`: the table only ever holds one page, so sorting it locally would
16
15
  * reorder twenty rows and quietly lie about the other ten thousand.
17
16
  */
18
- declare const AuditLogTable: ({ logs, sort, onSortChange, onView, onDelete, isLoading, }: AuditLogTableProps) => React.JSX.Element;
17
+ declare const AuditLogTable: ({ logs, sort, onSortChange, onView, isLoading, }: AuditLogTableProps) => React.JSX.Element;
19
18
  export { AuditLogTable };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The hash-chain verdict, shown above the list.
3
+ *
4
+ * An audit log that cannot say whether it has been tampered with is taking its
5
+ * own word for it. This runs the full chain walk on every visit to the page —
6
+ * deliberately, rather than on demand, so that the check is not something an
7
+ * administrator has to remember to do — and states the result in one line.
8
+ *
9
+ * A broken chain is the one thing on this page that should look alarming, so it
10
+ * is the only thing here rendered in red.
11
+ */
12
+ declare const IntegrityStatus: () => import("react").JSX.Element;
13
+ export { IntegrityStatus };
@@ -1,4 +1,4 @@
1
- import { AuditFilterOptions, AuditLog, AuditPagination } from '../types';
1
+ import { AuditFilterOptions, AuditLog, AuditPagination, IntegrityReport } from '../types';
2
2
  /**
3
3
  * Fetches one page of audit logs.
4
4
  *
@@ -26,11 +26,22 @@ export declare const useAuditFilterOptions: () => {
26
26
  options: AuditFilterOptions;
27
27
  isLoading: boolean;
28
28
  };
29
+ /**
30
+ * Runs the hash-chain verification.
31
+ *
32
+ * Fetched once per mount of the list page. The walk is server-side and paged,
33
+ * so its cost grows with the table, but it is a sequential read of an indexed
34
+ * primary key — the kind of thing a database does quickly — and the check is
35
+ * worth nothing if it is not run.
36
+ */
37
+ export declare const useAuditIntegrity: () => {
38
+ data: IntegrityReport | null;
39
+ isLoading: boolean;
40
+ error: string | null;
41
+ };
29
42
  /** Loads one audit record for the detail page. */
30
43
  export declare const useAuditLog: (id: string | undefined) => {
31
44
  data: AuditLog | null;
32
45
  isLoading: boolean;
33
46
  error: string | null;
34
47
  };
35
- /** Deletes one record. Requires `plugin::audit-log.delete` on the server. */
36
- export declare const useDeleteAuditLog: () => (id: number) => Promise<void>;
@@ -7,14 +7,6 @@ export declare const PERMISSIONS: {
7
7
  properties: {};
8
8
  conditions: never[];
9
9
  }[];
10
- delete: {
11
- action: string;
12
- subject: null;
13
- id: string;
14
- actionParameters: {};
15
- properties: {};
16
- conditions: never[];
17
- }[];
18
10
  settings: {
19
11
  action: string;
20
12
  subject: null;
@@ -5,7 +5,7 @@
5
5
  * own model: `Date` columns are ISO strings here, and the compiled ignore-list
6
6
  * matchers on `AuditConfig` never cross the wire. The canonical definitions live
7
7
  * in `server/src/types` and are re-exported from
8
- * `strapi-plugin-audit-log/strapi-server`; see the note there on why the two
8
+ * `strapi-cms-audit-log/strapi-server`; see the note there on why the two
9
9
  * halves cannot share one module.
10
10
  */
11
11
  export type AuditAction = 'create' | 'update' | 'delete' | 'publish' | 'unpublish';
@@ -15,7 +15,9 @@ export type AuditAction = 'create' | 'update' | 'delete' | 'publish' | 'unpublis
15
15
  * govern everything else.
16
16
  */
17
17
  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';
18
- export type AuditAnyAction = AuditAction | AuditSecurityAction;
18
+ /** The audit log's own maintenance: the retention purge recording itself. */
19
+ export type AuditMaintenanceAction = 'retention.purge';
20
+ export type AuditAnyAction = AuditAction | AuditSecurityAction | AuditMaintenanceAction;
19
21
  /** Whether the recorded attempt succeeded. Only security actions are ever `failure`. */
20
22
  export type AuditOutcome = 'success' | 'failure';
21
23
  export type AuditSource = 'admin' | 'api' | 'system' | 'cron' | 'migration' | 'unknown';
@@ -46,9 +48,31 @@ export interface AuditLog {
46
48
  outcome: AuditOutcome | null;
47
49
  /** Action-specific detail: the reason a login failed, the path of a denied request, an upload's filename. */
48
50
  metadata: Record<string, unknown> | null;
51
+ /** SHA-256 of this record and its predecessor's hash. Null on rows written before the chain existed. */
52
+ hash: string | null;
53
+ prevHash: string | null;
49
54
  createdAt: string;
50
55
  updatedAt: string;
51
56
  }
57
+ /** Verdict of a full walk of the hash chain. Mirrors the server's `IntegrityReport`. */
58
+ export interface IntegrityReport {
59
+ ok: boolean;
60
+ checked: number;
61
+ hashed: number;
62
+ legacy: number;
63
+ head: {
64
+ id: number;
65
+ hash: string;
66
+ } | null;
67
+ start: {
68
+ id: number;
69
+ } | null;
70
+ brokenAt?: {
71
+ id: number;
72
+ reason: string;
73
+ };
74
+ verifiedAt: string;
75
+ }
52
76
  export interface AuditPagination {
53
77
  page: number;
54
78
  pageSize: number;