@siglume/direct-request-payment 0.4.22 → 0.4.23
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 +12 -0
- package/bin/siglume-sdrp.mjs +58 -10
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/pricing.md +1 -1
- package/docs/sandbox.md +7 -0
- package/examples/hosted-checkout-python/pyproject.toml +1 -1
- package/package.json +2 -2
- package/templates/express/siglume-order-store.sql.ts +58 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.23 - 2026-06-20
|
|
4
|
+
|
|
5
|
+
- Made the local SDRP sandbox reject invalid checkout input early, including
|
|
6
|
+
non-positive `amount_minor`, unsupported currencies, and unsafe return URLs.
|
|
7
|
+
- Made sandbox checkout confirmation idempotent so repeated confirmation calls
|
|
8
|
+
return the original event and do not send duplicate webhooks.
|
|
9
|
+
- Made the Express SQL order-store adapter recoverable for custom SQL executors
|
|
10
|
+
without a transaction hook by marking failed webhook handling as retryable
|
|
11
|
+
instead of permanently treating it as a duplicate.
|
|
12
|
+
- Added E2E coverage for sandbox idempotency, invalid sandbox input, and
|
|
13
|
+
non-transactional webhook retry recovery.
|
|
14
|
+
|
|
3
15
|
## 0.4.22 - 2026-06-20
|
|
4
16
|
|
|
5
17
|
- Fixed clean-checkout TypeScript resolution for template imports so CI and npm
|
package/bin/siglume-sdrp.mjs
CHANGED
|
@@ -341,18 +341,36 @@ async function handleSandboxRequest(req, res, state, port) {
|
|
|
341
341
|
sendJson(res, 404, { error: { code: "EXTERNAL_402_MERCHANT_NOT_FOUND", message: "sandbox merchant not found" } });
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
344
|
+
let currency;
|
|
345
|
+
let amountMinor;
|
|
346
|
+
let successUrl;
|
|
347
|
+
let cancelUrl;
|
|
348
|
+
try {
|
|
349
|
+
currency = normalizeCurrency(body.currency || "JPY");
|
|
350
|
+
amountMinor = normalizePositiveAmountMinor(body.amount_minor);
|
|
351
|
+
successUrl = normalizeSandboxReturnUrl(body.success_url, "success_url");
|
|
352
|
+
cancelUrl = normalizeSandboxReturnUrl(body.cancel_url, "cancel_url");
|
|
353
|
+
} catch (error) {
|
|
354
|
+
sendJson(res, 400, {
|
|
355
|
+
error: {
|
|
356
|
+
code: "INVALID_CHECKOUT_SESSION_REQUEST",
|
|
357
|
+
message: error instanceof Error ? error.message : "invalid checkout session request",
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
344
362
|
const sessionId = `chk_sandbox_${state.sessions.size + 1}`;
|
|
345
363
|
const challengeHash = `sha256:sandbox_${hashString(`${sessionId}:${body.nonce || ""}`).slice(0, 32)}`;
|
|
346
364
|
const session = {
|
|
347
365
|
session_id: sessionId,
|
|
348
366
|
merchant: state.merchant,
|
|
349
|
-
amount_minor:
|
|
350
|
-
currency
|
|
351
|
-
token_symbol:
|
|
367
|
+
amount_minor: amountMinor,
|
|
368
|
+
currency,
|
|
369
|
+
token_symbol: currency === "USD" ? "USDC" : "JPYC",
|
|
352
370
|
status: "open",
|
|
353
371
|
challenge_hash: challengeHash,
|
|
354
|
-
success_url:
|
|
355
|
-
cancel_url:
|
|
372
|
+
success_url: successUrl,
|
|
373
|
+
cancel_url: cancelUrl,
|
|
356
374
|
metadata_jsonb: body.metadata && typeof body.metadata === "object" ? body.metadata : {},
|
|
357
375
|
checkout_url: `http://127.0.0.1:${port}/pay/${sessionId}`,
|
|
358
376
|
expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
|
@@ -418,15 +436,16 @@ async function handleSandboxRequest(req, res, state, port) {
|
|
|
418
436
|
sendJson(res, 404, { error: { code: "CHECKOUT_SESSION_NOT_FOUND", message: "sandbox session not found" } });
|
|
419
437
|
return;
|
|
420
438
|
}
|
|
439
|
+
if (session.status === "paid" && session.confirmation_event) {
|
|
440
|
+
sendEnvelope(res, 200, sandboxConfirmResponse(session, session.confirmation_event));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
421
443
|
session.status = "paid";
|
|
422
444
|
session.requirement_id = `dpr_sandbox_${sessionId}`;
|
|
423
445
|
const event = sandboxPaymentConfirmedEvent(session);
|
|
446
|
+
session.confirmation_event = event;
|
|
424
447
|
await deliverSandboxWebhook(state, event);
|
|
425
|
-
sendEnvelope(res, 200,
|
|
426
|
-
status: "paid",
|
|
427
|
-
redirect_url: `${session.success_url}${session.success_url.includes("?") ? "&" : "?"}session_id=${encodeURIComponent(sessionId)}`,
|
|
428
|
-
event: { id: event.id, type: event.type },
|
|
429
|
-
});
|
|
448
|
+
sendEnvelope(res, 200, sandboxConfirmResponse(session, event));
|
|
430
449
|
return;
|
|
431
450
|
}
|
|
432
451
|
|
|
@@ -488,6 +507,14 @@ function sandboxPaymentConfirmedEvent(session) {
|
|
|
488
507
|
});
|
|
489
508
|
}
|
|
490
509
|
|
|
510
|
+
function sandboxConfirmResponse(session, event) {
|
|
511
|
+
return {
|
|
512
|
+
status: "paid",
|
|
513
|
+
redirect_url: `${session.success_url}${session.success_url.includes("?") ? "&" : "?"}session_id=${encodeURIComponent(session.session_id)}`,
|
|
514
|
+
event: { id: event.id, type: event.type },
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
491
518
|
async function deliverSandboxWebhook(state, event) {
|
|
492
519
|
const rawBody = JSON.stringify(event);
|
|
493
520
|
const signature = await buildWebhookSignatureHeader(state.webhookSecret, rawBody);
|
|
@@ -725,6 +752,27 @@ function normalizeCurrency(value) {
|
|
|
725
752
|
return currency;
|
|
726
753
|
}
|
|
727
754
|
|
|
755
|
+
function normalizePositiveAmountMinor(value) {
|
|
756
|
+
const amountMinor = Number(value);
|
|
757
|
+
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
|
|
758
|
+
throw new Error("amount_minor must be a positive integer.");
|
|
759
|
+
}
|
|
760
|
+
return amountMinor;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function normalizeSandboxReturnUrl(value, name) {
|
|
764
|
+
const text = String(value || "").trim();
|
|
765
|
+
try {
|
|
766
|
+
const url = new URL(text);
|
|
767
|
+
if (url.protocol === "https:" || (url.protocol === "http:" && isLocalhost(url.hostname))) {
|
|
768
|
+
return url.href;
|
|
769
|
+
}
|
|
770
|
+
} catch {
|
|
771
|
+
// Fall through to a consistent validation error.
|
|
772
|
+
}
|
|
773
|
+
throw new Error(`${name} must be an https URL, or a local http URL in sandbox.`);
|
|
774
|
+
}
|
|
775
|
+
|
|
728
776
|
function isStandardAmount(currency, amountMinor) {
|
|
729
777
|
return Number.isSafeInteger(amountMinor) && amountMinor >= (currency === "USD" ? 301 : 501);
|
|
730
778
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -85,7 +85,7 @@ var DIRECT_REQUEST_PAYMENT_RECEIPT_KIND = "sdrp_direct_payment";
|
|
|
85
85
|
var DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND = "sdrp_direct_payment_allowance";
|
|
86
86
|
var DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE = "sdrp_direct_payment_requirement";
|
|
87
87
|
var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
88
|
-
var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.
|
|
88
|
+
var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.23";
|
|
89
89
|
var DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS = "settled";
|
|
90
90
|
var DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS = "pending_settlement";
|
|
91
91
|
var DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY = "per_payment_onchain";
|