@siglume/direct-request-payment 0.1.0 → 0.3.1

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/docs/security.md CHANGED
@@ -1,85 +1,110 @@
1
- # Security Guide
2
-
3
- Direct Request Payment is a wallet payment rail. Treat it like payment
4
- infrastructure, not like a generic API call.
5
-
6
- ## Do Not Expose Secrets
7
-
8
- These values must stay server-side:
9
-
10
- - `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET`
11
- - `SIGLUME_WEBHOOK_SECRET`
12
- - any merchant administrative credentials
13
-
14
- The buyer-facing browser may receive the signed `challenge` string, but never
15
- the secret that produced it.
16
-
17
- ## Bind the Order Server-Side
18
-
19
- The HMAC challenge covers:
20
-
21
- ```text
22
- merchant:amount_minor:currency:nonce
23
- ```
24
-
25
- Use a nonce derived from a durable order payment attempt, for example
26
- `order_123-attempt_1`. The nonce must not contain `:` because the platform
27
- challenge is encoded as `scheme:nonce:signature`. Store the returned
28
- `challenge_hash` on the order. When a
29
- webhook arrives, look up the order by `challenge_hash`.
30
-
31
- ## Do Not Trust Browser Amounts
32
-
33
- The merchant server owns:
34
-
35
- - SKU or plan
36
- - amount in minor units
37
- - currency
38
- - nonce
39
-
40
- If a browser says the order total is 1200 JPY, treat that as display state only.
41
- Re-read the order server-side before generating the challenge.
42
-
43
- ## Webhook Verification
44
-
45
- Verify the `Siglume-Signature` header using the raw request body. Do not parse
46
- and re-stringify JSON before verification.
47
-
48
- The SDK expects the Siglume signature format:
49
-
50
- ```text
51
- t=<unix timestamp>,v1=<hex hmac sha256>
52
- ```
53
-
54
- The signed payload is:
55
-
56
- ```text
57
- <timestamp>.<raw body>
58
- ```
59
-
60
- The default tolerance is 300 seconds.
61
-
62
- ## Idempotency
63
-
64
- Fulfill exactly once per order. Store at least:
65
-
66
- - order id
67
- - challenge hash
68
- - Siglume requirement id
69
- - on-chain receipt id or transaction hash if present
70
- - fulfillment state
71
-
72
- Duplicate webhook deliveries and manual redelivery can occur. A duplicate
73
- webhook with the same requirement id must not ship the order twice.
74
-
75
- ## What Direct Request Payment Is Not
76
-
77
- Direct Request Payment is not:
78
-
79
- - stored value
80
- - prepaid points
81
- - escrow
82
- - a platform balance
83
- - a card payment fallback
84
-
85
- It is a one-request wallet payment gate backed by an on-chain receipt.
1
+ # Security Guide
2
+
3
+ Direct Request Payment is a wallet payment rail. Treat it like payment
4
+ infrastructure, not like a generic API call.
5
+
6
+ ## Do Not Expose Secrets
7
+
8
+ These values must stay server-side:
9
+
10
+ - `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET`
11
+ - `SIGLUME_WEBHOOK_SECRET`
12
+ - any merchant administrative credentials
13
+
14
+ The buyer-facing browser may receive the signed `challenge` string, but never
15
+ the secret that produced it.
16
+
17
+ ## Keep JWT Roles Separate
18
+
19
+ Use the merchant's Siglume JWT only for setup actions such as `setupCheckout`,
20
+ challenge secret rotation, billing mandate preparation, and webhook subscription
21
+ creation.
22
+
23
+ Use the buyer's Siglume JWT only when creating and paying a payment requirement.
24
+ A merchant JWT or Developer Portal `cli_` key must not be used to charge a
25
+ customer wallet.
26
+
27
+ ## Bind the Order Server-Side
28
+
29
+ The HMAC challenge covers:
30
+
31
+ ```text
32
+ merchant:amount_minor:currency:nonce
33
+ ```
34
+
35
+ Use a nonce derived from a durable order payment attempt, for example
36
+ `order_123-attempt_1`. The nonce must not contain `:` because the platform
37
+ challenge is encoded as `scheme:nonce:signature`. Store the returned
38
+ `challenge_hash` on the order. When a
39
+ webhook arrives, look up the order by `challenge_hash`.
40
+
41
+ Recurring approvals use a different challenge scheme and HMAC material:
42
+
43
+ ```text
44
+ merchant:amount_minor:currency:cadence:nonce
45
+ ```
46
+
47
+ `cadence="monthly"` is for subscriptions. `cadence="daily"` is the scheduled
48
+ autopay approval tag; it does not itself limit occurrences to once per day.
49
+ Scheduled autopay execution is bounded by the buyer-approved per-run, daily, and
50
+ monthly auto-pay budget.
51
+
52
+ ## Do Not Trust Browser Amounts
53
+
54
+ The merchant server owns:
55
+
56
+ - SKU or plan
57
+ - amount in minor units
58
+ - currency
59
+ - nonce
60
+
61
+ If a browser says the order total is 1200 JPY, treat that as display state only.
62
+ Re-read the order server-side before generating the challenge.
63
+
64
+ ## Webhook Verification
65
+
66
+ Verify the `Siglume-Signature` header using the raw request body. Do not parse
67
+ and re-stringify JSON before verification.
68
+
69
+ The SDK expects the Siglume signature format:
70
+
71
+ ```text
72
+ t=<unix timestamp>,v1=<hex hmac sha256>
73
+ ```
74
+
75
+ The signed payload is:
76
+
77
+ ```text
78
+ <timestamp>.<raw body>
79
+ ```
80
+
81
+ The default tolerance is 300 seconds.
82
+
83
+ Use verified webhook data as the durable completion signal. Browser redirects,
84
+ client-side callbacks, or local transaction responses can improve UX, but they
85
+ should not be the only source used to fulfill an order.
86
+
87
+ ## Idempotency
88
+
89
+ Fulfill exactly once per order. Store at least:
90
+
91
+ - order id
92
+ - challenge hash
93
+ - Siglume requirement id
94
+ - on-chain receipt id or transaction hash if present
95
+ - fulfillment state
96
+
97
+ Duplicate webhook deliveries and manual redelivery can occur. A duplicate
98
+ webhook with the same requirement id must not ship the order twice.
99
+
100
+ ## What Direct Request Payment Is Not
101
+
102
+ Direct Request Payment is not:
103
+
104
+ - stored value
105
+ - prepaid points
106
+ - escrow
107
+ - a platform balance
108
+ - a card payment fallback
109
+
110
+ It is a one-request wallet payment gate backed by an on-chain receipt.
@@ -1,105 +1,106 @@
1
- import express from "express";
2
- import {
3
- createDirectRequestPaymentChallenge,
4
- DirectRequestPaymentClient,
5
- verifyDirectRequestPaymentWebhook,
6
- } from "@siglume/direct-request-payment";
7
-
8
- const app = express();
9
- const port = Number(process.env.PORT || 3000);
10
-
11
- // Use JSON for normal routes. Use raw body only on the webhook route.
12
- app.use((req, res, next) => {
13
- if (req.path === "/siglume/webhook") {
14
- next();
15
- return;
16
- }
17
- express.json()(req, res, next);
18
- });
19
-
20
- const orders = new Map<string, any>();
21
-
22
- const asyncRoute =
23
- (handler: express.RequestHandler): express.RequestHandler =>
24
- (req, res, next) => {
25
- Promise.resolve(handler(req, res, next)).catch(next);
26
- };
27
-
28
- app.post("/checkout/siglume/start", asyncRoute(async (req, res) => {
29
- const orderId = String(req.body.order_id || "");
30
- const order = orders.get(orderId);
31
- if (!order) {
32
- res.status(404).json({ error: "order_not_found" });
33
- return;
34
- }
35
-
36
- order.payment_attempt = Number(order.payment_attempt || 0) + 1;
37
- const challenge = await createDirectRequestPaymentChallenge({
38
- merchant: "example_merchant",
39
- amount_minor: order.amount_minor,
40
- currency: order.currency,
41
- secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
42
- nonce: `${order.id}-attempt_${order.payment_attempt}`,
43
- });
44
-
45
- order.siglume_challenge_hash = challenge.challenge_hash;
46
- order.siglume_payment_status = "pending";
47
-
48
- res.json({
49
- order_id: order.id,
50
- amount_minor: order.amount_minor,
51
- currency: order.currency,
52
- siglume_challenge: challenge.challenge,
53
- });
54
- }));
55
-
56
- app.post("/checkout/siglume/pay", asyncRoute(async (req, res) => {
57
- const order = orders.get(String(req.body.order_id || ""));
58
- if (!order) {
59
- res.status(404).json({ error: "order_not_found" });
60
- return;
61
- }
62
-
63
- // In production, obtain this from the authenticated buyer's Siglume session
64
- // or a hosted Siglume payment confirmation flow. Do not use a merchant secret
65
- // to charge a customer wallet.
66
- const siglume = new DirectRequestPaymentClient({
67
- auth_token: String(req.headers.authorization || "").replace(/^Bearer\s+/i, ""),
68
- });
69
-
70
- const requirement = await siglume.createPaymentRequirement({
71
- merchant: "example_merchant",
72
- amount_minor: order.amount_minor,
73
- currency: order.currency,
74
- challenge: String(req.body.siglume_challenge || ""),
75
- });
76
-
77
- res.json({ requirement });
78
- }));
79
-
80
- app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRoute(async (req, res) => {
81
- const header = String(req.headers["siglume-signature"] || "");
82
- const { event } = await verifyDirectRequestPaymentWebhook(
83
- process.env.SIGLUME_WEBHOOK_SECRET!,
84
- req.body,
85
- header,
86
- );
87
-
88
- if (event.type === "direct_payment.confirmed") {
89
- const challengeHash = String(event.data.challenge_hash || "");
90
- const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
91
- if (order) {
92
- order.siglume_payment_status = "paid";
93
- order.siglume_requirement_id = event.data.requirement_id || event.data.direct_payment_requirement_id;
94
- }
95
- }
96
-
97
- res.status(204).send();
98
- }));
99
-
100
- app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
101
- const message = error instanceof Error ? error.message : "internal_error";
102
- res.status(500).json({ error: message });
103
- });
104
-
105
- app.listen(port);
1
+ import express from "express";
2
+ import {
3
+ createDirectRequestPaymentChallenge,
4
+ DirectRequestPaymentClient,
5
+ verifyDirectRequestPaymentWebhook,
6
+ } from "@siglume/direct-request-payment";
7
+
8
+ const app = express();
9
+ const port = Number(process.env.PORT || 3000);
10
+ const merchantKey = process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "example_merchant";
11
+
12
+ // Use JSON for normal routes. Use raw body only on the webhook route.
13
+ app.use((req, res, next) => {
14
+ if (req.path === "/siglume/webhook") {
15
+ next();
16
+ return;
17
+ }
18
+ express.json()(req, res, next);
19
+ });
20
+
21
+ const orders = new Map<string, any>();
22
+
23
+ const asyncRoute =
24
+ (handler: express.RequestHandler): express.RequestHandler =>
25
+ (req, res, next) => {
26
+ Promise.resolve(handler(req, res, next)).catch(next);
27
+ };
28
+
29
+ app.post("/checkout/siglume/start", asyncRoute(async (req, res) => {
30
+ const orderId = String(req.body.order_id || "");
31
+ const order = orders.get(orderId);
32
+ if (!order) {
33
+ res.status(404).json({ error: "order_not_found" });
34
+ return;
35
+ }
36
+
37
+ order.payment_attempt = Number(order.payment_attempt || 0) + 1;
38
+ const challenge = await createDirectRequestPaymentChallenge({
39
+ merchant: merchantKey,
40
+ amount_minor: order.amount_minor,
41
+ currency: order.currency,
42
+ secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
43
+ nonce: `${order.id}-attempt_${order.payment_attempt}`,
44
+ });
45
+
46
+ order.siglume_challenge_hash = challenge.challenge_hash;
47
+ order.siglume_payment_status = "pending";
48
+
49
+ res.json({
50
+ order_id: order.id,
51
+ amount_minor: order.amount_minor,
52
+ currency: order.currency,
53
+ siglume_challenge: challenge.challenge,
54
+ });
55
+ }));
56
+
57
+ app.post("/checkout/siglume/pay", asyncRoute(async (req, res) => {
58
+ const order = orders.get(String(req.body.order_id || ""));
59
+ if (!order) {
60
+ res.status(404).json({ error: "order_not_found" });
61
+ return;
62
+ }
63
+
64
+ // In production, obtain this from the authenticated buyer's Siglume session
65
+ // or a hosted Siglume payment confirmation flow. Do not use a merchant secret
66
+ // to charge a customer wallet.
67
+ const siglume = new DirectRequestPaymentClient({
68
+ auth_token: String(req.headers.authorization || "").replace(/^Bearer\s+/i, ""),
69
+ });
70
+
71
+ const requirement = await siglume.createPaymentRequirement({
72
+ merchant: merchantKey,
73
+ amount_minor: order.amount_minor,
74
+ currency: order.currency,
75
+ challenge: String(req.body.siglume_challenge || ""),
76
+ });
77
+
78
+ res.json({ requirement });
79
+ }));
80
+
81
+ app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRoute(async (req, res) => {
82
+ const header = String(req.headers["siglume-signature"] || "");
83
+ const { event } = await verifyDirectRequestPaymentWebhook(
84
+ process.env.SIGLUME_WEBHOOK_SECRET!,
85
+ req.body,
86
+ header,
87
+ );
88
+
89
+ if (event.type === "direct_payment.confirmed") {
90
+ const challengeHash = String(event.data.challenge_hash || "");
91
+ const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
92
+ if (order) {
93
+ order.siglume_payment_status = "paid";
94
+ order.siglume_requirement_id = event.data.requirement_id || event.data.direct_payment_requirement_id;
95
+ }
96
+ }
97
+
98
+ res.status(204).send();
99
+ }));
100
+
101
+ app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
102
+ const message = error instanceof Error ? error.message : "internal_error";
103
+ res.status(500).json({ error: message });
104
+ });
105
+
106
+ app.listen(port);
@@ -0,0 +1,17 @@
1
+ import { DirectRequestPaymentMerchantClient } from "@siglume/direct-request-payment";
2
+
3
+ const merchant = new DirectRequestPaymentMerchantClient({
4
+ auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN,
5
+ });
6
+
7
+ const setup = await merchant.setupCheckout({
8
+ merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "example_merchant",
9
+ display_name: process.env.SIGLUME_DIRECT_PAYMENT_DISPLAY_NAME || "Example Merchant",
10
+ billing_plan: process.env.SIGLUME_DIRECT_PAYMENT_PLAN || "launch",
11
+ billing_currency: process.env.SIGLUME_DIRECT_PAYMENT_BILLING_CURRENCY || "JPY",
12
+ webhook_callback_url: process.env.SIGLUME_DIRECT_PAYMENT_WEBHOOK_URL,
13
+ max_amount_minor: Number(process.env.SIGLUME_DIRECT_PAYMENT_BILLING_CAP_MINOR || 100000),
14
+ create_webhook_subscription: Boolean(process.env.SIGLUME_DIRECT_PAYMENT_WEBHOOK_URL),
15
+ });
16
+
17
+ console.log(JSON.stringify(setup, null, 2));
package/package.json CHANGED
@@ -1,57 +1,71 @@
1
- {
2
- "name": "@siglume/direct-request-payment",
3
- "version": "0.1.0",
4
- "description": "Merchant SDK for Siglume Direct Request Payment checkout integrations",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/taihei-05/siglume-direct-request-payment.git"
9
- },
10
- "type": "module",
11
- "engines": {
12
- "node": ">=18"
13
- },
14
- "sideEffects": false,
15
- "main": "./dist/index.cjs",
16
- "module": "./dist/index.js",
17
- "types": "./dist/index.d.ts",
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": {
22
- "types": "./dist/index.d.ts",
23
- "default": "./dist/index.js"
24
- },
25
- "require": {
26
- "types": "./dist/index.d.cts",
27
- "default": "./dist/index.cjs"
28
- }
29
- },
30
- "./package.json": "./package.json"
31
- },
32
- "publishConfig": {
33
- "access": "public"
34
- },
35
- "files": [
36
- "dist",
37
- "docs",
38
- "examples",
39
- "README.md",
40
- "LICENSE"
41
- ],
42
- "scripts": {
43
- "build": "tsup",
44
- "lint": "npm run typecheck",
45
- "prepack": "npm run build",
46
- "prepublishOnly": "npm run build",
47
- "typecheck": "tsc --noEmit",
48
- "test": "vitest run",
49
- "pack:check": "npm pack --json"
50
- },
51
- "devDependencies": {
52
- "@types/node": "^20.16.5",
53
- "tsup": "^8.3.0",
54
- "typescript": "^5.6.3",
55
- "vitest": "^2.1.8"
56
- }
57
- }
1
+ {
2
+ "name": "@siglume/direct-request-payment",
3
+ "version": "0.3.1",
4
+ "description": "Merchant SDK for Siglume Direct Request Payment checkout integrations",
5
+ "keywords": [
6
+ "siglume",
7
+ "payment",
8
+ "checkout",
9
+ "external-402",
10
+ "wallet",
11
+ "sdk"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Siglume Contributors",
15
+ "homepage": "https://github.com/taihei-05/siglume-direct-request-payment#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/taihei-05/siglume-direct-request-payment/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/taihei-05/siglume-direct-request-payment.git"
22
+ },
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "sideEffects": false,
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "docs",
51
+ "examples",
52
+ "README.md",
53
+ "CHANGELOG.md",
54
+ "LICENSE"
55
+ ],
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "lint": "npm run typecheck",
59
+ "prepack": "npm run build",
60
+ "prepublishOnly": "npm run build",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run",
63
+ "pack:check": "npm pack --json"
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^20.16.5",
67
+ "tsup": "^8.3.0",
68
+ "typescript": "^5.6.3",
69
+ "vitest": "^2.1.8"
70
+ }
71
+ }