wative-core 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +634 -13
  2. package/LICENSE +114 -0
  3. package/README.md +144 -5
  4. package/dist/artifacts/evm/index.cjs +1 -1
  5. package/dist/artifacts/evm/index.d.cts +2 -2
  6. package/dist/artifacts/evm/index.d.ts +2 -2
  7. package/dist/artifacts/evm/index.js +1 -1
  8. package/dist/artifacts/svm/index.cjs +1 -1
  9. package/dist/artifacts/svm/index.d.cts +3 -3
  10. package/dist/artifacts/svm/index.d.ts +3 -3
  11. package/dist/artifacts/svm/index.js +1 -1
  12. package/dist/{container-provider-C59yqd5Z.d.ts → container-provider-D2QluIe_.d.ts} +10 -3
  13. package/dist/{container-provider-i614LDE1.d.cts → container-provider-DAUXclw_.d.cts} +10 -3
  14. package/dist/{evm-BLml33LX.d.cts → evm-GxBuNjq7.d.ts} +2 -2
  15. package/dist/{evm-DTzqGlL6.d.ts → evm-qXdZWugm.d.cts} +2 -2
  16. package/dist/{file-sink-B73MuigK.d.cts → file-sink-B0EjCBvG.d.ts} +8 -5
  17. package/dist/{file-sink-LS4hyXXi.d.ts → file-sink-DSOZbsJM.d.cts} +8 -5
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.cts +25 -8
  20. package/dist/index.d.ts +25 -8
  21. package/dist/index.js +1 -1
  22. package/dist/index.node.cjs +1 -1
  23. package/dist/index.node.d.cts +6 -6
  24. package/dist/index.node.d.ts +6 -6
  25. package/dist/index.node.js +1 -1
  26. package/dist/node/index.cjs +1 -1
  27. package/dist/node/index.d.cts +3 -3
  28. package/dist/node/index.d.ts +3 -3
  29. package/dist/node/index.js +1 -1
  30. package/dist/{svm-DieFbh9k.d.ts → svm-B_c0L-rJ.d.cts} +2 -2
  31. package/dist/{svm-CCzqNAWM.d.cts → svm-Ca4Xyw0Y.d.ts} +2 -2
  32. package/dist/{workspace-CamTHxso.d.cts → workspace-DdaY7sWa.d.cts} +23 -4
  33. package/dist/{workspace-CamTHxso.d.ts → workspace-DdaY7sWa.d.ts} +23 -4
  34. package/examples/15-package-resolution.test.cjs +187 -1
  35. package/examples/18-envelope-v3.test.cjs +106 -0
  36. package/package.json +6 -3
package/CHANGELOG.md CHANGED
@@ -2,32 +2,653 @@
2
2
 
3
3
  All notable changes to `wative-core` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [2.4.0] — 2026-08-11
6
+
7
+ Six breaking items, each one an operation that reported success without having
8
+ done what it said. A broadcast whose reply never arrived was reported as one
9
+ that never went out — on both chains. Moving an account between workspaces saved
10
+ its later changes to the wrong place; changing a password could leave the old one
11
+ working; erasing a browser workspace could leave everything in it; and restoring
12
+ a backup over an existing workspace kept records the backup did not contain. The
13
+ rest either clears secrets from memory sooner, or accepts input that was being
14
+ refused for no reason.
15
+
16
+ ### Security
17
+ - **BREAKING. A broadcast whose outcome is unknown no longer reports as one that
18
+ was refused — on both chains.** When the connection to an endpoint died during
19
+ a broadcast, or the endpoint answered with an error status or something that
20
+ was not a reply, the send reported `failed`: the one status meaning "that never
21
+ went out, send it again". Those bytes may well have reached the network. On
22
+ EVM a rebuilt transaction takes a fresh position from a node whose count the
23
+ relayed one already advanced; on Solana there is no position to collide at all,
24
+ so a rebuild takes a fresh blockhash, becomes a different transaction, and
25
+ nothing dedupes the two. Either way both can land.
26
+
27
+ Such a send now reports `timeout`, meaning the outcome is unknown, and the
28
+ identifier of the transaction that was signed is reported so it can be looked
29
+ up before anything else is sent.
30
+
31
+ An endpoint that answers with a reason it refused still reports `failed`,
32
+ because nothing was sent and sending again is safe. That is the ordinary way a
33
+ send fails — an underpriced or badly-positioned transaction on EVM, an expired
34
+ blockhash or a failed simulation on Solana — and for an expired blockhash
35
+ rebuilding is exactly the right response.
36
+ - **BREAKING. An endpoint that refuses is reported differently from one that
37
+ never answered.** Every way a request could go wrong reported the same thing,
38
+ including the one case where the endpoint read the request and declined it.
39
+ Those are opposite facts for a broadcast, and a caller needs to tell them
40
+ apart to know whether sending again is safe. A refusal now carries its own
41
+ error code and the reason the endpoint gave; a lost connection, an error
42
+ status, or an unreadable reply still report the endpoint as unreachable. The
43
+ two chains previously disagreed about which code a refusal carried; they now
44
+ agree.
45
+ - **A value an endpoint sent is cleaned before it appears in an error.** Two
46
+ errors raised while reading a reply placed that reply into the message exactly
47
+ as received, so control characters chosen by the endpoint were reproduced into
48
+ whatever log the caller writes, and an overlong value was repeated in full.
49
+ The value is still shown, since for a number the rejected text is the
50
+ diagnosis, but it is cleaned and shortened.
51
+
52
+ ### Security
53
+ - **BREAKING. Changing an account's password now reports failure rather than a
54
+ success that did not happen.** If the account was locked between the moment
55
+ the change was requested and the moment it was carried out, the recovery
56
+ phrase was quietly left under the old password while everything else about the
57
+ change was saved. The account then reported that its password had been
58
+ changed, after which the new password failed and the old one still worked —
59
+ the exact reverse of the promise, in the situation you change a password for.
60
+ An account managed by its workspace was additionally recorded as having a
61
+ password of its own, which made it unopenable the next time it was loaded.
62
+ Two changes started at the same time could also both report success, with the
63
+ second overwriting the first using a password the first had already retired.
64
+ Both cases now report failure and leave the account exactly as it was.
65
+ - **BREAKING. Erasing a browser workspace now reports whether it happened.**
66
+ Erasing one while the same application still had it open reported success and
67
+ left every stored record in the browser. Because nothing released the
68
+ workspace, that was the ordinary outcome rather than a rare one. The workspace
69
+ is now closed first so the erase can finish. An erase that cannot be confirmed
70
+ is reported as unconfirmed rather than as failed — it may still complete on its
71
+ own, and erasing again is always safe. Erasing where the browser offers no
72
+ storage at all now reports that, instead of returning quietly.
73
+ - **Secrets are cleared from memory once they are no longer needed.** Several
74
+ intermediate values produced while deriving a key were left behind after use,
75
+ and a derived key dropped from the internal cache was discarded without being
76
+ cleared, putting it beyond the reach of the cleanup that runs when a workspace
77
+ locks. Unlocking an account whose recovery phrase could not be read also left
78
+ that phrase in memory although the account reported itself locked. Keys and
79
+ addresses are unchanged: derivation was checked byte for byte against the
80
+ previous release and against the published Argon2 test vectors.
81
+ - **An endpoint that is not text is refused rather than saved unchecked.**
82
+ `rpcUrl` was the only network field with no type check, so a value that was not
83
+ text skipped every endpoint check — including the one that refuses addresses on
84
+ your local network — and was stored anyway. Leaving the field out reported an
85
+ error from outside this library rather than its own, including while loading a
86
+ stored network. An empty endpoint still means no endpoint configured.
87
+ - **A rejected name or colour is quoted before it is reported.** Both were placed
88
+ into the error message exactly as supplied, so control characters in them were
89
+ reproduced into whatever log you write. A name is not always your own — it can
90
+ come from a filename on disk, or from a backup being restored.
91
+
92
+ ### Fixed
93
+ - **BREAKING. An account moved to another workspace now belongs to the workspace
94
+ holding it.** Moving one wrote a single record into the receiving workspace and
95
+ then kept sending every later change back to the workspace it came from.
96
+ Reopening the receiving workspace showed the account without those changes, and
97
+ closing the original one made the account unusable even though the receiving
98
+ workspace was open and still listed it. For an imported private key, discarding
99
+ the original workspace lost the key. The account now saves to the workspace
100
+ holding it, unlocks with that workspace's password, and is closed by it.
101
+ Locking the receiving workspace while the move is still going on now closes the
102
+ account rather than leaving it open and unreachable. Re-deleting an account
103
+ that was moved in reports that it is already gone, rather than reporting it as
104
+ belonging elsewhere.
105
+ - **BREAKING. Restoring a backup over an existing workspace now replaces it.**
106
+ Records the backup did not contain were left in place beside the restored ones,
107
+ still under the previous password. They could not be opened, could not be seen,
108
+ kept their names reserved, and made removing a network fail from then on. Only
109
+ what the backup carries is kept now, and anything it omits is removed. A backup
110
+ that cannot be restored in full is refused before anything changes, and a
111
+ backup carrying no configuration is refused outright rather than leaving a
112
+ workspace no password can open. The workspace is closed at the end, since the
113
+ password that opened it belonged to the workspace that was just replaced.
114
+ - **A wallet can be labelled in your own script.** Tags and token symbols
115
+ accepted the letters of every script but refused the marks that most of them
116
+ require, so Devanagari, Bengali, Tamil, Thai, Arabic and Hebrew could not be
117
+ used at all. They now are. A label that would display as blank, or as nothing
118
+ at all, is still refused. Token symbols containing `+` are also accepted, which
119
+ several real tokens need.
120
+ - **The two spellings of one password are one password.** The same password can
121
+ arrive in two forms depending on the keyboard, clipboard or operating system it
122
+ came from, and both have always unlocked a wallet. Several places compared the
123
+ forms directly instead, so a correct password could be reported as wrong when
124
+ re-entered on an already-open workspace or account, when confirming a record's
125
+ password, and when creating an account that shares the workspace password.
126
+ Checking a new password against previous ones now recognises the same password
127
+ in a different form as a reuse.
128
+ - **A deleted account's name can be used again.** Every account name seen when a
129
+ workspace opened was reserved for as long as it stayed open, and deleting the
130
+ account did not release it. Creating an account under that name again quietly
131
+ produced a numbered variant, and adding one back was refused with a message
132
+ asking you to restore a file that had just been deleted.
133
+ - **A token address pasted with a leading space is accepted.** It was refused
134
+ here while the same address was accepted everywhere else in the library.
135
+ - **A hex value carrying two prefixes is refused rather than decoded.**
136
+ `0x0x41` produced two bytes with a zero in front instead of an error.
137
+ - **Moving an account needs the networks it uses to exist in the receiving
138
+ workspace.** An account refers to its networks by name, so moving one into a
139
+ workspace that does not have a network it uses wrote a record that could not
140
+ be read back. The move reported success, and after reopening the account was
141
+ simply absent — not listed, not reported as held back, and not even holding
142
+ its name, while a record containing its recovery phrase and every key stayed
143
+ in the workspace, unreachable and impossible to remove. The move is now
144
+ refused before anything is written, naming the missing network.
145
+ - **An account cannot be left holding no keys.** Both ways of removing wallets
146
+ checked that at least one wallet remained, which does not mean at least one
147
+ key remains: a wallet can legitimately be added without addresses, so removing
148
+ the wallet carrying an account's only keys left an account with none. It still
149
+ reported itself as an account, and reopening it produced a shell with nothing
150
+ in it. Removing wallets that hold no keys is unaffected, as is removing an
151
+ account outright.
152
+
153
+ ## [2.3.9] — 2026-08-10
154
+
155
+ Three breaking items. Two are on paths that were producing results no
156
+ counterparty could act on; the third changes how a refused broadcast is
157
+ reported, on both chains, so that it cannot be read as "safe to send again". Everything else here narrows what error messages reveal, or
158
+ accepts input that was being refused for no reason.
159
+
160
+ ### Security
161
+ - **BREAKING. The endpoint-reply check now covers Solana.** 2.3.6 stopped an
162
+ endpoint choosing which transaction was watched after a broadcast, but that
163
+ check only ever applied to EVM sends — the Solana lane still took the reply at
164
+ face value, so an endpoint could hand back an identifier of its own and report
165
+ results for a transaction you never sent. That reply is now checked against the
166
+ transaction that was actually signed, and everything afterwards follows the
167
+ signed value. A send refused
168
+ for this reason reports `timeout`, not `failed`, because the endpoint did
169
+ acknowledge it and the transaction may well have been relayed — look it up by
170
+ its signed signature before sending anything else, and do not rebuild it with a
171
+ fresh blockhash. The signed identifier is still reported, so it can be looked
172
+ up. Endpoints that answer correctly are unaffected.
173
+ - **BREAKING. A refused broadcast reports `timeout`, not `failed`, on EVM too.**
174
+ The same check on the EVM side already refused an endpoint that acknowledged a
175
+ broadcast with the wrong identifier, but reported the result as `failed` — the
176
+ one status a caller reads as "that never went out, send it again". The
177
+ endpoint did acknowledge it, so those bytes may be on the network: a retry can
178
+ then take a fresh position and both transactions land. It now reports
179
+ `timeout`, meaning the outcome is unknown, and the error still names both
180
+ identifiers. The transaction continues to report the identifier it signed, and
181
+ awaiting its submission now fails with that same explanation rather than
182
+ reporting that it was never sent.
183
+ - **An RPC URL missing its scheme was handed back with one attached.** Provider
184
+ dashboards present an endpoint host-first with the key in the path and no
185
+ scheme. Pasting that produced an error that repeated the whole value with
186
+ `https://` prefixed — a ready-to-use secret URL, in a message that can reach a
187
+ log file or a crash reporter. Both refusals for an unparseable RPC URL now say
188
+ what is wrong without repeating any of it. A rejected scheme and a blocked
189
+ host are still named, since those are not the caller's secret.
190
+ - **A value rejected as a malformed number is no longer shown in full.** This
191
+ completes the sweep 2.3.8 began, which was scoped to address fields and noted
192
+ that amounts and gas prices were still shown whole — that note no longer
193
+ applies. A private key or recovery phrase pasted into an amount, a gas price or
194
+ a gas limit used to appear in the error in its entirety. Such a value is now
195
+ shortened. It is shortened rather than removed because for a number the
196
+ rejected text is the diagnosis: a thousands separator, a stray decimal point or
197
+ an exponent is still perfectly legible.
198
+ - **The last two address errors stopped repeating their value.** A token
199
+ contract address that is not a valid address, and a network's multicall
200
+ address, were the two sites the 2.3.8 sweep left. Adding a custom token by
201
+ contract address is something an end user does by pasting, and the same slip
202
+ applies. Both now name the field instead of showing its contents.
203
+
204
+ ### Fixed
205
+ - **Malformed typed data, measured.** Of twenty-four malformed payload shapes,
206
+ twenty-two are now refused with this library's own error code and none escapes
207
+ as an untyped error. The remaining two — a domain given as an array or a
208
+ number — are signed deliberately, because every other implementation signs
209
+ them identically and refusing them would break real callers.
210
+ - **BREAKING. A field whose declared type is not a type at all is now refused.**
211
+ A type is a name followed by zero or more array suffixes and nothing else.
212
+ Anything trailing the suffix — `uint256[]extra`, `uint256[2]junk` — or an
213
+ unclosed bracket such as `uint256[` was accepted, and produced a signature no
214
+ counterparty could check. Such a payload now reports
215
+ `PARAMETER_ERROR` naming the type, which is what other implementations already
216
+ do. Every valid type signs exactly as before, including nested and fixed-size
217
+ arrays such as `uint256[2][3]`, a zero-padded length such as `uint256[01]`, and
218
+ the shorthands `uint` and `int`.
219
+ - **An amount too large to exist is refused when you set it, not when you
220
+ sign.** Every numeric field of an EVM transaction holds a 256-bit value, and
221
+ a larger one was accepted and carried all the way to signing, which then
222
+ failed with a message naming neither the field nor the reason. Such a value
223
+ is now refused immediately and by name. The largest legal value is still
224
+ accepted, and nothing was ever silently altered.
225
+ - **A wallet's address list can no longer be rewritten in place.** It is
226
+ declared read-only and, unlike an account's wallet list, nothing enforced
227
+ that: assigning an entry, truncating it, pushing to it or deleting from it
228
+ were all accepted, and the next ordinary save wrote the result through. Both
229
+ lists now refuse those, and both also refuse being frozen or having their
230
+ prototype changed — neither of which writes anything itself, but both of which
231
+ make a later legitimate change fail from somewhere the caller cannot connect
232
+ to what they did. Reading, iterating and the collections' own methods are
233
+ unchanged.
234
+ - **More invisible characters are removed from names.** Free-text names — a
235
+ wallet's display name, an asset or network name — are cleaned of characters
236
+ that carry no glyph before anything else looks at them, so nothing can hide
237
+ inside a word. Five that were missing are now included, among them the
238
+ directional isolates, which belong to the same family as the overrides that
239
+ were already covered. Emoji are untouched.
240
+ - **A malformed Solana address is reported the same way wherever it is given.**
241
+ Depending on which call it came through, the same mistake produced either this
242
+ library's own `PARAMETER_ERROR` or a bare decoding error with no code — and in
243
+ one case nothing at all until much later, from a place that could no longer
244
+ say which address was at fault. Every one of them now reports
245
+ `PARAMETER_ERROR` and names the field. Nothing that built before is refused.
246
+ - **A typed-data payload that declares `EIP712Domain` as null no longer throws a
247
+ raw error.** Naming that key as a field's type, with the key itself set to
248
+ `null` or `undefined`, produced a `TypeError` from inside the encoder instead
249
+ of a `PARAMETER_ERROR`. Leaving the key present but empty while nothing
250
+ references it is a normal payload and still signs, byte for byte as before.
251
+ - **A Solana fee payer naming the sending account is no longer refused.**
252
+ Signing rejected any fee payer that was not the exact same string as the
253
+ sender, so supplying the sender's own key as a public-key object or as raw key
254
+ bytes — both of which build correctly for external signing — was turned away,
255
+ as was `null`. All three now sign, and describe a transaction identical to
256
+ leaving the field out. A fee payer naming a genuinely different account is
257
+ still refused, and is still available for external signing.
258
+
259
+ ## [2.3.8] — 2026-08-09
260
+
261
+ Typed-data signing (`signTypedData`) is stricter about payloads that were
262
+ producing signatures no counterparty can verify. If you sign typed data, read
263
+ the three breaking items below. Nothing else here changes what is accepted.
264
+
265
+ ### Security
266
+ - **Four address errors no longer repeat the value they rejected.** A
267
+ destination that is not a valid address, a malformed Solana recipient, a
268
+ Solana fee payer that differs from the sender, and a transaction whose sender
269
+ does not match the address signing it: each used to include the value it had
270
+ just refused. Pasting a private key into a destination field is an easy slip,
271
+ and the message could then reach anything that records errors — a log file, a
272
+ crash reporter, a support ticket. Each now names the field that was wrong
273
+ instead of showing its contents. An address that IS valid is still shown, so
274
+ a checksum typo remains easy to spot. Note this covers address fields: a
275
+ value rejected as a malformed *number* — an amount, a gas price — is still
276
+ shown in full.
277
+ - **BREAKING. A domain carrying an unrecognised field is now refused.** Only
278
+ `name`, `version`, `chainId`, `verifyingContract` and `salt` are part of an
279
+ EIP-712 domain. A misspelled or mis-cased field — `chainID` with a capital D,
280
+ or keys lower-cased by a JSON round-trip — was previously dropped in silence,
281
+ and the signature was then bound to no chain and no contract at all. Such a
282
+ payload is refused rather than signed. A domain with no fields still signs,
283
+ unchanged.
284
+
285
+ ### Fixed
286
+ - **BREAKING. A field typed `uint` or `int` now signs as `uint256`/`int256`.**
287
+ These are the same types under a shorter name and every other implementation
288
+ treats them that way; this one did not, so the signature verified as a
289
+ different address than the actual signer, with no error anywhere. Any
290
+ signature previously produced for such a field changes — the new one is the
291
+ one a counterparty accepts. Fields with an explicit width are unaffected.
292
+ - **BREAKING. A fixed-size array field must hold exactly its declared number of
293
+ values.** `uint256[2]` given three values used to sign something no verifier
294
+ would evaluate; it is now refused.
295
+ - **Malformed typed data reports this library's error.** Bad values in `message`
296
+ — a missing or `null` nested struct, a number out of range for its declared
297
+ width, a malformed address or byte length, an unreadable field — now report
298
+ `PARAMETER_ERROR` naming the field, instead of escaping as a plain error with
299
+ no code. Payloads that signed before still sign, and produce the same
300
+ signature; only the error changed.
301
+ - **A malformed recipient on a native SOL transfer reports this library's
302
+ error.** Such an address was always rejected, but by an underlying library, as
303
+ a plain error carrying no code — so it could not be handled alongside every
304
+ other failure. It now reports `PARAMETER_ERROR` naming the field, and says
305
+ whether the address is not valid base58 or is the wrong length. Every address
306
+ that built before still builds; only the error changed.
307
+
308
+ ## [2.3.7] — 2026-08-08
309
+
310
+ A security and correctness release. Upgrade if you display names your users
311
+ typed, import Solana keys, send Solana transactions, or rely on the password
312
+ strength report.
313
+
314
+ ### Security
315
+ - **A hidden character could smuggle a dangerous link into a display name.**
316
+ Names, tags and other free-form text are cleaned before storage, and that
317
+ cleaning removed dangerous prefixes like `javascript:` before it removed
318
+ zero-width and control characters. A single invisible character placed inside
319
+ such a prefix therefore survived the check and was then tidied away, handing
320
+ back exactly the thing that was supposed to have been removed. Anything
321
+ putting a stored name on a page received a working link. All affected
322
+ prefixes and every invisible-character class are covered.
323
+ - **The password strength report described a different password than the one in
324
+ use.** Passwords are normalised before the key is derived, but the strength
325
+ check read the text exactly as typed. A password written in decomposed form —
326
+ ordinary for some keyboards and languages — was reported as longer and
327
+ stronger than the secret actually protecting the wallet, and could pass a
328
+ minimum-length rule the real one fails. The report now measures what is
329
+ actually used. Nothing about existing wallets or how keys are derived changes,
330
+ and the warning about passwords that shift form between machines is unchanged.
331
+
332
+ ### Fixed
333
+ - **A malformed Solana private key is now refused in this library's terms.**
334
+ Such a key was always rejected, but by an underlying library, as a plain error
335
+ carrying no error code — so it could not be handled alongside every other
336
+ failure. It now reports `INVALID_PRIVATE_KEY`, matching what the EVM side
337
+ already did.
338
+ - **A rejected logging change no longer takes effect anyway.** Changing sinks or
339
+ the log level while the workspace is locked correctly fails — but the new
340
+ settings were applied before that failure, leaving the previous sinks closed
341
+ and, for file logging, records being written under a configuration you were
342
+ told had been rejected. Nothing changes now unless the change succeeds.
343
+ - **Closing RPC clients now closes all of them.** Solana connections were left
344
+ open, each holding a network connection and two timers, once per endpoint used
345
+ for the lifetime of the process.
346
+ - **An expired Solana transaction now says so.** Previously it was polled for the
347
+ full two minutes and then reported as a timeout — the same answer given for a
348
+ slow node, though the two call for opposite responses. A timeout means the
349
+ outcome is unknown and sending another transaction risks paying twice; an
350
+ expired one can never be accepted, so replacing it is safe. It now ends as
351
+ dropped, saying the blockhash expired.
352
+ - **Wallet lookup by tag is no longer case-sensitive**, matching how the same
353
+ method already looked up accounts and addresses. Tags keep the capitalisation
354
+ you gave them.
355
+
356
+ ### Added
357
+ - **`workspace.damagedAccountSlugs`** lists account records that are present but
358
+ could not be opened. Such a record is deliberately kept rather than discarded,
359
+ and creating an account with the same name will not overwrite it — but until
360
+ now nothing reported that it existed, and the only visible sign was new
361
+ accounts receiving unexpected names. The list is read-only: removing such a
362
+ record is left to you, because it may still hold a recoverable recovery
363
+ phrase.
364
+
365
+ ## [2.3.6] — 2026-08-08
366
+
367
+ A correctness release for anyone who sends transactions. Several ways a
368
+ transaction could do something other than what you asked, or be reported as
369
+ something it was not, all of them silent.
370
+
371
+ ### Security
372
+ - **A redirect could carry your RPC request to a host you never configured.**
373
+ The RPC address you set was checked, but a redirect from that address was
374
+ followed automatically — and the kinds that preserve the request body would
375
+ re-send the whole call somewhere else, including addresses the check exists to
376
+ block. Redirects are no longer followed, and one is now reported as a
377
+ redirect, naming where it was being sent, rather than as an unreachable node.
378
+ - **The transaction being watched was chosen by the endpoint.** After a
379
+ broadcast, the identifier the endpoint replied with was taken at face value
380
+ and everything afterwards followed it — so an endpoint could hand back an
381
+ identifier of its own and report a result for a transaction you never sent,
382
+ while yours went unwatched. That reply is now checked against the transaction
383
+ that was actually signed. An endpoint that does not acknowledge a broadcast
384
+ still reports no identifier, exactly as before.
385
+ (Scope correction, added in 2.3.9: this covered EVM sends only. The wording
386
+ above did not say so, and the Solana lane kept taking the reply at face value
387
+ until 2.3.9.)
388
+ - **A mistyped token contract address was silently corrected.** The same
389
+ checksum repair that was fixed for transaction destinations in 2.3.5 also
390
+ applied where you supply a token's contract address. It is now refused.
391
+ Addresses given entirely in lower or upper case are unaffected.
392
+
393
+ ### Fixed
394
+ - **Call data with an odd number of digits changed the function being called.**
395
+ It was padded on the left, which shifts every byte — turning a token transfer
396
+ into a call to something else, with the arguments shifted too. The
397
+ transaction signed, sent and mined perfectly well; it simply did something
398
+ else. Such call data is now refused, because what was intended cannot be
399
+ known.
400
+ - **An automatic nonce could be handed out more than once.** The nonce was
401
+ requested from the node every time and nothing recorded what had already been
402
+ used, so transactions prepared before the node caught up all received the same
403
+ one. Sending three transfers in a row let one arrive while the rest could
404
+ never be mined, and all three reported as sent. A nonce you supply yourself is
405
+ still honoured exactly as given, so replacing an in-flight transaction with a
406
+ higher fee works as before.
407
+ - **A mined transaction could be reported as a failed send.** One unreadable
408
+ field in an otherwise successful receipt — the gas figures — was enough to
409
+ report the whole send as failed and to discard the receipt proving the funds
410
+ moved. Those figures are now read tolerantly and simply omitted when they
411
+ cannot be read.
412
+ - **An address will not sign for one chain and send to another.** An address and
413
+ the network it is pointed at could disagree about which chain they belong to;
414
+ signing now refuses rather than producing a signature for one chain and
415
+ handing it to another.
416
+ - **A signed transaction's fields can no longer be changed.** Editing a field
417
+ after signing was accepted and reported back, while the bytes that would be
418
+ sent still carried the original values. This covers both EVM and Solana
419
+ transactions. Build a new transaction to change something.
420
+ - **A wallet list handed to you can no longer be modified in place.** Assigning
421
+ to it changed the account's own list, and the next save wrote that through.
422
+ - **A dropped network no longer comes back.** Saving a network after removing it
423
+ added it again, in memory and on disk.
424
+ - **A blank address filter no longer matches the first address**, and passing
425
+ something that is not an account now reports a parameter error rather than an
426
+ unrelated type error.
427
+ - **Creating an account no longer overwrites one another handle just created.**
428
+ Two handles open on the same workspace could each create an account with the
429
+ same name, leaving one record holding the second recovery phrase and
430
+ destroying the first.
431
+
432
+ ## [2.3.5] — 2026-08-07
433
+
434
+ A security release. Upgrade if you send EVM transactions, or if you keep more
435
+ than one account in a workspace.
436
+
437
+ ### Security
438
+ - **A mistyped destination address was silently corrected.** An EVM address
439
+ carries a checksum whose whole purpose is to catch a mistyped character. When
440
+ a transaction destination arrived with a checksum that did not match, it was
441
+ quietly rewritten into a valid-looking address rather than refused — so a
442
+ single wrong character could send funds to an address you never typed, with no
443
+ warning. Such a destination is now rejected. Addresses given entirely in lower
444
+ or upper case carry no checksum to check and are unaffected, and address
445
+ lookup stays as forgiving as before.
446
+ - **An account could overwrite another account's stored secrets.** On macOS and
447
+ Windows, where file names ignore case, creating an account could write over a
448
+ stored account whose name differed only in capitalisation — destroying the
449
+ sealed recovery phrase it held. Names are now checked the way the file system
450
+ actually compares them.
451
+ - **Two addresses reached cloud metadata services.** The RPC URL check missed one
452
+ IPv6 spelling of the metadata address, and the well-known metadata host names
453
+ entirely.
454
+ - **Sealed material no longer appears when an object is logged.** The stored
455
+ recovery phrase and stored keys were ordinary visible properties, so they
456
+ surfaced in `console.log` output and in anything that walks an object's
457
+ properties.
458
+ - **A password policy option set to `undefined` no longer disables it.** Building
459
+ an options object from optional values — the ordinary way to forward
460
+ settings — removed the minimum-length rule entirely, so short passwords stopped
461
+ being reported.
462
+
463
+ ### Fixed
464
+ - **`confirmed` no longer fires for a reverted transaction.** Subscribers were
465
+ told a transaction had confirmed, receiving a receipt whose own success flag
466
+ was false, and only afterwards told it failed.
467
+ - **A stray file no longer blocks removing a network.** A single duplicate left
468
+ in the storage folder — the kind a file manager creates — made network removal
469
+ refuse permanently.
470
+ - **A log file prefix can no longer choose the directory.** A prefix containing
471
+ path separators wrote the log outside the folder it was given. Sizes and file
472
+ counts are validated too.
473
+ - **Aborting a transaction cannot leave a stale finish behind.** Internal
474
+ finishing can no longer be triggered while a transaction is still in flight.
475
+ - **Key material is cleared in three more places** where an unusual failure —
476
+ one bad address, an interrupted derivation, a rolled-back add — previously left
477
+ it in memory.
478
+
479
+ ## [2.3.4] — 2026-08-07
480
+
481
+ A security release. Upgrade if you hold keys in a long-running process, or if
482
+ you log anything from a Solana wallet.
483
+
484
+ ### Security
485
+ - **Key material could survive `lock()`.** Two separate cases. Records sealed by
486
+ the optional faster storage format left one readable copy of each record's key
487
+ in memory after the workspace was locked — and because that format seals each
488
+ secret the same way, such a copy is the secret itself. Separately, part of the
489
+ password-derivation scratch space was not being cleared, leaving
490
+ password-derived bytes resident. Both are now cleared, and both are covered by
491
+ checks that can actually observe the memory in question.
492
+ - **`lock()` could return while an account stayed unlocked.** An `unlock` that
493
+ landed at the same moment as a `lock` could complete inside it: the workspace
494
+ reported locked while the account still held its mnemonic and could sign, and
495
+ no later `lock()` could reach it. Locking is now closed to that race.
496
+
497
+ ### Fixed
498
+ - **Signing typed data with a `bytes` field.** A `Uint8Array` or `Buffer` was
499
+ hashed as text rather than as its bytes, so the signature did not match what
500
+ any other implementation computes — and two containers holding the same bytes
501
+ gave two different results. Passing a `0x` string was, and remains, correct.
502
+ Text that is not `0x`-hex is now rejected rather than silently hashed a
503
+ second, incompatible way.
504
+ - **Malformed typed data now raises the library's own error.** Several shapes —
505
+ including a `primaryType` naming a built-in type, and a struct that contains
506
+ itself — escaped as a raw `TypeError` with no error code. (This entry
507
+ originally gave a count. It was not reproducible against the released build
508
+ and has been replaced by the property it was trying to describe; the current
509
+ coverage is stated under 2.3.9.)
510
+ - **Solana signatures and transaction ids are no longer removed from logs.** The
511
+ redactor treated any long base58 value as a secret, which is exactly the shape
512
+ of a signature, so the one value worth correlating on disappeared silently.
513
+ Redaction by field NAME was broadened at the same time, so tokens, passcodes
514
+ and recovery keys that previously slipped through are now covered.
515
+ - **Logging a non-string no longer throws.** `logger.info(x)` where `x` was
516
+ undefined, a number or an object took down the caller.
517
+ - **Adopting an account into another workspace** now refuses when that
518
+ workspace's password would not open it, instead of writing a record that can
519
+ never be opened again.
520
+ - **Adding an account no longer loses concurrent work.** Wallets derived while
521
+ the account was being added were kept in memory but never written.
522
+
523
+ ## [2.3.3] — 2026-08-07
524
+
525
+ A security fix for typed-data signing. Upgrade if you use `signTypedData`.
526
+
527
+ ### Security
528
+ - **A signature could be produced for a value other than the one you were shown.**
529
+ When a typed-data payload declared a custom type whose name collides with one of
530
+ EIP-712's own built-in type names, a field of that type was not bound by the
531
+ signature: every value produced the same result, and an independent verifier
532
+ read the field as a fixed value nobody chose. The same collision could also
533
+ detach a signature from the chain and contract it was meant for. Because the
534
+ payload is normally supplied by the site requesting the signature, this did not
535
+ require anything unusual from the wallet holder. Such payloads are now rejected
536
+ with `PARAMETER_ERROR`, which is also what other EIP-712 implementations do, so
537
+ no payload that they accept is affected.
538
+
539
+ ### Fixed
540
+ - **A retired wallet index could come back.** Dropping a wallet records its index
541
+ so it is never derived again. If another account setting was changed at the same
542
+ moment, that record could be lost, and the next `deriveWallets()` returned the
543
+ same address and private key as the wallet that was dropped.
544
+ - **`sliceWallets` could empty an account.** It kept no minimum, so on an account
545
+ whose first wallet had already been dropped it could remove every remaining
546
+ wallet, leaving an account with no keys and reporting no error. It now refuses,
547
+ as `wallet.drop()` already did.
548
+ - **Aborting a transaction no longer reports a confirmation afterwards.**
549
+ `abort()` could still fire `confirmed` listeners for a receipt that arrived
550
+ after the abort, having already reported the failure. The receipt itself is
551
+ still kept on the tracker — if the transaction did land, you can still see it —
552
+ but it is no longer announced as a confirmation, and the abort remains the
553
+ reported cause.
554
+ - **Secrets are cleared in two cases where they were kept.** A wallet drop, or a
555
+ password reset, that failed because the workspace was locked at that moment
556
+ could leave key material in memory that a later `lock()` would not reach.
557
+
558
+ ### Changed
559
+ - ⚠️ **`whenFinalized()` now rejects instead of never settling.** Transaction
560
+ tracking ends at first inclusion, so on EVM — and on Solana whenever the node
561
+ reports `confirmed` first — this promise previously neither resolved nor
562
+ rejected. It now rejects with `UNSUPPORTED_OP`. A Solana node that reports
563
+ finality directly still resolves as before.
564
+
565
+ **Migration.** `void tracker.whenFinalized().then(...)` used to do nothing,
566
+ because the promise never settled; it is now an unhandled rejection and will
567
+ terminate a Node process. Attach a `.catch()`, or await it. Awaiting it
568
+ alongside `whenConfirmed()` is unaffected.
569
+ - **A `types.EIP712Domain` list that disagrees with your domain is now rejected**
570
+ rather than ignored. The domain separator has always been computed from the
571
+ domain object itself, so a mismatched list previously produced a signature that
572
+ your own verifier would reject, with nothing to point at the cause. An empty or
573
+ absent list still works, so the JSON-RPC shape is unaffected.
574
+
575
+ - ⚠️ **Documented late — this shipped in 2.3.3 without a note.** `record.unlock(password)`
576
+ on a record that is already open now re-checks the password instead of returning
577
+ success. Records arrive already open in the common case, so a caller using
578
+ `record.unlock(pw)` as a password check was previously told yes for **every**
579
+ string, including a wrong one. It also refuses once the workspace has been locked
580
+ or the record dropped, where a retained handle used to keep answering password
581
+ guesses indefinitely.
582
+
583
+ **Migration.** If you relied on `unlock()` succeeding for an already-open record,
584
+ pass the real password. Reading `.value` is unaffected — that is a snapshot you
585
+ already hold, and it is deliberately not gated.
586
+
587
+ ## [2.3.2]
588
+
589
+ Not released.
590
+
591
+ ## [2.3.1] — 2026-08-07
592
+
593
+ A types-only fix. Six types that appear in the public API could not be imported,
594
+ so TypeScript users could call a method but not write down what it took or
595
+ returned. Nothing changed at runtime, and JavaScript users are unaffected.
596
+
597
+ ### Fixed
598
+ - `OpenOptions` (the options bag for `Workspace.open()`), `PasswordCheckContext`,
599
+ `NetworkConfig`, `AbiItem`, `SplTokenArgs` and `SplTokenAccountSet` are now
600
+ exported and can be imported by name.
601
+
602
+ ## [2.3.0] — 2026-08-06
603
+
604
+ Unlocking is much faster. Drop-in from 2.2.x; wallets from 1.x need a manual step first.
605
+
606
+ ### Added
607
+ - An optional storage format that keeps unlocking fast however many addresses an account holds.
608
+
609
+ ### Changed
610
+ - Unlocking is roughly 25x faster. In a browser, allow `'wasm-unsafe-eval'` in your Content-Security-Policy, or it quietly falls back to a much slower path.
611
+
612
+ ### Removed
613
+ - Wallets created by 1.x can no longer be opened — export their secrets with 2.2.x before upgrading.
614
+
5
615
  ## [2.2.1] — 2026-07-29
6
616
 
7
- Documentation and packaging only — the library code is identical to 2.2.0.
617
+ Documentation and packaging only. `dist/` is byte-identical to 2.2.0 — no source changed, so the published build was deliberately left exactly as built and verified for that release.
8
618
 
9
619
  ### Changed
10
- - The runnable examples moved from `tests/` to `examples/`; update any path references.
11
- - Clearer README: how Workspace, Account, Wallet and Address relate, and when to use a recovery-phrase account versus an imported-key one.
620
+ - **The runnable examples ship as `examples/` instead of `tests/`.** They are the only human-readable code in the published package, and `tests/` reads as internal scaffolding nobody is invited to open. The `files` allowlist follows. Consumers who referenced `wative-core/tests/...` by path must update; nothing importable from the package entry changed.
621
+ - **README** gains npm / CI / node / license badges, an ASCII diagram of the containment model (`Workspace > Account > Wallet > Address`), and a section distinguishing the two account kinds. The HD/PK difference is not in the nesting but in what a `Wallet` *means* — a derivation slot holding both an EVM and a Solana key under HD, versus a single imported key on one chain under PK. That was previously described in prose only, and it is the thing new users get wrong first.
12
622
 
13
623
  ### Fixed
14
- - One example failed when run from an installed copy.
624
+ - **`11-subpath-imports` example** imported the chain artifacts by relative path into `dist/` rather than by subpath, despite its own header documenting the subpath form. It resolved inside the repo and nowhere else, so it passed in-repo while being broken for every real installation. Caught by the new published-package CI job before release.
625
+
626
+ ### Internal
627
+ - `.github/workflows/ci.yml`: runs the examples against this build on Node 22.12 and 24, and separately installs `wative-core` from the registry into a clean directory and runs the same files against it — exercising the real tarball's `files` list, `exports` map and dependency resolution. Runs weekly as well, since a published package sits on a moving dependency graph.
628
+ - The private development repo is now guarded against reaching npm (`private`, a `prepublishOnly` refusal, and a `publishConfig` registry pointing nowhere). It shares a package name with the public repo, so a stray publish there would have succeeded and replaced the package.
15
629
 
16
630
  ## [2.2.0] — 2026-07-28
17
631
 
18
- Runs in the browser now. Existing Node code keeps working unchanged.
632
+ > **The browser entry is here.** The main entry now resolves for a browser with **zero** unresolvable Node builtins, and ships an IndexedDB storage backend. **No public API was removed** — existing Node imports keep working unchanged, so this remains a minor.
19
633
 
20
634
  ### Added
21
- - **Browser support.** The library works in a web app, keeping wallets in the browser's own storage. See the browser example in the Quick start.
22
- - **Backup and restore.** Export a workspace as encrypted data and load it somewhere else. A workspace created in a browser opens on a desktop, and the other way round.
23
- - **Writing your own storage is simpler.** A custom backend now needs six small methods; encryption and file layout are handled for you.
635
+ - **`IdbProvider` — IndexedDB storage backend.** Created via the async factory `IdbProvider.create(name, opts)`, which negotiates durable storage before returning. Exposes `durability` (`"persistent" | "best-effort"`) and a `quota` snapshot.
636
+ - **`ContainerProvider`** — the storage-agnostic base extracted from `HybridProvider`. A custom backend now implements only six primitives (`_exist`, `_listItems`, `_read`, `_write`, `_remove`, `_ensureDir`) and inherits record framing, the encrypted envelope and its identity AAD, the key layout and the container session. Previously `Provider` declared the record API abstract, so a custom provider had to reimplement encryption — which is why the shipped custom-provider example carried a hand-rolled XOR cipher and a warning to use a real one.
637
+ - **`exportContainer()` / `importContainer()`** on `ContainerProvider`. Both require an unlocked workspace and throw `WORKSPACE_LOCKED` otherwise — a precondition that was not stated when they shipped. Sealed records move verbatim — never decrypted, so no key is materialized and nothing is re-keyed. Because every backend shares one framing, a container round-trips IndexedDB ⇄ filesystem and opens with the same password. Import refuses a non-empty target without `{ overwrite: true }` and rejects entries outside the container layout.
638
+ - **`wative-core/node` subpath** — `HybridProvider`, `FileSink` and the default-workspace-path resolver.
639
+ - **`STORAGE_NOT_DURABLE`** error code.
24
640
 
25
641
  ### Changed
26
- - `HybridProvider` and `FileSink` are still available from the main import in Node. They also live at `wative-core/node`, which is where new code should get them.
642
+ - **The root entry is condition-split.** A browser bundler resolves the universal core; Node resolves an entry that still re-exports `HybridProvider` and `FileSink`, so `import { Workspace, HybridProvider } from "wative-core"` compiles and runs exactly as before. Those two re-exports are **deprecated** in favour of `wative-core/node` and will be removed in a future major. Condition order is significant — `browser` is declared before `node`, since resolvers take the first match.
643
+ - `tsconfig` `lib` gains `DOM`; the browser is now a real target.
644
+
645
+ ### Browser notes
646
+ - Creating a **new** workspace in evictable storage is **refused** with `STORAGE_NOT_DURABLE` unless `{ acknowledgeEvictionRisk: true }` is passed. Opening an existing workspace is never blocked. Browser storage is evictable and this library holds private keys: Chrome and Firefox treat IndexedDB as best-effort unless `navigator.storage.persist()` is granted, Safari caps script-writable storage at roughly 7 days without user interaction, and private-mode windows may have no IndexedDB at all. Request persistence from a user gesture, and use `exportContainer()` for a durable backup.
647
+ - A **`Buffer` polyfill is still required** — `@solana/web3.js` and `@coral-xyz/anchor` read the global. Standard for any Solana dapp.
27
648
 
28
- ### Please note
29
- - Browsers can clear their own storage. Creating a **new** wallet in a browser is blocked unless the browser promises to keep the data, or you explicitly accept the risk — an existing wallet always opens. Give users a way to back up.
30
- - A `Buffer` polyfill is needed in the browser. One line, shown in the Quick start.
649
+ ### Internal
650
+ - `scripts/browser-e2e.mjs` (`pnpm test:browser`) bundles the real dist through the `browser` condition, loads it into headless Chromium and drives a full lifecycle against the browser's own IndexedDB, asserting the same published BIP-44 vectors the Node suite pins. Resolving is not the same as running — a build can resolve cleanly and still fail at runtime, as a 2.1.0 regression showed. Wired into `prepublishOnly`.
651
+ - The published bundle is exercised under each platform to prove the export map lands where intended; asserting its shape is not the same as asserting a resolver's behaviour.
31
652
 
32
653
  ## [2.1.0] — 2026-07-28
33
654
 
@@ -114,7 +735,7 @@ Runs in the browser now. Existing Node code keeps working unchanged.
114
735
  ### Security
115
736
  - **Envelope v2 — Argon2id + AAD-bound on-disk records.** New seals use Argon2id (RFC 9106; t=3, m=64 MiB, p=1) and bind every record's GCM tag to its on-disk identity. HybridProvider records bind to `wative:v2:record:<recordType>:<slug>`; per-account mnemonic ciphertexts bind to `wative:v2:account:<slug>:mnemonic`; per-address private-key ciphertexts bind to `wative:v2:account:<slug>:wallet:<id>:<vm>:pk`; recovery envelopes bind to `wative:v2:recovery:account:<slug>`. Closes the blob-swap class of attack where an attacker with workspace-dir write access (but not the password) could substitute one record's ciphertext for another's. Legacy v1 records (PBKDF2-SHA256, no AAD) continue to read; new writes are always v2.
116
737
  - **Slug-collision suffix now CSPRNG.** Was `Math.random()`; now `crypto.randomInt`. Prevents predictable filenames in `accounts/<slug>-<suffix>.db`.
117
- - **`#seed` Buffer scrubbed before drop.** `Account.lock()` now `.fill(0)`s the BIP-39 seed Buffer before nulling the reference. JS string immutability still prevents perfect zeroization of mnemonic + plaintext private keys (documented on `Workspace.lock()`).
738
+ - **The derived seed is zeroed on lock.** `Account.lock()` now overwrites the BIP-39 seed bytes before releasing them. JS string immutability still prevents perfect zeroization of mnemonic + plaintext private keys (documented on `Workspace.lock()`).
118
739
  - **`useTestKdfIterations` Jest auto-detect removed.** The `typeof globalThis.expect !== "undefined"` opt-in arm fired in any downstream consumer using Jest, not just our own test process. Now requires explicit `WATIVE_ALLOW_TEST_KDF=1` (or `NODE_ENV=test`). Internal-only function; not on the package surface.
119
740
 
120
741
  ## [2.0.1] — 2026-04-29