@hasna/events 0.1.16 → 0.1.17

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/commander.js CHANGED
@@ -99,16 +99,121 @@ function channelMatchesEvent(channel, event) {
99
99
  // src/storage.ts
100
100
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
101
101
  import { Buffer as Buffer2 } from "buffer";
102
+ import { existsSync as existsSync2 } from "fs";
103
+ import { join as join3 } from "path";
104
+
105
+ // src/app-home.ts
102
106
  import { existsSync } from "fs";
107
+ import { homedir as homedir2 } from "os";
108
+ import { join as join2, resolve } from "path";
109
+
110
+ // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
103
111
  import { homedir } from "os";
104
112
  import { join } from "path";
113
+ var KIND_ENV = {
114
+ config: "HASNA_CONFIG_HOME",
115
+ data: "HASNA_DATA_HOME",
116
+ state: "HASNA_STATE_HOME",
117
+ cache: "HASNA_CACHE_HOME"
118
+ };
119
+ var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
120
+ function assertApp(app) {
121
+ if (typeof app !== "string" || app.length === 0) {
122
+ throw new TypeError("paths: app must be a non-empty string");
123
+ }
124
+ if (!APP_SLUG_RE.test(app)) {
125
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
126
+ }
127
+ }
128
+ function envOf(options) {
129
+ return options.env ?? process.env;
130
+ }
131
+ function envValue(options, kind) {
132
+ const value = envOf(options)[KIND_ENV[kind]];
133
+ return typeof value === "string" && value.length > 0 ? value : undefined;
134
+ }
135
+ function isMacOS(platform) {
136
+ return platform === "darwin";
137
+ }
138
+ function baseDir(kind, options) {
139
+ const override = envValue(options, kind);
140
+ if (override)
141
+ return override;
142
+ const home = options.home ?? homedir();
143
+ const platform = options.platform ?? process.platform;
144
+ if (isMacOS(platform)) {
145
+ switch (kind) {
146
+ case "config":
147
+ case "data":
148
+ return join(home, "Library", "Application Support", "Hasna");
149
+ case "cache":
150
+ return join(home, "Library", "Caches", "Hasna");
151
+ case "state":
152
+ return join(home, "Library", "Logs", "Hasna");
153
+ }
154
+ }
155
+ switch (kind) {
156
+ case "config":
157
+ return join(home, ".config", "hasna");
158
+ case "data":
159
+ return join(home, ".local", "share", "hasna");
160
+ case "state":
161
+ return join(home, ".local", "state", "hasna");
162
+ case "cache":
163
+ return join(home, ".cache", "hasna");
164
+ }
165
+ }
166
+ function resolvePath(kind, options) {
167
+ assertApp(options.app);
168
+ const appSegment = options.internal === true ? join("internal", options.app) : options.app;
169
+ return join(baseDir(kind, options), appSegment);
170
+ }
171
+ function dataDir(options) {
172
+ return resolvePath("data", options);
173
+ }
174
+
175
+ // src/app-home.ts
105
176
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
106
177
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
178
+ var EVENTS_STORE_SENTINEL_FILE = "events.json";
179
+ function effectiveHome() {
180
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
181
+ }
182
+ function legacyHomeDir() {
183
+ return join2(effectiveHome(), ".hasna", "events");
184
+ }
185
+ function resolverHome() {
186
+ return dataDir({ app: "events", home: effectiveHome() || undefined });
187
+ }
188
+ function adoptResolverHome(resolved, env = process.env) {
189
+ const dataOverride = env.HASNA_DATA_HOME;
190
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
191
+ return true;
192
+ return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
193
+ }
194
+ function exactEventsHome() {
195
+ const dir = process.env[HASNA_EVENTS_DIR_ENV];
196
+ if (dir && dir.trim())
197
+ return dir.trim();
198
+ const home = process.env[HASNA_EVENTS_HOME_ENV];
199
+ if (home && home.trim())
200
+ return home.trim();
201
+ return;
202
+ }
203
+ function getEventsHome() {
204
+ const exact = exactEventsHome();
205
+ if (exact)
206
+ return resolve(exact);
207
+ const resolved = resolverHome();
208
+ return adoptResolverHome(resolved) ? resolve(resolved) : resolve(legacyHomeDir());
209
+ }
210
+
211
+ // src/storage.ts
107
212
  var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
108
213
  var DEFAULT_EVENT_PAGE_LIMIT = 100;
109
214
  var MAX_EVENT_PAGE_LIMIT = 1000;
110
215
  function getEventsDataDir(override) {
111
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
216
+ return override || getEventsHome();
112
217
  }
113
218
  function getActiveEventsDirEnv() {
114
219
  if (process.env[HASNA_EVENTS_DIR_ENV])
@@ -124,12 +229,12 @@ class JsonEventsStore {
124
229
  channelsPath;
125
230
  eventsPath;
126
231
  deliveriesPath;
127
- constructor(dataDir = getEventsDataDir()) {
128
- this.dataDir = dataDir;
129
- this.runtime = localJsonRuntime(dataDir);
130
- this.channelsPath = join(dataDir, "channels.json");
131
- this.eventsPath = join(dataDir, "events.json");
132
- this.deliveriesPath = join(dataDir, "deliveries.json");
232
+ constructor(dataDir2 = getEventsDataDir()) {
233
+ this.dataDir = dataDir2;
234
+ this.runtime = localJsonRuntime(dataDir2);
235
+ this.channelsPath = join3(dataDir2, "channels.json");
236
+ this.eventsPath = join3(dataDir2, "events.json");
237
+ this.deliveriesPath = join3(dataDir2, "deliveries.json");
133
238
  }
134
239
  async init() {
135
240
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -246,7 +351,7 @@ class JsonEventsStore {
246
351
  };
247
352
  }
248
353
  async ensureArrayFile(path) {
249
- if (!existsSync(path)) {
354
+ if (!existsSync2(path)) {
250
355
  await writeFile(path, `[]
251
356
  `, { encoding: "utf-8", mode: 384 });
252
357
  }
@@ -276,7 +381,7 @@ class JsonEventsStore {
276
381
  });
277
382
  }
278
383
  }
279
- function localJsonRuntime(dataDir = getEventsDataDir()) {
384
+ function localJsonRuntime(dataDir2 = getEventsDataDir()) {
280
385
  return {
281
386
  mode: "local-files",
282
387
  name: "json-events-store",
@@ -289,7 +394,7 @@ function localJsonRuntime(dataDir = getEventsDataDir()) {
289
394
  durable: true,
290
395
  idempotency: "best-effort-local",
291
396
  replayCursors: true,
292
- description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
397
+ description: `Local JSON files in ${dataDir2}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
293
398
  };
294
399
  }
295
400
  function encodeLocalJsonEventCursor(offset, options = {}) {
@@ -353,8 +458,8 @@ function assertCursorFilter(name, cursorValue, optionValue) {
353
458
  function findEventByIdentity(events, identity) {
354
459
  return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
355
460
  }
356
- async function getEventsStatus(dataDir) {
357
- const store = new JsonEventsStore(dataDir);
461
+ async function getEventsStatus(dataDir2) {
462
+ const store = new JsonEventsStore(dataDir2);
358
463
  await store.init();
359
464
  const [channels, events, deliveries] = await Promise.all([
360
465
  store.listChannels(),
@@ -396,9 +501,9 @@ async function getEventsStatus(dataDir) {
396
501
  }
397
502
  };
398
503
  }
399
- function statusFile(dataDir, fileName, records) {
400
- const path = join(dataDir, fileName);
401
- return { path, exists: existsSync(path), records };
504
+ function statusFile(dataDir2, fileName, records) {
505
+ const path = join3(dataDir2, fileName);
506
+ return { path, exists: existsSync2(path), records };
402
507
  }
403
508
 
404
509
  // src/signing.ts
@@ -435,9 +540,226 @@ function verifyWebhookSignature(secret, timestamp, body, signature, options = {}
435
540
  return verifyPayloadSignature(secret, timestamp, body, signature);
436
541
  }
437
542
 
543
+ // src/ssrf.ts
544
+ import { lookup as dnsLookup } from "dns/promises";
545
+ import { isIP } from "net";
546
+ var DEFAULT_MAX_REDIRECTS = 5;
547
+ var IPV4_PRIVATE_RANGES = [
548
+ [0, 16777215],
549
+ [167772160, 184549375],
550
+ [1681915904, 1686110207],
551
+ [2130706432, 2147483647],
552
+ [2851995648, 2852061183],
553
+ [2886729728, 2887778303],
554
+ [3221225472, 3221225727],
555
+ [3221225984, 3221226239],
556
+ [3227017984, 3227018239],
557
+ [3232235520, 3232301055],
558
+ [3323068416, 3323199487],
559
+ [3325256704, 3325256959],
560
+ [3405803776, 3405804031],
561
+ [3758096384, 4294967295]
562
+ ];
563
+ var IPV6_SPECIAL_PREFIXES = [
564
+ { groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
565
+ { groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
566
+ { groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
567
+ { groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
568
+ { groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
569
+ { groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
570
+ { groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
571
+ { groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
572
+ { groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
573
+ { groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
574
+ { groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
575
+ { groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
576
+ { groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
577
+ { groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
578
+ { groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
579
+ ];
580
+ function isPrivateAddress(address) {
581
+ const normalized = stripZoneId(address);
582
+ const version = isIP(normalized);
583
+ if (version === 4) {
584
+ const integer = ipv4ToInt(normalized);
585
+ if (integer === undefined)
586
+ return true;
587
+ return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
588
+ }
589
+ if (version === 6) {
590
+ const groups = ipv6Groups(normalized);
591
+ if (!groups)
592
+ return true;
593
+ for (const prefix of IPV6_SPECIAL_PREFIXES) {
594
+ if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
595
+ continue;
596
+ if (prefix.bits === 96 && groups[5] === 65535) {
597
+ return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
598
+ }
599
+ if (prefix.bits === 16 && groups[0] === 8194) {
600
+ return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
601
+ }
602
+ return true;
603
+ }
604
+ return false;
605
+ }
606
+ return true;
607
+ }
608
+ async function resolveWebhookTarget(url, policy = {}) {
609
+ const hostname = normalizeHostname(url.hostname);
610
+ const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
611
+ if (allowlist.includes(hostname)) {
612
+ const version2 = isIP(hostname);
613
+ if (version2 === 4 || version2 === 6) {
614
+ return { hostname, addresses: [hostname] };
615
+ }
616
+ const lookup2 = policy.lookup ?? defaultTargetLookup;
617
+ let resolved2;
618
+ try {
619
+ resolved2 = await lookup2(hostname);
620
+ } catch {
621
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
622
+ }
623
+ if (!Array.isArray(resolved2) || resolved2.length === 0) {
624
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
625
+ }
626
+ const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
627
+ return { hostname, addresses };
628
+ }
629
+ const version = isIP(hostname);
630
+ if (version === 4 || version === 6) {
631
+ if (isPrivateAddress(hostname)) {
632
+ throw new Error(`Webhook target ${hostname} is a private or special-use address`);
633
+ }
634
+ return { hostname, addresses: [hostname] };
635
+ }
636
+ const lookup = policy.lookup ?? defaultTargetLookup;
637
+ let resolved;
638
+ try {
639
+ resolved = await lookup(hostname);
640
+ } catch {
641
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
642
+ }
643
+ if (!Array.isArray(resolved) || resolved.length === 0) {
644
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
645
+ }
646
+ const allowed = [];
647
+ for (const entry of resolved) {
648
+ const address = normalizeHostname(entry.address);
649
+ if (isPrivateAddress(address)) {
650
+ if (allowlist.includes(address)) {
651
+ allowed.push(address);
652
+ continue;
653
+ }
654
+ throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
655
+ }
656
+ allowed.push(address);
657
+ }
658
+ if (allowed.length === 0) {
659
+ throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
660
+ }
661
+ return { hostname, addresses: allowed };
662
+ }
663
+ async function assertWebhookTargetAllowed(url, policy = {}) {
664
+ await resolveWebhookTarget(url, policy);
665
+ }
666
+ function normalizeMaxRedirects(value) {
667
+ if (value === undefined)
668
+ return DEFAULT_MAX_REDIRECTS;
669
+ if (!Number.isInteger(value) || value < 0)
670
+ throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
671
+ return value;
672
+ }
673
+ var defaultTargetLookup = async (hostname) => {
674
+ return dnsLookup(hostname, { all: true, verbatim: false });
675
+ };
676
+ function normalizeHostname(hostname) {
677
+ const lower = hostname.toLowerCase();
678
+ if (lower.startsWith("[") && lower.endsWith("]"))
679
+ return lower.slice(1, -1);
680
+ return lower;
681
+ }
682
+ function stripZoneId(address) {
683
+ const percent = address.indexOf("%");
684
+ return percent === -1 ? address : address.slice(0, percent);
685
+ }
686
+ function ipv4ToInt(address) {
687
+ const parts = address.split(".");
688
+ if (parts.length !== 4)
689
+ return;
690
+ let value = 0;
691
+ for (const part of parts) {
692
+ if (!/^\d{1,3}$/.test(part))
693
+ return;
694
+ const octet = Number(part);
695
+ if (octet > 255)
696
+ return;
697
+ value = value << 8 | octet;
698
+ }
699
+ return value >>> 0;
700
+ }
701
+ function ipv4IntToString(integer) {
702
+ return [
703
+ integer >>> 24 & 255,
704
+ integer >>> 16 & 255,
705
+ integer >>> 8 & 255,
706
+ integer & 255
707
+ ].join(".");
708
+ }
709
+ function ipv6Groups(address) {
710
+ const raw = stripZoneId(address);
711
+ const doubleColon = raw.indexOf("::");
712
+ const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
713
+ const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
714
+ const parseGroups = (text) => {
715
+ if (text === "")
716
+ return [];
717
+ const out = [];
718
+ for (const part of text.split(":")) {
719
+ if (part.includes(".")) {
720
+ const v4 = ipv4ToInt(part);
721
+ if (v4 === undefined)
722
+ return;
723
+ out.push(v4 >>> 16 & 65535, v4 & 65535);
724
+ } else {
725
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part))
726
+ return;
727
+ out.push(parseInt(part, 16));
728
+ }
729
+ }
730
+ return out;
731
+ };
732
+ const head = parseGroups(headText);
733
+ if (!head)
734
+ return;
735
+ const tail = parseGroups(tailText);
736
+ if (!tail)
737
+ return;
738
+ const total = head.length + tail.length;
739
+ if (doubleColon === -1) {
740
+ return total === 8 ? head : undefined;
741
+ }
742
+ if (total >= 8)
743
+ return;
744
+ return [...head, ...new Array(8 - total).fill(0), ...tail];
745
+ }
746
+ function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
747
+ let remaining = prefixBits;
748
+ for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
749
+ const take = Math.min(16, remaining);
750
+ const mask = 65535 << 16 - take & 65535;
751
+ if ((groups[index] & mask) !== (prefixGroups[index] & mask))
752
+ return false;
753
+ remaining -= take;
754
+ }
755
+ return true;
756
+ }
757
+
438
758
  // src/transports.ts
439
759
  import { randomUUID } from "crypto";
440
760
  import { spawn } from "child_process";
761
+ import { request as nodeHttpRequest } from "http";
762
+ import { request as nodeHttpsRequest } from "https";
441
763
  function now() {
442
764
  return new Date().toISOString();
443
765
  }
@@ -468,9 +790,18 @@ function buildWebhookRequest(event, channel, options = {}) {
468
790
  }
469
791
  return { body, headers };
470
792
  }
793
+ function normalizeWebhookUrl(raw) {
794
+ const url = new URL(raw);
795
+ if (url.username !== "" || url.password !== "") {
796
+ url.username = "";
797
+ url.password = "";
798
+ }
799
+ return url.toString();
800
+ }
471
801
  async function dispatchWebhook(event, channel, options = {}) {
472
802
  if (!channel.webhook)
473
803
  throw new Error(`Channel ${channel.id} has no webhook config`);
804
+ const webhookUrl = normalizeWebhookUrl(channel.webhook.url);
474
805
  const startedAt = now();
475
806
  let secret = channel.webhook.secret;
476
807
  if (channel.webhook.secretRef) {
@@ -487,10 +818,14 @@ async function dispatchWebhook(event, channel, options = {}) {
487
818
  }
488
819
  const timestamp = (options.now?.() ?? new Date).toISOString();
489
820
  const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
821
+ const validateTargets = options.webhookTargetPolicy !== undefined || options.fetchImpl === undefined;
822
+ if (validateTargets) {
823
+ return dispatchValidatedWebhook(event, channel, { body, headers, startedAt, options });
824
+ }
490
825
  const controller = new AbortController;
491
826
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
492
827
  try {
493
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
828
+ const response = await (options.fetchImpl ?? fetch)(webhookUrl, {
494
829
  method: "POST",
495
830
  headers,
496
831
  body,
@@ -518,6 +853,130 @@ async function dispatchWebhook(event, channel, options = {}) {
518
853
  clearTimeout(timeout);
519
854
  }
520
855
  }
856
+ function isRedirectStatus(status) {
857
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
858
+ }
859
+ function redirectKeepsBody(status) {
860
+ return status === 307 || status === 308;
861
+ }
862
+ async function pinnedNativeRequest(target, addresses, method, headers, body, signal, tls) {
863
+ const isHttps = target.protocol === "https:";
864
+ if (!isHttps && target.protocol !== "http:") {
865
+ throw new Error(`Webhook target uses unsupported protocol ${target.protocol}`);
866
+ }
867
+ const defaultPort = isHttps ? 443 : 80;
868
+ const port = target.port ? Number(target.port) : defaultPort;
869
+ const requestOptions = {
870
+ hostname: target.hostname,
871
+ port,
872
+ path: `${target.pathname}${target.search}`,
873
+ method,
874
+ headers,
875
+ ...tls?.ca ? { ca: tls.ca } : {},
876
+ lookup: (hostname, _options, callback) => {
877
+ const entries = addresses.map((address) => ({
878
+ address,
879
+ family: address.includes(":") ? 6 : 4
880
+ }));
881
+ callback(null, entries);
882
+ }
883
+ };
884
+ return new Promise((resolve2, reject) => {
885
+ const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
886
+ const onAbort = () => {
887
+ const error = new Error("The operation was aborted.");
888
+ error.name = "AbortError";
889
+ request.destroy(error);
890
+ };
891
+ if (signal.aborted)
892
+ onAbort();
893
+ else
894
+ signal.addEventListener("abort", onAbort, { once: true });
895
+ request.on("error", reject);
896
+ if (body !== undefined)
897
+ request.write(body);
898
+ request.end();
899
+ function onResponse(response) {
900
+ const chunks = [];
901
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
902
+ response.on("error", reject);
903
+ response.on("end", () => {
904
+ const headersRecord = {};
905
+ for (const [name, value] of Object.entries(response.headers)) {
906
+ if (typeof value === "string")
907
+ headersRecord[name] = value;
908
+ else if (Array.isArray(value))
909
+ headersRecord[name] = value.join(", ");
910
+ }
911
+ resolve2(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
912
+ });
913
+ }
914
+ });
915
+ }
916
+ async function dispatchValidatedWebhook(event, channel, input) {
917
+ const { body, headers, startedAt, options } = input;
918
+ const webhook = channel.webhook;
919
+ if (!webhook)
920
+ throw new Error(`Channel ${channel.id} has no webhook config`);
921
+ const policy = options.webhookTargetPolicy ?? {};
922
+ const maxRedirects = normalizeMaxRedirects(policy.maxRedirects);
923
+ const controller = new AbortController;
924
+ const timeout = setTimeout(() => controller.abort(), webhook.timeoutMs ?? 15000);
925
+ try {
926
+ let target = new URL(normalizeWebhookUrl(webhook.url));
927
+ let requestHeaders = headers;
928
+ let method = "POST";
929
+ let requestBody = body;
930
+ let redirectsFollowed = 0;
931
+ for (;; ) {
932
+ const resolved = await resolveWebhookTarget(target, policy).catch((error) => {
933
+ throw new Error(`Webhook target rejected by SSRF guard: ${error.message}`);
934
+ });
935
+ const response = options.fetchImpl ? await options.fetchImpl(target, {
936
+ method,
937
+ headers: requestHeaders,
938
+ body: requestBody,
939
+ signal: controller.signal,
940
+ redirect: "manual"
941
+ }) : await pinnedNativeRequest(target, resolved.addresses, method, requestHeaders, requestBody, controller.signal, options.tls);
942
+ const location = response.headers.get("location");
943
+ if (isRedirectStatus(response.status) && location) {
944
+ if (redirectsFollowed >= maxRedirects) {
945
+ return failedAttempt(startedAt, `Webhook target exceeded ${maxRedirects} redirects`);
946
+ }
947
+ redirectsFollowed += 1;
948
+ const next = new URL(location, target);
949
+ target = next;
950
+ if (!redirectKeepsBody(response.status)) {
951
+ method = "GET";
952
+ requestBody = undefined;
953
+ requestHeaders = Object.fromEntries(Object.entries(requestHeaders).filter(([name]) => name.toLowerCase() !== "content-type" && name.toLowerCase() !== "content-length"));
954
+ }
955
+ continue;
956
+ }
957
+ const responseBody = truncate(await response.text());
958
+ return {
959
+ attempt: 1,
960
+ status: response.ok ? "success" : "failed",
961
+ startedAt,
962
+ completedAt: now(),
963
+ responseStatus: response.status,
964
+ responseBody,
965
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
966
+ };
967
+ }
968
+ } catch (error) {
969
+ return {
970
+ attempt: 1,
971
+ status: "failed",
972
+ startedAt,
973
+ completedAt: now(),
974
+ error: error instanceof Error ? error.message : String(error)
975
+ };
976
+ } finally {
977
+ clearTimeout(timeout);
978
+ }
979
+ }
521
980
  function failedAttempt(startedAt, error) {
522
981
  return {
523
982
  attempt: 1,
@@ -546,7 +1005,7 @@ async function dispatchCommand(event, channel) {
546
1005
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
547
1006
  HASNA_EVENT_JSON: eventJson
548
1007
  };
549
- return new Promise((resolve) => {
1008
+ return new Promise((resolve2) => {
550
1009
  const child = spawn(channel.command.command, channel.command.args ?? [], {
551
1010
  cwd: channel.command.cwd,
552
1011
  env,
@@ -564,7 +1023,7 @@ async function dispatchCommand(event, channel) {
564
1023
  });
565
1024
  child.on("error", (error) => {
566
1025
  clearTimeout(timeout);
567
- resolve({
1026
+ resolve2({
568
1027
  attempt: 1,
569
1028
  status: "failed",
570
1029
  startedAt,
@@ -577,7 +1036,7 @@ async function dispatchCommand(event, channel) {
577
1036
  child.on("close", (code, signal) => {
578
1037
  clearTimeout(timeout);
579
1038
  const success = code === 0;
580
- resolve({
1039
+ resolve2({
581
1040
  attempt: 1,
582
1041
  status: success ? "success" : "failed",
583
1042
  startedAt,
@@ -1229,7 +1688,9 @@ class EventsClient {
1229
1688
  this.transportOptions = {
1230
1689
  fetchImpl: options.fetchImpl,
1231
1690
  secretResolver: options.secretResolver,
1232
- now: options.now
1691
+ now: options.now,
1692
+ tls: options.tls,
1693
+ webhookTargetPolicy: options.webhookTargetPolicy
1233
1694
  };
1234
1695
  this.catalog = options.catalog ?? defaultEventTypeCatalog;
1235
1696
  this.validateCatalogTypes = options.validateCatalogTypes ?? false;
@@ -1715,8 +2176,8 @@ function replaySummary(events, deliveries, nextCursor) {
1715
2176
  return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
1716
2177
  }
1717
2178
  export {
1718
- DEFAULT_EVENT_LIST_LIMIT,
1719
- registerChannelCommands,
2179
+ registerEventsCommands,
1720
2180
  registerEventCommands,
1721
- registerEventsCommands
2181
+ registerChannelCommands,
2182
+ DEFAULT_EVENT_LIST_LIMIT
1722
2183
  };