@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/CHANGELOG.md +61 -0
- package/LICENSE +21 -21
- package/README.md +317 -148
- package/dist/index.cjs +257 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -1
- package/dist/index.d.ts +134 -1
- package/dist/index.js +257 -1
- package/dist/index.js.map +1 -1
- package/docs/announcement-ja.md +69 -0
- package/docs/api-reference.md +280 -65
- package/docs/merchant-quickstart.md +180 -139
- package/docs/pricing.md +73 -56
- package/docs/security.md +110 -85
- package/examples/express-checkout.ts +106 -105
- package/examples/setup-merchant.ts +17 -0
- package/package.json +71 -57
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
|
-
##
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
The
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
order.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
order.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
4
|
-
"description": "Merchant SDK for Siglume Direct Request Payment checkout integrations",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
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
|
+
}
|