shieldpay-api-sdkjs 2.0.0
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/README.md +63 -0
- package/docs/index.html +412 -0
- package/index.js +322 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# shieldpay-sdk
|
|
2
|
+
|
|
3
|
+
Official JavaScript/TypeScript SDK for the [ShieldPay API](https://hnow.info).
|
|
4
|
+
|
|
5
|
+
Anti-spam, VPN blocking, email authentication, and PayPal payments — all in one package.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install shieldpay-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { ShieldPay } from 'shieldpay-sdk'
|
|
17
|
+
|
|
18
|
+
const sp = new ShieldPay({ apiKey: 'sk_live_YOUR_KEY' })
|
|
19
|
+
// All calls go to: https://hnow.info/api/worker=sk_live_YOUR_KEY/...
|
|
20
|
+
|
|
21
|
+
// Create a PayPal payment
|
|
22
|
+
const order = await sp.payment.create({
|
|
23
|
+
amount: 49.99,
|
|
24
|
+
currency: 'CHF',
|
|
25
|
+
return_url: 'https://yoursite.com/success',
|
|
26
|
+
cancel_url: 'https://yoursite.com/cancel',
|
|
27
|
+
})
|
|
28
|
+
window.location.href = order.approve_url // redirect to PayPal
|
|
29
|
+
|
|
30
|
+
// After PayPal redirects back, capture
|
|
31
|
+
const captured = await sp.payment.capture(orderId)
|
|
32
|
+
|
|
33
|
+
// Check IP reputation (Pro: real IPQS check)
|
|
34
|
+
const ip = await sp.security.checkIp('1.2.3.4')
|
|
35
|
+
|
|
36
|
+
// Passwordless email auth for your users
|
|
37
|
+
const { token } = await sp.auth.createToken('user@email.com')
|
|
38
|
+
const { verified } = await sp.auth.verifyToken(token)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API Endpoint
|
|
42
|
+
|
|
43
|
+
Your API key is embedded in the URL — no headers needed:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
https://hnow.info/api/worker=sk_live_YOUR_KEY/payment/create
|
|
47
|
+
https://hnow.info/api/worker=sk_live_YOUR_KEY/security/check-ip
|
|
48
|
+
https://hnow.info/api/worker=sk_live_YOUR_KEY/me
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Plans
|
|
52
|
+
|
|
53
|
+
| Feature | Free | Pro (1 CHF/mo) |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| PayPal payments | ✓ | ✓ |
|
|
56
|
+
| Transaction fee | 2% | **0%** |
|
|
57
|
+
| VPN/Tor blocking | Basic | Full IPQS |
|
|
58
|
+
| Auto-fallback | ✗ | ✓ |
|
|
59
|
+
| Email alerts | ✗ | ✓ |
|
|
60
|
+
|
|
61
|
+
## Docs
|
|
62
|
+
|
|
63
|
+
Full documentation: [https://hnow.info/SDK/docs/](https://hnow.info/SDK/docs/)
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
5
|
+
<title>ShieldPay SDK v2 — Docs</title>
|
|
6
|
+
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
|
7
|
+
<style>
|
|
8
|
+
:root{--bg:#0a0a0f;--surface:#111118;--border:#1e1e2e;--accent:#7c6aff;--accent2:#00e5c0;--danger:#ff4e6a;--warn:#ffb347;--text:#e8e8f0;--muted:#6b6b80;--card:#13131f}
|
|
9
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
10
|
+
body{background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;display:flex;min-height:100vh;line-height:1.6}
|
|
11
|
+
.sidebar{width:265px;border-right:1px solid var(--border);padding:0;flex-shrink:0;position:sticky;top:0;height:100vh;overflow-y:auto;background:var(--surface)}
|
|
12
|
+
.sidebar-top{padding:1.5rem 1.5rem 1rem;border-bottom:1px solid var(--border)}
|
|
13
|
+
.logo{font-family:'Syne',sans-serif;font-weight:800;font-size:1.15rem;color:var(--text);text-decoration:none;display:block;margin-bottom:.4rem}.logo span{color:var(--accent)}
|
|
14
|
+
.version-chip{font-family:'DM Mono',monospace;font-size:.68rem;background:rgba(124,106,255,.12);border:1px solid rgba(124,106,255,.25);color:var(--accent);padding:.15rem .55rem;border-radius:100px}
|
|
15
|
+
.nav-section{padding:.5rem 1.5rem .2rem;font-family:'DM Mono',monospace;font-size:.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:.12em;margin-top:.5rem}
|
|
16
|
+
.sidebar nav a{display:block;padding:.45rem 1.5rem;color:var(--muted);text-decoration:none;font-size:.84rem;border-left:2px solid transparent;transition:all .15s}
|
|
17
|
+
.sidebar nav a:hover{color:var(--text);background:rgba(124,106,255,.04);text-decoration:none}
|
|
18
|
+
.sidebar nav a.active{color:var(--text);border-left-color:var(--accent);background:rgba(124,106,255,.07)}
|
|
19
|
+
.main{flex:1;padding:3.5rem 4rem;max-width:860px}
|
|
20
|
+
h1{font-family:'Syne',sans-serif;font-size:2.3rem;font-weight:800;letter-spacing:-.04em;margin-bottom:.5rem}
|
|
21
|
+
h2{font-family:'Syne',sans-serif;font-size:1.45rem;font-weight:800;letter-spacing:-.03em;margin:3rem 0 .9rem;padding-top:3rem;border-top:1px solid var(--border)}
|
|
22
|
+
h2:first-of-type{border:none;padding:0;margin-top:1rem}
|
|
23
|
+
h3{font-family:'Syne',sans-serif;font-size:1rem;font-weight:700;margin:1.6rem 0 .5rem;color:var(--accent2)}
|
|
24
|
+
h4{font-family:'DM Mono',monospace;font-size:.85rem;font-weight:500;color:var(--text);margin:.8rem 0 .3rem}
|
|
25
|
+
p{color:var(--muted);line-height:1.75;margin-bottom:.9rem;font-size:.93rem}
|
|
26
|
+
a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
|
|
27
|
+
pre{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.5rem;overflow-x:auto;margin:1rem 0;font-family:'DM Mono',monospace;font-size:.81rem;line-height:1.75;position:relative}
|
|
28
|
+
code{font-family:'DM Mono',monospace;font-size:.83rem;background:rgba(124,106,255,.1);padding:.12rem .38rem;border-radius:4px;color:var(--accent)}
|
|
29
|
+
pre code{background:none;padding:0;color:inherit;font-size:inherit}
|
|
30
|
+
.kw{color:var(--accent2)}.st{color:#ffcb6b}.nm{color:#f78c6c}.cm{color:#4a5568;font-style:italic}.prop{color:#82aaff}.type{color:#c792ea}
|
|
31
|
+
.badge{display:inline-flex;align-items:center;padding:.18rem .55rem;border-radius:100px;font-family:'DM Mono',monospace;font-size:.7rem;font-weight:700;margin-left:.4rem;vertical-align:middle}
|
|
32
|
+
.b-get{background:rgba(0,229,192,.08);color:var(--accent2);border:1px solid rgba(0,229,192,.25)}
|
|
33
|
+
.b-post{background:rgba(124,106,255,.1);color:var(--accent);border:1px solid rgba(124,106,255,.25)}
|
|
34
|
+
.b-pro{background:rgba(255,179,71,.08);color:var(--warn);border:1px solid rgba(255,179,71,.25)}
|
|
35
|
+
.b-free{background:rgba(107,107,128,.1);color:var(--muted);border:1px solid var(--border)}
|
|
36
|
+
table{width:100%;border-collapse:collapse;margin:1rem 0;font-size:.86rem}
|
|
37
|
+
th{text-align:left;padding:.55rem .75rem;color:var(--muted);font-size:.72rem;font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border)}
|
|
38
|
+
td{padding:.6rem .75rem;border-bottom:1px solid rgba(30,30,46,.5);color:var(--text);vertical-align:top}
|
|
39
|
+
tr:last-child td{border:none}
|
|
40
|
+
.param-name{font-family:'DM Mono',monospace;font-size:.82rem;color:var(--accent2)}
|
|
41
|
+
.param-type{font-family:'DM Mono',monospace;font-size:.78rem;color:var(--type, #c792ea)}
|
|
42
|
+
.param-req{color:var(--danger);font-size:.75rem}
|
|
43
|
+
.alert{padding:.8rem 1rem;border-radius:8px;font-size:.86rem;margin:1rem 0;line-height:1.6}
|
|
44
|
+
.alert-info{background:rgba(124,106,255,.07);border:1px solid rgba(124,106,255,.2);color:var(--accent)}
|
|
45
|
+
.alert-warn{background:rgba(255,179,71,.07);border:1px solid rgba(255,179,71,.2);color:var(--warn)}
|
|
46
|
+
.alert-success{background:rgba(0,229,192,.07);border:1px solid rgba(0,229,192,.2);color:var(--accent2)}
|
|
47
|
+
.endpoint-line{display:flex;align-items:center;gap:.6rem;margin:.4rem 0;font-family:'DM Mono',monospace;font-size:.8rem}
|
|
48
|
+
.plan-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin:1rem 0}
|
|
49
|
+
.plan-box{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:1.2rem}
|
|
50
|
+
.plan-box.pro{border-color:rgba(124,106,255,.4)}
|
|
51
|
+
.plan-box h4{font-family:'Syne',sans-serif;font-weight:700;font-size:.95rem;margin-bottom:.7rem}
|
|
52
|
+
.plan-box ul{list-style:none;font-size:.85rem;color:var(--muted)}
|
|
53
|
+
.plan-box li{padding:.25rem 0;display:flex;gap:.5rem}.ck{color:var(--accent2)}.cx{color:var(--danger)}
|
|
54
|
+
hr{border:none;border-top:1px solid var(--border);margin:2rem 0}
|
|
55
|
+
@media(max-width:768px){.sidebar{display:none}.main{padding:2rem 1.5rem}}
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
|
|
60
|
+
<aside class="sidebar">
|
|
61
|
+
<div class="sidebar-top">
|
|
62
|
+
<a href="/" class="logo">Shield<span>Pay</span></a>
|
|
63
|
+
<span class="version-chip">v2.0.0</span>
|
|
64
|
+
</div>
|
|
65
|
+
<nav>
|
|
66
|
+
<div class="nav-section">Getting Started</div>
|
|
67
|
+
<a href="#install">Installation</a>
|
|
68
|
+
<a href="#quickstart">Quick Start</a>
|
|
69
|
+
<a href="#endpoint">API Endpoint</a>
|
|
70
|
+
<a href="#register">Registration</a>
|
|
71
|
+
<div class="nav-section">SDK Modules</div>
|
|
72
|
+
<a href="#payment">payment</a>
|
|
73
|
+
<a href="#auth">auth</a>
|
|
74
|
+
<a href="#security">security</a>
|
|
75
|
+
<a href="#account">account</a>
|
|
76
|
+
<div class="nav-section">REST API</div>
|
|
77
|
+
<a href="#endpoints">All Endpoints</a>
|
|
78
|
+
<a href="#errors">Errors & Codes</a>
|
|
79
|
+
<a href="#ratelimits">Rate Limits</a>
|
|
80
|
+
<div class="nav-section">Plans</div>
|
|
81
|
+
<a href="#plans">Free vs Pro</a>
|
|
82
|
+
<a href="#fees">Fee Calculation</a>
|
|
83
|
+
<div class="nav-section">Integration</div>
|
|
84
|
+
<a href="#webhooks">Webhooks</a>
|
|
85
|
+
<a href="#fallback">Auto-Fallback</a>
|
|
86
|
+
<div class="nav-section">Links</div>
|
|
87
|
+
<a href="/panel.php">Dashboard ↗</a>
|
|
88
|
+
<a href="/admin.php">Admin ↗</a>
|
|
89
|
+
<a href="/">Home ↗</a>
|
|
90
|
+
</nav>
|
|
91
|
+
</aside>
|
|
92
|
+
|
|
93
|
+
<main class="main">
|
|
94
|
+
|
|
95
|
+
<h1>ShieldPay SDK</h1>
|
|
96
|
+
<p>The official JavaScript/TypeScript SDK for the ShieldPay API — payments, anti-spam, VPN blocking, and email authentication in one package.</p>
|
|
97
|
+
|
|
98
|
+
<div class="alert alert-info">
|
|
99
|
+
<strong>Base URL:</strong> <code>https://hnow.info/api/worker={YOUR_API_KEY}/{endpoint}</code><br>
|
|
100
|
+
Your API key is embedded directly in the URL path — no separate Authorization header needed.
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- INSTALL -->
|
|
104
|
+
<h2 id="install">Installation</h2>
|
|
105
|
+
<pre><span class="cm"># npm</span>
|
|
106
|
+
npm install shieldpay-sdk
|
|
107
|
+
|
|
108
|
+
<span class="cm"># yarn</span>
|
|
109
|
+
yarn add shieldpay-sdk
|
|
110
|
+
|
|
111
|
+
<span class="cm"># pnpm</span>
|
|
112
|
+
pnpm add shieldpay-sdk</pre>
|
|
113
|
+
|
|
114
|
+
<p>Works in Node.js ≥18, Deno, Bun, Cloudflare Workers, and all modern browsers. Uses the native <code>fetch</code> API — no dependencies.</p>
|
|
115
|
+
|
|
116
|
+
<!-- QUICKSTART -->
|
|
117
|
+
<h2 id="quickstart">Quick Start</h2>
|
|
118
|
+
<pre><span class="kw">import</span> { ShieldPay } <span class="kw">from</span> <span class="st">'shieldpay-sdk'</span>
|
|
119
|
+
|
|
120
|
+
<span class="cm">// 1. Initialize with your API key from the dashboard</span>
|
|
121
|
+
<span class="kw">const</span> sp = <span class="kw">new</span> ShieldPay({ apiKey: <span class="st">'sk_live_YOUR_KEY'</span> })
|
|
122
|
+
<span class="cm">// → All calls go to: https://hnow.info/api/worker=sk_live_YOUR_KEY/...</span>
|
|
123
|
+
|
|
124
|
+
<span class="cm">// 2. Create a PayPal order</span>
|
|
125
|
+
<span class="kw">const</span> order = <span class="kw">await</span> sp.payment.create({
|
|
126
|
+
amount: <span class="nm">49.99</span>,
|
|
127
|
+
currency: <span class="st">'CHF'</span>,
|
|
128
|
+
description: <span class="st">'Premium subscription'</span>,
|
|
129
|
+
return_url: <span class="st">'https://yoursite.com/payment/success'</span>,
|
|
130
|
+
cancel_url: <span class="st">'https://yoursite.com/payment/cancel'</span>,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
<span class="cm">// 3. Redirect user to PayPal for approval</span>
|
|
134
|
+
window.location.href = order.approve_url
|
|
135
|
+
|
|
136
|
+
<span class="cm">// 4. After PayPal redirects back, capture the payment</span>
|
|
137
|
+
<span class="cm">// (in your return_url handler)</span>
|
|
138
|
+
<span class="kw">const</span> captured = <span class="kw">await</span> sp.payment.capture(orderId)
|
|
139
|
+
console.log(captured.status) <span class="cm">// "COMPLETED"</span></pre>
|
|
140
|
+
|
|
141
|
+
<!-- ENDPOINT -->
|
|
142
|
+
<h2 id="endpoint">API Endpoint Format</h2>
|
|
143
|
+
<p>ShieldPay uses a custom URL pattern that embeds your API key directly in the path. This makes it easy to call from any HTTP client without custom headers:</p>
|
|
144
|
+
|
|
145
|
+
<pre><span class="cm"># Pattern</span>
|
|
146
|
+
https://hnow.info/api/worker=<span class="nm">{API_KEY}</span>/<span class="nm">{endpoint}</span>
|
|
147
|
+
|
|
148
|
+
<span class="cm"># Examples</span>
|
|
149
|
+
GET https://hnow.info/api/worker=sk_live_abc/me
|
|
150
|
+
POST https://hnow.info/api/worker=sk_live_abc/payment/create
|
|
151
|
+
GET https://hnow.info/api/worker=sk_live_abc/payment/status?order_id=XYZ
|
|
152
|
+
POST https://hnow.info/api/worker=sk_live_abc/security/check-ip</pre>
|
|
153
|
+
|
|
154
|
+
<p>The <code>X-API-Key</code> header is also accepted for clients that prefer header-based auth. The SDK handles both automatically.</p>
|
|
155
|
+
|
|
156
|
+
<pre><span class="cm">// Get your full endpoint URL</span>
|
|
157
|
+
<span class="kw">const</span> sp = <span class="kw">new</span> ShieldPay({ apiKey: <span class="st">'sk_live_abc'</span> })
|
|
158
|
+
<span class="kw">const</span> url = sp.endpoint(<span class="st">'/payment/create'</span>)
|
|
159
|
+
<span class="cm">// → "https://hnow.info/api/worker=sk_live_abc/payment/create"</span></pre>
|
|
160
|
+
|
|
161
|
+
<!-- REGISTER -->
|
|
162
|
+
<h2 id="register">Registration & Email Verification</h2>
|
|
163
|
+
<p>Register a company and verify their email to receive an API key. These are <strong>static methods</strong> — no API key instance needed.</p>
|
|
164
|
+
|
|
165
|
+
<pre><span class="kw">import</span> { ShieldPay } <span class="kw">from</span> <span class="st">'shieldpay-sdk'</span>
|
|
166
|
+
|
|
167
|
+
<span class="cm">// 1. Register (anti-spam + VPN check runs automatically)</span>
|
|
168
|
+
<span class="kw">const</span> reg = <span class="kw">await</span> ShieldPay.register({
|
|
169
|
+
company: <span class="st">'Acme Corp'</span>,
|
|
170
|
+
email: <span class="st">'hello@acme.com'</span>,
|
|
171
|
+
password: <span class="st">'securepassword123'</span>,
|
|
172
|
+
})
|
|
173
|
+
<span class="cm">// → { ok: true, message: "Account created. Check your email to verify." }</span>
|
|
174
|
+
|
|
175
|
+
<span class="cm">// 2. User clicks the link in their email, which contains the token.
|
|
176
|
+
// Your app then calls:</span>
|
|
177
|
+
<span class="kw">const</span> verified = <span class="kw">await</span> ShieldPay.verifyEmail(<span class="st">'TOKEN_FROM_EMAIL_LINK'</span>)
|
|
178
|
+
<span class="cm">// → {
|
|
179
|
+
// ok: true,
|
|
180
|
+
// api_key: "sk_live_abc123...",
|
|
181
|
+
// api_endpoint: "https://hnow.info/api/worker=sk_live_abc123",
|
|
182
|
+
// message: "Email verified! Use your API key to authenticate."
|
|
183
|
+
// }</span>
|
|
184
|
+
|
|
185
|
+
<span class="cm">// 3. Store api_key securely and initialize</span>
|
|
186
|
+
<span class="kw">const</span> sp = <span class="kw">new</span> ShieldPay({ apiKey: verified.api_key })</pre>
|
|
187
|
+
|
|
188
|
+
<!-- PAYMENT -->
|
|
189
|
+
<h2 id="payment">payment module</h2>
|
|
190
|
+
<p>All PayPal order operations. Free plan: 2% fee deducted per transaction. Pro plan: 0% fee.</p>
|
|
191
|
+
|
|
192
|
+
<h3>payment.create(params) <span class="badge b-post">POST</span></h3>
|
|
193
|
+
<table>
|
|
194
|
+
<tr><th>Param</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr>
|
|
195
|
+
<tr><td class="param-name">amount</td><td class="param-type">number</td><td class="param-req">✓</td><td>—</td><td>Amount to charge (e.g. <code>49.99</code>)</td></tr>
|
|
196
|
+
<tr><td class="param-name">currency</td><td class="param-type">string</td><td></td><td><code>'CHF'</code></td><td>ISO 4217 currency code</td></tr>
|
|
197
|
+
<tr><td class="param-name">description</td><td class="param-type">string</td><td></td><td>—</td><td>Order description shown on PayPal</td></tr>
|
|
198
|
+
<tr><td class="param-name">return_url</td><td class="param-type">string</td><td></td><td>Dashboard</td><td>Redirect URL after successful payment</td></tr>
|
|
199
|
+
<tr><td class="param-name">cancel_url</td><td class="param-type">string</td><td></td><td>Dashboard</td><td>Redirect URL if user cancels</td></tr>
|
|
200
|
+
</table>
|
|
201
|
+
<pre><span class="kw">const</span> order = <span class="kw">await</span> sp.payment.create({
|
|
202
|
+
amount: <span class="nm">25.00</span>,
|
|
203
|
+
currency: <span class="st">'CHF'</span>,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
<span class="cm">// Response (Free plan):</span>
|
|
207
|
+
<span class="cm">// {</span>
|
|
208
|
+
<span class="cm">// ok: true,</span>
|
|
209
|
+
<span class="cm">// order_id: "9XW12345AB",</span>
|
|
210
|
+
<span class="cm">// approve_url: "https://www.paypal.com/checkoutnow?token=...",</span>
|
|
211
|
+
<span class="cm">// amount: 25.00,</span>
|
|
212
|
+
<span class="cm">// fee: 0.50, // 2% — 0 on Pro</span>
|
|
213
|
+
<span class="cm">// net_amount: 24.50,</span>
|
|
214
|
+
<span class="cm">// currency: "CHF",</span>
|
|
215
|
+
<span class="cm">// plan: "free",</span>
|
|
216
|
+
<span class="cm">// fee_note: "2% ShieldPay fee: CHF 0.50. Upgrade to Pro for 0% fees."</span>
|
|
217
|
+
<span class="cm">// }</span></pre>
|
|
218
|
+
|
|
219
|
+
<h3>payment.capture(orderId) <span class="badge b-post">POST</span></h3>
|
|
220
|
+
<p>Finalizes a payment after the user approves it on PayPal. Call this in your <code>return_url</code> handler. On Pro plan, if this fails, auto-fallback is triggered automatically.</p>
|
|
221
|
+
<pre><span class="kw">const</span> result = <span class="kw">await</span> sp.payment.capture(<span class="st">'9XW12345AB'</span>)
|
|
222
|
+
<span class="cm">// → { ok: true, captured: true, order_id: "...", status: "COMPLETED", payer: {...} }</span></pre>
|
|
223
|
+
|
|
224
|
+
<h3>payment.status(orderId) <span class="badge b-get">GET</span></h3>
|
|
225
|
+
<pre><span class="kw">const</span> s = <span class="kw">await</span> sp.payment.status(<span class="st">'9XW12345AB'</span>)
|
|
226
|
+
<span class="cm">// → { ok: true, order_id: "...", status: "CREATED|APPROVED|COMPLETED", amount: {...} }</span></pre>
|
|
227
|
+
|
|
228
|
+
<h3>payment.refund(captureId, amount?, note?) <span class="badge b-post">POST</span></h3>
|
|
229
|
+
<pre><span class="cm">// Full refund</span>
|
|
230
|
+
<span class="kw">await</span> sp.payment.refund(<span class="st">'CAPTURE_ID'</span>)
|
|
231
|
+
|
|
232
|
+
<span class="cm">// Partial refund</span>
|
|
233
|
+
<span class="kw">await</span> sp.payment.refund(<span class="st">'CAPTURE_ID'</span>, <span class="nm">10.00</span>, <span class="st">'Partial refund for returned item'</span>)</pre>
|
|
234
|
+
|
|
235
|
+
<h3>payment.list(options?) <span class="badge b-get">GET</span></h3>
|
|
236
|
+
<pre><span class="kw">const</span> { transactions } = <span class="kw">await</span> sp.payment.list({ limit: <span class="nm">50</span>, status: <span class="st">'completed'</span> })</pre>
|
|
237
|
+
|
|
238
|
+
<!-- AUTH MODULE -->
|
|
239
|
+
<h2 id="auth">auth module</h2>
|
|
240
|
+
<p>Passwordless email token authentication for your end users. Issue a one-time token, send it by email, verify it on return.</p>
|
|
241
|
+
|
|
242
|
+
<h3>auth.createToken(email) <span class="badge b-post">POST</span></h3>
|
|
243
|
+
<pre><span class="kw">const</span> { token, expires_at } = <span class="kw">await</span> sp.auth.createToken(<span class="st">'user@email.com'</span>)
|
|
244
|
+
<span class="cm">// Token expires in 15 minutes. Single-use.</span>
|
|
245
|
+
<span class="cm">// Send this token to the user's email in a magic link.</span>
|
|
246
|
+
|
|
247
|
+
<span class="cm">// Example magic link: https://yourapp.com/auth?token=TOKEN</span></pre>
|
|
248
|
+
|
|
249
|
+
<h3>auth.verifyToken(token) <span class="badge b-post">POST</span></h3>
|
|
250
|
+
<pre><span class="kw">const</span> result = <span class="kw">await</span> sp.auth.verifyToken(req.query.token)
|
|
251
|
+
<span class="cm">// → { ok: true, verified: true, email: "user@email.com" }</span>
|
|
252
|
+
|
|
253
|
+
<span class="cm">// If expired or already used:</span>
|
|
254
|
+
<span class="cm">// throws ShieldPayError: "Invalid or expired token" (status 410)</span></pre>
|
|
255
|
+
|
|
256
|
+
<!-- SECURITY MODULE -->
|
|
257
|
+
<h2 id="security">security module</h2>
|
|
258
|
+
|
|
259
|
+
<h3>security.checkIp(ip) <span class="badge b-post">POST</span> <span class="badge b-pro">Pro: IPQS</span></h3>
|
|
260
|
+
<p>Free plan: checks manual blocklist and Cloudflare threat score. Pro plan: real-time IPQS reputation check (VPN, Tor, fraud score).</p>
|
|
261
|
+
<pre><span class="kw">const</span> check = <span class="kw">await</span> sp.security.checkIp(<span class="st">'1.2.3.4'</span>)
|
|
262
|
+
<span class="cm">// Clean IP:</span>
|
|
263
|
+
<span class="cm">// → { ok: true, blocked: false, reason: null, country: "CH" }</span>
|
|
264
|
+
|
|
265
|
+
<span class="cm">// VPN detected (Pro):</span>
|
|
266
|
+
<span class="cm">// → { ok: true, blocked: true, reason: "Active VPN connection detected.", score: 92 }</span>
|
|
267
|
+
|
|
268
|
+
<span class="cm">// Tor exit node:</span>
|
|
269
|
+
<span class="cm">// → { ok: true, blocked: true, reason: "Tor exit node detected." }</span></pre>
|
|
270
|
+
|
|
271
|
+
<h3>security.checkEmail(email) <span class="badge b-post">POST</span></h3>
|
|
272
|
+
<pre><span class="kw">const</span> r = <span class="kw">await</span> sp.security.checkEmail(<span class="st">'test@mailinator.com'</span>)
|
|
273
|
+
<span class="cm">// → { ok: true, blocked: true, reason: 'Disposable email domain "mailinator.com" is not allowed.' }</span></pre>
|
|
274
|
+
|
|
275
|
+
<!-- ACCOUNT MODULE -->
|
|
276
|
+
<h2 id="account">account module</h2>
|
|
277
|
+
|
|
278
|
+
<h3>account.me() <span class="badge b-get">GET</span></h3>
|
|
279
|
+
<pre><span class="kw">const</span> { user, plan, api_endpoint } = <span class="kw">await</span> sp.account.me()
|
|
280
|
+
<span class="cm">// → { ok: true, user: { id, company, email, plan, ... }, plan: "pro",</span>
|
|
281
|
+
<span class="cm">// api_endpoint: "https://hnow.info/api/worker=sk_live_xxx" }</span></pre>
|
|
282
|
+
|
|
283
|
+
<h3>account.health() <span class="badge b-get">GET</span></h3>
|
|
284
|
+
<pre><span class="kw">const</span> status = <span class="kw">await</span> sp.account.health()
|
|
285
|
+
<span class="cm">// → { ok: true, ts: 1735000000000, region: "ZRH" }</span></pre>
|
|
286
|
+
|
|
287
|
+
<!-- ALL ENDPOINTS -->
|
|
288
|
+
<h2 id="endpoints">All Endpoints</h2>
|
|
289
|
+
<p>Base: <code>https://hnow.info/api/worker={KEY}</code></p>
|
|
290
|
+
<table>
|
|
291
|
+
<tr><th>Method</th><th>Path</th><th>Auth</th><th>Plan</th><th>Description</th></tr>
|
|
292
|
+
<tr><td><span class="badge b-get">GET</span></td><td><code>/health</code></td><td>—</td><td>Any</td><td>Health check + region</td></tr>
|
|
293
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/auth/register</code></td><td>—</td><td>Any</td><td>Register company (anti-spam check)</td></tr>
|
|
294
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/auth/verify</code></td><td>—</td><td>Any</td><td>Verify email token → get API key</td></tr>
|
|
295
|
+
<tr><td><span class="badge b-get">GET</span></td><td><code>/me</code></td><td>✓</td><td>Any</td><td>Account info + plan</td></tr>
|
|
296
|
+
<tr><td><span class="badge b-get">GET</span></td><td><code>/transactions</code></td><td>✓</td><td>Any</td><td>Transaction list (paginated)</td></tr>
|
|
297
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/payment/create</code></td><td>✓</td><td>Any</td><td>Create PayPal order</td></tr>
|
|
298
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/payment/capture</code></td><td>✓</td><td>Any</td><td>Capture approved order</td></tr>
|
|
299
|
+
<tr><td><span class="badge b-get">GET</span></td><td><code>/payment/status</code></td><td>✓</td><td>Any</td><td>Order status from PayPal</td></tr>
|
|
300
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/payment/refund</code></td><td>✓</td><td>Any</td><td>Refund a capture</td></tr>
|
|
301
|
+
<tr><td><span class="badge b-get">GET</span></td><td><code>/payment/list</code></td><td>✓</td><td>Any</td><td>List transactions</td></tr>
|
|
302
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/auth/token</code></td><td>✓</td><td>Any</td><td>Create email auth token for user</td></tr>
|
|
303
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/auth/token/verify</code></td><td>✓</td><td>Any</td><td>Verify email auth token</td></tr>
|
|
304
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/security/check-ip</code></td><td>✓</td><td>Pro+</td><td>IP reputation (IPQS on Pro)</td></tr>
|
|
305
|
+
<tr><td><span class="badge b-post">POST</span></td><td><code>/security/check-email</code></td><td>✓</td><td>Any</td><td>Disposable email check</td></tr>
|
|
306
|
+
</table>
|
|
307
|
+
|
|
308
|
+
<!-- ERRORS -->
|
|
309
|
+
<h2 id="errors">Errors & Codes</h2>
|
|
310
|
+
<p>All errors throw a <code>ShieldPayError</code> with <code>.message</code>, <code>.code</code>, and <code>.status</code>.</p>
|
|
311
|
+
<pre><span class="kw">import</span> { ShieldPay, ShieldPayError } <span class="kw">from</span> <span class="st">'shieldpay-sdk'</span>
|
|
312
|
+
|
|
313
|
+
<span class="kw">try</span> {
|
|
314
|
+
<span class="kw">const</span> order = <span class="kw">await</span> sp.payment.create({ amount: <span class="nm">-1</span> })
|
|
315
|
+
} <span class="kw">catch</span> (e) {
|
|
316
|
+
<span class="kw">if</span> (e <span class="kw">instanceof</span> ShieldPayError) {
|
|
317
|
+
console.error(e.message) <span class="cm">// "Invalid amount"</span>
|
|
318
|
+
console.error(e.code) <span class="cm">// "INVALID_AMOUNT"</span>
|
|
319
|
+
console.error(e.status) <span class="cm">// 400</span>
|
|
320
|
+
}
|
|
321
|
+
}</pre>
|
|
322
|
+
<table>
|
|
323
|
+
<tr><th>HTTP</th><th>Code</th><th>Meaning</th></tr>
|
|
324
|
+
<tr><td><code>400</code></td><td><code>INVALID_*</code></td><td>Bad request / missing or invalid parameters</td></tr>
|
|
325
|
+
<tr><td><code>401</code></td><td><code>INVALID_KEY</code></td><td>API key missing, invalid, or revoked</td></tr>
|
|
326
|
+
<tr><td><code>403</code></td><td><code>BLOCKED</code></td><td>Spam/VPN/rate limit blocked</td></tr>
|
|
327
|
+
<tr><td><code>404</code></td><td><code>NOT_FOUND</code></td><td>Endpoint or resource not found</td></tr>
|
|
328
|
+
<tr><td><code>409</code></td><td><code>CONFLICT</code></td><td>Email already registered</td></tr>
|
|
329
|
+
<tr><td><code>410</code></td><td><code>TOKEN_EXPIRED</code></td><td>Token expired or already used</td></tr>
|
|
330
|
+
<tr><td><code>429</code></td><td><code>RATE_LIMITED</code></td><td>Rate limit exceeded — check <code>Retry-After</code> header</td></tr>
|
|
331
|
+
<tr><td><code>502</code></td><td><code>UPSTREAM_ERROR</code></td><td>PayPal API error</td></tr>
|
|
332
|
+
<tr><td><code>TIMEOUT</code></td><td><code>TIMEOUT</code></td><td>Request timed out (default: 15s)</td></tr>
|
|
333
|
+
<tr><td><code>NETWORK</code></td><td><code>NETWORK_ERROR</code></td><td>Network unreachable</td></tr>
|
|
334
|
+
</table>
|
|
335
|
+
|
|
336
|
+
<!-- RATE LIMITS -->
|
|
337
|
+
<h2 id="ratelimits">Rate Limits</h2>
|
|
338
|
+
<table>
|
|
339
|
+
<tr><th>Scope</th><th>Limit</th><th>Window</th></tr>
|
|
340
|
+
<tr><td>API calls per key</td><td>1,000</td><td>1 hour (sliding)</td></tr>
|
|
341
|
+
<tr><td>Registrations per IP</td><td>5</td><td>1 hour</td></tr>
|
|
342
|
+
<tr><td>Login attempts per IP</td><td>10</td><td>15 minutes</td></tr>
|
|
343
|
+
<tr><td>Admin login per IP</td><td>5</td><td>15 minutes</td></tr>
|
|
344
|
+
</table>
|
|
345
|
+
<p>Rate limit headers are included on every response:</p>
|
|
346
|
+
<pre>X-RateLimit-Limit: 1000
|
|
347
|
+
X-RateLimit-Remaining: 994
|
|
348
|
+
Retry-After: 3142 <span class="cm"># seconds until reset (only on 429)</span></pre>
|
|
349
|
+
|
|
350
|
+
<!-- PLANS -->
|
|
351
|
+
<h2 id="plans">Free vs Pro</h2>
|
|
352
|
+
<div class="plan-grid">
|
|
353
|
+
<div class="plan-box">
|
|
354
|
+
<h4>Free — 0 CHF/mo</h4>
|
|
355
|
+
<ul>
|
|
356
|
+
<li><span class="ck">✓</span> Full API access</li>
|
|
357
|
+
<li><span class="ck">✓</span> PayPal payments</li>
|
|
358
|
+
<li><span class="ck">✓</span> Basic anti-spam</li>
|
|
359
|
+
<li><span class="ck">✓</span> Email token auth</li>
|
|
360
|
+
<li><span class="ck">✓</span> 1,000 req/hour</li>
|
|
361
|
+
<li style="color:var(--danger)"><span class="cx">✗</span> <strong>2% fee per transaction</strong></li>
|
|
362
|
+
<li style="color:var(--danger)"><span class="cx">✗</span> VPN/IPQS blocking</li>
|
|
363
|
+
<li style="color:var(--danger)"><span class="cx">✗</span> Auto-fallback</li>
|
|
364
|
+
</ul>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="plan-box pro">
|
|
367
|
+
<h4>Pro — 1 CHF/mo</h4>
|
|
368
|
+
<ul>
|
|
369
|
+
<li><span class="ck">✓</span> Everything in Free</li>
|
|
370
|
+
<li><span class="ck">✓</span> <strong>0% transaction fee</strong></li>
|
|
371
|
+
<li><span class="ck">✓</span> Real-time IPQS VPN/Tor detection</li>
|
|
372
|
+
<li><span class="ck">✓</span> ASN datacenter blocking</li>
|
|
373
|
+
<li><span class="ck">✓</span> Automatic payment fallback</li>
|
|
374
|
+
<li><span class="ck">✓</span> Retry queue (5min → 15min → 1h)</li>
|
|
375
|
+
<li><span class="ck">✓</span> Email alert on fallback</li>
|
|
376
|
+
</ul>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<!-- FEE CALCULATION -->
|
|
381
|
+
<h2 id="fees">Fee Calculation</h2>
|
|
382
|
+
<table>
|
|
383
|
+
<tr><th>Plan</th><th>Amount</th><th>Fee</th><th>Net to you</th></tr>
|
|
384
|
+
<tr><td>Free</td><td>100.00 CHF</td><td style="color:var(--warn)">2.00 CHF</td><td>98.00 CHF</td></tr>
|
|
385
|
+
<tr><td>Free</td><td>49.99 CHF</td><td style="color:var(--warn)">1.00 CHF</td><td>48.99 CHF</td></tr>
|
|
386
|
+
<tr><td>Pro</td><td>100.00 CHF</td><td style="color:var(--accent2)">0.00 CHF</td><td>100.00 CHF</td></tr>
|
|
387
|
+
<tr><td>Pro</td><td>1,000.00 CHF</td><td style="color:var(--accent2)">0.00 CHF</td><td>1,000.00 CHF</td></tr>
|
|
388
|
+
</table>
|
|
389
|
+
<p>The fee is calculated server-side and visible in the <code>payment.create()</code> response as <code>fee</code> and <code>net_amount</code>.</p>
|
|
390
|
+
|
|
391
|
+
<!-- WEBHOOKS -->
|
|
392
|
+
<h2 id="webhooks">PayPal Webhooks</h2>
|
|
393
|
+
<p>Register this URL in your PayPal Developer Dashboard → your app → Webhooks:</p>
|
|
394
|
+
<pre>https://hnow.info/api/webhook/paypal.php</pre>
|
|
395
|
+
<p>Required events: <code>PAYMENT.CAPTURE.COMPLETED</code>, <code>PAYMENT.CAPTURE.DENIED</code>, <code>PAYMENT.SALE.REFUNDED</code></p>
|
|
396
|
+
<p>ShieldPay verifies the PayPal webhook signature automatically on live mode.</p>
|
|
397
|
+
|
|
398
|
+
<!-- FALLBACK -->
|
|
399
|
+
<h2 id="fallback">Auto-Fallback (Pro)</h2>
|
|
400
|
+
<p>When a payment capture fails on a Pro account, ShieldPay automatically:</p>
|
|
401
|
+
<ol style="color:var(--muted);font-size:.9rem;padding-left:1.5rem;line-height:2.2">
|
|
402
|
+
<li>Logs a fallback event with <code>status: pending</code></li>
|
|
403
|
+
<li>Sends an email alert to the account's registered email</li>
|
|
404
|
+
<li>Enqueues a retry via Cloudflare Queues (after 5 minutes)</li>
|
|
405
|
+
<li>Retries up to 3 times (5min → 15min → 1 hour)</li>
|
|
406
|
+
<li>Admin can view and resolve fallbacks at <code>/admin.php?page=fallbacks</code></li>
|
|
407
|
+
</ol>
|
|
408
|
+
<p>Fallback is <strong>not available on the Free plan</strong>. Failed payments on Free simply return an error.</p>
|
|
409
|
+
|
|
410
|
+
</main>
|
|
411
|
+
</body>
|
|
412
|
+
</html>
|
package/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shieldpay-sdk v2.0.0
|
|
3
|
+
* Official SDK for the ShieldPay API
|
|
4
|
+
*
|
|
5
|
+
* Install: npm install shieldpay-sdk
|
|
6
|
+
* Docs: https://hnow.info/SDK/docs/
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { ShieldPay } from 'shieldpay-sdk'
|
|
10
|
+
* const sp = new ShieldPay({ apiKey: 'sk_live_xxx' })
|
|
11
|
+
* // SDK calls: https://hnow.info/api/worker=sk_live_xxx/{endpoint}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SDK_VERSION = '2.0.0'
|
|
15
|
+
const DEFAULT_BASE = 'https://hnow.info'
|
|
16
|
+
|
|
17
|
+
// ─── Main class ───────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export class ShieldPay {
|
|
20
|
+
#apiKey
|
|
21
|
+
#baseUrl
|
|
22
|
+
#timeout
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ apiKey: string, baseUrl?: string, timeout?: number }} options
|
|
26
|
+
*/
|
|
27
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE, timeout = 15000 } = {}) {
|
|
28
|
+
if (!apiKey) throw new ShieldPayError('apiKey is required', 'MISSING_KEY')
|
|
29
|
+
if (!apiKey.startsWith('sk_live_') && !apiKey.startsWith('sk_test_'))
|
|
30
|
+
throw new ShieldPayError('Invalid API key format. Expected sk_live_... or sk_test_...', 'INVALID_KEY')
|
|
31
|
+
|
|
32
|
+
this.#apiKey = apiKey
|
|
33
|
+
this.#baseUrl = baseUrl.replace(/\/$/, '')
|
|
34
|
+
this.#timeout = timeout
|
|
35
|
+
|
|
36
|
+
// Bound modules
|
|
37
|
+
this.payment = new PaymentModule(this)
|
|
38
|
+
this.auth = new AuthModule(this)
|
|
39
|
+
this.security = new SecurityModule(this)
|
|
40
|
+
this.account = new AccountModule(this)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the full API endpoint URL for a path.
|
|
45
|
+
* Format: https://hnow.info/api/worker={KEY}/{path}
|
|
46
|
+
*/
|
|
47
|
+
endpoint(path = '') {
|
|
48
|
+
return `${this.#baseUrl}/api/worker=${encodeURIComponent(this.#apiKey)}${path}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Internal HTTP client.
|
|
53
|
+
* @param {string} path - API path (e.g. '/payment/create')
|
|
54
|
+
* @param {RequestInit} options
|
|
55
|
+
* @returns {Promise<any>}
|
|
56
|
+
*/
|
|
57
|
+
async _request(path, options = {}) {
|
|
58
|
+
const url = this.endpoint(path)
|
|
59
|
+
const controller = new AbortController()
|
|
60
|
+
const timer = setTimeout(() => controller.abort(), this.#timeout)
|
|
61
|
+
|
|
62
|
+
let response
|
|
63
|
+
try {
|
|
64
|
+
response = await fetch(url, {
|
|
65
|
+
...options,
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'X-SDK-Version': `shieldpay-sdk/${SDK_VERSION}`,
|
|
69
|
+
...(options.headers || {}),
|
|
70
|
+
},
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
})
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e.name === 'AbortError') throw new ShieldPayError('Request timed out', 'TIMEOUT')
|
|
75
|
+
throw new ShieldPayError(`Network error: ${e.message}`, 'NETWORK_ERROR')
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = await response.json().catch(() => ({ ok: false, error: 'Invalid JSON response' }))
|
|
81
|
+
|
|
82
|
+
if (!response.ok || !data.ok) {
|
|
83
|
+
throw new ShieldPayError(
|
|
84
|
+
data.error || `HTTP ${response.status}`,
|
|
85
|
+
data.code || `HTTP_${response.status}`,
|
|
86
|
+
response.status,
|
|
87
|
+
data
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return data
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Payment Module ───────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
class PaymentModule {
|
|
98
|
+
constructor(sp) { this._sp = sp }
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a PayPal payment order.
|
|
102
|
+
* On Free plan: 2% fee is applied. On Pro: 0% fee.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} params
|
|
105
|
+
* @param {number} params.amount - Amount to charge
|
|
106
|
+
* @param {string} [params.currency] - ISO currency code (default: CHF)
|
|
107
|
+
* @param {string} [params.description]- Order description
|
|
108
|
+
* @param {string} [params.return_url] - URL after successful PayPal payment
|
|
109
|
+
* @param {string} [params.cancel_url] - URL if user cancels PayPal
|
|
110
|
+
*
|
|
111
|
+
* @returns {Promise<{order_id, approve_url, amount, fee, net_amount, currency, plan, fee_note}>}
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* const order = await sp.payment.create({ amount: 49.99, currency: 'CHF' })
|
|
115
|
+
* window.location.href = order.approve_url // redirect to PayPal
|
|
116
|
+
*/
|
|
117
|
+
async create({ amount, currency = 'CHF', description, return_url, cancel_url } = {}) {
|
|
118
|
+
if (!amount || isNaN(+amount) || +amount <= 0) throw new ShieldPayError('amount must be a positive number', 'INVALID_AMOUNT')
|
|
119
|
+
return this._sp._request('/payment/create', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
body: JSON.stringify({ amount: +amount, currency, description, return_url, cancel_url }),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Capture an approved PayPal order.
|
|
127
|
+
* Call this after PayPal redirects back to your return_url.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} orderId - PayPal order ID from create()
|
|
130
|
+
* @returns {Promise<{captured, order_id, status, amount, payer}>}
|
|
131
|
+
*/
|
|
132
|
+
async capture(orderId) {
|
|
133
|
+
if (!orderId) throw new ShieldPayError('orderId is required', 'MISSING_PARAM')
|
|
134
|
+
return this._sp._request('/payment/capture', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
body: JSON.stringify({ order_id: orderId }),
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the current status of a PayPal order.
|
|
142
|
+
* @param {string} orderId
|
|
143
|
+
* @returns {Promise<{order_id, status, amount, created, updated}>}
|
|
144
|
+
*/
|
|
145
|
+
async status(orderId) {
|
|
146
|
+
if (!orderId) throw new ShieldPayError('orderId is required', 'MISSING_PARAM')
|
|
147
|
+
return this._sp._request(`/payment/status?order_id=${encodeURIComponent(orderId)}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Refund a captured payment.
|
|
152
|
+
* @param {string} captureId - PayPal capture ID
|
|
153
|
+
* @param {number} [amount] - Partial refund amount (omit for full refund)
|
|
154
|
+
* @param {string} [note] - Note to customer
|
|
155
|
+
*/
|
|
156
|
+
async refund(captureId, amount, note) {
|
|
157
|
+
if (!captureId) throw new ShieldPayError('captureId is required', 'MISSING_PARAM')
|
|
158
|
+
return this._sp._request('/payment/refund', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
body: JSON.stringify({ capture_id: captureId, amount, note }),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* List recent transactions.
|
|
166
|
+
* @param {{ limit?: number, status?: string }} options
|
|
167
|
+
*/
|
|
168
|
+
async list({ limit = 20, status } = {}) {
|
|
169
|
+
const params = new URLSearchParams({ limit: String(limit) })
|
|
170
|
+
if (status) params.set('status', status)
|
|
171
|
+
return this._sp._request(`/payment/list?${params}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Auth Module ──────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
class AuthModule {
|
|
178
|
+
constructor(sp) { this._sp = sp }
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create a passwordless email login token for one of your end users.
|
|
182
|
+
* Token expires in 15 minutes.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} email - The user's email address
|
|
185
|
+
* @returns {Promise<{token, expires_at}>}
|
|
186
|
+
*/
|
|
187
|
+
async createToken(email) {
|
|
188
|
+
if (!email) throw new ShieldPayError('email is required', 'MISSING_PARAM')
|
|
189
|
+
return this._sp._request('/auth/token', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
body: JSON.stringify({ email }),
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Verify a token your user received by email.
|
|
197
|
+
* Tokens are single-use and expire after 15 minutes.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} token
|
|
200
|
+
* @returns {Promise<{verified, email}>}
|
|
201
|
+
*/
|
|
202
|
+
async verifyToken(token) {
|
|
203
|
+
if (!token) throw new ShieldPayError('token is required', 'MISSING_PARAM')
|
|
204
|
+
return this._sp._request('/auth/token/verify', {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
body: JSON.stringify({ token }),
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Security Module ──────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
class SecurityModule {
|
|
214
|
+
constructor(sp) { this._sp = sp }
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if an IP is blocked (VPN, Tor, datacenter, fraud score).
|
|
218
|
+
* Pro plan uses real-time IPQS reputation check.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} ip - IPv4 or IPv6 address
|
|
221
|
+
* @returns {Promise<{blocked, reason, country, score?}>}
|
|
222
|
+
*/
|
|
223
|
+
async checkIp(ip) {
|
|
224
|
+
if (!ip) throw new ShieldPayError('ip is required', 'MISSING_PARAM')
|
|
225
|
+
return this._sp._request('/security/check-ip', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
body: JSON.stringify({ ip }),
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if an email is disposable or blocked.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} email
|
|
235
|
+
* @returns {Promise<{blocked, reason}>}
|
|
236
|
+
*/
|
|
237
|
+
async checkEmail(email) {
|
|
238
|
+
if (!email) throw new ShieldPayError('email is required', 'MISSING_PARAM')
|
|
239
|
+
return this._sp._request('/security/check-email', {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
body: JSON.stringify({ email }),
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── Account Module ───────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
class AccountModule {
|
|
249
|
+
constructor(sp) { this._sp = sp }
|
|
250
|
+
|
|
251
|
+
/** Get your account info and plan. */
|
|
252
|
+
async me() {
|
|
253
|
+
return this._sp._request('/me')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** List all transactions. */
|
|
257
|
+
async transactions(options = {}) {
|
|
258
|
+
return this._sp._request('/transactions', { method: 'GET' })
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Health check — verify the API is reachable. */
|
|
262
|
+
async health() {
|
|
263
|
+
const res = await fetch(`${this._sp.endpoint('')}health`)
|
|
264
|
+
return res.json()
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Static registration (no instance needed) ─────────────────────────────────
|
|
269
|
+
|
|
270
|
+
ShieldPay.register = async function(params, baseUrl = DEFAULT_BASE) {
|
|
271
|
+
const { company, email, password } = params
|
|
272
|
+
if (!company || !email || !password) throw new ShieldPayError('company, email and password are required', 'MISSING_PARAMS')
|
|
273
|
+
|
|
274
|
+
const url = `${baseUrl}/api/auth/register`
|
|
275
|
+
const res = await fetch(url, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: { 'Content-Type': 'application/json', 'X-SDK-Version': `shieldpay-sdk/${SDK_VERSION}` },
|
|
278
|
+
body: JSON.stringify({ company, email, password }),
|
|
279
|
+
})
|
|
280
|
+
const data = await res.json()
|
|
281
|
+
if (!data.ok) throw new ShieldPayError(data.error, data.code, res.status)
|
|
282
|
+
return data
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
ShieldPay.verifyEmail = async function(token, baseUrl = DEFAULT_BASE) {
|
|
286
|
+
const url = `${baseUrl}/api/auth/verify`
|
|
287
|
+
const res = await fetch(url, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: { 'Content-Type': 'application/json', 'X-SDK-Version': `shieldpay-sdk/${SDK_VERSION}` },
|
|
290
|
+
body: JSON.stringify({ token }),
|
|
291
|
+
})
|
|
292
|
+
const data = await res.json()
|
|
293
|
+
if (!data.ok) throw new ShieldPayError(data.error, data.code, res.status)
|
|
294
|
+
return data // { api_key, api_endpoint, message }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── Error Class ──────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
export class ShieldPayError extends Error {
|
|
300
|
+
/**
|
|
301
|
+
* @param {string} message
|
|
302
|
+
* @param {string} [code] - Machine-readable error code
|
|
303
|
+
* @param {number} [status] - HTTP status code
|
|
304
|
+
* @param {object} [raw] - Full raw API response
|
|
305
|
+
*/
|
|
306
|
+
constructor(message, code = 'UNKNOWN', status, raw) {
|
|
307
|
+
super(message)
|
|
308
|
+
this.name = 'ShieldPayError'
|
|
309
|
+
this.code = code
|
|
310
|
+
this.status = status
|
|
311
|
+
this.raw = raw
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── CommonJS compatibility ───────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
318
|
+
module.exports = { ShieldPay, ShieldPayError }
|
|
319
|
+
module.exports.default = ShieldPay
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default ShieldPay
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shieldpay-api-sdkjs",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "FIXLE SDK for the ShieldPay API — payments, anti-spam, VPN blocking and email auth",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./index.js",
|
|
10
|
+
"require": "./index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node test.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["paypal", "payments", "anti-spam", "vpn-detection", "cloudflare-workers", "shieldpay"],
|
|
18
|
+
"author": "Fixle",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://fixle.ch/shieldpay/SDK/docs/",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/FixleCH/shieldpay-sdk"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|