@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,220 @@
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 {
8
+ AUDIT_ACTIONS,
9
+ AUDIT_CATALOG,
10
+ requireCatalogTarget,
11
+ } from "./catalog.ts";
12
+ import type { AuditMetadata } from "./types.ts";
13
+
14
+ type EmailAuthoringAction = Extract<
15
+ (typeof AUDIT_ACTIONS)[number],
16
+ | "email_template.created"
17
+ | "email_template.updated"
18
+ | "email_template.deleted"
19
+ | "email_layout.created"
20
+ | "email_layout.updated"
21
+ | "email_layout.deleted"
22
+ | "email_event_mapping.updated"
23
+ >;
24
+ export type EmailTemplateMetadataField =
25
+ "active" | "description" | "key" | "name";
26
+ export type EmailLayoutMetadataField = "key" | "name";
27
+
28
+ const actions = new Set<EmailAuthoringAction>([
29
+ "email_template.created",
30
+ "email_template.updated",
31
+ "email_template.deleted",
32
+ "email_layout.created",
33
+ "email_layout.updated",
34
+ "email_layout.deleted",
35
+ "email_event_mapping.updated",
36
+ ]);
37
+ const templateMetadataFields = new Set<EmailTemplateMetadataField>([
38
+ "active",
39
+ "description",
40
+ "key",
41
+ "name",
42
+ ]);
43
+ const layoutMetadataFields = new Set<EmailLayoutMetadataField>(["key", "name"]);
44
+ function buildEmailAuthoringAuditRecord(
45
+ metadata: AuditMetadata,
46
+ action: EmailAuthoringAction,
47
+ targetId: string,
48
+ attributes: Parameters<typeof buildAuditRecord>[2] = {},
49
+ ): AuditRecord {
50
+ return buildAuditRecord(
51
+ metadata,
52
+ { action, target_type: AUDIT_CATALOG[action], target_id: targetId },
53
+ attributes,
54
+ );
55
+ }
56
+ function requireNoExtra(record: AuditRecord, allowed: readonly string[]): void {
57
+ assertOnlyAuditAttributes(record, allowed);
58
+ }
59
+ function requireIdentifier(name: string, value: string | undefined): void {
60
+ if (!value || value.length > 255 || value.trim() !== value)
61
+ throw new TypeError(`${name} requires an identifier`);
62
+ }
63
+ function requireFields(
64
+ record: AuditRecord,
65
+ allowed: ReadonlySet<string>,
66
+ ): void {
67
+ const fields = record.changed_fields;
68
+ if (
69
+ !fields?.length ||
70
+ fields.some(
71
+ (field, i) => !allowed.has(field) || (i > 0 && fields[i - 1] >= field),
72
+ )
73
+ )
74
+ throw new TypeError("invalid or non-canonical changed_fields");
75
+ }
76
+ function requireRelation(record: AuditRecord, includeEventName = false): void {
77
+ const previous = record.previous_item_id ?? "";
78
+ const next = record.item_id ?? "";
79
+ if (previous === next) throw new TypeError("relation no-op");
80
+ if (previous !== "") requireIdentifier("previous_item_id", previous);
81
+ if (next !== "") requireIdentifier("item_id", next);
82
+ requireNoExtra(record, [
83
+ "changed_fields",
84
+ "previous_item_id",
85
+ "item_id",
86
+ ...(includeEventName ? ["event_name"] : []),
87
+ ]);
88
+ }
89
+ function isEmailEventKey(value: string | undefined): boolean {
90
+ return value !== undefined && /^[a-z0-9_]{1,64}$/.test(value);
91
+ }
92
+ function validateTemplateUpdate(record: AuditRecord): void {
93
+ if (
94
+ record.changed_fields?.length === 1 &&
95
+ record.changed_fields[0] === "layout"
96
+ )
97
+ return requireRelation(record);
98
+ requireMemberActor(record);
99
+ requireFields(record, templateMetadataFields);
100
+ requireNoExtra(record, ["changed_fields"]);
101
+ }
102
+ function validateLayoutUpdate(record: AuditRecord): void {
103
+ requireMemberActor(record);
104
+ requireFields(record, layoutMetadataFields);
105
+ requireNoExtra(record, ["changed_fields"]);
106
+ }
107
+ function validateMappingUpdate(record: AuditRecord): void {
108
+ requireMemberActor(record);
109
+ if (
110
+ record.changed_fields?.length !== 1 ||
111
+ record.changed_fields[0] !== "template" ||
112
+ !isEmailEventKey(record.event_name)
113
+ )
114
+ throw new TypeError("event mapping requires event_name and template");
115
+ requireRelation(record, true);
116
+ }
117
+ function requireMemberActor(record: AuditRecord): void {
118
+ if (record.actor_kind !== "member")
119
+ throw new TypeError("email authoring mutation requires member actor");
120
+ }
121
+
122
+ /** Returns true only for the seven reviewed Email Authoring audit actions. */
123
+ export function validateEmailAuthoringAuditRecord(
124
+ record: AuditRecord,
125
+ ): boolean {
126
+ if (!actions.has(record.action as EmailAuthoringAction)) return false;
127
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
128
+ if (!record.action.endsWith(".updated")) {
129
+ requireMemberActor(record);
130
+ requireNoExtra(record, []);
131
+ return true;
132
+ }
133
+ if (record.action === "email_template.updated")
134
+ validateTemplateUpdate(record);
135
+ else if (record.action === "email_layout.updated")
136
+ validateLayoutUpdate(record);
137
+ else validateMappingUpdate(record);
138
+ return true;
139
+ }
140
+ export function buildEmailTemplateCreatedAuditRecord(
141
+ metadata: AuditMetadata,
142
+ id: string,
143
+ ): AuditRecord {
144
+ return buildEmailAuthoringAuditRecord(metadata, "email_template.created", id);
145
+ }
146
+ export function buildEmailTemplateDeletedAuditRecord(
147
+ metadata: AuditMetadata,
148
+ id: string,
149
+ ): AuditRecord {
150
+ return buildEmailAuthoringAuditRecord(metadata, "email_template.deleted", id);
151
+ }
152
+ export function buildEmailTemplateMetadataAuditRecord(
153
+ metadata: AuditMetadata,
154
+ id: string,
155
+ fields: readonly EmailTemplateMetadataField[],
156
+ ): AuditRecord {
157
+ return buildEmailAuthoringAuditRecord(
158
+ metadata,
159
+ "email_template.updated",
160
+ id,
161
+ { changed_fields: canonicalAuditValues(fields) },
162
+ );
163
+ }
164
+ export function buildEmailTemplateLayoutRelationAuditRecord(
165
+ metadata: AuditMetadata,
166
+ id: string,
167
+ previousLayoutId: string,
168
+ layoutId: string,
169
+ ): AuditRecord {
170
+ return buildEmailAuthoringAuditRecord(
171
+ metadata,
172
+ "email_template.updated",
173
+ id,
174
+ {
175
+ changed_fields: ["layout"],
176
+ previous_item_id: previousLayoutId === "" ? undefined : previousLayoutId,
177
+ item_id: layoutId === "" ? undefined : layoutId,
178
+ },
179
+ );
180
+ }
181
+ export function buildEmailLayoutCreatedAuditRecord(
182
+ metadata: AuditMetadata,
183
+ id: string,
184
+ ): AuditRecord {
185
+ return buildEmailAuthoringAuditRecord(metadata, "email_layout.created", id);
186
+ }
187
+ export function buildEmailLayoutDeletedAuditRecord(
188
+ metadata: AuditMetadata,
189
+ id: string,
190
+ ): AuditRecord {
191
+ return buildEmailAuthoringAuditRecord(metadata, "email_layout.deleted", id);
192
+ }
193
+ export function buildEmailLayoutMetadataAuditRecord(
194
+ metadata: AuditMetadata,
195
+ id: string,
196
+ fields: readonly EmailLayoutMetadataField[],
197
+ ): AuditRecord {
198
+ return buildEmailAuthoringAuditRecord(metadata, "email_layout.updated", id, {
199
+ changed_fields: canonicalAuditValues(fields),
200
+ });
201
+ }
202
+ export function buildEmailEventMappingTemplateAuditRecord(
203
+ metadata: AuditMetadata,
204
+ eventName: string,
205
+ previousTemplateId: string,
206
+ templateId: string,
207
+ ): AuditRecord {
208
+ return buildEmailAuthoringAuditRecord(
209
+ metadata,
210
+ "email_event_mapping.updated",
211
+ eventName,
212
+ {
213
+ changed_fields: ["template"],
214
+ event_name: eventName,
215
+ previous_item_id:
216
+ previousTemplateId === "" ? undefined : previousTemplateId,
217
+ item_id: templateId === "" ? undefined : templateId,
218
+ },
219
+ );
220
+ }
@@ -0,0 +1,168 @@
1
+ import type { AuditRecord } from "../records.ts";
2
+ import { assertOnlyAuditAttributes } from "./attributes.ts";
3
+ import { buildAuditRecord } from "./builder.ts";
4
+ import { AUDIT_CATALOG, requireCatalogTarget } from "./catalog.ts";
5
+ import type { AuditMetadata } from "./types.ts";
6
+
7
+ type FileAction =
8
+ | "file.created"
9
+ | "file.updated"
10
+ | "file.deleted"
11
+ | "file_folder.created"
12
+ | "file_folder.updated"
13
+ | "file_folder.deleted";
14
+ const fileUpdateFields = new Set(["file_name", "folder_id"]);
15
+ const folderUpdateFields = new Set(["name", "parent_id"]);
16
+ function buildFileAuditRecord(
17
+ metadata: AuditMetadata,
18
+ action: FileAction,
19
+ targetId: string,
20
+ attributes: Parameters<typeof buildAuditRecord>[2] = {},
21
+ ): AuditRecord {
22
+ // The reviewed catalog, not string construction, owns this target mapping.
23
+ return buildAuditRecord(
24
+ metadata,
25
+ { action, target_type: AUDIT_CATALOG[action], target_id: targetId },
26
+ attributes,
27
+ );
28
+ }
29
+
30
+ function requireFields(
31
+ record: AuditRecord,
32
+ allowed: ReadonlySet<string>,
33
+ ): readonly string[] {
34
+ const fields = record.changed_fields;
35
+ if (
36
+ !fields?.length ||
37
+ fields.some(
38
+ (value, index) =>
39
+ !allowed.has(value) || (index > 0 && fields[index - 1] >= value),
40
+ )
41
+ )
42
+ throw new TypeError("invalid or non-canonical changed_fields");
43
+ return fields;
44
+ }
45
+ function requireNoExtra(record: AuditRecord, allowed: readonly string[]): void {
46
+ assertOnlyAuditAttributes(record, allowed);
47
+ }
48
+ function requireMove(
49
+ record: AuditRecord,
50
+ field: "folder_id" | "parent_id",
51
+ changedFields: readonly string[],
52
+ ): void {
53
+ const hasMove = changedFields.includes(field);
54
+ if (hasMove && record.previous_parent_id === record.new_parent_id)
55
+ throw new TypeError(`${field} requires a distinct parent transition`);
56
+ if (
57
+ !hasMove &&
58
+ (record.previous_parent_id !== undefined ||
59
+ record.new_parent_id !== undefined)
60
+ )
61
+ throw new TypeError(`parent IDs require changed_fields ${field}`);
62
+ }
63
+
64
+ /** Returns true only for the reviewed File and File Folder catalog actions. */
65
+ export function validateFileAuditRecord(record: AuditRecord): boolean {
66
+ if (
67
+ record.action !== "file.created" &&
68
+ record.action !== "file.updated" &&
69
+ record.action !== "file.deleted" &&
70
+ record.action !== "file_folder.created" &&
71
+ record.action !== "file_folder.updated" &&
72
+ record.action !== "file_folder.deleted"
73
+ )
74
+ return false;
75
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
76
+ if (
77
+ record.action === "file.created" ||
78
+ record.action === "file.deleted" ||
79
+ record.action === "file_folder.created" ||
80
+ record.action === "file_folder.deleted"
81
+ ) {
82
+ requireNoExtra(record, []);
83
+ return true;
84
+ }
85
+ if (record.action === "file_folder.updated") {
86
+ const changedFields = requireFields(record, folderUpdateFields);
87
+ requireMove(record, "parent_id", changedFields);
88
+ requireNoExtra(record, [
89
+ "changed_fields",
90
+ "previous_parent_id",
91
+ "new_parent_id",
92
+ ]);
93
+ return true;
94
+ }
95
+ const changedFields = requireFields(record, fileUpdateFields);
96
+ requireMove(record, "folder_id", changedFields);
97
+ requireNoExtra(record, [
98
+ "changed_fields",
99
+ "previous_parent_id",
100
+ "new_parent_id",
101
+ ]);
102
+ return true;
103
+ }
104
+
105
+ export function buildFileCreatedAuditRecord(
106
+ metadata: AuditMetadata,
107
+ fileId: string,
108
+ ): AuditRecord {
109
+ return buildFileAuditRecord(metadata, "file.created", fileId);
110
+ }
111
+ export function buildFileDeletedAuditRecord(
112
+ metadata: AuditMetadata,
113
+ fileId: string,
114
+ ): AuditRecord {
115
+ return buildFileAuditRecord(metadata, "file.deleted", fileId);
116
+ }
117
+ export function buildFileRenamedAuditRecord(
118
+ metadata: AuditMetadata,
119
+ fileId: string,
120
+ ): AuditRecord {
121
+ return buildFileAuditRecord(metadata, "file.updated", fileId, {
122
+ changed_fields: ["file_name"],
123
+ });
124
+ }
125
+ export function buildFileMovedAuditRecord(
126
+ metadata: AuditMetadata,
127
+ fileId: string,
128
+ previousParentId: string,
129
+ newParentId: string,
130
+ ): AuditRecord {
131
+ return buildFileAuditRecord(metadata, "file.updated", fileId, {
132
+ changed_fields: ["folder_id"],
133
+ previous_parent_id: previousParentId === "" ? undefined : previousParentId,
134
+ new_parent_id: newParentId === "" ? undefined : newParentId,
135
+ });
136
+ }
137
+ export function buildFileFolderCreatedAuditRecord(
138
+ metadata: AuditMetadata,
139
+ folderId: string,
140
+ ): AuditRecord {
141
+ return buildFileAuditRecord(metadata, "file_folder.created", folderId);
142
+ }
143
+ export function buildFileFolderDeletedAuditRecord(
144
+ metadata: AuditMetadata,
145
+ folderId: string,
146
+ ): AuditRecord {
147
+ return buildFileAuditRecord(metadata, "file_folder.deleted", folderId);
148
+ }
149
+ export function buildFileFolderRenamedAuditRecord(
150
+ metadata: AuditMetadata,
151
+ folderId: string,
152
+ ): AuditRecord {
153
+ return buildFileAuditRecord(metadata, "file_folder.updated", folderId, {
154
+ changed_fields: ["name"],
155
+ });
156
+ }
157
+ export function buildFileFolderMovedAuditRecord(
158
+ metadata: AuditMetadata,
159
+ folderId: string,
160
+ previousParentId: string,
161
+ newParentId: string,
162
+ ): AuditRecord {
163
+ return buildFileAuditRecord(metadata, "file_folder.updated", folderId, {
164
+ changed_fields: ["parent_id"],
165
+ previous_parent_id: previousParentId === "" ? undefined : previousParentId,
166
+ new_parent_id: newParentId === "" ? undefined : newParentId,
167
+ });
168
+ }
@@ -0,0 +1,229 @@
1
+ import type {
2
+ AuditCollectionOperation,
3
+ AuditItemOperation,
4
+ AuditItemScope,
5
+ AuditRecord,
6
+ AuditState,
7
+ } from "../records.ts";
8
+ import {
9
+ assertOnlyAuditAttributes,
10
+ canonicalAuditValues,
11
+ } from "./attributes.ts";
12
+ import { buildAuditRecord } from "./builder.ts";
13
+ import { AUDIT_CATALOG, requireCatalogTarget } from "./catalog.ts";
14
+ import type { AuditMetadata } from "./types.ts";
15
+
16
+ type FormSettingsField =
17
+ | "access_period"
18
+ | "auth_required"
19
+ | "direct_public"
20
+ | "duplicate_policy"
21
+ | "limit"
22
+ | "password"
23
+ | "required_role"
24
+ | "slug";
25
+ type FormLifecycleState = Extract<AuditState, "draft" | "published">;
26
+
27
+ const formSettingsFields = new Set<FormSettingsField>([
28
+ "access_period",
29
+ "auth_required",
30
+ "direct_public",
31
+ "duplicate_policy",
32
+ "limit",
33
+ "password",
34
+ "required_role",
35
+ "slug",
36
+ ]);
37
+ const formActions = new Set([
38
+ "form.created",
39
+ "form.updated",
40
+ "form.deleted",
41
+ "form_submission.created",
42
+ "form_submission.deleted",
43
+ ] as const);
44
+ function buildFormAuditRecord(
45
+ metadata: AuditMetadata,
46
+ action: typeof formActions extends Set<infer T> ? T : never,
47
+ targetId: string,
48
+ attributes: Parameters<typeof buildAuditRecord>[2] = {},
49
+ ): AuditRecord {
50
+ return buildAuditRecord(
51
+ metadata,
52
+ { action, target_type: AUDIT_CATALOG[action], target_id: targetId },
53
+ attributes,
54
+ );
55
+ }
56
+ function requireNoExtra(record: AuditRecord, allowed: readonly string[]): void {
57
+ assertOnlyAuditAttributes(record, allowed);
58
+ }
59
+ function requireIdentifier(name: string, value: string | undefined): void {
60
+ if (!value || value.length > 255 || value.trim() !== value)
61
+ throw new TypeError(`${name} requires an identifier`);
62
+ }
63
+ function requireFields(
64
+ record: AuditRecord,
65
+ allowed: ReadonlySet<string>,
66
+ ): void {
67
+ const fields = record.changed_fields;
68
+ if (
69
+ !fields?.length ||
70
+ fields.some(
71
+ (field, i) => !allowed.has(field) || (i > 0 && fields[i - 1] >= field),
72
+ )
73
+ )
74
+ throw new TypeError("invalid or non-canonical changed_fields");
75
+ }
76
+ function validateFormUpdate(record: AuditRecord): void {
77
+ if (record.changed_fields?.length !== 1) {
78
+ requireFields(record, formSettingsFields);
79
+ return requireNoExtra(record, ["changed_fields"]);
80
+ }
81
+ switch (record.changed_fields[0]) {
82
+ case "status":
83
+ if (
84
+ !(["draft", "published"] as const).includes(
85
+ record.previous_state as FormLifecycleState,
86
+ ) ||
87
+ !(["draft", "published"] as const).includes(
88
+ record.new_state as FormLifecycleState,
89
+ ) ||
90
+ record.previous_state === record.new_state
91
+ )
92
+ throw new TypeError(
93
+ "form status requires a distinct draft/published transition",
94
+ );
95
+ return requireNoExtra(record, [
96
+ "changed_fields",
97
+ "previous_state",
98
+ "new_state",
99
+ ]);
100
+ case "featured_image":
101
+ if (
102
+ (record.collection_operation !== "added" &&
103
+ record.collection_operation !== "removed") ||
104
+ !record.file_id
105
+ )
106
+ throw new TypeError(
107
+ "featured_image requires collection operation and file_id",
108
+ );
109
+ requireIdentifier("file_id", record.file_id);
110
+ return requireNoExtra(record, [
111
+ "changed_fields",
112
+ "collection_operation",
113
+ "file_id",
114
+ ]);
115
+ case "share_links":
116
+ if (
117
+ (record.item_operation !== "created" &&
118
+ record.item_operation !== "deleted") ||
119
+ (record.item_scope !== "form" && record.item_scope !== "dashboard")
120
+ )
121
+ throw new TypeError(
122
+ "form share_links requires form/dashboard scope and created/deleted operation",
123
+ );
124
+ requireIdentifier("item_id", record.item_id);
125
+ return requireNoExtra(record, [
126
+ "changed_fields",
127
+ "item_operation",
128
+ "item_scope",
129
+ "item_id",
130
+ ]);
131
+ default:
132
+ requireFields(record, formSettingsFields);
133
+ return requireNoExtra(record, ["changed_fields"]);
134
+ }
135
+ }
136
+
137
+ /** Returns true only for the five reviewed Form and Submission actions. */
138
+ export function validateFormAuditRecord(record: AuditRecord): boolean {
139
+ if (!formActions.has(record.action as never)) return false;
140
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
141
+ if (record.action === "form_submission.created") {
142
+ requireIdentifier("parent_id", record.parent_id);
143
+ requireNoExtra(record, ["parent_id"]);
144
+ } else if (record.action === "form.updated") validateFormUpdate(record);
145
+ else requireNoExtra(record, []);
146
+ return true;
147
+ }
148
+
149
+ export function buildFormCreatedAuditRecord(
150
+ metadata: AuditMetadata,
151
+ formId: string,
152
+ ): AuditRecord {
153
+ return buildFormAuditRecord(metadata, "form.created", formId);
154
+ }
155
+ export function buildFormDeletedAuditRecord(
156
+ metadata: AuditMetadata,
157
+ formId: string,
158
+ ): AuditRecord {
159
+ return buildFormAuditRecord(metadata, "form.deleted", formId);
160
+ }
161
+ export function buildFormSettingsAuditRecord(
162
+ metadata: AuditMetadata,
163
+ formId: string,
164
+ changedFields: readonly FormSettingsField[],
165
+ ): AuditRecord {
166
+ return buildFormAuditRecord(metadata, "form.updated", formId, {
167
+ changed_fields: canonicalAuditValues(changedFields),
168
+ });
169
+ }
170
+ export function buildFormLifecycleAuditRecord(
171
+ metadata: AuditMetadata,
172
+ formId: string,
173
+ previousState: FormLifecycleState,
174
+ newState: FormLifecycleState,
175
+ ): AuditRecord {
176
+ return buildFormAuditRecord(metadata, "form.updated", formId, {
177
+ changed_fields: ["status"],
178
+ previous_state: previousState,
179
+ new_state: newState,
180
+ });
181
+ }
182
+ export function buildFormFeaturedImageAuditRecord(
183
+ metadata: AuditMetadata,
184
+ formId: string,
185
+ fileId: string,
186
+ collectionOperation: AuditCollectionOperation,
187
+ ): AuditRecord {
188
+ return buildFormAuditRecord(metadata, "form.updated", formId, {
189
+ changed_fields: ["featured_image"],
190
+ file_id: fileId,
191
+ collection_operation: collectionOperation,
192
+ });
193
+ }
194
+ export function buildFormShareLinkAuditRecord(
195
+ metadata: AuditMetadata,
196
+ formId: string,
197
+ itemId: string,
198
+ itemScope: AuditItemScope,
199
+ itemOperation: Extract<AuditItemOperation, "created" | "deleted">,
200
+ ): AuditRecord {
201
+ return buildFormAuditRecord(metadata, "form.updated", formId, {
202
+ changed_fields: ["share_links"],
203
+ item_id: itemId,
204
+ item_scope: itemScope,
205
+ item_operation: itemOperation,
206
+ });
207
+ }
208
+ export function buildFormSubmissionCreatedAuditRecord(
209
+ metadata: AuditMetadata,
210
+ submissionId: string,
211
+ formId: string,
212
+ ): AuditRecord {
213
+ return buildFormAuditRecord(
214
+ metadata,
215
+ "form_submission.created",
216
+ submissionId,
217
+ { parent_id: formId },
218
+ );
219
+ }
220
+ export function buildFormSubmissionDeletedAuditRecord(
221
+ metadata: AuditMetadata,
222
+ submissionId: string,
223
+ ): AuditRecord {
224
+ return buildFormAuditRecord(
225
+ metadata,
226
+ "form_submission.deleted",
227
+ submissionId,
228
+ );
229
+ }