@siglume/direct-request-payment 0.4.3 → 0.4.4

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.4 - 2026-06-19
4
+
5
+ Correctness and security hardening release for the SDRP Direct Request Payment
6
+ SDK manual and public helpers.
7
+
8
+ - Clarified that Standard / Micro / Nano are selected by amount, removed the
9
+ unsupported "force Standard for immediate finality" implication, and changed
10
+ Hosted Checkout Standard examples to Standard-band amounts.
11
+ - Removed the Express sample route that accepted a buyer `Authorization` header
12
+ on the merchant server; human web checkout now redirects through Hosted
13
+ Checkout, while agent payment remains buyer-side direct API/tool work.
14
+ - Documented Micro / Nano decimal fee rounding, `rounding_delta_minor`, budget
15
+ reservation versus token locking, no guaranteed `past_due` collection, HTTP
16
+ result accounting, and operational status handling.
17
+ - Added typed TypeScript and named Python helpers for Micro / Nano statement
18
+ APIs.
19
+ - Added `request_hash_v2` helpers/docs and documented the new
20
+ requirement/webhook machine fields for pricing band, settlement cadence,
21
+ finality, protocol fee, and settlement status.
22
+ - Hardened TS/Python integer and `checkout_allowed_origins` validation.
23
+
3
24
  ## 0.4.3 - 2026-06-19
4
25
 
5
26
  Documentation-only release for SDRP Micro / Nano operations.
package/README.md CHANGED
@@ -89,7 +89,7 @@ await merchant.setupMerchant({
89
89
  // 2. Per order: create a session and redirect the shopper to checkout_url.
90
90
  const session = await merchant.createCheckoutSession({
91
91
  merchant: "your_merchant_key",
92
- amount_minor: 500, // server-fixed; the browser cannot change it
92
+ amount_minor: 1200, // server-fixed; the browser cannot change it
93
93
  currency: "JPY",
94
94
  nonce: order.id, // unique per order
95
95
  success_url: "https://www.your-shop.com/thanks",
@@ -121,7 +121,7 @@ merchant.setup_merchant(
121
121
  # 2. Per order: create a session and redirect the shopper to checkout_url.
122
122
  session = merchant.create_checkout_session(
123
123
  merchant="your_merchant_key",
124
- amount_minor=500, # server-fixed; the browser cannot change it
124
+ amount_minor=1200, # server-fixed; the browser cannot change it
125
125
  currency="JPY",
126
126
  nonce=order["id"], # unique per order
127
127
  success_url="https://www.your-shop.com/thanks",
@@ -183,6 +183,11 @@ Provider revenue in the Micro and Nano bands is not settled revenue until the
183
183
  weekly or monthly on-chain settlement succeeds. Siglume keeps outstanding failed
184
184
  settlements for retry under the published policy, but does not advance or
185
185
  guarantee provider revenue before settlement succeeds.
186
+ Micro / Nano budget checks reserve spending capacity only; they do not lock,
187
+ escrow, or guarantee the buyer's wallet balance, allowance, or settlement funds.
188
+ Sub-minor-unit Nano fees are accumulated with decimal precision and rounded only
189
+ when a settlement batch is created; see [Pricing](./docs/pricing.md) for the
190
+ rounding formula and `rounding_delta_minor` semantics.
186
191
  For operational reconciliation, expected revenue, settled revenue, retry state,
187
192
  and CSV exports, see
188
193
  [docs/metered-statements.md](./docs/metered-statements.md).
@@ -230,14 +235,20 @@ amounts differ.
230
235
 
231
236
  | Payment amount | Applied automatically | What you select | Fee | Settlement |
232
237
  | --- | --- | --- | --- | --- |
233
- | Over JPY 500 / over USD 3.00, or whenever immediate finality is required | Standard Payment | Select one Standard plan: Launch, Starter, Growth, or Pro | Launch: JPY 0 / USD 0 monthly, 1.8%; Starter: JPY 980 / USD 6 monthly, 1.0%; Growth: JPY 2,980 / USD 18 monthly, 0.7%; Pro: JPY 9,800 / USD 60 monthly, 0.5%. Minimum JPY 30 / USD 0.20 per payment. | Settled on-chain immediately after the payment confirms |
238
+ | Over JPY 500 / over USD 3.00 | Standard Payment | Select one Standard plan: Launch, Starter, Growth, or Pro | Launch: JPY 0 / USD 0 monthly, 1.8%; Starter: JPY 980 / USD 6 monthly, 1.0%; Growth: JPY 2,980 / USD 18 monthly, 0.7%; Pro: JPY 9,800 / USD 60 monthly, 0.5%. Minimum JPY 30 / USD 0.20 per payment. | Settled on-chain immediately after the payment confirms |
234
239
  | JPY 50-500 / over USD 0.30 and up to USD 3.00 | Micro Payment | Applied automatically by amount | USD 0.01 / Tx, about JPY 2 | Weekly settlement - see [Settlement schedule](./docs/pricing.md#settlement-schedule) |
235
240
  | Under JPY 50 / up to USD 0.30 | Nano Payment | Applied automatically by amount | USD 0.001 / usage, about JPY 0.2 | Monthly settlement - see [Settlement schedule](./docs/pricing.md#settlement-schedule) |
236
241
 
237
242
  A merchant billing mandate is required before accepting payments, even on the
238
- Launch plan. `fee_bps` returned on a payment requirement is the authoritative
239
- fee rate for that payment in the merchant's settlement currency. The full fee
240
- table and the weekly / monthly settlement schedule live in
243
+ Launch plan. The current public API does not expose a flag that forces a
244
+ JPY 500-and-under / USD 3-and-under payment into Standard immediate settlement.
245
+ If immediate on-chain settlement is a hard requirement, price the item in the
246
+ Standard band or confirm a merchant-specific contract with Siglume before
247
+ launch. For Standard Payment, `fee_bps` returned on a payment requirement is the
248
+ authoritative fee rate for that payment in the merchant's settlement currency.
249
+ For Micro / Nano, the statement APIs expose `protocol_fee_minor`,
250
+ `gross_buyer_debit_minor`, `buyer_debit_minor`, and `rounding_delta_minor`.
251
+ The full fee table and the weekly / monthly settlement schedule live in
241
252
  [docs/pricing.md](./docs/pricing.md). Statement APIs for "how much was used,
242
253
  when will it close, when can it debit, and what is settled" are documented in
243
254
  [docs/metered-statements.md](./docs/metered-statements.md).
@@ -516,6 +527,11 @@ if verified["event"]["type"] == "direct_payment.confirmed":
516
527
  pass
517
528
  ```
518
529
 
530
+ New `direct_payment.confirmed` payloads include `pricing_band`,
531
+ `settlement_cadence`, `finality`, `protocol_fee_minor`, `settlement_status`, and
532
+ when available `request_hash_v2`. Use these machine fields instead of inferring
533
+ settlement semantics from the event name alone.
534
+
519
535
  ## Security Rules
520
536
 
521
537
  - Keep the challenge secret on the merchant server only.
@@ -538,7 +554,8 @@ Read [docs/security.md](./docs/security.md) before going live.
538
554
  - Store the returned `SIGLUME_WEBHOOK_SECRET` only on the merchant server.
539
555
  - Persist `challenge_hash`, `requirement_id`, and fulfillment state per order.
540
556
  - Fulfill orders only from verified webhook data, with idempotency.
541
- - Treat `fee_bps` returned by Siglume as the runtime fee source of truth.
557
+ - Treat `fee_bps` returned by Siglume as the Standard Payment runtime fee source
558
+ of truth; use statement API amount fields for Micro / Nano.
542
559
 
543
560
  ## Compatibility Notes
544
561
 
package/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ __export(src_exports, {
38
38
  DIRECT_REQUEST_PAYMENT_RECEIPT_KIND: () => DIRECT_REQUEST_PAYMENT_RECEIPT_KIND,
39
39
  DIRECT_REQUEST_PAYMENT_RECURRING_CHALLENGE_SCHEME: () => DIRECT_REQUEST_PAYMENT_RECURRING_CHALLENGE_SCHEME,
40
40
  DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE: () => DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE,
41
+ DIRECT_REQUEST_PAYMENT_SDK_VERSION: () => DIRECT_REQUEST_PAYMENT_SDK_VERSION,
41
42
  DirectRequestPaymentClient: () => DirectRequestPaymentClient,
42
43
  DirectRequestPaymentMerchantClient: () => DirectRequestPaymentMerchantClient,
43
44
  HostedCheckoutNotAvailableError: () => HostedCheckoutNotAvailableError,
@@ -58,6 +59,7 @@ __export(src_exports, {
58
59
  createExternal402RecurringChallenge: () => createExternal402RecurringChallenge,
59
60
  directRequestPaymentChallengeHash: () => directRequestPaymentChallengeHash,
60
61
  directRequestPaymentRequestHash: () => directRequestPaymentRequestHash,
62
+ directRequestPaymentRequestHashV2: () => directRequestPaymentRequestHashV2,
61
63
  parseDirectRequestPaymentChallenge: () => parseDirectRequestPaymentChallenge,
62
64
  parseDirectRequestPaymentWebhookEvent: () => parseDirectRequestPaymentWebhookEvent,
63
65
  verifyDirectRequestPaymentChallenge: () => verifyDirectRequestPaymentChallenge,
@@ -76,6 +78,8 @@ var DIRECT_REQUEST_PAYMENT_RECEIPT_KIND = "sdrp_direct_payment";
76
78
  var DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND = "sdrp_direct_payment_allowance";
77
79
  var DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE = "sdrp_direct_payment_requirement";
78
80
  var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
81
+ var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.4";
82
+ var DIRECT_REQUEST_PAYMENT_CONFIRMED_WEBHOOK_MODES = /* @__PURE__ */ new Set([DIRECT_REQUEST_PAYMENT_MODE, "metered_settlement_batch"]);
79
83
  var SiglumeDirectRequestPaymentError = class extends Error {
80
84
  constructor(message) {
81
85
  super(message);
@@ -132,7 +136,7 @@ var DirectRequestPaymentClient = class {
132
136
  this.auth_token = authToken;
133
137
  this.base_url = (options.base_url ?? envValue("SIGLUME_API_BASE") ?? DEFAULT_SIGLUME_API_BASE).replace(/\/+$/, "");
134
138
  this.timeout_ms = Math.max(1, Math.trunc(options.timeout_ms ?? 15e3));
135
- this.user_agent = options.user_agent ?? "@siglume/direct-request-payment/0.4.3";
139
+ this.user_agent = options.user_agent ?? `@siglume/direct-request-payment/${DIRECT_REQUEST_PAYMENT_SDK_VERSION}`;
136
140
  this.fetch_impl = fetchImpl;
137
141
  }
138
142
  async createPaymentRequirement(input) {
@@ -180,6 +184,53 @@ var DirectRequestPaymentClient = class {
180
184
  async executeAllowanceTransaction(requirement, options = {}) {
181
185
  return this.executePreparedTransaction(buildAllowanceExecutionPayload(requirement, options));
182
186
  }
187
+ async getBuyerMeteredSummary(input = {}) {
188
+ return this.request(
189
+ "GET",
190
+ meteredQueryPath("/sdrp/metered/my-summary", input)
191
+ );
192
+ }
193
+ async listBuyerUsageEvents(input = {}) {
194
+ return this.request(
195
+ "GET",
196
+ meteredQueryPath("/sdrp/metered/my-usage-events", input)
197
+ );
198
+ }
199
+ async listBuyerSettlementBatches(input = {}) {
200
+ return this.request(
201
+ "GET",
202
+ meteredQueryPath("/sdrp/metered/my-settlement-batches", input)
203
+ );
204
+ }
205
+ async getProviderMeteredSummary(input = {}) {
206
+ return this.request(
207
+ "GET",
208
+ meteredQueryPath("/sdrp/metered/provider/summary", input)
209
+ );
210
+ }
211
+ async listProviderUsageEvents(input = {}) {
212
+ return this.request(
213
+ "GET",
214
+ meteredQueryPath("/sdrp/metered/provider/usage-events", input)
215
+ );
216
+ }
217
+ async listProviderSettlementBatches(input = {}) {
218
+ return this.request(
219
+ "GET",
220
+ meteredQueryPath("/sdrp/metered/provider/settlement-batches", input)
221
+ );
222
+ }
223
+ async getProviderSettlementBatch(settlement_batch_id, input = {}) {
224
+ return this.request(
225
+ "GET",
226
+ meteredQueryPath(
227
+ `/sdrp/metered/provider/settlement-batches/${encodeURIComponent(
228
+ requireNonEmpty(settlement_batch_id, "settlement_batch_id")
229
+ )}`,
230
+ input
231
+ )
232
+ );
233
+ }
183
234
  async request(method, path, json_body) {
184
235
  const controller = new AbortController();
185
236
  const timeout = setTimeout(() => controller.abort(), this.timeout_ms);
@@ -237,7 +288,7 @@ var DirectRequestPaymentMerchantClient = class {
237
288
  this.auth_token = authToken;
238
289
  this.base_url = (options.base_url ?? envValue("SIGLUME_API_BASE") ?? DEFAULT_SIGLUME_API_BASE).replace(/\/+$/, "");
239
290
  this.timeout_ms = Math.max(1, Math.trunc(options.timeout_ms ?? 15e3));
240
- this.user_agent = options.user_agent ?? "@siglume/direct-request-payment/0.4.3";
291
+ this.user_agent = options.user_agent ?? `@siglume/direct-request-payment/${DIRECT_REQUEST_PAYMENT_SDK_VERSION}`;
241
292
  this.fetch_impl = fetchImpl;
242
293
  }
243
294
  async setupMerchant(input) {
@@ -530,6 +581,16 @@ async function directRequestPaymentRequestHash(input) {
530
581
  const material = `${normalizeMerchant(input.merchant)}${positiveInteger(input.amount_minor, "amount_minor")}${normalizeCurrency(input.currency)}${requireNonEmpty(input.challenge, "challenge")}`;
531
582
  return sha256Prefixed(material);
532
583
  }
584
+ async function directRequestPaymentRequestHashV2(input) {
585
+ const material = JSON.stringify({
586
+ amount_minor: positiveInteger(input.amount_minor, "amount_minor"),
587
+ challenge: requireNonEmpty(input.challenge, "challenge"),
588
+ currency: normalizeCurrency(input.currency),
589
+ merchant: normalizeMerchant(input.merchant),
590
+ version: 2
591
+ });
592
+ return sha256Prefixed(material);
593
+ }
533
594
  function buildPaymentExecutionPayload(requirement, options = {}) {
534
595
  return buildPreparedTransactionExecutionPayload(requirement, requirement.transaction_request, {
535
596
  receipt_kind: DIRECT_REQUEST_PAYMENT_RECEIPT_KIND,
@@ -603,8 +664,10 @@ function parseDirectRequestPaymentWebhookEvent(payload) {
603
664
  occurred_at: requireNonEmpty(stringOrNull(event.occurred_at) ?? "", "webhook occurred_at"),
604
665
  data: { ...data }
605
666
  };
606
- if (parsed.type === "direct_payment.confirmed" && parsed.data.mode !== DIRECT_REQUEST_PAYMENT_MODE) {
607
- throw new SiglumeWebhookPayloadError("direct_payment.confirmed webhook must carry data.mode='external_402'.");
667
+ if (parsed.type === "direct_payment.confirmed" && !DIRECT_REQUEST_PAYMENT_CONFIRMED_WEBHOOK_MODES.has(String(parsed.data.mode ?? ""))) {
668
+ throw new SiglumeWebhookPayloadError(
669
+ "direct_payment.confirmed webhook must carry a supported Direct Request Payment mode."
670
+ );
608
671
  }
609
672
  return parsed;
610
673
  }
@@ -658,6 +721,13 @@ function normalizeToken(value) {
658
721
  }
659
722
  return token;
660
723
  }
724
+ function normalizeMeteredPlanType(value) {
725
+ const planType = requireNonEmpty(value, "plan_type").toLowerCase();
726
+ if (planType === "micro" || planType === "nano") {
727
+ return planType;
728
+ }
729
+ throw new SiglumeDirectRequestPaymentError("plan_type must be micro or nano.");
730
+ }
661
731
  function normalizeAllowedCurrencies(value) {
662
732
  const normalized = {};
663
733
  if (Array.isArray(value)) {
@@ -695,7 +765,15 @@ function normalizeOriginList(value) {
695
765
  "each checkout_allowed_origins entry must be an absolute origin such as https://shop.example.com."
696
766
  );
697
767
  }
698
- const origin = `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}`;
768
+ if (url.username || url.password) {
769
+ throw new SiglumeDirectRequestPaymentError("checkout_allowed_origins entries must not include userinfo.");
770
+ }
771
+ if (!isAllowedCheckoutOriginScheme(url)) {
772
+ throw new SiglumeDirectRequestPaymentError(
773
+ "checkout_allowed_origins entries must use https, except http is allowed for localhost, 127.0.0.1, or [::1]."
774
+ );
775
+ }
776
+ const origin = url.origin.toLowerCase();
699
777
  if (!seen.has(origin)) {
700
778
  seen.add(origin);
701
779
  origins.push(origin);
@@ -703,8 +781,44 @@ function normalizeOriginList(value) {
703
781
  }
704
782
  return origins;
705
783
  }
784
+ function isAllowedCheckoutOriginScheme(url) {
785
+ if (url.protocol === "https:") {
786
+ return Boolean(url.hostname);
787
+ }
788
+ if (url.protocol !== "http:") {
789
+ return false;
790
+ }
791
+ const hostname = url.hostname.toLowerCase();
792
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
793
+ }
794
+ function meteredQueryPath(path, input) {
795
+ const params = new URLSearchParams();
796
+ if (input.plan_type !== void 0) {
797
+ params.set("plan_type", normalizeMeteredPlanType(input.plan_type));
798
+ }
799
+ if (input.token_symbol !== void 0) {
800
+ params.set("token_symbol", normalizeToken(input.token_symbol));
801
+ }
802
+ if ("status" in input && input.status !== void 0) {
803
+ params.set("status", requireNonEmpty(input.status, "status"));
804
+ }
805
+ if ("listing_id" in input && input.listing_id !== void 0) {
806
+ params.set("listing_id", requireNonEmpty(input.listing_id, "listing_id"));
807
+ }
808
+ if ("capability_key" in input && input.capability_key !== void 0) {
809
+ params.set("capability_key", requireNonEmpty(input.capability_key, "capability_key"));
810
+ }
811
+ if ("limit" in input && input.limit !== void 0) {
812
+ params.set("limit", String(positiveInteger(input.limit, "limit")));
813
+ }
814
+ const query = params.toString();
815
+ return query ? `${path}?${query}` : path;
816
+ }
706
817
  function positiveInteger(value, name) {
707
- const parsed = Number(value);
818
+ if (typeof value !== "number") {
819
+ throw new SiglumeDirectRequestPaymentError(`${name} must be a positive safe integer.`);
820
+ }
821
+ const parsed = value;
708
822
  if (!Number.isSafeInteger(parsed) || parsed <= 0) {
709
823
  throw new SiglumeDirectRequestPaymentError(`${name} must be a positive safe integer.`);
710
824
  }