@nile-squad/nylonpay-ts 1.0.7 → 1.0.9

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/index.js CHANGED
@@ -2,7 +2,7 @@ import { createHash, createHmac, timingSafeEqual, randomBytes } from 'crypto';
2
2
  import { Ok, Err, safeTry } from 'slang-ts';
3
3
  import { type, platform, arch, release, hostname } from 'os';
4
4
 
5
- // src/sdk.ts
5
+ // src/create-nylon-pay.ts
6
6
 
7
7
  // src/pubsub.ts
8
8
  function createEmitter() {
@@ -61,9 +61,7 @@ var DEFAULT_MAX_RETRIES = 3;
61
61
  var DEFAULT_MAX_POLL_INTERVAL_MS = 2e3;
62
62
  var DEFAULT_MAX_POLL_DURATION_MS = 3e5;
63
63
  var DEFAULT_MAX_POLL_ATTEMPTS = 150;
64
- var DEFAULT_STREAMING = true;
65
- var STREAM_PATH = "/sse/transaction";
66
- var MAX_STREAM_RECONNECTS = 2;
64
+ var POLL_JITTER_MS = 250;
67
65
  var SDK_SERVICE = "sdk";
68
66
  var SDK_ACTIONS = {
69
67
  collectPayment: "sdk-collect-payment",
@@ -91,13 +89,22 @@ function generateFingerprint() {
91
89
  function generateNonce(length = 16) {
92
90
  return randomBytes(length).toString("hex");
93
91
  }
92
+ function compareByCodePoint(first, second) {
93
+ if (first < second) {
94
+ return -1;
95
+ }
96
+ if (first > second) {
97
+ return 1;
98
+ }
99
+ return 0;
100
+ }
94
101
  function sortValue(value) {
95
102
  if (Array.isArray(value)) {
96
103
  return value.map((entry) => sortValue(entry));
97
104
  }
98
105
  if (value && typeof value === "object") {
99
106
  const sortedEntries = Object.entries(value).sort(
100
- ([firstKey], [secondKey]) => firstKey.localeCompare(secondKey)
107
+ ([firstKey], [secondKey]) => compareByCodePoint(firstKey, secondKey)
101
108
  );
102
109
  return Object.fromEntries(
103
110
  sortedEntries.map(([entryKey, entryValue]) => [
@@ -121,42 +128,6 @@ function createSignature(input) {
121
128
  function createTimestamp() {
122
129
  return Date.now().toString();
123
130
  }
124
-
125
- // src/sse-parse.ts
126
- function parseBlock(block) {
127
- let event = "message";
128
- const dataLines = [];
129
- for (const rawLine of block.split("\n")) {
130
- const line = rawLine.replace(/\r$/, "");
131
- if (line.startsWith(":")) {
132
- continue;
133
- }
134
- if (line.startsWith("event:")) {
135
- event = line.slice("event:".length).trim();
136
- } else if (line.startsWith("data:")) {
137
- dataLines.push(line.slice("data:".length).trim());
138
- }
139
- }
140
- if (dataLines.length === 0) {
141
- return null;
142
- }
143
- return { event, data: dataLines.join("\n") };
144
- }
145
- function parseSseBuffer(buffer) {
146
- const messages = [];
147
- let rest = buffer;
148
- let separator = rest.indexOf("\n\n");
149
- while (separator !== -1) {
150
- const block = rest.slice(0, separator);
151
- rest = rest.slice(separator + 2);
152
- const message = parseBlock(block);
153
- if (message) {
154
- messages.push(message);
155
- }
156
- separator = rest.indexOf("\n\n");
157
- }
158
- return { messages, rest };
159
- }
160
131
  function verifyResponseSignature(data, signature, secret) {
161
132
  const expectedSignature = createHmac("sha256", secret).update(createCanonicalPayload(data)).digest("hex");
162
133
  const providedBuffer = Buffer.from(signature, "hex");
@@ -169,13 +140,6 @@ function verifyResponseSignature(data, signature, secret) {
169
140
 
170
141
  // src/transport.ts
171
142
  var CACHED_FINGERPRINT = generateFingerprint();
172
- function streamUrl(baseUrl) {
173
- try {
174
- return new URL(baseUrl).origin + STREAM_PATH;
175
- } catch {
176
- return baseUrl.replace(/\/api\/services\/?$/u, "") + STREAM_PATH;
177
- }
178
- }
179
143
  var KNOWN_CATEGORIES = /* @__PURE__ */ new Set([
180
144
  "auth",
181
145
  "validation",
@@ -341,22 +305,30 @@ function createTransport({
341
305
  const { status, message, data } = responseBody;
342
306
  if (status === true) {
343
307
  const { data: strippedData, responseSignature } = stripResponseSignature(data);
344
- if (responseSignature) {
345
- const isValid = verifyResponseSignature(
346
- strippedData,
347
- responseSignature,
348
- apiSecret
308
+ if (!responseSignature) {
309
+ cleanup();
310
+ return Err(
311
+ JSON.stringify({
312
+ category: "internal",
313
+ message: "Response signature missing",
314
+ retryable: false
315
+ })
316
+ );
317
+ }
318
+ const isValid = verifyResponseSignature(
319
+ strippedData,
320
+ responseSignature,
321
+ apiSecret
322
+ );
323
+ if (!isValid) {
324
+ cleanup();
325
+ return Err(
326
+ JSON.stringify({
327
+ category: "internal",
328
+ message: "Response signature verification failed",
329
+ retryable: false
330
+ })
349
331
  );
350
- if (!isValid) {
351
- cleanup();
352
- return Err(
353
- JSON.stringify({
354
- category: "internal",
355
- message: "Response signature verification failed",
356
- retryable: false
357
- })
358
- );
359
- }
360
332
  }
361
333
  cleanup();
362
334
  return Ok(strippedData);
@@ -381,91 +353,7 @@ function createTransport({
381
353
  }
382
354
  return attempt(0);
383
355
  }
384
- function openStream(input) {
385
- const controller = new AbortController();
386
- let closed = false;
387
- const close = () => {
388
- if (closed) {
389
- return;
390
- }
391
- closed = true;
392
- controller.abort();
393
- };
394
- const payload = {
395
- reference: input.reference,
396
- _fingerprint: CACHED_FINGERPRINT
397
- };
398
- const headers = buildAuthHeaders({ apiKey, apiSecret, payload });
399
- const url = streamUrl(baseUrl);
400
- const run = async () => {
401
- const fetchResult = await safeTry(
402
- () => fetchImpl(url, {
403
- method: "POST",
404
- headers,
405
- body: JSON.stringify(payload),
406
- signal: controller.signal
407
- })
408
- );
409
- if (fetchResult.isErr) {
410
- if (!closed) {
411
- input.onError(fetchResult.error);
412
- }
413
- return;
414
- }
415
- const response = fetchResult.value;
416
- if (!response.ok || !response.body) {
417
- const textResult = await safeTry(() => response.text());
418
- const message = textResult.isOk ? textResult.value : `HTTP ${response.status}`;
419
- if (!closed) {
420
- input.onError(message || `HTTP ${response.status}`);
421
- }
422
- return;
423
- }
424
- const reader = response.body.getReader();
425
- const decoder = new TextDecoder();
426
- let buffer = "";
427
- while (!closed) {
428
- const chunkResult = await safeTry(() => reader.read());
429
- if (chunkResult.isErr) {
430
- if (!closed) {
431
- input.onError(chunkResult.error);
432
- }
433
- return;
434
- }
435
- const chunk = chunkResult.value;
436
- if (chunk.done) {
437
- break;
438
- }
439
- buffer += decoder.decode(chunk.value, { stream: true });
440
- const { messages, rest } = parseSseBuffer(buffer);
441
- buffer = rest;
442
- for (const message of messages) {
443
- if (message.event === "status") {
444
- const statusResult = await safeTry(
445
- () => JSON.parse(message.data)
446
- );
447
- if (statusResult.isOk) {
448
- input.onStatus(statusResult.value);
449
- }
450
- } else if (message.event === "error") {
451
- const errDataResult = await safeTry(
452
- () => JSON.parse(message.data)
453
- );
454
- const text = errDataResult.isOk ? errDataResult.value.message ?? message.data : message.data;
455
- close();
456
- input.onError(text);
457
- return;
458
- }
459
- }
460
- }
461
- if (!closed) {
462
- input.onClose();
463
- }
464
- };
465
- void run();
466
- return { close };
467
- }
468
- return { send, openStream, parseError };
356
+ return { send, parseError };
469
357
  }
470
358
  function parseError(error) {
471
359
  try {
@@ -515,11 +403,7 @@ function createPaymentInstance(initialResponse, deps) {
515
403
  fetchTransaction: deps.fetchTransaction,
516
404
  pollIntervalMs: deps.pollIntervalMs ?? 2e3,
517
405
  maxPollDuration: deps.maxPollDuration ?? 3e5,
518
- maxPollAttempts: deps.maxPollAttempts ?? 150,
519
- streaming: deps.streaming ?? false,
520
- openStream: deps.openStream,
521
- streamHandle: null,
522
- streamReconnects: 0
406
+ maxPollAttempts: deps.maxPollAttempts ?? 150
523
407
  };
524
408
  function resolveWithError(error) {
525
409
  state.resolved = true;
@@ -555,6 +439,9 @@ function createPaymentInstance(initialResponse, deps) {
555
439
  stopUpdates();
556
440
  }
557
441
  async function handleStatusUpdate(response) {
442
+ if (state.resolved) {
443
+ return;
444
+ }
558
445
  if (response.reference !== state.reference) {
559
446
  resolveWithError(
560
447
  `Reference mismatch: expected ${state.reference} but got ${response.reference}`
@@ -588,10 +475,11 @@ function createPaymentInstance(initialResponse, deps) {
588
475
  if (state.resolved || state.pollingTimer) {
589
476
  return;
590
477
  }
478
+ const delay2 = state.pollIntervalMs + Math.random() * POLL_JITTER_MS;
591
479
  state.pollingTimer = setTimeout(() => {
592
480
  state.pollingTimer = null;
593
481
  void pollStatus();
594
- }, state.pollIntervalMs);
482
+ }, delay2);
595
483
  }
596
484
  async function pollStatus() {
597
485
  if (state.resolved) {
@@ -619,42 +507,6 @@ function createPaymentInstance(initialResponse, deps) {
619
507
  }
620
508
  scheduleNextPoll();
621
509
  }
622
- function closeStream() {
623
- if (state.streamHandle) {
624
- state.streamHandle.close();
625
- state.streamHandle = null;
626
- }
627
- }
628
- function startStream() {
629
- if (state.resolved || !state.openStream) {
630
- return;
631
- }
632
- state.streamHandle = state.openStream({
633
- reference: state.reference,
634
- onStatus: (status) => {
635
- void handleStatusUpdate(status);
636
- },
637
- onError: () => handleStreamFailure(),
638
- onClose: () => handleStreamFailure()
639
- });
640
- }
641
- function handleStreamFailure() {
642
- closeStream();
643
- if (state.resolved) {
644
- return;
645
- }
646
- if (state.streamReconnects < MAX_STREAM_RECONNECTS) {
647
- state.streamReconnects += 1;
648
- const backoff = 500 * 2 ** (state.streamReconnects - 1) + Math.random() * 250;
649
- setTimeout(() => {
650
- if (!state.resolved) {
651
- startStream();
652
- }
653
- }, backoff);
654
- return;
655
- }
656
- scheduleNextPoll();
657
- }
658
510
  function startUpdates() {
659
511
  if (TERMINAL_STATES.has(state.status)) {
660
512
  setTimeout(() => {
@@ -662,10 +514,6 @@ function createPaymentInstance(initialResponse, deps) {
662
514
  }, 0);
663
515
  return;
664
516
  }
665
- if (state.streaming && state.openStream) {
666
- startStream();
667
- return;
668
- }
669
517
  scheduleNextPoll();
670
518
  }
671
519
  function stopUpdates() {
@@ -673,7 +521,6 @@ function createPaymentInstance(initialResponse, deps) {
673
521
  clearTimeout(state.pollingTimer);
674
522
  state.pollingTimer = null;
675
523
  }
676
- closeStream();
677
524
  }
678
525
  function on(event, handler) {
679
526
  state.emitter.on(event, handler);
@@ -746,21 +593,78 @@ function createPaymentInstance(initialResponse, deps) {
746
593
  }
747
594
  return paymentInstance;
748
595
  }
596
+ var DEFAULT_TOLERANCE_SECONDS = 300;
597
+ function decodePayload(payload) {
598
+ return typeof payload === "string" ? payload : Buffer.from(payload).toString("utf8");
599
+ }
600
+ function extractSignedTimestampMs(payloadString) {
601
+ let parsed;
602
+ try {
603
+ parsed = JSON.parse(payloadString);
604
+ } catch {
605
+ return null;
606
+ }
607
+ if (!parsed || typeof parsed !== "object") {
608
+ return null;
609
+ }
610
+ const raw = parsed.timestamp;
611
+ if (typeof raw === "number" && Number.isFinite(raw)) {
612
+ return raw < 1e12 ? raw * 1e3 : raw;
613
+ }
614
+ if (typeof raw === "string") {
615
+ const ms = Date.parse(raw);
616
+ return Number.isNaN(ms) ? null : ms;
617
+ }
618
+ return null;
619
+ }
749
620
  function verifyWebhookSignature(input) {
750
- const payloadBytes = typeof input.payload === "string" ? Buffer.from(input.payload, "utf8") : Buffer.from(input.payload);
621
+ const payloadString = decodePayload(input.payload);
622
+ const payloadBytes = Buffer.from(payloadString, "utf8");
751
623
  const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
752
624
  const providedBuffer = Buffer.from(input.signature, "hex");
753
625
  const expectedBuffer = Buffer.from(expectedSignature, "hex");
754
626
  if (providedBuffer.length !== expectedBuffer.length) {
755
627
  return false;
756
628
  }
757
- return timingSafeEqual(providedBuffer, expectedBuffer);
629
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
630
+ return false;
631
+ }
632
+ const toleranceSeconds = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
633
+ if (toleranceSeconds <= 0) {
634
+ return true;
635
+ }
636
+ const timestampMs = extractSignedTimestampMs(payloadString);
637
+ if (timestampMs === null) {
638
+ return false;
639
+ }
640
+ const ageMs = Math.abs(Date.now() - timestampMs);
641
+ return ageMs <= toleranceSeconds * 1e3;
758
642
  }
759
643
 
760
644
  // src/sdk.ts
761
645
  function generateReference() {
762
646
  return randomBytes(16).toString("hex").slice(0, 15);
763
647
  }
648
+ var REFERENCE_MIN_LENGTH = 13;
649
+ var REFERENCE_MAX_LENGTH = 15;
650
+ function resolveReference(reference) {
651
+ if (reference === void 0) {
652
+ return generateReference();
653
+ }
654
+ if (reference.length < REFERENCE_MIN_LENGTH || reference.length > REFERENCE_MAX_LENGTH) {
655
+ throwValidation(
656
+ `reference must be ${REFERENCE_MIN_LENGTH}\u2013${REFERENCE_MAX_LENGTH} characters`
657
+ );
658
+ }
659
+ return reference;
660
+ }
661
+ async function runHook(hook, ...args) {
662
+ if (!hook || hook.enabled === false) return void 0;
663
+ const result = await safeTry(async () => hook.fn(...args));
664
+ if (result.isOk) return result.value;
665
+ await safeTry(async () => hook.onError(result.error));
666
+ return void 0;
667
+ }
764
668
  function throwValidation(message) {
765
669
  throw createSdkError({ category: "validation", message });
766
670
  }
@@ -794,12 +698,10 @@ function createSdkInstance(config) {
794
698
  }),
795
699
  pollIntervalMs: config.maxPollIntervalMs,
796
700
  maxPollDuration: config.maxPollDurationMs,
797
- maxPollAttempts: config.maxPollAttempts,
798
- streaming: config.streaming,
799
- openStream: config.streaming ? transport.openStream : void 0
701
+ maxPollAttempts: config.maxPollAttempts
800
702
  };
801
703
  async function collectPayment(input) {
802
- const reference = input.reference ?? generateReference();
704
+ const reference = resolveReference(input.reference);
803
705
  validateAmount(input.amount);
804
706
  validateNonEmpty(input.customer.name, "customer.name");
805
707
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
@@ -808,24 +710,18 @@ function createSdkInstance(config) {
808
710
  throwValidation('bank details are required when method is "bank"');
809
711
  }
810
712
  let payload = { ...input, reference };
811
- if (config.hooks?.beforeCollect) {
812
- const mutated = await config.hooks.beforeCollect(payload);
813
- if (mutated != null)
814
- payload = { ...mutated, reference: mutated.reference ?? reference };
815
- }
713
+ const mutated = await runHook(config.hooks?.beforeCollect, payload);
714
+ if (mutated != null)
715
+ payload = { ...mutated, reference: mutated.reference ?? reference };
816
716
  const result = await transport.send({
817
717
  action: SDK_ACTIONS.collectPayment,
818
718
  payload
819
719
  });
820
- if (config.hooks?.afterCollect) {
821
- await config.hooks.afterCollect(
822
- result.isOk ? Ok({
823
- reference: result.value.reference,
824
- status: result.value.status
825
- }) : Err(result.error),
826
- payload
827
- );
828
- }
720
+ await runHook(
721
+ config.hooks?.afterCollect,
722
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
723
+ payload
724
+ );
829
725
  if (result.isErr) {
830
726
  const sdkErr = parseError(result.error);
831
727
  return createPaymentInstance(
@@ -836,7 +732,7 @@ function createSdkInstance(config) {
836
732
  return createPaymentInstance(result.value, commonDeps);
837
733
  }
838
734
  async function collectPaymentAndResolve(input) {
839
- const reference = input.reference ?? generateReference();
735
+ const reference = resolveReference(input.reference);
840
736
  validateAmount(input.amount);
841
737
  validateNonEmpty(input.customer.name, "customer.name");
842
738
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
@@ -845,31 +741,25 @@ function createSdkInstance(config) {
845
741
  throwValidation('bank details are required when method is "bank"');
846
742
  }
847
743
  let payload = { ...input, reference };
848
- if (config.hooks?.beforeCollect) {
849
- const mutated = await config.hooks.beforeCollect(payload);
850
- if (mutated != null)
851
- payload = { ...mutated, reference: mutated.reference ?? reference };
852
- }
744
+ const mutated = await runHook(config.hooks?.beforeCollect, payload);
745
+ if (mutated != null)
746
+ payload = { ...mutated, reference: mutated.reference ?? reference };
853
747
  const result = await transport.send({
854
748
  action: SDK_ACTIONS.collectPaymentAndResolve,
855
749
  payload
856
750
  });
857
- if (config.hooks?.afterCollect) {
858
- await config.hooks.afterCollect(
859
- result.isOk ? Ok({
860
- reference: result.value.reference,
861
- status: result.value.status
862
- }) : Err(result.error),
863
- payload
864
- );
865
- }
751
+ await runHook(
752
+ config.hooks?.afterCollect,
753
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
754
+ payload
755
+ );
866
756
  if (result.isOk) {
867
757
  return Ok(result.value);
868
758
  }
869
759
  return Err(result.error);
870
760
  }
871
761
  async function makePayout(input) {
872
- const reference = input.reference ?? generateReference();
762
+ const reference = resolveReference(input.reference);
873
763
  validateAmount(input.amount);
874
764
  validateNonEmpty(input.customer.name, "customer.name");
875
765
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
@@ -883,24 +773,18 @@ function createSdkInstance(config) {
883
773
  "destination.accountNumber"
884
774
  );
885
775
  let payload = { ...input, reference };
886
- if (config.hooks?.beforePayout) {
887
- const mutated = await config.hooks.beforePayout(payload);
888
- if (mutated != null)
889
- payload = { ...mutated, reference: mutated.reference ?? reference };
890
- }
776
+ const mutated = await runHook(config.hooks?.beforePayout, payload);
777
+ if (mutated != null)
778
+ payload = { ...mutated, reference: mutated.reference ?? reference };
891
779
  const result = await transport.send({
892
780
  action: SDK_ACTIONS.makePayout,
893
781
  payload
894
782
  });
895
- if (config.hooks?.afterPayout) {
896
- await config.hooks.afterPayout(
897
- result.isOk ? Ok({
898
- reference: result.value.reference,
899
- status: result.value.status
900
- }) : Err(result.error),
901
- payload
902
- );
903
- }
783
+ await runHook(
784
+ config.hooks?.afterPayout,
785
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
786
+ payload
787
+ );
904
788
  if (result.isErr) {
905
789
  const sdkErr = parseError(result.error);
906
790
  return createPaymentInstance(
@@ -911,7 +795,7 @@ function createSdkInstance(config) {
911
795
  return createPaymentInstance(result.value, commonDeps);
912
796
  }
913
797
  async function makePayoutAndResolve(input) {
914
- const reference = input.reference ?? generateReference();
798
+ const reference = resolveReference(input.reference);
915
799
  validateAmount(input.amount);
916
800
  validateNonEmpty(input.customer.name, "customer.name");
917
801
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
@@ -925,24 +809,18 @@ function createSdkInstance(config) {
925
809
  "destination.accountNumber"
926
810
  );
927
811
  let payload = { ...input, reference };
928
- if (config.hooks?.beforePayout) {
929
- const mutated = await config.hooks.beforePayout(payload);
930
- if (mutated != null)
931
- payload = { ...mutated, reference: mutated.reference ?? reference };
932
- }
812
+ const mutated = await runHook(config.hooks?.beforePayout, payload);
813
+ if (mutated != null)
814
+ payload = { ...mutated, reference: mutated.reference ?? reference };
933
815
  const result = await transport.send({
934
816
  action: SDK_ACTIONS.makePayoutAndResolve,
935
817
  payload
936
818
  });
937
- if (config.hooks?.afterPayout) {
938
- await config.hooks.afterPayout(
939
- result.isOk ? Ok({
940
- reference: result.value.reference,
941
- status: result.value.status
942
- }) : Err(result.error),
943
- payload
944
- );
945
- }
819
+ await runHook(
820
+ config.hooks?.afterPayout,
821
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
822
+ payload
823
+ );
946
824
  if (result.isOk) {
947
825
  return Ok(result.value);
948
826
  }
@@ -984,7 +862,7 @@ function createSdkInstance(config) {
984
862
  return Err(result.error);
985
863
  }
986
864
  async function createInvoice(input) {
987
- const reference = input.reference ?? generateReference();
865
+ const reference = resolveReference(input.reference);
988
866
  validateAmount(input.amount);
989
867
  validateNonEmpty(input.description, "description");
990
868
  if (input.items) {
@@ -1042,7 +920,8 @@ function createNylonPay(config) {
1042
920
  throw new Error('apiSecret must start with "nps_"');
1043
921
  }
1044
922
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1045
- const instanceKey = `${config.apiKey}:${baseUrl}`;
923
+ const secretHash = createHash("sha256").update(config.apiSecret).digest("hex").slice(0, 16);
924
+ const instanceKey = `${config.apiKey}:${baseUrl}:${secretHash}`;
1046
925
  if (!config.force) {
1047
926
  const existing = instances.get(instanceKey);
1048
927
  if (existing) return existing;
@@ -1056,7 +935,6 @@ function createNylonPay(config) {
1056
935
  maxPollIntervalMs: config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS,
1057
936
  maxPollDurationMs: config.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS,
1058
937
  maxPollAttempts: config.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS,
1059
- streaming: config.streaming ?? DEFAULT_STREAMING,
1060
938
  fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
1061
939
  hooks: config.hooks
1062
940
  };