@echovisionlab/geul-telemetry 0.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 (58) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +38 -0
  3. package/fixtures/audit-records.json +316 -0
  4. package/fixtures/collaboration-checkpoint-entity-types.json +15 -0
  5. package/fixtures/domain-audit-actions.json +132 -0
  6. package/fixtures/domain-audit-wire-parity.json +1751 -0
  7. package/fixtures/http-request-results.json +21 -0
  8. package/fixtures/job-catalog.json +23 -0
  9. package/fixtures/locale-content-audit.json +126 -0
  10. package/fixtures/log-redaction.json +72 -0
  11. package/fixtures/request-record.json +14 -0
  12. package/fixtures/security-access-records.json +61 -0
  13. package/fixtures/service-identities.json +11 -0
  14. package/fixtures/system-catalog.json +28 -0
  15. package/fixtures/translation-job-terminal-catalog.json +45 -0
  16. package/fixtures/translation-settings-fields.json +1 -0
  17. package/package.json +61 -0
  18. package/policy/log-redaction.json +113 -0
  19. package/src/actor.ts +81 -0
  20. package/src/audit/account.ts +206 -0
  21. package/src/audit/actor-policy.ts +63 -0
  22. package/src/audit/artist-label.ts +312 -0
  23. package/src/audit/attributes.ts +107 -0
  24. package/src/audit/builder.ts +25 -0
  25. package/src/audit/catalog.ts +116 -0
  26. package/src/audit/content.ts +723 -0
  27. package/src/audit/email-authoring.ts +220 -0
  28. package/src/audit/file.ts +168 -0
  29. package/src/audit/form.ts +229 -0
  30. package/src/audit/integrations.ts +207 -0
  31. package/src/audit/locale-code.ts +6 -0
  32. package/src/audit/locale-content.ts +358 -0
  33. package/src/audit/member-account.ts +313 -0
  34. package/src/audit/member.ts +171 -0
  35. package/src/audit/program.ts +339 -0
  36. package/src/audit/reference-data.ts +230 -0
  37. package/src/audit/reference-entities.ts +332 -0
  38. package/src/audit/relation-download-policy.ts +292 -0
  39. package/src/audit/release-campaign.ts +422 -0
  40. package/src/audit/series-type.ts +298 -0
  41. package/src/audit/settings.ts +306 -0
  42. package/src/audit/source-locale.ts +337 -0
  43. package/src/audit/types.ts +9 -0
  44. package/src/audit.ts +21 -0
  45. package/src/authentication.ts +183 -0
  46. package/src/error-metadata.ts +11 -0
  47. package/src/index.ts +11 -0
  48. package/src/job.ts +56 -0
  49. package/src/log-attribute-policy.ts +21 -0
  50. package/src/otel.ts +64 -0
  51. package/src/records.ts +1244 -0
  52. package/src/redaction.ts +35 -0
  53. package/src/request-context.ts +73 -0
  54. package/src/request-id.ts +13 -0
  55. package/src/request-result.ts +73 -0
  56. package/src/request.ts +60 -0
  57. package/src/system.ts +355 -0
  58. package/src/trace.ts +13 -0
@@ -0,0 +1,298 @@
1
+ import type {
2
+ AuditCollectionOperation,
3
+ AuditRecord,
4
+ AuditRelationship,
5
+ AuditState,
6
+ } from "../records.ts";
7
+ import {
8
+ assertOnlyAuditAttributes,
9
+ canonicalAuditValues,
10
+ } from "./attributes.ts";
11
+ import { buildAuditRecord } from "./builder.ts";
12
+ import {
13
+ AUDIT_ACTIONS,
14
+ AUDIT_CATALOG,
15
+ requireCatalogTarget,
16
+ } from "./catalog.ts";
17
+ import type { AuditMetadata } from "./types.ts";
18
+
19
+ type SeriesTypeAction = Extract<
20
+ (typeof AUDIT_ACTIONS)[number],
21
+ | "post_series.created"
22
+ | "post_series.updated"
23
+ | "post_series.deleted"
24
+ | "program_event_type.created"
25
+ | "program_event_type.updated"
26
+ | "program_event_type.deleted"
27
+ >;
28
+ type PostSeriesLifecycleState = Extract<AuditState, "draft" | "published">;
29
+ export type PostSeriesSourceMetadataField = "slug" | "source_copy";
30
+ export type ProgramEventTypeConfigField =
31
+ "requires_place" | "requires_stream_url" | "slug" | "sort_order" | "status";
32
+
33
+ const targetTypes = new Set(["post_series", "program_event_type"] as const);
34
+ const seriesTypeActions = new Set<SeriesTypeAction>(
35
+ AUDIT_ACTIONS.filter((action) =>
36
+ targetTypes.has(
37
+ AUDIT_CATALOG[action] as "post_series" | "program_event_type",
38
+ ),
39
+ ) as SeriesTypeAction[],
40
+ );
41
+ const postSeriesSourceMetadataFields = new Set<PostSeriesSourceMetadataField>([
42
+ "slug",
43
+ "source_copy",
44
+ ]);
45
+ const programEventTypeConfigFields = new Set<ProgramEventTypeConfigField>([
46
+ "requires_place",
47
+ "requires_stream_url",
48
+ "slug",
49
+ "sort_order",
50
+ "status",
51
+ ]);
52
+ function buildSeriesTypeAuditRecord(
53
+ metadata: AuditMetadata,
54
+ action: SeriesTypeAction,
55
+ targetId: string,
56
+ attributes: Parameters<typeof buildAuditRecord>[2] = {},
57
+ ): AuditRecord {
58
+ return buildAuditRecord(
59
+ metadata,
60
+ { action, target_type: AUDIT_CATALOG[action], target_id: targetId },
61
+ attributes,
62
+ );
63
+ }
64
+ function requireNoExtra(record: AuditRecord, allowed: readonly string[]): void {
65
+ assertOnlyAuditAttributes(record, allowed);
66
+ }
67
+ function requireIdentifier(name: string, value: string | undefined): void {
68
+ if (!value || value.length > 255 || value.trim() !== value)
69
+ throw new TypeError(`${name} requires an identifier`);
70
+ }
71
+ function requireFields(
72
+ record: AuditRecord,
73
+ allowed: ReadonlySet<string>,
74
+ ): void {
75
+ const fields = record.changed_fields;
76
+ if (
77
+ !fields?.length ||
78
+ fields.some(
79
+ (field, index) =>
80
+ !allowed.has(field) || (index > 0 && fields[index - 1] >= field),
81
+ )
82
+ )
83
+ throw new TypeError("invalid or non-canonical changed_fields");
84
+ }
85
+ function requirePostSeriesLifecycle(record: AuditRecord): void {
86
+ if (
87
+ record.changed_fields?.length !== 1 ||
88
+ record.changed_fields[0] !== "status" ||
89
+ !(
90
+ (record.previous_state === "draft" && record.new_state === "published") ||
91
+ (record.previous_state === "published" && record.new_state === "draft")
92
+ )
93
+ )
94
+ throw new TypeError(
95
+ "post series lifecycle requires draft/published transition",
96
+ );
97
+ requireNoExtra(record, ["changed_fields", "previous_state", "new_state"]);
98
+ }
99
+ function requirePostSeriesManager(record: AuditRecord): void {
100
+ if (
101
+ record.changed_fields?.length !== 1 ||
102
+ record.changed_fields[0] !== "managers" ||
103
+ !["none", "manager"].includes(record.previous_relationship ?? "") ||
104
+ !["none", "manager"].includes(record.new_relationship ?? "") ||
105
+ record.previous_relationship === record.new_relationship
106
+ )
107
+ throw new TypeError(
108
+ "post series manager requires an allowed relationship transition",
109
+ );
110
+ requireIdentifier("subject_member_id", record.subject_member_id);
111
+ requireNoExtra(record, [
112
+ "changed_fields",
113
+ "subject_member_id",
114
+ "previous_relationship",
115
+ "new_relationship",
116
+ ]);
117
+ }
118
+ function requirePostSeriesMembership(record: AuditRecord): void {
119
+ if (
120
+ record.changed_fields?.length !== 1 ||
121
+ record.changed_fields[0] !== "posts" ||
122
+ record.previous_series_id === record.new_series_id
123
+ )
124
+ throw new TypeError(
125
+ "post series membership requires subject and distinct series",
126
+ );
127
+ requireIdentifier("subject_post_id", record.subject_post_id);
128
+ requireNoExtra(record, [
129
+ "changed_fields",
130
+ "subject_post_id",
131
+ "previous_series_id",
132
+ "new_series_id",
133
+ ]);
134
+ }
135
+ function requirePostSeriesOrder(record: AuditRecord): void {
136
+ if (
137
+ record.changed_fields?.length !== 1 ||
138
+ record.changed_fields[0] !== "post_order" ||
139
+ record.post_ids === undefined
140
+ )
141
+ throw new TypeError("post series order requires post_ids");
142
+ for (const postId of record.post_ids) requireIdentifier("post_ids", postId);
143
+ requireNoExtra(record, ["changed_fields", "post_ids"]);
144
+ }
145
+ function requirePostSeriesFeaturedImage(record: AuditRecord): void {
146
+ if (
147
+ record.changed_fields?.length !== 1 ||
148
+ record.changed_fields[0] !== "featured_image" ||
149
+ (record.collection_operation !== "added" &&
150
+ record.collection_operation !== "removed")
151
+ )
152
+ throw new TypeError(
153
+ "post series featured image requires collection operation",
154
+ );
155
+ requireIdentifier("file_id", record.file_id);
156
+ requireNoExtra(record, ["changed_fields", "collection_operation", "file_id"]);
157
+ }
158
+ function validatePostSeriesUpdate(record: AuditRecord): void {
159
+ switch (record.changed_fields?.[0]) {
160
+ case "status":
161
+ return requirePostSeriesLifecycle(record);
162
+ case "managers":
163
+ return requirePostSeriesManager(record);
164
+ case "posts":
165
+ return requirePostSeriesMembership(record);
166
+ case "post_order":
167
+ return requirePostSeriesOrder(record);
168
+ case "featured_image":
169
+ return requirePostSeriesFeaturedImage(record);
170
+ default:
171
+ requireFields(record, postSeriesSourceMetadataFields);
172
+ return requireNoExtra(record, ["changed_fields"]);
173
+ }
174
+ }
175
+
176
+ /** Returns true only for Post Series and Program Event Type catalog actions. */
177
+ export function validateSeriesTypeAuditRecord(record: AuditRecord): boolean {
178
+ if (!seriesTypeActions.has(record.action as SeriesTypeAction)) return false;
179
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
180
+ if (!record.action.endsWith(".updated")) {
181
+ requireNoExtra(record, []);
182
+ return true;
183
+ }
184
+ if (record.action === "post_series.updated") validatePostSeriesUpdate(record);
185
+ else {
186
+ requireFields(record, programEventTypeConfigFields);
187
+ requireNoExtra(record, ["changed_fields"]);
188
+ }
189
+ return true;
190
+ }
191
+
192
+ export function buildPostSeriesCreatedAuditRecord(
193
+ metadata: AuditMetadata,
194
+ id: string,
195
+ ): AuditRecord {
196
+ return buildSeriesTypeAuditRecord(metadata, "post_series.created", id);
197
+ }
198
+ export function buildPostSeriesDeletedAuditRecord(
199
+ metadata: AuditMetadata,
200
+ id: string,
201
+ ): AuditRecord {
202
+ return buildSeriesTypeAuditRecord(metadata, "post_series.deleted", id);
203
+ }
204
+ export function buildPostSeriesSourceMetadataAuditRecord(
205
+ metadata: AuditMetadata,
206
+ id: string,
207
+ fields: readonly PostSeriesSourceMetadataField[],
208
+ ): AuditRecord {
209
+ return buildSeriesTypeAuditRecord(metadata, "post_series.updated", id, {
210
+ changed_fields: canonicalAuditValues(fields),
211
+ });
212
+ }
213
+ export function buildPostSeriesLifecycleAuditRecord(
214
+ metadata: AuditMetadata,
215
+ id: string,
216
+ previousState: PostSeriesLifecycleState,
217
+ newState: PostSeriesLifecycleState,
218
+ ): AuditRecord {
219
+ return buildSeriesTypeAuditRecord(metadata, "post_series.updated", id, {
220
+ changed_fields: ["status"],
221
+ previous_state: previousState,
222
+ new_state: newState,
223
+ });
224
+ }
225
+ export function buildPostSeriesManagerAuditRecord(
226
+ metadata: AuditMetadata,
227
+ id: string,
228
+ subjectMemberId: string,
229
+ previousRelationship: Extract<AuditRelationship, "none" | "manager">,
230
+ newRelationship: Extract<AuditRelationship, "none" | "manager">,
231
+ ): AuditRecord {
232
+ return buildSeriesTypeAuditRecord(metadata, "post_series.updated", id, {
233
+ changed_fields: ["managers"],
234
+ subject_member_id: subjectMemberId,
235
+ previous_relationship: previousRelationship,
236
+ new_relationship: newRelationship,
237
+ });
238
+ }
239
+ export function buildPostSeriesMembershipAuditRecord(
240
+ metadata: AuditMetadata,
241
+ id: string,
242
+ subjectPostId: string,
243
+ previousSeriesId: string,
244
+ newSeriesId: string,
245
+ ): AuditRecord {
246
+ return buildSeriesTypeAuditRecord(metadata, "post_series.updated", id, {
247
+ changed_fields: ["posts"],
248
+ subject_post_id: subjectPostId,
249
+ previous_series_id: previousSeriesId === "" ? undefined : previousSeriesId,
250
+ new_series_id: newSeriesId === "" ? undefined : newSeriesId,
251
+ });
252
+ }
253
+ export function buildPostSeriesOrderAuditRecord(
254
+ metadata: AuditMetadata,
255
+ id: string,
256
+ postIds: readonly string[],
257
+ ): AuditRecord {
258
+ return buildSeriesTypeAuditRecord(metadata, "post_series.updated", id, {
259
+ changed_fields: ["post_order"],
260
+ post_ids: [...postIds],
261
+ });
262
+ }
263
+ export function buildPostSeriesFeaturedImageAuditRecord(
264
+ metadata: AuditMetadata,
265
+ id: string,
266
+ collectionOperation: AuditCollectionOperation,
267
+ fileId: string,
268
+ ): AuditRecord {
269
+ return buildSeriesTypeAuditRecord(metadata, "post_series.updated", id, {
270
+ changed_fields: ["featured_image"],
271
+ collection_operation: collectionOperation,
272
+ file_id: fileId,
273
+ });
274
+ }
275
+ export function buildProgramEventTypeCreatedAuditRecord(
276
+ metadata: AuditMetadata,
277
+ id: string,
278
+ ): AuditRecord {
279
+ return buildSeriesTypeAuditRecord(metadata, "program_event_type.created", id);
280
+ }
281
+ export function buildProgramEventTypeDeletedAuditRecord(
282
+ metadata: AuditMetadata,
283
+ id: string,
284
+ ): AuditRecord {
285
+ return buildSeriesTypeAuditRecord(metadata, "program_event_type.deleted", id);
286
+ }
287
+ export function buildProgramEventTypeConfigUpdatedAuditRecord(
288
+ metadata: AuditMetadata,
289
+ id: string,
290
+ fields: readonly ProgramEventTypeConfigField[],
291
+ ): AuditRecord {
292
+ return buildSeriesTypeAuditRecord(
293
+ metadata,
294
+ "program_event_type.updated",
295
+ id,
296
+ { changed_fields: canonicalAuditValues(fields) },
297
+ );
298
+ }
@@ -0,0 +1,306 @@
1
+ import type { AuditRecord } from "../records.ts";
2
+ import {
3
+ assertOnlyAuditAttributes,
4
+ canonicalAuditValues,
5
+ } from "./attributes.ts";
6
+ import { buildAuditRecord } from "./builder.ts";
7
+ import { requireCatalogTarget } from "./catalog.ts";
8
+ import type { AuditMetadata } from "./types.ts";
9
+
10
+ function buildSettingsAuditRecord(
11
+ metadata: AuditMetadata,
12
+ action: AuditRecord["action"],
13
+ targetType: string,
14
+ targetId: string,
15
+ attributes: Parameters<typeof buildAuditRecord>[2],
16
+ ): AuditRecord {
17
+ return buildAuditRecord(
18
+ metadata,
19
+ { action, target_type: targetType, target_id: targetId },
20
+ attributes,
21
+ );
22
+ }
23
+
24
+ const siteFields = new Set([
25
+ "company_address",
26
+ "company_name",
27
+ "default_comments_enabled",
28
+ "default_map_theme_id",
29
+ "favicon_file_id",
30
+ "google_analytics_id",
31
+ "homepage_page_id",
32
+ "legal_email",
33
+ "loader_file_ids",
34
+ "logo_dark_file_id",
35
+ "logo_email_file_id",
36
+ "logo_light_file_id",
37
+ "menu_avatar_dropdown_id",
38
+ "menu_footer_id",
39
+ "menu_header_id",
40
+ "menu_secondary_id",
41
+ "meta_description",
42
+ "og_image_config",
43
+ "primary_color",
44
+ "privacy_email",
45
+ "privacy_og_background_file_id",
46
+ "site_og_background_file_id",
47
+ "site_title",
48
+ "social_links",
49
+ "support_email",
50
+ "tax_id",
51
+ "terms_og_background_file_id",
52
+ ]);
53
+ const lifecycle = new Set([
54
+ "draft:scheduled",
55
+ "scheduled:draft",
56
+ "draft:active",
57
+ "scheduled:active",
58
+ "active:archived",
59
+ ]);
60
+
61
+ function requireFields(
62
+ record: AuditRecord,
63
+ allowed: ReadonlySet<string>,
64
+ ): void {
65
+ const fields = record.changed_fields;
66
+ if (
67
+ !fields?.length ||
68
+ fields.some(
69
+ (value, i) => !allowed.has(value) || (i > 0 && fields[i - 1] >= value),
70
+ )
71
+ )
72
+ throw new TypeError("invalid or non-canonical changed_fields");
73
+ }
74
+ function requireNoExtra(record: AuditRecord, allowed: readonly string[]): void {
75
+ assertOnlyAuditAttributes(record, allowed);
76
+ }
77
+ function requirePolicyIdentity(record: AuditRecord): void {
78
+ const versionNumber = record.version_number;
79
+ if (
80
+ (record.policy_type !== "terms" && record.policy_type !== "privacy") ||
81
+ !Number.isSafeInteger(versionNumber) ||
82
+ versionNumber === undefined ||
83
+ versionNumber < 1
84
+ )
85
+ throw new TypeError("legal policy requires policy_type and version_number");
86
+ }
87
+
88
+ /** Returns true only for this module's exact catalog actions. */
89
+ export function validateSettingsAuditRecord(record: AuditRecord): boolean {
90
+ if (record.action === "site_settings.updated") {
91
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
92
+ if (record.target_id !== "1")
93
+ throw new TypeError("site_settings target must be 1");
94
+ requireFields(record, siteFields);
95
+ requireNoExtra(record, ["changed_fields"]);
96
+ return true;
97
+ }
98
+ if (
99
+ record.action === "map_theme.created" ||
100
+ record.action === "map_theme.deleted"
101
+ ) {
102
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
103
+ requireNoExtra(record, []);
104
+ return true;
105
+ }
106
+ if (record.action === "map_theme.updated") {
107
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
108
+ if (record.actor_kind !== "member")
109
+ throw new TypeError("map theme content mutation requires member actor");
110
+ requireFields(record, new Set(["content"]));
111
+ requireNoExtra(record, ["changed_fields"]);
112
+ return true;
113
+ }
114
+ if (
115
+ record.action === "legal_policy.created" ||
116
+ record.action === "legal_policy.deleted"
117
+ ) {
118
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
119
+ requirePolicyIdentity(record);
120
+ requireNoExtra(record, ["policy_type", "version_number"]);
121
+ return true;
122
+ }
123
+ if (record.action !== "legal_policy.updated") return false;
124
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
125
+ requirePolicyIdentity(record);
126
+ if (
127
+ record.changed_fields?.length === 1 &&
128
+ record.changed_fields[0] === "share_links"
129
+ ) {
130
+ if (record.actor_kind !== "member")
131
+ throw new TypeError("legal policy share links require member actor");
132
+ if (
133
+ !["created", "deleted"].includes(record.item_operation ?? "") ||
134
+ !record.item_id
135
+ )
136
+ throw new TypeError(
137
+ "legal policy share link requires item operation and id",
138
+ );
139
+ requireNoExtra(record, [
140
+ "changed_fields",
141
+ "policy_type",
142
+ "version_number",
143
+ "item_operation",
144
+ "item_id",
145
+ ]);
146
+ return true;
147
+ }
148
+ if (
149
+ record.actor_kind !== "member" &&
150
+ !(record.actor_kind === "system" && record.actor_service === "geul-backend")
151
+ )
152
+ throw new TypeError(
153
+ "legal policy lifecycle requires member or geul-backend system actor",
154
+ );
155
+ requireFields(record, new Set(["effective_at", "status"]));
156
+ if (
157
+ !record.previous_state ||
158
+ !record.new_state ||
159
+ !lifecycle.has(`${record.previous_state}:${record.new_state}`) ||
160
+ (record.changed_fields?.includes("effective_at") && !record.effective_at)
161
+ )
162
+ throw new TypeError("invalid legal policy lifecycle");
163
+ requireNoExtra(record, [
164
+ "changed_fields",
165
+ "policy_type",
166
+ "version_number",
167
+ "previous_state",
168
+ "new_state",
169
+ "effective_at",
170
+ ]);
171
+ return true;
172
+ }
173
+
174
+ export function buildSiteSettingsUpdatedAuditRecord(
175
+ metadata: AuditMetadata,
176
+ changedFields: readonly string[],
177
+ ): AuditRecord {
178
+ return buildSettingsAuditRecord(
179
+ metadata,
180
+ "site_settings.updated",
181
+ "site_settings",
182
+ "1",
183
+ {
184
+ changed_fields: canonicalAuditValues(changedFields),
185
+ },
186
+ );
187
+ }
188
+ export function buildMapThemeCreatedAuditRecord(
189
+ metadata: AuditMetadata,
190
+ themeId: string,
191
+ ): AuditRecord {
192
+ return buildSettingsAuditRecord(
193
+ metadata,
194
+ "map_theme.created",
195
+ "map_theme",
196
+ themeId,
197
+ {},
198
+ );
199
+ }
200
+ export function buildMapThemeDeletedAuditRecord(
201
+ metadata: AuditMetadata,
202
+ themeId: string,
203
+ ): AuditRecord {
204
+ return buildSettingsAuditRecord(
205
+ metadata,
206
+ "map_theme.deleted",
207
+ "map_theme",
208
+ themeId,
209
+ {},
210
+ );
211
+ }
212
+ export function buildMapThemeContentUpdatedAuditRecord(
213
+ metadata: AuditMetadata,
214
+ themeId: string,
215
+ ): AuditRecord {
216
+ return buildSettingsAuditRecord(
217
+ metadata,
218
+ "map_theme.updated",
219
+ "map_theme",
220
+ themeId,
221
+ {
222
+ changed_fields: ["content"],
223
+ },
224
+ );
225
+ }
226
+ export function buildLegalPolicyCreatedAuditRecord(
227
+ metadata: AuditMetadata,
228
+ id: string,
229
+ policyType: "terms" | "privacy",
230
+ versionNumber: number,
231
+ ): AuditRecord {
232
+ return buildSettingsAuditRecord(
233
+ metadata,
234
+ "legal_policy.created",
235
+ "legal_policy",
236
+ id,
237
+ {
238
+ policy_type: policyType,
239
+ version_number: versionNumber,
240
+ },
241
+ );
242
+ }
243
+ export function buildLegalPolicyDeletedAuditRecord(
244
+ metadata: AuditMetadata,
245
+ id: string,
246
+ policyType: "terms" | "privacy",
247
+ versionNumber: number,
248
+ ): AuditRecord {
249
+ return buildSettingsAuditRecord(
250
+ metadata,
251
+ "legal_policy.deleted",
252
+ "legal_policy",
253
+ id,
254
+ {
255
+ policy_type: policyType,
256
+ version_number: versionNumber,
257
+ },
258
+ );
259
+ }
260
+ export function buildLegalPolicyLifecycleAuditRecord(
261
+ metadata: AuditMetadata,
262
+ id: string,
263
+ policyType: "terms" | "privacy",
264
+ versionNumber: number,
265
+ changedFields: readonly ("effective_at" | "status")[],
266
+ previousState: AuditRecord["previous_state"],
267
+ newState: AuditRecord["new_state"],
268
+ effectiveAt?: string,
269
+ ): AuditRecord {
270
+ return buildSettingsAuditRecord(
271
+ metadata,
272
+ "legal_policy.updated",
273
+ "legal_policy",
274
+ id,
275
+ {
276
+ changed_fields: canonicalAuditValues(changedFields),
277
+ policy_type: policyType,
278
+ version_number: versionNumber,
279
+ previous_state: previousState,
280
+ new_state: newState,
281
+ effective_at: effectiveAt,
282
+ },
283
+ );
284
+ }
285
+ export function buildLegalPolicyShareLinkAuditRecord(
286
+ metadata: AuditMetadata,
287
+ id: string,
288
+ policyType: "terms" | "privacy",
289
+ versionNumber: number,
290
+ operation: "created" | "deleted",
291
+ itemId: string,
292
+ ): AuditRecord {
293
+ return buildSettingsAuditRecord(
294
+ metadata,
295
+ "legal_policy.updated",
296
+ "legal_policy",
297
+ id,
298
+ {
299
+ changed_fields: ["share_links"],
300
+ policy_type: policyType,
301
+ version_number: versionNumber,
302
+ item_operation: operation,
303
+ item_id: itemId,
304
+ },
305
+ );
306
+ }