@hasna/events 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/filter.js CHANGED
@@ -96,7 +96,7 @@ function channelMatchesEvent(channel, event) {
96
96
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
97
97
  }
98
98
  export {
99
- matchString,
99
+ channelMatchesEvent,
100
100
  eventMatchesFilter,
101
- channelMatchesEvent
101
+ matchString
102
102
  };
package/dist/index.js CHANGED
@@ -444,21 +444,27 @@ function now() {
444
444
  function truncate(value, max = 4096) {
445
445
  return value.length > max ? `${value.slice(0, max)}...` : value;
446
446
  }
447
- function buildWebhookRequest(event, channel) {
447
+ function buildWebhookRequest(event, channel, options = {}) {
448
448
  if (!channel.webhook)
449
449
  throw new Error(`Channel ${channel.id} has no webhook config`);
450
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
451
+ if (/^x-hasna-/i.test(name)) {
452
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
453
+ }
454
+ }
450
455
  const body = JSON.stringify(event);
451
- const timestamp = event.time;
456
+ const timestamp = options.timestamp ?? new Date().toISOString();
452
457
  const headers = {
453
458
  "Content-Type": "application/json",
454
459
  "User-Agent": "@hasna/events",
455
460
  "X-Hasna-Event-Id": event.id,
456
461
  "X-Hasna-Event-Type": event.type,
457
- "X-Hasna-Timestamp": timestamp,
458
- ...channel.webhook.headers
462
+ ...channel.webhook.headers,
463
+ "X-Hasna-Timestamp": timestamp
459
464
  };
460
- if (channel.webhook.secret) {
461
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
465
+ const secret = options.secret ?? channel.webhook.secret;
466
+ if (secret) {
467
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
462
468
  }
463
469
  return { body, headers };
464
470
  }
@@ -466,7 +472,21 @@ async function dispatchWebhook(event, channel, options = {}) {
466
472
  if (!channel.webhook)
467
473
  throw new Error(`Channel ${channel.id} has no webhook config`);
468
474
  const startedAt = now();
469
- const { body, headers } = buildWebhookRequest(event, channel);
475
+ let secret = channel.webhook.secret;
476
+ if (channel.webhook.secretRef) {
477
+ if (!options.secretResolver) {
478
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
479
+ }
480
+ try {
481
+ secret = await options.secretResolver(channel.webhook.secretRef);
482
+ } catch {
483
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
484
+ }
485
+ if (!secret)
486
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
487
+ }
488
+ const timestamp = (options.now?.() ?? new Date).toISOString();
489
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
470
490
  const controller = new AbortController;
471
491
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
472
492
  try {
@@ -498,6 +518,15 @@ async function dispatchWebhook(event, channel, options = {}) {
498
518
  clearTimeout(timeout);
499
519
  }
500
520
  }
521
+ function failedAttempt(startedAt, error) {
522
+ return {
523
+ attempt: 1,
524
+ status: "failed",
525
+ startedAt,
526
+ completedAt: now(),
527
+ error
528
+ };
529
+ }
501
530
  async function dispatchCommand(event, channel) {
502
531
  if (!channel.command)
503
532
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -762,8 +791,416 @@ function registerDistributionEventTypes(catalog = defaultEventTypeCatalog) {
762
791
  }
763
792
  return catalog;
764
793
  }
794
+ // src/app-event.ts
795
+ var APP_EVENT_V1_SCHEMA_VERSION = "hasna.app_event.v1";
796
+ var APP_EVENT_V1_METADATA_KEY = "app_event";
797
+ var APP_EVENT_V1_MAX_SUMMARY_LENGTH = 512;
798
+ var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
799
+ var APP_EVENT_V1_MAX_REFS = 32;
800
+ var APP_EVENT_V1_MAX_TARGETS = 16;
801
+
802
+ class AppEventValidationError extends Error {
803
+ issues;
804
+ constructor(issues) {
805
+ super(`Invalid ${APP_EVENT_V1_SCHEMA_VERSION}: ${issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
806
+ this.name = "AppEventValidationError";
807
+ this.issues = issues;
808
+ }
809
+ }
810
+
811
+ class AppEventReplaySafetyError extends Error {
812
+ eventId;
813
+ constructor(eventId) {
814
+ super(`App event ${eventId} is not marked replay-safe`);
815
+ this.name = "AppEventReplaySafetyError";
816
+ this.eventId = eventId;
817
+ }
818
+ }
819
+ var SEVERITIES = ["debug", "info", "notice", "warning", "error", "critical"];
820
+ var ACTOR_KINDS = ["agent", "human", "service", "model", "workflow", "system"];
821
+ var SENSITIVITIES = ["public", "internal", "confidential", "restricted"];
822
+ var REDACTION_STATES = ["none", "partial", "full"];
823
+ var DELIVERY_INTENTS = ["notification", "state_sync", "audit", "command"];
824
+ var DELIVERY_MODES = ["at_most_once", "at_least_once"];
825
+ function validateAppEventV1(value) {
826
+ const issues = [];
827
+ if (!isRecord(value))
828
+ return { ok: false, issues: [{ path: "<root>", message: "must be an object" }] };
829
+ rejectUnknownKeys(value, [
830
+ "event_id",
831
+ "event_type",
832
+ "schema_version",
833
+ "source",
834
+ "occurred_at",
835
+ "severity",
836
+ "idempotency",
837
+ "correlation",
838
+ "subject",
839
+ "actor",
840
+ "project_mappings",
841
+ "summary",
842
+ "data",
843
+ "resource_refs",
844
+ "evidence_refs",
845
+ "sensitivity",
846
+ "redaction",
847
+ "delivery"
848
+ ], "", issues);
849
+ requireString2(value, "event_id", "event_id", issues, 200);
850
+ requireString2(value, "event_type", "event_type", issues, 200);
851
+ if (value.schema_version !== APP_EVENT_V1_SCHEMA_VERSION) {
852
+ issues.push({ path: "schema_version", message: `must equal ${APP_EVENT_V1_SCHEMA_VERSION}` });
853
+ }
854
+ requireTimestamp(value, "occurred_at", issues);
855
+ requireEnum(value, "severity", SEVERITIES, "severity", issues);
856
+ requireString2(value, "summary", "summary", issues, APP_EVENT_V1_MAX_SUMMARY_LENGTH);
857
+ const source = requireRecord(value, "source", issues);
858
+ if (source) {
859
+ rejectUnknownKeys(source, ["app", "version", "machine"], "source", issues);
860
+ requireString2(source, "app", "source.app", issues, 200);
861
+ requireString2(source, "version", "source.version", issues, 100);
862
+ requireString2(source, "machine", "source.machine", issues, 200);
863
+ }
864
+ const idempotency = requireRecord(value, "idempotency", issues);
865
+ if (idempotency) {
866
+ rejectUnknownKeys(idempotency, ["dedupe_key", "replay_safe", "replay_of_event_id"], "idempotency", issues);
867
+ requireString2(idempotency, "dedupe_key", "idempotency.dedupe_key", issues, 512);
868
+ requireBoolean(idempotency, "replay_safe", "idempotency.replay_safe", issues);
869
+ optionalString2(idempotency, "replay_of_event_id", "idempotency.replay_of_event_id", issues, 200);
870
+ if (idempotency.replay_of_event_id === value.event_id) {
871
+ issues.push({ path: "idempotency.replay_of_event_id", message: "must not reference the event itself" });
872
+ }
873
+ }
874
+ const correlation = requireRecord(value, "correlation", issues);
875
+ if (correlation) {
876
+ rejectUnknownKeys(correlation, ["correlation_id", "causation_id", "trace_id"], "correlation", issues);
877
+ requireString2(correlation, "correlation_id", "correlation.correlation_id", issues, 200);
878
+ optionalString2(correlation, "causation_id", "correlation.causation_id", issues, 200);
879
+ optionalString2(correlation, "trace_id", "correlation.trace_id", issues, 200);
880
+ }
881
+ validateSubject(value, issues);
882
+ validateActor(value, issues);
883
+ validateProjectMappings(value, issues);
884
+ validateData(value.data, issues);
885
+ validateResourceRefs(value.resource_refs, issues);
886
+ validateEvidenceRefs(value.evidence_refs, issues);
887
+ validateSensitivity(value, issues);
888
+ validateRedaction(value, issues);
889
+ validateDelivery(value, issues);
890
+ return issues.length === 0 ? { ok: true } : { ok: false, issues };
891
+ }
892
+ function assertAppEventV1(value) {
893
+ const result = validateAppEventV1(value);
894
+ if (!result.ok)
895
+ throw new AppEventValidationError(result.issues);
896
+ }
897
+ function assertAppEventV1ReplaySafe(event) {
898
+ assertAppEventV1(event);
899
+ if (!event.idempotency.replay_safe)
900
+ throw new AppEventReplaySafetyError(event.event_id);
901
+ }
902
+ function appEventV1ReplayIdentity(event) {
903
+ assertAppEventV1ReplaySafe(event);
904
+ return { eventId: event.event_id, dedupeKey: event.idempotency.dedupe_key };
905
+ }
906
+ function appEventV1ToEventInput(event) {
907
+ assertAppEventV1(event);
908
+ const metadata = {
909
+ profile: APP_EVENT_V1_SCHEMA_VERSION,
910
+ source_version: event.source.version,
911
+ source_machine: event.source.machine,
912
+ replay_safe: event.idempotency.replay_safe,
913
+ replay_of_event_id: event.idempotency.replay_of_event_id,
914
+ correlation: structuredClone(event.correlation),
915
+ subject: structuredClone(event.subject),
916
+ actor: structuredClone(event.actor),
917
+ project_mappings: structuredClone(event.project_mappings),
918
+ resource_refs: structuredClone(event.resource_refs),
919
+ evidence_refs: structuredClone(event.evidence_refs),
920
+ sensitivity: structuredClone(event.sensitivity),
921
+ redaction: structuredClone(event.redaction),
922
+ delivery: structuredClone(event.delivery)
923
+ };
924
+ return {
925
+ id: event.event_id,
926
+ source: event.source.app,
927
+ type: event.event_type,
928
+ time: event.occurred_at,
929
+ subject: event.subject.uri ?? `${event.subject.kind}:${event.subject.id}`,
930
+ severity: event.severity,
931
+ data: structuredClone(event.data),
932
+ message: event.summary,
933
+ dedupeKey: event.idempotency.dedupe_key,
934
+ schemaVersion: APP_EVENT_V1_SCHEMA_VERSION,
935
+ metadata: { [APP_EVENT_V1_METADATA_KEY]: metadata }
936
+ };
937
+ }
938
+ function appEventV1FromEventEnvelope(envelope) {
939
+ const metadata = envelope.metadata[APP_EVENT_V1_METADATA_KEY];
940
+ if (!isRecord(metadata) || metadata.profile !== APP_EVENT_V1_SCHEMA_VERSION) {
941
+ throw new AppEventValidationError([{
942
+ path: `metadata.${APP_EVENT_V1_METADATA_KEY}.profile`,
943
+ message: `must equal ${APP_EVENT_V1_SCHEMA_VERSION}`
944
+ }]);
945
+ }
946
+ const event = {
947
+ event_id: envelope.id,
948
+ event_type: envelope.type,
949
+ schema_version: envelope.schemaVersion,
950
+ source: {
951
+ app: envelope.source,
952
+ version: metadata.source_version,
953
+ machine: metadata.source_machine
954
+ },
955
+ occurred_at: envelope.time,
956
+ severity: envelope.severity,
957
+ idempotency: {
958
+ dedupe_key: envelope.dedupeKey,
959
+ replay_safe: metadata.replay_safe,
960
+ replay_of_event_id: metadata.replay_of_event_id
961
+ },
962
+ correlation: metadata.correlation,
963
+ subject: metadata.subject,
964
+ actor: metadata.actor,
965
+ project_mappings: metadata.project_mappings,
966
+ summary: envelope.message,
967
+ data: structuredClone(envelope.data),
968
+ resource_refs: metadata.resource_refs,
969
+ evidence_refs: metadata.evidence_refs,
970
+ sensitivity: metadata.sensitivity,
971
+ redaction: metadata.redaction,
972
+ delivery: metadata.delivery
973
+ };
974
+ assertAppEventV1(event);
975
+ return structuredClone(event);
976
+ }
977
+ function validateSubject(value, issues) {
978
+ const subject = requireRecord(value, "subject", issues);
979
+ if (!subject)
980
+ return;
981
+ rejectUnknownKeys(subject, ["kind", "id", "uri"], "subject", issues);
982
+ requireString2(subject, "kind", "subject.kind", issues, 100);
983
+ requireString2(subject, "id", "subject.id", issues, 200);
984
+ optionalString2(subject, "uri", "subject.uri", issues, 2048);
985
+ }
986
+ function validateActor(value, issues) {
987
+ const actor = requireRecord(value, "actor", issues);
988
+ if (!actor)
989
+ return;
990
+ rejectUnknownKeys(actor, ["kind", "id", "name"], "actor", issues);
991
+ requireEnum(actor, "kind", ACTOR_KINDS, "actor.kind", issues);
992
+ requireString2(actor, "id", "actor.id", issues, 200);
993
+ optionalString2(actor, "name", "actor.name", issues, 200);
994
+ }
995
+ function validateProjectMappings(value, issues) {
996
+ const project = requireRecord(value, "project_mappings", issues);
997
+ if (!project)
998
+ return;
999
+ rejectUnknownKeys(project, ["canonical_id", "slug", "repository", "workspace", "external_ids"], "project_mappings", issues);
1000
+ requireString2(project, "canonical_id", "project_mappings.canonical_id", issues, 200);
1001
+ optionalString2(project, "slug", "project_mappings.slug", issues, 200);
1002
+ optionalString2(project, "repository", "project_mappings.repository", issues, 2048);
1003
+ optionalString2(project, "workspace", "project_mappings.workspace", issues, 2048);
1004
+ const externalIds = requireRecord(project, "external_ids", issues, "project_mappings.external_ids");
1005
+ if (externalIds) {
1006
+ if (Object.keys(externalIds).length > APP_EVENT_V1_MAX_TARGETS) {
1007
+ issues.push({ path: "project_mappings.external_ids", message: `must have at most ${APP_EVENT_V1_MAX_TARGETS} entries` });
1008
+ }
1009
+ for (const [key, entry] of Object.entries(externalIds)) {
1010
+ if (!key.trim() || typeof entry !== "string" || !entry.trim()) {
1011
+ issues.push({ path: `project_mappings.external_ids.${key}`, message: "keys and values must be non-empty strings" });
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+ function validateData(value, issues) {
1017
+ if (!isRecord(value)) {
1018
+ issues.push({ path: "data", message: "must be an object" });
1019
+ return;
1020
+ }
1021
+ try {
1022
+ const bytes = new TextEncoder().encode(JSON.stringify(value)).byteLength;
1023
+ if (bytes > APP_EVENT_V1_MAX_DATA_BYTES) {
1024
+ issues.push({ path: "data", message: `must serialize to at most ${APP_EVENT_V1_MAX_DATA_BYTES} UTF-8 bytes` });
1025
+ }
1026
+ } catch {
1027
+ issues.push({ path: "data", message: "must be JSON serializable" });
1028
+ }
1029
+ }
1030
+ function validateResourceRefs(value, issues) {
1031
+ validateRefArray(value, "resource_refs", issues, (ref, path) => {
1032
+ rejectUnknownKeys(ref, ["kind", "id", "uri", "source_package", "external_id"], path, issues);
1033
+ requireString2(ref, "kind", `${path}.kind`, issues, 100);
1034
+ requireString2(ref, "id", `${path}.id`, issues, 200);
1035
+ optionalString2(ref, "uri", `${path}.uri`, issues, 2048);
1036
+ optionalString2(ref, "source_package", `${path}.source_package`, issues, 200);
1037
+ optionalString2(ref, "external_id", `${path}.external_id`, issues, 200);
1038
+ });
1039
+ }
1040
+ function validateEvidenceRefs(value, issues) {
1041
+ validateRefArray(value, "evidence_refs", issues, (ref, path) => {
1042
+ rejectUnknownKeys(ref, ["kind", "id", "uri", "sha256", "redaction"], path, issues);
1043
+ requireString2(ref, "kind", `${path}.kind`, issues, 100);
1044
+ requireString2(ref, "id", `${path}.id`, issues, 200);
1045
+ requireString2(ref, "uri", `${path}.uri`, issues, 2048);
1046
+ optionalString2(ref, "sha256", `${path}.sha256`, issues, 64);
1047
+ if (typeof ref.sha256 === "string" && !/^[a-f0-9]{64}$/i.test(ref.sha256)) {
1048
+ issues.push({ path: `${path}.sha256`, message: "must be a 64-character hexadecimal digest" });
1049
+ }
1050
+ requireEnum(ref, "redaction", REDACTION_STATES, `${path}.redaction`, issues);
1051
+ });
1052
+ }
1053
+ function validateSensitivity(value, issues) {
1054
+ const sensitivity = requireRecord(value, "sensitivity", issues);
1055
+ if (!sensitivity)
1056
+ return;
1057
+ rejectUnknownKeys(sensitivity, ["classification", "contains_personal_data"], "sensitivity", issues);
1058
+ requireEnum(sensitivity, "classification", SENSITIVITIES, "sensitivity.classification", issues);
1059
+ requireBoolean(sensitivity, "contains_personal_data", "sensitivity.contains_personal_data", issues);
1060
+ }
1061
+ function validateRedaction(value, issues) {
1062
+ const redaction = requireRecord(value, "redaction", issues);
1063
+ if (!redaction)
1064
+ return;
1065
+ rejectUnknownKeys(redaction, ["state", "fields", "safe_for_logs"], "redaction", issues);
1066
+ requireEnum(redaction, "state", REDACTION_STATES, "redaction.state", issues);
1067
+ validateStringArray(redaction.fields, "redaction.fields", APP_EVENT_V1_MAX_REFS, issues, true);
1068
+ requireBoolean(redaction, "safe_for_logs", "redaction.safe_for_logs", issues);
1069
+ if (redaction.state === "none" && Array.isArray(redaction.fields) && redaction.fields.length > 0) {
1070
+ issues.push({ path: "redaction.fields", message: "must be empty when redaction.state is none" });
1071
+ }
1072
+ }
1073
+ function validateDelivery(value, issues) {
1074
+ const delivery = requireRecord(value, "delivery", issues);
1075
+ if (!delivery)
1076
+ return;
1077
+ rejectUnknownKeys(delivery, ["intent", "mode", "targets", "agent_conversation_injection"], "delivery", issues);
1078
+ requireEnum(delivery, "intent", DELIVERY_INTENTS, "delivery.intent", issues);
1079
+ requireEnum(delivery, "mode", DELIVERY_MODES, "delivery.mode", issues);
1080
+ validateStringArray(delivery.targets, "delivery.targets", APP_EVENT_V1_MAX_TARGETS, issues, false);
1081
+ if (delivery.agent_conversation_injection !== false) {
1082
+ issues.push({ path: "delivery.agent_conversation_injection", message: "must be false" });
1083
+ }
1084
+ }
1085
+ function validateRefArray(value, path, issues, validate) {
1086
+ if (!Array.isArray(value)) {
1087
+ issues.push({ path, message: "must be an array" });
1088
+ return;
1089
+ }
1090
+ if (value.length > APP_EVENT_V1_MAX_REFS) {
1091
+ issues.push({ path, message: `must contain at most ${APP_EVENT_V1_MAX_REFS} entries` });
1092
+ }
1093
+ value.forEach((entry, index) => {
1094
+ if (!isRecord(entry))
1095
+ issues.push({ path: `${path}.${index}`, message: "must be an object" });
1096
+ else
1097
+ validate(entry, `${path}.${index}`);
1098
+ });
1099
+ }
1100
+ function validateStringArray(value, path, maxItems, issues, allowEmpty) {
1101
+ if (!Array.isArray(value)) {
1102
+ issues.push({ path, message: "must be an array" });
1103
+ return;
1104
+ }
1105
+ if (!allowEmpty && value.length === 0)
1106
+ issues.push({ path, message: "must contain at least one entry" });
1107
+ if (value.length > maxItems)
1108
+ issues.push({ path, message: `must contain at most ${maxItems} entries` });
1109
+ value.forEach((entry, index) => {
1110
+ if (typeof entry !== "string" || !entry.trim()) {
1111
+ issues.push({ path: `${path}.${index}`, message: "must be a non-empty string" });
1112
+ }
1113
+ });
1114
+ }
1115
+ function isRecord(value) {
1116
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1117
+ }
1118
+ function rejectUnknownKeys(value, allowed, path, issues) {
1119
+ for (const key of Object.keys(value)) {
1120
+ if (!allowed.includes(key))
1121
+ issues.push({ path: path ? `${path}.${key}` : key, message: "is not allowed" });
1122
+ }
1123
+ }
1124
+ function requireRecord(value, key, issues, path = key) {
1125
+ const entry = value[key];
1126
+ if (!isRecord(entry)) {
1127
+ issues.push({ path, message: "must be an object" });
1128
+ return;
1129
+ }
1130
+ return entry;
1131
+ }
1132
+ function requireString2(value, key, path, issues, maxLength) {
1133
+ const entry = value[key];
1134
+ if (typeof entry !== "string" || !entry.trim())
1135
+ issues.push({ path, message: "must be a non-empty string" });
1136
+ else if (entry.length > maxLength)
1137
+ issues.push({ path, message: `must have at most ${maxLength} characters` });
1138
+ }
1139
+ function optionalString2(value, key, path, issues, maxLength) {
1140
+ if (value[key] === undefined)
1141
+ return;
1142
+ requireString2(value, key, path, issues, maxLength);
1143
+ }
1144
+ function requireBoolean(value, key, path, issues) {
1145
+ if (typeof value[key] !== "boolean")
1146
+ issues.push({ path, message: "must be a boolean" });
1147
+ }
1148
+ function requireEnum(value, key, allowed, path, issues) {
1149
+ if (typeof value[key] !== "string" || !allowed.includes(value[key])) {
1150
+ issues.push({ path, message: `must be one of: ${allowed.join(", ")}` });
1151
+ }
1152
+ }
1153
+ function requireTimestamp(value, key, issues) {
1154
+ const entry = value[key];
1155
+ if (typeof entry !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(entry) || Number.isNaN(Date.parse(entry))) {
1156
+ issues.push({ path: key, message: "must be an RFC 3339 date-time" });
1157
+ }
1158
+ }
1159
+
765
1160
  // src/index.ts
766
1161
  import { randomUUID as randomUUID2 } from "crypto";
1162
+
1163
+ // src/redaction.ts
1164
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
1165
+ if (paths.length === 0)
1166
+ return event;
1167
+ const copy = structuredClone(event);
1168
+ for (const path of paths) {
1169
+ setPath(copy, path, replacement);
1170
+ }
1171
+ return copy;
1172
+ }
1173
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
1174
+ return redactValue(event, replacement);
1175
+ }
1176
+ function shouldRedactKey(key) {
1177
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
1178
+ }
1179
+ function redactValue(value, replacement) {
1180
+ if (Array.isArray(value))
1181
+ return value.map((item) => redactValue(item, replacement));
1182
+ if (!value || typeof value !== "object")
1183
+ return value;
1184
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
1185
+ key,
1186
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
1187
+ ]));
1188
+ }
1189
+ function setPath(input, path, replacement) {
1190
+ const parts = path.split(".");
1191
+ let cursor = input;
1192
+ for (const part of parts.slice(0, -1)) {
1193
+ const next = cursor[part];
1194
+ if (!next || typeof next !== "object")
1195
+ return;
1196
+ cursor = next;
1197
+ }
1198
+ const last = parts.at(-1);
1199
+ if (last && last in cursor)
1200
+ cursor[last] = replacement;
1201
+ }
1202
+
1203
+ // src/index.ts
767
1204
  function createEvent(input) {
768
1205
  return {
769
1206
  id: input.id ?? randomUUID2(),
@@ -789,7 +1226,11 @@ class EventsClient {
789
1226
  constructor(options = {}) {
790
1227
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
791
1228
  this.redactors = options.redactors ?? [];
792
- this.transportOptions = { fetchImpl: options.fetchImpl };
1229
+ this.transportOptions = {
1230
+ fetchImpl: options.fetchImpl,
1231
+ secretResolver: options.secretResolver,
1232
+ now: options.now
1233
+ };
793
1234
  this.catalog = options.catalog ?? defaultEventTypeCatalog;
794
1235
  this.validateCatalogTypes = options.validateCatalogTypes ?? false;
795
1236
  }
@@ -967,15 +1408,6 @@ class EventsClient {
967
1408
  return createDeliveryResult(event, channel, attempts);
968
1409
  }
969
1410
  }
970
- function redactPaths(event, paths, replacement = "[REDACTED]") {
971
- if (paths.length === 0)
972
- return event;
973
- const copy = structuredClone(event);
974
- for (const path of paths) {
975
- setPath(copy, path, replacement);
976
- }
977
- return copy;
978
- }
979
1411
  function sanitizeChannelForOutput(channel) {
980
1412
  const copy = structuredClone(channel);
981
1413
  if (copy.webhook?.secret)
@@ -988,35 +1420,6 @@ function sanitizeChannelForOutput(channel) {
988
1420
  function sanitizeChannelsForOutput(channels) {
989
1421
  return channels.map(sanitizeChannelForOutput);
990
1422
  }
991
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
992
- return redactValue(event, replacement);
993
- }
994
- function shouldRedactKey(key) {
995
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
996
- }
997
- function redactValue(value, replacement) {
998
- if (Array.isArray(value))
999
- return value.map((item) => redactValue(item, replacement));
1000
- if (!value || typeof value !== "object")
1001
- return value;
1002
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
1003
- key,
1004
- shouldRedactKey(key) ? replacement : redactValue(item, replacement)
1005
- ]));
1006
- }
1007
- function setPath(input, path, replacement) {
1008
- const parts = path.split(".");
1009
- let cursor = input;
1010
- for (const part of parts.slice(0, -1)) {
1011
- const next = cursor[part];
1012
- if (!next || typeof next !== "object")
1013
- return;
1014
- cursor = next;
1015
- }
1016
- const last = parts.at(-1);
1017
- if (last && last in cursor)
1018
- cursor[last] = replacement;
1019
- }
1020
1423
  function queryClientEvents(events, options) {
1021
1424
  let rows = events;
1022
1425
  if (options.eventId)
@@ -1044,50 +1447,64 @@ function normalizeRetryPolicy(policy) {
1044
1447
  };
1045
1448
  }
1046
1449
  export {
1047
- verifyWebhookSignature,
1048
- verifyPayloadSignature,
1049
- validateRolloutData,
1050
- validateReleasePublishedData,
1051
- validateFeedbackTriagedData,
1052
- validateFeedbackCreatedData,
1053
- validateAppInstalledData,
1054
- validateAnnouncementSentData,
1055
- signPayload,
1056
- sanitizeChannelsForOutput,
1057
- sanitizeChannelForOutput,
1058
- registerDistributionEventTypes,
1059
- redactSensitiveKeys,
1060
- redactPaths,
1061
- normalizeEventPageLimit,
1062
- matchString,
1063
- localJsonRuntime,
1064
- isTimestampWithinTolerance,
1065
- getEventsStatus,
1066
- getEventsDataDir,
1067
- getActiveEventsDirEnv,
1068
- eventMatchesFilter,
1069
- encodeLocalJsonEventCursor,
1070
- dispatchWebhook,
1071
- dispatchCommand,
1072
- dispatchChannel,
1073
- defaultEventTypeCatalog,
1074
- decodeLocalJsonEventCursor,
1075
- createEvent,
1076
- createDistributionEventDefinitions,
1077
- createDeliveryResult,
1078
- channelMatchesEvent,
1079
- buildWebhookRequest,
1080
- buildSignatureBase,
1081
- MAX_EVENT_PAGE_LIMIT,
1082
- LOCAL_JSON_EVENT_CURSOR_PREFIX,
1083
- JsonEventsStore,
1084
- HASNA_EVENTS_HOME_ENV,
1085
- HASNA_EVENTS_DIR_ENV,
1086
- EventsClient,
1087
- EventValidationError,
1088
- EventTypeCatalog,
1089
- DISTRIBUTION_EVENT_TYPES,
1090
- DISTRIBUTION_EVENT_CONTRACT_SCHEMAS,
1450
+ APP_EVENT_V1_MAX_DATA_BYTES,
1451
+ APP_EVENT_V1_MAX_REFS,
1452
+ APP_EVENT_V1_MAX_SUMMARY_LENGTH,
1453
+ APP_EVENT_V1_MAX_TARGETS,
1454
+ APP_EVENT_V1_METADATA_KEY,
1455
+ APP_EVENT_V1_SCHEMA_VERSION,
1456
+ AppEventReplaySafetyError,
1457
+ AppEventValidationError,
1458
+ DEFAULT_EVENT_PAGE_LIMIT,
1091
1459
  DEFAULT_SIGNATURE_TOLERANCE_MS,
1092
- DEFAULT_EVENT_PAGE_LIMIT
1460
+ DISTRIBUTION_EVENT_CONTRACT_SCHEMAS,
1461
+ DISTRIBUTION_EVENT_TYPES,
1462
+ EventTypeCatalog,
1463
+ EventValidationError,
1464
+ EventsClient,
1465
+ HASNA_EVENTS_DIR_ENV,
1466
+ HASNA_EVENTS_HOME_ENV,
1467
+ JsonEventsStore,
1468
+ LOCAL_JSON_EVENT_CURSOR_PREFIX,
1469
+ MAX_EVENT_PAGE_LIMIT,
1470
+ appEventV1FromEventEnvelope,
1471
+ appEventV1ReplayIdentity,
1472
+ appEventV1ToEventInput,
1473
+ assertAppEventV1,
1474
+ assertAppEventV1ReplaySafe,
1475
+ buildSignatureBase,
1476
+ buildWebhookRequest,
1477
+ channelMatchesEvent,
1478
+ createDeliveryResult,
1479
+ createDistributionEventDefinitions,
1480
+ createEvent,
1481
+ decodeLocalJsonEventCursor,
1482
+ defaultEventTypeCatalog,
1483
+ dispatchChannel,
1484
+ dispatchCommand,
1485
+ dispatchWebhook,
1486
+ encodeLocalJsonEventCursor,
1487
+ eventMatchesFilter,
1488
+ getActiveEventsDirEnv,
1489
+ getEventsDataDir,
1490
+ getEventsStatus,
1491
+ isTimestampWithinTolerance,
1492
+ localJsonRuntime,
1493
+ matchString,
1494
+ normalizeEventPageLimit,
1495
+ redactPaths,
1496
+ redactSensitiveKeys,
1497
+ registerDistributionEventTypes,
1498
+ sanitizeChannelForOutput,
1499
+ sanitizeChannelsForOutput,
1500
+ signPayload,
1501
+ validateAnnouncementSentData,
1502
+ validateAppEventV1,
1503
+ validateAppInstalledData,
1504
+ validateFeedbackCreatedData,
1505
+ validateFeedbackTriagedData,
1506
+ validateReleasePublishedData,
1507
+ validateRolloutData,
1508
+ verifyPayloadSignature,
1509
+ verifyWebhookSignature
1093
1510
  };
package/dist/signing.js CHANGED
@@ -33,10 +33,10 @@ function verifyWebhookSignature(secret, timestamp, body, signature, options = {}
33
33
  return verifyPayloadSignature(secret, timestamp, body, signature);
34
34
  }
35
35
  export {
36
- verifyWebhookSignature,
37
- verifyPayloadSignature,
38
- signPayload,
39
- isTimestampWithinTolerance,
36
+ DEFAULT_SIGNATURE_TOLERANCE_MS,
40
37
  buildSignatureBase,
41
- DEFAULT_SIGNATURE_TOLERANCE_MS
38
+ isTimestampWithinTolerance,
39
+ signPayload,
40
+ verifyPayloadSignature,
41
+ verifyWebhookSignature
42
42
  };