@siglume/direct-request-payment 0.4.7 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.9 - 2026-06-19
4
+
5
+ Classifier consistency patch release.
6
+
7
+ - Required Micro / Nano settlement batch confirmations to carry aggregated
8
+ on-chain finality, a valid Micro/Nano pricing band, and the expected weekly or
9
+ monthly settlement cadence before returning `metered_batch_settled`.
10
+ - Added TypeScript and Python tests for missing finality, wrong finality,
11
+ missing pricing band, and Micro/Nano cadence mismatches.
12
+
13
+ ## 0.4.8 - 2026-06-19
14
+
15
+ Webhook sample hardening release.
16
+
17
+ - Added TypeScript and Python confirmation classifiers for Standard settled
18
+ payments, Micro / Nano accepted usage, and Micro / Nano settled batches.
19
+ - Updated public webhook samples to require finality, settlement status, and
20
+ non-empty settlement identifiers before fulfilling or reconciling.
21
+ - Routed unknown Micro / Nano challenge hashes and malformed settlement batch
22
+ confirmations to manual review in copy-paste samples.
23
+
3
24
  ## 0.4.7 - 2026-06-19
4
25
 
5
26
  Documentation-only patch release.
package/README.md CHANGED
@@ -103,10 +103,10 @@ const session = await merchant.createCheckoutSession({
103
103
  });
104
104
  redirect(session.checkout_url); // -> https://siglume.com/pay/<session_id>
105
105
 
106
- // 3. Handle the signed direct_payment.confirmed webhook. Fulfill Standard only
107
- // when pricing_band=standard, finality=per_payment_onchain, and
108
- // settlement_status=settled. Treat Micro / Nano as accepted but unsettled
109
- // until the later metered settlement batch is settled.
106
+ // 3. Handle the signed direct_payment.confirmed webhook. Use
107
+ // classifyDirectPaymentConfirmation(event). Fulfill Standard only for
108
+ // standard_settled; treat metered_usage_accepted as fulfilled-unsettled
109
+ // until the later metered_batch_settled event arrives.
110
110
  // Poll merchant.getCheckoutSession(session.session_id) if you also want to
111
111
  // show status in your own UI.
112
112
  ```
@@ -138,10 +138,10 @@ session = merchant.create_checkout_session(
138
138
  )
139
139
  redirect(session["checkout_url"]) # -> https://siglume.com/pay/<session_id>
140
140
 
141
- # 3. Handle the signed direct_payment.confirmed webhook. Fulfill Standard only
142
- # when pricing_band=standard, finality=per_payment_onchain, and
143
- # settlement_status=settled. Treat Micro / Nano as accepted but unsettled
144
- # until the later metered settlement batch is settled.
141
+ # 3. Handle the signed direct_payment.confirmed webhook. Use
142
+ # classify_direct_payment_confirmation(event). Fulfill Standard only for
143
+ # standard_settled; treat metered_usage_accepted as fulfilled-unsettled
144
+ # until the later metered_batch_settled event arrives.
145
145
  # Poll merchant.get_checkout_session(session["session_id"]) if you also want
146
146
  # to show status in your own UI.
147
147
  ```
@@ -525,7 +525,10 @@ the payload. Create a marketplace webhook subscription with
525
525
  signing secret once.
526
526
 
527
527
  ```ts
528
- import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
528
+ import {
529
+ classifyDirectPaymentConfirmation,
530
+ verifyDirectRequestPaymentWebhook,
531
+ } from "@siglume/direct-request-payment";
529
532
 
530
533
  const { event } = await verifyDirectRequestPaymentWebhook(
531
534
  process.env.SIGLUME_WEBHOOK_SECRET!,
@@ -534,18 +537,16 @@ const { event } = await verifyDirectRequestPaymentWebhook(
534
537
  );
535
538
 
536
539
  if (event.type === "direct_payment.confirmed") {
537
- if (event.data.mode === "metered_settlement_batch") {
540
+ const confirmation = classifyDirectPaymentConfirmation(event);
541
+ if (confirmation.kind === "metered_batch_settled") {
538
542
  // Reconcile settled Micro / Nano batches by settlement_batch_id /
539
543
  // usage_event_digest; these events do not carry an order challenge hash.
540
- } else if (
541
- event.data.pricing_band === "standard" &&
542
- event.data.finality === "per_payment_onchain" &&
543
- event.data.settlement_status === "settled"
544
- ) {
544
+ } else if (confirmation.kind === "standard_settled") {
545
545
  // Mark the order paid once if event.data.challenge_hash/order mapping matches.
546
- } else if (event.data.pricing_band === "micro" || event.data.pricing_band === "nano") {
547
- // Mark fulfilled-but-unsettled only if your business allows fulfillment
548
- // before the aggregated Micro / Nano settlement succeeds.
546
+ } else if (confirmation.kind === "metered_usage_accepted") {
547
+ // Mark fulfilled-but-unsettled after matching confirmation.challenge_hash.
548
+ } else {
549
+ // Route confirmation.reason to manual review. Do not mark paid or fulfilled.
549
550
  }
550
551
  }
551
552
  ```
@@ -553,7 +554,10 @@ if (event.type === "direct_payment.confirmed") {
553
554
  ```py
554
555
  import os
555
556
 
556
- from siglume_direct_request_payment import verify_direct_request_payment_webhook
557
+ from siglume_direct_request_payment import (
558
+ classify_direct_payment_confirmation,
559
+ verify_direct_request_payment_webhook,
560
+ )
557
561
 
558
562
  verified = verify_direct_request_payment_webhook(
559
563
  os.environ["SIGLUME_WEBHOOK_SECRET"],
@@ -562,30 +566,30 @@ verified = verify_direct_request_payment_webhook(
562
566
  )
563
567
 
564
568
  if verified["event"]["type"] == "direct_payment.confirmed":
565
- data = verified["event"]["data"]
566
- if data.get("mode") == "metered_settlement_batch":
569
+ confirmation = classify_direct_payment_confirmation(verified["event"])
570
+ if confirmation["kind"] == "metered_batch_settled":
567
571
  # Reconcile settled Micro / Nano batches by settlement_batch_id /
568
572
  # usage_event_digest; these events do not carry an order challenge hash.
569
573
  pass
570
- elif (
571
- data.get("pricing_band") == "standard"
572
- and data.get("finality") == "per_payment_onchain"
573
- and data.get("settlement_status") == "settled"
574
- ):
574
+ elif confirmation["kind"] == "standard_settled":
575
575
  # Mark the order paid once if event.data.challenge_hash/order mapping matches.
576
576
  pass
577
- elif data.get("pricing_band") in ("micro", "nano"):
578
- # Mark fulfilled-but-unsettled only if your business allows fulfillment
579
- # before the aggregated Micro / Nano settlement succeeds.
577
+ elif confirmation["kind"] == "metered_usage_accepted":
578
+ # Mark fulfilled-but-unsettled after matching confirmation["challenge_hash"].
579
+ pass
580
+ else:
581
+ # Route confirmation["reason"] to manual review. Do not mark paid or fulfilled.
580
582
  pass
581
583
  ```
582
584
 
583
585
  New `direct_payment.confirmed` payloads include `pricing_band`,
584
586
  `settlement_cadence`, `finality`, `protocol_fee_minor`, `settlement_status`,
585
587
  `settlement_batch_id`, `chain_receipt_id`, `usage_event_digest`, `settled_at`,
586
- and when available `request_hash_v2`. Use these machine fields instead of
587
- inferring settlement semantics from the event name alone. Do not mark an order
588
- paid from the event type alone.
588
+ and when available `request_hash_v2`. Use
589
+ `classifyDirectPaymentConfirmation(event)` /
590
+ `classify_direct_payment_confirmation(event)` or the same machine-field checks
591
+ instead of inferring settlement semantics from the event name alone. Do not mark
592
+ an order paid from the event type alone.
589
593
 
590
594
  ## Security Rules
591
595
 
package/dist/index.cjs CHANGED
@@ -34,11 +34,15 @@ __export(src_exports, {
34
34
  DEFAULT_WEBHOOK_TOLERANCE_SECONDS: () => DEFAULT_WEBHOOK_TOLERANCE_SECONDS,
35
35
  DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND: () => DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND,
36
36
  DIRECT_REQUEST_PAYMENT_CHALLENGE_SCHEME: () => DIRECT_REQUEST_PAYMENT_CHALLENGE_SCHEME,
37
+ DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS: () => DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS,
38
+ DIRECT_REQUEST_PAYMENT_METERED_FINALITY: () => DIRECT_REQUEST_PAYMENT_METERED_FINALITY,
37
39
  DIRECT_REQUEST_PAYMENT_MODE: () => DIRECT_REQUEST_PAYMENT_MODE,
38
40
  DIRECT_REQUEST_PAYMENT_RECEIPT_KIND: () => DIRECT_REQUEST_PAYMENT_RECEIPT_KIND,
39
41
  DIRECT_REQUEST_PAYMENT_RECURRING_CHALLENGE_SCHEME: () => DIRECT_REQUEST_PAYMENT_RECURRING_CHALLENGE_SCHEME,
40
42
  DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE: () => DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE,
41
43
  DIRECT_REQUEST_PAYMENT_SDK_VERSION: () => DIRECT_REQUEST_PAYMENT_SDK_VERSION,
44
+ DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY: () => DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY,
45
+ DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS: () => DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS,
42
46
  DirectRequestPaymentClient: () => DirectRequestPaymentClient,
43
47
  DirectRequestPaymentMerchantClient: () => DirectRequestPaymentMerchantClient,
44
48
  HostedCheckoutNotAvailableError: () => HostedCheckoutNotAvailableError,
@@ -50,6 +54,7 @@ __export(src_exports, {
50
54
  buildPaymentExecutionPayload: () => buildPaymentExecutionPayload,
51
55
  buildPreparedTransactionExecutionPayload: () => buildPreparedTransactionExecutionPayload,
52
56
  buildWebhookSignatureHeader: () => buildWebhookSignatureHeader,
57
+ classifyDirectPaymentConfirmation: () => classifyDirectPaymentConfirmation,
53
58
  computeWebhookSignature: () => computeWebhookSignature,
54
59
  createDirectRequestPaymentChallenge: () => createDirectRequestPaymentChallenge,
55
60
  createDirectRequestPaymentChallengeSignature: () => createDirectRequestPaymentChallengeSignature,
@@ -78,7 +83,11 @@ var DIRECT_REQUEST_PAYMENT_RECEIPT_KIND = "sdrp_direct_payment";
78
83
  var DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND = "sdrp_direct_payment_allowance";
79
84
  var DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE = "sdrp_direct_payment_requirement";
80
85
  var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
81
- var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.7";
86
+ var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.9";
87
+ var DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS = "settled";
88
+ var DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS = "pending_settlement";
89
+ var DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY = "per_payment_onchain";
90
+ var DIRECT_REQUEST_PAYMENT_METERED_FINALITY = "aggregated_onchain_settlement";
82
91
  var DIRECT_REQUEST_PAYMENT_CONFIRMED_WEBHOOK_MODES = /* @__PURE__ */ new Set([DIRECT_REQUEST_PAYMENT_MODE, "metered_settlement_batch"]);
83
92
  var SiglumeDirectRequestPaymentError = class extends Error {
84
93
  constructor(message) {
@@ -671,6 +680,120 @@ function parseDirectRequestPaymentWebhookEvent(payload) {
671
680
  }
672
681
  return parsed;
673
682
  }
683
+ function classifyDirectPaymentConfirmation(event) {
684
+ const data = event.data;
685
+ const requirementId = stringOrNull(data.requirement_id) ?? stringOrNull(data.direct_payment_requirement_id);
686
+ const challengeHash = stringOrNull(data.challenge_hash);
687
+ const pricingBand = stringOrNull(data.pricing_band);
688
+ const settlementCadence = stringOrNull(data.settlement_cadence);
689
+ const finality = stringOrNull(data.finality);
690
+ const settlementStatus = stringOrNull(data.settlement_status);
691
+ if (event.type !== "direct_payment.confirmed") {
692
+ return {
693
+ kind: "unknown",
694
+ event,
695
+ data,
696
+ reason: "not_direct_payment_confirmed",
697
+ requirement_id: requirementId,
698
+ settlement_batch_id: stringOrNull(data.settlement_batch_id),
699
+ pricing_band: pricingBand,
700
+ settlement_cadence: settlementCadence,
701
+ settlement_status: settlementStatus,
702
+ finality
703
+ };
704
+ }
705
+ if (data.mode === "metered_settlement_batch") {
706
+ const settlementBatchId = stringOrNull(data.settlement_batch_id);
707
+ const chainReceiptId = stringOrNull(data.chain_receipt_id);
708
+ const usageEventDigest = stringOrNull(data.usage_event_digest);
709
+ if (settlementStatus === DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS && finality === DIRECT_REQUEST_PAYMENT_METERED_FINALITY && (pricingBand === "micro" || pricingBand === "nano") && settlementCadence === (pricingBand === "micro" ? "weekly" : "monthly") && settlementBatchId && chainReceiptId && usageEventDigest) {
710
+ return {
711
+ kind: "metered_batch_settled",
712
+ event,
713
+ data,
714
+ pricing_band: pricingBand,
715
+ settlement_cadence: pricingBand === "micro" ? "weekly" : "monthly",
716
+ settlement_batch_id: settlementBatchId,
717
+ chain_receipt_id: chainReceiptId,
718
+ usage_event_digest: usageEventDigest,
719
+ settled_at: stringOrNull(data.settled_at)
720
+ };
721
+ }
722
+ return {
723
+ kind: "unknown",
724
+ event,
725
+ data,
726
+ reason: "invalid_metered_settlement_confirmation",
727
+ requirement_id: requirementId,
728
+ settlement_batch_id: settlementBatchId,
729
+ pricing_band: pricingBand,
730
+ settlement_cadence: settlementCadence,
731
+ settlement_status: settlementStatus,
732
+ finality
733
+ };
734
+ }
735
+ if (pricingBand === "standard") {
736
+ const chainReceiptId = stringOrNull(data.chain_receipt_id);
737
+ if (finality === DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY && settlementStatus === DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS && requirementId && challengeHash && chainReceiptId) {
738
+ return {
739
+ kind: "standard_settled",
740
+ event,
741
+ data,
742
+ requirement_id: requirementId,
743
+ challenge_hash: challengeHash,
744
+ chain_receipt_id: chainReceiptId,
745
+ request_hash_v2: stringOrNull(data.request_hash_v2)
746
+ };
747
+ }
748
+ return {
749
+ kind: "unknown",
750
+ event,
751
+ data,
752
+ reason: "missing_standard_settlement_fields",
753
+ requirement_id: requirementId,
754
+ pricing_band: pricingBand,
755
+ settlement_cadence: settlementCadence,
756
+ settlement_status: settlementStatus,
757
+ finality
758
+ };
759
+ }
760
+ if (pricingBand === "micro" || pricingBand === "nano") {
761
+ if (finality === DIRECT_REQUEST_PAYMENT_METERED_FINALITY && settlementStatus === DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS && requirementId && challengeHash) {
762
+ return {
763
+ kind: "metered_usage_accepted",
764
+ event,
765
+ data,
766
+ pricing_band: pricingBand,
767
+ requirement_id: requirementId,
768
+ challenge_hash: challengeHash,
769
+ request_hash_v2: stringOrNull(data.request_hash_v2)
770
+ };
771
+ }
772
+ return {
773
+ kind: "unknown",
774
+ event,
775
+ data,
776
+ reason: "missing_metered_usage_fields",
777
+ requirement_id: requirementId,
778
+ pricing_band: pricingBand,
779
+ settlement_cadence: settlementCadence,
780
+ settlement_status: settlementStatus,
781
+ finality
782
+ };
783
+ }
784
+ return {
785
+ kind: "unknown",
786
+ event,
787
+ data,
788
+ reason: "unknown_confirmation_shape",
789
+ requirement_id: requirementId,
790
+ settlement_batch_id: stringOrNull(data.settlement_batch_id),
791
+ pricing_band: pricingBand,
792
+ settlement_cadence: settlementCadence,
793
+ settlement_status: settlementStatus,
794
+ finality
795
+ };
796
+ }
674
797
  async function verifyDirectRequestPaymentWebhook(signing_secret, body, signature_header, options = {}) {
675
798
  const verification = await verifyWebhookSignature(signing_secret, body, signature_header, options);
676
799
  const text = new TextDecoder().decode(bodyBytes(body));