@v3finance/core-sdk 0.2.2 → 0.2.4

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
@@ -3,6 +3,43 @@
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.4 — 2026-11-12
7
+
8
+ Spec snapshot refreshed from the live API (commit `5e8f3e3`,
9
+ [v3-finance/policy-engine#24](https://github.com/v3-finance/policy-engine/pull/24)). No operation
10
+ was added or removed, no type changed, and no code in this package changed: the API now refuses a
11
+ counterparty address that belongs to the vault itself.
12
+
13
+ ### Fixed
14
+
15
+ - `counterparties.create()` and `counterparties.addAddress()` — an address that is one of this
16
+ vault's own addresses on the same network is refused with 409 `ADDRESS_IS_VAULT_ADDRESS`, and
17
+ `details` is `{address, network}`. Nothing is created or added. It used to be accepted, and the
18
+ retro-match then tagged internal transfers on that address as operations with the counterparty —
19
+ irreversibly, since removing the address does not untag them. Inside a chain or in the apply of
20
+ an approved request the code stays the chain's (`CHAIN_FAILED_ROLLED_BACK`, or `APPLY_FAILED` in
21
+ `applyError`), with the same sentence in `message`.
22
+ - `docs/errors.md`: the code in the counterparty table, and where it arrives under another code.
23
+
24
+ ## 0.2.3 — 2026-11-12
25
+
26
+ Spec snapshot refreshed from the live API (commit `031d7bf`,
27
+ [v3-finance/policy-engine#23](https://github.com/v3-finance/policy-engine/pull/23)). No operation
28
+ was added or removed, no type changed, and no code in this package changed: two failures that used
29
+ to answer 500 `INTERNAL_ERROR` now answer 409 with a code of their own.
30
+
31
+ ### Fixed
32
+
33
+ - `folders.update()` — renaming a folder to a name its owner already uses for another folder is a
34
+ 409 `GROUP_NAME_CONFLICT`, the same answer `folders.create()` gives. It used to be a 500
35
+ `INTERNAL_ERROR`.
36
+ - `addresses.create()` — when `create_address` requires signatures but fewer people can give them
37
+ than the threshold asks for, the call throws 409 `APPROVAL_UNSATISFIABLE` (no `details`); nothing
38
+ was created, and there is no 202. Do not retry: an administrator has to change the policy or the
39
+ approver group. Any other failure to create the approval request is 409 `APPROVAL_FAILED`. Both
40
+ used to be a 500 `INTERNAL_ERROR`.
41
+ - `docs/errors.md`: these codes in the address and folder tables.
42
+
6
43
  ## 0.2.2 — 2026-11-12
7
44
 
8
45
  Spec snapshot refreshed from the live API (commit `3ca1538`,
@@ -10,7 +10,7 @@
10
10
  // the API does not slip in unclassified.
11
11
  //
12
12
  // Operations: 171 · admin: 40 · both: 1 · member: 127 · service: 3
13
- // Spec sha256: 30be5f9b9733d1d1d67eebf21e0bfaa457651e951676211ec9fa464cca5d3795
13
+ // Spec sha256: 1f2a93f5a31a423e3301b4bfa103ad3db26723285cae060d2e8a904a2b3bffe0
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.OPERATIONS = void 0;
16
16
  exports.isAdminOperation = isAdminOperation;
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent in the `user-agent` header. */
2
- export declare const SDK_VERSION = "0.2.2";
2
+ export declare const SDK_VERSION = "0.2.4";
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
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_BASE_URL = exports.SDK_VERSION = void 0;
4
4
  /** SDK version, sent in the `user-agent` header. */
5
- exports.SDK_VERSION = '0.2.2';
5
+ exports.SDK_VERSION = '0.2.4';
6
6
  /** Default API base URL. */
7
7
  exports.DEFAULT_BASE_URL = 'https://policy-engine-rest.ezig.workers.dev';
8
8
  //# sourceMappingURL=version.js.map
@@ -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: 30be5f9b9733d1d1d67eebf21e0bfaa457651e951676211ec9fa464cca5d3795
12
+ // Spec sha256: 1f2a93f5a31a423e3301b4bfa103ad3db26723285cae060d2e8a904a2b3bffe0
13
13
  export const OPERATIONS = {
14
14
  "DELETE /api/v1/me/push-tokens/{token}": {
15
15
  "method": "delete",
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent in the `user-agent` header. */
2
- export declare const SDK_VERSION = "0.2.2";
2
+ export declare const SDK_VERSION = "0.2.4";
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.2';
2
+ export const SDK_VERSION = '0.2.4';
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/errors.md CHANGED
@@ -194,8 +194,10 @@ Two sets of uppercase strings travel inside **successful** bodies and will misle
194
194
  | `NO_DYNAMIC_ROUTE` | 500 | The vault's derivation scheme has no incrementing segment. | Configuration fault — address creation is impossible in this vault until an admin fixes it. |
195
195
  | `TOKEN_NOT_REGISTERED` | 500 | An asset segment points at a token missing from the registry. | Configuration fault. Contact an administrator. |
196
196
  | `POLICY_DENIED` | 403 | `block`, or no policy permits `create_address`. `details` holds the decision. | Terminal. Show `message`. |
197
+ | `APPROVAL_UNSATISFIABLE` | 409 | `create_address` requires signatures, but fewer people can give them than the threshold asks for (the initiator does not count unless the policy allows approving one's own request). Neither the address nor an approval request was created. Unlike the transfer's, it carries no `details`. | **Do not retry.** An administrator has to change the policy or the approver group. See [Easily confused](#easily-confused). |
198
+ | `APPROVAL_FAILED` | 409 | The approval request could not be created for a reason other than `APPROVAL_UNSATISFIABLE`. See the note under [Declared but never returned](#declared-but-never-returned). | Retry once. If it repeats, it is a server-side fault — escalate. |
197
199
 
198
- 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).)
200
+ 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).) The exception is signatures that cannot be gathered at all: then there is no request and no 202, but `APPROVAL_UNSATISFIABLE` above.
199
201
 
200
202
  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:
201
203
 
@@ -225,7 +227,7 @@ Address shares, folder shares, member-visibility grants. All grant `POST`s are i
225
227
  | `NOT_FOLDER_MANAGER` | 403 | Only the folder's creator changes its contents. | Terminal. Ask the creator. |
226
228
  | `NOT_ADDRESS_OWNER` | 403 | You manage the folder, but the address you are adding is not yours — putting it in a shared folder would widen access to it behind its owner's back. | Terminal, and deliberately so. Only the address's own owner can add it. |
227
229
  | `ALREADY_MEMBER` | 409 | The address is already in the folder. | Benign. Treat as success and refresh. |
228
- | `GROUP_NAME_CONFLICT` | 409 | You already have a folder with that name (names are unique per owner, not per vault). | Ask for a different name. |
230
+ | `GROUP_NAME_CONFLICT` | 409 | Creating a folder, or renaming one (`PATCH`), with a name its owner already uses for another folder (names are unique per owner, not per vault). | Ask for a different name. |
229
231
  | `ACCESS_VIA_GROUP` | 409 | You tried to remove your own access, but it comes from a user group, not a personal grant. `details` has `{groupId, groupName}`. | Explain it with the group name from `details`: the user must leave the group, or an admin must revoke the group's grant. |
230
232
  | `NOT_YOURS` | 403 | You tried to remove someone else's grant via the "remove me" endpoint. | A client bug. Revoking another person's access is the object owner's prerogative. |
231
233
  | `FORBIDDEN` | 403 | Generic refusal when removing an address from a folder. | Terminal. |
@@ -327,18 +329,21 @@ Category keys and field keys and types are **immutable** after creation, and any
327
329
 
328
330
  ## Counterparties
329
331
 
330
- Note the statuses: conflicts in this domain answer **400**, not 409.
332
+ Note the statuses: conflicts in this domain answer **400**, not 409 — all but `ADDRESS_IS_VAULT_ADDRESS`, which is a 409.
331
333
 
332
334
  | Code | Status | When | What to do |
333
335
  |---|---|---|---|
334
336
  | `NAME_TAKEN` | 400 | A counterparty with that name already exists in the vault. | Ask for a different name. |
335
337
  | `ADDRESS_TAKEN` | 400 | One of the addresses already belongs to another counterparty here. `message` names it when it can be resolved. | Show which address collides; the user must remove it from the other counterparty first. |
338
+ | `ADDRESS_IS_VAULT_ADDRESS` | 409 | An address is one of this vault's own addresses on the same network — from `counterparties.create()` with `addresses`, or `counterparties.addAddress()`. It used to be accepted, and then the retro-match tagged past transfers and deposits on that address with the counterparty, matching tagged new ones, and removing the address does not untag them: internal transfers became operations with a counterparty in reports. Checked before anything is written — nothing was created or added. `details` is `{address, network}`: the first such address, trimmed, with the network lowercased. An EVM-shaped address matches regardless of letter case; an address of another vault is accepted. | Terminal, not a retry. Mark the address field from `details` and say it belongs to the vault itself. |
336
339
  | `NETWORK_NOT_REGISTERED` | 400 | An address names a network missing from the registry. `message` lists them. | Terminal for a non-admin: an administrator must register the network. |
337
340
  | `VALIDATION` | 400 | A required field (`name`, `address`, `code`) is empty after trimming. | Normal form validation. |
338
341
  | `CODE_TAKEN` | 400 | A counterparty **group** with that code already exists in the vault. | Ask for a different code. |
339
342
  | `ALREADY_MEMBER` | 400 | The counterparty is already in the group. | Benign. Treat as success and refresh. |
340
343
  | `NOT_FOUND` | 404 | The counterparty or group does not exist in this vault. | Refresh. (Arrives as a status-derived `NOT_FOUND` — see [Declared but never returned](#declared-but-never-returned).) |
341
344
 
345
+ `ADDRESS_IS_VAULT_ADDRESS` arrives under its own code only from those two calls answered directly. When the same check fails inside a chain or in the apply of an approved request, the code is the chain's or the apply's, and the sentence travels in `message`, without `details.address` / `details.network`: a chain that needed no signatures — `counterparties.create()` with `groupId` or `newGroup`, or `approvals.createComposite()` — throws 400 `CHAIN_FAILED_ROLLED_BACK` (`CHAIN_FAILED_PARTIAL` if an earlier step could not be rolled back) with `Step N failed: …` and `details.stepIndex`; an approved request answers the vote with 200 and `applyError.code` `APPLY_FAILED` (a single request, the bare sentence) or `APPLY_FAILED_ROLLED_BACK` / `APPLY_FAILED_PARTIAL` (a composite one, with the `Step N failed:` prefix). The check comes first in the step as well: a `counterparty_update` that also renames the counterparty leaves the name untouched.
346
+
342
347
  Changing a group's membership is scoped to its own vault, and that is checked at the write, including when it arrives via an approved request. A cross-vault attempt fails as a plain 404, not with a distinct code.
343
348
 
344
349
  Group membership also gates destination whitelists: archiving a counterparty group revokes the whitelist it backs, because membership resolves over non-archived groups only. There is no error for this — transfers simply start being denied with `POLICY_DENIED`.
@@ -426,7 +431,7 @@ Rate limiting outside `/api/auth/*` is not implemented in the API itself. A 429
426
431
 
427
432
  ## Easily confused
428
433
 
429
- **`APPROVAL_UNSATISFIABLE` / `UNSATISFIABLE` is not a transient conflict.** It is a 409, and 409s are usually worth a retry. This one never is: it means the policy demands more signatures than there are people able to give them, so every retry fails identically. The only resolution is an administrator changing the policy or the approver group. Say that, and do not offer a retry button. On a transfer, the reserve has already been returned when you see this, so no funds are stuck.
434
+ **`APPROVAL_UNSATISFIABLE` / `UNSATISFIABLE` is not a transient conflict.** It is a 409, and 409s are usually worth a retry. This one never is: it means the policy demands more signatures than there are people able to give them, so every retry fails identically. The only resolution is an administrator changing the policy or the approver group. Say that, and do not offer a retry button. On a transfer, the reserve has already been returned when you see this, so no funds are stuck; on address creation, neither the address nor a request exists.
430
435
 
431
436
  **`TRANSFER_STILL_EXECUTING` means "wait", not "no".** The transfer went into `pending` less than 10 minutes ago and execution may still be in flight. The server refuses on purpose: intervening could cause a **second broadcast**, which is far more expensive than waiting. Do not surface it as a failure, do not loop on it — back off past the window and offer the action again.
432
437
 
@@ -506,4 +511,4 @@ These codes exist on the server and never reach a client. Most are simply never
506
511
 
507
512
  Two further codes are reachable only through server misconfiguration and are worth knowing so you classify them correctly rather than showing them to a user: `DB_REQUIRED` (500, a money path reached without its database binding — fail-closed, nothing is left locked) and `VAULT_HAS_NO_WALLET_SERVICE` / `NO_DYNAMIC_ROUTE` / `TOKEN_NOT_REGISTERED` (500, vault configuration gaps that make transfers or address creation impossible until an administrator fixes them).
508
513
 
509
- **Conditions I could not pin down:** `TRANSFER_NOT_EXECUTABLE` is thrown from several places with three different statuses (404 when the transfer is missing, 409 when it is not `pending`, 500 when its execution context fails to load), so apart from "re-read the transfer and trust its status" there is no single client action that fits all of them. `APPROVAL_FAILED` is the fallback for any approval-request creation failure that is not `UNSATISFIABLE`; which failures reach it is not evident from the code, so "retry once with a new key, then escalate" is the honest guidance rather than a specific cause.
514
+ **Conditions I could not pin down:** `TRANSFER_NOT_EXECUTABLE` is thrown from several places with three different statuses (404 when the transfer is missing, 409 when it is not `pending`, 500 when its execution context fails to load), so apart from "re-read the transfer and trust its status" there is no single client action that fits all of them. `APPROVAL_FAILED` is the fallback for any approval-request creation failure that is not `UNSATISFIABLE`, on a transfer and on address creation; which failures reach it is not evident from the code, so "retry once (on a transfer, with a new key), then escalate" is the honest guidance rather than a specific cause.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v3finance/core-sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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": {
@@ -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: 30be5f9b9733d1d1d67eebf21e0bfaa457651e951676211ec9fa464cca5d3795
12
+ // Spec sha256: 1f2a93f5a31a423e3301b4bfa103ad3db26723285cae060d2e8a904a2b3bffe0
13
13
 
14
14
  export type Audience = 'member' | 'admin' | 'service' | 'both' | 'unknown'
15
15
 
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.2'
2
+ export const SDK_VERSION = '0.2.4'
3
3
 
4
4
  /** Default API base URL. */
5
5
  export const DEFAULT_BASE_URL = 'https://policy-engine-rest.ezig.workers.dev'