@invonetwork/web-sdk 0.4.1 → 0.4.2

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 CHANGED
@@ -4,6 +4,25 @@ All notable changes to `@invonetwork/web-sdk` are documented here. This project
4
4
  [Semantic Versioning](https://semver.org/). Releases are managed with
5
5
  [changesets](https://github.com/changesets/changesets).
6
6
 
7
+ ## [0.4.2] — 2026-06-30
8
+
9
+ Fixes from an independent line-by-line audit against the live backend.
10
+
11
+ - **`linkDevice` now works.** `device/link/webauthn/begin` returns a wrapped
12
+ `{ link_id, options }` body (challenge nested under `options`) and binds the
13
+ challenge to a server-generated `link_id`. The client now unwraps `options` and
14
+ echoes the server's `link_id` to `/complete` (it previously threw on an undefined
15
+ challenge and sent the wrong id).
16
+ - **`InvoError.code` is now populated for the direct purchase rail + guardian flows.**
17
+ `errorFromResponse` reads `error_code` (SecureErrorHandler + `/purchase-currency`)
18
+ in addition to `code`, and promotes known no-code tokens (`flow_paused`,
19
+ `spending_limit_exceeded`, `receiver_not_enrolled_use_claim_code`) to `.code`. Fixes
20
+ `isDuplicateRequest` on the direct rail.
21
+ - **Guardian/minor routing.** The 202 guardian body also carries
22
+ `verification_method:"sms"`; the SDK now suppresses it (reports
23
+ `verificationMethod: undefined`) so a guardian-pending minor isn't routed into the
24
+ SMS-PIN UI. README example reordered to branch on `guardianApproval` first.
25
+
7
26
  ## [0.4.1] — 2026-06-30
8
27
 
9
28
  Docs only — replaced the README's internal-leaking "Deployment prerequisites"
package/README.md CHANGED
@@ -260,10 +260,11 @@ const t = await server.initiateTransfer({
260
260
  });
261
261
  // (initiateSend uses sender*/receiver* + receivingGameId instead)
262
262
 
263
+ // Check guardianApproval FIRST — the guardian path takes precedence.
263
264
  switch (true) {
265
+ case !!t.guardianApproval: // minor/guardian path (HTTP 202) → pending approval, no PIN UI
264
266
  case t.verificationMethod === "in_app": // sender is passkey-enrolled → approve in the browser
265
267
  case t.verificationMethod === "sms": // not enrolled, a PIN was sent → show a PIN-entry fallback
266
- case !!t.guardianApproval: // minor/guardian path (HTTP 202) → do NOT show the PIN UI
267
268
  }
268
269
 
269
270
  // 2. BROWSER — the sender approves with their passkey (give them t.transactionId + the player token)
@@ -280,7 +281,7 @@ try {
280
281
  }
281
282
  ```
282
283
 
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.
284
+ - **`verificationMethod`** (from initiate): `"in_app"` → passkey approve; `"sms"` → un-enrolled, PIN sent; `undefined` + `guardianApproval` → minor/guardian (HTTP 202), do **not** show the PIN UI. On the guardian path the SDK reports `verificationMethod: undefined` (even though the raw 202 body also carries `"sms"`) so `guardianApproval` wins — but branch on it first to be safe.
284
285
  - **Claim codes** are returned only by `approveTransfer`; they're the out-of-band fallback when the recipient isn't enrolled.
285
286
  - **`err.isReceiverNotEnrolled`** on `confirmReceipt*` is the explicit signal to switch to claim-code entry.
286
287
  - Transfer self-claim may be disabled for your tenant; if it is, surface the claim-code path instead.
@@ -50,8 +50,12 @@ function errorFromResponse(status, body, requestId) {
50
50
  if (body && typeof body === "object") {
51
51
  const b = body;
52
52
  if (typeof b["code"] === "string") code = b["code"];
53
+ else if (typeof b["error_code"] === "string") code = b["error_code"];
53
54
  if (typeof b["error"] === "string") message = b["error"];
54
55
  else if (typeof b["message"] === "string") message = b["message"];
56
+ if (!code && typeof b["error"] === "string" && /^(flow_paused|spending_limit_exceeded|receiver_not_enrolled_use_claim_code)$/.test(b["error"])) {
57
+ code = b["error"];
58
+ }
55
59
  } else if (typeof body === "string" && body.trim()) {
56
60
  message = body;
57
61
  }
@@ -245,5 +249,5 @@ function retryAfterMs(parsed, headers) {
245
249
  }
246
250
 
247
251
  export { Http, InvoError, assertSecureBaseUrl };
248
- //# sourceMappingURL=chunk-EEWOAUXO.js.map
249
- //# sourceMappingURL=chunk-EEWOAUXO.js.map
252
+ //# sourceMappingURL=chunk-JOVATUDY.js.map
253
+ //# sourceMappingURL=chunk-JOVATUDY.js.map
package/dist/index.cjs CHANGED
@@ -52,8 +52,12 @@ function errorFromResponse(status, body, requestId) {
52
52
  if (body && typeof body === "object") {
53
53
  const b = body;
54
54
  if (typeof b["code"] === "string") code = b["code"];
55
+ else if (typeof b["error_code"] === "string") code = b["error_code"];
55
56
  if (typeof b["error"] === "string") message = b["error"];
56
57
  else if (typeof b["message"] === "string") message = b["message"];
58
+ if (!code && typeof b["error"] === "string" && /^(flow_paused|spending_limit_exceeded|receiver_not_enrolled_use_claim_code)$/.test(b["error"])) {
59
+ code = b["error"];
60
+ }
57
61
  } else if (typeof body === "string" && body.trim()) {
58
62
  message = body;
59
63
  }
@@ -399,16 +403,22 @@ var InvoClient = class {
399
403
  */
400
404
  async linkDevice(linkId, opts) {
401
405
  if (!linkId) throw new Error("linkDevice requires a `linkId`.");
406
+ this.assertWebAuthn();
402
407
  const signal = opts?.signal;
403
408
  return this.withTokenRetry(async () => {
404
- const assertion = await this.runAssertion(
409
+ const begin = await this.post(
405
410
  "/api/sdk/device/link/webauthn/begin",
406
411
  { link_id: linkId },
407
412
  signal
408
413
  );
414
+ const cred = await navigator.credentials.get({
415
+ publicKey: toRequestOptions(begin.options),
416
+ signal
417
+ });
418
+ if (!cred) throw new Error("Passkey assertion was cancelled or returned no credential.");
409
419
  const raw = await this.post(
410
420
  "/api/sdk/device/link/webauthn/complete",
411
- { link_id: linkId, webauthn_assertion: assertion },
421
+ { link_id: begin.link_id, webauthn_assertion: assertionToJSON(cred) },
412
422
  signal
413
423
  );
414
424
  return { status: String(raw["status"] ?? ""), raw };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { assertSecureBaseUrl, Http, InvoError } from './chunk-EEWOAUXO.js';
2
- export { InvoError } from './chunk-EEWOAUXO.js';
1
+ import { assertSecureBaseUrl, Http, InvoError } from './chunk-JOVATUDY.js';
2
+ export { InvoError } from './chunk-JOVATUDY.js';
3
3
 
4
4
  // src/shared/webauthn.ts
5
5
  function b64urlToBuffer(value) {
@@ -154,16 +154,22 @@ var InvoClient = class {
154
154
  */
155
155
  async linkDevice(linkId, opts) {
156
156
  if (!linkId) throw new Error("linkDevice requires a `linkId`.");
157
+ this.assertWebAuthn();
157
158
  const signal = opts?.signal;
158
159
  return this.withTokenRetry(async () => {
159
- const assertion = await this.runAssertion(
160
+ const begin = await this.post(
160
161
  "/api/sdk/device/link/webauthn/begin",
161
162
  { link_id: linkId },
162
163
  signal
163
164
  );
165
+ const cred = await navigator.credentials.get({
166
+ publicKey: toRequestOptions(begin.options),
167
+ signal
168
+ });
169
+ if (!cred) throw new Error("Passkey assertion was cancelled or returned no credential.");
164
170
  const raw = await this.post(
165
171
  "/api/sdk/device/link/webauthn/complete",
166
- { link_id: linkId, webauthn_assertion: assertion },
172
+ { link_id: begin.link_id, webauthn_assertion: assertionToJSON(cred) },
167
173
  signal
168
174
  );
169
175
  return { status: String(raw["status"] ?? ""), raw };
package/dist/server.cjs CHANGED
@@ -54,8 +54,12 @@ function errorFromResponse(status, body, requestId) {
54
54
  if (body && typeof body === "object") {
55
55
  const b = body;
56
56
  if (typeof b["code"] === "string") code = b["code"];
57
+ else if (typeof b["error_code"] === "string") code = b["error_code"];
57
58
  if (typeof b["error"] === "string") message = b["error"];
58
59
  else if (typeof b["message"] === "string") message = b["message"];
60
+ if (!code && typeof b["error"] === "string" && /^(flow_paused|spending_limit_exceeded|receiver_not_enrolled_use_claim_code)$/.test(b["error"])) {
61
+ code = b["error"];
62
+ }
59
63
  } else if (typeof body === "string" && body.trim()) {
60
64
  message = body;
61
65
  }
@@ -869,7 +873,7 @@ var InvoServer = class {
869
873
  const guardian = raw["guardian_approval"];
870
874
  return {
871
875
  transactionId: String(raw["transaction_id"] ?? ""),
872
- verificationMethod: vm === "in_app" || vm === "sms" ? vm : void 0,
876
+ verificationMethod: guardian ? void 0 : vm === "in_app" || vm === "sms" ? vm : void 0,
873
877
  guardianApproval: guardian ?? void 0,
874
878
  raw
875
879
  };
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
- import { InvoError, assertSecureBaseUrl, Http } from './chunk-EEWOAUXO.js';
2
- export { InvoError } from './chunk-EEWOAUXO.js';
1
+ import { InvoError, assertSecureBaseUrl, Http } from './chunk-JOVATUDY.js';
2
+ export { InvoError } from './chunk-JOVATUDY.js';
3
3
  import { createHmac } from 'crypto';
4
4
 
5
5
  var DEFAULT_TOLERANCE_SEC = 300;
@@ -624,7 +624,7 @@ var InvoServer = class {
624
624
  const guardian = raw["guardian_approval"];
625
625
  return {
626
626
  transactionId: String(raw["transaction_id"] ?? ""),
627
- verificationMethod: vm === "in_app" || vm === "sms" ? vm : void 0,
627
+ verificationMethod: guardian ? void 0 : vm === "in_app" || vm === "sms" ? vm : void 0,
628
628
  guardianApproval: guardian ?? void 0,
629
629
  raw
630
630
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invonetwork/web-sdk",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "INVO Web SDK — currency purchase + passkey (WebAuthn) verification for partner web platforms.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,