@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
package/src/records.ts ADDED
@@ -0,0 +1,1244 @@
1
+ import { parseServiceName, type RecordActor } from "./actor.ts";
2
+ import type { CollaborationCheckpointEntityType } from "./system.ts";
3
+ import { parseJobFailureReason, parseJobKind } from "./job.ts";
4
+ import { isCanonicalSourceIp, isRequestId } from "./request-context.ts";
5
+ import { classifyHTTPResult } from "./request-result.ts";
6
+ import {
7
+ AUDIT_ACTIONS as auditActions,
8
+ type AuditAction,
9
+ } from "./audit/catalog.ts";
10
+ import {
11
+ auditActionAllowsAnonymous,
12
+ systemActorMayAppendAudit,
13
+ } from "./audit/actor-policy.ts";
14
+ import { validateFileAuditRecord } from "./audit/file.ts";
15
+ import { validateArtistLabelAuditRecord } from "./audit/artist-label.ts";
16
+ import { validateIntegrationAuditRecord } from "./audit/integrations.ts";
17
+ import { validateReferenceEntityAuditRecord } from "./audit/reference-entities.ts";
18
+ import { validateReferenceDataAuditRecord } from "./audit/reference-data.ts";
19
+ import { validateSeriesTypeAuditRecord } from "./audit/series-type.ts";
20
+ import { validateProgramAuditRecord } from "./audit/program.ts";
21
+ import { validateReleaseCampaignAuditRecord } from "./audit/release-campaign.ts";
22
+ import { validateEmailAuthoringAuditRecord } from "./audit/email-authoring.ts";
23
+ import { validateSettingsAuditRecord } from "./audit/settings.ts";
24
+ import { validateFormAuditRecord } from "./audit/form.ts";
25
+ import { validateContentAuditRecord } from "./audit/content.ts";
26
+ import { validateMemberAccountAuditRecord } from "./audit/member-account.ts";
27
+ import { validateSourceLocaleAuditRecord } from "./audit/source-locale.ts";
28
+ import { validateLocaleContentAuditRecord } from "./audit/locale-content.ts";
29
+ import { assertKnownAuditRecordKeys } from "./audit/attributes.ts";
30
+
31
+ export interface Correlation {
32
+ readonly request_id?: string;
33
+ readonly trace_id?: string;
34
+ readonly span_id?: string;
35
+ }
36
+
37
+ export type RequestOutcome = "succeeded" | "blocked" | "failed";
38
+
39
+ export type RequestRecord = Correlation &
40
+ RecordActor & {
41
+ readonly event: "request.completed";
42
+ readonly occurred_at: string;
43
+ readonly http_method?: string;
44
+ readonly http_route?: string;
45
+ readonly rpc_service?: string;
46
+ readonly rpc_method?: string;
47
+ readonly status_code: number;
48
+ readonly duration_ms: number;
49
+ readonly outcome: RequestOutcome;
50
+ readonly error_code?: string;
51
+ readonly reason?: string;
52
+ };
53
+
54
+ export const SYSTEM_EVENTS = [
55
+ "service.ready",
56
+ "service.degraded",
57
+ "service.stopping",
58
+ "service.failed",
59
+ "dependency.degraded",
60
+ "dependency.recovered",
61
+ "queue.publish.succeeded",
62
+ "queue.publish.failed",
63
+ "queue.delivery.succeeded",
64
+ "queue.delivery.failed",
65
+ "queue.delivery.requeued",
66
+ "queue.retry.accepted",
67
+ "queue.retry.failed",
68
+ "queue.dlq.accepted",
69
+ "queue.dlq.failed",
70
+ "job.started",
71
+ "job.succeeded",
72
+ "job.failed",
73
+ "audit.append.failed",
74
+ "collaboration.checkpoint.failed",
75
+ "client.render.failed",
76
+ "telemetry.pipeline.degraded",
77
+ "telemetry.pipeline.recovered",
78
+ "translation.job.terminal",
79
+ ] as const;
80
+
81
+ export const SYSTEM_EVENT_OUTCOMES: Partial<Record<SystemEvent, string>> = {
82
+ "service.ready": "ready",
83
+ "service.degraded": "degraded",
84
+ "service.stopping": "stopping",
85
+ "service.failed": "failed",
86
+ "dependency.degraded": "degraded",
87
+ "dependency.recovered": "recovered",
88
+ "queue.publish.succeeded": "succeeded",
89
+ "queue.publish.failed": "failed",
90
+ "queue.delivery.succeeded": "succeeded",
91
+ "queue.delivery.failed": "failed",
92
+ "queue.delivery.requeued": "requeued",
93
+ "queue.retry.accepted": "accepted",
94
+ "queue.retry.failed": "failed",
95
+ "queue.dlq.accepted": "accepted",
96
+ "queue.dlq.failed": "failed",
97
+ "job.started": "started",
98
+ "job.succeeded": "succeeded",
99
+ "job.failed": "failed",
100
+ "audit.append.failed": "failed",
101
+ "collaboration.checkpoint.failed": "failed",
102
+ "client.render.failed": "failed",
103
+ "telemetry.pipeline.degraded": "degraded",
104
+ "telemetry.pipeline.recovered": "recovered",
105
+ };
106
+
107
+ export type SystemEvent = (typeof SYSTEM_EVENTS)[number];
108
+
109
+ export const TRANSLATION_JOB_TERMINAL_OUTCOMES = [
110
+ "applied",
111
+ "failed",
112
+ "cancelled",
113
+ ] as const;
114
+ export type TranslationJobTerminalOutcome =
115
+ (typeof TRANSLATION_JOB_TERMINAL_OUTCOMES)[number];
116
+
117
+ export const TRANSLATION_ENTITY_TYPES = [
118
+ "post",
119
+ "page",
120
+ "work",
121
+ "program_event",
122
+ "release",
123
+ "artist",
124
+ "label",
125
+ "menu",
126
+ "email_template",
127
+ "email_layout",
128
+ "campaign",
129
+ "form",
130
+ "privacy",
131
+ "terms",
132
+ "series",
133
+ ] as const;
134
+ export type TranslationEntityType = (typeof TRANSLATION_ENTITY_TYPES)[number];
135
+
136
+ export const TRANSLATION_FAILURE_REASONS = [
137
+ "provider_configuration",
138
+ "provider_authentication",
139
+ "provider_rate_limited",
140
+ "provider_unavailable",
141
+ "provider_rejected",
142
+ "provider_response_invalid",
143
+ "target_apply_failed",
144
+ "og_handoff_failed",
145
+ "internal",
146
+ ] as const;
147
+ export type TranslationFailureReason =
148
+ (typeof TRANSLATION_FAILURE_REASONS)[number];
149
+
150
+ export const COLLABORATION_CHECKPOINT_ENTITY_TYPES = [
151
+ "post",
152
+ "page",
153
+ "work",
154
+ "release",
155
+ "label",
156
+ "artist",
157
+ "campaign",
158
+ "program_event",
159
+ "map_theme",
160
+ "terms_history",
161
+ "privacy_history",
162
+ "form",
163
+ "email_layout",
164
+ ] as const satisfies readonly CollaborationCheckpointEntityType[];
165
+
166
+ const COLLABORATION_CONFLICT_REASONS = [
167
+ "locale_ownership_changed",
168
+ "shared_structure_changed",
169
+ "shared_materialization_changed",
170
+ "document_revision_changed",
171
+ "target_revision_changed",
172
+ ];
173
+
174
+ export interface SystemRecord extends Correlation {
175
+ readonly event: SystemEvent;
176
+ readonly occurred_at: string;
177
+ readonly component?: string;
178
+ readonly dependency?: string;
179
+ readonly operation?: string;
180
+ readonly domain?: string;
181
+ readonly queue?: string;
182
+ readonly message_id?: string;
183
+ readonly command_id?: string;
184
+ readonly retry_count?: number;
185
+ readonly duration_ms?: number;
186
+ readonly job_kind?: string;
187
+ readonly job_id?: string;
188
+ readonly entity_type?: string;
189
+ readonly entity_id?: string;
190
+ readonly target_locale?: string;
191
+ readonly record_class?: AuditRecordClass;
192
+ readonly action?: string;
193
+ readonly outcome?: string;
194
+ readonly error_code?: string;
195
+ readonly reason?: string;
196
+ readonly error_classification?: string;
197
+ }
198
+
199
+ export const AUDIT_ACTIONS = auditActions;
200
+ export type { AuditAction } from "./audit/catalog.ts";
201
+
202
+ export type AuditRecordClass = "domain_audit" | "security_access";
203
+ export type AccountSessionScope = "current" | "one" | "others";
204
+ export type AuditCollectionOperation = "added" | "removed";
205
+ export type AuditItemOperation = "created" | "updated" | "deleted";
206
+ export type AuditItemScope = "form" | "dashboard";
207
+ export type AuditAssetSlot = "light" | "dark";
208
+ export type AuditRelationship =
209
+ "none" | "author" | "collaborator" | "owner" | "manager";
210
+ export type AuditState =
211
+ | "none"
212
+ | "active"
213
+ | "banned"
214
+ | "confirmation_pending"
215
+ | "scheduled"
216
+ | "recovery_confirmation_pending"
217
+ | "cancelled"
218
+ | "recovered"
219
+ | "released"
220
+ | "disabled"
221
+ | "public"
222
+ | "authenticated"
223
+ | "restricted"
224
+ | "subscribed"
225
+ | "unsubscribed"
226
+ | "draft"
227
+ | "published"
228
+ | "archived"
229
+ | "sending"
230
+ | "sent"
231
+ | "failed";
232
+ export type AuditAppendFailureReason =
233
+ | "record_invalid"
234
+ | "transaction_missing"
235
+ | "persistence_failed"
236
+ | "database_missing"
237
+ | "request_context_missing"
238
+ | "actor_invalid"
239
+ | "record_build_failed";
240
+
241
+ export interface AuditRecord extends Correlation {
242
+ readonly audit_id: string;
243
+ readonly occurred_at: string;
244
+ readonly action: AuditAction;
245
+ readonly target_type: string;
246
+ readonly target_id: string;
247
+ readonly nickname?: string;
248
+ readonly asset_id?: string;
249
+ readonly consent_id?: string;
250
+ readonly changed_fields?: readonly string[];
251
+ readonly collection_operation?: AuditCollectionOperation;
252
+ readonly asset_slot?: AuditAssetSlot;
253
+ readonly previous_state?: AuditState;
254
+ readonly new_state?: AuditState;
255
+ readonly scheduled_at?: string;
256
+ readonly scheduled_time_zone?: string;
257
+ readonly version_id?: string;
258
+ readonly contributor_member_ids?: readonly string[];
259
+ readonly previous_role?: string;
260
+ readonly new_role?: string;
261
+ readonly email?: string;
262
+ readonly previous_email?: string;
263
+ readonly new_email?: string;
264
+ readonly provider?: string;
265
+ readonly provider_subject?: string;
266
+ readonly passkey_ids?: readonly string[];
267
+ readonly session_scope?: AccountSessionScope;
268
+ readonly session_ids?: readonly string[];
269
+ readonly preferred_locale?: string;
270
+ readonly locale?: string;
271
+ readonly previous_locale?: string;
272
+ readonly new_locale?: string;
273
+ /** Present empty arrays intentionally represent an empty member-tag set. */
274
+ readonly tag_ids?: readonly string[];
275
+ readonly tag_name?: string;
276
+ readonly event_name?: string;
277
+ readonly policy_type?: "terms" | "privacy";
278
+ readonly version_number?: number;
279
+ readonly effective_at?: string;
280
+ readonly item_operation?: AuditItemOperation;
281
+ readonly item_id?: string;
282
+ readonly item_scope?: AuditItemScope;
283
+ /** Parent Form for a submission; submission payload is never audited. */
284
+ readonly parent_id?: string;
285
+ /** Empty denotes a deliberately cleared relation. */
286
+ readonly previous_item_id?: string;
287
+ readonly file_id?: string;
288
+ /** Present arrays preserve an Artist gallery's ordered file binding. */
289
+ readonly file_ids?: readonly string[];
290
+ readonly subject_member_id?: string;
291
+ readonly subject_post_id?: string;
292
+ readonly previous_relationship?: AuditRelationship;
293
+ readonly new_relationship?: AuditRelationship;
294
+ readonly previous_series_id?: string;
295
+ readonly new_series_id?: string;
296
+ /** Present arrays preserve the exact persisted Post Series order, including empty. */
297
+ readonly post_ids?: readonly string[];
298
+ /** File/folder move boundary; empty string denotes the root folder. */
299
+ readonly previous_parent_id?: string;
300
+ /** File/folder move boundary; empty string denotes the root folder. */
301
+ readonly new_parent_id?: string;
302
+ /** Deliberately present empty arrays represent an empty previous audience. */
303
+ readonly previous_item_ids?: readonly string[];
304
+ /** Deliberately present empty arrays represent an empty new audience. */
305
+ readonly item_ids?: readonly string[];
306
+ readonly actor_kind: "anonymous" | "member" | "system";
307
+ readonly actor_member_id?: string;
308
+ readonly actor_service?: string;
309
+ }
310
+
311
+ export type SecurityAction =
312
+ | "authentication.succeeded"
313
+ | "authentication.failed"
314
+ | "authentication.blocked"
315
+ | "authorization.denied"
316
+ | "personal_data.accessed";
317
+ export type AuthenticationFlowKind =
318
+ "login" | "registration" | "reauthentication";
319
+ export type AuthenticationMethod = "email_code" | "oidc" | "passkey";
320
+ export type AuthenticationPrincipalState = "onboarding_only" | "active";
321
+ export type AuthenticationFailureReason =
322
+ | "proof_rejected"
323
+ | "account_blocked"
324
+ | "provider_denied"
325
+ | "provider_failed"
326
+ | "member_link_invalid"
327
+ | "internal_error";
328
+ export type AuthenticationBlockReason =
329
+ | "flow_invalid"
330
+ | "request_invalid"
331
+ | "integrity_check_failed"
332
+ | "rate_limited"
333
+ | "service_unavailable";
334
+ export type AuthorizationDenialReason =
335
+ "authentication_required" | "permission_denied";
336
+ export type PersonalDataAccessKind = "read";
337
+
338
+ export interface SecurityAccessRecord extends Correlation {
339
+ readonly access_id: string;
340
+ readonly occurred_at: string;
341
+ readonly action: SecurityAction;
342
+ readonly actor_kind: "anonymous" | "member" | "system";
343
+ readonly actor_member_id?: string;
344
+ readonly actor_service?: string;
345
+ readonly source_ip: string;
346
+ readonly flow_kind?: AuthenticationFlowKind;
347
+ readonly authentication_method?: AuthenticationMethod;
348
+ readonly principal_state?: AuthenticationPrincipalState;
349
+ readonly provider?: string;
350
+ readonly reason?: string;
351
+ readonly attempted_action?: string;
352
+ readonly permission?: string;
353
+ readonly subject_type?: string;
354
+ readonly subject_id?: string;
355
+ readonly access_kind?: PersonalDataAccessKind;
356
+ readonly data_category?: string;
357
+ }
358
+
359
+ const authenticationFailureReasons: readonly AuthenticationFailureReason[] = [
360
+ "proof_rejected",
361
+ "account_blocked",
362
+ "provider_denied",
363
+ "provider_failed",
364
+ "member_link_invalid",
365
+ "internal_error",
366
+ ];
367
+ const authenticationBlockReasons: readonly AuthenticationBlockReason[] = [
368
+ "flow_invalid",
369
+ "request_invalid",
370
+ "integrity_check_failed",
371
+ "rate_limited",
372
+ "service_unavailable",
373
+ ];
374
+ const authorizationDenialReasons: readonly AuthorizationDenialReason[] = [
375
+ "authentication_required",
376
+ "permission_denied",
377
+ ];
378
+
379
+ export interface TelemetrySink {
380
+ write(record: RequestRecord | SystemRecord): void | Promise<void>;
381
+ }
382
+
383
+ export function validateRequestRecord(record: RequestRecord): void {
384
+ validateOccurredAt(record.occurred_at);
385
+ if (record.request_id === undefined || !isRequestId(record.request_id)) {
386
+ throw new TypeError(
387
+ "request record requires a canonical UUIDv4 request_id",
388
+ );
389
+ }
390
+ validateTraceCorrelation(record);
391
+ validateRecordActor(record);
392
+ const hasHttp =
393
+ record.http_method !== undefined &&
394
+ record.http_method !== "" &&
395
+ record.http_route !== undefined &&
396
+ record.http_route !== "" &&
397
+ record.rpc_service === undefined &&
398
+ record.rpc_method === undefined;
399
+ const hasRpc =
400
+ record.rpc_service !== undefined &&
401
+ record.rpc_service !== "" &&
402
+ record.rpc_method !== undefined &&
403
+ record.rpc_method !== "" &&
404
+ record.http_route === undefined;
405
+ if (!hasHttp && !hasRpc) {
406
+ throw new TypeError(
407
+ "request record requires exactly one HTTP route or RPC service and method",
408
+ );
409
+ }
410
+ if (
411
+ !Number.isInteger(record.status_code) ||
412
+ record.status_code < 100 ||
413
+ record.status_code > 599
414
+ ) {
415
+ throw new TypeError("status_code must be an HTTP status code");
416
+ }
417
+ if (!Number.isInteger(record.duration_ms) || record.duration_ms < 0) {
418
+ throw new TypeError("duration_ms must be a non-negative integer");
419
+ }
420
+ if (!["succeeded", "blocked", "failed"].includes(record.outcome)) {
421
+ throw new TypeError(`invalid request outcome: ${record.outcome}`);
422
+ }
423
+ if (
424
+ record.outcome === "succeeded" &&
425
+ (record.error_code !== undefined || record.reason !== undefined)
426
+ ) {
427
+ throw new TypeError(
428
+ "successful request cannot contain error_code or reason",
429
+ );
430
+ }
431
+ if (
432
+ record.outcome !== "succeeded" &&
433
+ record.error_code === undefined &&
434
+ record.reason === undefined
435
+ ) {
436
+ throw new TypeError(
437
+ "blocked or failed request requires error_code or reason",
438
+ );
439
+ }
440
+ if (hasHttp) {
441
+ const expected = classifyHTTPResult(record.status_code, record.duration_ms);
442
+ if (
443
+ record.outcome !== expected.outcome ||
444
+ record.reason !== expected.reason
445
+ ) {
446
+ throw new TypeError(
447
+ "HTTP request outcome and reason must match the shared status classifier",
448
+ );
449
+ }
450
+ }
451
+ }
452
+
453
+ export function validateSystemRecord(record: SystemRecord): void {
454
+ validateOccurredAt(record.occurred_at);
455
+ if (!(SYSTEM_EVENTS as readonly string[]).includes(record.event)) {
456
+ throw new TypeError(`unknown system event: ${record.event}`);
457
+ }
458
+ if (record.request_id !== undefined && !isRequestId(record.request_id)) {
459
+ throw new TypeError("request_id must be a canonical UUIDv4");
460
+ }
461
+ validateTraceCorrelation(record);
462
+ if (record.event === "translation.job.terminal") {
463
+ if (
464
+ !TRANSLATION_JOB_TERMINAL_OUTCOMES.includes(
465
+ record.outcome as TranslationJobTerminalOutcome,
466
+ )
467
+ ) {
468
+ throw new TypeError(
469
+ "translation.job.terminal requires a catalog outcome",
470
+ );
471
+ }
472
+ } else if (record.outcome !== SYSTEM_EVENT_OUTCOMES[record.event]) {
473
+ throw new TypeError(
474
+ `system event ${record.event} requires outcome ${SYSTEM_EVENT_OUTCOMES[record.event]}`,
475
+ );
476
+ }
477
+ if (
478
+ record.event !== "translation.job.terminal" &&
479
+ record.error_classification !== undefined
480
+ ) {
481
+ throw new TypeError(
482
+ `system event ${record.event} does not allow error_classification`,
483
+ );
484
+ }
485
+ for (const [name, value] of Object.entries({
486
+ component: record.component,
487
+ dependency: record.dependency,
488
+ operation: record.operation,
489
+ domain: record.domain,
490
+ job_kind: record.job_kind,
491
+ entity_type: record.entity_type,
492
+ record_class: record.record_class,
493
+ outcome: record.outcome,
494
+ error_code: record.error_code,
495
+ reason: record.reason,
496
+ error_classification: record.error_classification,
497
+ })) {
498
+ if (value !== undefined && !isBoundedCode(value)) {
499
+ throw new TypeError(`system field ${name} must be a bounded code`);
500
+ }
501
+ }
502
+ if (
503
+ record.entity_id !== undefined &&
504
+ (record.entity_id.length === 0 || record.entity_id.length > 128)
505
+ ) {
506
+ throw new TypeError("entity_id must be a non-empty bounded identifier");
507
+ }
508
+ if (
509
+ record.target_locale !== undefined &&
510
+ !isBoundedLocaleCode(record.target_locale)
511
+ ) {
512
+ throw new TypeError("target_locale must be a bounded locale code");
513
+ }
514
+ for (const [name, value] of [
515
+ ["retry_count", record.retry_count],
516
+ ["duration_ms", record.duration_ms],
517
+ ] as const) {
518
+ if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
519
+ throw new TypeError(`${name} must be a non-negative integer`);
520
+ }
521
+ }
522
+ validateSystemRequiredFields(record);
523
+ }
524
+
525
+ export function validateAuditRecord(record: AuditRecord): void {
526
+ assertKnownAuditRecordKeys(record);
527
+ validateOccurredAt(record.occurred_at);
528
+ if (!isRequestId(record.audit_id)) {
529
+ throw new TypeError("audit_id must be a canonical UUIDv4");
530
+ }
531
+ if (record.request_id !== undefined && !isRequestId(record.request_id)) {
532
+ throw new TypeError("request_id must be a canonical UUIDv4");
533
+ }
534
+ validateTraceCorrelation(record);
535
+ validateRecordActor(record as RecordActor);
536
+ if (
537
+ (record as RecordActor).actor_kind === "anonymous" &&
538
+ !auditActionAllowsAnonymous(record.action)
539
+ ) {
540
+ throw new TypeError("domain audit actor cannot be anonymous");
541
+ }
542
+ if (record.actor_kind === "system" && !systemActorMayAppendAudit(record)) {
543
+ throw new TypeError(
544
+ `audit action ${record.action} cannot use system actor`,
545
+ );
546
+ }
547
+ if (validateLocaleContentAuditRecord(record)) return;
548
+ if (validateSourceLocaleAuditRecord(record)) return;
549
+ if (validateFileAuditRecord(record)) return;
550
+ if (validateArtistLabelAuditRecord(record)) return;
551
+ if (validateSettingsAuditRecord(record)) return;
552
+ if (validateIntegrationAuditRecord(record)) return;
553
+ if (validateReferenceDataAuditRecord(record)) return;
554
+ if (validateReferenceEntityAuditRecord(record)) return;
555
+ if (validateSeriesTypeAuditRecord(record)) return;
556
+ if (validateProgramAuditRecord(record)) return;
557
+ if (validateReleaseCampaignAuditRecord(record)) return;
558
+ if (validateEmailAuthoringAuditRecord(record)) return;
559
+ if (validateFormAuditRecord(record)) return;
560
+ if (validateContentAuditRecord(record)) return;
561
+ if (validateMemberAccountAuditRecord(record)) return;
562
+ throw new TypeError(`unsupported audit action ${String(record.action)}`);
563
+ }
564
+
565
+ export function validateSecurityAccessRecord(
566
+ record: SecurityAccessRecord,
567
+ ): void {
568
+ validateOccurredAt(record.occurred_at);
569
+ if (
570
+ !isRequestId(record.access_id) ||
571
+ record.request_id === undefined ||
572
+ !isRequestId(record.request_id)
573
+ ) {
574
+ throw new TypeError(
575
+ "security access record requires canonical UUIDv4 access_id and request_id",
576
+ );
577
+ }
578
+ validateTraceCorrelation(record);
579
+ validateRecordActor(record as RecordActor);
580
+ if (!isCanonicalSourceIp(record.source_ip)) {
581
+ throw new TypeError("source_ip must be a canonical IPv4 or IPv6 address");
582
+ }
583
+ switch (record.action) {
584
+ case "authentication.succeeded": {
585
+ requireOnlyAuthenticationAttributes(record);
586
+ if (record.actor_kind !== "member") {
587
+ throw new TypeError(
588
+ "successful authentication requires a member actor",
589
+ );
590
+ }
591
+ if (
592
+ !isAuthenticationFlow(record.flow_kind) ||
593
+ !isAuthenticationMethod(record.authentication_method)
594
+ ) {
595
+ throw new TypeError(
596
+ "successful authentication requires flow_kind and authentication_method",
597
+ );
598
+ }
599
+ if (!isAuthenticationPrincipalState(record.principal_state)) {
600
+ throw new TypeError(
601
+ "successful authentication requires principal_state",
602
+ );
603
+ }
604
+ if (record.reason !== undefined) {
605
+ throw new TypeError("successful authentication cannot contain reason");
606
+ }
607
+ validateAuthenticationProvider(record);
608
+ return;
609
+ }
610
+ case "authentication.failed": {
611
+ requireOnlyAuthenticationAttributes(record);
612
+ if (record.principal_state !== undefined) {
613
+ throw new TypeError(
614
+ "authentication failure cannot contain principal_state",
615
+ );
616
+ }
617
+ if (record.actor_kind === "system") {
618
+ throw new TypeError("authentication failure cannot use a system actor");
619
+ }
620
+ if (
621
+ !isAuthenticationFlow(record.flow_kind) ||
622
+ !isAuthenticationMethod(record.authentication_method)
623
+ ) {
624
+ throw new TypeError(
625
+ "authentication failure requires flow_kind and authentication_method",
626
+ );
627
+ }
628
+ if (
629
+ record.actor_kind === "member" &&
630
+ record.flow_kind !== "reauthentication"
631
+ ) {
632
+ throw new TypeError(
633
+ "only reauthentication failure or block can use a member actor",
634
+ );
635
+ }
636
+ if (
637
+ !authenticationFailureReasons.includes(
638
+ record.reason as AuthenticationFailureReason,
639
+ )
640
+ ) {
641
+ throw new TypeError(
642
+ `invalid authentication failure reason: ${record.reason}`,
643
+ );
644
+ }
645
+ validateAuthenticationProvider(record);
646
+ return;
647
+ }
648
+ case "authentication.blocked": {
649
+ requireOnlyAuthenticationAttributes(record);
650
+ if (record.principal_state !== undefined) {
651
+ throw new TypeError(
652
+ "authentication block cannot contain principal_state",
653
+ );
654
+ }
655
+ if (record.actor_kind === "system") {
656
+ throw new TypeError("authentication block cannot use a system actor");
657
+ }
658
+ if (
659
+ record.flow_kind !== undefined &&
660
+ !isAuthenticationFlow(record.flow_kind)
661
+ ) {
662
+ throw new TypeError(`invalid authentication flow_kind`);
663
+ }
664
+ if (
665
+ record.authentication_method !== undefined &&
666
+ !isAuthenticationMethod(record.authentication_method)
667
+ ) {
668
+ throw new TypeError(`invalid authentication_method`);
669
+ }
670
+ if (
671
+ record.actor_kind === "member" &&
672
+ record.flow_kind !== "reauthentication"
673
+ ) {
674
+ throw new TypeError(
675
+ "only reauthentication failure or block can use a member actor",
676
+ );
677
+ }
678
+ if (
679
+ !authenticationBlockReasons.includes(
680
+ record.reason as AuthenticationBlockReason,
681
+ )
682
+ ) {
683
+ throw new TypeError(
684
+ `invalid authentication block reason: ${record.reason}`,
685
+ );
686
+ }
687
+ validateAuthenticationProvider(record);
688
+ return;
689
+ }
690
+ case "authorization.denied": {
691
+ requireOnlyAuthorizationAttributes(record);
692
+ if (
693
+ record.actor_kind === "system" ||
694
+ !isValidAuthorizationScope(
695
+ record.attempted_action ?? "",
696
+ record.permission ?? "",
697
+ )
698
+ ) {
699
+ throw new TypeError(
700
+ "authorization denial requires a cataloged attempted action and permission",
701
+ );
702
+ }
703
+ if (
704
+ !authorizationDenialReasons.includes(
705
+ record.reason as AuthorizationDenialReason,
706
+ )
707
+ ) {
708
+ throw new TypeError(
709
+ `invalid authorization denial reason: ${record.reason}`,
710
+ );
711
+ }
712
+ return;
713
+ }
714
+ case "personal_data.accessed": {
715
+ requireOnlyPersonalDataAttributes(record);
716
+ if (
717
+ record.actor_kind !== "member" ||
718
+ record.access_kind !== "read" ||
719
+ !isValidPersonalDataSubject(
720
+ record.subject_type ?? "",
721
+ record.subject_id ?? "",
722
+ record.data_category ?? "",
723
+ )
724
+ ) {
725
+ throw new TypeError(
726
+ "personal data access requires a cataloged member read scope",
727
+ );
728
+ }
729
+ return;
730
+ }
731
+ default:
732
+ throw new TypeError(`unknown security action: ${String(record.action)}`);
733
+ }
734
+ }
735
+
736
+ function isValidAuthorizationScope(
737
+ attemptedAction: string,
738
+ permission: string,
739
+ ): boolean {
740
+ return (
741
+ isConnectProcedure(attemptedAction) && permission === "procedure:invoke"
742
+ );
743
+ }
744
+
745
+ function validateAuthenticationProvider(record: SecurityAccessRecord): void {
746
+ if (record.provider === undefined) {
747
+ return;
748
+ }
749
+ if (
750
+ record.authentication_method !== "oidc" ||
751
+ !isBoundedCode(record.provider)
752
+ ) {
753
+ throw new TypeError(
754
+ "provider is only valid as a bounded code for oidc authentication",
755
+ );
756
+ }
757
+ }
758
+
759
+ function requireOnlyAuthenticationAttributes(
760
+ record: SecurityAccessRecord,
761
+ ): void {
762
+ if (
763
+ record.attempted_action !== undefined ||
764
+ record.permission !== undefined ||
765
+ record.subject_type !== undefined ||
766
+ record.subject_id !== undefined ||
767
+ record.access_kind !== undefined ||
768
+ record.data_category !== undefined
769
+ ) {
770
+ throw new TypeError(
771
+ "authentication access contains attributes for another security action",
772
+ );
773
+ }
774
+ }
775
+
776
+ function requireOnlyAuthorizationAttributes(
777
+ record: SecurityAccessRecord,
778
+ ): void {
779
+ if (
780
+ record.flow_kind !== undefined ||
781
+ record.authentication_method !== undefined ||
782
+ record.principal_state !== undefined ||
783
+ record.provider !== undefined ||
784
+ record.subject_type !== undefined ||
785
+ record.subject_id !== undefined ||
786
+ record.access_kind !== undefined ||
787
+ record.data_category !== undefined
788
+ ) {
789
+ throw new TypeError(
790
+ "authorization denial contains attributes for another security action",
791
+ );
792
+ }
793
+ }
794
+
795
+ function requireOnlyPersonalDataAttributes(record: SecurityAccessRecord): void {
796
+ if (
797
+ record.flow_kind !== undefined ||
798
+ record.authentication_method !== undefined ||
799
+ record.principal_state !== undefined ||
800
+ record.provider !== undefined ||
801
+ record.reason !== undefined ||
802
+ record.attempted_action !== undefined ||
803
+ record.permission !== undefined
804
+ ) {
805
+ throw new TypeError(
806
+ "personal data access contains attributes for another security action",
807
+ );
808
+ }
809
+ }
810
+
811
+ function isValidPersonalDataSubject(
812
+ subjectType: string,
813
+ subjectId: string,
814
+ dataCategory: string,
815
+ ): boolean {
816
+ if (subjectType === "member_collection") {
817
+ return subjectId === "1" && dataCategory === "member_administration";
818
+ }
819
+ if (!isCanonicalUuid(subjectId)) {
820
+ return false;
821
+ }
822
+ switch (subjectType) {
823
+ case "member":
824
+ return dataCategory === "member_administration";
825
+ case "campaign":
826
+ return dataCategory === "campaign_recipients";
827
+ case "form":
828
+ return dataCategory === "form_submissions";
829
+ case "form_submission":
830
+ return dataCategory === "form_submission";
831
+ default:
832
+ return false;
833
+ }
834
+ }
835
+
836
+ function isCanonicalUuid(value: string): boolean {
837
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(
838
+ value,
839
+ );
840
+ }
841
+
842
+ function isConnectProcedure(value: string): boolean {
843
+ return (
844
+ value.length >= 4 &&
845
+ value.length <= 128 &&
846
+ /^\/[A-Za-z0-9_.]+\/[A-Za-z0-9_]+$/.test(value)
847
+ );
848
+ }
849
+
850
+ export async function emitRequest(
851
+ sink: TelemetrySink,
852
+ record: RequestRecord,
853
+ ): Promise<void> {
854
+ validateRequestRecord(record);
855
+ await sink.write(record);
856
+ }
857
+
858
+ export async function emitSystem(
859
+ sink: TelemetrySink,
860
+ record: SystemRecord,
861
+ ): Promise<void> {
862
+ validateSystemRecord(record);
863
+ await sink.write(record);
864
+ }
865
+
866
+ function validateOccurredAt(value: string): void {
867
+ if (
868
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value) ||
869
+ Number.isNaN(Date.parse(value))
870
+ ) {
871
+ throw new TypeError("occurred_at must be a UTC RFC3339 timestamp");
872
+ }
873
+ }
874
+
875
+ function validateTraceCorrelation(record: Correlation): void {
876
+ if (record.trace_id === undefined && record.span_id === undefined) {
877
+ return;
878
+ }
879
+ if (
880
+ record.trace_id === undefined ||
881
+ record.span_id === undefined ||
882
+ !/^[0-9a-f]{32}$/.test(record.trace_id) ||
883
+ /^0+$/.test(record.trace_id) ||
884
+ !/^[0-9a-f]{16}$/.test(record.span_id) ||
885
+ /^0+$/.test(record.span_id)
886
+ ) {
887
+ throw new TypeError(
888
+ "trace_id and span_id must be valid and provided together",
889
+ );
890
+ }
891
+ }
892
+
893
+ function validateRecordActor(
894
+ record: Readonly<{
895
+ actor_kind: string;
896
+ actor_member_id?: string;
897
+ actor_service?: string;
898
+ }>,
899
+ ): void {
900
+ switch (record.actor_kind) {
901
+ case "anonymous":
902
+ if (
903
+ record.actor_member_id !== undefined ||
904
+ record.actor_service !== undefined
905
+ ) {
906
+ throw new TypeError(
907
+ "anonymous record actor cannot contain member or service identity",
908
+ );
909
+ }
910
+ return;
911
+ case "member":
912
+ if (
913
+ record.actor_member_id === undefined ||
914
+ record.actor_member_id === "" ||
915
+ record.actor_service !== undefined
916
+ ) {
917
+ throw new TypeError(
918
+ "member record actor requires only actor_member_id",
919
+ );
920
+ }
921
+ return;
922
+ case "system":
923
+ if (
924
+ record.actor_service === undefined ||
925
+ record.actor_service === "" ||
926
+ record.actor_member_id !== undefined
927
+ ) {
928
+ throw new TypeError("system record actor requires only actor_service");
929
+ }
930
+ parseServiceName(record.actor_service);
931
+ return;
932
+ default:
933
+ throw new TypeError(`invalid actor kind: ${record.actor_kind}`);
934
+ }
935
+ }
936
+
937
+ function validateSystemRequiredFields(record: SystemRecord): void {
938
+ const requireStrings = (...values: (string | undefined)[]): void => {
939
+ if (values.some((value) => value === undefined || value === "")) {
940
+ throw new TypeError(
941
+ `system event ${record.event} is missing a required field`,
942
+ );
943
+ }
944
+ };
945
+ const requireFailure = (): void => {
946
+ if (record.error_code === undefined && record.reason === undefined) {
947
+ throw new TypeError(
948
+ `system event ${record.event} requires error_code or reason`,
949
+ );
950
+ }
951
+ };
952
+ switch (record.event) {
953
+ case "service.ready":
954
+ case "service.stopping":
955
+ requireStrings(record.component);
956
+ return;
957
+ case "service.degraded":
958
+ case "service.failed":
959
+ requireStrings(record.component);
960
+ requireFailure();
961
+ return;
962
+ case "dependency.recovered":
963
+ requireStrings(record.dependency, record.operation);
964
+ return;
965
+ case "dependency.degraded":
966
+ requireStrings(record.dependency, record.operation);
967
+ requireFailure();
968
+ return;
969
+ case "queue.publish.succeeded":
970
+ case "queue.publish.failed":
971
+ requireStrings(record.queue, record.message_id, record.command_id);
972
+ if (record.duration_ms === undefined) {
973
+ throw new TypeError(
974
+ `system event ${record.event} requires duration_ms`,
975
+ );
976
+ }
977
+ if (record.event === "queue.publish.failed") requireFailure();
978
+ if (record.event === "queue.publish.succeeded") {
979
+ requireNoFailure(record);
980
+ } else {
981
+ requireQueueReason(record, ["enqueue_failed"]);
982
+ }
983
+ return;
984
+ case "queue.delivery.succeeded":
985
+ case "queue.delivery.failed":
986
+ case "queue.delivery.requeued":
987
+ requireStrings(record.queue, record.message_id, record.command_id);
988
+ if (
989
+ record.retry_count === undefined ||
990
+ record.duration_ms === undefined
991
+ ) {
992
+ throw new TypeError(
993
+ `system event ${record.event} requires retry_count and duration_ms`,
994
+ );
995
+ }
996
+ if (record.event === "queue.delivery.succeeded") {
997
+ requireNoFailure(record);
998
+ } else if (record.event === "queue.delivery.requeued") {
999
+ requireQueueReason(record, ["shutdown", "handler_failed"]);
1000
+ } else {
1001
+ requireFailure();
1002
+ requireQueueReason(record, ["handler_failed", "completion_failed"]);
1003
+ }
1004
+ return;
1005
+ case "queue.retry.accepted":
1006
+ case "queue.retry.failed":
1007
+ case "queue.dlq.accepted":
1008
+ case "queue.dlq.failed":
1009
+ requireStrings(record.queue, record.message_id, record.command_id);
1010
+ if (record.retry_count === undefined) {
1011
+ throw new TypeError(
1012
+ `system event ${record.event} requires retry_count`,
1013
+ );
1014
+ }
1015
+ if (record.event.endsWith(".failed")) {
1016
+ requireFailure();
1017
+ requireQueueReason(record, [
1018
+ record.event === "queue.retry.failed"
1019
+ ? "visibility_update_failed"
1020
+ : "archive_failed",
1021
+ ]);
1022
+ } else {
1023
+ requireNoFailure(record);
1024
+ }
1025
+ return;
1026
+ case "job.started":
1027
+ requireStrings(record.job_kind, record.job_id);
1028
+ parseJobKind(record.job_kind!);
1029
+ return;
1030
+ case "job.succeeded":
1031
+ case "job.failed":
1032
+ requireStrings(record.job_kind, record.job_id);
1033
+ const jobKind = parseJobKind(record.job_kind!);
1034
+ if (record.duration_ms === undefined) {
1035
+ throw new TypeError(
1036
+ `system event ${record.event} requires duration_ms`,
1037
+ );
1038
+ }
1039
+ if (record.event === "job.failed") {
1040
+ if (record.error_code !== undefined || record.reason === undefined) {
1041
+ throw new TypeError(
1042
+ `system event ${record.event} requires a catalog job failure reason`,
1043
+ );
1044
+ }
1045
+ parseJobFailureReason(jobKind, record.reason);
1046
+ }
1047
+ return;
1048
+ case "audit.append.failed":
1049
+ requireStrings(record.record_class, record.action);
1050
+ if (!isKnownAppendAction(record.record_class!, record.action!)) {
1051
+ throw new TypeError(
1052
+ "audit.append.failed requires a catalog action for record_class",
1053
+ );
1054
+ }
1055
+ if (
1056
+ record.error_code !== undefined ||
1057
+ ![
1058
+ "record_invalid",
1059
+ "transaction_missing",
1060
+ "persistence_failed",
1061
+ "database_missing",
1062
+ "request_context_missing",
1063
+ "actor_invalid",
1064
+ "record_build_failed",
1065
+ ].includes(record.reason ?? "")
1066
+ ) {
1067
+ throw new TypeError(
1068
+ "audit.append.failed requires a catalog audit append reason",
1069
+ );
1070
+ }
1071
+ return;
1072
+ case "collaboration.checkpoint.failed":
1073
+ requireStrings(record.domain, record.entity_type, record.entity_id);
1074
+ if (
1075
+ record.domain !== "collaboration" ||
1076
+ record.retry_count === undefined ||
1077
+ record.error_code !== undefined ||
1078
+ !COLLABORATION_CHECKPOINT_ENTITY_TYPES.some(
1079
+ (entityType) => entityType === record.entity_type,
1080
+ ) ||
1081
+ ![
1082
+ "shared_document_unavailable",
1083
+ "source_document_unavailable",
1084
+ ...COLLABORATION_CONFLICT_REASONS,
1085
+ "persist_failed",
1086
+ ].includes(record.reason ?? "") ||
1087
+ (COLLABORATION_CONFLICT_REASONS.includes(record.reason ?? "") &&
1088
+ record.retry_count !== 1)
1089
+ ) {
1090
+ throw new TypeError(
1091
+ "collaboration.checkpoint.failed requires a terminal checkpoint context and catalog reason",
1092
+ );
1093
+ }
1094
+ return;
1095
+ case "client.render.failed":
1096
+ requireStrings(record.domain, record.component);
1097
+ if (
1098
+ record.domain !== "client" ||
1099
+ !["general", "admin", "global"].includes(record.component ?? "") ||
1100
+ record.error_code !== undefined ||
1101
+ record.reason !== "react_error_boundary"
1102
+ ) {
1103
+ throw new TypeError(
1104
+ "client.render.failed requires a catalog component and reason",
1105
+ );
1106
+ }
1107
+ return;
1108
+ case "translation.job.terminal":
1109
+ requireStrings(
1110
+ record.domain,
1111
+ record.job_id,
1112
+ record.entity_type,
1113
+ record.target_locale,
1114
+ );
1115
+ requireTranslationTerminalOnlyFields(record);
1116
+ if (
1117
+ record.domain !== "translation" ||
1118
+ !isCanonicalUuid(record.job_id!) ||
1119
+ !TRANSLATION_ENTITY_TYPES.includes(
1120
+ record.entity_type as TranslationEntityType,
1121
+ ) ||
1122
+ !isBoundedLocaleCode(record.target_locale!) ||
1123
+ record.duration_ms === undefined
1124
+ ) {
1125
+ throw new TypeError(
1126
+ "translation terminal event requires a canonical job and locale context",
1127
+ );
1128
+ }
1129
+ if (record.outcome !== "failed") {
1130
+ if (record.error_classification !== undefined) {
1131
+ throw new TypeError(
1132
+ "non-failed translation.job.terminal cannot contain error_classification",
1133
+ );
1134
+ }
1135
+ requireNoFailure(record);
1136
+ return;
1137
+ }
1138
+ if (
1139
+ !TRANSLATION_FAILURE_REASONS.includes(
1140
+ record.error_classification as TranslationFailureReason,
1141
+ )
1142
+ ) {
1143
+ throw new TypeError(
1144
+ "failed translation.job.terminal requires a catalog error_classification",
1145
+ );
1146
+ }
1147
+ return;
1148
+ case "telemetry.pipeline.recovered":
1149
+ requireStrings(record.component);
1150
+ return;
1151
+ case "telemetry.pipeline.degraded":
1152
+ requireStrings(record.component);
1153
+ requireFailure();
1154
+ }
1155
+ }
1156
+
1157
+ function requireNoFailure(record: SystemRecord): void {
1158
+ if (record.error_code !== undefined || record.reason !== undefined) {
1159
+ throw new TypeError(
1160
+ `system event ${record.event} does not allow failure fields`,
1161
+ );
1162
+ }
1163
+ }
1164
+
1165
+ function requireTranslationTerminalOnlyFields(record: SystemRecord): void {
1166
+ if (
1167
+ record.component !== undefined ||
1168
+ record.dependency !== undefined ||
1169
+ record.operation !== undefined ||
1170
+ record.queue !== undefined ||
1171
+ record.message_id !== undefined ||
1172
+ record.command_id !== undefined ||
1173
+ record.retry_count !== undefined ||
1174
+ record.job_kind !== undefined ||
1175
+ record.entity_id !== undefined ||
1176
+ record.record_class !== undefined ||
1177
+ record.action !== undefined ||
1178
+ record.error_code !== undefined ||
1179
+ record.reason !== undefined
1180
+ ) {
1181
+ throw new TypeError(
1182
+ "translation terminal event contains unsupported fields",
1183
+ );
1184
+ }
1185
+ }
1186
+
1187
+ function requireQueueReason(
1188
+ record: SystemRecord,
1189
+ allowed: readonly string[],
1190
+ ): void {
1191
+ if (record.error_code !== undefined || !allowed.includes(record.reason!)) {
1192
+ throw new TypeError(
1193
+ `system event ${record.event} requires a catalog queue reason`,
1194
+ );
1195
+ }
1196
+ }
1197
+
1198
+ function isKnownAppendAction(recordClass: string, action: string): boolean {
1199
+ if (recordClass === "domain_audit") {
1200
+ return (AUDIT_ACTIONS as readonly string[]).includes(action);
1201
+ }
1202
+ if (recordClass === "security_access") {
1203
+ return [
1204
+ "authentication.succeeded",
1205
+ "authentication.failed",
1206
+ "authentication.blocked",
1207
+ "authorization.denied",
1208
+ "personal_data.accessed",
1209
+ ].includes(action);
1210
+ }
1211
+ return false;
1212
+ }
1213
+
1214
+ function isBoundedCode(value: string): boolean {
1215
+ return /^[a-z0-9_]{1,64}$/.test(value);
1216
+ }
1217
+
1218
+ function isBoundedLocaleCode(value: string): boolean {
1219
+ return (
1220
+ value.length <= 64 && /^[A-Za-z0-9]{1,8}(-[A-Za-z0-9]{1,8})*$/.test(value)
1221
+ );
1222
+ }
1223
+
1224
+ function isAuthenticationFlow(
1225
+ value: AuthenticationFlowKind | undefined,
1226
+ ): value is AuthenticationFlowKind {
1227
+ return (
1228
+ value === "login" ||
1229
+ value === "registration" ||
1230
+ value === "reauthentication"
1231
+ );
1232
+ }
1233
+
1234
+ function isAuthenticationMethod(
1235
+ value: AuthenticationMethod | undefined,
1236
+ ): value is AuthenticationMethod {
1237
+ return value === "email_code" || value === "oidc" || value === "passkey";
1238
+ }
1239
+
1240
+ function isAuthenticationPrincipalState(
1241
+ value: AuthenticationPrincipalState | undefined,
1242
+ ): value is AuthenticationPrincipalState {
1243
+ return value === "onboarding_only" || value === "active";
1244
+ }