@wishknish/knishio-client-js 0.8.2 → 0.9.2
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/dist/client.cjs.js +29 -102
- package/dist/client.cjs.js.map +1 -1
- package/dist/client.es.mjs +978 -958
- package/dist/client.es.mjs.map +1 -1
- package/dist/client.iife.js +29 -102
- package/dist/client.iife.js.map +1 -1
- package/package.json +9 -13
- package/src/KnishIOClient.js +112 -46
- package/src/Molecule.js +36 -30
- package/src/Wallet.js +98 -3
- package/src/index.js +0 -4
- package/src/libraries/urql/UrqlClientWrapper.js +88 -0
- package/src/mutation/MutationTransferTokens.js +18 -0
- package/src/query/QueryUserActivity.js +0 -151
- package/src/response/ResponseQueryUserActivity.js +0 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wishknish/knishio-client-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
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.",
|
|
@@ -65,22 +65,18 @@
|
|
|
65
65
|
"wonka": "^6.3.5"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@jest/globals": "^
|
|
68
|
+
"@jest/globals": "^30.4.1",
|
|
69
69
|
"@rollup/plugin-babel": "^6.1.0",
|
|
70
70
|
"@rollup/plugin-commonjs": "^28.0.9",
|
|
71
71
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
72
|
+
"@swc/core": "^1.15.43",
|
|
73
|
+
"@swc/jest": "^0.2.39",
|
|
72
74
|
"buffer": "^6.0.3",
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"eslint-plugin-import": "^2.32.0",
|
|
77
|
-
"eslint-plugin-n": "^16.6.2",
|
|
78
|
-
"eslint-plugin-promise": "^6.6.0",
|
|
79
|
-
"eslint-plugin-vue": "^9.33.0",
|
|
80
|
-
"esmock": "^2.7.3",
|
|
81
|
-
"jest": "^29.7.0",
|
|
75
|
+
"eslint": "^9.39.0",
|
|
76
|
+
"jest": "^30.4.2",
|
|
77
|
+
"neostandard": "^0.13.0",
|
|
82
78
|
"rollup": "^4.57.1",
|
|
83
|
-
"vite": "^
|
|
79
|
+
"vite": "^7.3.5"
|
|
84
80
|
},
|
|
85
81
|
"browserslist": [
|
|
86
82
|
"> 1%",
|
|
@@ -89,7 +85,7 @@
|
|
|
89
85
|
],
|
|
90
86
|
"scripts": {
|
|
91
87
|
"build": "vite build --config build/vite.config.mjs",
|
|
92
|
-
"lint": "eslint
|
|
88
|
+
"lint": "eslint src",
|
|
93
89
|
"test": "jest",
|
|
94
90
|
"test:coverage": "jest --coverage",
|
|
95
91
|
"selftest": "npm run build && node tests/scripts/self-test.js",
|
package/src/KnishIOClient.js
CHANGED
|
@@ -88,7 +88,6 @@ import ActiveWalletSubscribe from './subscribe/ActiveWalletSubscribe.js'
|
|
|
88
88
|
import ActiveSessionSubscribe from './subscribe/ActiveSessionSubscribe.js'
|
|
89
89
|
import MutationActiveSession from './mutation/MutationActiveSession.js'
|
|
90
90
|
import QueryActiveSession from './query/QueryActiveSession.js'
|
|
91
|
-
import QueryUserActivity from './query/QueryUserActivity.js'
|
|
92
91
|
import QueryToken from './query/QueryToken.js'
|
|
93
92
|
import BatchIdException from './exception/BatchIdException.js'
|
|
94
93
|
import AuthorizationRejectedException from './exception/AuthorizationRejectedException.js'
|
|
@@ -1216,50 +1215,6 @@ export default class KnishIOClient {
|
|
|
1216
1215
|
})
|
|
1217
1216
|
}
|
|
1218
1217
|
|
|
1219
|
-
/**
|
|
1220
|
-
* Queries user activity based on the provided parameters.
|
|
1221
|
-
*
|
|
1222
|
-
* @param {string} bundleHash - The bundle hash.
|
|
1223
|
-
* @param {string} metaType - The meta type.
|
|
1224
|
-
* @param {string} metaId - The meta ID.
|
|
1225
|
-
* @param {string} ipAddress - The IP address.
|
|
1226
|
-
* @param {string} browser - The browser.
|
|
1227
|
-
* @param {string} osCpu - The operating system and CPU.
|
|
1228
|
-
* @param {string} resolution - The screen resolution.
|
|
1229
|
-
* @param {string} timeZone - The time zone.
|
|
1230
|
-
* @param {string} countBy - The count by parameter.
|
|
1231
|
-
* @param {string} interval - The interval parameter.
|
|
1232
|
-
*
|
|
1233
|
-
* @returns {Promise<ResponseQueryUserActivity>} The result of the query.
|
|
1234
|
-
*/
|
|
1235
|
-
async queryUserActivity ({
|
|
1236
|
-
bundleHash,
|
|
1237
|
-
metaType,
|
|
1238
|
-
metaId,
|
|
1239
|
-
ipAddress,
|
|
1240
|
-
browser,
|
|
1241
|
-
osCpu,
|
|
1242
|
-
resolution,
|
|
1243
|
-
timeZone,
|
|
1244
|
-
countBy,
|
|
1245
|
-
interval
|
|
1246
|
-
}) {
|
|
1247
|
-
const query = this.createQuery(QueryUserActivity)
|
|
1248
|
-
|
|
1249
|
-
return await this.executeQuery(query, {
|
|
1250
|
-
bundleHash,
|
|
1251
|
-
metaType,
|
|
1252
|
-
metaId,
|
|
1253
|
-
ipAddress,
|
|
1254
|
-
browser,
|
|
1255
|
-
osCpu,
|
|
1256
|
-
resolution,
|
|
1257
|
-
timeZone,
|
|
1258
|
-
countBy,
|
|
1259
|
-
interval
|
|
1260
|
-
})
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
1218
|
/**
|
|
1264
1219
|
* Builds and executes a molecule to declare an active session for the given MetaType
|
|
1265
1220
|
*
|
|
@@ -1966,6 +1921,109 @@ export default class KnishIOClient {
|
|
|
1966
1921
|
return await this.executeQuery(query)
|
|
1967
1922
|
}
|
|
1968
1923
|
|
|
1924
|
+
/**
|
|
1925
|
+
* Transfers tokens from one source wallet to MULTIPLE recipients in a single molecule.
|
|
1926
|
+
*
|
|
1927
|
+
* One molecule funds N recipients (WP line 544: "utilizing the sender's Wallet to fund multiple
|
|
1928
|
+
* recipients with one transaction"). Each recipient receives its own amount and, for stackable
|
|
1929
|
+
* tokens, its own subset of token units. The source is fully drained and the unsent change
|
|
1930
|
+
* returns via the remainder atom (UTXO), so the V-atoms conserve. Single-recipient transfers
|
|
1931
|
+
* should keep using transferToken(); this is its N-recipient generalization.
|
|
1932
|
+
*
|
|
1933
|
+
* @param {Object} options - The transfer options.
|
|
1934
|
+
* @param {string} options.token - The token to transfer.
|
|
1935
|
+
* @param {Array} options.recipients - [{ bundleHash, amount?, units?, batchId? }] per recipient.
|
|
1936
|
+
* @param {Object} [options.sourceWallet=null] - The source wallet; queried if not provided.
|
|
1937
|
+
*
|
|
1938
|
+
* @returns {Promise} - A Promise that resolves to the transaction result.
|
|
1939
|
+
*
|
|
1940
|
+
* @throws {StackableUnitAmountException} - If a recipient provides both amount and units.
|
|
1941
|
+
* @throws {TransferBalanceException} - If the source wallet does not have enough balance.
|
|
1942
|
+
*/
|
|
1943
|
+
async transferTokens ({
|
|
1944
|
+
token,
|
|
1945
|
+
recipients,
|
|
1946
|
+
sourceWallet = null
|
|
1947
|
+
}) {
|
|
1948
|
+
// Per-recipient amount: units.length for stackable, else the explicit amount
|
|
1949
|
+
const amounts = recipients.map(recipient => {
|
|
1950
|
+
const units = recipient.units || []
|
|
1951
|
+
if (units.length > 0) {
|
|
1952
|
+
// Can't move stackable units AND provide amount
|
|
1953
|
+
if (recipient.amount > 0) {
|
|
1954
|
+
throw new StackableUnitAmountException()
|
|
1955
|
+
}
|
|
1956
|
+
return units.length
|
|
1957
|
+
}
|
|
1958
|
+
return recipient.amount || 0
|
|
1959
|
+
})
|
|
1960
|
+
const total = amounts.reduce((sum, value) => sum + value, 0)
|
|
1961
|
+
|
|
1962
|
+
// Get a source wallet (loads its token units)
|
|
1963
|
+
if (sourceWallet === null) {
|
|
1964
|
+
sourceWallet = await this.querySourceWallet({
|
|
1965
|
+
token,
|
|
1966
|
+
amount: total
|
|
1967
|
+
})
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Do you have enough tokens?
|
|
1971
|
+
if (sourceWallet === null || Decimal.cmp(sourceWallet.balance, total) < 0) {
|
|
1972
|
+
throw new TransferBalanceException()
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// Build a shadow recipient wallet per recipient, each with its own batch ID
|
|
1976
|
+
const recipientWallets = recipients.map(recipient => {
|
|
1977
|
+
const recipientWallet = Wallet.create({
|
|
1978
|
+
bundle: recipient.bundleHash,
|
|
1979
|
+
token
|
|
1980
|
+
})
|
|
1981
|
+
|
|
1982
|
+
// Compute the batch ID for the recipient (typically used by stackable tokens)
|
|
1983
|
+
if (recipient.batchId !== undefined && recipient.batchId !== null) {
|
|
1984
|
+
recipientWallet.batchId = recipient.batchId
|
|
1985
|
+
} else {
|
|
1986
|
+
recipientWallet.initBatchId({
|
|
1987
|
+
sourceWallet
|
|
1988
|
+
})
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
return recipientWallet
|
|
1992
|
+
})
|
|
1993
|
+
|
|
1994
|
+
// Create a remainder from the source wallet
|
|
1995
|
+
const remainderWallet = sourceWallet.createRemainder(this.getSecret())
|
|
1996
|
+
|
|
1997
|
+
// --- Token units splitting (N-way): source keeps the union, each recipient its subset,
|
|
1998
|
+
// remainder the kept units
|
|
1999
|
+
sourceWallet.splitUnitsMulti(
|
|
2000
|
+
recipients.map(recipient => recipient.units || []),
|
|
2001
|
+
recipientWallets,
|
|
2002
|
+
remainderWallet
|
|
2003
|
+
)
|
|
2004
|
+
// ---
|
|
2005
|
+
|
|
2006
|
+
// Build the molecule itself
|
|
2007
|
+
const molecule = await this.createMolecule({
|
|
2008
|
+
sourceWallet,
|
|
2009
|
+
remainderWallet
|
|
2010
|
+
})
|
|
2011
|
+
/**
|
|
2012
|
+
* @type {MutationTransferTokens}
|
|
2013
|
+
*/
|
|
2014
|
+
const query = await this.createMoleculeMutation({
|
|
2015
|
+
mutationClass: MutationTransferTokens,
|
|
2016
|
+
molecule
|
|
2017
|
+
})
|
|
2018
|
+
|
|
2019
|
+
query.fillMoleculeMulti({
|
|
2020
|
+
recipientWallets,
|
|
2021
|
+
amounts
|
|
2022
|
+
})
|
|
2023
|
+
|
|
2024
|
+
return await this.executeQuery(query)
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1969
2027
|
/**
|
|
1970
2028
|
* Deposits buffer token into the source wallet.
|
|
1971
2029
|
*
|
|
@@ -2343,7 +2401,15 @@ export default class KnishIOClient {
|
|
|
2343
2401
|
molecule
|
|
2344
2402
|
})
|
|
2345
2403
|
|
|
2346
|
-
|
|
2404
|
+
// PQ-transport Phase E (cycle 163): convey the AUTH source wallet's ML-KEM768 public key as a
|
|
2405
|
+
// SIGNED `walletPubkey` meta on the U-atom (fillMolecule → initAuthorization → sign), so the
|
|
2406
|
+
// validator can encrypt CipherHash responses back to THIS wallet (the one that decrypts them).
|
|
2407
|
+
// Signed → tamper-proof. Only when present (PQ-capable wallet).
|
|
2408
|
+
const authMeta = { encrypt: (encrypt ? 'true' : 'false') }
|
|
2409
|
+
if (wallet.pubkey) {
|
|
2410
|
+
authMeta.walletPubkey = wallet.pubkey
|
|
2411
|
+
}
|
|
2412
|
+
query.fillMolecule({ meta: authMeta })
|
|
2347
2413
|
/**
|
|
2348
2414
|
* @type {ResponseRequestAuthorization}
|
|
2349
2415
|
*/
|
package/src/Molecule.js
CHANGED
|
@@ -577,6 +577,11 @@ export default class Molecule {
|
|
|
577
577
|
recipientWallet,
|
|
578
578
|
amount
|
|
579
579
|
}) {
|
|
580
|
+
// Canonical token transfer (incl. stackable): a UTXO-style FULL-balance debit + remainder.
|
|
581
|
+
// The source wallet is fully drained (-balance) and the change returns via the remainder atom,
|
|
582
|
+
// so the 3 V-atoms conserve (sum == 0) and the validator's stackable full-drain guard accepts.
|
|
583
|
+
// Per-unit (stackable) movement is layered by calling Wallet.splitUnits(units, remainder,
|
|
584
|
+
// recipient) BEFORE this — there is no separate partial-debit transfer mode.
|
|
580
585
|
if (this.sourceWallet.balance - amount < 0) {
|
|
581
586
|
throw new BalanceInsufficientException()
|
|
582
587
|
}
|
|
@@ -608,54 +613,55 @@ export default class Molecule {
|
|
|
608
613
|
}
|
|
609
614
|
|
|
610
615
|
/**
|
|
611
|
-
*
|
|
612
|
-
* source debit, recipient credit, remainder.
|
|
613
|
-
* Propagates batchId from source wallet.
|
|
616
|
+
* Multi-recipient value transfer (WP line 544: one molecule funds N recipients).
|
|
614
617
|
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
618
|
+
* Builds 1 source atom (full-balance debit, carrying the SENT union of all units) + one atom
|
|
619
|
+
* per recipient (its amount + its own units, walletBundle-bonded) + 1 remainder atom (the KEPT
|
|
620
|
+
* change). Conservation: -balance + Σ amounts + (balance - Σ amounts) == 0.
|
|
621
|
+
*
|
|
622
|
+
* Per-unit (stackable) routing is layered by calling Wallet.splitUnitsMulti(...) BEFORE this —
|
|
623
|
+
* it sets the source's tokenUnits to the union, each recipient's to its subset, and the
|
|
624
|
+
* remainder's to the kept units. The single-recipient initValue() is the N=1 special case.
|
|
625
|
+
*
|
|
626
|
+
* @param {Wallet[]} recipientWallets
|
|
627
|
+
* @param {number[]} amounts - parallel to recipientWallets
|
|
617
628
|
* @return {Molecule}
|
|
618
629
|
*/
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
630
|
+
initValues ({
|
|
631
|
+
recipientWallets,
|
|
632
|
+
amounts
|
|
622
633
|
}) {
|
|
623
|
-
|
|
624
|
-
throw new NegativeAmountException('Molecule::addStackableTransfer() - Amount must be positive!')
|
|
625
|
-
}
|
|
634
|
+
const total = amounts.reduce((sum, value) => sum + Number(value), 0)
|
|
626
635
|
|
|
627
|
-
if (this.sourceWallet.balance -
|
|
636
|
+
if (this.sourceWallet.balance - total < 0) {
|
|
628
637
|
throw new BalanceInsufficientException()
|
|
629
638
|
}
|
|
630
639
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// Source debit atom
|
|
640
|
+
// Source atom: debit the full balance (UTXO); carries the SENT union of all units
|
|
634
641
|
this.addAtom(Atom.create({
|
|
635
642
|
isotope: 'V',
|
|
636
643
|
wallet: this.sourceWallet,
|
|
637
|
-
value: -
|
|
638
|
-
batchId
|
|
644
|
+
value: -this.sourceWallet.balance
|
|
639
645
|
}))
|
|
640
646
|
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
647
|
+
// One atom per recipient: its amount + its own units, bonded to the recipient bundle
|
|
648
|
+
recipientWallets.forEach((recipientWallet, index) => {
|
|
649
|
+
this.addAtom(Atom.create({
|
|
650
|
+
isotope: 'V',
|
|
651
|
+
wallet: recipientWallet,
|
|
652
|
+
value: amounts[index],
|
|
653
|
+
metaType: 'walletBundle',
|
|
654
|
+
metaId: recipientWallet.bundle
|
|
655
|
+
}))
|
|
656
|
+
})
|
|
650
657
|
|
|
651
|
-
// Remainder atom
|
|
658
|
+
// Remainder atom: the change back to the sender; carries the KEPT units
|
|
652
659
|
this.addAtom(Atom.create({
|
|
653
660
|
isotope: 'V',
|
|
654
661
|
wallet: this.remainderWallet,
|
|
655
|
-
value: this.sourceWallet.balance -
|
|
662
|
+
value: this.sourceWallet.balance - total,
|
|
656
663
|
metaType: 'walletBundle',
|
|
657
|
-
metaId: this.remainderWallet.bundle
|
|
658
|
-
batchId
|
|
664
|
+
metaId: this.remainderWallet.bundle
|
|
659
665
|
}))
|
|
660
666
|
|
|
661
667
|
return this
|
package/src/Wallet.js
CHANGED
|
@@ -398,6 +398,47 @@ export default class Wallet {
|
|
|
398
398
|
remainderWallet.tokenUnits = remainderTokenUnits
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Split token units across MULTIPLE recipients (WP line 544).
|
|
403
|
+
*
|
|
404
|
+
* The source retains the SENT union (all units leaving), each recipient gets its own subset,
|
|
405
|
+
* and the remainder gets the KEPT units (those not assigned to any recipient). Mirrors
|
|
406
|
+
* splitUnits() but N-way: the source's tokenUnits become the union (not a single recipient's
|
|
407
|
+
* subset) so the source atom carries the full SENT set the validator reads as the authority.
|
|
408
|
+
*
|
|
409
|
+
* @param {array[]} recipientUnitLists - array of unit-id arrays, parallel to recipientWallets
|
|
410
|
+
* @param {Wallet[]} recipientWallets
|
|
411
|
+
* @param {Wallet} remainderWallet
|
|
412
|
+
*/
|
|
413
|
+
splitUnitsMulti (
|
|
414
|
+
recipientUnitLists,
|
|
415
|
+
recipientWallets,
|
|
416
|
+
remainderWallet
|
|
417
|
+
) {
|
|
418
|
+
// The union of all unit ids leaving the source
|
|
419
|
+
const sentIds = new Set()
|
|
420
|
+
recipientUnitLists.forEach(unitList => {
|
|
421
|
+
unitList.forEach(id => sentIds.add(id))
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// Nothing to split (fungible transfer) — leave token units untouched
|
|
425
|
+
if (sentIds.size === 0) {
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Each recipient gets its own subset of the source's token units
|
|
430
|
+
recipientWallets.forEach((recipientWallet, index) => {
|
|
431
|
+
const ids = recipientUnitLists[index]
|
|
432
|
+
recipientWallet.tokenUnits = this.tokenUnits.filter(tokenUnit => ids.includes(tokenUnit.id))
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
// The remainder keeps everything not sent to any recipient
|
|
436
|
+
remainderWallet.tokenUnits = this.tokenUnits.filter(tokenUnit => !sentIds.has(tokenUnit.id))
|
|
437
|
+
|
|
438
|
+
// The source carries the SENT union (the ownership authority the validator reads)
|
|
439
|
+
this.tokenUnits = this.tokenUnits.filter(tokenUnit => sentIds.has(tokenUnit.id))
|
|
440
|
+
}
|
|
441
|
+
|
|
401
442
|
/**
|
|
402
443
|
* Create a remainder wallet from the source one
|
|
403
444
|
*
|
|
@@ -445,6 +486,18 @@ export default class Wallet {
|
|
|
445
486
|
const messageString = JSON.stringify(message)
|
|
446
487
|
const messageUint8 = new TextEncoder().encode(messageString)
|
|
447
488
|
const deserializedPubkey = this.deserializeKey(recipientPubkey)
|
|
489
|
+
// ML-KEM-768 public keys are exactly 1184 bytes. A wrong-length key here almost always means the
|
|
490
|
+
// node did not advertise an ML-KEM public key in its auth `key` field (e.g. a validator predating
|
|
491
|
+
// the PQ-transport build). Fail with an actionable message rather than the crypto lib's cryptic
|
|
492
|
+
// `"publicKey" expected Uint8Array of length 1184, got length=N` assertion.
|
|
493
|
+
const ML_KEM_768_PUBLIC_KEY_BYTES = 1184
|
|
494
|
+
if (deserializedPubkey.length !== ML_KEM_768_PUBLIC_KEY_BYTES) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`KnishIO: cannot ML-KEM-encrypt — recipient public key is ${deserializedPubkey.length} bytes, ` +
|
|
497
|
+
`expected ${ML_KEM_768_PUBLIC_KEY_BYTES} (ML-KEM-768). The node likely did not advertise an ML-KEM ` +
|
|
498
|
+
'public key (upgrade the validator to a PQ-transport build), or authenticate with { encrypt: false }.'
|
|
499
|
+
)
|
|
500
|
+
}
|
|
448
501
|
const { cipherText, sharedSecret } = MlKEM768.encapsulate(deserializedPubkey)
|
|
449
502
|
const encryptedMessage = await this.encryptWithSharedSecret(messageUint8, sharedSecret)
|
|
450
503
|
return {
|
|
@@ -454,6 +507,17 @@ export default class Wallet {
|
|
|
454
507
|
}
|
|
455
508
|
|
|
456
509
|
async decryptMessage (encryptedData) {
|
|
510
|
+
const decryptedString = await this._mlkemDecryptToString(encryptedData)
|
|
511
|
+
return decryptedString === null ? null : JSON.parse(decryptedString)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* ML-KEM768 decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string
|
|
516
|
+
* (no JSON.parse). Shared by {@link decryptMessage} (which JSON.parses the result)
|
|
517
|
+
* and the PQ CipherHash transport ({@link decryptMyMessageML768}, which needs the raw
|
|
518
|
+
* response JSON text). PQ-transport Phase E (cycle 163).
|
|
519
|
+
*/
|
|
520
|
+
async _mlkemDecryptToString (encryptedData) {
|
|
457
521
|
const { cipherText, encryptedMessage } = encryptedData
|
|
458
522
|
|
|
459
523
|
let sharedSecret
|
|
@@ -486,9 +550,8 @@ export default class Wallet {
|
|
|
486
550
|
return null
|
|
487
551
|
}
|
|
488
552
|
|
|
489
|
-
let decryptedString
|
|
490
553
|
try {
|
|
491
|
-
|
|
554
|
+
return new TextDecoder().decode(decryptedUint8)
|
|
492
555
|
} catch (e) {
|
|
493
556
|
console.warn('Wallet::decryptMessage() - Decoding failed', e)
|
|
494
557
|
console.info('Wallet::decryptMessage() - my public key', this.pubkey)
|
|
@@ -497,8 +560,40 @@ export default class Wallet {
|
|
|
497
560
|
console.info('Wallet::decryptMessage() - decrypted Uint8Array', decryptedUint8)
|
|
498
561
|
return null
|
|
499
562
|
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Multi-recipient map key for a public key: `Base64_standard(SHAKE256(pubkey_utf8, 8 bytes))`
|
|
567
|
+
* — matches the validator's `hash_share` and the other SDKs' `hashShare`/`shortHash`.
|
|
568
|
+
* `shake256(pubkey, 64)` = 64 bits = 8 bytes, hex; hex-decode → standard base64. PQ Phase E.
|
|
569
|
+
*/
|
|
570
|
+
hashShare (pubkey) {
|
|
571
|
+
const hex = shake256(pubkey, 64)
|
|
572
|
+
const bytes = Uint8Array.from(hex.match(/.{2}/g).map(b => parseInt(b, 16)))
|
|
573
|
+
return this.serializeKey(bytes)
|
|
574
|
+
}
|
|
500
575
|
|
|
501
|
-
|
|
576
|
+
/**
|
|
577
|
+
* Post-quantum (ML-KEM768) `CipherHash` request envelope: a stringified single-recipient
|
|
578
|
+
* map `{ "<hashShare(recipientPubkey)>": {cipherText, encryptedMessage} }` (object-valued,
|
|
579
|
+
* via {@link encryptMessage}). Matches the Rust validator's CipherHash handler. PQ Phase E.
|
|
580
|
+
*/
|
|
581
|
+
async encryptStringML768 (message, recipientPubkey) {
|
|
582
|
+
const envelope = await this.encryptMessage(message, recipientPubkey)
|
|
583
|
+
return JSON.stringify({ [this.hashShare(recipientPubkey)]: envelope })
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Decrypt a `CipherHash` response map addressed to THIS wallet's ML-KEM pubkey
|
|
588
|
+
* (`hashShare(this.pubkey)`) → the RAW decrypted GraphQL response JSON text (NOT JSON.parsed;
|
|
589
|
+
* it replaces the HTTP response body for the normal parser). `null` if no entry / decrypt fails.
|
|
590
|
+
*/
|
|
591
|
+
async decryptMyMessageML768 (map) {
|
|
592
|
+
const envelope = map[this.hashShare(this.pubkey)]
|
|
593
|
+
if (!envelope) {
|
|
594
|
+
return null
|
|
595
|
+
}
|
|
596
|
+
return this._mlkemDecryptToString(envelope)
|
|
502
597
|
}
|
|
503
598
|
|
|
504
599
|
async encryptWithSharedSecret (message, sharedSecret) {
|
package/src/index.js
CHANGED
|
@@ -91,7 +91,6 @@ import QueryMetaTypeViaAtom from './query/QueryMetaTypeViaAtom.js'
|
|
|
91
91
|
import QueryMetaTypeViaMolecule from './query/QueryMetaTypeViaMolecule.js'
|
|
92
92
|
import QueryPolicy from './query/QueryPolicy.js'
|
|
93
93
|
import QueryToken from './query/QueryToken.js'
|
|
94
|
-
import QueryUserActivity from './query/QueryUserActivity.js'
|
|
95
94
|
import QueryWalletBundle from './query/QueryWalletBundle.js'
|
|
96
95
|
import QueryWalletList from './query/QueryWalletList.js'
|
|
97
96
|
|
|
@@ -142,7 +141,6 @@ import ResponsePeering from './response/ResponsePeering.js'
|
|
|
142
141
|
import ResponsePolicy from './response/ResponsePolicy.js'
|
|
143
142
|
import ResponseProposeMolecule from './response/ResponseProposeMolecule.js'
|
|
144
143
|
import ResponseQueryActiveSession from './response/ResponseQueryActiveSession.js'
|
|
145
|
-
import ResponseQueryUserActivity from './response/ResponseQueryUserActivity.js'
|
|
146
144
|
import ResponseRequestAuthorization from './response/ResponseRequestAuthorization.js'
|
|
147
145
|
import ResponseRequestAuthorizationGuest from './response/ResponseRequestAuthorizationGuest.js'
|
|
148
146
|
import ResponseRequestTokens from './response/ResponseRequestTokens.js'
|
|
@@ -253,7 +251,6 @@ export {
|
|
|
253
251
|
QueryMetaTypeViaMolecule,
|
|
254
252
|
QueryPolicy,
|
|
255
253
|
QueryToken,
|
|
256
|
-
QueryUserActivity,
|
|
257
254
|
QueryWalletBundle,
|
|
258
255
|
QueryWalletList,
|
|
259
256
|
|
|
@@ -298,7 +295,6 @@ export {
|
|
|
298
295
|
ResponsePolicy,
|
|
299
296
|
ResponseProposeMolecule,
|
|
300
297
|
ResponseQueryActiveSession,
|
|
301
|
-
ResponseQueryUserActivity,
|
|
302
298
|
ResponseRequestAuthorization,
|
|
303
299
|
ResponseRequestAuthorizationGuest,
|
|
304
300
|
ResponseRequestTokens,
|
|
@@ -7,6 +7,25 @@ import {
|
|
|
7
7
|
import { createClient as createWSClient } from 'graphql-ws'
|
|
8
8
|
import { pipe, map, subscribe } from 'wonka'
|
|
9
9
|
|
|
10
|
+
// PQ-transport Phase E (cycle 163): the post-quantum CipherHash wrapper query. The validator
|
|
11
|
+
// intercepts the `CipherHash` op, decrypts `$Hash`, executes the inner request, and returns the
|
|
12
|
+
// encrypted response in `{ data: { CipherHash: { hash } } }`.
|
|
13
|
+
const CIPHER_HASH_QUERY = 'query ( $Hash: String! ) { CipherHash ( Hash: $Hash ) { hash } }'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Light parse of a GraphQL request body's operation type + root field name, for the CipherHash
|
|
17
|
+
* bypass decision (no full GraphQL parse needed). Operation type = the first query/mutation/
|
|
18
|
+
* subscription keyword (default 'query' for an anonymous `{ ... }`); root field = the first
|
|
19
|
+
* identifier inside the top-level selection set.
|
|
20
|
+
*/
|
|
21
|
+
function parseOperation (query) {
|
|
22
|
+
const typeMatch = (query || '').match(/\b(query|mutation|subscription)\b/i)
|
|
23
|
+
const type = typeMatch ? typeMatch[1].toLowerCase() : 'query'
|
|
24
|
+
const braceIdx = (query || '').indexOf('{')
|
|
25
|
+
const nameMatch = braceIdx >= 0 ? query.slice(braceIdx + 1).match(/[A-Za-z_][A-Za-z0-9_]*/) : null
|
|
26
|
+
return { type, name: nameMatch ? nameMatch[0] : '' }
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
class UrqlClientWrapper {
|
|
11
30
|
constructor ({ serverUri, socket = null, encrypt = false }) {
|
|
12
31
|
this.$__client = this.createUrqlClient({ serverUri, socket, encrypt })
|
|
@@ -44,6 +63,10 @@ class UrqlClientWrapper {
|
|
|
44
63
|
return createClient({
|
|
45
64
|
url: serverUri,
|
|
46
65
|
exchanges,
|
|
66
|
+
// PQ-transport Phase E: when encryption is on, route fetch through the CipherHash
|
|
67
|
+
// wrapper (encrypt the request body to the validator's ML-KEM pubkey, decrypt the
|
|
68
|
+
// response). Undefined → urql uses the global fetch (plaintext).
|
|
69
|
+
...(encrypt ? { fetch: (input, init) => this.cipherFetch(input, init) } : {}),
|
|
47
70
|
fetchOptions: () => ({
|
|
48
71
|
headers: {
|
|
49
72
|
'X-Auth-Token': this.$__authToken
|
|
@@ -54,6 +77,71 @@ class UrqlClientWrapper {
|
|
|
54
77
|
})
|
|
55
78
|
}
|
|
56
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Whether an outgoing GraphQL request body should be wrapped in CipherHash. Bypass (plaintext):
|
|
82
|
+
* introspection `__schema`, `ContinuId`, the `AccessToken` mutation, and the U-isotope
|
|
83
|
+
* `ProposeMolecule` (auth bootstrap — the key exchange itself can't be encrypted). Mirrors the
|
|
84
|
+
* Kotlin/validator bypass set.
|
|
85
|
+
*/
|
|
86
|
+
shouldEncrypt (body) {
|
|
87
|
+
let parsed
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(body)
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
const { type, name } = parseOperation(parsed.query)
|
|
94
|
+
if (type === 'query' && (name === '__schema' || name === 'ContinuId')) return false
|
|
95
|
+
if (type === 'mutation' && name === 'AccessToken') return false
|
|
96
|
+
if (type === 'mutation' && name === 'ProposeMolecule') {
|
|
97
|
+
const isotope = parsed.variables && parsed.variables.molecule &&
|
|
98
|
+
parsed.variables.molecule.atoms && parsed.variables.molecule.atoms[0] &&
|
|
99
|
+
parsed.variables.molecule.atoms[0].isotope
|
|
100
|
+
if (isotope === 'U') return false
|
|
101
|
+
}
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Custom `fetch` that wraps a GraphQL request in the ML-KEM CipherHash envelope and decrypts the
|
|
107
|
+
* response (PQ-transport Phase E). Operates on the raw POST body (mirrors Kotlin's
|
|
108
|
+
* encryptBody/decryptBody). Reads the CURRENT client wallet + validator pubkey from auth.
|
|
109
|
+
*/
|
|
110
|
+
async cipherFetch (input, init) {
|
|
111
|
+
const wallet = this.getWallet()
|
|
112
|
+
const serverPubkey = this.getPubKey()
|
|
113
|
+
let encryptedRequest = false
|
|
114
|
+
let requestInit = init
|
|
115
|
+
|
|
116
|
+
if (wallet && serverPubkey && init && typeof init.body === 'string' && this.shouldEncrypt(init.body)) {
|
|
117
|
+
const hashVar = await wallet.encryptStringML768(init.body, serverPubkey)
|
|
118
|
+
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) }
|
|
119
|
+
encryptedRequest = true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const response = await fetch(input, requestInit)
|
|
123
|
+
if (!encryptedRequest) {
|
|
124
|
+
return response
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Decrypt the CipherHash response back to the inner GraphQL response JSON.
|
|
128
|
+
const text = await response.text()
|
|
129
|
+
const init2 = { status: response.status, statusText: response.statusText, headers: response.headers }
|
|
130
|
+
let parsed
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(text)
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return new Response(text, init2)
|
|
135
|
+
}
|
|
136
|
+
const hash = parsed && parsed.data && parsed.data.CipherHash && parsed.data.CipherHash.hash
|
|
137
|
+
if (typeof hash !== 'string') {
|
|
138
|
+
// Plaintext (e.g. a validator-side error response) — pass through unchanged.
|
|
139
|
+
return new Response(text, init2)
|
|
140
|
+
}
|
|
141
|
+
const decrypted = await wallet.decryptMyMessageML768(JSON.parse(hash))
|
|
142
|
+
return new Response(decrypted != null ? decrypted : text, init2)
|
|
143
|
+
}
|
|
144
|
+
|
|
57
145
|
setAuthData ({ token, pubkey, wallet }) {
|
|
58
146
|
this.$__authToken = token
|
|
59
147
|
this.$__pubkey = pubkey
|
|
@@ -70,6 +70,24 @@ export default class MutationTransferTokens extends MutationProposeMolecule {
|
|
|
70
70
|
this.$__molecule.check(this.$__molecule.sourceWallet)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Fills the Molecule for a MULTI-recipient transfer (WP line 544)
|
|
75
|
+
*
|
|
76
|
+
* @param {Wallet[]} recipientWallets
|
|
77
|
+
* @param {number[]} amounts - parallel to recipientWallets
|
|
78
|
+
*/
|
|
79
|
+
fillMoleculeMulti ({
|
|
80
|
+
recipientWallets,
|
|
81
|
+
amounts
|
|
82
|
+
}) {
|
|
83
|
+
this.$__molecule.initValues({
|
|
84
|
+
recipientWallets,
|
|
85
|
+
amounts
|
|
86
|
+
})
|
|
87
|
+
this.$__molecule.sign({})
|
|
88
|
+
this.$__molecule.check(this.$__molecule.sourceWallet)
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
/**
|
|
74
92
|
* Builds a Response object out of a JSON string
|
|
75
93
|
*
|