wative-core 2.2.1 → 2.3.9

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 +486 -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-i614LDE1.d.cts → container-provider-CkKGyHis.d.cts} +10 -3
  13. package/dist/{container-provider-C59yqd5Z.d.ts → container-provider-D_557LVN.d.ts} +10 -3
  14. package/dist/{evm-BLml33LX.d.cts → evm-Xw4zBLUH.d.ts} +2 -2
  15. package/dist/{evm-DTzqGlL6.d.ts → evm-gsdoVj16.d.cts} +2 -2
  16. package/dist/{file-sink-LS4hyXXi.d.ts → file-sink-C_SlT9IC.d.cts} +8 -5
  17. package/dist/{file-sink-B73MuigK.d.cts → file-sink-D33jvque.d.ts} +8 -5
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.cts +20 -7
  20. package/dist/index.d.ts +20 -7
  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-CTCJsYri.d.cts} +2 -2
  31. package/dist/{svm-CCzqNAWM.d.cts → svm-CUuqpl1h.d.ts} +2 -2
  32. package/dist/{workspace-CamTHxso.d.cts → workspace-Du_jy51u.d.cts} +19 -4
  33. package/dist/{workspace-CamTHxso.d.ts → workspace-Du_jy51u.d.ts} +19 -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,505 @@
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.3.9] — 2026-08-10
6
+
7
+ Three breaking items. Two are on paths that were producing results no
8
+ counterparty could act on; the third changes how a refused broadcast is
9
+ reported, on both chains, so that it cannot be read as "safe to send again". Everything else here narrows what error messages reveal, or
10
+ accepts input that was being refused for no reason.
11
+
12
+ ### Security
13
+ - **BREAKING. The endpoint-reply check now covers Solana.** 2.3.6 stopped an
14
+ endpoint choosing which transaction was watched after a broadcast, but that
15
+ check only ever applied to EVM sends — the Solana lane still took the reply at
16
+ face value, so an endpoint could hand back an identifier of its own and report
17
+ results for a transaction you never sent. That reply is now checked against the
18
+ transaction that was actually signed, and everything afterwards follows the
19
+ signed value. A send refused
20
+ for this reason reports `timeout`, not `failed`, because the endpoint did
21
+ acknowledge it and the transaction may well have been relayed — look it up by
22
+ its signed signature before sending anything else, and do not rebuild it with a
23
+ fresh blockhash. The signed identifier is still reported, so it can be looked
24
+ up. Endpoints that answer correctly are unaffected.
25
+ - **BREAKING. A refused broadcast reports `timeout`, not `failed`, on EVM too.**
26
+ The same check on the EVM side already refused an endpoint that acknowledged a
27
+ broadcast with the wrong identifier, but reported the result as `failed` — the
28
+ one status a caller reads as "that never went out, send it again". The
29
+ endpoint did acknowledge it, so those bytes may be on the network: a retry can
30
+ then take a fresh position and both transactions land. It now reports
31
+ `timeout`, meaning the outcome is unknown, and the error still names both
32
+ identifiers. The transaction continues to report the identifier it signed, and
33
+ awaiting its submission now fails with that same explanation rather than
34
+ reporting that it was never sent.
35
+ - **An RPC URL missing its scheme was handed back with one attached.** Provider
36
+ dashboards present an endpoint host-first with the key in the path and no
37
+ scheme. Pasting that produced an error that repeated the whole value with
38
+ `https://` prefixed — a ready-to-use secret URL, in a message that can reach a
39
+ log file or a crash reporter. Both refusals for an unparseable RPC URL now say
40
+ what is wrong without repeating any of it. A rejected scheme and a blocked
41
+ host are still named, since those are not the caller's secret.
42
+ - **A value rejected as a malformed number is no longer shown in full.** This
43
+ completes the sweep 2.3.8 began, which was scoped to address fields and noted
44
+ that amounts and gas prices were still shown whole — that note no longer
45
+ applies. A private key or recovery phrase pasted into an amount, a gas price or
46
+ a gas limit used to appear in the error in its entirety. Such a value is now
47
+ shortened. It is shortened rather than removed because for a number the
48
+ rejected text is the diagnosis: a thousands separator, a stray decimal point or
49
+ an exponent is still perfectly legible.
50
+ - **The last two address errors stopped repeating their value.** A token
51
+ contract address that is not a valid address, and a network's multicall
52
+ address, were the two sites the 2.3.8 sweep left. Adding a custom token by
53
+ contract address is something an end user does by pasting, and the same slip
54
+ applies. Both now name the field instead of showing its contents.
55
+
56
+ ### Fixed
57
+ - **Malformed typed data, measured.** Of twenty-four malformed payload shapes,
58
+ twenty-two are now refused with this library's own error code and none escapes
59
+ as an untyped error. The remaining two — a domain given as an array or a
60
+ number — are signed deliberately, because every other implementation signs
61
+ them identically and refusing them would break real callers.
62
+ - **BREAKING. A field whose declared type is not a type at all is now refused.**
63
+ A type is a name followed by zero or more array suffixes and nothing else.
64
+ Anything trailing the suffix — `uint256[]extra`, `uint256[2]junk` — or an
65
+ unclosed bracket such as `uint256[` was accepted, and produced a signature no
66
+ counterparty could check. Such a payload now reports
67
+ `PARAMETER_ERROR` naming the type, which is what other implementations already
68
+ do. Every valid type signs exactly as before, including nested and fixed-size
69
+ arrays such as `uint256[2][3]`, a zero-padded length such as `uint256[01]`, and
70
+ the shorthands `uint` and `int`.
71
+ - **An amount too large to exist is refused when you set it, not when you
72
+ sign.** Every numeric field of an EVM transaction holds a 256-bit value, and
73
+ a larger one was accepted and carried all the way to signing, which then
74
+ failed with a message naming neither the field nor the reason. Such a value
75
+ is now refused immediately and by name. The largest legal value is still
76
+ accepted, and nothing was ever silently altered.
77
+ - **A wallet's address list can no longer be rewritten in place.** It is
78
+ declared read-only and, unlike an account's wallet list, nothing enforced
79
+ that: assigning an entry, truncating it, pushing to it or deleting from it
80
+ were all accepted, and the next ordinary save wrote the result through. Both
81
+ lists now refuse those, and both also refuse being frozen or having their
82
+ prototype changed — neither of which writes anything itself, but both of which
83
+ make a later legitimate change fail from somewhere the caller cannot connect
84
+ to what they did. Reading, iterating and the collections' own methods are
85
+ unchanged.
86
+ - **More invisible characters are removed from names.** Free-text names — a
87
+ wallet's display name, an asset or network name — are cleaned of characters
88
+ that carry no glyph before anything else looks at them, so nothing can hide
89
+ inside a word. Five that were missing are now included, among them the
90
+ directional isolates, which belong to the same family as the overrides that
91
+ were already covered. Emoji are untouched.
92
+ - **A malformed Solana address is reported the same way wherever it is given.**
93
+ Depending on which call it came through, the same mistake produced either this
94
+ library's own `PARAMETER_ERROR` or a bare decoding error with no code — and in
95
+ one case nothing at all until much later, from a place that could no longer
96
+ say which address was at fault. Every one of them now reports
97
+ `PARAMETER_ERROR` and names the field. Nothing that built before is refused.
98
+ - **A typed-data payload that declares `EIP712Domain` as null no longer throws a
99
+ raw error.** Naming that key as a field's type, with the key itself set to
100
+ `null` or `undefined`, produced a `TypeError` from inside the encoder instead
101
+ of a `PARAMETER_ERROR`. Leaving the key present but empty while nothing
102
+ references it is a normal payload and still signs, byte for byte as before.
103
+ - **A Solana fee payer naming the sending account is no longer refused.**
104
+ Signing rejected any fee payer that was not the exact same string as the
105
+ sender, so supplying the sender's own key as a public-key object or as raw key
106
+ bytes — both of which build correctly for external signing — was turned away,
107
+ as was `null`. All three now sign, and describe a transaction identical to
108
+ leaving the field out. A fee payer naming a genuinely different account is
109
+ still refused, and is still available for external signing.
110
+
111
+ ## [2.3.8] — 2026-08-09
112
+
113
+ Typed-data signing (`signTypedData`) is stricter about payloads that were
114
+ producing signatures no counterparty can verify. If you sign typed data, read
115
+ the three breaking items below. Nothing else here changes what is accepted.
116
+
117
+ ### Security
118
+ - **Four address errors no longer repeat the value they rejected.** A
119
+ destination that is not a valid address, a malformed Solana recipient, a
120
+ Solana fee payer that differs from the sender, and a transaction whose sender
121
+ does not match the address signing it: each used to include the value it had
122
+ just refused. Pasting a private key into a destination field is an easy slip,
123
+ and the message could then reach anything that records errors — a log file, a
124
+ crash reporter, a support ticket. Each now names the field that was wrong
125
+ instead of showing its contents. An address that IS valid is still shown, so
126
+ a checksum typo remains easy to spot. Note this covers address fields: a
127
+ value rejected as a malformed *number* — an amount, a gas price — is still
128
+ shown in full.
129
+ - **BREAKING. A domain carrying an unrecognised field is now refused.** Only
130
+ `name`, `version`, `chainId`, `verifyingContract` and `salt` are part of an
131
+ EIP-712 domain. A misspelled or mis-cased field — `chainID` with a capital D,
132
+ or keys lower-cased by a JSON round-trip — was previously dropped in silence,
133
+ and the signature was then bound to no chain and no contract at all. Such a
134
+ payload is refused rather than signed. A domain with no fields still signs,
135
+ unchanged.
136
+
137
+ ### Fixed
138
+ - **BREAKING. A field typed `uint` or `int` now signs as `uint256`/`int256`.**
139
+ These are the same types under a shorter name and every other implementation
140
+ treats them that way; this one did not, so the signature verified as a
141
+ different address than the actual signer, with no error anywhere. Any
142
+ signature previously produced for such a field changes — the new one is the
143
+ one a counterparty accepts. Fields with an explicit width are unaffected.
144
+ - **BREAKING. A fixed-size array field must hold exactly its declared number of
145
+ values.** `uint256[2]` given three values used to sign something no verifier
146
+ would evaluate; it is now refused.
147
+ - **Malformed typed data reports this library's error.** Bad values in `message`
148
+ — a missing or `null` nested struct, a number out of range for its declared
149
+ width, a malformed address or byte length, an unreadable field — now report
150
+ `PARAMETER_ERROR` naming the field, instead of escaping as a plain error with
151
+ no code. Payloads that signed before still sign, and produce the same
152
+ signature; only the error changed.
153
+ - **A malformed recipient on a native SOL transfer reports this library's
154
+ error.** Such an address was always rejected, but by an underlying library, as
155
+ a plain error carrying no code — so it could not be handled alongside every
156
+ other failure. It now reports `PARAMETER_ERROR` naming the field, and says
157
+ whether the address is not valid base58 or is the wrong length. Every address
158
+ that built before still builds; only the error changed.
159
+
160
+ ## [2.3.7] — 2026-08-08
161
+
162
+ A security and correctness release. Upgrade if you display names your users
163
+ typed, import Solana keys, send Solana transactions, or rely on the password
164
+ strength report.
165
+
166
+ ### Security
167
+ - **A hidden character could smuggle a dangerous link into a display name.**
168
+ Names, tags and other free-form text are cleaned before storage, and that
169
+ cleaning removed dangerous prefixes like `javascript:` before it removed
170
+ zero-width and control characters. A single invisible character placed inside
171
+ such a prefix therefore survived the check and was then tidied away, handing
172
+ back exactly the thing that was supposed to have been removed. Anything
173
+ putting a stored name on a page received a working link. All affected
174
+ prefixes and every invisible-character class are covered.
175
+ - **The password strength report described a different password than the one in
176
+ use.** Passwords are normalised before the key is derived, but the strength
177
+ check read the text exactly as typed. A password written in decomposed form —
178
+ ordinary for some keyboards and languages — was reported as longer and
179
+ stronger than the secret actually protecting the wallet, and could pass a
180
+ minimum-length rule the real one fails. The report now measures what is
181
+ actually used. Nothing about existing wallets or how keys are derived changes,
182
+ and the warning about passwords that shift form between machines is unchanged.
183
+
184
+ ### Fixed
185
+ - **A malformed Solana private key is now refused in this library's terms.**
186
+ Such a key was always rejected, but by an underlying library, as a plain error
187
+ carrying no error code — so it could not be handled alongside every other
188
+ failure. It now reports `INVALID_PRIVATE_KEY`, matching what the EVM side
189
+ already did.
190
+ - **A rejected logging change no longer takes effect anyway.** Changing sinks or
191
+ the log level while the workspace is locked correctly fails — but the new
192
+ settings were applied before that failure, leaving the previous sinks closed
193
+ and, for file logging, records being written under a configuration you were
194
+ told had been rejected. Nothing changes now unless the change succeeds.
195
+ - **Closing RPC clients now closes all of them.** Solana connections were left
196
+ open, each holding a network connection and two timers, once per endpoint used
197
+ for the lifetime of the process.
198
+ - **An expired Solana transaction now says so.** Previously it was polled for the
199
+ full two minutes and then reported as a timeout — the same answer given for a
200
+ slow node, though the two call for opposite responses. A timeout means the
201
+ outcome is unknown and sending another transaction risks paying twice; an
202
+ expired one can never be accepted, so replacing it is safe. It now ends as
203
+ dropped, saying the blockhash expired.
204
+ - **Wallet lookup by tag is no longer case-sensitive**, matching how the same
205
+ method already looked up accounts and addresses. Tags keep the capitalisation
206
+ you gave them.
207
+
208
+ ### Added
209
+ - **`workspace.damagedAccountSlugs`** lists account records that are present but
210
+ could not be opened. Such a record is deliberately kept rather than discarded,
211
+ and creating an account with the same name will not overwrite it — but until
212
+ now nothing reported that it existed, and the only visible sign was new
213
+ accounts receiving unexpected names. The list is read-only: removing such a
214
+ record is left to you, because it may still hold a recoverable recovery
215
+ phrase.
216
+
217
+ ## [2.3.6] — 2026-08-08
218
+
219
+ A correctness release for anyone who sends transactions. Several ways a
220
+ transaction could do something other than what you asked, or be reported as
221
+ something it was not, all of them silent.
222
+
223
+ ### Security
224
+ - **A redirect could carry your RPC request to a host you never configured.**
225
+ The RPC address you set was checked, but a redirect from that address was
226
+ followed automatically — and the kinds that preserve the request body would
227
+ re-send the whole call somewhere else, including addresses the check exists to
228
+ block. Redirects are no longer followed, and one is now reported as a
229
+ redirect, naming where it was being sent, rather than as an unreachable node.
230
+ - **The transaction being watched was chosen by the endpoint.** After a
231
+ broadcast, the identifier the endpoint replied with was taken at face value
232
+ and everything afterwards followed it — so an endpoint could hand back an
233
+ identifier of its own and report a result for a transaction you never sent,
234
+ while yours went unwatched. That reply is now checked against the transaction
235
+ that was actually signed. An endpoint that does not acknowledge a broadcast
236
+ still reports no identifier, exactly as before.
237
+ (Scope correction, added in 2.3.9: this covered EVM sends only. The wording
238
+ above did not say so, and the Solana lane kept taking the reply at face value
239
+ until 2.3.9.)
240
+ - **A mistyped token contract address was silently corrected.** The same
241
+ checksum repair that was fixed for transaction destinations in 2.3.5 also
242
+ applied where you supply a token's contract address. It is now refused.
243
+ Addresses given entirely in lower or upper case are unaffected.
244
+
245
+ ### Fixed
246
+ - **Call data with an odd number of digits changed the function being called.**
247
+ It was padded on the left, which shifts every byte — turning a token transfer
248
+ into a call to something else, with the arguments shifted too. The
249
+ transaction signed, sent and mined perfectly well; it simply did something
250
+ else. Such call data is now refused, because what was intended cannot be
251
+ known.
252
+ - **An automatic nonce could be handed out more than once.** The nonce was
253
+ requested from the node every time and nothing recorded what had already been
254
+ used, so transactions prepared before the node caught up all received the same
255
+ one. Sending three transfers in a row let one arrive while the rest could
256
+ never be mined, and all three reported as sent. A nonce you supply yourself is
257
+ still honoured exactly as given, so replacing an in-flight transaction with a
258
+ higher fee works as before.
259
+ - **A mined transaction could be reported as a failed send.** One unreadable
260
+ field in an otherwise successful receipt — the gas figures — was enough to
261
+ report the whole send as failed and to discard the receipt proving the funds
262
+ moved. Those figures are now read tolerantly and simply omitted when they
263
+ cannot be read.
264
+ - **An address will not sign for one chain and send to another.** An address and
265
+ the network it is pointed at could disagree about which chain they belong to;
266
+ signing now refuses rather than producing a signature for one chain and
267
+ handing it to another.
268
+ - **A signed transaction's fields can no longer be changed.** Editing a field
269
+ after signing was accepted and reported back, while the bytes that would be
270
+ sent still carried the original values. This covers both EVM and Solana
271
+ transactions. Build a new transaction to change something.
272
+ - **A wallet list handed to you can no longer be modified in place.** Assigning
273
+ to it changed the account's own list, and the next save wrote that through.
274
+ - **A dropped network no longer comes back.** Saving a network after removing it
275
+ added it again, in memory and on disk.
276
+ - **A blank address filter no longer matches the first address**, and passing
277
+ something that is not an account now reports a parameter error rather than an
278
+ unrelated type error.
279
+ - **Creating an account no longer overwrites one another handle just created.**
280
+ Two handles open on the same workspace could each create an account with the
281
+ same name, leaving one record holding the second recovery phrase and
282
+ destroying the first.
283
+
284
+ ## [2.3.5] — 2026-08-07
285
+
286
+ A security release. Upgrade if you send EVM transactions, or if you keep more
287
+ than one account in a workspace.
288
+
289
+ ### Security
290
+ - **A mistyped destination address was silently corrected.** An EVM address
291
+ carries a checksum whose whole purpose is to catch a mistyped character. When
292
+ a transaction destination arrived with a checksum that did not match, it was
293
+ quietly rewritten into a valid-looking address rather than refused — so a
294
+ single wrong character could send funds to an address you never typed, with no
295
+ warning. Such a destination is now rejected. Addresses given entirely in lower
296
+ or upper case carry no checksum to check and are unaffected, and address
297
+ lookup stays as forgiving as before.
298
+ - **An account could overwrite another account's stored secrets.** On macOS and
299
+ Windows, where file names ignore case, creating an account could write over a
300
+ stored account whose name differed only in capitalisation — destroying the
301
+ sealed recovery phrase it held. Names are now checked the way the file system
302
+ actually compares them.
303
+ - **Two addresses reached cloud metadata services.** The RPC URL check missed one
304
+ IPv6 spelling of the metadata address, and the well-known metadata host names
305
+ entirely.
306
+ - **Sealed material no longer appears when an object is logged.** The stored
307
+ recovery phrase and stored keys were ordinary visible properties, so they
308
+ surfaced in `console.log` output and in anything that walks an object's
309
+ properties.
310
+ - **A password policy option set to `undefined` no longer disables it.** Building
311
+ an options object from optional values — the ordinary way to forward
312
+ settings — removed the minimum-length rule entirely, so short passwords stopped
313
+ being reported.
314
+
315
+ ### Fixed
316
+ - **`confirmed` no longer fires for a reverted transaction.** Subscribers were
317
+ told a transaction had confirmed, receiving a receipt whose own success flag
318
+ was false, and only afterwards told it failed.
319
+ - **A stray file no longer blocks removing a network.** A single duplicate left
320
+ in the storage folder — the kind a file manager creates — made network removal
321
+ refuse permanently.
322
+ - **A log file prefix can no longer choose the directory.** A prefix containing
323
+ path separators wrote the log outside the folder it was given. Sizes and file
324
+ counts are validated too.
325
+ - **Aborting a transaction cannot leave a stale finish behind.** Internal
326
+ finishing can no longer be triggered while a transaction is still in flight.
327
+ - **Key material is cleared in three more places** where an unusual failure —
328
+ one bad address, an interrupted derivation, a rolled-back add — previously left
329
+ it in memory.
330
+
331
+ ## [2.3.4] — 2026-08-07
332
+
333
+ A security release. Upgrade if you hold keys in a long-running process, or if
334
+ you log anything from a Solana wallet.
335
+
336
+ ### Security
337
+ - **Key material could survive `lock()`.** Two separate cases. Records sealed by
338
+ the optional faster storage format left one readable copy of each record's key
339
+ in memory after the workspace was locked — and because that format seals each
340
+ secret the same way, such a copy is the secret itself. Separately, part of the
341
+ password-derivation scratch space was not being cleared, leaving
342
+ password-derived bytes resident. Both are now cleared, and both are covered by
343
+ checks that can actually observe the memory in question.
344
+ - **`lock()` could return while an account stayed unlocked.** An `unlock` that
345
+ landed at the same moment as a `lock` could complete inside it: the workspace
346
+ reported locked while the account still held its mnemonic and could sign, and
347
+ no later `lock()` could reach it. Locking is now closed to that race.
348
+
349
+ ### Fixed
350
+ - **Signing typed data with a `bytes` field.** A `Uint8Array` or `Buffer` was
351
+ hashed as text rather than as its bytes, so the signature did not match what
352
+ any other implementation computes — and two containers holding the same bytes
353
+ gave two different results. Passing a `0x` string was, and remains, correct.
354
+ Text that is not `0x`-hex is now rejected rather than silently hashed a
355
+ second, incompatible way.
356
+ - **Malformed typed data now raises the library's own error.** Several shapes —
357
+ including a `primaryType` naming a built-in type, and a struct that contains
358
+ itself — escaped as a raw `TypeError` with no error code. (This entry
359
+ originally gave a count. It was not reproducible against the released build
360
+ and has been replaced by the property it was trying to describe; the current
361
+ coverage is stated under 2.3.9.)
362
+ - **Solana signatures and transaction ids are no longer removed from logs.** The
363
+ redactor treated any long base58 value as a secret, which is exactly the shape
364
+ of a signature, so the one value worth correlating on disappeared silently.
365
+ Redaction by field NAME was broadened at the same time, so tokens, passcodes
366
+ and recovery keys that previously slipped through are now covered.
367
+ - **Logging a non-string no longer throws.** `logger.info(x)` where `x` was
368
+ undefined, a number or an object took down the caller.
369
+ - **Adopting an account into another workspace** now refuses when that
370
+ workspace's password would not open it, instead of writing a record that can
371
+ never be opened again.
372
+ - **Adding an account no longer loses concurrent work.** Wallets derived while
373
+ the account was being added were kept in memory but never written.
374
+
375
+ ## [2.3.3] — 2026-08-07
376
+
377
+ A security fix for typed-data signing. Upgrade if you use `signTypedData`.
378
+
379
+ ### Security
380
+ - **A signature could be produced for a value other than the one you were shown.**
381
+ When a typed-data payload declared a custom type whose name collides with one of
382
+ EIP-712's own built-in type names, a field of that type was not bound by the
383
+ signature: every value produced the same result, and an independent verifier
384
+ read the field as a fixed value nobody chose. The same collision could also
385
+ detach a signature from the chain and contract it was meant for. Because the
386
+ payload is normally supplied by the site requesting the signature, this did not
387
+ require anything unusual from the wallet holder. Such payloads are now rejected
388
+ with `PARAMETER_ERROR`, which is also what other EIP-712 implementations do, so
389
+ no payload that they accept is affected.
390
+
391
+ ### Fixed
392
+ - **A retired wallet index could come back.** Dropping a wallet records its index
393
+ so it is never derived again. If another account setting was changed at the same
394
+ moment, that record could be lost, and the next `deriveWallets()` returned the
395
+ same address and private key as the wallet that was dropped.
396
+ - **`sliceWallets` could empty an account.** It kept no minimum, so on an account
397
+ whose first wallet had already been dropped it could remove every remaining
398
+ wallet, leaving an account with no keys and reporting no error. It now refuses,
399
+ as `wallet.drop()` already did.
400
+ - **Aborting a transaction no longer reports a confirmation afterwards.**
401
+ `abort()` could still fire `confirmed` listeners for a receipt that arrived
402
+ after the abort, having already reported the failure. The receipt itself is
403
+ still kept on the tracker — if the transaction did land, you can still see it —
404
+ but it is no longer announced as a confirmation, and the abort remains the
405
+ reported cause.
406
+ - **Secrets are cleared in two cases where they were kept.** A wallet drop, or a
407
+ password reset, that failed because the workspace was locked at that moment
408
+ could leave key material in memory that a later `lock()` would not reach.
409
+
410
+ ### Changed
411
+ - ⚠️ **`whenFinalized()` now rejects instead of never settling.** Transaction
412
+ tracking ends at first inclusion, so on EVM — and on Solana whenever the node
413
+ reports `confirmed` first — this promise previously neither resolved nor
414
+ rejected. It now rejects with `UNSUPPORTED_OP`. A Solana node that reports
415
+ finality directly still resolves as before.
416
+
417
+ **Migration.** `void tracker.whenFinalized().then(...)` used to do nothing,
418
+ because the promise never settled; it is now an unhandled rejection and will
419
+ terminate a Node process. Attach a `.catch()`, or await it. Awaiting it
420
+ alongside `whenConfirmed()` is unaffected.
421
+ - **A `types.EIP712Domain` list that disagrees with your domain is now rejected**
422
+ rather than ignored. The domain separator has always been computed from the
423
+ domain object itself, so a mismatched list previously produced a signature that
424
+ your own verifier would reject, with nothing to point at the cause. An empty or
425
+ absent list still works, so the JSON-RPC shape is unaffected.
426
+
427
+ - ⚠️ **Documented late — this shipped in 2.3.3 without a note.** `record.unlock(password)`
428
+ on a record that is already open now re-checks the password instead of returning
429
+ success. Records arrive already open in the common case, so a caller using
430
+ `record.unlock(pw)` as a password check was previously told yes for **every**
431
+ string, including a wrong one. It also refuses once the workspace has been locked
432
+ or the record dropped, where a retained handle used to keep answering password
433
+ guesses indefinitely.
434
+
435
+ **Migration.** If you relied on `unlock()` succeeding for an already-open record,
436
+ pass the real password. Reading `.value` is unaffected — that is a snapshot you
437
+ already hold, and it is deliberately not gated.
438
+
439
+ ## [2.3.2]
440
+
441
+ Not released.
442
+
443
+ ## [2.3.1] — 2026-08-07
444
+
445
+ A types-only fix. Six types that appear in the public API could not be imported,
446
+ so TypeScript users could call a method but not write down what it took or
447
+ returned. Nothing changed at runtime, and JavaScript users are unaffected.
448
+
449
+ ### Fixed
450
+ - `OpenOptions` (the options bag for `Workspace.open()`), `PasswordCheckContext`,
451
+ `NetworkConfig`, `AbiItem`, `SplTokenArgs` and `SplTokenAccountSet` are now
452
+ exported and can be imported by name.
453
+
454
+ ## [2.3.0] — 2026-08-06
455
+
456
+ Unlocking is much faster. Drop-in from 2.2.x; wallets from 1.x need a manual step first.
457
+
458
+ ### Added
459
+ - An optional storage format that keeps unlocking fast however many addresses an account holds.
460
+
461
+ ### Changed
462
+ - 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.
463
+
464
+ ### Removed
465
+ - Wallets created by 1.x can no longer be opened — export their secrets with 2.2.x before upgrading.
466
+
5
467
  ## [2.2.1] — 2026-07-29
6
468
 
7
- Documentation and packaging only — the library code is identical to 2.2.0.
469
+ 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
470
 
9
471
  ### 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.
472
+ - **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.
473
+ - **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
474
 
13
475
  ### Fixed
14
- - One example failed when run from an installed copy.
476
+ - **`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.
477
+
478
+ ### Internal
479
+ - `.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.
480
+ - 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
481
 
16
482
  ## [2.2.0] — 2026-07-28
17
483
 
18
- Runs in the browser now. Existing Node code keeps working unchanged.
484
+ > **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
485
 
20
486
  ### 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.
487
+ - **`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.
488
+ - **`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.
489
+ - **`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.
490
+ - **`wative-core/node` subpath** — `HybridProvider`, `FileSink` and the default-workspace-path resolver.
491
+ - **`STORAGE_NOT_DURABLE`** error code.
24
492
 
25
493
  ### 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.
494
+ - **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.
495
+ - `tsconfig` `lib` gains `DOM`; the browser is now a real target.
496
+
497
+ ### Browser notes
498
+ - 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.
499
+ - A **`Buffer` polyfill is still required** — `@solana/web3.js` and `@coral-xyz/anchor` read the global. Standard for any Solana dapp.
27
500
 
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.
501
+ ### Internal
502
+ - `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`.
503
+ - 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
504
 
32
505
  ## [2.1.0] — 2026-07-28
33
506
 
@@ -114,7 +587,7 @@ Runs in the browser now. Existing Node code keeps working unchanged.
114
587
  ### Security
115
588
  - **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
589
  - **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()`).
590
+ - **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
591
  - **`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
592
 
120
593
  ## [2.0.1] — 2026-04-29