@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/LICENSE +198 -13
- package/README.md +184 -28
- package/dist/app-event.js +382 -0
- package/dist/catalog.js +12 -12
- package/dist/cli/index.js +1415 -80
- package/dist/commander.js +452 -49
- package/dist/durable-spool.js +184 -0
- package/dist/durable-worker.js +378 -0
- package/dist/durable.js +2232 -0
- package/dist/filter.js +2 -2
- package/dist/index.js +508 -91
- package/dist/signing.js +5 -5
- package/dist/storage.js +12 -12
- package/dist/transports.js +40 -11
- package/fixtures/hasna.app_event.v1.json +108 -0
- package/hasna.contract.json +70 -0
- package/package.json +43 -13
- package/schemas/hasna.app_event.v1.json +186 -0
- package/types/app-event.d.ts +130 -0
- package/types/durable-spool.d.ts +30 -0
- package/types/durable-worker.d.ts +27 -0
- package/types/durable.d.ts +112 -0
- package/{dist → types}/index.d.ts +2 -2
- package/types/redaction.d.ts +4 -0
- package/{dist → types}/transports.d.ts +8 -1
- package/{dist → types}/types.d.ts +9 -0
- /package/{dist → types}/catalog.d.ts +0 -0
- /package/{dist → types}/cli/index.d.ts +0 -0
- /package/{dist → types}/commander.d.ts +0 -0
- /package/{dist → types}/filter-options.d.ts +0 -0
- /package/{dist → types}/filter.d.ts +0 -0
- /package/{dist → types}/signing.d.ts +0 -0
- /package/{dist → types}/storage.d.ts +0 -0
package/dist/commander.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 =
|
|
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
|
-
|
|
458
|
-
|
|
462
|
+
...channel.webhook.headers,
|
|
463
|
+
"X-Hasna-Timestamp": timestamp
|
|
459
464
|
};
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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 = {
|
|
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)
|
|
@@ -1312,8 +1715,8 @@ function replaySummary(events, deliveries, nextCursor) {
|
|
|
1312
1715
|
return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
|
|
1313
1716
|
}
|
|
1314
1717
|
export {
|
|
1315
|
-
|
|
1316
|
-
registerEventCommands,
|
|
1718
|
+
DEFAULT_EVENT_LIST_LIMIT,
|
|
1317
1719
|
registerChannelCommands,
|
|
1318
|
-
|
|
1720
|
+
registerEventCommands,
|
|
1721
|
+
registerEventsCommands
|
|
1319
1722
|
};
|