@secondlayer/subgraphs 7.2.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ import { Kysely } from "kysely";
16
16
  */
17
17
  declare const MAX_WEBHOOK_TIMEOUT_MS: number;
18
18
  declare const LOCK_WINDOW_MS: number;
19
+ declare function allowPrivateEgress(): boolean;
19
20
  interface ResolvedAddress {
20
21
  address: string;
21
22
  family: number;
@@ -105,4 +106,4 @@ interface StartEmitterOptions {
105
106
  stopDrainDeadlineMs?: number;
106
107
  }
107
108
  declare function startEmitter(opts?: StartEmitterOptions): Promise<() => Promise<void>>;
108
- export { LOCK_WINDOW_MS, MAX_REDIRECT_HOPS, MAX_WEBHOOK_TIMEOUT_MS, StartEmitterOptions, __postToWebhookForTest, __setDnsLookupForTest, checkEgressAllowed, deliverTestEvent, startEmitter };
109
+ export { LOCK_WINDOW_MS, MAX_REDIRECT_HOPS, MAX_WEBHOOK_TIMEOUT_MS, StartEmitterOptions, __postToWebhookForTest, __setDnsLookupForTest, allowPrivateEgress, checkEgressAllowed, deliverTestEvent, startEmitter };
@@ -6,12 +6,12 @@ import {
6
6
  getTargetDb
7
7
  } from "@secondlayer/shared/db";
8
8
  import { getWebhookSigningSecret } from "@secondlayer/shared/db/queries/webhooks";
9
- import { logger as logger3 } from "@secondlayer/shared/logger";
9
+ import { logger as logger4 } from "@secondlayer/shared/logger";
10
10
  import { listen, targetListenerUrl } from "@secondlayer/shared/queue/listener";
11
11
  import {
12
12
  webhookTimeoutMsCeiling
13
13
  } from "@secondlayer/shared/schemas/webhooks";
14
- import { sql } from "kysely";
14
+ import { sql as sql2 } from "kysely";
15
15
 
16
16
  // src/runtime/formats/index.ts
17
17
  import { signSecondlayerWebhook } from "@secondlayer/shared/crypto/secondlayer-webhook";
@@ -194,6 +194,106 @@ function recordDelivery() {
194
194
  pending += 1;
195
195
  }
196
196
 
197
+ // src/runtime/receiver-cert.ts
198
+ import { isIP } from "node:net";
199
+ import tls from "node:tls";
200
+ import { logger as logger3 } from "@secondlayer/shared/logger";
201
+ import { sql } from "kysely";
202
+ var PROBE_TIMEOUT_MS = 5000;
203
+ var RECHECK_AFTER_HOURS = 6;
204
+ var BATCH_LIMIT = 50;
205
+ async function probeCertExpiry(url, opts) {
206
+ let parsed;
207
+ try {
208
+ parsed = new URL(url);
209
+ } catch {
210
+ return null;
211
+ }
212
+ if (parsed.protocol !== "https:")
213
+ return null;
214
+ if (!allowPrivateEgress()) {
215
+ let refusal;
216
+ try {
217
+ refusal = await checkEgressAllowed(url);
218
+ } catch {
219
+ return null;
220
+ }
221
+ if (refusal)
222
+ return null;
223
+ }
224
+ const raw = parsed.hostname;
225
+ const host = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw;
226
+ const connect = opts?.connect ?? tls.connect;
227
+ const timeoutMs = opts?.timeoutMs ?? PROBE_TIMEOUT_MS;
228
+ return new Promise((resolve) => {
229
+ const open = {};
230
+ let settled = false;
231
+ const finish = (expires) => {
232
+ if (settled)
233
+ return;
234
+ settled = true;
235
+ clearTimeout(timer);
236
+ try {
237
+ open.socket?.destroy();
238
+ } catch {}
239
+ resolve(expires);
240
+ };
241
+ const timer = setTimeout(() => finish(null), timeoutMs);
242
+ let probe;
243
+ try {
244
+ probe = connect({
245
+ host,
246
+ port: Number(parsed.port) || 443,
247
+ ...isIP(host) ? {} : { servername: host },
248
+ rejectUnauthorized: false
249
+ });
250
+ } catch {
251
+ finish(null);
252
+ return;
253
+ }
254
+ open.socket = probe;
255
+ probe.once("secureConnect", () => {
256
+ try {
257
+ const expires = new Date(probe.getPeerCertificate().valid_to);
258
+ finish(Number.isNaN(expires.getTime()) ? null : expires);
259
+ } catch {
260
+ finish(null);
261
+ }
262
+ });
263
+ probe.once("error", () => finish(null));
264
+ });
265
+ }
266
+ async function refreshReceiverCerts(db, opts) {
267
+ const probe = opts?.probe ?? probeCertExpiry;
268
+ const due = await db.selectFrom("webhooks").select(["id", "url"]).where("status", "=", "active").where("url", "like", "https:%").where((eb) => eb.or([
269
+ eb("receiver_cert_checked_at", "is", null),
270
+ eb("receiver_cert_checked_at", "<", sql`now() - make_interval(hours => ${RECHECK_AFTER_HOURS})`)
271
+ ])).orderBy(sql`receiver_cert_checked_at asc nulls first`).limit(BATCH_LIMIT).execute();
272
+ const idsByOrigin = new Map;
273
+ for (const { id, url } of due) {
274
+ let origin;
275
+ try {
276
+ origin = new URL(url).origin;
277
+ } catch {
278
+ continue;
279
+ }
280
+ const entry = idsByOrigin.get(origin) ?? { url, ids: [] };
281
+ entry.ids.push(id);
282
+ idsByOrigin.set(origin, entry);
283
+ }
284
+ for (const [origin, { url, ids }] of idsByOrigin) {
285
+ const expires = await probe(url);
286
+ await db.updateTable("webhooks").set({
287
+ receiver_cert_expires_at: expires,
288
+ receiver_cert_checked_at: new Date
289
+ }).where("id", "in", ids).execute();
290
+ logger3.debug("[emitter] receiver cert probed", {
291
+ origin,
292
+ expiresAt: expires?.toISOString() ?? null
293
+ });
294
+ }
295
+ }
296
+
197
297
  // src/runtime/emitter.ts
198
298
  var BATCH_SIZE = 50;
199
299
  var LIVE_SHARE = 0.9;
@@ -288,6 +388,7 @@ function isPrivateEgress(url) {
288
388
  return true;
289
389
  return isPrivateIp(host);
290
390
  }
391
+ var RECEIVER_CERT_INTERVAL_MS = 30 * 60000;
291
392
  function allowPrivateEgress() {
292
393
  return process.env.SECONDLAYER_ALLOW_PRIVATE_EGRESS === "true";
293
394
  }
@@ -345,7 +446,7 @@ async function postToWebhook(url, body, headers, timeoutMs) {
345
446
  if (!allowPrivateEgress()) {
346
447
  const refusal = await checkEgressAllowed(target);
347
448
  if (refusal) {
348
- logger3.warn("[emitter] refused private egress", {
449
+ logger4.warn("[emitter] refused private egress", {
349
450
  url: target,
350
451
  reason: refusal
351
452
  });
@@ -485,7 +586,7 @@ async function settleDelivered(db, outboxRow) {
485
586
  locked_until: null
486
587
  }).where("id", "=", outboxRow.id).where("status", "=", "pending").executeTakeFirst();
487
588
  if (Number(result.numUpdatedRows ?? 0) === 0) {
488
- logger3.info("webhook.settle.orphaned", {
589
+ logger4.info("webhook.settle.orphaned", {
489
590
  outboxId: outboxRow.id,
490
591
  webhookId: outboxRow.webhook_id,
491
592
  outcome: "delivered"
@@ -516,14 +617,14 @@ async function settleFailed(db, outboxRow, sub, errText) {
516
617
  locked_until: null
517
618
  }).where("id", "=", outboxRow.id).where("status", "=", "pending").executeTakeFirst();
518
619
  if (Number(result.numUpdatedRows ?? 0) === 0) {
519
- logger3.info("webhook.settle.orphaned", {
620
+ logger4.info("webhook.settle.orphaned", {
520
621
  outboxId: outboxRow.id,
521
622
  webhookId: outboxRow.webhook_id,
522
623
  outcome: "failed"
523
624
  });
524
625
  return;
525
626
  }
526
- const incResult = await sql`
627
+ const incResult = await sql2`
527
628
  UPDATE webhooks
528
629
  SET circuit_failures = circuit_failures + 1,
529
630
  last_delivery_at = NOW(),
@@ -540,7 +641,7 @@ async function settleFailed(db, outboxRow, sub, errText) {
540
641
  circuit_opened_at: new Date,
541
642
  updated_at: new Date
542
643
  }).where("id", "=", sub.id).execute();
543
- logger3.warn("Webhook circuit tripped — paused after consecutive failures", {
644
+ logger4.warn("Webhook circuit tripped — paused after consecutive failures", {
544
645
  webhook: sub.name,
545
646
  failures: newFailures
546
647
  });
@@ -585,23 +686,23 @@ async function claimAndDispatchOnce(db, state, emitterId, trigger) {
585
686
  const liveLimit = Math.max(1, Math.round(BATCH_SIZE * LIVE_SHARE));
586
687
  const replayLimit = BATCH_SIZE - liveLimit;
587
688
  const { combined, liveCount, replayCount } = await db.transaction().execute(async (tx) => {
588
- const live = await sql`
689
+ const live = await sql2`
589
690
  SELECT * FROM webhook_outbox
590
691
  WHERE status = 'pending'
591
692
  AND next_attempt_at <= NOW()
592
693
  AND is_replay = FALSE
593
694
  ORDER BY next_attempt_at ASC
594
695
  FOR UPDATE SKIP LOCKED
595
- LIMIT ${sql.lit(liveLimit)}
696
+ LIMIT ${sql2.lit(liveLimit)}
596
697
  `.execute(tx);
597
- const replay = await sql`
698
+ const replay = await sql2`
598
699
  SELECT * FROM webhook_outbox
599
700
  WHERE status = 'pending'
600
701
  AND next_attempt_at <= NOW()
601
702
  AND is_replay = TRUE
602
703
  ORDER BY next_attempt_at ASC
603
704
  FOR UPDATE SKIP LOCKED
604
- LIMIT ${sql.lit(replayLimit)}
705
+ LIMIT ${sql2.lit(replayLimit)}
605
706
  `.execute(tx);
606
707
  const rows = [...live.rows, ...replay.rows];
607
708
  if (rows.length === 0) {
@@ -654,7 +755,7 @@ async function claimAndDispatchOnce(db, state, emitterId, trigger) {
654
755
  const oldestCreatedAtMs = Math.min(...combined.map((r) => new Date(r.created_at).getTime()));
655
756
  const inFlight = subIds.reduce((sum, id) => sum + (state.subQueues.get(id)?.inFlight ?? 0), 0);
656
757
  const concurrency = subIds.reduce((sum, id) => sum + (subById.get(id)?.concurrency || 4), 0);
657
- logger3.info("emitter claim cycle", {
758
+ logger4.info("emitter claim cycle", {
658
759
  event: "emitter_claim",
659
760
  trigger,
660
761
  claimed: combined.length,
@@ -709,7 +810,7 @@ async function runOne(db, state, sq, row) {
709
810
  await settleFailed(db, row, sq.sub, err);
710
811
  }
711
812
  } catch (err) {
712
- logger3.error("Emitter dispatch crashed", {
813
+ logger4.error("Emitter dispatch crashed", {
713
814
  outboxId: row.id,
714
815
  error: err instanceof Error ? err.message : String(err)
715
816
  });
@@ -720,15 +821,15 @@ async function runOne(db, state, sq, row) {
720
821
  }
721
822
  }
722
823
  async function runRetention(db) {
723
- await sql`
824
+ await sql2`
724
825
  DELETE FROM webhook_outbox
725
826
  WHERE status = 'delivered' AND delivered_at < NOW() - interval '7 days'
726
827
  `.execute(db);
727
- await sql`
828
+ await sql2`
728
829
  DELETE FROM webhook_deliveries
729
830
  WHERE dispatched_at < NOW() - interval '30 days'
730
831
  `.execute(db);
731
- await sql`
832
+ await sql2`
732
833
  DELETE FROM webhook_outbox
733
834
  WHERE status = 'dead' AND failed_at < NOW() - interval '90 days'
734
835
  `.execute(db);
@@ -746,30 +847,30 @@ async function startEmitter(opts) {
746
847
  };
747
848
  const pollIntervalMs = opts?.pollIntervalMs ?? 120000;
748
849
  const retentionIntervalMs = opts?.retentionIntervalMs ?? 60 * 60000;
749
- logger3.info("[emitter] started", { id: emitterId });
850
+ logger4.info("[emitter] started", { id: emitterId });
750
851
  const listenUrl = targetListenerUrl();
751
852
  const stopNew = await listen("webhooks:new_outbox", () => {
752
853
  if (!state.running)
753
854
  return;
754
- claimAndDrain(db, state, emitterId, "notify").catch((err) => logger3.error("[emitter] claim failed", {
855
+ claimAndDrain(db, state, emitterId, "notify").catch((err) => logger4.error("[emitter] claim failed", {
755
856
  error: err instanceof Error ? err.message : String(err)
756
857
  }));
757
858
  }, { connectionString: listenUrl });
758
859
  const stopChanged = await listen("webhooks:changed", () => {
759
860
  if (!state.running)
760
861
  return;
761
- claimAndDrain(db, state, emitterId, "changed").catch((err) => logger3.error("[emitter] claim failed", {
862
+ claimAndDrain(db, state, emitterId, "changed").catch((err) => logger4.error("[emitter] claim failed", {
762
863
  error: err instanceof Error ? err.message : String(err)
763
864
  }));
764
865
  }, { connectionString: listenUrl });
765
- logger3.info("[emitter] wake listeners connected", {
866
+ logger4.info("[emitter] wake listeners connected", {
766
867
  channels: ["webhooks:new_outbox", "webhooks:changed"],
767
868
  db: describeDbUrl(listenUrl)
768
869
  });
769
870
  const poll = setInterval(() => {
770
871
  if (!state.running)
771
872
  return;
772
- claimAndDrain(db, state, emitterId, "poll").catch((err) => logger3.error("[emitter] poll claim failed", {
873
+ claimAndDrain(db, state, emitterId, "poll").catch((err) => logger4.error("[emitter] poll claim failed", {
773
874
  error: err instanceof Error ? err.message : String(err)
774
875
  }));
775
876
  }, pollIntervalMs);
@@ -777,14 +878,22 @@ async function startEmitter(opts) {
777
878
  const retention = setInterval(() => {
778
879
  if (!state.running)
779
880
  return;
780
- runRetention(db).catch((err) => logger3.error("[emitter] retention failed", {
881
+ runRetention(db).catch((err) => logger4.error("[emitter] retention failed", {
781
882
  error: err instanceof Error ? err.message : String(err)
782
883
  }));
783
884
  }, retentionIntervalMs);
885
+ const receiverCerts = setInterval(() => {
886
+ if (!state.running)
887
+ return;
888
+ refreshReceiverCerts(db).catch((err) => logger4.error("[emitter] receiver cert refresh failed", {
889
+ error: err instanceof Error ? err.message : String(err)
890
+ }));
891
+ }, RECEIVER_CERT_INTERVAL_MS);
784
892
  return async () => {
785
893
  state.running = false;
786
894
  clearInterval(poll);
787
895
  clearInterval(retention);
896
+ clearInterval(receiverCerts);
788
897
  await stopNew();
789
898
  await stopChanged();
790
899
  const pendingRuns = new Set(state.inFlightRuns);
@@ -804,7 +913,7 @@ async function startEmitter(opts) {
804
913
  clearTimeout(timer);
805
914
  const abandoned = timedOut ? state.inFlightRuns.size : 0;
806
915
  const drained = drainTarget - abandoned;
807
- logger3.info("[emitter] stopped", {
916
+ logger4.info("[emitter] stopped", {
808
917
  id: emitterId,
809
918
  event: "emitter_stopped",
810
919
  drained,
@@ -818,10 +927,11 @@ export {
818
927
  MAX_WEBHOOK_TIMEOUT_MS,
819
928
  __postToWebhookForTest,
820
929
  __setDnsLookupForTest,
930
+ allowPrivateEgress,
821
931
  checkEgressAllowed,
822
932
  deliverTestEvent,
823
933
  startEmitter
824
934
  };
825
935
 
826
- //# debugId=CAA46708A06EDDEB64756E2164756E21
936
+ //# debugId=C2E9CBDDCAC108F464756E2164756E21
827
937
  //# sourceMappingURL=emitter.js.map