@v3finance/core-sdk 0.1.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 (217) hide show
  1. package/CHANGELOG.md +441 -7
  2. package/README.md +219 -73
  3. package/dist/cjs/client.d.ts +67 -22
  4. package/dist/cjs/client.d.ts.map +1 -1
  5. package/dist/cjs/client.js +38 -2
  6. package/dist/cjs/client.js.map +1 -1
  7. package/dist/cjs/core/errors.d.ts +30 -10
  8. package/dist/cjs/core/errors.d.ts.map +1 -1
  9. package/dist/cjs/core/errors.js +47 -15
  10. package/dist/cjs/core/errors.js.map +1 -1
  11. package/dist/cjs/core/gated.d.ts +30 -6
  12. package/dist/cjs/core/gated.d.ts.map +1 -1
  13. package/dist/cjs/core/gated.js +21 -6
  14. package/dist/cjs/core/gated.js.map +1 -1
  15. package/dist/cjs/core/http.d.ts +12 -8
  16. package/dist/cjs/core/http.d.ts.map +1 -1
  17. package/dist/cjs/core/http.js +147 -43
  18. package/dist/cjs/core/http.js.map +1 -1
  19. package/dist/cjs/core/pagination.d.ts +28 -1
  20. package/dist/cjs/core/pagination.d.ts.map +1 -1
  21. package/dist/cjs/core/pagination.js +36 -1
  22. package/dist/cjs/core/pagination.js.map +1 -1
  23. package/dist/cjs/generated/operations.js +1 -1
  24. package/dist/cjs/generated/types.d.ts +965 -179
  25. package/dist/cjs/generated/types.d.ts.map +1 -1
  26. package/dist/cjs/index.d.ts +32 -8
  27. package/dist/cjs/index.d.ts.map +1 -1
  28. package/dist/cjs/index.js +12 -0
  29. package/dist/cjs/index.js.map +1 -1
  30. package/dist/cjs/resources/addresses.d.ts +23 -8
  31. package/dist/cjs/resources/addresses.d.ts.map +1 -1
  32. package/dist/cjs/resources/addresses.js +8 -1
  33. package/dist/cjs/resources/addresses.js.map +1 -1
  34. package/dist/cjs/resources/admin.d.ts +150 -25
  35. package/dist/cjs/resources/admin.d.ts.map +1 -1
  36. package/dist/cjs/resources/admin.js +94 -23
  37. package/dist/cjs/resources/admin.js.map +1 -1
  38. package/dist/cjs/resources/approvals.d.ts +46 -5
  39. package/dist/cjs/resources/approvals.d.ts.map +1 -1
  40. package/dist/cjs/resources/approvals.js +27 -4
  41. package/dist/cjs/resources/approvals.js.map +1 -1
  42. package/dist/cjs/resources/balances.d.ts +152 -16
  43. package/dist/cjs/resources/balances.d.ts.map +1 -1
  44. package/dist/cjs/resources/balances.js +9 -5
  45. package/dist/cjs/resources/balances.js.map +1 -1
  46. package/dist/cjs/resources/categories.d.ts +93 -21
  47. package/dist/cjs/resources/categories.d.ts.map +1 -1
  48. package/dist/cjs/resources/categories.js +65 -18
  49. package/dist/cjs/resources/categories.js.map +1 -1
  50. package/dist/cjs/resources/counterparties.d.ts +26 -3
  51. package/dist/cjs/resources/counterparties.d.ts.map +1 -1
  52. package/dist/cjs/resources/counterparties.js +17 -0
  53. package/dist/cjs/resources/counterparties.js.map +1 -1
  54. package/dist/cjs/resources/folders.d.ts +51 -10
  55. package/dist/cjs/resources/folders.d.ts.map +1 -1
  56. package/dist/cjs/resources/folders.js +36 -5
  57. package/dist/cjs/resources/folders.js.map +1 -1
  58. package/dist/cjs/resources/history.d.ts +20 -2
  59. package/dist/cjs/resources/history.d.ts.map +1 -1
  60. package/dist/cjs/resources/history.js.map +1 -1
  61. package/dist/cjs/resources/me.d.ts +27 -4
  62. package/dist/cjs/resources/me.d.ts.map +1 -1
  63. package/dist/cjs/resources/me.js +22 -1
  64. package/dist/cjs/resources/me.js.map +1 -1
  65. package/dist/cjs/resources/members.d.ts +19 -4
  66. package/dist/cjs/resources/members.d.ts.map +1 -1
  67. package/dist/cjs/resources/members.js +15 -6
  68. package/dist/cjs/resources/members.js.map +1 -1
  69. package/dist/cjs/resources/operations.d.ts +64 -8
  70. package/dist/cjs/resources/operations.d.ts.map +1 -1
  71. package/dist/cjs/resources/operations.js +26 -2
  72. package/dist/cjs/resources/operations.js.map +1 -1
  73. package/dist/cjs/resources/registries.d.ts +87 -9
  74. package/dist/cjs/resources/registries.d.ts.map +1 -1
  75. package/dist/cjs/resources/registries.js +54 -11
  76. package/dist/cjs/resources/registries.js.map +1 -1
  77. package/dist/cjs/resources/reports.d.ts +300 -41
  78. package/dist/cjs/resources/reports.d.ts.map +1 -1
  79. package/dist/cjs/resources/reports.js +135 -7
  80. package/dist/cjs/resources/reports.js.map +1 -1
  81. package/dist/cjs/resources/sharing.d.ts +61 -10
  82. package/dist/cjs/resources/sharing.d.ts.map +1 -1
  83. package/dist/cjs/resources/sharing.js +51 -4
  84. package/dist/cjs/resources/sharing.js.map +1 -1
  85. package/dist/cjs/resources/transfers.d.ts +44 -6
  86. package/dist/cjs/resources/transfers.d.ts.map +1 -1
  87. package/dist/cjs/resources/transfers.js +41 -3
  88. package/dist/cjs/resources/transfers.js.map +1 -1
  89. package/dist/cjs/resources/types.d.ts +16 -0
  90. package/dist/cjs/resources/types.d.ts.map +1 -1
  91. package/dist/cjs/units.d.ts.map +1 -1
  92. package/dist/cjs/units.js +18 -2
  93. package/dist/cjs/units.js.map +1 -1
  94. package/dist/cjs/version.d.ts +1 -1
  95. package/dist/cjs/version.js +1 -1
  96. package/dist/esm/client.d.ts +67 -22
  97. package/dist/esm/client.d.ts.map +1 -1
  98. package/dist/esm/client.js +38 -2
  99. package/dist/esm/client.js.map +1 -1
  100. package/dist/esm/core/errors.d.ts +30 -10
  101. package/dist/esm/core/errors.d.ts.map +1 -1
  102. package/dist/esm/core/errors.js +47 -15
  103. package/dist/esm/core/errors.js.map +1 -1
  104. package/dist/esm/core/gated.d.ts +30 -6
  105. package/dist/esm/core/gated.d.ts.map +1 -1
  106. package/dist/esm/core/gated.js +21 -6
  107. package/dist/esm/core/gated.js.map +1 -1
  108. package/dist/esm/core/http.d.ts +12 -8
  109. package/dist/esm/core/http.d.ts.map +1 -1
  110. package/dist/esm/core/http.js +148 -44
  111. package/dist/esm/core/http.js.map +1 -1
  112. package/dist/esm/core/pagination.d.ts +28 -1
  113. package/dist/esm/core/pagination.d.ts.map +1 -1
  114. package/dist/esm/core/pagination.js +36 -1
  115. package/dist/esm/core/pagination.js.map +1 -1
  116. package/dist/esm/generated/operations.js +1 -1
  117. package/dist/esm/generated/types.d.ts +965 -179
  118. package/dist/esm/generated/types.d.ts.map +1 -1
  119. package/dist/esm/index.d.ts +32 -8
  120. package/dist/esm/index.d.ts.map +1 -1
  121. package/dist/esm/index.js +17 -5
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/resources/addresses.d.ts +23 -8
  124. package/dist/esm/resources/addresses.d.ts.map +1 -1
  125. package/dist/esm/resources/addresses.js +8 -1
  126. package/dist/esm/resources/addresses.js.map +1 -1
  127. package/dist/esm/resources/admin.d.ts +150 -25
  128. package/dist/esm/resources/admin.d.ts.map +1 -1
  129. package/dist/esm/resources/admin.js +94 -23
  130. package/dist/esm/resources/admin.js.map +1 -1
  131. package/dist/esm/resources/approvals.d.ts +46 -5
  132. package/dist/esm/resources/approvals.d.ts.map +1 -1
  133. package/dist/esm/resources/approvals.js +27 -4
  134. package/dist/esm/resources/approvals.js.map +1 -1
  135. package/dist/esm/resources/balances.d.ts +152 -16
  136. package/dist/esm/resources/balances.d.ts.map +1 -1
  137. package/dist/esm/resources/balances.js +9 -5
  138. package/dist/esm/resources/balances.js.map +1 -1
  139. package/dist/esm/resources/categories.d.ts +93 -21
  140. package/dist/esm/resources/categories.d.ts.map +1 -1
  141. package/dist/esm/resources/categories.js +65 -18
  142. package/dist/esm/resources/categories.js.map +1 -1
  143. package/dist/esm/resources/counterparties.d.ts +26 -3
  144. package/dist/esm/resources/counterparties.d.ts.map +1 -1
  145. package/dist/esm/resources/counterparties.js +17 -0
  146. package/dist/esm/resources/counterparties.js.map +1 -1
  147. package/dist/esm/resources/folders.d.ts +51 -10
  148. package/dist/esm/resources/folders.d.ts.map +1 -1
  149. package/dist/esm/resources/folders.js +36 -5
  150. package/dist/esm/resources/folders.js.map +1 -1
  151. package/dist/esm/resources/history.d.ts +20 -2
  152. package/dist/esm/resources/history.d.ts.map +1 -1
  153. package/dist/esm/resources/history.js.map +1 -1
  154. package/dist/esm/resources/me.d.ts +27 -4
  155. package/dist/esm/resources/me.d.ts.map +1 -1
  156. package/dist/esm/resources/me.js +22 -1
  157. package/dist/esm/resources/me.js.map +1 -1
  158. package/dist/esm/resources/members.d.ts +19 -4
  159. package/dist/esm/resources/members.d.ts.map +1 -1
  160. package/dist/esm/resources/members.js +15 -6
  161. package/dist/esm/resources/members.js.map +1 -1
  162. package/dist/esm/resources/operations.d.ts +64 -8
  163. package/dist/esm/resources/operations.d.ts.map +1 -1
  164. package/dist/esm/resources/operations.js +26 -2
  165. package/dist/esm/resources/operations.js.map +1 -1
  166. package/dist/esm/resources/registries.d.ts +87 -9
  167. package/dist/esm/resources/registries.d.ts.map +1 -1
  168. package/dist/esm/resources/registries.js +54 -11
  169. package/dist/esm/resources/registries.js.map +1 -1
  170. package/dist/esm/resources/reports.d.ts +300 -41
  171. package/dist/esm/resources/reports.d.ts.map +1 -1
  172. package/dist/esm/resources/reports.js +135 -7
  173. package/dist/esm/resources/reports.js.map +1 -1
  174. package/dist/esm/resources/sharing.d.ts +61 -10
  175. package/dist/esm/resources/sharing.d.ts.map +1 -1
  176. package/dist/esm/resources/sharing.js +51 -4
  177. package/dist/esm/resources/sharing.js.map +1 -1
  178. package/dist/esm/resources/transfers.d.ts +44 -6
  179. package/dist/esm/resources/transfers.d.ts.map +1 -1
  180. package/dist/esm/resources/transfers.js +41 -3
  181. package/dist/esm/resources/transfers.js.map +1 -1
  182. package/dist/esm/resources/types.d.ts +16 -0
  183. package/dist/esm/resources/types.d.ts.map +1 -1
  184. package/dist/esm/units.d.ts.map +1 -1
  185. package/dist/esm/units.js +18 -2
  186. package/dist/esm/units.js.map +1 -1
  187. package/dist/esm/version.d.ts +1 -1
  188. package/dist/esm/version.js +1 -1
  189. package/docs/concepts.md +193 -0
  190. package/docs/errors.md +491 -0
  191. package/package.json +7 -6
  192. package/src/client.ts +89 -25
  193. package/src/core/errors.ts +49 -15
  194. package/src/core/gated.ts +31 -7
  195. package/src/core/http.ts +173 -49
  196. package/src/core/pagination.ts +38 -1
  197. package/src/generated/operations.ts +1 -1
  198. package/src/generated/types.ts +1001 -180
  199. package/src/index.ts +64 -7
  200. package/src/resources/addresses.ts +24 -8
  201. package/src/resources/admin.ts +186 -33
  202. package/src/resources/approvals.ts +55 -9
  203. package/src/resources/balances.ts +186 -14
  204. package/src/resources/categories.ts +125 -30
  205. package/src/resources/counterparties.ts +23 -6
  206. package/src/resources/folders.ts +74 -18
  207. package/src/resources/history.ts +27 -2
  208. package/src/resources/me.ts +30 -5
  209. package/src/resources/members.ts +26 -7
  210. package/src/resources/operations.ts +73 -8
  211. package/src/resources/registries.ts +90 -11
  212. package/src/resources/reports.ts +348 -67
  213. package/src/resources/sharing.ts +71 -18
  214. package/src/resources/transfers.ts +51 -7
  215. package/src/resources/types.ts +16 -0
  216. package/src/units.ts +19 -2
  217. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -3,11 +3,442 @@
3
3
  All notable changes to this package are documented in this file. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
5
5
 
6
+ ## 0.2.1 — 2026-11-12
7
+
8
+ ### Added
9
+
10
+ Spec snapshot refreshed from the live API (commit `07ac55d`). No operation was added or removed.
11
+
12
+ - `addresses.create()` with a `groupId` answers 201 with `warnings` when the address was created
13
+ but could not be put into the folder — the folder was deleted, the right to it was lost after
14
+ the check, or the write failed. The codes are in the new `FolderSkipWarning`:
15
+ `FOLDER_NOT_FOUND`, `FOLDER_FORBIDDEN`, `FOLDER_WRITE_FAILED`.
16
+ - `approvals.vote()` — `applyWarnings` on `RequestVoteResult`, the same warning for a vote that
17
+ applied an approved `create_address`. The API now keeps the folder chosen when the request was
18
+ filed (it used to drop it, and the address was created outside the folder without a word) and
19
+ checks the right to it when the request is applied. The request's history gets a
20
+ `folder_skipped` event; `ApprovalRequestEvent.type` is a `string`, so that is not a type change.
21
+ - `docs/errors.md`: the codes above, which are warnings and not errors, and the codes of the
22
+ web-login refusals in front of the auth library — `ADMIN_CANNOT_APPROVE_WEB_LOGIN`,
23
+ `WEB_SESSION_CANNOT_APPROVE`, `WEB_SESSION_CANNOT_MANAGE_SESSIONS` — plus the library's
24
+ `FAILED_TO_GET_SESSION`, which the API refers to. The note that the API does no rate limiting of
25
+ its own is corrected: requesting a web-login code is limited, with a 429 `RATE_LIMITED`.
26
+
27
+ ### Fixed
28
+
29
+ - **The client did not work in a browser at all.** Every request failed with a `network` error
30
+ before it was sent. The transport stored `fetch` in a private field and called it as a method,
31
+ which passes the client as the receiver; a browser's `fetch` rejects any receiver but the window
32
+ (`TypeError: Illegal invocation`). Node and React Native do not check, so nothing caught it until
33
+ the first web page used the package. `fetch` is now called with no receiver, and when no custom
34
+ one is given the global is looked up per call — so instrumentation or a mock server that
35
+ replaces `globalThis.fetch` after the client is built is honoured too. No workaround is needed
36
+ any more; if you passed `fetch: (input, init) => fetch(input, init)` to get around this, it can
37
+ go.
38
+ - `addresses.update()` answered without `owner`, `access` and `sharing`, although its type is the
39
+ listing row and declares the first two as required. That was the API's handler, and it is fixed
40
+ there: the answer is now the same row `addresses.list()` returns, so it can replace the address
41
+ in a list. `sharing` is accordingly required on `Address` (`AddressListItem`) and on
42
+ `AddressDetail`. Reading code is unaffected; a fixture or mock that builds one of these without
43
+ `sharing` stops compiling.
44
+ - `transfers.send()` — the documentation said `outcome: 'replayed'` means "already sent". It does
45
+ not: it means a transfer with this `idempotencyKey` already existed, and `data` is that transfer
46
+ as it stands now — confirmed, waiting for signatures, still executing or failed. Read
47
+ `data.transaction.status`. Corrected in the TSDoc of `transfers.send` and `Gated`, in the README,
48
+ `docs/concepts.md` and `docs/errors.md`. No code changed.
49
+
50
+ ## 0.2.0 — 2026-11-11
51
+
52
+ The API corrected its own document between 0.1.0 and this release:
53
+ [v3-finance/policy-engine#13](https://github.com/v3-finance/policy-engine/pull/13) brought the
54
+ response schemas in line with what the handlers send and tied the two together with tests on the
55
+ API's side. Every answer this package had been describing by hand, because the document was wrong
56
+ about it, is generated again — and a number of generated shapes moved as a result. Those moves are
57
+ listed under **Breaking** below; read that part before upgrading.
58
+
59
+ ### Added
60
+
61
+ - `docs/errors.md` — every error code the API can return, grouped by domain, with what causes it
62
+ and what a client should do about it, plus the codes that are easy to mishandle and the ones
63
+ declared in the server's types but never actually returned.
64
+ - `registries.evaluate(input, { explain: true })` — the per-policy trace of a decision: for every
65
+ enabled policy the person has for the action, whether it applied and, if not, which selector it
66
+ failed on. The API already read `?explain=true` but did not declare it, and a typed client
67
+ should not invent parameters; it is declared now. All four decisions carry `trace[]` then, a
68
+ bare `deny` included. Without the flag the field is absent, which is why `EvaluationResult`
69
+ keeps it optional.
70
+ - One schema per report type — `TransactionActivityReport`, `CounterpartyActivityReport`,
71
+ `CounterpartyGroupActivityReport`, `AssetMovementReport`, `AddressActivityReport`,
72
+ `AccountStatementReport`, `OperationsExceptionsReport`, `ApprovalActivityReport`,
73
+ `BalancePositionReport`, next to the existing `CategoryAnalytics` — and `AnyReport`, the union of
74
+ the ten. With them come the row types they are built from (`ByCounterpartyAddressRow`,
75
+ `ByGroupCounterpartyRow`, `ByCounterpartyBriefRow`, `StatementTokenRow`, `ReportAccountBalance`).
76
+ - Named request bodies for the reporting resource: `CreateReportTemplateBody`,
77
+ `PatchReportTemplateBody`, `CreateReportSnapshotBody`, `CreateReportExportBody` and
78
+ `ReportDrilldownBody`.
79
+ - Three snapshot schemas instead of one: `ReportSnapshot` (what creating one answers, figures under
80
+ `result`), `ReportSnapshotDetail` (one snapshot, figures under `resultJson`) and
81
+ `ReportSnapshotListItem`, plus `ReportSnapshotList` for the listing.
82
+ - Ledger and balances: `LedgerPosting`, `LedgerEntryToken`, `LedgerEntriesPage`,
83
+ `BalancesAsOfTotal`, `BalancesAsOfAccount`, and `BalancesAsOfWithAccounts` — what
84
+ `balances.asOf()` answers when no summary was asked for.
85
+ - Sharing: `ParticipantVia` (where a person's access comes from, as a union discriminated by
86
+ `kind`), `ShareBatchItem` (`ShareBatchGrantedItem | ShareBatchRejectedItem`, discriminated by
87
+ `ok`), `ShareBatchAllRejected` (the body of the all-refused 400) and `ShareGrantCreated`.
88
+ - Fields the API has been sending and now declares, so they are reachable through the types:
89
+ `usdValue`, `decisionPolicyVersionId`, `decisionCounterpartyGroupsJson`, `counterpartyId` and
90
+ `idempotencyKey` on `TransactionDetail`; `currentVersionId` on `Policy`, `PolicyDetail`,
91
+ `PolicyWithRules` and the rows of `VaultPoliciesPage`; `policyVersionId` on `PolicyTrace`;
92
+ `policyId` on `PolicyApprover`; `revokedAt` and `revokedById` on `AddressAccessGrant` and
93
+ `VisibilityGrant` (which also gains `updatedAt`); `groupId` on the rows of `VaultPoliciesPage`;
94
+ and `_count` on `AddressGroup` and `AddressGroupDetail` — an ORM counter that equals
95
+ `memberCount`, declared only because it arrives; read `memberCount`.
96
+ One of these changed behaviour as well as type: `POST /policies` used to answer
97
+ `currentVersionId: null` although the first version had already been taken, and now answers its
98
+ id.
99
+
100
+ ### Changed
101
+
102
+ - Spec snapshot refreshed from the live API twice since 0.1.0.
103
+
104
+ The first refresh (API commit `ebd03eb`) moved no type. The envelope no longer drops the
105
+ machine-readable keys a handler sets beside `error`: it carries them inside `details`, so
106
+ `error.details.field` names the category field that failed validation (it is an explicit `null`
107
+ when the error is not about one field) and `error.details.stepIndex` the composite step that was
108
+ denied, counted from 0. Against the 0.1.0 snapshot that was 306 changed descriptions in
109
+ `openapi/spec.json` and 6 changed examples — the invitation token example stopped looking like a
110
+ live secret — and two lines in `src/generated/`.
111
+
112
+ The second (API commit `165504b`, v3-finance/policy-engine#13) is the one this release is about.
113
+ The document now names 262 schemas, up from 228: 34 added, none removed, and 30 of the existing
114
+ ones changed shape. 72 operations under `/vaults/{code}` now declare a 403 or a 404 they did not
115
+ before; that changes no type — it documents refusals those endpoints already gave, the
116
+ vault-membership gate first among them.
117
+ - `docs/errors.md` told callers those two keys were stripped and had to be parsed out of `message`.
118
+ That was accurate when it was written and is not any more. Corrected with it:
119
+ - an unmatched path and an unhandled exception now answer with the envelope as JSON, so a
120
+ non-JSON error body usually means the edge or a proxy answered rather than the API. Not always,
121
+ and the document now says so: a malformed or empty body sent under a JSON content type is
122
+ refused by the framework's validator with a 400 `text/plain`, which is raised as an exception
123
+ carrying its own finished response and so never passes through the envelope layer. No resource
124
+ method of this package can produce one;
125
+ - a validation failure does not reach a client as the flat `{error: "…"}` string the document
126
+ showed. The handler builds that shape, the envelope layer repackages it, and what arrives is
127
+ the ordinary envelope with a status-derived `BAD_REQUEST`;
128
+ - there is nothing diagnostic left in `message` to forward on a signing-service failure or a
129
+ failed report — but only the report one is a constant (`Report generation failed`). The
130
+ signing-service message is a template carrying the upstream status (`Wallet Service returned
131
+ 400`), on address creation as well as on transfers; what is withheld is the service's own
132
+ response body, which goes to the worker log.
133
+ - `reports.run(type, …)` answers with the schema of THAT report. `ReportResult` used to map nine of
134
+ the ten types to one loose `ActivityReport` in which every block was optional, so
135
+ `run('balance_position', …)` offered a `summary` that report never has and no `totalsByAsset`,
136
+ which is all it has. Each type now maps to its own schema; `runUnified(type, …)` narrows the same
137
+ way, and `runTemplate()` answers `AnyReport`, because a template's type is not known to the
138
+ compiler.
139
+ - Every request body of the reporting resource is typed. `createTemplate`, `updateTemplate`,
140
+ `createSnapshot` and `createExport` took `unknown` because the document declared those bodies
141
+ inline and there was nothing to import; it names them now. `queryOperations` takes
142
+ `DrilldownQuery`, which is `ReportDrilldownBody` with its two free-form bags (`filters`,
143
+ `drilldown`) replaced by the keys the endpoint actually reads — the generated type alone would
144
+ let a misspelled filter compile and make a figure disagree with its own breakdown — and answers
145
+ `DrilldownResponse`.
146
+ - `me.updateProfile()` is typed `UserProfile`, as it was, and the type is now TRUE. The PATCH
147
+ handler used to return the database row without `needsEmail`, so the answer read `undefined`
148
+ where a `boolean` was promised — on the flag that gates the "add your real email" prompt. The
149
+ API sends it on both paths now.
150
+ - Names this package exports that the document also has — `LedgerPosting`, `ReportSnapshotDetail`,
151
+ `ReportSnapshotListItem` — are strict aliases of the generated schemas, as are `LedgerEntryItem`
152
+ (of `LedgerEntry`) and `PolicyRow` (of `PolicyTableItem`). Only `ReportSnapshotListItem` existed
153
+ in 0.1.0, as a hand-written `Pick` of the same eleven fields.
154
+ - `ReportType` is taken from the document's own enum rather than spelled out. Same ten values.
155
+ - `AssignCategoryBody.values` is optional, as it is to the API, which defaults it to `{}`. A
156
+ category with required fields still needs them filled.
157
+ - `sharing.share()` and `folders.grant()` are typed with `ShareGrantCreated`. It is the same union
158
+ as before, `AddressAccessGrant | ShareBatchResult`, under the name the document gives it.
159
+ - `balances.entries({ addressId })` narrows for a non-administrator too. The API used to ignore
160
+ the filter for members and answer with entries across all of their addresses; it now applies it
161
+ within the member's own addresses, and somebody else's address answers an empty page rather than
162
+ a 403. Nothing changed on this side — the same call returns less.
163
+ - `balances.entries({ entryType })` is typed with `LedgerEntryType`: the six kinds the journal
164
+ reports — `genesis` among them, the opening correction written when the ledger was first
165
+ backfilled — or the stored spelling of the three that are renamed on the way out. That is the
166
+ FILTER; an entry itself still carries a `string`, as the document declares.
167
+
168
+ #### Breaking
169
+
170
+ Type-level only: no method was renamed or removed, no exported name was dropped, and an existing
171
+ call sends the same request. What changed is what the compiler believes about answers, so an
172
+ upgrade can surface compile errors in code that reads them — in nearly every case at a spot that
173
+ was already wrong at runtime: a field that read `undefined`, a string treated as a number.
174
+
175
+ **A field changed type**
176
+
177
+ - `Comparison.incomingUsdChangePercent`, `outgoingUsdChangePercent`,
178
+ `grossTurnoverUsdChangePercent`, `operationsCountChangePercent` and
179
+ `averageOperationUsdChangePercent` — `number | null` → `string | null`.
180
+ - `Observation.changePercent` — `number | null` → `string | null`.
181
+
182
+ These six are decimal STRINGS on the wire (`"12.5"`, `"-40"`) and always were; the document said
183
+ `number`. Arithmetic or `toFixed()` on them compiled and operated on a string. Put them through
184
+ `parseDecimal`, or `Number()` if a float is acceptable for display.
185
+ - `ReportMeta.dataCutoff` — `string | null` → `{ timestamp: string, blockNumber: number | null }`.
186
+ - `ReportMeta.schemaVersion` — `string | number | null` → `string`.
187
+ - `CategoryAnalytics.report` — no longer `ReportMeta` but the five fields that report really
188
+ carries (`type`, `generatedAt`, `from`, `to`, `timezone`).
189
+ - `ActivityReport` — an interface whose twelve blocks were all optional → a deprecated UNION of
190
+ the nine per-type schemas. Reading `report.summary` off it no longer compiles, because not every
191
+ member has one. Use the schema of the report you asked for, or `AnyReport`. Narrow a union by a
192
+ block only that report has (`'totalsByAsset' in report`): `report.type` will not do it, since
193
+ the document types that field as the same ten-value enum on every member.
194
+ - `reports.runTemplate()` — `ActivityReport` → `AnyReport`, which adds `CategoryAnalytics`.
195
+ - `reports.run()` and `reports.runUnified()` — `ActivityReport` → the schema of the report asked
196
+ for. Reading a block that report does not have stops compiling: `.approvals` on anything but
197
+ `approval_activity`, `.summary` on `account_statement`. Those reads returned `undefined` before.
198
+
199
+ **A parameter stopped accepting any string**
200
+
201
+ - `ReportFilters`: `dateBasis`, `groupBy`, `comparison`, `counterpartyClassification`,
202
+ `folderClassification`, `breakdownBy` and `agg` are the enums the document declares. `groupBy:
203
+ 'quarter'` used to compile and answer 400.
204
+ - `reports.run('address_activity', …)` requires exactly one of `addressId` and `folderId`. Neither,
205
+ or both, was a 400 — delivered as `REPORT_INVALID_PERIOD`, which points at the dates.
206
+ - `balances.entries({ entryType })` — `string` → the union of entry types.
207
+ - `reports.snapshot()` — `ReportSnapshot` → `ReportSnapshotDetail`. There is no `result` on it: the
208
+ figures are under `resultJson`, where the detail endpoint has always put them. `result` exists
209
+ only on what `createSnapshot()` answers.
210
+ - `BalancesAsOf.totals` and `.accounts` — arrays of untyped records → `BalancesAsOfTotal[]` and
211
+ `BalancesAsOfAccount[]`. `accounts` is also OPTIONAL in the schema now (absent under
212
+ `summary: true`); the three overloads of `balances.asOf()` still tell the two cases apart, so
213
+ `(await balances.asOf(at)).accounts` needs no check.
214
+ - `LedgerEntry.postings[]` — the fields the document used to promise beyond the two ids (`amount`,
215
+ `amountDecimal`, `direction`, `token`, `account`) are gone, replaced by what arrives:
216
+ `LedgerPosting`, with the figure in `amountRawSigned`/`amountDecimalSigned`.
217
+ `LedgerEntry.token` is `LedgerEntryToken | null` instead of an untyped record, and
218
+ `operationType` is `'transfer' | 'deposit' | null`.
219
+ - `Participant.via` — an array of untyped records → `ParticipantVia[]`.
220
+ - `CategoryAccessScope` — `unknown` → `'owner_only' | 'vault_members'`, which is what
221
+ `viewScope`, `assignScope` and `editScope` take on `categories.create()`/`update()` and what a
222
+ category carries. The API still ACCEPTS `restricted` on input as an alias of `owner_only`, stores
223
+ it as `owner_only` and never answers with it; the type leaves it out on purpose, so a caller
224
+ that sends it stops compiling. Send `owner_only`.
225
+
226
+ **A field was removed from a type — it never arrived**
227
+
228
+ - `ReportTemplate.createdById` → the field is `ownerId`. `description` is declared, and
229
+ `parametersJson` is never `null`.
230
+ - `ByAddressOwnerRow.ownerId` and `.owner` → the field is `userId`, and the row also carries
231
+ `addressesCount`, `incomingByAsset` and `outgoingByAsset`.
232
+ - `AssetAmount.contractAddress`. It is on `ByAssetRow`, not on an amount.
233
+ - `PolicyTableItem.conditionJson` and `.approvalJson` — neither policy listing selects them
234
+ (`admin.policies.list`, `admin.policyGroups.policies`), so a row cannot tell you whether a policy
235
+ is conditional. Load the policy itself for that.
236
+ - `_count.limits` on the rows of `VaultPoliciesPage`.
237
+
238
+ **A field became required or lost `null`**
239
+
240
+ Harmless when reading, a compile error when BUILDING one of these — a fixture or a mock: the six
241
+ status counters, `activeDays`, `firstOperationAt` and `lastOperationAt` on `ReportSummary`; every
242
+ field of `ReportMeta` (all are required now, `from`, `to`, `timezone` and `generatedById` are never
243
+ `null`, and `type`, `dateBasis`, `baseCurrency` and `classificationMode` are enums); `tokenId`,
244
+ `assetCode` and `network` on `AssetAmount` and `ByAssetRow`; `severity` (`'info' | 'warning'`) and
245
+ `drilldownFilters` on `Observation`; most of `ReportSnapshot`, `result` included; `vaultId`,
246
+ `requestedAt`, `storageKey`, `errorCode` and `errorMessage` on `ReportExport`; `updatedAt` on
247
+ `ReportTemplate`; `metric.agg` on `CategoryAnalytics`; `LedgerEntry.reference`; and the fields
248
+ listed under Added. `ShareBatchResult.results[]` is a discriminated union now, so an item must be
249
+ consistent with its `ok`.
250
+
251
+ **A request body that was `unknown` is checked**
252
+
253
+ `createTemplate`, `updateTemplate`, `createSnapshot` and `createExport`, as above; and
254
+ `queryOperations`, whose body went from `unknown` to `DrilldownQuery` and whose answer from
255
+ `unknown` to `DrilldownResponse`. A body the API would have refused with a 400 no longer compiles.
256
+
257
+ `parameters` on the first three is typed as a free-form record, and it is NOT a query string — the
258
+ API hands it to the report builder as it is, parsing nothing and defaulting nothing. So the
259
+ obvious move, spreading the filters of a live report into it, is safe only with two conditions:
260
+
261
+ - **`categoryField` is ignored there.** A live report parses `'ticker:EUR'` into a filter; a
262
+ template or snapshot does not, and answers 200 with figures for EVERY value — a frozen report
263
+ that silently is not the one on screen. Send the parsed form instead:
264
+ `categoryFieldValues: { ticker: ['EUR'] }`. This is a defect of the API, reported there; the
265
+ package documents it rather than hides it.
266
+ - **`timezone` has no default there.** The live endpoints fall back to `UTC`; pass it explicitly.
267
+
268
+ Multi-value filters must be arrays (a comma-separated string is not split), and because the type
269
+ is an index signature a value typed as the `ReportFilters` interface needs a spread
270
+ (`parameters: { ...filters }`).
271
+
272
+ **Unchanged in type, corrected in meaning**
273
+
274
+ - `AssetAmount.amount` is still a `string`, and its description no longer says "raw smallest
275
+ units". It is a DECIMAL string in units of the asset (`"1.5"`), signed only in the `netFlow*`
276
+ blocks. Code that passed it to `amount()` was already failing at runtime on the first fractional
277
+ value; use `parseDecimal`. The same holds for the `byAsset` rows and the balances of
278
+ `asset_movement`, `account_statement` and `balance_position`. The one raw figure in a report is
279
+ `balanceRaw` in the opening and closing balances of `address_activity`.
280
+ - `BalancesAsOfTotal.available` and `.reserved` are DECIMAL too — duplicates of
281
+ `availableDecimal`/`reservedDecimal` — while the fields of the same names on `balances.list()`
282
+ are raw. Raw figures of a replay exist only per account, in `accounts[].balance`.
283
+ - The percentage fields that are new to the document with the per-type schemas —
284
+ `turnoverShareUsd`, `shareOfGroupTurnoverUsd`, `shareOfUserActivityUsd`, the `…RatePercent`
285
+ fields of `approval_activity`, the two shares in `balance_position.concentration` — are decimal
286
+ strings as well.
287
+
288
+ ### Fixed
289
+
290
+ Nineteen of the fixes below NARROW a type that used to be `string`, `unknown` or a loose
291
+ `Record`. Code that passed a value the API rejects now fails to compile instead of failing at
292
+ runtime with a 400 — which is the point, but it means an upgrade can surface new compile errors.
293
+ None of them renames or removes an exported name.
294
+
295
+ **Calls that could not be made at all**
296
+
297
+ - `reports.run('balance_position', …)` accepted no parameters. Its entry declared
298
+ `Record<string, never>`, and that index signature applies to the fields of `ReportFilters` too
299
+ once the two are intersected, collapsing every one of them to `never`. The only argument that
300
+ compiled was `{}` — so the report could not be asked for a date, which is the whole point of a
301
+ position report. All six parameters the handler reads now compile.
302
+ - The `idempotency-key` header is no longer sent. The server has never read it, which by itself
303
+ bought nothing; in a browser it cost the entire request, because the API's CORS preflight allows
304
+ exactly `Content-Type` and `Authorization`, so a money call carrying the header was never sent
305
+ and surfaced as a network error no retry explained. The `idempotencyKey` option stays, accepted
306
+ and deprecated: the body field is what protects a transfer.
307
+ - `units.format()` threw `RangeError` for any token with 0 or 1 decimals (`minimumFractionDigits`
308
+ defaulted to 2 above a `maximumFractionDigits` of `decimals`). The decimals ceiling was also 36
309
+ while the API and the package's own generated type allow 38, so `registries.createToken` could
310
+ register a token that `amount()` then refused.
311
+
312
+ **Answers that were read from the wrong place**
313
+
314
+ - `balances.entries()` typed postings from the document, which declared `amount`,
315
+ `amountDecimal`, `direction`, `token` and `account`. The handler sends none of them — the money
316
+ field is `amountRawSigned` — so `BigInt(posting.amount)` threw on the only endpoint that exposes
317
+ the ledger. The document has been corrected, and the type is the generated `LedgerPosting`.
318
+ - `reports.snapshot()` promised `result`; the detail handler returns the stored row, so the field
319
+ is `resultJson`. The numbers were unreachable through the type and `result` read `undefined`
320
+ every time. The document now has a schema for each answer: `snapshot()` is typed with
321
+ `ReportSnapshotDetail`, and `createSnapshot()` keeps `ReportSnapshot` with `result` — that
322
+ handler does map it.
323
+ - `balances.asOf()` exposed 2 of its 8 parameters. Opening the rest meant opening `summary`, and
324
+ with `summary` the handler omits `accounts` altogether. Three overloads tell the cases apart —
325
+ `BalancesAsOfWithAccounts` without a summary, `BalancesAsOfSummary` with one, the union when the
326
+ flag is decided at runtime — so the type never promises an array that will not be there. The
327
+ document agrees now: `accounts` is optional on `BalancesAsOf`.
328
+
329
+ **Values the API rejects no longer compile**
330
+
331
+ - `approvals.list({status})` took any string, but approval statuses are UPPERCASE while transfer
332
+ statuses are lowercase and both appear on one screen; `'pending'` compiled and got a 400.
333
+ - `history.list({status})` took any string, and the package's own exported `TransactionStatus`
334
+ includes `submitted`, which that filter does not accept — the SDK was suggesting a value that
335
+ returns 400.
336
+ - `balances.list({purpose})` knew 2 of the 4 ledger purposes.
337
+ - `OperationType`, `OperationKind` and `PendingApproval` were hand-written aliases widened to
338
+ `string`, and an explicit re-export shadows the generated schema of the same name — so
339
+ `OperationType` reached consumers as `string` rather than `'deposit' | 'transfer'`, and a typo
340
+ in an operation type was caught only by the API. All three are strict aliases now, and
341
+ `test/nameShadowing.test.ts` fails if one widens again.
342
+ - Request bodies that were `unknown` on `categories.addField`/`updateField`/`addApprover`/
343
+ `addGrant`, `admin.invitations.acceptDebug`, `vaults.buildDerivationPath`/`parseDerivationPath`
344
+ now carry their schemas. Response types were filled in only where the handler was read and
345
+ matched.
346
+
347
+ **Transport**
348
+
349
+ - `timeoutMs` and `signal` stopped applying once the response headers arrived: the timer was
350
+ cleared around `fetch`, and the body streamed after. A server that answered and then stalled
351
+ mid-body left a request hanging with nothing to end it. Both now stay armed until the body is in
352
+ hand, and a body read that fails is no longer swallowed into `data: undefined` typed as the
353
+ shape the caller asked for.
354
+ - A non-JSON error body put the proxy's whole HTML page into `message` (measured at 2.3 KB), and
355
+ `request()` and `fetchRaw()` disagreed about the same 502. They now answer alike, HTML is
356
+ suppressed and a short text body is kept.
357
+ - `Gated.status` reported 201 when the server had answered 200.
358
+ - A sub-second timeout rendered as "within 0s".
359
+
360
+ **Documentation that no longer matched the API**: the shape of a validation error, the claim that
361
+ a non-JSON error body can no longer come from the API, `CATEGORY_NOT_FOUND`'s actual paths,
362
+ `REPORT_VAULT_NOT_FOUND` (unreachable — all seven in-handler vault 404s in the reports router are
363
+ dead code), `details.param` on `REPORT_INVALID_PARAMS`, `SUBJECT_REQUIRED` across its three
364
+ different answers, four of nine observation codes, the missing `OPERATION_RELATION_INVALID`, the
365
+ transport codes (`NETWORK_ERROR`, `TIMEOUT`, `ABORTED`, `PARSE_ERROR`) that were named nowhere,
366
+ the count of endpoints left out, the `replayed` branch that only `transfers.send` can reach, and
367
+ the keyset-pagination claim (11 list endpoints are cursored, 18 return `items` alone, 8 a bare
368
+ array).
369
+
370
+ - `admin.users.setRole` took any string for the platform role. The spec declares
371
+ `enum: ["admin", "user"]`, so this is a narrowing the contract already backed — and worth the
372
+ compile error on a privilege path, where `string` meant finding out from a 400 after sending a
373
+ role change. The response keeps `role: string`: the spec declares it bare, and narrowing an
374
+ answer we do not control would be a guess. The type is exported as `PlatformRole`.
375
+
376
+ - `approvals.vote` returned `unknown`. It is typed from `RequestVoteResult`, which the spec
377
+ declares as a component but never `$ref`s — the operation inlines its 200 instead. The two were
378
+ compared property by property and are the same schema, down to identical `required` sets; the
379
+ only difference is an `additionalProperties: false` that does not reach the TypeScript shape.
380
+ Worth checking rather than assuming, because a component nothing references usually means the
381
+ opposite — an orphan left behind, which is what the `Address` schema turned out to be.
382
+ - `deleteSnapshot` and `deleteTemplate` returned `unknown` for a body the spec pins as
383
+ `{deleted: true}`.
384
+
385
+ **Guards added**, because none of the above was caught by a green CI: `generate:check` compares
386
+ only the generated files against the snapshot, and the surface test only checks that paths are
387
+ reachable. New metatests cover name shadowing, query parameters missing from a method's type,
388
+ request bodies left as `unknown`, and the completeness of `docs/errors.md`.
389
+
390
+ Two more came with the corrected document. `test/reportResults.test.ts` holds every method of the
391
+ reporting resource to the schema the API declares for the operation it calls — the check that
392
+ would have caught nine report types sharing one catch-all. And the body test now has a second
393
+ list beside the untyped one: bodies typed by hand OVER a named schema, each held to that schema's
394
+ fields. It has one entry, `queryOperations`; the list of untyped bodies is empty.
395
+
396
+ `test/transport.test.ts` pins the transport fixes by BEHAVIOUR, which none of the metatests can
397
+ reach: they are all about *when* something happens — when the timer is cleared, when the body is
398
+ read, which status is reported — and no type describes that. Its stub aborts its body stream when
399
+ the signal fires, the way a real `fetch` does, and that detail is the whole test: a stub that
400
+ ignores the signal passes even against the broken client, because nothing can interrupt a body
401
+ nobody wired up. Each case was checked by reverting the fix — restoring the old timer placement
402
+ turns four of them red with "test timed out", which is the production symptom exactly.
403
+
404
+ **Seven places where the API's document disagreed with its own handlers** turned up while this
405
+ release was being prepared. hono-openapi does not validate answers against the document, so
406
+ nothing here could fix them — only describe the answer by hand and say why.
407
+ v3-finance/policy-engine#13 fixed all seven at the source, and the hand-written descriptions have
408
+ been dropped in favour of the generated types:
409
+
410
+ | What was wrong | What it meant in 0.1.0 | Now |
411
+ |---|---|---|
412
+ | Ledger postings declared five fields that never arrive and none of the six that do | `posting.amount` was `undefined`; `BigInt(posting.amount)` threw | generated `LedgerPosting`, figure in `amountRawSigned` |
413
+ | One schema for a snapshot, though creating one answers `result` and reading one `resultJson` | `snapshot(id).result` read `undefined`; the figures were unreachable through the type | three schemas; `snapshot()` answers `ReportSnapshotDetail` |
414
+ | `accounts` required on `BalancesAsOf`, though absent under `summary=true` | nothing yet — `asOf()` did not offer `summary` | optional in the schema; the overloads of `asOf()` say which call gets it |
415
+ | `AssetAmount.amount` described as raw smallest units | `amount(x.amount, decimals)` threw on the first fractional value | described as the decimal string it is |
416
+ | The all-refused batch grant answers 400 with `{results}`, declared as an error envelope | the per-subject reasons were in `error.details`, which nothing described | declared as a second form of that 400, `ShareBatchAllRejected`. The SDK still throws on it, body in `details` |
417
+ | `usdValue`, `decisionPolicyVersionId`, `currentVersionId` sent but not declared | unreachable through the types | declared; see Added |
418
+ | `PATCH /me/profile` declared `needsEmail` and did not send it | `updateProfile()` answered `needsEmail: undefined` | the HANDLER was fixed, not the document: the flag is sent |
419
+
420
+ What remains hand-written is hand-written because the document has nothing to offer, not because
421
+ it is wrong: query parameters (the generator emits schemas only, never `parameters`), the few
422
+ answers the API still declares inline without a name (`AmlScore`, the status-history row), and the
423
+ unions it carries only as a bare `string` — `TransactionStatus`, `ApprovalRequestStatus`,
424
+ `LedgerEntryType`. One answer stays untyped for the first of those reasons: the group access
425
+ summary of `admin.policyGroups.access`.
426
+
427
+ - `codeFromStatus` now uses the API's own name for every status the API names: 401 is
428
+ `UNAUTHENTICATED` (was `UNAUTHORIZED`), 422 is `UNPROCESSABLE_ENTITY` (was `UNPROCESSABLE`), 503
429
+ is `SERVICE_UNAVAILABLE` (was `UNAVAILABLE`), and 413/501 are now named at all. Previously a
430
+ non-JSON response gave a code the server never uses for that condition, so a client branching on
431
+ the server's name missed it. 502 and 504 keep the SDK's own names, because the API has no
432
+ envelope for them — they arrive from the edge.
433
+ The test covering this mapping asserted only the statuses that already agreed; it now covers all
434
+ of them.
435
+
6
436
  ## 0.1.0 — 2026-11-10
7
437
 
8
- First release. The whole API is covered — 171 operations grouped into resources, with the
9
- administrative surface marked — and a test asserts that every endpoint is reachable, so one added
10
- to the API shows up as a failure rather than as a gap nobody noticed.
438
+ First release. Every endpoint a client calls is covered — 157 of the API's 171 operations, grouped
439
+ into resources, with the administrative surface marked; the fourteen exceptions are listed under
440
+ Known limitations. A test asserts that each of the rest is reachable, so one added to the API shows
441
+ up as a failure rather than as a gap nobody noticed.
11
442
 
12
443
  ### Added
13
444
 
@@ -17,8 +448,8 @@ to the API shows up as a failure rather than as a gap nobody noticed.
17
448
  - Injected authentication: `getAuthHeaders`, `onTokenRotated`, `onUnauthorized`. Works with a
18
449
  browser cookie, a native cookie jar or a bearer token; the package stores no credentials.
19
450
  - `CoreError` with a `code` that is always present and a `kind` separating HTTP, network, timeout,
20
- aborted and parse failures. Three real response shapes are parsed, including the validation
21
- failure that arrives as a plain string.
451
+ aborted and parse failures. Three real response shapes are parsed, including a flat
452
+ `{error: "…"}` string — which is what a non-JSON error body is read as.
22
453
  - Money as smallest-unit strings with BigInt arithmetic (`amount`, `parseDecimal`, `format`, `add`,
23
454
  `sub`, `compare`). There is no `toNumber`, on purpose: it would be a convenient door into the
24
455
  precision loss the module exists to prevent.
@@ -40,5 +471,8 @@ to the API shows up as a failure rather than as a gap nobody noticed.
40
471
  - The `assets` filter on reports is accepted by the API and silently ignored by it, so it is marked
41
472
  deprecated here. Use `tokenIds`.
42
473
  - `runTemplate` cannot override a template's stored filters: the endpoint accepts no body.
43
- - `/welcome` and `/webhooks/wallet` are not exposed — a browser landing page and the wallet
44
- service's inbound channel respectively.
474
+ - 14 of the 171 operations are not exposed. `/welcome` and `/webhooks/wallet` are a browser landing
475
+ page and the wallet service's inbound channel; the other 12 are the `/address-groups/*` paths,
476
+ which are the older prefix of the same endpoints the folders resource serves under
477
+ `/address-folders` — one entity, two prefixes, and exposing both would only raise the question of
478
+ which to call.