@invonetwork/web-sdk 0.3.0 → 0.4.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 +122 -96
- package/README.md +42 -20
- package/dist/{chunk-DV3WZGMH.js → chunk-EEWOAUXO.js} +28 -10
- package/dist/index.cjs +74 -36
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.js +50 -30
- package/dist/server.cjs +210 -39
- package/dist/server.d.cts +78 -19
- package/dist/server.d.ts +78 -19
- package/dist/server.js +185 -34
- package/dist/{types-CBkoUymV.d.cts → types-CBMLNwbe.d.cts} +32 -1
- package/dist/{types-CBkoUymV.d.ts → types-CBMLNwbe.d.ts} +32 -1
- package/package.json +76 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,96 +1,122 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@invonetwork/web-sdk` are documented here. This project follows
|
|
4
|
-
[Semantic Versioning](https://semver.org/).
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@invonetwork/web-sdk` are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/). Releases are managed with
|
|
5
|
+
[changesets](https://github.com/changesets/changesets).
|
|
6
|
+
|
|
7
|
+
## [0.4.1] — 2026-06-30
|
|
8
|
+
|
|
9
|
+
Docs only — replaced the README's internal-leaking "Deployment prerequisites"
|
|
10
|
+
(backend flag names, DB columns, gating mechanics) with a partner-facing
|
|
11
|
+
"Before you go live"; republished so the npm page README is current.
|
|
12
|
+
|
|
13
|
+
## [0.4.0] — 2026-06-30
|
|
14
|
+
|
|
15
|
+
Additive release — more server reads, edge-ready webhooks, cancellation, and tooling.
|
|
16
|
+
|
|
17
|
+
- **`getInboundPending({ playerEmail | playerPhone })`** — live, unclaimed inbound
|
|
18
|
+
sends/transfers for a player (the source of truth behind the "you have X to collect"
|
|
19
|
+
badge; pairs with `transfer.claim_pending`).
|
|
20
|
+
- **`verifyWebhookAsync`** — Web Crypto variant of `verifyWebhook` that runs on
|
|
21
|
+
Cloudflare Workers / Deno / Vercel+Netlify Edge / Bun / browsers; and
|
|
22
|
+
**`createWebhookHandler`** — a zero-dep Fetch-API `(Request) => Promise<Response>`
|
|
23
|
+
webhook route handler (Next.js App Router, Workers, Deno, Hono, Bun).
|
|
24
|
+
- **`iterateItemPurchaseHistory`** — async iterator that pages through a player's
|
|
25
|
+
full item-purchase history.
|
|
26
|
+
- **Per-call `AbortSignal`** — every method accepts an optional `{ signal }`; an
|
|
27
|
+
aborted call throws `InvoError` code `ABORTED` and is never retried.
|
|
28
|
+
- **Tooling**: ESLint (+ lint in CI), changesets release automation, `SECURITY.md`,
|
|
29
|
+
and `CODEOWNERS`.
|
|
30
|
+
|
|
31
|
+
## [0.3.0] — 2026-06-30
|
|
32
|
+
|
|
33
|
+
Additive release — new server capabilities plus transport resilience/observability.
|
|
34
|
+
|
|
35
|
+
- **Webhook verification** (`/server`): `verifyWebhook(rawBody, signatureHeader, secret | secrets, opts?)`
|
|
36
|
+
— constant-time HMAC-SHA256 over `${t}.${rawBody}`, 5-minute replay window,
|
|
37
|
+
multi-secret rotation; returns a typed `InvoWebhookEvent` discriminated union
|
|
38
|
+
(`purchase.*`, `item.purchased`, `transfer.*`, `payout.status_changed`, `webhook.test`).
|
|
39
|
+
Throws `InvoError` (`WEBHOOK_SIGNATURE_INVALID` / `WEBHOOK_TIMESTAMP_EXPIRED` /
|
|
40
|
+
`WEBHOOK_MALFORMED` / `WEBHOOK_SECRET_MISSING`). Server-only; the browser bundle
|
|
41
|
+
stays crypto-free. Independently security-audited.
|
|
42
|
+
- **`getPlayerBalance({ playerEmail | playerId })`** (`/server`): typed `player` / `balances` / `summary`.
|
|
43
|
+
- **Automatic retries**: network errors/timeouts, `429` (honoring `retry_after`), and
|
|
44
|
+
`5xx` are retried with exponential backoff + jitter. New config `maxRetries`
|
|
45
|
+
(default 2, `0` disables) and `retryBaseDelayMs` (default 250).
|
|
46
|
+
- **Observability hooks**: optional `onRequest` / `onResponse` / `onError` on both
|
|
47
|
+
entries (best-effort/non-throwing); `InvoError.requestId` carries the backend
|
|
48
|
+
request id for support/tracing.
|
|
49
|
+
- **Typed reads**: `confirmPayment` → `ConfirmPaymentResult`; `getOrderDetails` /
|
|
50
|
+
`getItemOrderDetails` → `OrderDetailsResult`; `getItemPurchaseHistory` →
|
|
51
|
+
`ItemHistoryResult` (previously untyped `Record`). All keep `raw`.
|
|
52
|
+
- **Light validation**: `mintPlayerToken` and `createCheckout` require a non-blank
|
|
53
|
+
`playerEmail` (throws `INVALID_INPUT` before the network call).
|
|
54
|
+
- **License**: `package.json` `license` is now `SEE LICENSE IN LICENSE` (was
|
|
55
|
+
`UNLICENSED`); `LICENSE` rewritten as an explicit install-and-use grant for
|
|
56
|
+
building INVO integrations.
|
|
57
|
+
|
|
58
|
+
## [0.2.1] — 2026-06-30
|
|
59
|
+
|
|
60
|
+
Docs only — no code change (republished so the npm page README is current).
|
|
61
|
+
|
|
62
|
+
- Rewrote the README into a complete integration/deployment guide: capability
|
|
63
|
+
overview, architecture, deployment prerequisites, per-flow sections (currency
|
|
64
|
+
purchase, item purchase, sends, transfers, passkeys), webhooks, errors, and a
|
|
65
|
+
full API reference for both entries.
|
|
66
|
+
- Added INVO console onboarding: `https://console.invo.network` (production) and
|
|
67
|
+
`https://dev.console.invo.network` (testing/sandbox), mapped to their API base URLs.
|
|
68
|
+
|
|
69
|
+
## [0.2.0] — 2026-06-30
|
|
70
|
+
|
|
71
|
+
Adds **item purchase** (spend existing game currency on an in-game item, §4.8) — an
|
|
72
|
+
additive, server-only surface.
|
|
73
|
+
|
|
74
|
+
- `InvoServer` (`/server`): `purchaseItem`, `getItemPurchaseHistory`, `getItemOrderDetails`.
|
|
75
|
+
- Server-side, game-secret auth — no passkey, no real money, no payment rail (it's a
|
|
76
|
+
balance debit). Grant the item off the `item.purchased` webhook.
|
|
77
|
+
- Client-side guards: required fields (trim-checked), `itemQuantity` integer `1..1000`,
|
|
78
|
+
prices `> 0` and `<= 999999.99` (magnitude-safe 2-decimal check), and
|
|
79
|
+
`totalPrice == unitPrice × itemQuantity (±0.01)` compared in integer cents.
|
|
80
|
+
- Load-bearing response fields (`transaction_id`, `order_id`) throw `INVALID_RESPONSE`
|
|
81
|
+
if missing on a 200.
|
|
82
|
+
- `InvoError` helpers: `isInsufficientBalance` (gated to 400; not the `429` throttle),
|
|
83
|
+
`isDuplicateRequest` (409), `retryAfter` (numeric or string `retry_after`); all
|
|
84
|
+
null-safe against non-JSON error bodies.
|
|
85
|
+
- Independently audited (2 agents) against handoff doc §4.8/§6/§8; contract verified,
|
|
86
|
+
error-classification edge cases fixed.
|
|
87
|
+
|
|
88
|
+
## [0.1.0] — 2026-06-30
|
|
89
|
+
|
|
90
|
+
Initial scaffold.
|
|
91
|
+
|
|
92
|
+
- `InvoServer` (`/server`): `mintPlayerToken`, `initiateSend`, `initiateTransfer`,
|
|
93
|
+
`createCheckout`, `purchaseCurrency`, `confirmPayment`, `getOrderDetails`.
|
|
94
|
+
- Client-side purchase guards (§4.7): USD amount `0 < x ≤ 999.99`, required
|
|
95
|
+
`purchaseReference`, and `rail:"steam"` rejected before the network call
|
|
96
|
+
(`INVALID_INPUT` / `MISSING_PURCHASE_REFERENCE` / `WRONG_RAIL_ENDPOINT`).
|
|
97
|
+
- `InvoClient` (browser): `enrollPasskey`, `approveSend`/`approveTransfer`,
|
|
98
|
+
`confirmReceiptSend`/`confirmReceiptTransfer`, and `linkDevice` for the
|
|
99
|
+
interchangeable-methods flow (§4.6).
|
|
100
|
+
- Optional `refreshToken` hook: transparently re-mints and retries once on
|
|
101
|
+
`SDK_TOKEN_EXPIRED` (§11.2).
|
|
102
|
+
- Shared: typed `InvoError`, isomorphic HTTP client, WebAuthn JSON⇄binary helpers.
|
|
103
|
+
- Tests: HTTP layer, server request mapping + purchase guards, browser client
|
|
104
|
+
flows (enroll/approve/link/token-refresh), and WebAuthn serialization.
|
|
105
|
+
- Contracts extracted + auditor-verified against the INVO backend.
|
|
106
|
+
|
|
107
|
+
### Hardening (independent red-team pass)
|
|
108
|
+
|
|
109
|
+
- **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`** —
|
|
110
|
+
`initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
|
|
111
|
+
`guardianApproval` block on the guardian path, so callers don't route into the
|
|
112
|
+
PIN UI by mistake (§4.3).
|
|
113
|
+
- `usdAmount` validation tightened: rejects non-plain-decimal strings
|
|
114
|
+
(`"0x10"`, `"1e2"`, whitespace) and >2 decimal places before any network call.
|
|
115
|
+
- Load-bearing response fields (`token`, `checkout_url`) now throw `INVALID_RESPONSE`
|
|
116
|
+
instead of silently surfacing as empty strings.
|
|
117
|
+
- `getOrderDetails` requires at least one of `orderId`/`transactionId`.
|
|
118
|
+
- Token refresh now re-runs the **whole** passkey ceremony on `SDK_TOKEN_EXPIRED`
|
|
119
|
+
(never replays a single-use assertion) and single-flights concurrent refreshes.
|
|
120
|
+
- `baseUrl` must be `https://` (localhost exempt) so the token/secret can't travel
|
|
121
|
+
in cleartext.
|
|
122
|
+
- Published tarball excludes sourcemaps (no proprietary source shipped).
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
First-party TypeScript SDK for integrating **INVO** into partner **web** platforms (storefronts, web games, dashboards). It wraps INVO's web money flows behind a typed, versioned API — the web analog of the Unity/Unreal plugins.
|
|
4
4
|
|
|
5
|
-
> **Status:** `v0.
|
|
5
|
+
> **Status:** `v0.4.0`, published on npm. The backend it wraps is **live** on sandbox + production, so you can build and test against sandbox today.
|
|
6
6
|
> Canonical partner reference: **https://docs.invo.network/docs/currency-purchase** and **https://docs.invo.network/docs/game-developer-integration**.
|
|
7
7
|
|
|
8
8
|
## What it does
|
|
@@ -22,7 +22,7 @@ The **game secret stays on your server**; the browser only ever holds a short-li
|
|
|
22
22
|
|
|
23
23
|
- [Install](#install)
|
|
24
24
|
- [Architecture & the two entry points](#architecture--the-two-entry-points)
|
|
25
|
-
- [
|
|
25
|
+
- [Before you go live](#before-you-go-live)
|
|
26
26
|
- [Configuration](#configuration)
|
|
27
27
|
- [Currency purchase (real money in)](#currency-purchase-real-money-in)
|
|
28
28
|
- [Item purchase (spend game currency)](#item-purchase-spend-game-currency)
|
|
@@ -78,25 +78,16 @@ Build and test against the **dev console + sandbox** first, then switch to the *
|
|
|
78
78
|
|
|
79
79
|
**Never import `/server` into browser code** — it carries the game secret. The two entries are built separately for exactly this reason.
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## Before you go live
|
|
82
82
|
|
|
83
|
-
INVO
|
|
83
|
+
INVO enables each flow for your tenant in the [console](#get-your-account--game-secret-invo-console). What you need to do:
|
|
84
84
|
|
|
85
|
-
**
|
|
86
|
-
-
|
|
87
|
-
- `
|
|
88
|
-
-
|
|
89
|
-
- Tenant migrated (`games.sdk_verification_enabled`).
|
|
90
|
-
- **Per-tenant RP ID + origins** (`webauthn_rp_id`, `webauthn_origins`). There's no separate "webauthn on" flag — *the presence of a valid RP ID is the gate*. Until it's set, WebAuthn endpoints return `403 WEBAUTHN_NOT_ENABLED_FOR_TENANT`. **You must serve your integration from an origin listed in `webauthn_origins`,** or passkeys won't validate.
|
|
85
|
+
- **Store the game secret server-side** and expose a small endpoint that calls `mintPlayerToken` so the browser can fetch/refresh its token. Never ship the secret to the browser.
|
|
86
|
+
- **For passkeys (sends/transfers):** give INVO the **web origin(s)** you'll serve from. Passkeys only validate on approved origins — if a call returns `WEBAUTHN_NOT_ENABLED_FOR_TENANT`, your origins aren't set up yet; contact INVO. (Until enrolled, the sender falls back to the SMS-PIN path automatically.)
|
|
87
|
+
- **For currency purchase:** card checkout works out of the box; ask INVO to enable the `game`/`steam` rails if you need them.
|
|
88
|
+
- **For item purchase:** nothing extra — it's a currency-balance debit.
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
- `platform` (card) rail is always on; `game`/`steam` rails are off by default and each gated by their own flag + per-game config.
|
|
94
|
-
- Honors the platform `purchases` kill switch (`503 flow_paused` when paused).
|
|
95
|
-
|
|
96
|
-
**For item purchase**
|
|
97
|
-
- The game must be in `live` or `testing` state (else `403`). No passkey/payment flags involved — it's a balance debit.
|
|
98
|
-
|
|
99
|
-
**On your side:** store the game secret in server-side config/secrets (never ship it to the browser), and expose a small endpoint that calls `mintPlayerToken` so the browser can fetch/refresh its token.
|
|
90
|
+
If a flow isn't enabled for your tenant yet, calls return a clear `InvoError` (e.g. `TENANT_NOT_MIGRATED`, `WEBAUTHN_NOT_ENABLED_FOR_TENANT`, or `flow_paused`) — coordinate with your INVO contact to turn it on.
|
|
100
91
|
|
|
101
92
|
## Configuration
|
|
102
93
|
|
|
@@ -232,7 +223,7 @@ const item = await server.purchaseItem({
|
|
|
232
223
|
- Client-side validation (missing fields, quantity outside `1..1000`, bad price, total ≠ unit×qty) throws `INVALID_INPUT` **before** any network call.
|
|
233
224
|
- Fee split: **90% developer / 10% INVO** by default (per-partner override). Not guardian-gated.
|
|
234
225
|
|
|
235
|
-
**Companion reads:** `server.getItemPurchaseHistory({ playerEmail, limit?, offset? })` and `server.getItemOrderDetails({ orderId | transactionId | clientRequestId })` (pass **exactly one** id — use `clientRequestId` for recovery: "did this purchase complete?").
|
|
226
|
+
**Companion reads:** `server.getItemPurchaseHistory({ playerEmail, limit?, offset? })` and `server.getItemOrderDetails({ orderId | transactionId | clientRequestId })` (pass **exactly one** id — use `clientRequestId` for recovery: "did this purchase complete?"). To walk the full history, `for await (const row of server.iterateItemPurchaseHistory({ playerEmail }))` pages automatically.
|
|
236
227
|
|
|
237
228
|
---
|
|
238
229
|
|
|
@@ -292,7 +283,15 @@ try {
|
|
|
292
283
|
- **`verificationMethod`** (from initiate): `"in_app"` → passkey approve; `"sms"` → un-enrolled, PIN sent; `undefined` + `guardianApproval` → minor/guardian (HTTP 202), do **not** show the PIN UI.
|
|
293
284
|
- **Claim codes** are returned only by `approveTransfer`; they're the out-of-band fallback when the recipient isn't enrolled.
|
|
294
285
|
- **`err.isReceiverNotEnrolled`** on `confirmReceipt*` is the explicit signal to switch to claim-code entry.
|
|
295
|
-
- Transfer self-claim
|
|
286
|
+
- Transfer self-claim may be disabled for your tenant; if it is, surface the claim-code path instead.
|
|
287
|
+
|
|
288
|
+
**"You have X to collect"** — to render a collect badge, list a player's live, unclaimed inbound sends/transfers (the source of truth behind the `transfer.claim_pending` webhook):
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const { inboundPending } = await server.getInboundPending({ playerEmail: "q@example.com" });
|
|
292
|
+
// each row: { transactionId, flow, amount, netAmount, sourceGame, toPhone, toIdentityId, claimCodeExpiresAt }
|
|
293
|
+
// match toPhone to the logged-in player (toIdentityId is null when the phone maps to >1 of your players)
|
|
294
|
+
```
|
|
296
295
|
|
|
297
296
|
---
|
|
298
297
|
|
|
@@ -349,6 +348,22 @@ function handler(req, res) {
|
|
|
349
348
|
|
|
350
349
|
`verifyWebhook` does constant-time HMAC-SHA256 over `${t}.${rawBody}`, enforces a 5-minute replay window, and accepts an **array of secrets** during rotation (`verifyWebhook(body, sig, [oldSecret, newSecret])`). It returns a typed `InvoWebhookEvent` (discriminate on `event_type`) and throws `InvoError` (`WEBHOOK_SIGNATURE_INVALID` / `WEBHOOK_TIMESTAMP_EXPIRED` / `WEBHOOK_MALFORMED` / `WEBHOOK_SECRET_MISSING`) on any failure. **De-dupe yourself on `X-Invo-Idempotency-Key`** — the SDK verifies, it doesn't track delivery.
|
|
351
350
|
|
|
351
|
+
**Edge / serverless** (Cloudflare Workers, Deno, Vercel/Netlify Edge, Bun): `verifyWebhook` uses `node:crypto`, so use **`verifyWebhookAsync`** (Web Crypto) — same args/result, just `await` it — or the ready-made Fetch-API handler:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { createWebhookHandler } from "@invonetwork/web-sdk/server";
|
|
355
|
+
|
|
356
|
+
// Next.js App Router — app/invo/webhooks/route.ts
|
|
357
|
+
export const POST = createWebhookHandler({
|
|
358
|
+
secret: process.env.INVO_WEBHOOK_SECRET!,
|
|
359
|
+
onEvent: async (event, { idempotencyKey }) => {
|
|
360
|
+
// de-dupe on idempotencyKey, then grant value. Throw to return 500 (Invo retries).
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`createWebhookHandler` returns `(request: Request) => Promise<Response>` and runs in Next.js, Workers, Deno, Hono, and Bun. Bad signature → `400`; a throwing `onEvent` → `500`.
|
|
366
|
+
|
|
352
367
|
### Event types
|
|
353
368
|
|
|
354
369
|
| Event | Fires for | Use it to |
|
|
@@ -381,6 +396,7 @@ new InvoServer({
|
|
|
381
396
|
> Note: hook payloads include the request `url`, which for some calls embeds a player email (e.g. balance-by-email). Tokens and the game secret are sent as headers and are **never** passed to hooks — but redact the `url` if you log hook payloads.
|
|
382
397
|
|
|
383
398
|
- **Request ids.** `InvoError.requestId` carries the backend request id (from `x-invo-request-id` / `x-request-id`) — quote it in support tickets.
|
|
399
|
+
- **Cancellation.** Every method takes an optional `{ signal }` (an `AbortSignal`) as its last argument — `server.getPlayerBalance({ playerEmail }, { signal })`. Aborting throws `InvoError` with `.code === "ABORTED"`, and an aborted call is never retried.
|
|
384
400
|
|
|
385
401
|
---
|
|
386
402
|
|
|
@@ -434,7 +450,13 @@ try {
|
|
|
434
450
|
| `getItemPurchaseHistory({ playerEmail, limit?, offset? })` | `{ history, pagination, raw }` |
|
|
435
451
|
| `getItemOrderDetails({ orderId? \| transactionId? \| clientRequestId? })` | `{ order, financialSummary, statusTimeline, raw }` |
|
|
436
452
|
| `getPlayerBalance({ playerEmail? \| playerId? })` | `{ player, balances, summary, raw }` |
|
|
453
|
+
| `getInboundPending({ playerEmail? \| playerPhone? })` | `{ inboundPending, raw }` — live unclaimed inbound sends/transfers |
|
|
454
|
+
| `iterateItemPurchaseHistory({ playerEmail, pageSize? })` | async iterator over all history rows |
|
|
437
455
|
| `verifyWebhook(rawBody, signatureHeader, secret \| secrets, opts?)` | typed `InvoWebhookEvent` (throws on bad signature) |
|
|
456
|
+
| `verifyWebhookAsync(...)` | same as `verifyWebhook`, Web Crypto (edge/Workers/Deno/Bun) |
|
|
457
|
+
| `createWebhookHandler({ secret, onEvent })` | `(Request) => Promise<Response>` webhook route handler |
|
|
458
|
+
|
|
459
|
+
Every method also accepts an optional final `{ signal }` (`AbortSignal`) for cancellation.
|
|
438
460
|
|
|
439
461
|
### `InvoClient` (`@invonetwork/web-sdk`)
|
|
440
462
|
|
|
@@ -96,11 +96,11 @@ var _Http = class _Http {
|
|
|
96
96
|
* (e.g. single-use WebAuthn assertions) are NEVER auto-retried.
|
|
97
97
|
*/
|
|
98
98
|
async post(path, body, auth, opts) {
|
|
99
|
-
return this.request("POST", path, body, auth, opts?.idempotent ?? false);
|
|
99
|
+
return this.request("POST", path, body, auth, opts?.idempotent ?? false, opts?.signal);
|
|
100
100
|
}
|
|
101
101
|
// GET is always idempotent → safe to retry.
|
|
102
|
-
async get(path, auth) {
|
|
103
|
-
return this.request("GET", path, void 0, auth, true);
|
|
102
|
+
async get(path, auth, opts) {
|
|
103
|
+
return this.request("GET", path, void 0, auth, true, opts?.signal);
|
|
104
104
|
}
|
|
105
105
|
authHeaders(auth) {
|
|
106
106
|
switch (auth.kind) {
|
|
@@ -112,7 +112,7 @@ var _Http = class _Http {
|
|
|
112
112
|
return {};
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
async request(method, path, body, auth, idempotent) {
|
|
115
|
+
async request(method, path, body, auth, idempotent, signal) {
|
|
116
116
|
const url = `${this.baseUrl}${path}`;
|
|
117
117
|
const headers = {
|
|
118
118
|
Accept: "application/json",
|
|
@@ -122,10 +122,13 @@ var _Http = class _Http {
|
|
|
122
122
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
123
123
|
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
124
124
|
for (let attempt = 0; ; attempt++) {
|
|
125
|
+
if (signal?.aborted) throw abortError(path);
|
|
125
126
|
const start = Date.now();
|
|
126
127
|
this.fire("onRequest", { method, url, attempt });
|
|
127
128
|
const controller = new AbortController();
|
|
128
129
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
130
|
+
const onAbort = () => controller.abort();
|
|
131
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
129
132
|
let res;
|
|
130
133
|
let networkError;
|
|
131
134
|
try {
|
|
@@ -138,12 +141,14 @@ var _Http = class _Http {
|
|
|
138
141
|
});
|
|
139
142
|
} finally {
|
|
140
143
|
clearTimeout(timer);
|
|
144
|
+
signal?.removeEventListener("abort", onAbort);
|
|
141
145
|
}
|
|
142
146
|
if (networkError) {
|
|
147
|
+
if (signal?.aborted) throw abortError(path);
|
|
143
148
|
const willRetry = idempotent && attempt < this.maxRetries;
|
|
144
149
|
this.fire("onError", { method, url, attempt, error: networkError, willRetry });
|
|
145
150
|
if (willRetry) {
|
|
146
|
-
await sleep(this.backoff(attempt));
|
|
151
|
+
await sleep(this.backoff(attempt), signal);
|
|
147
152
|
continue;
|
|
148
153
|
}
|
|
149
154
|
throw networkError;
|
|
@@ -179,7 +184,7 @@ var _Http = class _Http {
|
|
|
179
184
|
}
|
|
180
185
|
this.fire("onError", { method, url, attempt, error: err, willRetry: wait !== void 0 });
|
|
181
186
|
if (wait !== void 0) {
|
|
182
|
-
await sleep(wait);
|
|
187
|
+
await sleep(wait, signal);
|
|
183
188
|
continue;
|
|
184
189
|
}
|
|
185
190
|
throw err;
|
|
@@ -205,8 +210,21 @@ var _Http = class _Http {
|
|
|
205
210
|
/** HTTP statuses worth retrying — rate limit + transient gateway/server errors. */
|
|
206
211
|
_Http.RETRIABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
207
212
|
var Http = _Http;
|
|
208
|
-
function sleep(ms) {
|
|
209
|
-
return new Promise((resolve) =>
|
|
213
|
+
function sleep(ms, signal) {
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
if (signal?.aborted) return resolve();
|
|
216
|
+
const timer = setTimeout(done, ms);
|
|
217
|
+
const onAbort = () => done();
|
|
218
|
+
function done() {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
signal?.removeEventListener("abort", onAbort);
|
|
221
|
+
resolve();
|
|
222
|
+
}
|
|
223
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function abortError(path) {
|
|
227
|
+
return new InvoError({ message: `Request to ${path} was aborted`, code: "ABORTED", status: 0 });
|
|
210
228
|
}
|
|
211
229
|
function pickRequestId(headers) {
|
|
212
230
|
if (!headers || typeof headers.get !== "function") return void 0;
|
|
@@ -227,5 +245,5 @@ function retryAfterMs(parsed, headers) {
|
|
|
227
245
|
}
|
|
228
246
|
|
|
229
247
|
export { Http, InvoError, assertSecureBaseUrl };
|
|
230
|
-
//# sourceMappingURL=chunk-
|
|
231
|
-
//# sourceMappingURL=chunk-
|
|
248
|
+
//# sourceMappingURL=chunk-EEWOAUXO.js.map
|
|
249
|
+
//# sourceMappingURL=chunk-EEWOAUXO.js.map
|