@hypawave/sdk 0.2.2 → 0.4.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/AGENTS.md +1 -1
- package/CHANGELOG.md +44 -0
- package/README.md +10 -5
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +99 -50
- package/dist/index.mjs +99 -50
- package/package.json +7 -2
- package/skills/hypawave/SKILL.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -96,7 +96,7 @@ For Paths 3a / 3b, there is no SDK — use raw HTTP with pubkey signatures per t
|
|
|
96
96
|
3. **Funds flow buyer→seller directly.** Never route principal through any Hypawave endpoint. Only activation fees (small, Hypawave-issued bolt11s) go to Hypawave.
|
|
97
97
|
4. **Honor `terms_hash`** on Path 3b offers. If the server returns `409 terms_changed`, re-read the offer before paying.
|
|
98
98
|
5. **Do not invent endpoints.** If a field or path is not in openapi.json, it does not exist.
|
|
99
|
-
6. **Encrypt client-side** for file attachments. AES-256-GCM. Hypawave never sees plaintext.
|
|
99
|
+
6. **Encrypt client-side** for file attachments. AES-256-GCM. Hypawave never sees plaintext. `storeFile` requires `ciphertext_sha256` (SHA-256 hex of the bytes you upload) — Hypawave verifies + seals it so buyers can verify what they download.
|
|
100
100
|
|
|
101
101
|
## When you hit something the manual doesn't cover
|
|
102
102
|
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@hypawave/sdk` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.4.0]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `StoreFileParams.ciphertext_sha256` (**required**) — the lowercase-hex SHA-256
|
|
12
|
+
of the exact ciphertext you upload. Hypawave verifies it against the stored
|
|
13
|
+
bytes and seals them at the first bolt11 mint, and returns it at key retrieval
|
|
14
|
+
so buyers can verify downloaded bytes before decrypting (content integrity /
|
|
15
|
+
anti-bait-and-switch).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `storeFile()` now requires `ciphertext_sha256`. Compute `sha256` over the same
|
|
19
|
+
bytes you PUT to the presigned upload URL and pass it through. Calls without it
|
|
20
|
+
are rejected by the API with `400 validation_error`.
|
|
21
|
+
|
|
22
|
+
## [0.3.0]
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Automatic request retries with full-jitter exponential backoff, honoring the
|
|
26
|
+
`Retry-After` header on 429 / overload responses.
|
|
27
|
+
- `maxRetries` config option (default `3`; set `0` to disable retries).
|
|
28
|
+
- Adaptive backoff in `waitForSettlement` (ramps from `pollInterval` up to
|
|
29
|
+
`maxPollInterval`, default 2s → 20s) instead of a fixed-interval poll.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Retry policy is gated by safety: every `GET` and the server-idempotent POSTs
|
|
33
|
+
(`confirmPayment`, `topup`, `getUnlockStatus`, `getOfferDownloadUrl`) retry on
|
|
34
|
+
5xx and network errors; non-idempotent POSTs (e.g. `createInvoice`) retry only
|
|
35
|
+
on 429. This prevents duplicate side effects on retry.
|
|
36
|
+
- `timeout` now applies per attempt rather than to the whole call.
|
|
37
|
+
- Query parameters accept `string | number | boolean | undefined`; values are
|
|
38
|
+
serialized centrally and `undefined` is omitted.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- Non-JSON responses (e.g. an HTML 502 from an overloaded proxy) now surface as
|
|
42
|
+
a clean `HypawaveAPIError` instead of throwing an opaque JSON parse error.
|
|
43
|
+
- A successful (2xx) response with a non-JSON body now throws instead of
|
|
44
|
+
returning a malformed success object.
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**Bitcoin Lightning SDK for AI Agent Payments — non-custodial settlement with preimage-proof unlocks**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@hypawave/sdk)
|
|
6
|
+
[](https://github.com/hypawave/sdk/actions/workflows/ci.yml)
|
|
6
7
|
[](https://github.com/hypawave/sdk/blob/main/LICENSE)
|
|
7
8
|
[](https://nodejs.org)
|
|
8
9
|
|
|
@@ -156,7 +157,8 @@ Both files point to two authoritative web sources so instructions stay fresh:
|
|
|
156
157
|
|-----------|------|---------|-------------|
|
|
157
158
|
| `apiKey` | `string` | — | API key (`sk_test_*` or `sk_live_*`) |
|
|
158
159
|
| `baseUrl` | `string` | `https://hypawave.com` | API base URL |
|
|
159
|
-
| `timeout` | `number` | `30000` | Request timeout in ms |
|
|
160
|
+
| `timeout` | `number` | `30000` | Request timeout in ms (per attempt) |
|
|
161
|
+
| `maxRetries` | `number` | `3` | Max automatic retries on 429 / retryable 5xx / network errors (`0` disables). Honors `Retry-After`. |
|
|
160
162
|
|
|
161
163
|
### `createInvoice(params)`
|
|
162
164
|
|
|
@@ -330,12 +332,15 @@ const { downloadUrl } = await pp.getOfferDownloadUrl(paymentIntentId, {
|
|
|
330
332
|
|
|
331
333
|
### `waitForSettlement(invoiceId, options?)`
|
|
332
334
|
|
|
333
|
-
Poll until an invoice settles, fails, or expires.
|
|
335
|
+
Poll until an invoice settles, fails, or expires. Polling backs off
|
|
336
|
+
adaptively — the interval ramps from `pollInterval` up to `maxPollInterval`
|
|
337
|
+
(with jitter) so a busy backend isn't hit on a fixed cadence.
|
|
334
338
|
|
|
335
339
|
```typescript
|
|
336
340
|
const result = await pp.waitForSettlement(invoiceId, {
|
|
337
|
-
pollInterval: 2000,
|
|
338
|
-
|
|
341
|
+
pollInterval: 2000, // starting interval in ms (default: 2000)
|
|
342
|
+
maxPollInterval: 20000, // ceiling the interval ramps toward (default: 20000)
|
|
343
|
+
timeout: 300000, // max wait time in ms (default: 300000)
|
|
339
344
|
});
|
|
340
345
|
|
|
341
346
|
if (result.unlocked) {
|
|
@@ -379,7 +384,7 @@ Additional methods available — see types for full signatures, or [openapi.json
|
|
|
379
384
|
- `listInvoices(params?)` — list invoices with filters and pagination
|
|
380
385
|
- `getPayerReceipt(invoiceId, preimage)` — payer receipt fetch using the Lightning preimage as proof of payment (no API key needed)
|
|
381
386
|
- `getUploadUrl(params)` — signed URL for encrypted file upload (creator side)
|
|
382
|
-
- `storeFile(params)` — register an uploaded file against an invoice
|
|
387
|
+
- `storeFile(params)` — register an uploaded file against an invoice (requires `ciphertext_sha256`: the SHA-256 hex of the bytes you uploaded; Hypawave verifies + seals it so buyers can verify what they download)
|
|
383
388
|
- `storeFileKey(params)` — register a file's encryption key against an invoice
|
|
384
389
|
|
|
385
390
|
## Error Handling
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,8 @@ interface HypawaveConfig {
|
|
|
2
2
|
apiKey: string;
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
timeout?: number;
|
|
5
|
+
/** Max automatic retries on 429 / retryable 5xx / network errors. Default 3. Set 0 to disable. */
|
|
6
|
+
maxRetries?: number;
|
|
5
7
|
}
|
|
6
8
|
interface CreateInvoiceParams {
|
|
7
9
|
client_email: string;
|
|
@@ -106,6 +108,13 @@ interface StoreFileParams {
|
|
|
106
108
|
file_name: string;
|
|
107
109
|
encrypted_file_url: string;
|
|
108
110
|
iv_hex: string;
|
|
111
|
+
/**
|
|
112
|
+
* Lowercase-hex SHA-256 of the exact ciphertext uploaded to storage. Required.
|
|
113
|
+
* Hypawave verifies it against the stored bytes and seals them at the first
|
|
114
|
+
* bolt11 mint, and returns it at key retrieval so buyers can verify what they
|
|
115
|
+
* download. Compute over the same bytes you PUT to the presigned upload URL.
|
|
116
|
+
*/
|
|
117
|
+
ciphertext_sha256: string;
|
|
109
118
|
key_hash?: string;
|
|
110
119
|
size?: number;
|
|
111
120
|
}
|
|
@@ -259,6 +268,7 @@ declare class Hypawave {
|
|
|
259
268
|
private readonly apiKey;
|
|
260
269
|
private readonly baseUrl;
|
|
261
270
|
private readonly timeout;
|
|
271
|
+
private readonly maxRetries;
|
|
262
272
|
constructor(config: HypawaveConfig);
|
|
263
273
|
private request;
|
|
264
274
|
createInvoice(params: CreateInvoiceParams): Promise<CreateInvoiceResponse>;
|
|
@@ -292,6 +302,7 @@ declare class Hypawave {
|
|
|
292
302
|
getSettings(): Promise<PublicSettingsResponse>;
|
|
293
303
|
waitForSettlement(invoiceId: number, options?: {
|
|
294
304
|
pollInterval?: number;
|
|
305
|
+
maxPollInterval?: number;
|
|
295
306
|
timeout?: number;
|
|
296
307
|
}): Promise<UnlockStatusResponse["statuses"][string]>;
|
|
297
308
|
payAndConfirm(params: PayAndConfirmParams): Promise<PayAndConfirmResult>;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ interface HypawaveConfig {
|
|
|
2
2
|
apiKey: string;
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
timeout?: number;
|
|
5
|
+
/** Max automatic retries on 429 / retryable 5xx / network errors. Default 3. Set 0 to disable. */
|
|
6
|
+
maxRetries?: number;
|
|
5
7
|
}
|
|
6
8
|
interface CreateInvoiceParams {
|
|
7
9
|
client_email: string;
|
|
@@ -106,6 +108,13 @@ interface StoreFileParams {
|
|
|
106
108
|
file_name: string;
|
|
107
109
|
encrypted_file_url: string;
|
|
108
110
|
iv_hex: string;
|
|
111
|
+
/**
|
|
112
|
+
* Lowercase-hex SHA-256 of the exact ciphertext uploaded to storage. Required.
|
|
113
|
+
* Hypawave verifies it against the stored bytes and seals them at the first
|
|
114
|
+
* bolt11 mint, and returns it at key retrieval so buyers can verify what they
|
|
115
|
+
* download. Compute over the same bytes you PUT to the presigned upload URL.
|
|
116
|
+
*/
|
|
117
|
+
ciphertext_sha256: string;
|
|
109
118
|
key_hash?: string;
|
|
110
119
|
size?: number;
|
|
111
120
|
}
|
|
@@ -259,6 +268,7 @@ declare class Hypawave {
|
|
|
259
268
|
private readonly apiKey;
|
|
260
269
|
private readonly baseUrl;
|
|
261
270
|
private readonly timeout;
|
|
271
|
+
private readonly maxRetries;
|
|
262
272
|
constructor(config: HypawaveConfig);
|
|
263
273
|
private request;
|
|
264
274
|
createInvoice(params: CreateInvoiceParams): Promise<CreateInvoiceResponse>;
|
|
@@ -292,6 +302,7 @@ declare class Hypawave {
|
|
|
292
302
|
getSettings(): Promise<PublicSettingsResponse>;
|
|
293
303
|
waitForSettlement(invoiceId: number, options?: {
|
|
294
304
|
pollInterval?: number;
|
|
305
|
+
maxPollInterval?: number;
|
|
295
306
|
timeout?: number;
|
|
296
307
|
}): Promise<UnlockStatusResponse["statuses"][string]>;
|
|
297
308
|
payAndConfirm(params: PayAndConfirmParams): Promise<PayAndConfirmResult>;
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,20 @@ var HypawaveAPIError = class extends Error {
|
|
|
51
51
|
// src/client.ts
|
|
52
52
|
var DEFAULT_BASE_URL = "https://hypawave.com";
|
|
53
53
|
var DEFAULT_TIMEOUT = 3e4;
|
|
54
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
55
|
+
var RETRY_BASE_MS = 500;
|
|
56
|
+
var RETRY_CAP_MS = 8e3;
|
|
57
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
58
|
+
function backoffDelay(attempt, retryAfter) {
|
|
59
|
+
if (retryAfter) {
|
|
60
|
+
const secs = Number(retryAfter);
|
|
61
|
+
if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
|
|
62
|
+
const date = Date.parse(retryAfter);
|
|
63
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
64
|
+
}
|
|
65
|
+
const exp = Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt);
|
|
66
|
+
return Math.random() * exp;
|
|
67
|
+
}
|
|
54
68
|
async function sha256hex(hex) {
|
|
55
69
|
const bytes = new Uint8Array(hex.match(/.{2}/g).map((b) => parseInt(b, 16)));
|
|
56
70
|
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
@@ -67,42 +81,71 @@ var Hypawave = class {
|
|
|
67
81
|
this.apiKey = config.apiKey;
|
|
68
82
|
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
69
83
|
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
84
|
+
this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_MAX_RETRIES);
|
|
70
85
|
}
|
|
71
|
-
async request(method, path, body, query) {
|
|
86
|
+
async request(method, path, body, query, opts) {
|
|
72
87
|
let url = `${this.baseUrl}${path}`;
|
|
73
88
|
if (query) {
|
|
74
|
-
const params = new URLSearchParams(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const controller = new AbortController();
|
|
78
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
79
|
-
try {
|
|
80
|
-
const res = await fetch(url, {
|
|
81
|
-
method,
|
|
82
|
-
headers: {
|
|
83
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
84
|
-
"Content-Type": "application/json",
|
|
85
|
-
Accept: "application/json"
|
|
86
|
-
},
|
|
87
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
88
|
-
signal: controller.signal
|
|
89
|
-
});
|
|
90
|
-
const data = await res.json();
|
|
91
|
-
if (!res.ok) {
|
|
92
|
-
throw new HypawaveAPIError(res.status, data);
|
|
89
|
+
const params = new URLSearchParams();
|
|
90
|
+
for (const [k, v] of Object.entries(query)) {
|
|
91
|
+
if (v !== void 0) params.set(k, String(v));
|
|
93
92
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
const qs = params.toString();
|
|
94
|
+
if (qs) url += `?${qs}`;
|
|
95
|
+
}
|
|
96
|
+
const retrySafe = opts?.retrySafe ?? method === "GET";
|
|
97
|
+
for (let attempt = 0; ; attempt++) {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(url, {
|
|
102
|
+
method,
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
Accept: "application/json"
|
|
107
|
+
},
|
|
108
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
109
|
+
signal: controller.signal
|
|
101
110
|
});
|
|
111
|
+
let data;
|
|
112
|
+
let parseFailed = false;
|
|
113
|
+
try {
|
|
114
|
+
data = await res.json();
|
|
115
|
+
} catch {
|
|
116
|
+
parseFailed = true;
|
|
117
|
+
data = res.ok ? { error: "invalid_response", message: "Server returned a successful status with a non-JSON body." } : { error: `http_${res.status}`, message: `Server returned status ${res.status} with a non-JSON body.` };
|
|
118
|
+
}
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const retryable = res.status === 429 || res.status >= 500 && retrySafe;
|
|
121
|
+
if (retryable && attempt < this.maxRetries) {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
await sleep(backoffDelay(attempt, res.headers.get("retry-after")));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
throw new HypawaveAPIError(res.status, data);
|
|
127
|
+
}
|
|
128
|
+
if (parseFailed) {
|
|
129
|
+
throw new HypawaveAPIError(res.status, data);
|
|
130
|
+
}
|
|
131
|
+
return data;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err instanceof HypawaveAPIError) throw err;
|
|
134
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
135
|
+
throw new HypawaveAPIError(408, {
|
|
136
|
+
error: "timeout",
|
|
137
|
+
message: `Request timed out after ${this.timeout}ms`
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (retrySafe && attempt < this.maxRetries) {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
await sleep(backoffDelay(attempt, null));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
} finally {
|
|
147
|
+
clearTimeout(timer);
|
|
102
148
|
}
|
|
103
|
-
throw err;
|
|
104
|
-
} finally {
|
|
105
|
-
clearTimeout(timer);
|
|
106
149
|
}
|
|
107
150
|
}
|
|
108
151
|
async createInvoice(params) {
|
|
@@ -112,7 +155,9 @@ var Hypawave = class {
|
|
|
112
155
|
return this.request("GET", "/api/paystream-cb", void 0, { token: accessToken });
|
|
113
156
|
}
|
|
114
157
|
async confirmPayment(invoiceId, params) {
|
|
115
|
-
return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params
|
|
158
|
+
return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params, void 0, {
|
|
159
|
+
retrySafe: true
|
|
160
|
+
});
|
|
116
161
|
}
|
|
117
162
|
getPaymentPayload(params, response) {
|
|
118
163
|
return {
|
|
@@ -145,12 +190,16 @@ var Hypawave = class {
|
|
|
145
190
|
* - duplicate_topup — a pending top-up invoice already exists
|
|
146
191
|
*/
|
|
147
192
|
async topup(_params) {
|
|
148
|
-
return this.request("POST", "/api/agent/topup", {});
|
|
193
|
+
return this.request("POST", "/api/agent/topup", {}, void 0, { retrySafe: true });
|
|
149
194
|
}
|
|
150
195
|
async getUnlockStatus(invoiceIds) {
|
|
151
|
-
return this.request(
|
|
152
|
-
|
|
153
|
-
|
|
196
|
+
return this.request(
|
|
197
|
+
"POST",
|
|
198
|
+
"/api/get-unlock-status",
|
|
199
|
+
{ invoice_ids: invoiceIds },
|
|
200
|
+
void 0,
|
|
201
|
+
{ retrySafe: true }
|
|
202
|
+
);
|
|
154
203
|
}
|
|
155
204
|
async getKey(invoiceFileId, token) {
|
|
156
205
|
const query = { invoice_file_id: invoiceFileId };
|
|
@@ -167,16 +216,11 @@ var Hypawave = class {
|
|
|
167
216
|
return this.request("POST", "/api/agent/store-file-key", params);
|
|
168
217
|
}
|
|
169
218
|
async listInvoices(params) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"GET",
|
|
176
|
-
"/api/agent/list-invoices",
|
|
177
|
-
void 0,
|
|
178
|
-
Object.keys(query).length ? query : void 0
|
|
179
|
-
);
|
|
219
|
+
return this.request("GET", "/api/agent/list-invoices", void 0, {
|
|
220
|
+
limit: params?.limit,
|
|
221
|
+
offset: params?.offset,
|
|
222
|
+
status: params?.status
|
|
223
|
+
});
|
|
180
224
|
}
|
|
181
225
|
async getInvoiceFiles(invoiceIds, token) {
|
|
182
226
|
const body = { invoice_ids: invoiceIds };
|
|
@@ -192,12 +236,14 @@ var Hypawave = class {
|
|
|
192
236
|
return this.request(
|
|
193
237
|
"POST",
|
|
194
238
|
`/api/offers/payment-intent/${paymentIntentId}/download-url`,
|
|
195
|
-
params
|
|
239
|
+
params,
|
|
240
|
+
void 0,
|
|
241
|
+
{ retrySafe: true }
|
|
196
242
|
);
|
|
197
243
|
}
|
|
198
244
|
async getReceipt(invoiceId) {
|
|
199
245
|
return this.request("GET", "/api/agent/receipt", void 0, {
|
|
200
|
-
invoice_id:
|
|
246
|
+
invoice_id: invoiceId
|
|
201
247
|
});
|
|
202
248
|
}
|
|
203
249
|
async getPayerReceipt(invoiceId, preimage) {
|
|
@@ -209,10 +255,11 @@ var Hypawave = class {
|
|
|
209
255
|
return this.request("GET", "/api/public-settings");
|
|
210
256
|
}
|
|
211
257
|
async waitForSettlement(invoiceId, options) {
|
|
212
|
-
const
|
|
258
|
+
const base = options?.pollInterval ?? 2e3;
|
|
259
|
+
const maxInterval = options?.maxPollInterval ?? 2e4;
|
|
213
260
|
const timeout = options?.timeout ?? 3e5;
|
|
214
261
|
const start = Date.now();
|
|
215
|
-
|
|
262
|
+
for (let attempt = 0; Date.now() - start < timeout; attempt++) {
|
|
216
263
|
const status = await this.getUnlockStatus([invoiceId]);
|
|
217
264
|
const entry = status.statuses?.[String(invoiceId)];
|
|
218
265
|
if (entry?.unlocked) {
|
|
@@ -221,7 +268,9 @@ var Hypawave = class {
|
|
|
221
268
|
if (entry?.status === "failed" || entry?.status === "expired") {
|
|
222
269
|
return entry;
|
|
223
270
|
}
|
|
224
|
-
|
|
271
|
+
const ceiling = Math.min(maxInterval, base * 2 ** attempt);
|
|
272
|
+
const wait = ceiling / 2 + Math.random() * (ceiling / 2);
|
|
273
|
+
await sleep(Math.min(wait, Math.max(0, start + timeout - Date.now())));
|
|
225
274
|
}
|
|
226
275
|
throw new HypawaveAPIError(408, {
|
|
227
276
|
error: "settlement_timeout",
|
package/dist/index.mjs
CHANGED
|
@@ -24,6 +24,20 @@ var HypawaveAPIError = class extends Error {
|
|
|
24
24
|
// src/client.ts
|
|
25
25
|
var DEFAULT_BASE_URL = "https://hypawave.com";
|
|
26
26
|
var DEFAULT_TIMEOUT = 3e4;
|
|
27
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
28
|
+
var RETRY_BASE_MS = 500;
|
|
29
|
+
var RETRY_CAP_MS = 8e3;
|
|
30
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
function backoffDelay(attempt, retryAfter) {
|
|
32
|
+
if (retryAfter) {
|
|
33
|
+
const secs = Number(retryAfter);
|
|
34
|
+
if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
|
|
35
|
+
const date = Date.parse(retryAfter);
|
|
36
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
37
|
+
}
|
|
38
|
+
const exp = Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt);
|
|
39
|
+
return Math.random() * exp;
|
|
40
|
+
}
|
|
27
41
|
async function sha256hex(hex) {
|
|
28
42
|
const bytes = new Uint8Array(hex.match(/.{2}/g).map((b) => parseInt(b, 16)));
|
|
29
43
|
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
@@ -40,42 +54,71 @@ var Hypawave = class {
|
|
|
40
54
|
this.apiKey = config.apiKey;
|
|
41
55
|
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
42
56
|
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
57
|
+
this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_MAX_RETRIES);
|
|
43
58
|
}
|
|
44
|
-
async request(method, path, body, query) {
|
|
59
|
+
async request(method, path, body, query, opts) {
|
|
45
60
|
let url = `${this.baseUrl}${path}`;
|
|
46
61
|
if (query) {
|
|
47
|
-
const params = new URLSearchParams(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const controller = new AbortController();
|
|
51
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
52
|
-
try {
|
|
53
|
-
const res = await fetch(url, {
|
|
54
|
-
method,
|
|
55
|
-
headers: {
|
|
56
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
57
|
-
"Content-Type": "application/json",
|
|
58
|
-
Accept: "application/json"
|
|
59
|
-
},
|
|
60
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
61
|
-
signal: controller.signal
|
|
62
|
-
});
|
|
63
|
-
const data = await res.json();
|
|
64
|
-
if (!res.ok) {
|
|
65
|
-
throw new HypawaveAPIError(res.status, data);
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
for (const [k, v] of Object.entries(query)) {
|
|
64
|
+
if (v !== void 0) params.set(k, String(v));
|
|
66
65
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
const qs = params.toString();
|
|
67
|
+
if (qs) url += `?${qs}`;
|
|
68
|
+
}
|
|
69
|
+
const retrySafe = opts?.retrySafe ?? method === "GET";
|
|
70
|
+
for (let attempt = 0; ; attempt++) {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(url, {
|
|
75
|
+
method,
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
Accept: "application/json"
|
|
80
|
+
},
|
|
81
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
82
|
+
signal: controller.signal
|
|
74
83
|
});
|
|
84
|
+
let data;
|
|
85
|
+
let parseFailed = false;
|
|
86
|
+
try {
|
|
87
|
+
data = await res.json();
|
|
88
|
+
} catch {
|
|
89
|
+
parseFailed = true;
|
|
90
|
+
data = res.ok ? { error: "invalid_response", message: "Server returned a successful status with a non-JSON body." } : { error: `http_${res.status}`, message: `Server returned status ${res.status} with a non-JSON body.` };
|
|
91
|
+
}
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const retryable = res.status === 429 || res.status >= 500 && retrySafe;
|
|
94
|
+
if (retryable && attempt < this.maxRetries) {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
await sleep(backoffDelay(attempt, res.headers.get("retry-after")));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
throw new HypawaveAPIError(res.status, data);
|
|
100
|
+
}
|
|
101
|
+
if (parseFailed) {
|
|
102
|
+
throw new HypawaveAPIError(res.status, data);
|
|
103
|
+
}
|
|
104
|
+
return data;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof HypawaveAPIError) throw err;
|
|
107
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
108
|
+
throw new HypawaveAPIError(408, {
|
|
109
|
+
error: "timeout",
|
|
110
|
+
message: `Request timed out after ${this.timeout}ms`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (retrySafe && attempt < this.maxRetries) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
await sleep(backoffDelay(attempt, null));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
75
121
|
}
|
|
76
|
-
throw err;
|
|
77
|
-
} finally {
|
|
78
|
-
clearTimeout(timer);
|
|
79
122
|
}
|
|
80
123
|
}
|
|
81
124
|
async createInvoice(params) {
|
|
@@ -85,7 +128,9 @@ var Hypawave = class {
|
|
|
85
128
|
return this.request("GET", "/api/paystream-cb", void 0, { token: accessToken });
|
|
86
129
|
}
|
|
87
130
|
async confirmPayment(invoiceId, params) {
|
|
88
|
-
return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params
|
|
131
|
+
return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params, void 0, {
|
|
132
|
+
retrySafe: true
|
|
133
|
+
});
|
|
89
134
|
}
|
|
90
135
|
getPaymentPayload(params, response) {
|
|
91
136
|
return {
|
|
@@ -118,12 +163,16 @@ var Hypawave = class {
|
|
|
118
163
|
* - duplicate_topup — a pending top-up invoice already exists
|
|
119
164
|
*/
|
|
120
165
|
async topup(_params) {
|
|
121
|
-
return this.request("POST", "/api/agent/topup", {});
|
|
166
|
+
return this.request("POST", "/api/agent/topup", {}, void 0, { retrySafe: true });
|
|
122
167
|
}
|
|
123
168
|
async getUnlockStatus(invoiceIds) {
|
|
124
|
-
return this.request(
|
|
125
|
-
|
|
126
|
-
|
|
169
|
+
return this.request(
|
|
170
|
+
"POST",
|
|
171
|
+
"/api/get-unlock-status",
|
|
172
|
+
{ invoice_ids: invoiceIds },
|
|
173
|
+
void 0,
|
|
174
|
+
{ retrySafe: true }
|
|
175
|
+
);
|
|
127
176
|
}
|
|
128
177
|
async getKey(invoiceFileId, token) {
|
|
129
178
|
const query = { invoice_file_id: invoiceFileId };
|
|
@@ -140,16 +189,11 @@ var Hypawave = class {
|
|
|
140
189
|
return this.request("POST", "/api/agent/store-file-key", params);
|
|
141
190
|
}
|
|
142
191
|
async listInvoices(params) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"GET",
|
|
149
|
-
"/api/agent/list-invoices",
|
|
150
|
-
void 0,
|
|
151
|
-
Object.keys(query).length ? query : void 0
|
|
152
|
-
);
|
|
192
|
+
return this.request("GET", "/api/agent/list-invoices", void 0, {
|
|
193
|
+
limit: params?.limit,
|
|
194
|
+
offset: params?.offset,
|
|
195
|
+
status: params?.status
|
|
196
|
+
});
|
|
153
197
|
}
|
|
154
198
|
async getInvoiceFiles(invoiceIds, token) {
|
|
155
199
|
const body = { invoice_ids: invoiceIds };
|
|
@@ -165,12 +209,14 @@ var Hypawave = class {
|
|
|
165
209
|
return this.request(
|
|
166
210
|
"POST",
|
|
167
211
|
`/api/offers/payment-intent/${paymentIntentId}/download-url`,
|
|
168
|
-
params
|
|
212
|
+
params,
|
|
213
|
+
void 0,
|
|
214
|
+
{ retrySafe: true }
|
|
169
215
|
);
|
|
170
216
|
}
|
|
171
217
|
async getReceipt(invoiceId) {
|
|
172
218
|
return this.request("GET", "/api/agent/receipt", void 0, {
|
|
173
|
-
invoice_id:
|
|
219
|
+
invoice_id: invoiceId
|
|
174
220
|
});
|
|
175
221
|
}
|
|
176
222
|
async getPayerReceipt(invoiceId, preimage) {
|
|
@@ -182,10 +228,11 @@ var Hypawave = class {
|
|
|
182
228
|
return this.request("GET", "/api/public-settings");
|
|
183
229
|
}
|
|
184
230
|
async waitForSettlement(invoiceId, options) {
|
|
185
|
-
const
|
|
231
|
+
const base = options?.pollInterval ?? 2e3;
|
|
232
|
+
const maxInterval = options?.maxPollInterval ?? 2e4;
|
|
186
233
|
const timeout = options?.timeout ?? 3e5;
|
|
187
234
|
const start = Date.now();
|
|
188
|
-
|
|
235
|
+
for (let attempt = 0; Date.now() - start < timeout; attempt++) {
|
|
189
236
|
const status = await this.getUnlockStatus([invoiceId]);
|
|
190
237
|
const entry = status.statuses?.[String(invoiceId)];
|
|
191
238
|
if (entry?.unlocked) {
|
|
@@ -194,7 +241,9 @@ var Hypawave = class {
|
|
|
194
241
|
if (entry?.status === "failed" || entry?.status === "expired") {
|
|
195
242
|
return entry;
|
|
196
243
|
}
|
|
197
|
-
|
|
244
|
+
const ceiling = Math.min(maxInterval, base * 2 ** attempt);
|
|
245
|
+
const wait = ceiling / 2 + Math.random() * (ceiling / 2);
|
|
246
|
+
await sleep(Math.min(wait, Math.max(0, start + timeout - Date.now())));
|
|
198
247
|
}
|
|
199
248
|
throw new HypawaveAPIError(408, {
|
|
200
249
|
error: "settlement_timeout",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypawave/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "TypeScript SDK for Lightning settlement, preimage proof, and execution unlocks for AI agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
"skills",
|
|
18
18
|
"AGENTS.md",
|
|
19
19
|
"README.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
20
21
|
"LICENSE"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
24
28
|
"prepublishOnly": "npm run build"
|
|
25
29
|
},
|
|
26
30
|
"keywords": [
|
|
@@ -65,7 +69,8 @@
|
|
|
65
69
|
"homepage": "https://hypawave.com/docs",
|
|
66
70
|
"devDependencies": {
|
|
67
71
|
"tsup": "^8.0.0",
|
|
68
|
-
"typescript": "^5.0.0"
|
|
72
|
+
"typescript": "^5.0.0",
|
|
73
|
+
"vitest": "^3.0.0"
|
|
69
74
|
},
|
|
70
75
|
"engines": {
|
|
71
76
|
"node": ">=18"
|
package/skills/hypawave/SKILL.md
CHANGED
|
@@ -91,7 +91,7 @@ For Paths 3a / 3b, there is no SDK — use raw HTTP with pubkey signatures per t
|
|
|
91
91
|
3. **Funds flow buyer→seller directly.** Never route principal through any Hypawave endpoint. Only activation fees (small, Hypawave-issued bolt11s) go to Hypawave.
|
|
92
92
|
4. **Honor `terms_hash`** on Path 3b offers. If the server returns `409 terms_changed`, re-read the offer before paying.
|
|
93
93
|
5. **Do not invent endpoints.** If a field or path is not in openapi.json, it does not exist.
|
|
94
|
-
6. **Encrypt client-side** for file attachments. AES-256-GCM. Hypawave never sees plaintext.
|
|
94
|
+
6. **Encrypt client-side** for file attachments. AES-256-GCM. Hypawave never sees plaintext. `storeFile` requires `ciphertext_sha256` (SHA-256 hex of the bytes you upload) — Hypawave verifies + seals it so buyers can verify what they download.
|
|
95
95
|
|
|
96
96
|
## When you hit something the manual doesn't cover
|
|
97
97
|
|