@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,313 @@
1
+ import { isRequestId } from "../request-context.ts";
2
+ import type { AuditRecord, AuditState } from "../records.ts";
3
+ import { assertOnlyAuditAttributes } from "./attributes.ts";
4
+ import { requireCatalogTarget } from "./catalog.ts";
5
+
6
+ function isIdentifier(value: string): boolean {
7
+ return value.length > 0 && value.length <= 255 && value.trim() === value;
8
+ }
9
+
10
+ function isEmail(value: string): boolean {
11
+ return (
12
+ value.length <= 320 &&
13
+ value.trim() === value &&
14
+ !/[\s]/.test(value) &&
15
+ value.split("@").length === 2
16
+ );
17
+ }
18
+
19
+ function isBoundedCode(value: string): boolean {
20
+ return /^[a-z0-9_]{1,64}$/.test(value);
21
+ }
22
+
23
+ function requireSortedUnique(
24
+ name: string,
25
+ values: readonly string[],
26
+ valid: (value: string) => boolean,
27
+ ): void {
28
+ for (let index = 0; index < values.length; index += 1) {
29
+ const value = values[index];
30
+ if (!valid(value)) throw new TypeError(`${name} contains an invalid value`);
31
+ if (index > 0 && values[index - 1] >= value)
32
+ throw new TypeError(`${name} must be sorted and unique`);
33
+ }
34
+ }
35
+
36
+ function requireOnly(record: AuditRecord, ...allowed: readonly string[]): void {
37
+ assertOnlyAuditAttributes(record, allowed);
38
+ }
39
+
40
+ function requireTransition(
41
+ record: AuditRecord,
42
+ field: string,
43
+ allowed: readonly (readonly [AuditState, AuditState])[],
44
+ ): void {
45
+ if (
46
+ record.previous_state === undefined ||
47
+ record.new_state === undefined ||
48
+ record.previous_state === record.new_state ||
49
+ !allowed.some(
50
+ ([previous, next]) =>
51
+ previous === record.previous_state && next === record.new_state,
52
+ )
53
+ )
54
+ throw new TypeError(
55
+ `audit action ${record.action} field ${field} rejects transition ${String(record.previous_state)} to ${String(record.new_state)}`,
56
+ );
57
+ requireOnly(record, "changed_fields", "previous_state", "new_state");
58
+ }
59
+
60
+ function validateMember(record: AuditRecord): void {
61
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
62
+ if (
63
+ record.changed_fields?.length === 2 &&
64
+ record.changed_fields[0] === "nickname" &&
65
+ record.changed_fields[1] === "onboarded"
66
+ ) {
67
+ if (
68
+ record.actor_kind !== "member" ||
69
+ record.actor_member_id !== record.target_id
70
+ )
71
+ throw new TypeError(
72
+ "member.updated onboarding requires the committed member as actor and target",
73
+ );
74
+ if (
75
+ record.nickname === undefined ||
76
+ record.nickname === "" ||
77
+ record.nickname.trim() !== record.nickname ||
78
+ [...record.nickname].length > 100
79
+ )
80
+ throw new TypeError(
81
+ "member.updated onboarding requires a trimmed nickname between 1 and 100 characters",
82
+ );
83
+ requireOnly(record, "changed_fields", "nickname");
84
+ return;
85
+ }
86
+ const fields = record.changed_fields;
87
+ if (fields === undefined || fields.length === 0)
88
+ throw new TypeError(
89
+ "member.updated requires a catalog changed_fields variant",
90
+ );
91
+ requireSortedUnique("changed_fields", fields, isBoundedCode);
92
+ const field = fields[0];
93
+ if (field === "role") {
94
+ if (
95
+ !isBoundedCode(record.previous_role ?? "") ||
96
+ !isBoundedCode(record.new_role ?? "") ||
97
+ record.previous_role === record.new_role
98
+ )
99
+ throw new TypeError(
100
+ "member.updated role requires distinct previous_role and new_role",
101
+ );
102
+ requireOnly(record, "changed_fields", "previous_role", "new_role");
103
+ return;
104
+ }
105
+ if (field === "avatar") {
106
+ if (
107
+ (record.collection_operation !== "added" &&
108
+ record.collection_operation !== "removed") ||
109
+ !isIdentifier(record.asset_id ?? "")
110
+ )
111
+ throw new TypeError(
112
+ "member avatar requires collection operation and asset_id",
113
+ );
114
+ requireOnly(record, "changed_fields", "collection_operation", "asset_id");
115
+ return;
116
+ }
117
+ if (field === "tags") {
118
+ if (record.tag_ids === undefined)
119
+ throw new TypeError("member tags requires tag_ids");
120
+ requireSortedUnique("tag_ids", record.tag_ids, isIdentifier);
121
+ requireOnly(record, "changed_fields", "tag_ids");
122
+ return;
123
+ }
124
+ const profile = new Set(["bio", "nickname", "social_links", "website"]);
125
+ if (fields.every((candidate) => profile.has(candidate))) {
126
+ if (fields.includes("nickname")) {
127
+ if (
128
+ record.nickname === undefined ||
129
+ record.nickname === "" ||
130
+ record.nickname.trim() !== record.nickname
131
+ )
132
+ throw new TypeError("member nickname requires a trimmed value");
133
+ } else if (record.nickname !== undefined)
134
+ throw new TypeError("member nickname requires changed_fields nickname");
135
+ requireOnly(record, "changed_fields", "nickname");
136
+ return;
137
+ }
138
+ const preference = new Set(["cookie_consent", "preferred_locale"]);
139
+ if (fields.every((candidate) => preference.has(candidate))) {
140
+ if (fields.includes("preferred_locale")) {
141
+ if (!isBoundedCode(record.preferred_locale ?? ""))
142
+ throw new TypeError(
143
+ "member preferred_locale requires a bounded locale",
144
+ );
145
+ } else if (record.preferred_locale !== undefined)
146
+ throw new TypeError(
147
+ "member preferred_locale requires changed_fields preferred_locale",
148
+ );
149
+ if (fields.includes("cookie_consent")) {
150
+ if (!isIdentifier(record.consent_id ?? ""))
151
+ throw new TypeError("member cookie_consent requires consent_id");
152
+ } else if (record.consent_id !== undefined)
153
+ throw new TypeError(
154
+ "member consent_id requires changed_fields cookie_consent",
155
+ );
156
+ requireOnly(record, "changed_fields", "preferred_locale", "consent_id");
157
+ return;
158
+ }
159
+ if (fields.length !== 1 || field !== "status")
160
+ throw new TypeError(
161
+ "member.updated requires a catalog changed_fields variant",
162
+ );
163
+ requireTransition(record, "status", [
164
+ ["active", "banned"],
165
+ ["banned", "active"],
166
+ ]);
167
+ }
168
+
169
+ function validateAccount(record: AuditRecord): void {
170
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
171
+ if (record.changed_fields?.length !== 1)
172
+ throw new TypeError("account.updated requires one catalog changed_field");
173
+ const field = record.changed_fields[0];
174
+ const requireCollection = (): void => {
175
+ if (
176
+ record.collection_operation !== "added" &&
177
+ record.collection_operation !== "removed"
178
+ )
179
+ throw new TypeError(
180
+ `audit action ${record.action} requires a catalog collection_operation`,
181
+ );
182
+ };
183
+ switch (field) {
184
+ case "canonical_email":
185
+ if (
186
+ !isEmail(record.previous_email ?? "") ||
187
+ !isEmail(record.new_email ?? "") ||
188
+ record.previous_email === record.new_email
189
+ )
190
+ throw new TypeError(
191
+ "account.updated canonical_email requires distinct previous_email and new_email",
192
+ );
193
+ requireOnly(record, "changed_fields", "previous_email", "new_email");
194
+ return;
195
+ case "login_emails":
196
+ if (!isEmail(record.email ?? ""))
197
+ throw new TypeError("account.updated login_emails requires email");
198
+ requireCollection();
199
+ requireOnly(record, "changed_fields", "collection_operation", "email");
200
+ return;
201
+ case "social_logins":
202
+ if (
203
+ !isBoundedCode(record.provider ?? "") ||
204
+ !isIdentifier(record.provider_subject ?? "")
205
+ )
206
+ throw new TypeError(
207
+ "account.updated social_logins requires provider and provider_subject",
208
+ );
209
+ requireCollection();
210
+ requireOnly(
211
+ record,
212
+ "changed_fields",
213
+ "collection_operation",
214
+ "provider",
215
+ "provider_subject",
216
+ );
217
+ return;
218
+ case "passkeys":
219
+ if (record.passkey_ids === undefined || record.passkey_ids.length === 0)
220
+ throw new TypeError("account.updated passkeys requires passkey_ids");
221
+ requireSortedUnique("passkey_ids", record.passkey_ids, isIdentifier);
222
+ requireCollection();
223
+ requireOnly(
224
+ record,
225
+ "changed_fields",
226
+ "collection_operation",
227
+ "passkey_ids",
228
+ );
229
+ return;
230
+ case "sessions":
231
+ if (record.collection_operation !== "removed")
232
+ throw new TypeError(
233
+ "account.updated sessions requires collection_operation removed",
234
+ );
235
+ if (
236
+ record.session_scope !== "current" &&
237
+ record.session_scope !== "one" &&
238
+ record.session_scope !== "others"
239
+ )
240
+ throw new TypeError(
241
+ "account.updated sessions requires a catalog session_scope",
242
+ );
243
+ if (record.session_ids === undefined)
244
+ throw new TypeError("account.updated sessions requires session_ids");
245
+ requireSortedUnique("session_ids", record.session_ids, isRequestId);
246
+ if (
247
+ ((record.session_scope === "current" ||
248
+ record.session_scope === "one") &&
249
+ record.session_ids.length !== 1) ||
250
+ (record.session_scope === "others" && record.session_ids.length === 0)
251
+ )
252
+ throw new TypeError(
253
+ `account.updated sessions scope ${record.session_scope} has invalid session_ids`,
254
+ );
255
+ requireOnly(
256
+ record,
257
+ "changed_fields",
258
+ "collection_operation",
259
+ "session_scope",
260
+ "session_ids",
261
+ );
262
+ return;
263
+ case "newsletter_subscription":
264
+ requireTransition(record, "newsletter_subscription", [
265
+ ["subscribed", "unsubscribed"],
266
+ ["unsubscribed", "subscribed"],
267
+ ]);
268
+ return;
269
+ case "deletion_state":
270
+ requireTransition(record, "deletion_state", [
271
+ ["none", "confirmation_pending"],
272
+ ["cancelled", "confirmation_pending"],
273
+ ["recovered", "confirmation_pending"],
274
+ ["confirmation_pending", "scheduled"],
275
+ ["none", "scheduled"],
276
+ ["cancelled", "scheduled"],
277
+ ["recovered", "scheduled"],
278
+ ["scheduled", "cancelled"],
279
+ ["recovery_confirmation_pending", "recovered"],
280
+ ]);
281
+ return;
282
+ default:
283
+ throw new TypeError(`account.updated rejects changed_field ${field}`);
284
+ }
285
+ }
286
+
287
+ export function validateMemberAccountAuditRecord(record: AuditRecord): boolean {
288
+ switch (record.action) {
289
+ case "member.updated":
290
+ validateMember(record);
291
+ return true;
292
+ case "member_tag.created":
293
+ case "member_tag.deleted":
294
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
295
+ if (
296
+ record.tag_name === undefined ||
297
+ record.tag_name === "" ||
298
+ record.tag_name.trim() !== record.tag_name
299
+ )
300
+ throw new TypeError("member tag requires trimmed tag_name");
301
+ requireOnly(record, "tag_name");
302
+ return true;
303
+ case "account.updated":
304
+ validateAccount(record);
305
+ return true;
306
+ case "account.deleted":
307
+ requireCatalogTarget(record.action, record.target_type, record.target_id);
308
+ requireOnly(record);
309
+ return true;
310
+ default:
311
+ return false;
312
+ }
313
+ }
@@ -0,0 +1,171 @@
1
+ import type { AuditRecord, AuditState } from "../records.ts";
2
+ import { canonicalAuditValues } from "./attributes.ts";
3
+ import { buildAuditRecord } from "./builder.ts";
4
+ import type { AuditMetadata } from "./types.ts";
5
+
6
+ function buildMemberAuditRecord(
7
+ metadata: AuditMetadata,
8
+ memberId: string,
9
+ attributes: Parameters<typeof buildAuditRecord>[2],
10
+ ): AuditRecord {
11
+ return buildAuditRecord(
12
+ metadata,
13
+ { action: "member.updated", target_type: "member", target_id: memberId },
14
+ attributes,
15
+ );
16
+ }
17
+
18
+ export function buildMemberOnboardingCompletedAuditRecord(
19
+ metadata: AuditMetadata,
20
+ memberId: string,
21
+ nickname: string,
22
+ ): AuditRecord {
23
+ return buildMemberAuditRecord(metadata, memberId, {
24
+ changed_fields: ["nickname", "onboarded"],
25
+ nickname,
26
+ });
27
+ }
28
+
29
+ export function buildMemberRoleUpdatedAuditRecord(
30
+ metadata: AuditMetadata,
31
+ memberId: string,
32
+ previousRole: string,
33
+ newRole: string,
34
+ ): AuditRecord {
35
+ return buildMemberAuditRecord(metadata, memberId, {
36
+ changed_fields: ["role"],
37
+ previous_role: previousRole,
38
+ new_role: newRole,
39
+ });
40
+ }
41
+
42
+ export function buildMemberProfileUpdatedAuditRecord(
43
+ metadata: AuditMetadata,
44
+ memberId: string,
45
+ fields: readonly string[],
46
+ nickname: string,
47
+ ): AuditRecord {
48
+ return buildMemberAuditRecord(metadata, memberId, {
49
+ changed_fields: canonicalAuditValues(fields),
50
+ ...(nickname === "" ? {} : { nickname }),
51
+ });
52
+ }
53
+
54
+ export function buildMemberAvatarUpdatedAuditRecord(
55
+ metadata: AuditMetadata,
56
+ memberId: string,
57
+ operation: "added" | "removed",
58
+ assetId: string,
59
+ ): AuditRecord {
60
+ return buildMemberAuditRecord(metadata, memberId, {
61
+ changed_fields: ["avatar"],
62
+ collection_operation: operation,
63
+ asset_id: assetId,
64
+ });
65
+ }
66
+
67
+ export function buildMemberPreferencesUpdatedAuditRecord(
68
+ metadata: AuditMetadata,
69
+ memberId: string,
70
+ fields: readonly string[],
71
+ preferredLocale: string,
72
+ consentId: string,
73
+ ): AuditRecord {
74
+ return buildMemberAuditRecord(metadata, memberId, {
75
+ changed_fields: canonicalAuditValues(fields),
76
+ ...(preferredLocale === "" ? {} : { preferred_locale: preferredLocale }),
77
+ ...(consentId === "" ? {} : { consent_id: consentId }),
78
+ });
79
+ }
80
+
81
+ export function buildMemberTagsUpdatedAuditRecord(
82
+ metadata: AuditMetadata,
83
+ memberId: string,
84
+ tagIds: readonly string[],
85
+ ): AuditRecord {
86
+ return buildMemberAuditRecord(metadata, memberId, {
87
+ changed_fields: ["tags"],
88
+ tag_ids: canonicalAuditValues(tagIds),
89
+ });
90
+ }
91
+
92
+ function buildMemberTagAuditRecord(
93
+ metadata: AuditMetadata,
94
+ action: "member_tag.created" | "member_tag.deleted",
95
+ tagId: string,
96
+ tagName: string,
97
+ ): AuditRecord {
98
+ return buildAuditRecord(
99
+ metadata,
100
+ {
101
+ action,
102
+ target_type: "member_tag",
103
+ target_id: tagId,
104
+ },
105
+ {
106
+ tag_name: tagName,
107
+ },
108
+ );
109
+ }
110
+
111
+ export function buildMemberTagCreatedAuditRecord(
112
+ metadata: AuditMetadata,
113
+ tagId: string,
114
+ tagName: string,
115
+ ): AuditRecord {
116
+ return buildMemberTagAuditRecord(
117
+ metadata,
118
+ "member_tag.created",
119
+ tagId,
120
+ tagName,
121
+ );
122
+ }
123
+
124
+ export function buildMemberTagDeletedAuditRecord(
125
+ metadata: AuditMetadata,
126
+ tagId: string,
127
+ tagName: string,
128
+ ): AuditRecord {
129
+ return buildMemberTagAuditRecord(
130
+ metadata,
131
+ "member_tag.deleted",
132
+ tagId,
133
+ tagName,
134
+ );
135
+ }
136
+
137
+ function buildMemberStatusUpdatedAuditRecord(
138
+ metadata: AuditMetadata,
139
+ memberId: string,
140
+ previousState: AuditState,
141
+ newState: AuditState,
142
+ ): AuditRecord {
143
+ return buildMemberAuditRecord(metadata, memberId, {
144
+ changed_fields: ["status"],
145
+ previous_state: previousState,
146
+ new_state: newState,
147
+ });
148
+ }
149
+
150
+ export function buildMemberBannedAuditRecord(
151
+ metadata: AuditMetadata,
152
+ memberId: string,
153
+ ): AuditRecord {
154
+ return buildMemberStatusUpdatedAuditRecord(
155
+ metadata,
156
+ memberId,
157
+ "active",
158
+ "banned",
159
+ );
160
+ }
161
+ export function buildMemberUnbannedAuditRecord(
162
+ metadata: AuditMetadata,
163
+ memberId: string,
164
+ ): AuditRecord {
165
+ return buildMemberStatusUpdatedAuditRecord(
166
+ metadata,
167
+ memberId,
168
+ "banned",
169
+ "active",
170
+ );
171
+ }