@v3finance/core-sdk 0.2.0 → 0.2.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +5 -2
  3. package/dist/cjs/core/gated.d.ts +9 -3
  4. package/dist/cjs/core/gated.d.ts.map +1 -1
  5. package/dist/cjs/core/gated.js +9 -3
  6. package/dist/cjs/core/gated.js.map +1 -1
  7. package/dist/cjs/core/http.d.ts.map +1 -1
  8. package/dist/cjs/core/http.js +17 -3
  9. package/dist/cjs/core/http.js.map +1 -1
  10. package/dist/cjs/generated/operations.js +1 -1
  11. package/dist/cjs/generated/types.d.ts +17 -7
  12. package/dist/cjs/generated/types.d.ts.map +1 -1
  13. package/dist/cjs/resources/addresses.d.ts +4 -3
  14. package/dist/cjs/resources/addresses.d.ts.map +1 -1
  15. package/dist/cjs/resources/addresses.js +4 -3
  16. package/dist/cjs/resources/addresses.js.map +1 -1
  17. package/dist/cjs/resources/transfers.d.ts +18 -2
  18. package/dist/cjs/resources/transfers.d.ts.map +1 -1
  19. package/dist/cjs/resources/transfers.js +18 -2
  20. package/dist/cjs/resources/transfers.js.map +1 -1
  21. package/dist/cjs/resources/types.d.ts +0 -4
  22. package/dist/cjs/resources/types.d.ts.map +1 -1
  23. package/dist/cjs/version.d.ts +1 -1
  24. package/dist/cjs/version.js +1 -1
  25. package/dist/esm/core/gated.d.ts +9 -3
  26. package/dist/esm/core/gated.d.ts.map +1 -1
  27. package/dist/esm/core/gated.js +9 -3
  28. package/dist/esm/core/gated.js.map +1 -1
  29. package/dist/esm/core/http.d.ts.map +1 -1
  30. package/dist/esm/core/http.js +17 -3
  31. package/dist/esm/core/http.js.map +1 -1
  32. package/dist/esm/generated/operations.js +1 -1
  33. package/dist/esm/generated/types.d.ts +17 -7
  34. package/dist/esm/generated/types.d.ts.map +1 -1
  35. package/dist/esm/resources/addresses.d.ts +4 -3
  36. package/dist/esm/resources/addresses.d.ts.map +1 -1
  37. package/dist/esm/resources/addresses.js +4 -3
  38. package/dist/esm/resources/addresses.js.map +1 -1
  39. package/dist/esm/resources/transfers.d.ts +18 -2
  40. package/dist/esm/resources/transfers.d.ts.map +1 -1
  41. package/dist/esm/resources/transfers.js +18 -2
  42. package/dist/esm/resources/transfers.js.map +1 -1
  43. package/dist/esm/resources/types.d.ts +0 -4
  44. package/dist/esm/resources/types.d.ts.map +1 -1
  45. package/dist/esm/version.d.ts +1 -1
  46. package/dist/esm/version.js +1 -1
  47. package/docs/concepts.md +6 -4
  48. package/docs/errors.md +25 -5
  49. package/package.json +1 -1
  50. package/src/core/gated.ts +9 -3
  51. package/src/core/http.ts +17 -3
  52. package/src/generated/operations.ts +1 -1
  53. package/src/generated/types.ts +18 -7
  54. package/src/resources/addresses.ts +4 -3
  55. package/src/resources/transfers.ts +18 -2
  56. package/src/resources/types.ts +0 -4
  57. package/src/version.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"transfers.js","sourceRoot":"","sources":["../../../src/resources/transfers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAmB,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAE,OAAO,EAAc,MAAM,kBAAkB,CAAA;AAgBtD;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,iBAAiB;IACnB,KAAK,CAAY;IACjB,KAAK,CAAQ;IAEtB,YAAY,IAAgB,EAAE,SAAiB;QAC7C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC,KAAK,GAAG,kBAAkB,GAAG,CAAC,SAAS,CAAC,YAAY,CAAA;IAC3D,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,IAAI,CACR,KAAyB,EACzB,OAAO,GAAiD,EAAE;QAE1D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAA0B,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE;YACnF,IAAI,EAAE,KAAK;YACX,GAAG,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC;YACvF,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;YACnE,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;SAC7E,CAAC,CAAA;QACF,OAAO,OAAO,CAAmD,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE;YACrF,WAAW,EAAE,IAAI;SAClB,CAAC,CAAA;IACJ,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,UAAkB;QACpB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAoB,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IACzF,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,CAAC,UAAkB,EAAE,KAAgB;QACvC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CACvB,MAAM,EACN,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,EAC5C,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAA;IACH,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,UAAkB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAe,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;IAC5F,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,SAAS,CACP,UAAkB,EAClB,KAAK,GAA0B,EAAE;QAEjC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CACvB,MAAM,EACN,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,EAC5C,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAA;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"transfers.js","sourceRoot":"","sources":["../../../src/resources/transfers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAmB,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAE,OAAO,EAAc,MAAM,kBAAkB,CAAA;AAgBtD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,iBAAiB;IACnB,KAAK,CAAY;IACjB,KAAK,CAAQ;IAEtB,YAAY,IAAgB,EAAE,SAAiB;QAC7C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC,KAAK,GAAG,kBAAkB,GAAG,CAAC,SAAS,CAAC,YAAY,CAAA;IAC3D,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,CACR,KAAyB,EACzB,OAAO,GAAiD,EAAE;QAE1D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAA0B,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE;YACnF,IAAI,EAAE,KAAK;YACX,GAAG,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC;YACvF,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;YACnE,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;SAC7E,CAAC,CAAA;QACF,OAAO,OAAO,CAAmD,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE;YACrF,WAAW,EAAE,IAAI;SAClB,CAAC,CAAA;IACJ,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,UAAkB;QACpB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAoB,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IACzF,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,CAAC,UAAkB,EAAE,KAAgB;QACvC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CACvB,MAAM,EACN,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,EAC5C,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAA;IACH,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,UAAkB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAe,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;IAC5F,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,SAAS,CACP,UAAkB,EAClB,KAAK,GAA0B,EAAE;QAEjC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CACvB,MAAM,EACN,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,EAC5C,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAA;IACH,CAAC;CACF"}
@@ -34,10 +34,6 @@ import type { AddressDetail as GeneratedAddressDetail, AddressListItem, CreateTr
34
34
  * the two must not be merged, because "no account" and "empty account"
35
35
  * deserve different wording. Amounts are raw smallest units; put them through
36
36
  * the helpers in `units.ts` rather than `Number`.
37
- *
38
- * One field is thinner than the type admits: `sharing` is optional because
39
- * `PATCH /addresses/{id}` reuses this schema WITHOUT it (the document says so
40
- * itself, on the property). Listings and the detail endpoint do send it.
41
37
  */
42
38
  export type Address = AddressListItem;
43
39
  /** A single address with its sharing and access details. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/resources/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,aAAa,IAAI,sBAAsB,EACvC,eAAe,EACf,uBAAuB,EACvB,WAAW,IAAI,oBAAoB,EACnC,UAAU,EACX,MAAM,uBAAuB,CAAA;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,MAAM,OAAO,GAAG,eAAe,CAAA;AAErC,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,sBAAsB,CAAA;AAElD,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG,uBAAuB,CAAC,aAAa,CAAC,CAAA;AAEhE,+CAA+C;AAC/C,MAAM,MAAM,WAAW,GAAG,oBAAoB,CAAA;AAE9C,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAAG,UAAU,CAAA;AAEnC;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GACzB,mBAAmB,GACnB,SAAS,GACT,WAAW,GACX,WAAW,GACX,QAAQ,GACR,WAAW,GACX,UAAU,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/resources/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,aAAa,IAAI,sBAAsB,EACvC,eAAe,EACf,uBAAuB,EACvB,WAAW,IAAI,oBAAoB,EACnC,UAAU,EACX,MAAM,uBAAuB,CAAA;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,OAAO,GAAG,eAAe,CAAA;AAErC,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,sBAAsB,CAAA;AAElD,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG,uBAAuB,CAAC,aAAa,CAAC,CAAA;AAEhE,+CAA+C;AAC/C,MAAM,MAAM,WAAW,GAAG,oBAAoB,CAAA;AAE9C,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAAG,UAAU,CAAA;AAEnC;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GACzB,mBAAmB,GACnB,SAAS,GACT,WAAW,GACX,WAAW,GACX,QAAQ,GACR,WAAW,GACX,UAAU,CAAA"}
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent in the `user-agent` header. */
2
- export declare const SDK_VERSION = "0.2.0";
2
+ export declare const SDK_VERSION = "0.2.1";
3
3
  /** Default API base URL. */
4
4
  export declare const DEFAULT_BASE_URL = "https://policy-engine-rest.ezig.workers.dev";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent in the `user-agent` header. */
2
- export const SDK_VERSION = '0.2.0';
2
+ export const SDK_VERSION = '0.2.1';
3
3
  /** Default API base URL. */
4
4
  export const DEFAULT_BASE_URL = 'https://policy-engine-rest.ezig.workers.dev';
5
5
  //# sourceMappingURL=version.js.map
package/docs/concepts.md CHANGED
@@ -57,7 +57,7 @@ The API expresses this with the HTTP status, because **the response bodies are i
57
57
  | --- | --- |
58
58
  | 201 | applied — it happened |
59
59
  | 202 | pending — funds are RESERVED, nothing has moved, there is a request to sign |
60
- | 200 | replayed — the same idempotency key; it already happened earlier |
60
+ | 200 | replayed — the same idempotency key; the transfer already existed, in whatever state it is now |
61
61
 
62
62
  The SDK turns that into `Gated`:
63
63
 
@@ -65,13 +65,15 @@ The SDK turns that into `Gated`:
65
65
  switch (result.outcome) {
66
66
  case 'applied': return toast('Transfer sent')
67
67
  case 'pending': return toast('Awaiting signatures — funds are reserved')
68
- case 'replayed': return toast('This transfer was already sent')
68
+ case 'replayed': return toast(describeStatus(result.data.transaction.status))
69
69
  }
70
70
  ```
71
71
 
72
72
  `replayed` is kept apart from `applied` on purpose: a replay means the previous attempt reached the
73
- server and only the answer was lost, so the person should be told "already sent" rather than being
74
- told "sent" a second time.
73
+ server and only the answer was lost — not that it succeeded. The body is that transfer as it stands
74
+ now, so `replayed` must not be shown as "sent": `data.transaction.status` says whether it went out
75
+ (`confirmed`, or `submitted`), is waiting for signatures (`awaiting_approval`), is still executing
76
+ (`pending`) or did not go out (`failed`, `rejected`, `cancelled`).
75
77
 
76
78
  **Only `transfers.send` can return it.** `idempotencyKey` exists on exactly one request body in the
77
79
  whole API — the transfer — so it is the only place where a 200 means "this already happened". On
package/docs/errors.md CHANGED
@@ -181,6 +181,16 @@ Two sets of uppercase strings travel inside **successful** bodies and will misle
181
181
 
182
182
  Address creation that requires signatures is **not** an error: the server answers **202** with `{status:"awaiting_approval", approvalRequestId, approvalThreshold}`. Nothing is thrown. (`POLICY_REQUIRES_APPROVAL` exists in the server's type but is never sent — see [Declared but never returned](#declared-but-never-returned).)
183
183
 
184
+ Creating an address **into a folder** (`groupId`) can succeed without the folder, and that is not an error either. The right to the folder is checked before anything is created — a refusal there is `GROUP_CREATE_FORBIDDEN` above — but the address is put into the folder only after the signing service and monitoring, and the right is checked again at that moment. If that fails, the address still exists and the answer is still **201**, with `warnings: [{code, message}]` in the body:
185
+
186
+ | Code | Meaning |
187
+ |---|---|
188
+ | `FOLDER_NOT_FOUND` | The folder was deleted. |
189
+ | `FOLDER_FORBIDDEN` | The address's creator may no longer add addresses to the folder. |
190
+ | `FOLDER_WRITE_FAILED` | Writing the membership failed. |
191
+
192
+ The same three arrive in `applyWarnings` of a **200** vote that applied an approved `create_address`: there the folder is checked when the request is applied, not when it was filed, and the request's history gets a `folder_skipped` event. Nothing is thrown in either case, so read the field. The address is fine; do not show it inside the folder.
193
+
184
194
  ## Grants and sharing
185
195
 
186
196
  Address shares, folder shares, member-visibility grants. All grant `POST`s are idempotent: repeating a pair changes the rights, and a revoked grant is reactivated. There is no `GRANT_EXISTS` anywhere.
@@ -385,7 +395,17 @@ A risk threshold in a policy is expressed as a **level**, not a numeric score, a
385
395
 
386
396
  `/api/auth/*` is the auth library's own namespace. It answers with a flat `{code, message}` that the API's envelope does not rewrite; `CoreError` reads it, so `code` is populated, but the values come from that library and are not part of this API's contract. Do not branch on a fixed list there — branch on `status`, and show `message`.
387
397
 
388
- Rate limiting is not implemented in the API itself. A 429 therefore comes from the platform in front of it, usually without a JSON body, which means `code: "RATE_LIMITED"` and no `details`. Back off and retry.
398
+ One of the library's codes is named here anyway, because the API itself refers to it: `FAILED_TO_GET_SESSION`. The library raises it when it fails to read or renew a session — a sign-out or a revocation deleting the session in the middle of a renewal is one way to get there. The API retries the session lookup once on it; on the API's own paths a failure that survives the retry arrives as `INTERNAL_ERROR`, and only `/api/auth/*` can hand you `FAILED_TO_GET_SESSION` itself, in the library's flat form. Treat it by `status`, like the rest of that namespace.
399
+
400
+ Three refusals the API puts **in front of** the library are its own: they arrive in the API's envelope, and their codes can be branched on.
401
+
402
+ | Code | Status | When | What to do |
403
+ |---|---|---|---|
404
+ | `ADMIN_CANNOT_APPROVE_WEB_LOGIN` | 403 | `POST /api/auth/device/approve` by a platform administrator. Approving a web login gives the holder of the code a new session of the approving user, and a platform administrator's session is not handed to the web version at all. | Terminal. Approve from an account that is not a platform administrator. |
405
+ | `WEB_SESSION_CANNOT_APPROVE` | 403 | The same approval sent with a session that was itself obtained through a web login. Only a session signed in another way may approve, so one leaked web token cannot keep minting new ones. | Approve from the mobile app. |
406
+ | `WEB_SESSION_CANNOT_MANAGE_SESSIONS` | 403 | A web-login session called `list-sessions`, `revoke-session`, `revoke-sessions` or `revoke-other-sessions`. Signing out (`sign-out`) is not affected. | Hide session management in a web client; it is done from the mobile app. |
407
+
408
+ Rate limiting outside `/api/auth/*` is not implemented in the API itself. A 429 there comes from the platform in front of it, usually without a JSON body, which means `code: "RATE_LIMITED"` and no `details`. Inside `/api/auth/*` a 429 can also come from the library's own limiter, as a bare `{message}`; and requesting a web-login code (`/api/auth/device/code`) is limited by the API itself, with a 429 in the API's envelope and `code: "RATE_LIMITED"`. Whichever answered, back off — for `X-Retry-After` seconds when the header is there — and retry.
389
409
 
390
410
  ## Easily confused
391
411
 
@@ -438,15 +458,15 @@ try {
438
458
  } else throw e
439
459
  }
440
460
 
441
- if (result.outcome === 'replayed') showAlreadySent(result.data)
442
- else if (result.outcome === 'pending') showAwaitingApproval(result.data)
443
- else showSent(result.data)
461
+ if (result.outcome === 'pending') showAwaitingApproval(result.data)
462
+ else if (result.outcome === 'applied') showSent(result.data)
463
+ else showByStatus(result.data.transaction.status) // replayed: NOT necessarily sent — see below
444
464
  ```
445
465
 
446
466
  Rules that follow from how the server handles the key:
447
467
 
448
468
  - **The key is reserved before any external call.** A replay is therefore recognised even if the first attempt died mid-broadcast. The server answers **200** and does not broadcast again; the SDK labels that `outcome: 'replayed'`.
449
- - **Tell "replayed" apart from "sent".** A replay means the earlier attempt did reach the server and only the answer was lost. Say "already sent", not "sent" a second time.
469
+ - **`replayed` does not mean "sent".** It means "a transfer with this key already exists": the earlier attempt did reach the server and only its answer was lost. `data` is that transfer as it stands **now**, and what happened to it is in `data.transaction.status` — `confirmed` (or `submitted`): it left; `awaiting_approval`: it is waiting for signatures, funds reserved; `pending`: it is still executing; `failed`, `rejected` or `cancelled`: it did not leave. Tell the person that, not "sent".
450
470
  - **A new key after a `failed` or `rejected` transfer.** The old key is bound to that row, and reusing it returns the dead transfer instead of sending a new one.
451
471
  - **No key sent, and a timeout?** Do **not** retry. Fetch the transfer list or the operation history first and establish whether the transfer exists. Make the SDK's own callers always pass a key so this situation cannot arise.
452
472
  - **`network` and `aborted` carry the same uncertainty as `timeout`** on money paths. The request may have been received and executed; only the response was lost. Treat all three identically: same key, or verify first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v3finance/core-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The official TypeScript SDK for the V3 Finance Core API — custodial addresses, transfers under policy control, approvals, shared access, labelling and reports",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/core/gated.ts CHANGED
@@ -15,8 +15,10 @@
15
15
  * frozen.
16
16
  *
17
17
  * `replayed` is kept separate from `applied` on purpose. A replay means the
18
- * previous attempt DID reach the server and only the answer was lost, so the
19
- * person should be told "already sent", not "sent" a second time.
18
+ * previous attempt DID reach the server and only the answer was lost — not
19
+ * that it succeeded. The body is that operation as it stands now: a replayed
20
+ * transfer may have gone out, be waiting for signatures, be still executing
21
+ * or have failed, and only its own status says which.
20
22
  */
21
23
 
22
24
  export type GatedOutcome = 'applied' | 'pending' | 'replayed'
@@ -74,7 +76,11 @@ export function toGated<TApplied, TPending>(
74
76
  }
75
77
  }
76
78
 
77
- /** Narrowing: the operation ran, now or earlier, and money moved. */
79
+ /**
80
+ * Narrowing: `applied` or `replayed`. For `applied` the operation ran now.
81
+ * For `replayed` it only existed already — whether money moved is in the
82
+ * body's own status, not in this check.
83
+ */
78
84
  export function isApplied<A, P>(
79
85
  r: Gated<A, P>,
80
86
  ): r is Extract<Gated<A, P>, { outcome: 'applied' | 'replayed' }> {
package/src/core/http.ts CHANGED
@@ -157,13 +157,27 @@ export class HttpClient {
157
157
  constructor(options: ClientOptions & { baseUrl: string }) {
158
158
  this.baseUrl = options.baseUrl.replace(/\/+$/, '')
159
159
  this.#options = options
160
- const f = options.fetch ?? globalThis.fetch
161
- if (typeof f !== 'function') {
160
+ const custom = options.fetch
161
+ if (typeof (custom ?? globalThis.fetch) !== 'function') {
162
162
  throw new Error(
163
163
  '@v3finance/core-sdk: fetch is unavailable in this runtime — pass one to the client options.',
164
164
  )
165
165
  }
166
- this.#fetch = f
166
+ // NEVER store the function and call it as `this.#fetch(…)`. A private
167
+ // field call passes the client as the receiver, and a browser's `fetch`
168
+ // checks its receiver: anything but the window is
169
+ // `TypeError: Illegal invocation`, thrown before a single byte leaves.
170
+ // Node and React Native do not check, which is how every request from a
171
+ // web page failed while the whole test suite passed — and since a
172
+ // `TypeError` out of `fetch` is what a dropped connection looks like, the
173
+ // failure surfaced as a "network error" on a perfectly good network.
174
+ //
175
+ // The wrapper calls with no receiver at all. Without a custom fetch the
176
+ // global one is looked up per call rather than captured here, so whatever
177
+ // patches it later (instrumentation, a mock server) is still honoured.
178
+ this.#fetch = custom
179
+ ? (input, init) => custom(input, init)
180
+ : (input, init) => globalThis.fetch(input, init)
167
181
  this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
168
182
  }
169
183
 
@@ -9,7 +9,7 @@
9
9
  // the API does not slip in unclassified.
10
10
  //
11
11
  // Operations: 171 · admin: 40 · both: 1 · member: 127 · service: 3
12
- // Spec sha256: a03bd2e06186734bd8816c3d764dd90a48c1c31d7edda2743a3ae29550601cd1
12
+ // Spec sha256: 017bae30075b232ab4efdcfb7785335020be3060b46e3569b1cd887fcf037597
13
13
 
14
14
  export type Audience = 'member' | 'admin' | 'service' | 'both' | 'unknown'
15
15
 
@@ -250,8 +250,8 @@ export interface AddressDetail {
250
250
  tokenId: string | null
251
251
  /** format: date-time */
252
252
  createdAt: string
253
- /** Сводка совместного доступа. Приходит в списке и детали адреса; PATCH /addresses/{id} переиспользует эту же схему и sharing НЕ возвращает */
254
- sharing?: SharingSummary
253
+ /** Сводка совместного доступа. Приходит всегда: в списке, ответе PATCH и детали адреса */
254
+ sharing: SharingSummary
255
255
  /** Код токена адреса (из segmentsJson + asset route) */
256
256
  asset: string | null
257
257
  assetName: string | null
@@ -491,8 +491,8 @@ export interface AddressListItem {
491
491
  assetSymbol: string | null
492
492
  /** Баланс в decimal (по token.decimals). NULL — ledger-счёта ещё нет */
493
493
  balanceDecimal: string | null
494
- /** Сводка совместного доступа. Приходит в списке и детали адреса; PATCH /addresses/{id} переиспользует эту же схему и sharing НЕ возвращает */
495
- sharing?: SharingSummary
494
+ /** Сводка совместного доступа. Приходит всегда: в списке, ответе PATCH и детали адреса */
495
+ sharing: SharingSummary
496
496
  /** Владелец адреса (создатель) с UI-метаданными */
497
497
  owner: MemberCard | null
498
498
  access: AddressAccess
@@ -791,11 +791,11 @@ export interface ApprovalRequestDetail {
791
791
 
792
792
  export interface ApprovalRequestEvent {
793
793
  id: string
794
- /** created|voted|approved|rejected|cancelled|applied|apply_failed|stale */
794
+ /** created|voted|approved|rejected|cancelled|applied|apply_failed|stale|folder_skipped. folder_skipped идёт ПОСЛЕ applied: адрес по заявке create_address создан, но в папку, выбранную при подаче, не положен (папку удалили или у инициатора отозвали право) — причина в comment */
795
795
  type: string
796
796
  actorId: string | null
797
797
  decision: string | null
798
- /** Пояснение ПОДПИСАНТА. У события applied всегда null — id созданного объекта лежит в resultId, чтобы клиент не напечатал его как комментарий */
798
+ /** У voted — пояснение ПОДПИСАНТА; у stale, apply_failed и folder_skipped — системное пояснение для человека. У события applied всегда null — id созданного объекта лежит в resultId, чтобы клиент не напечатал его как комментарий */
799
799
  comment: string | null
800
800
  /** Только у applied: id созданного/изменённого объекта */
801
801
  resultId: string | null
@@ -1328,7 +1328,7 @@ export interface CreateAddressBody {
1328
1328
  * length: 1..16
1329
1329
  */
1330
1330
  emoji?: string
1331
- /** Создать адрес В группу адресов (нужно право create_address на группе). Новый адрес — member группы, доступен её участникам */
1331
+ /** Создать адрес В группу адресов (нужно право create_address на группе). Новый адрес — member группы, доступен её участникам. Если создание ушло на подпись (202), папка сохраняется в заявке, а право на неё перепроверяется при применении: отозвали — адрес создаётся без папки, с оговоркой applyWarnings в ответе голосования. При прямом создании (201) то же — оговорка `warnings` в теле ответа */
1332
1332
  groupId?: string
1333
1333
  }
1334
1334
 
@@ -1354,6 +1354,15 @@ export interface CreateAddressResult {
1354
1354
  label: string | null
1355
1355
  description: string | null
1356
1356
  }
1357
+ /** Создано, но не всё: адрес есть, а в папку из `groupId` не положен. Право на папку роут проверяет до создания (иначе 403), а запись членства — уже после Wallet Service и мониторинга; право могли отозвать в этом окне или запись могла не пройти. Есть только когда непусто; не показывать адрес в папке, если оговорка пришла */
1358
+ warnings?: FolderSkipWarning[]
1359
+ }
1360
+
1361
+ export interface FolderSkipWarning {
1362
+ /** FOLDER_NOT_FOUND — папку удалили; FOLDER_FORBIDDEN — у создателя адреса больше нет права заводить в неё адреса; FOLDER_WRITE_FAILED — сбой записи */
1363
+ code: "FOLDER_NOT_FOUND" | "FOLDER_FORBIDDEN" | "FOLDER_WRITE_FAILED"
1364
+ /** Готовая фраза для человека */
1365
+ message: string
1357
1366
  }
1358
1367
 
1359
1368
  export interface CreateCompositeBody {
@@ -3203,6 +3212,8 @@ export interface RequestVoteResult {
3203
3212
  resolvedResultId: string | null
3204
3213
  /** Только для композитных заявок: прогресс по шагам */
3205
3214
  stepsProgress?: StepProgress[]
3215
+ /** Применено, но не всё: адрес по заявке create_address создан (resolvedResultId), а в папку, выбранную при подаче, не положен. Право на папку проверяется при ПРИМЕНЕНИИ, а не при подаче. Есть только когда непусто; в истории заявки — событие folder_skipped */
3216
+ applyWarnings?: FolderSkipWarning[]
3206
3217
  }
3207
3218
 
3208
3219
  export interface StepProgress {
@@ -73,9 +73,10 @@ export class AddressesResource {
73
73
  /**
74
74
  * Metadata only: this touches neither money nor the derivation path.
75
75
  *
76
- * Answers with the listing shape, minus `sharing` — the endpoint reuses the
77
- * schema but does not fill that property, which is why it is optional on
78
- * `Address`. Refresh the list rather than reading the share count back.
76
+ * Answers with the same row as the listing — `owner`, `access` and
77
+ * `sharing` included — so it can replace the address in a list without a
78
+ * refetch. Recent operations and `shares`, which `get()` carries, are not
79
+ * in it.
79
80
  */
80
81
  update(addressId: string, input: PatchAddressBody): Promise<Address> {
81
82
  return this.#http.request<Address>('PATCH', `${this.#base}/${seg(addressId)}`, { body: input })
@@ -23,10 +23,20 @@ export interface VoteInput {
23
23
  *
24
24
  * applied (201) — executed, the money left;
25
25
  * pending (202) — awaiting signatures, funds RESERVED but not sent;
26
- * replayed (200) — a repeat; the transfer already existed.
26
+ * replayed (200) — a transfer with this `idempotencyKey` already existed,
27
+ * and this is that transfer as it stands NOW.
27
28
  *
28
29
  * They cannot be told apart by the body. Showing "sent" on `pending` tells
29
30
  * the person money has gone when it is frozen.
31
+ *
32
+ * `replayed` does not mean "sent" either. It says only that the request was
33
+ * seen before; what became of it is in `data.transaction.status`:
34
+ *
35
+ * confirmed, submitted — it left;
36
+ * awaiting_approval — waiting for signatures, funds reserved;
37
+ * pending — still executing;
38
+ * failed, rejected,
39
+ * cancelled — it did not leave.
30
40
  */
31
41
  export class TransfersResource {
32
42
  readonly #http: HttpClient
@@ -37,7 +47,13 @@ export class TransfersResource {
37
47
  this.#base = `/api/v1/vaults/${seg(vaultCode)}/transfers`
38
48
  }
39
49
 
40
- /** Sends a transfer. The timeout is long: the path calls the wallet service. */
50
+ /**
51
+ * Sends a transfer. The timeout is long: the path calls the wallet service.
52
+ *
53
+ * On `outcome: 'replayed'` read `data.transaction.status` before telling
54
+ * anyone anything — a replay of an attempt that failed, or that is still
55
+ * waiting for signatures, comes back the same way as one that went out.
56
+ */
41
57
  async send(
42
58
  input: CreateTransferBody,
43
59
  options: { signal?: AbortSignal; timeoutMs?: number } = {},
@@ -42,10 +42,6 @@ import type {
42
42
  * the two must not be merged, because "no account" and "empty account"
43
43
  * deserve different wording. Amounts are raw smallest units; put them through
44
44
  * the helpers in `units.ts` rather than `Number`.
45
- *
46
- * One field is thinner than the type admits: `sharing` is optional because
47
- * `PATCH /addresses/{id}` reuses this schema WITHOUT it (the document says so
48
- * itself, on the property). Listings and the detail endpoint do send it.
49
45
  */
50
46
  export type Address = AddressListItem
51
47
 
package/src/version.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent in the `user-agent` header. */
2
- export const SDK_VERSION = '0.2.0'
2
+ export const SDK_VERSION = '0.2.1'
3
3
 
4
4
  /** Default API base URL. */
5
5
  export const DEFAULT_BASE_URL = 'https://policy-engine-rest.ezig.workers.dev'