@wishknish/knishio-client-js 0.9.4 → 1.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishknish/knishio-client-js",
3
- "version": "0.9.4",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "productName": "Knish.IO Javascript SDK Client",
6
6
  "description": "JavaScript implementation of the Knish.IO SDK to consume Knish.IO GraphQL APIs.",
@@ -48,16 +48,16 @@
48
48
  "quantum-safe",
49
49
  "sdk",
50
50
  "javascript",
51
- "ml-kem768",
51
+ "ml-kem1024",
52
52
  "cryptography",
53
53
  "crystals-kyber",
54
54
  "fips-202",
55
55
  "fips-203"
56
56
  ],
57
57
  "dependencies": {
58
- "@noble/post-quantum": "^0.5.4",
59
- "@thumbmarkjs/thumbmarkjs": "^0.19.1",
60
- "@urql/core": "^5.2.0",
58
+ "@noble/post-quantum": "^0.7.1",
59
+ "@thumbmarkjs/thumbmarkjs": "^1.11.0",
60
+ "@urql/core": "^6.0.3",
61
61
  "graphql": "^16.12.0",
62
62
  "graphql-ws": "^6.0.7",
63
63
  "isomorphic-fetch": "^3.0.0",
@@ -66,17 +66,13 @@
66
66
  },
67
67
  "devDependencies": {
68
68
  "@jest/globals": "^30.4.1",
69
- "@rollup/plugin-babel": "^6.1.0",
70
- "@rollup/plugin-commonjs": "^28.0.9",
71
- "@rollup/plugin-node-resolve": "^16.0.3",
72
69
  "@swc/core": "^1.15.43",
73
70
  "@swc/jest": "^0.2.39",
74
71
  "buffer": "^6.0.3",
75
72
  "eslint": "^9.39.0",
76
73
  "jest": "^30.4.2",
77
74
  "neostandard": "^0.13.0",
78
- "rollup": "^4.57.1",
79
- "vite": "^7.3.5"
75
+ "vite": "^8.2.2"
80
76
  },
81
77
  "browserslist": [
82
78
  "> 1%",
@@ -96,5 +92,8 @@
96
92
  "/src",
97
93
  "/dist"
98
94
  ],
99
- "packageManager": "yarn@4.15.0"
95
+ "packageManager": "yarn@4.15.0",
96
+ "engines": {
97
+ "node": ">=20.0.0"
98
+ }
100
99
  }
package/src/AuthToken.js CHANGED
@@ -83,6 +83,26 @@ export default class AuthToken {
83
83
  return authToken
84
84
  }
85
85
 
86
+ /**
87
+ * ML-KEM parameter set a restored session must use, resolved in three tiers:
88
+ * an explicit snapshot field, then the stored validator key's length, then ML-KEM-768.
89
+ *
90
+ * The final tier is deliberately NOT the constructor default. A snapshot with neither an
91
+ * explicit field nor a recognisable key can only have come from a pre-bump build, and every
92
+ * pre-bump build was 768-only — defaulting to 1024 would make the restored wallet advertise
93
+ * a public key the validator never recorded for that token.
94
+ *
95
+ * @param {object} snapshot
96
+ * @return {number}
97
+ */
98
+ static resolveMlKemParameterSet (snapshot) {
99
+ const explicit = snapshot.wallet && snapshot.wallet.mlKemParameterSet
100
+ if (explicit) {
101
+ return Number(explicit)
102
+ }
103
+ return Wallet.mlKemParameterSetFromPubkey(snapshot.pubkey) || 768
104
+ }
105
+
86
106
  /**
87
107
  *
88
108
  * @param {object} snapshot
@@ -94,7 +114,8 @@ export default class AuthToken {
94
114
  secret,
95
115
  token: 'AUTH',
96
116
  position: snapshot.wallet.position,
97
- characters: snapshot.wallet.characters
117
+ characters: snapshot.wallet.characters,
118
+ mlKemParameterSet: AuthToken.resolveMlKemParameterSet(snapshot)
98
119
  })
99
120
  return AuthToken.create({
100
121
  token: snapshot.token,
@@ -122,7 +143,7 @@ export default class AuthToken {
122
143
 
123
144
  /**
124
145
  *
125
- * @return {{wallet: {characters, position}, encrypt, expiresAt, token, pubkey}}
146
+ * @return {{wallet: {characters, position, mlKemParameterSet}, encrypt, expiresAt, token, pubkey}}
126
147
  */
127
148
  getSnapshot () {
128
149
  return {
@@ -132,7 +153,8 @@ export default class AuthToken {
132
153
  encrypt: this.$__encrypt,
133
154
  wallet: {
134
155
  position: this.$__wallet.position,
135
- characters: this.$__wallet.characters
156
+ characters: this.$__wallet.characters,
157
+ mlKemParameterSet: this.$__wallet.mlKemParameterSet
136
158
  }
137
159
  }
138
160
  }
@@ -129,7 +129,8 @@ export default class KnishIOClient {
129
129
  serverSdkVersion = 3,
130
130
  logging = false,
131
131
  defaultRequestPolicy = null,
132
- secretStorage = null
132
+ secretStorage = null,
133
+ mlKemParameterSet = 1024
133
134
  }) {
134
135
  this.initialize({
135
136
  uri,
@@ -139,7 +140,8 @@ export default class KnishIOClient {
139
140
  serverSdkVersion,
140
141
  logging,
141
142
  defaultRequestPolicy,
142
- secretStorage
143
+ secretStorage,
144
+ mlKemParameterSet
143
145
  })
144
146
  }
145
147
 
@@ -161,7 +163,8 @@ export default class KnishIOClient {
161
163
  serverSdkVersion = 3,
162
164
  logging = false,
163
165
  defaultRequestPolicy = null,
164
- secretStorage = null
166
+ secretStorage = null,
167
+ mlKemParameterSet = 1024
165
168
  }) {
166
169
  this.reset()
167
170
 
@@ -171,6 +174,7 @@ export default class KnishIOClient {
171
174
  // policy. A long-lived server/sync client set to 'network-only' never serves
172
175
  // a stale cache-first read; browser/SPA consumers leave this null (cache-first).
173
176
  this.$__defaultRequestPolicy = defaultRequestPolicy
177
+ this.setMlKemParameterSet(mlKemParameterSet)
174
178
  this.$__authTokenObjects = {}
175
179
  this.$__authInProcess = false
176
180
  this.abortControllers = new Map()
@@ -202,6 +206,30 @@ export default class KnishIOClient {
202
206
  this.$__serverSdkVersion = serverSdkVersion
203
207
  }
204
208
 
209
+ /**
210
+ * Get active ML-KEM parameter set (1024 default or 768 step-back)
211
+ *
212
+ * @return {number}
213
+ */
214
+ getMlKemParameterSet () {
215
+ return this.$__mlKemParameterSet || 1024
216
+ }
217
+
218
+ /**
219
+ * Set active ML-KEM parameter set (1024 default or 768 step-back)
220
+ *
221
+ * @param {number|string} parameterSet
222
+ * @return {KnishIOClient}
223
+ */
224
+ setMlKemParameterSet (parameterSet) {
225
+ const paramNum = Number(parameterSet)
226
+ if (![1024, 768].includes(paramNum)) {
227
+ throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`)
228
+ }
229
+ this.$__mlKemParameterSet = paramNum
230
+ return this
231
+ }
232
+
205
233
  /**
206
234
  * Get random uri from specified this.$__uris
207
235
  *
@@ -509,7 +537,8 @@ export default class KnishIOClient {
509
537
 
510
538
  if (!sourceWallet) {
511
539
  sourceWallet = new Wallet({
512
- secret: this.getSecret()
540
+ secret: this.getSecret(),
541
+ mlKemParameterSet: this.getMlKemParameterSet()
513
542
  })
514
543
  } else {
515
544
  sourceWallet.key = Wallet.generateKey({
@@ -598,7 +627,8 @@ export default class KnishIOClient {
598
627
  bundle,
599
628
  token: 'USER',
600
629
  batchId: sourceWallet.batchId,
601
- characters: sourceWallet.characters
630
+ characters: sourceWallet.characters,
631
+ mlKemParameterSet: this.getMlKemParameterSet()
602
632
  })
603
633
 
604
634
  return new Molecule({
@@ -608,7 +638,8 @@ export default class KnishIOClient {
608
638
  remainderWallet: this.getRemainderWallet(),
609
639
  cellSlug: this.getCellSlug(),
610
640
  version: this.getServerSdkVersion(),
611
- continuIdPosition
641
+ continuIdPosition,
642
+ mlKemParameterSet: this.getMlKemParameterSet()
612
643
  })
613
644
  }
614
645
 
@@ -1237,7 +1268,8 @@ export default class KnishIOClient {
1237
1268
  }) {
1238
1269
  const newWallet = new Wallet({
1239
1270
  secret: this.getSecret(),
1240
- token
1271
+ token,
1272
+ mlKemParameterSet: this.getMlKemParameterSet()
1241
1273
  })
1242
1274
 
1243
1275
  /**
@@ -1369,7 +1401,8 @@ export default class KnishIOClient {
1369
1401
  secret: this.getSecret(),
1370
1402
  bundle: this.getBundle(),
1371
1403
  token,
1372
- batchId
1404
+ batchId,
1405
+ mlKemParameterSet: this.getMlKemParameterSet()
1373
1406
  })
1374
1407
 
1375
1408
  /**
@@ -1783,7 +1816,8 @@ export default class KnishIOClient {
1783
1816
  } else {
1784
1817
  to = Wallet.create({
1785
1818
  secret: to,
1786
- token
1819
+ token,
1820
+ mlKemParameterSet: this.getMlKemParameterSet()
1787
1821
  })
1788
1822
  }
1789
1823
  }
@@ -1935,7 +1969,8 @@ export default class KnishIOClient {
1935
1969
  // Attempt to get the recipient's wallet, if not provided
1936
1970
  const recipientWallet = Wallet.create({
1937
1971
  bundle: bundleHash,
1938
- token
1972
+ token,
1973
+ mlKemParameterSet: this.getMlKemParameterSet()
1939
1974
  })
1940
1975
 
1941
1976
  // Compute the batch ID for the recipient
@@ -2035,7 +2070,8 @@ export default class KnishIOClient {
2035
2070
  const recipientWallets = recipients.map(recipient => {
2036
2071
  const recipientWallet = Wallet.create({
2037
2072
  bundle: recipient.bundleHash,
2038
- token
2073
+ token,
2074
+ mlKemParameterSet: this.getMlKemParameterSet()
2039
2075
  })
2040
2076
 
2041
2077
  // Compute the batch ID for the recipient (typically used by stackable tokens)
@@ -2334,7 +2370,8 @@ export default class KnishIOClient {
2334
2370
  // Generate new recipient wallet if only recipient secret has been passed
2335
2371
  const recipientWallet = Wallet.create({
2336
2372
  bundle: bundleHash,
2337
- token: tokenSlug
2373
+ token: tokenSlug,
2374
+ mlKemParameterSet: this.getMlKemParameterSet()
2338
2375
  })
2339
2376
 
2340
2377
  // Set batch ID
@@ -2390,7 +2427,8 @@ export default class KnishIOClient {
2390
2427
  // Create a wallet for encryption
2391
2428
  const wallet = new Wallet({
2392
2429
  secret: generateSecret(await this.getFingerprint()),
2393
- token: 'AUTH'
2430
+ token: 'AUTH',
2431
+ mlKemParameterSet: this.getMlKemParameterSet()
2394
2432
  })
2395
2433
 
2396
2434
  /**
@@ -2443,7 +2481,8 @@ export default class KnishIOClient {
2443
2481
  // Generate a signing wallet
2444
2482
  const wallet = new Wallet({
2445
2483
  secret,
2446
- token: 'AUTH'
2484
+ token: 'AUTH',
2485
+ mlKemParameterSet: this.getMlKemParameterSet()
2447
2486
  })
2448
2487
 
2449
2488
  // Create a wallet with a signing wallet
@@ -2460,7 +2499,7 @@ export default class KnishIOClient {
2460
2499
  molecule
2461
2500
  })
2462
2501
 
2463
- // PQ-transport Phase E (cycle 163): convey the AUTH source wallet's ML-KEM768 public key as a
2502
+ // PQ-transport Phase E (cycle 163): convey the AUTH source wallet's ML-KEM public key as a
2464
2503
  // SIGNED `walletPubkey` meta on the U-atom (fillMolecule → initAuthorization → sign), so the
2465
2504
  // validator can encrypt CipherHash responses back to THIS wallet (the one that decrypts them).
2466
2505
  // Signed → tamper-proof. Only when present (PQ-capable wallet).
package/src/Molecule.js CHANGED
@@ -87,8 +87,10 @@ export default class Molecule {
87
87
  remainderWallet = null,
88
88
  cellSlug = null,
89
89
  version = null,
90
- continuIdPosition = null
90
+ continuIdPosition = null,
91
+ mlKemParameterSet = null
91
92
  }) {
93
+ this.mlKemParameterSet = mlKemParameterSet || (sourceWallet && sourceWallet.mlKemParameterSet) || 1024
92
94
  this.status = null
93
95
  this.molecularHash = null
94
96
  this.createdAt = String(+new Date())
@@ -110,7 +112,8 @@ export default class Molecule {
110
112
  bundle,
111
113
  token: sourceWallet.token,
112
114
  batchId: sourceWallet.batchId,
113
- characters: sourceWallet.characters
115
+ characters: sourceWallet.characters,
116
+ mlKemParameterSet: this.mlKemParameterSet
114
117
  })
115
118
  }
116
119
  }
@@ -343,7 +346,8 @@ export default class Molecule {
343
346
  if (!this.remainderWallet || this.remainderWallet.token !== 'USER') {
344
347
  this.remainderWallet = Wallet.create({
345
348
  secret: this.secret,
346
- bundle: this.bundle
349
+ bundle: this.bundle,
350
+ mlKemParameterSet: this.mlKemParameterSet
347
351
  })
348
352
  }
349
353
 
@@ -479,7 +483,8 @@ export default class Molecule {
479
483
  // Create burn address wallet (null bundle = token destruction)
480
484
  const burnWallet = new Wallet({
481
485
  bundle: '0000000000000000000000000000000000000000000000000000000000000000',
482
- token: this.sourceWallet.token
486
+ token: this.sourceWallet.token,
487
+ mlKemParameterSet: this.mlKemParameterSet
483
488
  })
484
489
 
485
490
  // V-atom 1: Debit full balance from source
@@ -685,7 +690,8 @@ export default class Molecule {
685
690
  secret: this.secret,
686
691
  bundle: this.bundle,
687
692
  token: this.sourceWallet.token,
688
- batchId: this.sourceWallet.batchId
693
+ batchId: this.sourceWallet.batchId,
694
+ mlKemParameterSet: this.mlKemParameterSet
689
695
  })
690
696
  bufferWallet.tradeRates = tradeRates
691
697
 
@@ -1329,7 +1335,8 @@ export default class Molecule {
1329
1335
  position: data.sourceWallet.position,
1330
1336
  bundle: data.sourceWallet.bundle,
1331
1337
  batchId: data.sourceWallet.batchId,
1332
- characters: data.sourceWallet.characters
1338
+ characters: data.sourceWallet.characters,
1339
+ mlKemParameterSet: molecule.mlKemParameterSet
1333
1340
  })
1334
1341
 
1335
1342
  // Set additional properties for validation context
@@ -1351,7 +1358,8 @@ export default class Molecule {
1351
1358
  position: data.remainderWallet.position,
1352
1359
  bundle: data.remainderWallet.bundle,
1353
1360
  batchId: data.remainderWallet.batchId,
1354
- characters: data.remainderWallet.characters
1361
+ characters: data.remainderWallet.characters,
1362
+ mlKemParameterSet: molecule.mlKemParameterSet
1355
1363
  })
1356
1364
 
1357
1365
  // Set additional properties for validation context
package/src/Wallet.js CHANGED
@@ -59,7 +59,13 @@ import {
59
59
  } from './libraries/crypto.js'
60
60
  import TokenUnit from './TokenUnit.js'
61
61
  import WalletCredentialException from './exception/WalletCredentialException.js'
62
- import { ml_kem768 as MlKEM768 } from '@noble/post-quantum/ml-kem.js'
62
+ import { ml_kem768 as MlKEM768, ml_kem1024 as MlKEM1024 } from '@noble/post-quantum/ml-kem.js'
63
+
64
+ const ML_KEM_PARAMS = {
65
+ 1024: { kem: MlKEM1024, pkBytes: 1568, skBytes: 3168, ctBytes: 1568 },
66
+ 768: { kem: MlKEM768, pkBytes: 1184, skBytes: 2400, ctBytes: 1088 }
67
+ }
68
+ const DEFAULT_ML_KEM_PARAMETER_SET = 1024
63
69
 
64
70
  /**
65
71
  * Wallet class represents the set of public and private
@@ -84,8 +90,14 @@ export default class Wallet {
84
90
  address = null,
85
91
  position = null,
86
92
  batchId = null,
87
- characters = null
93
+ characters = null,
94
+ mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
88
95
  }) {
96
+ const paramSetNum = Number(mlKemParameterSet)
97
+ if (!ML_KEM_PARAMS[paramSetNum]) {
98
+ throw new Error(`KnishIO: unsupported ML-KEM parameter set ${mlKemParameterSet}; expected 1024 or 768.`)
99
+ }
100
+ this.mlKemParameterSet = paramSetNum
89
101
  this.token = token
90
102
  this.balance = '0'
91
103
  this.molecules = {}
@@ -141,7 +153,8 @@ export default class Wallet {
141
153
  bundle = null,
142
154
  token,
143
155
  batchId = null,
144
- characters = null
156
+ characters = null,
157
+ mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
145
158
  }) {
146
159
  let position = null
147
160
 
@@ -163,7 +176,8 @@ export default class Wallet {
163
176
  token,
164
177
  position,
165
178
  batchId,
166
- characters
179
+ characters,
180
+ mlKemParameterSet
167
181
  })
168
182
  }
169
183
 
@@ -280,23 +294,49 @@ export default class Wallet {
280
294
  }
281
295
 
282
296
  /**
283
- * Initializes the ML-KEM key pair
297
+ * Derive an ML-KEM keypair for an arbitrary parameter set from the wallet's key seed.
298
+ *
299
+ * Returns `null` when the wallet holds no key — a secret-less wallet, which is what
300
+ * {@link Molecule.fromJSON} builds for validation context. `generateSecret(null, …)` does NOT
301
+ * throw, so without this the wallet would derive a plausible-looking identity from a bogus seed
302
+ * and fail three layers down at AES-GCM instead of at the missing key. The guard lives here
303
+ * rather than at each call site so a new caller cannot miss it.
304
+ *
305
+ * @param {number} parameterSet - 1024 or 768
306
+ * @return {{pubkey: string, privkey: Uint8Array, params: object}|null}
284
307
  */
285
- initializeMLKEM () {
286
- // Generate a 64-byte (512-bit) seed from the Knish.IO private key
287
- // Use deterministic approach: generateSecret(key, 128) → 128 hex chars = 64 bytes
308
+ _deriveMlKemKeypair (parameterSet) {
309
+ const params = ML_KEM_PARAMS[parameterSet]
310
+ if (!params) {
311
+ throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`)
312
+ }
313
+ if (!this.key) {
314
+ return null
315
+ }
288
316
  const seedHex = generateSecret(this.key, 128) // 128 hex chars = 64 bytes
289
-
290
- // Convert the hex string to a Uint8Array
291
317
  const seed = new Uint8Array(64)
292
318
  for (let i = 0; i < 64; i++) {
293
319
  seed[i] = parseInt(seedHex.substr(i * 2, 2), 16)
294
320
  }
321
+ const { publicKey, secretKey } = params.kem.keygen(seed)
322
+ return {
323
+ pubkey: this.serializeKey(publicKey),
324
+ privkey: secretKey,
325
+ params
326
+ }
327
+ }
295
328
 
296
- const { publicKey, secretKey } = MlKEM768.keygen(seed)
297
-
298
- this.pubkey = this.serializeKey(publicKey)
299
- this.privkey = secretKey // Note: We're keeping privkey as UInt8Array for security
329
+ /**
330
+ * Initializes the ML-KEM key pair. Only ever reached from the constructor's `secret` branch,
331
+ * so the derivation cannot come back empty here.
332
+ */
333
+ initializeMLKEM () {
334
+ const derived = this._deriveMlKemKeypair(this.mlKemParameterSet)
335
+ if (!derived) {
336
+ return
337
+ }
338
+ this.pubkey = derived.pubkey
339
+ this.privkey = derived.privkey
300
340
  }
301
341
 
302
342
  serializeKey (key) {
@@ -321,6 +361,35 @@ export default class Wallet {
321
361
  return new Uint8Array(binaryString.length).map((_, i) => binaryString.charCodeAt(i))
322
362
  }
323
363
 
364
+ /**
365
+ * ML-KEM parameter set implied by a serialized public key's raw byte length. FIPS 203's
366
+ * key lengths are disjoint (1568 bytes → ML-KEM-1024, 1184 bytes → ML-KEM-768), so a stored
367
+ * peer key recovers the parameter set of the session it belongs to without a wire-format
368
+ * change. Used by {@link AuthToken.restore} to resolve a snapshot that predates the field.
369
+ *
370
+ * @param {string|null} pubkey - Base64-serialized ML-KEM public key
371
+ * @return {number|null} 1024, 768, or null when the length matches neither
372
+ */
373
+ static mlKemParameterSetFromPubkey (pubkey) {
374
+ if (!pubkey) {
375
+ return null
376
+ }
377
+ let byteLength
378
+ try {
379
+ byteLength = typeof Buffer !== 'undefined'
380
+ ? Buffer.from(pubkey, 'base64').length
381
+ : atob(pubkey).length
382
+ } catch (e) {
383
+ return null
384
+ }
385
+ for (const [set, params] of Object.entries(ML_KEM_PARAMS)) {
386
+ if (params.pkBytes === byteLength) {
387
+ return Number(set)
388
+ }
389
+ }
390
+ return null
391
+ }
392
+
324
393
  /**
325
394
  * Returns balance as a Number for arithmetic operations.
326
395
  * WARNING: Precision loss for values > 2^53.
@@ -499,19 +568,19 @@ export default class Wallet {
499
568
  const messageString = JSON.stringify(message)
500
569
  const messageUint8 = new TextEncoder().encode(messageString)
501
570
  const deserializedPubkey = this.deserializeKey(recipientPubkey)
502
- // ML-KEM-768 public keys are exactly 1184 bytes. A wrong-length key here almost always means the
503
- // node did not advertise an ML-KEM public key in its auth `key` field (e.g. a validator predating
504
- // the PQ-transport build). Fail with an actionable message rather than the crypto lib's cryptic
505
- // `"publicKey" expected Uint8Array of length 1184, got length=N` assertion.
506
- const ML_KEM_768_PUBLIC_KEY_BYTES = 1184
507
- if (deserializedPubkey.length !== ML_KEM_768_PUBLIC_KEY_BYTES) {
571
+ // ML-KEM public keys are exactly 1568 bytes (ML-KEM-1024) or 1184 bytes (ML-KEM-768). A wrong-length key here
572
+ // almost always means the node did not advertise an ML-KEM public key in its auth `key` field (e.g. a validator
573
+ // predating the PQ-transport build). Fail with an actionable message rather than the crypto lib's cryptic
574
+ // `"publicKey" expected Uint8Array of length N, got length=M` assertion.
575
+ const params = ML_KEM_PARAMS[this.mlKemParameterSet]
576
+ if (deserializedPubkey.length !== params.pkBytes) {
508
577
  throw new Error(
509
578
  `KnishIO: cannot ML-KEM-encrypt — recipient public key is ${deserializedPubkey.length} bytes, ` +
510
- `expected ${ML_KEM_768_PUBLIC_KEY_BYTES} (ML-KEM-768). The node likely did not advertise an ML-KEM ` +
511
- 'public key (upgrade the validator to a PQ-transport build), or authenticate with { encrypt: false }.'
579
+ `expected ${params.pkBytes} (ML-KEM-${this.mlKemParameterSet}). The peer is not running ML-KEM-${this.mlKemParameterSet}; ` +
580
+ 'upgrade the peer, or step this client back to the other parameter set.'
512
581
  )
513
582
  }
514
- const { cipherText, sharedSecret } = MlKEM768.encapsulate(deserializedPubkey)
583
+ const { cipherText, sharedSecret } = params.kem.encapsulate(deserializedPubkey)
515
584
  const encryptedMessage = await this.encryptWithSharedSecret(messageUint8, sharedSecret)
516
585
  return {
517
586
  cipherText: this.serializeKey(cipherText),
@@ -525,17 +594,46 @@ export default class Wallet {
525
594
  }
526
595
 
527
596
  /**
528
- * ML-KEM768 decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string
597
+ * ML-KEM decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string
529
598
  * (no JSON.parse). Shared by {@link decryptMessage} (which JSON.parses the result)
530
- * and the PQ CipherHash transport ({@link decryptMyMessageML768}, which needs the raw
599
+ * and the PQ CipherHash transport ({@link decryptMyMessageML}, which needs the raw
531
600
  * response JSON text). PQ-transport Phase E (cycle 163).
532
601
  */
533
602
  async _mlkemDecryptToString (encryptedData) {
534
603
  const { cipherText, encryptedMessage } = encryptedData
535
604
 
605
+ const deserializedCipherText = this.deserializeKey(cipherText)
606
+ const configuredParams = ML_KEM_PARAMS[this.mlKemParameterSet]
607
+ const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024
608
+
609
+ // Inbound is PERMISSIVE: a ciphertext at either parameter set decrypts, provided it is addressed
610
+ // to one of THIS wallet's own ML-KEM identities. The 64-byte seed is parameter-set-independent,
611
+ // so the other identity is derived on demand and its private key is released with this call's
612
+ // scope — never cached on the wallet. Outbound encapsulation stays STRICT (see encryptMessage);
613
+ // reading a 768 record we own downgrades nothing, but encapsulating at 768 would.
614
+ let decapsParams = configuredParams
615
+ let decapsPrivkey = this.privkey
616
+ if (deserializedCipherText.length !== configuredParams.ctBytes) {
617
+ if (deserializedCipherText.length !== ML_KEM_PARAMS[otherSet].ctBytes) {
618
+ console.error(
619
+ `Wallet::decryptMessage() - Ciphertext length mismatch: got ${deserializedCipherText.length}, expected ${configuredParams.ctBytes}`
620
+ )
621
+ return null
622
+ }
623
+ // `null` here means the wallet holds no key to derive from (a secret-less validation
624
+ // wallet); preserve the existing failure observable rather than decapsulating with nothing.
625
+ const derived = this._deriveMlKemKeypair(otherSet)
626
+ if (!derived) {
627
+ console.error(`Wallet::decryptMessage() - cannot derive the ML-KEM-${otherSet} identity: wallet has no key`)
628
+ return null
629
+ }
630
+ decapsParams = derived.params
631
+ decapsPrivkey = derived.privkey
632
+ }
633
+
536
634
  let sharedSecret
537
635
  try {
538
- sharedSecret = MlKEM768.decapsulate(this.deserializeKey(cipherText), this.privkey)
636
+ sharedSecret = decapsParams.kem.decapsulate(deserializedCipherText, decapsPrivkey)
539
637
  } catch (e) {
540
638
  console.error('Wallet::decryptMessage() - Decapsulation failed', e)
541
639
  console.info('Wallet::decryptMessage() - my public key', this.pubkey)
@@ -587,11 +685,11 @@ export default class Wallet {
587
685
  }
588
686
 
589
687
  /**
590
- * Post-quantum (ML-KEM768) `CipherHash` request envelope: a stringified single-recipient
688
+ * Post-quantum (ML-KEM) `CipherHash` request envelope: a stringified single-recipient
591
689
  * map `{ "<hashShare(recipientPubkey)>": {cipherText, encryptedMessage} }` (object-valued,
592
690
  * via {@link encryptMessage}). Matches the Rust validator's CipherHash handler. PQ Phase E.
593
691
  */
594
- async encryptStringML768 (message, recipientPubkey) {
692
+ async encryptStringML (message, recipientPubkey) {
595
693
  const envelope = await this.encryptMessage(message, recipientPubkey)
596
694
  return JSON.stringify({ [this.hashShare(recipientPubkey)]: envelope })
597
695
  }
@@ -601,8 +699,17 @@ export default class Wallet {
601
699
  * (`hashShare(this.pubkey)`) → the RAW decrypted GraphQL response JSON text (NOT JSON.parsed;
602
700
  * it replaces the HTTP response body for the normal parser). `null` if no entry / decrypt fails.
603
701
  */
604
- async decryptMyMessageML768 (map) {
605
- const envelope = map[this.hashShare(this.pubkey)]
702
+ async decryptMyMessageML (map) {
703
+ let envelope = map[this.hashShare(this.pubkey)]
704
+ if (!envelope) {
705
+ // Inbound permissive: try the hash share of the on-demand derived other-set pubkey.
706
+ // A secret-less wallet derives nothing, so the lookup is simply skipped.
707
+ const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024
708
+ const other = this._deriveMlKemKeypair(otherSet)
709
+ if (other) {
710
+ envelope = map[this.hashShare(other.pubkey)]
711
+ }
712
+ }
606
713
  if (!envelope) {
607
714
  return null
608
715
  }
@@ -63,6 +63,11 @@ class UrqlClientWrapper {
63
63
  return createClient({
64
64
  url: serverUri,
65
65
  exchanges,
66
+ // urql 5 had no default and always POSTed; urql 6 defaults to 'within-url-limit', which
67
+ // URL-encodes short queries and sends NO body. That would silently disable the CipherHash
68
+ // envelope below — cipherFetch's `typeof init.body === 'string'` guard fails on a GET, so
69
+ // the query would leave as plaintext URL parameters with no error. Pin POST explicitly.
70
+ preferGetMethod: false,
66
71
  // PQ-transport Phase E: when encryption is on, route fetch through the CipherHash
67
72
  // wrapper (encrypt the request body to the validator's ML-KEM pubkey, decrypt the
68
73
  // response). Undefined → urql uses the global fetch (plaintext).
@@ -114,7 +119,7 @@ class UrqlClientWrapper {
114
119
  let requestInit = init
115
120
 
116
121
  if (wallet && serverPubkey && init && typeof init.body === 'string' && this.shouldEncrypt(init.body)) {
117
- const hashVar = await wallet.encryptStringML768(init.body, serverPubkey)
122
+ const hashVar = await wallet.encryptStringML(init.body, serverPubkey)
118
123
  requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) }
119
124
  encryptedRequest = true
120
125
  }
@@ -138,7 +143,7 @@ class UrqlClientWrapper {
138
143
  // Plaintext (e.g. a validator-side error response) — pass through unchanged.
139
144
  return new Response(text, init2)
140
145
  }
141
- const decrypted = await wallet.decryptMyMessageML768(JSON.parse(hash))
146
+ const decrypted = await wallet.decryptMyMessageML(JSON.parse(hash))
142
147
  return new Response(decrypted != null ? decrypted : text, init2)
143
148
  }
144
149