@echovisionlab/geul-telemetry 0.1.2 → 0.1.3
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.
- package/README.md +7 -0
- package/package.json +1 -1
- package/src/audit/content-shared.ts +184 -0
- package/src/audit/content.ts +43 -691
- package/src/audit/page.ts +137 -0
- package/src/audit/post.ts +263 -0
- package/src/audit/work.ts +183 -0
package/README.md
CHANGED
|
@@ -13,6 +13,13 @@ pnpm add @echovisionlab/geul-telemetry
|
|
|
13
13
|
Browser code should import the `/actor`, `/redaction`, or `/trace` subpaths.
|
|
14
14
|
The package root also includes Node request context and is server-only.
|
|
15
15
|
|
|
16
|
+
## Audit records
|
|
17
|
+
|
|
18
|
+
Post, Page, and Work semantic builders and update validators live in their
|
|
19
|
+
respective internal entity modules. The existing audit facade keeps the shared
|
|
20
|
+
builder exports stable and validates records against the same typed wire
|
|
21
|
+
contract and action catalog.
|
|
22
|
+
|
|
16
23
|
## Development
|
|
17
24
|
|
|
18
25
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echovisionlab/geul-telemetry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared logging, tracing, audit, security, request-correlation, and redaction contracts for Geul services.",
|
|
6
6
|
"author": "state303 <state303@dsub.io>",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { AuditRecord, AuditState } from "../records.ts";
|
|
2
|
+
import {
|
|
3
|
+
canonicalAuditValues,
|
|
4
|
+
assertOnlyAuditAttributes,
|
|
5
|
+
type AuditRecordAttributeName,
|
|
6
|
+
} from "./attributes.ts";
|
|
7
|
+
import { buildAuditRecord } from "./builder.ts";
|
|
8
|
+
import { AUDIT_CATALOG } from "./catalog.ts";
|
|
9
|
+
import type { AuditMetadata } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export type ContentTarget = "post" | "page" | "work";
|
|
12
|
+
type ContentAction =
|
|
13
|
+
| "post.created"
|
|
14
|
+
| "post.updated"
|
|
15
|
+
| "post.deleted"
|
|
16
|
+
| "page.created"
|
|
17
|
+
| "page.updated"
|
|
18
|
+
| "page.deleted"
|
|
19
|
+
| "work.created"
|
|
20
|
+
| "work.updated"
|
|
21
|
+
| "work.deleted";
|
|
22
|
+
|
|
23
|
+
export function buildContentAuditRecord(
|
|
24
|
+
metadata: AuditMetadata,
|
|
25
|
+
action: ContentAction,
|
|
26
|
+
targetId: string,
|
|
27
|
+
attributes: Parameters<typeof buildAuditRecord>[2] = {},
|
|
28
|
+
): AuditRecord {
|
|
29
|
+
return buildAuditRecord(
|
|
30
|
+
metadata,
|
|
31
|
+
{ action, target_type: AUDIT_CATALOG[action], target_id: targetId },
|
|
32
|
+
attributes,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function requireFields(
|
|
37
|
+
record: AuditRecord,
|
|
38
|
+
allowed: ReadonlySet<string>,
|
|
39
|
+
): readonly string[] {
|
|
40
|
+
const fields = record.changed_fields;
|
|
41
|
+
if (
|
|
42
|
+
!fields?.length ||
|
|
43
|
+
fields.some(
|
|
44
|
+
(field, i) => !allowed.has(field) || (i > 0 && fields[i - 1] >= field),
|
|
45
|
+
)
|
|
46
|
+
) {
|
|
47
|
+
throw new TypeError("invalid or non-canonical changed_fields");
|
|
48
|
+
}
|
|
49
|
+
return fields;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function requireIdentifier(
|
|
53
|
+
name: string,
|
|
54
|
+
value: string | undefined,
|
|
55
|
+
): void {
|
|
56
|
+
if (!value || value.length > 255 || value.trim() !== value) {
|
|
57
|
+
throw new TypeError(`${name} requires an identifier`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function requireOnly(
|
|
62
|
+
record: AuditRecord,
|
|
63
|
+
allowed: readonly AuditRecordAttributeName[],
|
|
64
|
+
): void {
|
|
65
|
+
assertOnlyAuditAttributes(record, allowed);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function requireVersion(record: AuditRecord): void {
|
|
69
|
+
if (!record.version_id || !record.contributor_member_ids?.length) {
|
|
70
|
+
throw new TypeError(
|
|
71
|
+
"version requires version_id and contributor_member_ids",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
record.contributor_member_ids.some(
|
|
76
|
+
(id, i) =>
|
|
77
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(
|
|
78
|
+
id,
|
|
79
|
+
) ||
|
|
80
|
+
(i > 0 && record.contributor_member_ids![i - 1] >= id),
|
|
81
|
+
)
|
|
82
|
+
) {
|
|
83
|
+
throw new TypeError("version requires sorted unique UUIDv4 contributors");
|
|
84
|
+
}
|
|
85
|
+
requireOnly(record, [
|
|
86
|
+
"changed_fields",
|
|
87
|
+
"version_id",
|
|
88
|
+
"contributor_member_ids",
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function requireContentVersionActor(record: AuditRecord): void {
|
|
93
|
+
const version =
|
|
94
|
+
record.changed_fields?.length === 1 &&
|
|
95
|
+
record.changed_fields[0] === "version";
|
|
96
|
+
if (version) {
|
|
97
|
+
if (
|
|
98
|
+
record.actor_kind !== "system" ||
|
|
99
|
+
record.actor_service !== "geul-collab"
|
|
100
|
+
)
|
|
101
|
+
throw new TypeError("content version requires geul-collab system actor");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (record.actor_kind === "system") {
|
|
105
|
+
throw new TypeError(
|
|
106
|
+
"content system actor is limited to geul-collab version checkpoints",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function requireStateTransition(
|
|
112
|
+
record: AuditRecord,
|
|
113
|
+
allowed: readonly AuditState[],
|
|
114
|
+
): void {
|
|
115
|
+
if (
|
|
116
|
+
!record.previous_state ||
|
|
117
|
+
!record.new_state ||
|
|
118
|
+
record.previous_state === record.new_state ||
|
|
119
|
+
!allowed.includes(record.previous_state) ||
|
|
120
|
+
!allowed.includes(record.new_state)
|
|
121
|
+
) {
|
|
122
|
+
throw new TypeError("lifecycle requires an allowed state transition");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function requireShareLink(record: AuditRecord): void {
|
|
127
|
+
if (
|
|
128
|
+
record.item_operation !== "created" &&
|
|
129
|
+
record.item_operation !== "deleted"
|
|
130
|
+
) {
|
|
131
|
+
throw new TypeError(
|
|
132
|
+
"share link requires created or deleted item operation",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
requireIdentifier("item_id", record.item_id);
|
|
136
|
+
requireOnly(record, ["changed_fields", "item_operation", "item_id"]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function requireFeaturedImage(
|
|
140
|
+
record: AuditRecord,
|
|
141
|
+
attribute: "asset_id" | "file_id",
|
|
142
|
+
): void {
|
|
143
|
+
if (
|
|
144
|
+
record.collection_operation !== "added" &&
|
|
145
|
+
record.collection_operation !== "removed"
|
|
146
|
+
) {
|
|
147
|
+
throw new TypeError("featured image requires a binding operation");
|
|
148
|
+
}
|
|
149
|
+
requireIdentifier(attribute, record[attribute]);
|
|
150
|
+
requireOnly(record, ["changed_fields", "collection_operation", attribute]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function requireVersionRestore(record: AuditRecord): void {
|
|
154
|
+
requireIdentifier("version_id", record.version_id);
|
|
155
|
+
requireOnly(record, ["changed_fields", "version_id"]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function buildVersionCreatedAuditRecord(
|
|
159
|
+
metadata: AuditMetadata,
|
|
160
|
+
action: Extract<
|
|
161
|
+
ContentAction,
|
|
162
|
+
"post.updated" | "page.updated" | "work.updated"
|
|
163
|
+
>,
|
|
164
|
+
targetId: string,
|
|
165
|
+
versionId: string,
|
|
166
|
+
contributorMemberIds: readonly string[],
|
|
167
|
+
): AuditRecord {
|
|
168
|
+
return buildContentAuditRecord(metadata, action, targetId, {
|
|
169
|
+
changed_fields: ["version"],
|
|
170
|
+
version_id: versionId,
|
|
171
|
+
contributor_member_ids: canonicalAuditValues(contributorMemberIds),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildRootAuditRecord(
|
|
176
|
+
m: AuditMetadata,
|
|
177
|
+
action: Extract<
|
|
178
|
+
ContentAction,
|
|
179
|
+
`${ContentTarget}.created` | `${ContentTarget}.deleted`
|
|
180
|
+
>,
|
|
181
|
+
id: string,
|
|
182
|
+
): AuditRecord {
|
|
183
|
+
return buildContentAuditRecord(m, action, id);
|
|
184
|
+
}
|