@invonetwork/web-sdk 0.3.0 → 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/CHANGELOG.md +21 -1
- package/README.md +33 -2
- 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 +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to `@invonetwork/web-sdk` are documented here. This project follows
|
|
4
|
-
[Semantic Versioning](https://semver.org/).
|
|
4
|
+
[Semantic Versioning](https://semver.org/). Releases are managed with
|
|
5
|
+
[changesets](https://github.com/changesets/changesets).
|
|
6
|
+
|
|
7
|
+
## [0.4.0] — 2026-06-30
|
|
8
|
+
|
|
9
|
+
Additive release — more server reads, edge-ready webhooks, cancellation, and tooling.
|
|
10
|
+
|
|
11
|
+
- **`getInboundPending({ playerEmail | playerPhone })`** — live, unclaimed inbound
|
|
12
|
+
sends/transfers for a player (the source of truth behind the "you have X to collect"
|
|
13
|
+
badge; pairs with `transfer.claim_pending`).
|
|
14
|
+
- **`verifyWebhookAsync`** — Web Crypto variant of `verifyWebhook` that runs on
|
|
15
|
+
Cloudflare Workers / Deno / Vercel+Netlify Edge / Bun / browsers; and
|
|
16
|
+
**`createWebhookHandler`** — a zero-dep Fetch-API `(Request) => Promise<Response>`
|
|
17
|
+
webhook route handler (Next.js App Router, Workers, Deno, Hono, Bun).
|
|
18
|
+
- **`iterateItemPurchaseHistory`** — async iterator that pages through a player's
|
|
19
|
+
full item-purchase history.
|
|
20
|
+
- **Per-call `AbortSignal`** — every method accepts an optional `{ signal }`; an
|
|
21
|
+
aborted call throws `InvoError` code `ABORTED` and is never retried.
|
|
22
|
+
- **Tooling**: ESLint (+ lint in CI), changesets release automation, `SECURITY.md`,
|
|
23
|
+
and `CODEOWNERS`.
|
|
5
24
|
|
|
6
25
|
## [0.3.0] — 2026-06-30
|
|
7
26
|
|
|
@@ -80,6 +99,7 @@ Initial scaffold.
|
|
|
80
99
|
- Contracts extracted + auditor-verified against the INVO backend.
|
|
81
100
|
|
|
82
101
|
### Hardening (independent red-team pass)
|
|
102
|
+
|
|
83
103
|
- **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`** —
|
|
84
104
|
`initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
|
|
85
105
|
`guardianApproval` block on the guardian path, so callers don't route into the
|
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
|
|
@@ -232,7 +232,7 @@ const item = await server.purchaseItem({
|
|
|
232
232
|
- Client-side validation (missing fields, quantity outside `1..1000`, bad price, total ≠ unit×qty) throws `INVALID_INPUT` **before** any network call.
|
|
233
233
|
- Fee split: **90% developer / 10% INVO** by default (per-partner override). Not guardian-gated.
|
|
234
234
|
|
|
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?").
|
|
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?"). To walk the full history, `for await (const row of server.iterateItemPurchaseHistory({ playerEmail }))` pages automatically.
|
|
236
236
|
|
|
237
237
|
---
|
|
238
238
|
|
|
@@ -294,6 +294,14 @@ try {
|
|
|
294
294
|
- **`err.isReceiverNotEnrolled`** on `confirmReceipt*` is the explicit signal to switch to claim-code entry.
|
|
295
295
|
- Transfer self-claim additionally requires `SDK_TRANSFER_CONFIRM_RECEIPT_ENABLED`; if it's off, surface the claim-code path.
|
|
296
296
|
|
|
297
|
+
**"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):
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const { inboundPending } = await server.getInboundPending({ playerEmail: "q@example.com" });
|
|
301
|
+
// each row: { transactionId, flow, amount, netAmount, sourceGame, toPhone, toIdentityId, claimCodeExpiresAt }
|
|
302
|
+
// match toPhone to the logged-in player (toIdentityId is null when the phone maps to >1 of your players)
|
|
303
|
+
```
|
|
304
|
+
|
|
297
305
|
---
|
|
298
306
|
|
|
299
307
|
## Passkeys (enroll, approve, link)
|
|
@@ -349,6 +357,22 @@ function handler(req, res) {
|
|
|
349
357
|
|
|
350
358
|
`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
359
|
|
|
360
|
+
**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:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { createWebhookHandler } from "@invonetwork/web-sdk/server";
|
|
364
|
+
|
|
365
|
+
// Next.js App Router — app/invo/webhooks/route.ts
|
|
366
|
+
export const POST = createWebhookHandler({
|
|
367
|
+
secret: process.env.INVO_WEBHOOK_SECRET!,
|
|
368
|
+
onEvent: async (event, { idempotencyKey }) => {
|
|
369
|
+
// de-dupe on idempotencyKey, then grant value. Throw to return 500 (Invo retries).
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`createWebhookHandler` returns `(request: Request) => Promise<Response>` and runs in Next.js, Workers, Deno, Hono, and Bun. Bad signature → `400`; a throwing `onEvent` → `500`.
|
|
375
|
+
|
|
352
376
|
### Event types
|
|
353
377
|
|
|
354
378
|
| Event | Fires for | Use it to |
|
|
@@ -381,6 +405,7 @@ new InvoServer({
|
|
|
381
405
|
> 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
406
|
|
|
383
407
|
- **Request ids.** `InvoError.requestId` carries the backend request id (from `x-invo-request-id` / `x-request-id`) — quote it in support tickets.
|
|
408
|
+
- **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
409
|
|
|
385
410
|
---
|
|
386
411
|
|
|
@@ -434,7 +459,13 @@ try {
|
|
|
434
459
|
| `getItemPurchaseHistory({ playerEmail, limit?, offset? })` | `{ history, pagination, raw }` |
|
|
435
460
|
| `getItemOrderDetails({ orderId? \| transactionId? \| clientRequestId? })` | `{ order, financialSummary, statusTimeline, raw }` |
|
|
436
461
|
| `getPlayerBalance({ playerEmail? \| playerId? })` | `{ player, balances, summary, raw }` |
|
|
462
|
+
| `getInboundPending({ playerEmail? \| playerPhone? })` | `{ inboundPending, raw }` — live unclaimed inbound sends/transfers |
|
|
463
|
+
| `iterateItemPurchaseHistory({ playerEmail, pageSize? })` | async iterator over all history rows |
|
|
437
464
|
| `verifyWebhook(rawBody, signatureHeader, secret \| secrets, opts?)` | typed `InvoWebhookEvent` (throws on bad signature) |
|
|
465
|
+
| `verifyWebhookAsync(...)` | same as `verifyWebhook`, Web Crypto (edge/Workers/Deno/Bun) |
|
|
466
|
+
| `createWebhookHandler({ secret, onEvent })` | `(Request) => Promise<Response>` webhook route handler |
|
|
467
|
+
|
|
468
|
+
Every method also accepts an optional final `{ signal }` (`AbortSignal`) for cancellation.
|
|
438
469
|
|
|
439
470
|
### `InvoClient` (`@invonetwork/web-sdk`)
|
|
440
471
|
|
|
@@ -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
|
package/dist/index.cjs
CHANGED
|
@@ -98,11 +98,11 @@ var _Http = class _Http {
|
|
|
98
98
|
* (e.g. single-use WebAuthn assertions) are NEVER auto-retried.
|
|
99
99
|
*/
|
|
100
100
|
async post(path, body, auth, opts) {
|
|
101
|
-
return this.request("POST", path, body, auth, opts?.idempotent ?? false);
|
|
101
|
+
return this.request("POST", path, body, auth, opts?.idempotent ?? false, opts?.signal);
|
|
102
102
|
}
|
|
103
103
|
// GET is always idempotent → safe to retry.
|
|
104
|
-
async get(path, auth) {
|
|
105
|
-
return this.request("GET", path, void 0, auth, true);
|
|
104
|
+
async get(path, auth, opts) {
|
|
105
|
+
return this.request("GET", path, void 0, auth, true, opts?.signal);
|
|
106
106
|
}
|
|
107
107
|
authHeaders(auth) {
|
|
108
108
|
switch (auth.kind) {
|
|
@@ -114,7 +114,7 @@ var _Http = class _Http {
|
|
|
114
114
|
return {};
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
async request(method, path, body, auth, idempotent) {
|
|
117
|
+
async request(method, path, body, auth, idempotent, signal) {
|
|
118
118
|
const url = `${this.baseUrl}${path}`;
|
|
119
119
|
const headers = {
|
|
120
120
|
Accept: "application/json",
|
|
@@ -124,10 +124,13 @@ var _Http = class _Http {
|
|
|
124
124
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
125
125
|
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
126
126
|
for (let attempt = 0; ; attempt++) {
|
|
127
|
+
if (signal?.aborted) throw abortError(path);
|
|
127
128
|
const start = Date.now();
|
|
128
129
|
this.fire("onRequest", { method, url, attempt });
|
|
129
130
|
const controller = new AbortController();
|
|
130
131
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
132
|
+
const onAbort = () => controller.abort();
|
|
133
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
131
134
|
let res;
|
|
132
135
|
let networkError;
|
|
133
136
|
try {
|
|
@@ -140,12 +143,14 @@ var _Http = class _Http {
|
|
|
140
143
|
});
|
|
141
144
|
} finally {
|
|
142
145
|
clearTimeout(timer);
|
|
146
|
+
signal?.removeEventListener("abort", onAbort);
|
|
143
147
|
}
|
|
144
148
|
if (networkError) {
|
|
149
|
+
if (signal?.aborted) throw abortError(path);
|
|
145
150
|
const willRetry = idempotent && attempt < this.maxRetries;
|
|
146
151
|
this.fire("onError", { method, url, attempt, error: networkError, willRetry });
|
|
147
152
|
if (willRetry) {
|
|
148
|
-
await sleep(this.backoff(attempt));
|
|
153
|
+
await sleep(this.backoff(attempt), signal);
|
|
149
154
|
continue;
|
|
150
155
|
}
|
|
151
156
|
throw networkError;
|
|
@@ -181,7 +186,7 @@ var _Http = class _Http {
|
|
|
181
186
|
}
|
|
182
187
|
this.fire("onError", { method, url, attempt, error: err, willRetry: wait !== void 0 });
|
|
183
188
|
if (wait !== void 0) {
|
|
184
|
-
await sleep(wait);
|
|
189
|
+
await sleep(wait, signal);
|
|
185
190
|
continue;
|
|
186
191
|
}
|
|
187
192
|
throw err;
|
|
@@ -207,8 +212,21 @@ var _Http = class _Http {
|
|
|
207
212
|
/** HTTP statuses worth retrying — rate limit + transient gateway/server errors. */
|
|
208
213
|
_Http.RETRIABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
209
214
|
var Http = _Http;
|
|
210
|
-
function sleep(ms) {
|
|
211
|
-
return new Promise((resolve) =>
|
|
215
|
+
function sleep(ms, signal) {
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
if (signal?.aborted) return resolve();
|
|
218
|
+
const timer = setTimeout(done, ms);
|
|
219
|
+
const onAbort = () => done();
|
|
220
|
+
function done() {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
signal?.removeEventListener("abort", onAbort);
|
|
223
|
+
resolve();
|
|
224
|
+
}
|
|
225
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function abortError(path) {
|
|
229
|
+
return new InvoError({ message: `Request to ${path} was aborted`, code: "ABORTED", status: 0 });
|
|
212
230
|
}
|
|
213
231
|
function pickRequestId(headers) {
|
|
214
232
|
if (!headers || typeof headers.get !== "function") return void 0;
|
|
@@ -333,38 +351,43 @@ var InvoClient = class {
|
|
|
333
351
|
}
|
|
334
352
|
}
|
|
335
353
|
/** Enroll a passkey for the token's identity (register/begin -> create() -> register/complete). */
|
|
336
|
-
async enrollPasskey() {
|
|
354
|
+
async enrollPasskey(opts) {
|
|
337
355
|
this.assertWebAuthn();
|
|
356
|
+
const signal = opts?.signal;
|
|
338
357
|
return this.withTokenRetry(async () => {
|
|
339
358
|
const options = await this.post(
|
|
340
|
-
"/api/sdk/webauthn/register/begin"
|
|
359
|
+
"/api/sdk/webauthn/register/begin",
|
|
360
|
+
void 0,
|
|
361
|
+
signal
|
|
341
362
|
);
|
|
342
363
|
const cred = await navigator.credentials.create({
|
|
343
|
-
publicKey: toCreationOptions(options)
|
|
364
|
+
publicKey: toCreationOptions(options),
|
|
365
|
+
signal
|
|
344
366
|
});
|
|
345
367
|
if (!cred) throw new Error("Passkey creation was cancelled or returned no credential.");
|
|
346
368
|
const raw = await this.post(
|
|
347
369
|
"/api/sdk/webauthn/register/complete",
|
|
348
|
-
{ credential: registrationToJSON(cred) }
|
|
370
|
+
{ credential: registrationToJSON(cred) },
|
|
371
|
+
signal
|
|
349
372
|
);
|
|
350
373
|
return { status: String(raw["status"] ?? ""), device: raw["device"] ?? null, raw };
|
|
351
374
|
});
|
|
352
375
|
}
|
|
353
376
|
/** Approve a SEND with the player's passkey. */
|
|
354
|
-
async approveSend(transactionId) {
|
|
355
|
-
return this.approve("send", transactionId);
|
|
377
|
+
async approveSend(transactionId, opts) {
|
|
378
|
+
return this.approve("send", transactionId, opts);
|
|
356
379
|
}
|
|
357
380
|
/** Approve a TRANSFER with the player's passkey (returns the sender's claim code). */
|
|
358
|
-
async approveTransfer(transactionId) {
|
|
359
|
-
return this.approve("transfers", transactionId);
|
|
381
|
+
async approveTransfer(transactionId, opts) {
|
|
382
|
+
return this.approve("transfers", transactionId, opts);
|
|
360
383
|
}
|
|
361
384
|
/** Recipient self-claims a SEND with their passkey. */
|
|
362
|
-
async confirmReceiptSend(transactionId) {
|
|
363
|
-
return this.confirmReceipt("send", transactionId);
|
|
385
|
+
async confirmReceiptSend(transactionId, opts) {
|
|
386
|
+
return this.confirmReceipt("send", transactionId, opts);
|
|
364
387
|
}
|
|
365
388
|
/** Recipient self-claims a TRANSFER with their passkey. */
|
|
366
|
-
async confirmReceiptTransfer(transactionId) {
|
|
367
|
-
return this.confirmReceipt("transfers", transactionId);
|
|
389
|
+
async confirmReceiptTransfer(transactionId, opts) {
|
|
390
|
+
return this.confirmReceipt("transfers", transactionId, opts);
|
|
368
391
|
}
|
|
369
392
|
/**
|
|
370
393
|
* Interchangeable methods (§4.6): prove an *already-enrolled* method (e.g. the
|
|
@@ -374,15 +397,19 @@ var InvoClient = class {
|
|
|
374
397
|
*
|
|
375
398
|
* begin -> navigator.credentials.get() -> complete with { link_id, webauthn_assertion }.
|
|
376
399
|
*/
|
|
377
|
-
async linkDevice(linkId) {
|
|
400
|
+
async linkDevice(linkId, opts) {
|
|
378
401
|
if (!linkId) throw new Error("linkDevice requires a `linkId`.");
|
|
402
|
+
const signal = opts?.signal;
|
|
379
403
|
return this.withTokenRetry(async () => {
|
|
380
|
-
const assertion = await this.runAssertion(
|
|
381
|
-
|
|
382
|
-
|
|
404
|
+
const assertion = await this.runAssertion(
|
|
405
|
+
"/api/sdk/device/link/webauthn/begin",
|
|
406
|
+
{ link_id: linkId },
|
|
407
|
+
signal
|
|
408
|
+
);
|
|
383
409
|
const raw = await this.post(
|
|
384
410
|
"/api/sdk/device/link/webauthn/complete",
|
|
385
|
-
{ link_id: linkId, webauthn_assertion: assertion }
|
|
411
|
+
{ link_id: linkId, webauthn_assertion: assertion },
|
|
412
|
+
signal
|
|
386
413
|
);
|
|
387
414
|
return { status: String(raw["status"] ?? ""), raw };
|
|
388
415
|
});
|
|
@@ -390,8 +417,8 @@ var InvoClient = class {
|
|
|
390
417
|
// --- internals ---
|
|
391
418
|
/** POST with the current player token. Token-expiry retry is handled one level
|
|
392
419
|
* up by withTokenRetry (which re-runs the whole ceremony, not a single call). */
|
|
393
|
-
async post(path, body) {
|
|
394
|
-
return this.http.post(path, body, this.auth);
|
|
420
|
+
async post(path, body, signal) {
|
|
421
|
+
return this.http.post(path, body, this.auth, { signal });
|
|
395
422
|
}
|
|
396
423
|
/**
|
|
397
424
|
* Run a whole flow, retrying it ONCE if any call fails with SDK_TOKEN_EXPIRED
|
|
@@ -422,22 +449,29 @@ var InvoClient = class {
|
|
|
422
449
|
this.auth = { kind: "bearer", token: fresh };
|
|
423
450
|
return true;
|
|
424
451
|
}
|
|
425
|
-
async runAssertion(beginPath, beginBody) {
|
|
452
|
+
async runAssertion(beginPath, beginBody, signal) {
|
|
426
453
|
this.assertWebAuthn();
|
|
427
|
-
const options = await this.post(beginPath, beginBody);
|
|
454
|
+
const options = await this.post(beginPath, beginBody, signal);
|
|
428
455
|
const cred = await navigator.credentials.get({
|
|
429
|
-
publicKey: toRequestOptions(options)
|
|
456
|
+
publicKey: toRequestOptions(options),
|
|
457
|
+
signal
|
|
430
458
|
});
|
|
431
459
|
if (!cred) throw new Error("Passkey assertion was cancelled or returned no credential.");
|
|
432
460
|
return assertionToJSON(cred);
|
|
433
461
|
}
|
|
434
|
-
async approve(flow, transactionId) {
|
|
462
|
+
async approve(flow, transactionId, opts) {
|
|
435
463
|
const id = encodeURIComponent(transactionId);
|
|
464
|
+
const signal = opts?.signal;
|
|
436
465
|
return this.withTokenRetry(async () => {
|
|
437
|
-
const assertion = await this.runAssertion(
|
|
466
|
+
const assertion = await this.runAssertion(
|
|
467
|
+
`/api/sdk/${flow}/${id}/approve/webauthn/begin`,
|
|
468
|
+
void 0,
|
|
469
|
+
signal
|
|
470
|
+
);
|
|
438
471
|
const raw = await this.post(
|
|
439
472
|
`/api/sdk/${flow}/${id}/approve`,
|
|
440
|
-
{ webauthn_assertion: assertion }
|
|
473
|
+
{ webauthn_assertion: assertion },
|
|
474
|
+
signal
|
|
441
475
|
);
|
|
442
476
|
return {
|
|
443
477
|
status: String(raw["status"] ?? ""),
|
|
@@ -449,15 +483,19 @@ var InvoClient = class {
|
|
|
449
483
|
};
|
|
450
484
|
});
|
|
451
485
|
}
|
|
452
|
-
async confirmReceipt(flow, transactionId) {
|
|
486
|
+
async confirmReceipt(flow, transactionId, opts) {
|
|
453
487
|
const id = encodeURIComponent(transactionId);
|
|
488
|
+
const signal = opts?.signal;
|
|
454
489
|
return this.withTokenRetry(async () => {
|
|
455
490
|
const assertion = await this.runAssertion(
|
|
456
|
-
`/api/sdk/${flow}/${id}/confirm-receipt/webauthn/begin
|
|
491
|
+
`/api/sdk/${flow}/${id}/confirm-receipt/webauthn/begin`,
|
|
492
|
+
void 0,
|
|
493
|
+
signal
|
|
457
494
|
);
|
|
458
495
|
const raw = await this.post(
|
|
459
496
|
`/api/sdk/${flow}/${id}/confirm-receipt`,
|
|
460
|
-
{ webauthn_assertion: assertion }
|
|
497
|
+
{ webauthn_assertion: assertion },
|
|
498
|
+
signal
|
|
461
499
|
);
|
|
462
500
|
return { status: String(raw["status"] ?? ""), raw };
|
|
463
501
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ClientConfig, A as ApproveResult,
|
|
2
|
-
export { I as InvoError,
|
|
1
|
+
import { C as ClientConfig, a as CallOptions, A as ApproveResult, b as ConfirmReceiptResult, L as LinkDeviceResult } from './types-CBMLNwbe.cjs';
|
|
2
|
+
export { I as InvoError, c as InvoErrorInfo, d as InvoHooks, e as InvoRequestInfo, f as InvoResponseInfo, R as Rail, V as VerificationMethod } from './types-CBMLNwbe.cjs';
|
|
3
3
|
|
|
4
4
|
declare class InvoClient {
|
|
5
5
|
private readonly http;
|
|
@@ -10,19 +10,19 @@ declare class InvoClient {
|
|
|
10
10
|
constructor(config: ClientConfig);
|
|
11
11
|
private assertWebAuthn;
|
|
12
12
|
/** Enroll a passkey for the token's identity (register/begin -> create() -> register/complete). */
|
|
13
|
-
enrollPasskey(): Promise<{
|
|
13
|
+
enrollPasskey(opts?: CallOptions): Promise<{
|
|
14
14
|
status: string;
|
|
15
15
|
device: unknown;
|
|
16
16
|
raw: Record<string, unknown>;
|
|
17
17
|
}>;
|
|
18
18
|
/** Approve a SEND with the player's passkey. */
|
|
19
|
-
approveSend(transactionId: string): Promise<ApproveResult>;
|
|
19
|
+
approveSend(transactionId: string, opts?: CallOptions): Promise<ApproveResult>;
|
|
20
20
|
/** Approve a TRANSFER with the player's passkey (returns the sender's claim code). */
|
|
21
|
-
approveTransfer(transactionId: string): Promise<ApproveResult>;
|
|
21
|
+
approveTransfer(transactionId: string, opts?: CallOptions): Promise<ApproveResult>;
|
|
22
22
|
/** Recipient self-claims a SEND with their passkey. */
|
|
23
|
-
confirmReceiptSend(transactionId: string): Promise<ConfirmReceiptResult>;
|
|
23
|
+
confirmReceiptSend(transactionId: string, opts?: CallOptions): Promise<ConfirmReceiptResult>;
|
|
24
24
|
/** Recipient self-claims a TRANSFER with their passkey. */
|
|
25
|
-
confirmReceiptTransfer(transactionId: string): Promise<ConfirmReceiptResult>;
|
|
25
|
+
confirmReceiptTransfer(transactionId: string, opts?: CallOptions): Promise<ConfirmReceiptResult>;
|
|
26
26
|
/**
|
|
27
27
|
* Interchangeable methods (§4.6): prove an *already-enrolled* method (e.g. the
|
|
28
28
|
* INVO app device key) to authorize adding a new partner passkey. The returned
|
|
@@ -31,7 +31,7 @@ declare class InvoClient {
|
|
|
31
31
|
*
|
|
32
32
|
* begin -> navigator.credentials.get() -> complete with { link_id, webauthn_assertion }.
|
|
33
33
|
*/
|
|
34
|
-
linkDevice(linkId: string): Promise<LinkDeviceResult>;
|
|
34
|
+
linkDevice(linkId: string, opts?: CallOptions): Promise<LinkDeviceResult>;
|
|
35
35
|
/** POST with the current player token. Token-expiry retry is handled one level
|
|
36
36
|
* up by withTokenRetry (which re-runs the whole ceremony, not a single call). */
|
|
37
37
|
private post;
|
|
@@ -49,4 +49,4 @@ declare class InvoClient {
|
|
|
49
49
|
private confirmReceipt;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export { ApproveResult, ClientConfig, ConfirmReceiptResult, InvoClient, LinkDeviceResult };
|
|
52
|
+
export { ApproveResult, CallOptions, ClientConfig, ConfirmReceiptResult, InvoClient, LinkDeviceResult };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ClientConfig, A as ApproveResult,
|
|
2
|
-
export { I as InvoError,
|
|
1
|
+
import { C as ClientConfig, a as CallOptions, A as ApproveResult, b as ConfirmReceiptResult, L as LinkDeviceResult } from './types-CBMLNwbe.js';
|
|
2
|
+
export { I as InvoError, c as InvoErrorInfo, d as InvoHooks, e as InvoRequestInfo, f as InvoResponseInfo, R as Rail, V as VerificationMethod } from './types-CBMLNwbe.js';
|
|
3
3
|
|
|
4
4
|
declare class InvoClient {
|
|
5
5
|
private readonly http;
|
|
@@ -10,19 +10,19 @@ declare class InvoClient {
|
|
|
10
10
|
constructor(config: ClientConfig);
|
|
11
11
|
private assertWebAuthn;
|
|
12
12
|
/** Enroll a passkey for the token's identity (register/begin -> create() -> register/complete). */
|
|
13
|
-
enrollPasskey(): Promise<{
|
|
13
|
+
enrollPasskey(opts?: CallOptions): Promise<{
|
|
14
14
|
status: string;
|
|
15
15
|
device: unknown;
|
|
16
16
|
raw: Record<string, unknown>;
|
|
17
17
|
}>;
|
|
18
18
|
/** Approve a SEND with the player's passkey. */
|
|
19
|
-
approveSend(transactionId: string): Promise<ApproveResult>;
|
|
19
|
+
approveSend(transactionId: string, opts?: CallOptions): Promise<ApproveResult>;
|
|
20
20
|
/** Approve a TRANSFER with the player's passkey (returns the sender's claim code). */
|
|
21
|
-
approveTransfer(transactionId: string): Promise<ApproveResult>;
|
|
21
|
+
approveTransfer(transactionId: string, opts?: CallOptions): Promise<ApproveResult>;
|
|
22
22
|
/** Recipient self-claims a SEND with their passkey. */
|
|
23
|
-
confirmReceiptSend(transactionId: string): Promise<ConfirmReceiptResult>;
|
|
23
|
+
confirmReceiptSend(transactionId: string, opts?: CallOptions): Promise<ConfirmReceiptResult>;
|
|
24
24
|
/** Recipient self-claims a TRANSFER with their passkey. */
|
|
25
|
-
confirmReceiptTransfer(transactionId: string): Promise<ConfirmReceiptResult>;
|
|
25
|
+
confirmReceiptTransfer(transactionId: string, opts?: CallOptions): Promise<ConfirmReceiptResult>;
|
|
26
26
|
/**
|
|
27
27
|
* Interchangeable methods (§4.6): prove an *already-enrolled* method (e.g. the
|
|
28
28
|
* INVO app device key) to authorize adding a new partner passkey. The returned
|
|
@@ -31,7 +31,7 @@ declare class InvoClient {
|
|
|
31
31
|
*
|
|
32
32
|
* begin -> navigator.credentials.get() -> complete with { link_id, webauthn_assertion }.
|
|
33
33
|
*/
|
|
34
|
-
linkDevice(linkId: string): Promise<LinkDeviceResult>;
|
|
34
|
+
linkDevice(linkId: string, opts?: CallOptions): Promise<LinkDeviceResult>;
|
|
35
35
|
/** POST with the current player token. Token-expiry retry is handled one level
|
|
36
36
|
* up by withTokenRetry (which re-runs the whole ceremony, not a single call). */
|
|
37
37
|
private post;
|
|
@@ -49,4 +49,4 @@ declare class InvoClient {
|
|
|
49
49
|
private confirmReceipt;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export { ApproveResult, ClientConfig, ConfirmReceiptResult, InvoClient, LinkDeviceResult };
|
|
52
|
+
export { ApproveResult, CallOptions, ClientConfig, ConfirmReceiptResult, InvoClient, LinkDeviceResult };
|