@secondlayer/sdk 12.1.0 → 12.3.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.
package/dist/index.d.ts CHANGED
@@ -871,6 +871,17 @@ type StreamsEventsConsumeParams<
871
871
  maxPages?: number;
872
872
  maxEmptyPolls?: number;
873
873
  signal?: AbortSignal;
874
+ /**
875
+ * Cuts the empty-poll backoff short: on an empty page, the loop races
876
+ * `emptyBackoffMs` against this promise instead of sleeping the full
877
+ * amount. Pass a function that resolves once when new data MIGHT be
878
+ * available (e.g. a Postgres NOTIFY listener) — it is called fresh on
879
+ * every empty page, never awaited more than once concurrently per page.
880
+ * The backoff timer still runs underneath, so a wake source that never
881
+ * resolves (or a `wake` this call omits) degrades to the same polling
882
+ * behavior as before.
883
+ */
884
+ wake?: () => Promise<void>;
874
885
  };
875
886
  /**
876
887
  * One yielded page from {@link StreamsClient.consume} — the
@@ -2789,17 +2800,40 @@ declare function getSubgraph<T extends {
2789
2800
  }>(def: T, options?: Partial<SecondLayerOptions> | SecondLayer | Subgraphs): InferSubgraphClient2<T>;
2790
2801
  import { SubgraphAgentSchema as SubgraphAgentSchema3, SubgraphSpecFormat as SubgraphSpecFormat2, SubgraphSpecOptions as SubgraphSpecOptions3 } from "@secondlayer/shared/subgraphs/spec";
2791
2802
  import { SubgraphDetail as SubgraphDetail2 } from "@secondlayer/shared/schemas/subgraphs";
2792
- import { DeadRow as DeadRow3, DeliveryRow as DeliveryRow3, WebhookDetail as WebhookDetail3 } from "@secondlayer/shared/schemas/webhooks";
2803
+ import { DeadRow as DeadRow3, DeliveryRow as DeliveryRow3, WebhookDetail as WebhookDetail3, WebhookSummary as WebhookSummary3 } from "@secondlayer/shared/schemas/webhooks";
2793
2804
  /**
2794
2805
  * Webhook diagnosis, shared by `secondlayer webhooks doctor` and the
2795
2806
  * dashboard's webhook detail page. Moved out of the CLI (unchanged behavior)
2796
2807
  * so the web can build the same judgment without printing CLI-command text.
2808
+ *
2809
+ * Plan 068 adds five deterministic, numbers-only detectors on top of the
2810
+ * existing flag checks: no model, no history beyond the deliveries window
2811
+ * already fetched. Thresholds are named constants below, conservative on
2812
+ * purpose — a wrong insight costs more trust than a missed one.
2797
2813
  */
2798
2814
  declare function isSuccessDelivery(row: DeliveryRow3): boolean;
2799
- type DoctorIssueCode = "warning" | "paused" | "last_error" | "circuit" | "dead_letters" | "subgraph_gaps" | "subgraph_catching_up" | "no_deliveries";
2815
+ type DoctorSeverity = "bad" | "warn" | "calm";
2816
+ interface DoctorEvidence {
2817
+ label: string;
2818
+ value: string;
2819
+ }
2820
+ interface DoctorFix {
2821
+ text: string;
2822
+ /** A `secondlayer webhooks ...` command the reader can copy. */
2823
+ command?: string;
2824
+ /** Anchor into the docs' webhooks troubleshooting section. */
2825
+ docsPath?: string;
2826
+ }
2827
+ type DoctorIssueCode = "warning" | "paused" | "last_error" | "circuit" | "dead_letters" | "subgraph_gaps" | "subgraph_catching_up" | "no_deliveries" | "receiver_rate_limited" | "receiver_down" | "receiver_rejects" | "receiver_slow" | "delivery_lag";
2800
2828
  interface DoctorIssue {
2801
2829
  code: DoctorIssueCode;
2802
2830
  detail?: string;
2831
+ severity: DoctorSeverity;
2832
+ /** 2-4 numbers-only facts backing the issue. Only the five new detectors
2833
+ * and the list-summary circuit issue carry this — the older flag checks
2834
+ * keep their plain `detail` string. */
2835
+ evidence?: DoctorEvidence[];
2836
+ fix?: DoctorFix;
2803
2837
  }
2804
2838
  interface DoctorReport {
2805
2839
  webhook: WebhookDetail3;
@@ -2822,6 +2856,10 @@ interface DoctorReport {
2822
2856
  /** CLI-facing next steps, one per issue, in the same order as `issues`. */
2823
2857
  hints: string[];
2824
2858
  issues: DoctorIssue[];
2859
+ /** The single most severe issue (bad > warn > calm, then rule priority),
2860
+ * or `null` when there are none. The UI leads with this and collapses
2861
+ * the rest underneath. */
2862
+ primary: DoctorIssue | null;
2825
2863
  }
2826
2864
  declare function buildDoctorReport(input: {
2827
2865
  webhook: WebhookDetail3;
@@ -2830,6 +2868,15 @@ declare function buildDoctorReport(input: {
2830
2868
  subgraph?: SubgraphDetail2 | null;
2831
2869
  }): DoctorReport;
2832
2870
  /**
2871
+ * The list page's own reduced detector: computed from `WebhookSummary` alone
2872
+ * (no deliveries, no dead-letter rows — the list endpoint returns neither,
2873
+ * and fetching per-row detail or deliveries here would fan out one request
2874
+ * per row). Only `status` and `circuitOpenedAt` distinguish a real problem
2875
+ * from a manual pause at this level; deliveries-based rules (rate limits,
2876
+ * downtime, rejects, slowness, lag) only ever show on the detail page.
2877
+ */
2878
+ declare function buildListIssue(webhook: Pick<WebhookSummary3, "status" | "circuitOpenedAt" | "lastSuccessAt">): DoctorIssue | null;
2879
+ /**
2833
2880
  * The two pieces every "deploy a consumer" guide leaves out, packaged:
2834
2881
  * a liveness endpoint and a graceful-shutdown signal. Both are
2835
2882
  * runtime-agnostic — the SDK never binds a port or assumes Bun/Node.
@@ -3258,4 +3305,4 @@ declare const Cursor: {
3258
3305
  };
3259
3306
  };
3260
3307
  import { decodeClarityValue, toJsonSafe } from "@secondlayer/shared/streams-rows";
3261
- export { ACCOUNT_KEY_ENV, ActiveSubgraphOperation, ApiError, ArchiveAuthError, ArchiveClient, ArchiveCreditsBalance, ArchiveDataset, ArchiveFetchItem, ArchiveFetchResult, ArchiveFlow, ArchiveGateNotConfiguredError, ArchiveLoadOptions, ArchiveManifest, ArchivePartition, ArchiveQuote, ArchiveSignatureError, ArchiveStatus, ArchiveVerifyInput, ArchiveVerifyRangeStatus, ArchiveVerifyResult, AuthError, BlockEnvelope, BlocksEnvelope, BlocksListParams, BlocksWalkParams, CanonicalEnvelope, CanonicalListParams, CanonicalWalkParams, ChainApplyDeliveryOf, ChainApplyEnvelope, ChainApplyEnvelopeOf, ChainEventEnvelope, ChainFtBurnData, ChainFtMintData, ChainFtTransferData, ChainNftBurnData, ChainNftMintData, ChainNftTransferData, ChainPrintEventData, ChainReorgOrphanedEntry, ChainReorgRollbackDelivery, ChainReorgRollbackEnvelope, ChainStxBurnData, ChainStxLockData, ChainStxMintData, ChainStxTransferData, ChainTestDelivery, ChainTrigger, ChainTriggerType, ChainTxLevelEvent, ChainWebhookDelivery2 as ChainWebhookDelivery, ChainWebhookEnvelope, ConsumerBatchContext, ConsumerHealth, ConsumerSink, ContextAccount, ContextField, ContextFieldError, ContextSnapshot, ContractCallsConsumeParams, ContractCallsEnvelope, ContractCallsListParams, ContractCallsWalkParams, ContractConformance, ContractSummary, Contracts, ContractsEnvelope, ContractsListParams, CreateArchiveClientOptions, CreateWebhookRequest2 as CreateWebhookRequest, CreateWebhookResponse2 as CreateWebhookResponse, Cursor, DEFAULT_REQUEST_TIMEOUT_MS, DeadRow2 as DeadRow, DecodedEventColumns, DecodedEventRow, DecodedFtBurn, DecodedFtBurnPayload, DecodedFtMint, DecodedFtMintPayload, DecodedFtTransfer, DecodedFtTransferPayload, DecodedNftBurn, DecodedNftBurnPayload, DecodedNftMint, DecodedNftMintPayload, DecodedNftTransfer, DecodedNftTransferPayload, DecodedPrint, DecodedPrintPayload, DecodedPrintValue, DecodedStxBurn, DecodedStxBurnPayload, DecodedStxLock, DecodedStxLockPayload, DecodedStxMint, DecodedStxMintPayload, DecodedStxTransfer, DecodedStxTransferPayload, DeliveryRow2 as DeliveryRow, DoctorIssue, DoctorIssueCode, DoctorReport, EtagCache, EtagEntry, EventsConsumeParams, EventsEnvelope, EventsListParams, EventsWalkParams, FetchLike2 as FetchLike, FtTransfer, FtTransferEvent, FtTransferPayload2 as FtTransferPayload, FtTransfersEnvelope, FtTransfersListParams, FtTransfersResource, FtTransfersWalkParams, INDEX_MAX_PAGE_SIZE, INSTANCE_TOKEN_ENV, Index, IndexAlwaysFields, IndexBlock, IndexCanonicalBlock, IndexConsumeOptions, IndexContractCall, IndexDiscovery, IndexEvent, IndexEventFields, IndexEventOf, IndexEventType, IndexEventTypeFilters, IndexEventsResource, IndexFeedEnvelope, IndexFeedFetcher, IndexFeedItem, IndexFtBurn, IndexFtMint, IndexFtTransfer, IndexMapDelete, IndexMapInsert, IndexMapSet, IndexMempoolTransaction, IndexNestedContractCall, IndexNftBurn, IndexNftMint, IndexNftTransfer, IndexPostCondition, IndexPox5Event, IndexPox5EventTopic, IndexPoxCycle, IndexPoxFunctionCount, IndexPrint, IndexReorg, IndexSbtcDeposit, IndexSbtcDepositDetail, IndexSbtcEvent, IndexSbtcSummary, IndexSbtcWithdrawal, IndexSbtcWithdrawalDetail, IndexSbtcWithdrawalPhase, IndexStackingAction, IndexStxBurn, IndexStxLock, IndexStxMint, IndexStxTransfer, IndexTip, IndexTransaction, IndexVarSet, InstanceClient, InstanceDiagnosis3 as InstanceDiagnosis, InstanceIssue, InstanceState, InstanceStatus, InsufficientArchiveCreditsError, LOCAL_API_URL, LoadedArchive, MemoryEtagCache, MempoolEnvelope, MempoolListParams, MempoolTransactionEnvelope, MempoolWalkParams, NftTransfer, NftTransferEvent, NftTransferPayload2 as NftTransferPayload, NftTransfersEnvelope, NftTransfersListParams, NftTransfersResource, NftTransfersWalkParams, Pox5EventsEnvelope, Pox5EventsListParams, Pox5EventsWalkParams, Pox5Resource, PoxCycleEnvelope, PoxCyclesEnvelope, PoxCyclesListParams, PoxCyclesWalkParams, PoxResource, PrintSchemaResponse, PublicStatus2 as PublicStatus, RateLimitError, ReplayResult2 as ReplayResult, RequestOptions, RewardSet2 as RewardSet, RotateSecretResponse2 as RotateSecretResponse, SbtcDepositEnvelope, SbtcDepositEvent, SbtcDepositsConsumeParams, SbtcDepositsEnvelope, SbtcDepositsListParams, SbtcDepositsWalkParams, SbtcEventTopic2 as SbtcEventTopic, SbtcEventsConsumeParams, SbtcEventsEnvelope, SbtcEventsListParams, SbtcEventsWalkParams, SbtcResource, SbtcSummaryEnvelope, SbtcWithdrawalEnvelope, SbtcWithdrawalEvent, SbtcWithdrawalStatus, SbtcWithdrawalSweptConfirmedEvent, SbtcWithdrawalsEnvelope, SbtcWithdrawalsListParams, SbtcWithdrawalsWalkParams, SecondLayer, SecondLayerArchive, SecondLayerError, SecondLayerErrorOptions, SecondLayerOptions, SinkTx, StackingEnvelope, StackingListParams, StackingWalkParams, StandardWebhooksHeaders2 as StandardWebhooksHeaders, StreamsBatch, StreamsBatchContext, StreamsCanonicalBlock, StreamsClient, StreamsConsumeParams, StreamsDumpFile, StreamsDumps, StreamsDumpsManifest, StreamsEvent, StreamsEventPayload, StreamsEventType, StreamsEventsConsumeParams, StreamsEventsConsumeResult, StreamsEventsEnvelope, StreamsEventsListEnvelope, StreamsEventsListParams, StreamsEventsStreamParams, StreamsEventsSubscribeParams, StreamsReorg, StreamsReorgContext, StreamsReorgsListEnvelope, StreamsReorgsListParams, StreamsServerError, StreamsSignatureError, StreamsSubscription, StreamsTip, StreamsWireEvent2 as StreamsWireEvent, SubgraphAgentSchema3 as SubgraphAgentSchema, SubgraphOperationStatus, SubgraphSpecFormat2 as SubgraphSpecFormat, SubgraphSpecOptions3 as SubgraphSpecOptions, Subgraphs, TransactionEnvelope, TransactionProof, TransactionProofVerifyResult, TransactionsEnvelope, TransactionsListParams, TransactionsWalkParams, UpdateWebhookRequest2 as UpdateWebhookRequest, ValidationError, VmStreamsEvent, WalkOptions, WebhookDetail2 as WebhookDetail, WebhookFormat, WebhookHeaderInput, WebhookKind, WebhookRuntime, WebhookStatus, WebhookSummary2 as WebhookSummary, WebhookTestResult2 as WebhookTestResult, Webhooks, WithSinkTx, buildDoctorReport, consumeIndexFeed, consumerHealth, contextField, createArchiveClient, createStreamsClient, decode, decodeChainWebhook, decodeClarityValue, decodeFtBurn, decodeFtMint, decodeFtTransfer, decodeNftBurn, decodeNftMint, decodeNftTransfer, decodePrint, decodeStxBurn, decodeStxLock, decodeStxMint, decodeStxTransfer, fetchRewardSet, getSubgraph, isFtBurn, isFtMint, isFtTransfer, isNftBurn, isNftMint, isNftTransfer, isPrint, isStxBurn, isStxLock, isStxMint, isStxTransfer, isSuccessDelivery, parseRetryAfter, resolveAccountKey, resolveApiKey, resolveBaseUrl, shutdownSignal, toJsonSafe, trigger, verifySecondlayerSignature, verify as verifyStandardWebhooksHeaders, verifyTransactionProof, verifyWebhookSignature };
3308
+ export { ACCOUNT_KEY_ENV, ActiveSubgraphOperation, ApiError, ArchiveAuthError, ArchiveClient, ArchiveCreditsBalance, ArchiveDataset, ArchiveFetchItem, ArchiveFetchResult, ArchiveFlow, ArchiveGateNotConfiguredError, ArchiveLoadOptions, ArchiveManifest, ArchivePartition, ArchiveQuote, ArchiveSignatureError, ArchiveStatus, ArchiveVerifyInput, ArchiveVerifyRangeStatus, ArchiveVerifyResult, AuthError, BlockEnvelope, BlocksEnvelope, BlocksListParams, BlocksWalkParams, CanonicalEnvelope, CanonicalListParams, CanonicalWalkParams, ChainApplyDeliveryOf, ChainApplyEnvelope, ChainApplyEnvelopeOf, ChainEventEnvelope, ChainFtBurnData, ChainFtMintData, ChainFtTransferData, ChainNftBurnData, ChainNftMintData, ChainNftTransferData, ChainPrintEventData, ChainReorgOrphanedEntry, ChainReorgRollbackDelivery, ChainReorgRollbackEnvelope, ChainStxBurnData, ChainStxLockData, ChainStxMintData, ChainStxTransferData, ChainTestDelivery, ChainTrigger, ChainTriggerType, ChainTxLevelEvent, ChainWebhookDelivery2 as ChainWebhookDelivery, ChainWebhookEnvelope, ConsumerBatchContext, ConsumerHealth, ConsumerSink, ContextAccount, ContextField, ContextFieldError, ContextSnapshot, ContractCallsConsumeParams, ContractCallsEnvelope, ContractCallsListParams, ContractCallsWalkParams, ContractConformance, ContractSummary, Contracts, ContractsEnvelope, ContractsListParams, CreateArchiveClientOptions, CreateWebhookRequest2 as CreateWebhookRequest, CreateWebhookResponse2 as CreateWebhookResponse, Cursor, DEFAULT_REQUEST_TIMEOUT_MS, DeadRow2 as DeadRow, DecodedEventColumns, DecodedEventRow, DecodedFtBurn, DecodedFtBurnPayload, DecodedFtMint, DecodedFtMintPayload, DecodedFtTransfer, DecodedFtTransferPayload, DecodedNftBurn, DecodedNftBurnPayload, DecodedNftMint, DecodedNftMintPayload, DecodedNftTransfer, DecodedNftTransferPayload, DecodedPrint, DecodedPrintPayload, DecodedPrintValue, DecodedStxBurn, DecodedStxBurnPayload, DecodedStxLock, DecodedStxLockPayload, DecodedStxMint, DecodedStxMintPayload, DecodedStxTransfer, DecodedStxTransferPayload, DeliveryRow2 as DeliveryRow, DoctorEvidence, DoctorFix, DoctorIssue, DoctorIssueCode, DoctorReport, DoctorSeverity, EtagCache, EtagEntry, EventsConsumeParams, EventsEnvelope, EventsListParams, EventsWalkParams, FetchLike2 as FetchLike, FtTransfer, FtTransferEvent, FtTransferPayload2 as FtTransferPayload, FtTransfersEnvelope, FtTransfersListParams, FtTransfersResource, FtTransfersWalkParams, INDEX_MAX_PAGE_SIZE, INSTANCE_TOKEN_ENV, Index, IndexAlwaysFields, IndexBlock, IndexCanonicalBlock, IndexConsumeOptions, IndexContractCall, IndexDiscovery, IndexEvent, IndexEventFields, IndexEventOf, IndexEventType, IndexEventTypeFilters, IndexEventsResource, IndexFeedEnvelope, IndexFeedFetcher, IndexFeedItem, IndexFtBurn, IndexFtMint, IndexFtTransfer, IndexMapDelete, IndexMapInsert, IndexMapSet, IndexMempoolTransaction, IndexNestedContractCall, IndexNftBurn, IndexNftMint, IndexNftTransfer, IndexPostCondition, IndexPox5Event, IndexPox5EventTopic, IndexPoxCycle, IndexPoxFunctionCount, IndexPrint, IndexReorg, IndexSbtcDeposit, IndexSbtcDepositDetail, IndexSbtcEvent, IndexSbtcSummary, IndexSbtcWithdrawal, IndexSbtcWithdrawalDetail, IndexSbtcWithdrawalPhase, IndexStackingAction, IndexStxBurn, IndexStxLock, IndexStxMint, IndexStxTransfer, IndexTip, IndexTransaction, IndexVarSet, InstanceClient, InstanceDiagnosis3 as InstanceDiagnosis, InstanceIssue, InstanceState, InstanceStatus, InsufficientArchiveCreditsError, LOCAL_API_URL, LoadedArchive, MemoryEtagCache, MempoolEnvelope, MempoolListParams, MempoolTransactionEnvelope, MempoolWalkParams, NftTransfer, NftTransferEvent, NftTransferPayload2 as NftTransferPayload, NftTransfersEnvelope, NftTransfersListParams, NftTransfersResource, NftTransfersWalkParams, Pox5EventsEnvelope, Pox5EventsListParams, Pox5EventsWalkParams, Pox5Resource, PoxCycleEnvelope, PoxCyclesEnvelope, PoxCyclesListParams, PoxCyclesWalkParams, PoxResource, PrintSchemaResponse, PublicStatus2 as PublicStatus, RateLimitError, ReplayResult2 as ReplayResult, RequestOptions, RewardSet2 as RewardSet, RotateSecretResponse2 as RotateSecretResponse, SbtcDepositEnvelope, SbtcDepositEvent, SbtcDepositsConsumeParams, SbtcDepositsEnvelope, SbtcDepositsListParams, SbtcDepositsWalkParams, SbtcEventTopic2 as SbtcEventTopic, SbtcEventsConsumeParams, SbtcEventsEnvelope, SbtcEventsListParams, SbtcEventsWalkParams, SbtcResource, SbtcSummaryEnvelope, SbtcWithdrawalEnvelope, SbtcWithdrawalEvent, SbtcWithdrawalStatus, SbtcWithdrawalSweptConfirmedEvent, SbtcWithdrawalsEnvelope, SbtcWithdrawalsListParams, SbtcWithdrawalsWalkParams, SecondLayer, SecondLayerArchive, SecondLayerError, SecondLayerErrorOptions, SecondLayerOptions, SinkTx, StackingEnvelope, StackingListParams, StackingWalkParams, StandardWebhooksHeaders2 as StandardWebhooksHeaders, StreamsBatch, StreamsBatchContext, StreamsCanonicalBlock, StreamsClient, StreamsConsumeParams, StreamsDumpFile, StreamsDumps, StreamsDumpsManifest, StreamsEvent, StreamsEventPayload, StreamsEventType, StreamsEventsConsumeParams, StreamsEventsConsumeResult, StreamsEventsEnvelope, StreamsEventsListEnvelope, StreamsEventsListParams, StreamsEventsStreamParams, StreamsEventsSubscribeParams, StreamsReorg, StreamsReorgContext, StreamsReorgsListEnvelope, StreamsReorgsListParams, StreamsServerError, StreamsSignatureError, StreamsSubscription, StreamsTip, StreamsWireEvent2 as StreamsWireEvent, SubgraphAgentSchema3 as SubgraphAgentSchema, SubgraphOperationStatus, SubgraphSpecFormat2 as SubgraphSpecFormat, SubgraphSpecOptions3 as SubgraphSpecOptions, Subgraphs, TransactionEnvelope, TransactionProof, TransactionProofVerifyResult, TransactionsEnvelope, TransactionsListParams, TransactionsWalkParams, UpdateWebhookRequest2 as UpdateWebhookRequest, ValidationError, VmStreamsEvent, WalkOptions, WebhookDetail2 as WebhookDetail, WebhookFormat, WebhookHeaderInput, WebhookKind, WebhookRuntime, WebhookStatus, WebhookSummary2 as WebhookSummary, WebhookTestResult2 as WebhookTestResult, Webhooks, WithSinkTx, buildDoctorReport, buildListIssue, consumeIndexFeed, consumerHealth, contextField, createArchiveClient, createStreamsClient, decode, decodeChainWebhook, decodeClarityValue, decodeFtBurn, decodeFtMint, decodeFtTransfer, decodeNftBurn, decodeNftMint, decodeNftTransfer, decodePrint, decodeStxBurn, decodeStxLock, decodeStxMint, decodeStxTransfer, fetchRewardSet, getSubgraph, isFtBurn, isFtMint, isFtTransfer, isNftBurn, isNftMint, isNftTransfer, isPrint, isStxBurn, isStxLock, isStxMint, isStxTransfer, isSuccessDelivery, parseRetryAfter, resolveAccountKey, resolveApiKey, resolveBaseUrl, shutdownSignal, toJsonSafe, trigger, verifySecondlayerSignature, verify as verifyStandardWebhooksHeaders, verifyTransactionProof, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -970,7 +970,10 @@ async function consumeStreamsEvents(opts) {
970
970
  if (mode === "bounded") {
971
971
  return { cursor, pages, emptyPolls };
972
972
  }
973
- await sleep(emptyBackoffMs, opts.signal);
973
+ await (opts.wake ? Promise.race([
974
+ sleep(emptyBackoffMs, opts.signal),
975
+ opts.wake().catch(() => new Promise(() => {}))
976
+ ]) : sleep(emptyBackoffMs, opts.signal));
974
977
  continue;
975
978
  }
976
979
  return { cursor, pages, emptyPolls };
@@ -2117,7 +2120,8 @@ function createStreamsClient(options) {
2117
2120
  signal: params.signal,
2118
2121
  retryCount: params.retryCount,
2119
2122
  retryDelay: params.retryDelay,
2120
- onError: params.onError
2123
+ onError: params.onError,
2124
+ wake: params.wake
2121
2125
  });
2122
2126
  }
2123
2127
  return {
@@ -2670,6 +2674,229 @@ function getSubgraph(def, options = {}) {
2670
2674
  function isSuccessDelivery(row) {
2671
2675
  return row.statusCode !== null && row.statusCode >= 200 && row.statusCode < 300;
2672
2676
  }
2677
+ var RATE_LIMITED_MIN_ATTEMPTS = 10;
2678
+ var RATE_LIMITED_MIN_RATIO = 0.5;
2679
+ var DOWN_MIN_CONSECUTIVE = 5;
2680
+ var REJECTS_MIN_CONSECUTIVE = 5;
2681
+ var SLOW_WINDOW = 20;
2682
+ var SLOW_MIN_SAMPLES = 5;
2683
+ var SLOW_TIMEOUT_RATIO = 0.5;
2684
+ var LAG_WINDOW = 20;
2685
+ var LAG_MIN_SAMPLES = 5;
2686
+ var LAG_THRESHOLD_MS = 60000;
2687
+ var ERROR_SNIPPET_MAX = 200;
2688
+ function median(values) {
2689
+ if (values.length === 0)
2690
+ return 0;
2691
+ const sorted = [...values].sort((a, b) => a - b);
2692
+ return sorted[Math.floor(sorted.length / 2)] ?? 0;
2693
+ }
2694
+ function percentile(values, p) {
2695
+ if (values.length === 0)
2696
+ return 0;
2697
+ const sorted = [...values].sort((a, b) => a - b);
2698
+ const idx = Math.min(sorted.length - 1, Math.floor(p * sorted.length));
2699
+ return sorted[idx] ?? 0;
2700
+ }
2701
+ function formatMs(ms) {
2702
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
2703
+ }
2704
+ function formatShortDate(iso) {
2705
+ return `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`;
2706
+ }
2707
+ function consecutiveFromStart(rows, predicate) {
2708
+ const out = [];
2709
+ for (const row of rows) {
2710
+ if (!predicate(row))
2711
+ break;
2712
+ out.push(row);
2713
+ }
2714
+ return out;
2715
+ }
2716
+ function summarizeStatusBreakdown(rows) {
2717
+ const counts = new Map;
2718
+ for (const row of rows) {
2719
+ const key = row.statusCode === null ? "timeout/no response" : String(row.statusCode);
2720
+ counts.set(key, (counts.get(key) ?? 0) + 1);
2721
+ }
2722
+ return [...counts.entries()].map(([key, n]) => `${n}× ${key}`).join(", ");
2723
+ }
2724
+ var RETRY_AFTER_RE = /retry-after:?\s*(\d+)/i;
2725
+ function extractRetryAfterSeconds(rows) {
2726
+ for (const row of rows) {
2727
+ if (row.statusCode !== 429 || !row.errorMessage)
2728
+ continue;
2729
+ const match = row.errorMessage.match(RETRY_AFTER_RE);
2730
+ if (match?.[1])
2731
+ return match[1];
2732
+ }
2733
+ return null;
2734
+ }
2735
+ function detectReceiverRateLimited(webhook, deliveries) {
2736
+ if (deliveries.length < RATE_LIMITED_MIN_ATTEMPTS)
2737
+ return null;
2738
+ const count429 = deliveries.filter((d) => d.statusCode === 429).length;
2739
+ if (count429 / deliveries.length < RATE_LIMITED_MIN_RATIO)
2740
+ return null;
2741
+ const oldest = deliveries[deliveries.length - 1];
2742
+ const newest = deliveries[0];
2743
+ const evidence = [
2744
+ {
2745
+ label: "429 responses",
2746
+ value: `${count429} of ${deliveries.length} attempts`
2747
+ },
2748
+ {
2749
+ label: "window",
2750
+ value: oldest && newest ? `${formatShortDate(oldest.dispatchedAt)} → ${formatShortDate(newest.dispatchedAt)}` : "unknown"
2751
+ }
2752
+ ];
2753
+ const retryAfter = extractRetryAfterSeconds(deliveries);
2754
+ if (retryAfter) {
2755
+ evidence.push({ label: "latest Retry-After", value: `${retryAfter}s` });
2756
+ }
2757
+ const suggestedConcurrency = Math.max(1, Math.floor(webhook.concurrency / 2));
2758
+ return {
2759
+ code: "receiver_rate_limited",
2760
+ severity: "warn",
2761
+ evidence,
2762
+ fix: {
2763
+ text: "Lower concurrency, or honor the Retry-After header on your receiver.",
2764
+ command: `secondlayer webhooks update ${webhook.id} --concurrency ${suggestedConcurrency}`,
2765
+ docsPath: "/docs/webhooks#receiver-rate-limited"
2766
+ }
2767
+ };
2768
+ }
2769
+ function detectReceiverDown(webhook, deliveries) {
2770
+ const streak = consecutiveFromStart(deliveries, (d) => d.statusCode === null || d.statusCode >= 500);
2771
+ if (streak.length < DOWN_MIN_CONSECUTIVE)
2772
+ return null;
2773
+ return {
2774
+ code: "receiver_down",
2775
+ severity: "bad",
2776
+ evidence: [
2777
+ { label: "consecutive failures", value: String(streak.length) },
2778
+ { label: "breakdown", value: summarizeStatusBreakdown(streak) },
2779
+ {
2780
+ label: "last success",
2781
+ value: webhook.lastSuccessAt ? formatShortDate(webhook.lastSuccessAt) : "never"
2782
+ }
2783
+ ],
2784
+ fix: {
2785
+ text: "Check your receiver's health and logs, then send a test event.",
2786
+ command: `secondlayer webhooks test ${webhook.id}`,
2787
+ docsPath: "/docs/webhooks#receiver-down"
2788
+ }
2789
+ };
2790
+ }
2791
+ function detectReceiverRejects(webhook, deliveries) {
2792
+ const streak = consecutiveFromStart(deliveries, (d) => d.statusCode !== null && d.statusCode >= 400 && d.statusCode < 500 && d.statusCode !== 429);
2793
+ if (streak.length < REJECTS_MIN_CONSECUTIVE)
2794
+ return null;
2795
+ const evidence = [
2796
+ { label: "consecutive rejects", value: String(streak.length) },
2797
+ { label: "breakdown", value: summarizeStatusBreakdown(streak) }
2798
+ ];
2799
+ const withText = streak.find((d) => d.errorMessage || d.responseBody);
2800
+ const snippet = withText ? (withText.errorMessage ?? withText.responseBody ?? "").slice(0, ERROR_SNIPPET_MAX) : null;
2801
+ if (snippet)
2802
+ evidence.push({ label: "first error", value: snippet });
2803
+ return {
2804
+ code: "receiver_rejects",
2805
+ severity: "bad",
2806
+ evidence,
2807
+ fix: {
2808
+ text: "Check your receiver's auth config or signature verification.",
2809
+ command: `secondlayer webhooks test ${webhook.id}`,
2810
+ docsPath: "/docs/webhooks#receiver-rejects"
2811
+ }
2812
+ };
2813
+ }
2814
+ function detectReceiverSlow(webhook, deliveries) {
2815
+ const durations = deliveries.slice(0, SLOW_WINDOW).map((d) => d.durationMs).filter((d) => d !== null);
2816
+ if (durations.length < SLOW_MIN_SAMPLES)
2817
+ return null;
2818
+ const med = median(durations);
2819
+ if (med < webhook.timeoutMs * SLOW_TIMEOUT_RATIO)
2820
+ return null;
2821
+ return {
2822
+ code: "receiver_slow",
2823
+ severity: "warn",
2824
+ evidence: [
2825
+ { label: "median response time", value: formatMs(med) },
2826
+ {
2827
+ label: "p95 response time",
2828
+ value: formatMs(percentile(durations, 0.95))
2829
+ },
2830
+ { label: "timeout", value: formatMs(webhook.timeoutMs) }
2831
+ ],
2832
+ fix: {
2833
+ text: "Speed up your receiver, or raise the timeout (up to 30s hosted).",
2834
+ command: `secondlayer webhooks update ${webhook.id} --timeout-ms 30000`,
2835
+ docsPath: "/docs/webhooks#receiver-slow"
2836
+ }
2837
+ };
2838
+ }
2839
+ function detectDeliveryLag(deliveries) {
2840
+ const window = deliveries.slice(0, LAG_WINDOW).filter((d) => d.blockTime !== null);
2841
+ if (window.length < LAG_MIN_SAMPLES)
2842
+ return null;
2843
+ const lags = window.map((d) => new Date(d.dispatchedAt).getTime() - new Date(d.blockTime).getTime());
2844
+ const med = median(lags);
2845
+ if (med <= LAG_THRESHOLD_MS)
2846
+ return null;
2847
+ const blockTimes = window.map((d) => d.blockTime).sort();
2848
+ const oldest = blockTimes[0];
2849
+ const newest = blockTimes[blockTimes.length - 1];
2850
+ return {
2851
+ code: "delivery_lag",
2852
+ severity: "warn",
2853
+ evidence: [
2854
+ { label: "median lag", value: formatMs(med) },
2855
+ {
2856
+ label: "block time range",
2857
+ value: oldest && newest ? `${formatShortDate(oldest)} → ${formatShortDate(newest)}` : "unknown"
2858
+ }
2859
+ ],
2860
+ fix: {
2861
+ text: "We're behind on delivery; no action needed on your end."
2862
+ }
2863
+ };
2864
+ }
2865
+ var SEVERITY_RANK = {
2866
+ bad: 0,
2867
+ warn: 1,
2868
+ calm: 2
2869
+ };
2870
+ var CODE_PRIORITY = [
2871
+ "circuit",
2872
+ "receiver_down",
2873
+ "receiver_rejects",
2874
+ "dead_letters",
2875
+ "paused",
2876
+ "last_error",
2877
+ "warning",
2878
+ "receiver_rate_limited",
2879
+ "receiver_slow",
2880
+ "delivery_lag",
2881
+ "no_deliveries",
2882
+ "subgraph_gaps",
2883
+ "subgraph_catching_up"
2884
+ ];
2885
+ function pickPrimary(issues) {
2886
+ if (issues.length === 0)
2887
+ return null;
2888
+ return [...issues].sort((a, b) => {
2889
+ const sevDiff = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
2890
+ if (sevDiff !== 0)
2891
+ return sevDiff;
2892
+ return CODE_PRIORITY.indexOf(a.code) - CODE_PRIORITY.indexOf(b.code);
2893
+ })[0] ?? null;
2894
+ }
2895
+ function hintFor(issue) {
2896
+ if (!issue.fix)
2897
+ return issue.detail ?? issue.code;
2898
+ return issue.fix.command ? `${issue.fix.text} ${issue.fix.command}` : issue.fix.text;
2899
+ }
2673
2900
  function buildDoctorReport(input) {
2674
2901
  const successful = input.deliveries.filter(isSuccessDelivery).length;
2675
2902
  const failed = input.deliveries.length - successful;
@@ -2686,35 +2913,60 @@ function buildDoctorReport(input) {
2686
2913
  const issues = [];
2687
2914
  if (input.webhook.warning) {
2688
2915
  hints.push(input.webhook.warning);
2689
- issues.push({ code: "warning", detail: input.webhook.warning });
2916
+ issues.push({
2917
+ code: "warning",
2918
+ detail: input.webhook.warning,
2919
+ severity: "warn"
2920
+ });
2690
2921
  }
2691
2922
  if (input.webhook.status === "paused") {
2692
2923
  hints.push(`Resume when the receiver is healthy: secondlayer webhooks resume ${input.webhook.id}`);
2693
- issues.push({ code: "paused" });
2924
+ issues.push({ code: "paused", severity: "warn" });
2694
2925
  }
2695
2926
  if (input.webhook.lastError) {
2696
2927
  hints.push("Run secondlayer webhooks test to reproduce the receiver request.");
2697
- issues.push({ code: "last_error", detail: input.webhook.lastError });
2928
+ issues.push({
2929
+ code: "last_error",
2930
+ detail: input.webhook.lastError,
2931
+ severity: "warn"
2932
+ });
2698
2933
  }
2699
2934
  if (input.webhook.circuitOpenedAt || input.webhook.circuitFailures > 0) {
2700
2935
  hints.push("Circuit breaker has failures; inspect receiver logs and delivery status codes.");
2701
- issues.push({ code: "circuit" });
2936
+ issues.push({ code: "circuit", severity: "bad" });
2702
2937
  }
2703
2938
  if (input.dead.length > 0) {
2704
2939
  hints.push(`Dead-letter rows exist; inspect with secondlayer webhooks dead ${input.webhook.id} and requeue selected rows.`);
2705
- issues.push({ code: "dead_letters" });
2940
+ issues.push({ code: "dead_letters", severity: "bad" });
2706
2941
  }
2707
2942
  if (subgraph?.gapCount && subgraph.gapCount > 0) {
2708
2943
  hints.push(`Linked subgraph has gaps; run secondlayer subgraphs gaps ${input.webhook.subgraphName}.`);
2709
- issues.push({ code: "subgraph_gaps" });
2944
+ issues.push({ code: "subgraph_gaps", severity: "calm" });
2710
2945
  }
2711
2946
  if (subgraph?.syncStatus === "catching_up") {
2712
2947
  hints.push("Linked subgraph is still catching up; new matching rows may arrive later.");
2713
- issues.push({ code: "subgraph_catching_up" });
2948
+ issues.push({ code: "subgraph_catching_up", severity: "calm" });
2949
+ }
2950
+ for (const detector of [
2951
+ detectReceiverRateLimited,
2952
+ detectReceiverDown,
2953
+ detectReceiverRejects,
2954
+ detectReceiverSlow
2955
+ ]) {
2956
+ const issue = detector(input.webhook, input.deliveries);
2957
+ if (issue) {
2958
+ issues.push(issue);
2959
+ hints.push(hintFor(issue));
2960
+ }
2961
+ }
2962
+ const lagIssue = detectDeliveryLag(input.deliveries);
2963
+ if (lagIssue) {
2964
+ issues.push(lagIssue);
2965
+ hints.push(hintFor(lagIssue));
2714
2966
  }
2715
2967
  if (input.deliveries.length === 0) {
2716
2968
  hints.push("No deliveries yet; confirm the table is receiving inserted rows that match the filter.");
2717
- issues.push({ code: "no_deliveries" });
2969
+ issues.push({ code: "no_deliveries", severity: "calm" });
2718
2970
  }
2719
2971
  if (hints.length === 0) {
2720
2972
  hints.push("No immediate action needed.");
@@ -2730,9 +2982,35 @@ function buildDoctorReport(input) {
2730
2982
  deadCount: input.dead.length,
2731
2983
  subgraph,
2732
2984
  hints,
2733
- issues
2985
+ issues,
2986
+ primary: pickPrimary(issues)
2734
2987
  };
2735
2988
  }
2989
+ function buildListIssue(webhook) {
2990
+ if (webhook.status !== "paused")
2991
+ return null;
2992
+ if (webhook.circuitOpenedAt) {
2993
+ return {
2994
+ code: "circuit",
2995
+ severity: "bad",
2996
+ evidence: [
2997
+ {
2998
+ label: "circuit opened",
2999
+ value: formatShortDate(webhook.circuitOpenedAt)
3000
+ },
3001
+ {
3002
+ label: "last success",
3003
+ value: webhook.lastSuccessAt ? formatShortDate(webhook.lastSuccessAt) : "never"
3004
+ }
3005
+ ],
3006
+ fix: {
3007
+ text: "Check your receiver's health and logs, then send a test event.",
3008
+ docsPath: "/docs/webhooks#receiver-down"
3009
+ }
3010
+ };
3011
+ }
3012
+ return { code: "paused", severity: "warn" };
3013
+ }
2736
3014
  // src/consumer-ops.ts
2737
3015
  function consumerHealth(options = {}) {
2738
3016
  const staleAfterMs = options.staleAfterMs ?? 120000;
@@ -2986,6 +3264,7 @@ export {
2986
3264
  ValidationError,
2987
3265
  Webhooks,
2988
3266
  buildDoctorReport,
3267
+ buildListIssue,
2989
3268
  consumeIndexFeed,
2990
3269
  consumerHealth,
2991
3270
  contextField,
@@ -3032,5 +3311,5 @@ export {
3032
3311
  verifyWebhookSignature
3033
3312
  };
3034
3313
 
3035
- //# debugId=AE812DACE82FC03C64756E2164756E21
3314
+ //# debugId=17DA719CAEA1958964756E2164756E21
3036
3315
  //# sourceMappingURL=index.js.map