@wishknish/knishio-client-ts 0.9.5 → 0.9.7

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-ts",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "productName": "Knish.IO TypeScript SDK Client",
5
5
  "description": "TypeScript implementation of the Knish.IO SDK to consume Knish.IO GraphQL APIs with enhanced type safety.",
6
6
  "license": "GPL-3.0-or-later",
@@ -58,32 +58,30 @@
58
58
  "fips-203"
59
59
  ],
60
60
  "dependencies": {
61
- "@noble/post-quantum": "^0.5.4",
62
- "@thumbmarkjs/thumbmarkjs": "^0.19.1",
63
- "@urql/core": "^5.2.0",
64
- "graphql": "^16.11.0",
65
- "graphql-ws": "^6.0.7",
66
- "isomorphic-fetch": "^3.0.0",
67
- "jssha": "^3.3.1",
61
+ "@noble/post-quantum": "^0.7.0",
62
+ "@urql/core": "^6.0.3",
63
+ "graphql": "^16.14.2",
64
+ "graphql-ws": "^6.2.1",
65
+ "jssha": "^3.3.2",
68
66
  "tslib": "^2.8.1",
69
- "wonka": "^6.3.5",
67
+ "wonka": "^6.3.6",
70
68
  "zod": "^4.5.4"
71
69
  },
72
70
  "devDependencies": {
73
- "@types/isomorphic-fetch": "^0.0.39",
74
71
  "@types/jssha": "^3.0.0",
75
- "@types/node": "^22",
76
- "@typescript-eslint/eslint-plugin": "^8.63.0",
77
- "@typescript-eslint/parser": "^8.63.0",
78
- "@vitest/coverage-v8": "^4.1.10",
79
- "eslint": "^9",
80
- "prettier": "^3",
72
+ "@types/node": "^26",
73
+ "@typescript-eslint/eslint-plugin": "^8.67.0",
74
+ "@typescript-eslint/parser": "^8.67.0",
75
+ "@vitest/coverage-v8": "^4.1.11",
76
+ "eslint": "^10",
77
+ "prettier": "^3.9.6",
81
78
  "tsup": "^8.5.1",
82
- "tsx": "^4.23.0",
79
+ "tsx": "^4.23.12",
83
80
  "typescript": "^5",
84
- "vite": "^7",
85
- "vite-plugin-dts": "^5.0.3",
86
- "vitest": "^4.1.10"
81
+ "vitest": "^4.1.11"
82
+ },
83
+ "resolutions": {
84
+ "wonka": "6.3.6"
87
85
  },
88
86
  "browserslist": [
89
87
  "> 1%",
@@ -103,7 +101,6 @@
103
101
  "test:watch": "vitest",
104
102
  "test:coverage": "vitest run --coverage",
105
103
  "test:ui": "vitest --ui",
106
- "dev": "vite",
107
104
  "prepublishOnly": "npm run build",
108
105
  "selftest": "npm run build && node self-test.cjs"
109
106
  },
@@ -104,6 +104,7 @@ import MutationLinkIdentifier from '@/mutation/MutationLinkIdentifier'
104
104
  import MutationPeering from '@/mutation/MutationPeering'
105
105
  import MutationAppendRequest from '@/mutation/MutationAppendRequest'
106
106
  import MutationProposeMolecule from '@/mutation/MutationProposeMolecule'
107
+ import MutationReplenishToken from '@/mutation/MutationReplenishToken'
107
108
 
108
109
  // Response imports
109
110
  import ResponseProposeMolecule from '@/response/ResponseProposeMolecule'
@@ -127,6 +128,8 @@ import {
127
128
  SignatureMismatchException,
128
129
  AtomIndexException
129
130
  } from '@/exception'
131
+ import type { ISecretStorageProvider } from '@/types/storage'
132
+ import MemorySecretStorageProvider from '@/storage/MemorySecretStorageProvider'
130
133
 
131
134
  // Type imports
132
135
  import type {
@@ -149,6 +152,7 @@ import type {
149
152
  export default class KnishIOClient {
150
153
  private $__secret: string = ''
151
154
  private $__bundle: string = ''
155
+ private $__secretStorage: ISecretStorageProvider | null = null
152
156
  private $__cellSlug: string | null = null
153
157
  private $__encrypt: boolean = false
154
158
  private $__uris: string[] = []
@@ -184,6 +188,7 @@ export default class KnishIOClient {
184
188
  serverSdkVersion?: number
185
189
  logging?: boolean
186
190
  defaultRequestPolicy?: RequestPolicy | null
191
+ secretStorage?: ISecretStorageProvider | null
187
192
  }) {
188
193
  // Phase 2 Enhancement: Use standardized configuration validation
189
194
  const standardValidationResult = ConfigValidator.validateClientConfig(config)
@@ -235,6 +240,10 @@ export default class KnishIOClient {
235
240
  logging,
236
241
  defaultRequestPolicy
237
242
  })
243
+
244
+ if (config.secretStorage) {
245
+ this.$__secretStorage = config.secretStorage
246
+ }
238
247
  }
239
248
 
240
249
  /**
@@ -364,6 +373,7 @@ export default class KnishIOClient {
364
373
  reset(): void {
365
374
  this.$__secret = ''
366
375
  this.$__bundle = ''
376
+ this.$__secretStorage = null
367
377
  this.$__encrypt = false
368
378
  this.$__cellSlug = null
369
379
  this.$__authToken = null
@@ -433,7 +443,7 @@ export default class KnishIOClient {
433
443
  * Returns whether a secret is stored for this session
434
444
  */
435
445
  hasSecret(): boolean {
436
- return !!this.$__secret && this.$__secret.length > 0
446
+ return (!!this.$__secret && this.$__secret.length > 0) || (!!this.$__secretStorage && !!this.$__bundle && this.$__bundle.length > 0)
437
447
  }
438
448
 
439
449
  /**
@@ -446,6 +456,36 @@ export default class KnishIOClient {
446
456
  return this.$__secret
447
457
  }
448
458
 
459
+ /**
460
+ * Sets the secret storage provider and optionally sets the bundle hash
461
+ */
462
+ setSecretStorage(storage: ISecretStorageProvider, bundleHash?: string): void {
463
+ this.$__secretStorage = storage
464
+ if (bundleHash) {
465
+ this.$__bundle = bundleHash
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Returns current secret storage provider
471
+ */
472
+ getSecretStorage(): ISecretStorageProvider | null {
473
+ return this.$__secretStorage
474
+ }
475
+
476
+ /**
477
+ * Asynchronously retrieves the secret from storage or returns in-memory secret
478
+ */
479
+ async retrieveSecret(options?: { passphrase?: string }): Promise<string | null> {
480
+ if (this.$__secret && this.$__secret.length > 0) {
481
+ return this.$__secret
482
+ }
483
+ if (this.$__secretStorage && this.$__bundle && this.$__bundle.length > 0) {
484
+ return await this.$__secretStorage.retrieveSecret(this.$__bundle, options)
485
+ }
486
+ return null
487
+ }
488
+
449
489
  /**
450
490
  * Returns whether a bundle hash is being stored for this session
451
491
  */
@@ -494,6 +534,14 @@ export default class KnishIOClient {
494
534
  } = {}): Promise<Molecule> {
495
535
  this.log('info', 'KnishIOClient::createMolecule() - Creating a new molecule...')
496
536
 
537
+ if (!secret) {
538
+ if (this.$__secret && this.$__secret.length > 0) {
539
+ secret = this.getSecret()
540
+ } else if (this.$__secretStorage && this.$__bundle && this.$__bundle.length > 0) {
541
+ secret = await this.$__secretStorage.retrieveSecret(this.$__bundle)
542
+ }
543
+ }
544
+
497
545
  secret = secret || this.getSecret()
498
546
  bundle = bundle || this.getBundle()
499
547
 
@@ -546,6 +594,7 @@ export default class KnishIOClient {
546
594
 
547
595
  return new Molecule({
548
596
  secret,
597
+ bundle,
549
598
  sourceWallet,
550
599
  remainderWallet: this.getRemainderWallet()!,
551
600
  cellSlug: this.getCellSlug(),
@@ -611,8 +660,9 @@ export default class KnishIOClient {
611
660
  // Guard with $__authInProcess to prevent recursive auth refresh
612
661
  if (this.$__authToken && this.$__authToken.isExpired() && !this.$__authInProcess) {
613
662
  this.log('info', 'KnishIOClient::executeQuery() - Access token is expired. Getting new one...')
663
+ const authSecret = this.$__secret || (await this.retrieveSecret()) || ''
614
664
  await this.requestAuthToken({
615
- secret: this.$__secret,
665
+ secret: authSecret,
616
666
  cellSlug: this.$__cellSlug,
617
667
  encrypt: this.$__encrypt
618
668
  })
@@ -672,6 +722,13 @@ export default class KnishIOClient {
672
722
  setSecret(secret: string): void {
673
723
  this.$__secret = secret
674
724
  this.$__bundle = generateBundleHash(secret)
725
+ if (!this.$__secretStorage) {
726
+ const memStorage = new MemorySecretStorageProvider()
727
+ memStorage.storeSecret(this.$__bundle, secret)
728
+ this.$__secretStorage = memStorage
729
+ } else {
730
+ this.$__secretStorage.storeSecret(this.$__bundle, secret)
731
+ }
675
732
  }
676
733
 
677
734
  /**
@@ -923,6 +980,11 @@ export default class KnishIOClient {
923
980
  secret = generateSecret(seed)
924
981
  }
925
982
 
983
+ // Retrieve secret from storage provider if available
984
+ if (secret === null && this.$__secretStorage && this.$__bundle) {
985
+ secret = await this.$__secretStorage.retrieveSecret(this.$__bundle)
986
+ }
987
+
926
988
  // Set cell slug if provided
927
989
  if (cellSlug) {
928
990
  this.setCellSlug(cellSlug)
@@ -1836,27 +1898,55 @@ export default class KnishIOClient {
1836
1898
  }
1837
1899
 
1838
1900
  /**
1839
- * Replenish tokens
1901
+ * Replenish a non-finite token supply.
1902
+ * Matches JS SDK KnishIOClient.replenishToken (KnishIOClient.js:2195-2231).
1840
1903
  */
1841
1904
  async replenishToken({
1842
1905
  token,
1843
1906
  amount = null,
1844
1907
  units = null,
1845
- sourceWallet: _sourceWallet = null
1908
+ sourceWallet = null
1846
1909
  }: {
1847
1910
  token: TokenSlug | string
1848
1911
  amount?: number | string | null
1849
- units?: string[] | null
1912
+ units?: Array<[string, string, Record<string, any>?]> | null
1850
1913
  sourceWallet?: Wallet | null
1851
1914
  }): Promise<Response> {
1852
1915
  this.log('info', `KnishIOClient::replenishToken() - Replenishing ${amount || 'units'} of ${token}...`)
1853
1916
 
1854
- // Similar to requestTokens but with specific replenishment logic
1855
- return this.requestTokens({
1856
- token,
1857
- amount,
1858
- units
1917
+ if (!sourceWallet) {
1918
+ sourceWallet = (await this.queryBalance({ token }))?.payload() as Wallet | null
1919
+ }
1920
+ if (!sourceWallet) {
1921
+ throw new TransferBalanceException('Source wallet is missing or invalid.')
1922
+ }
1923
+
1924
+ // Remainder wallet (same token, so the V-isotope pair conserves)
1925
+ const remainderWallet = sourceWallet.createRemainder(this.getSecret())
1926
+
1927
+ const molecule = await this.createMolecule({
1928
+ sourceWallet,
1929
+ remainderWallet
1859
1930
  })
1931
+ molecule.replenishToken({
1932
+ amount: Number(amount ?? 0),
1933
+ units: units ?? []
1934
+ })
1935
+ molecule.sign({ bundle: this.getBundle() })
1936
+ molecule.check()
1937
+
1938
+ const mutation = await this.createMoleculeMutation({
1939
+ mutationClass: MutationReplenishToken,
1940
+ molecule
1941
+ })
1942
+
1943
+ const response = await this.executeQuery(mutation)
1944
+
1945
+ if (!response) {
1946
+ throw new CodeException('Token replenishment failed')
1947
+ }
1948
+
1949
+ return response
1860
1950
  }
1861
1951
 
1862
1952
  /**
@@ -47,6 +47,7 @@ License: https://github.com/WishKnish/KnishIO-Client-TS/blob/master/LICENSE
47
47
  */
48
48
 
49
49
  import Meta, { type NormalizedMeta } from './Meta'
50
+ import PolicyMeta from './PolicyMeta'
50
51
 
51
52
  // Configuration constants matching JS SDK
52
53
  const USE_META_CONTEXT = false
@@ -172,12 +173,19 @@ export default class AtomMeta {
172
173
  * @return This instance for chaining
173
174
  */
174
175
  addPolicy(policy: Record<string, any>): AtomMeta {
175
- // For now, just stringify the policy
176
- // TODO: Implement PolicyMeta class if needed
176
+ // Normalise through PolicyMeta before serialising. PolicyMeta.fillDefault adds a default
177
+ // entry for every meta key not already covered by the caller's policy, so the stored string
178
+ // carries materially more than the input — and this string is hashed into the atom, so a
179
+ // raw JSON.stringify(policy) diverges from every other SDK.
180
+ // Matches JS SDK AtomMeta.addPolicy (AtomMeta.js:170-175), including passing
181
+ // Object.keys(this.meta): `meta` is a NormalizedMeta[] in both SDKs, so these are array
182
+ // indices rather than meta key names. Faithful to the JS behaviour — do not "correct" it.
183
+ const policyMeta = new PolicyMeta(policy, Object.keys(this.meta))
184
+
177
185
  this.merge({
178
- policy: JSON.stringify(policy)
186
+ policy: policyMeta.toJson()
179
187
  })
180
-
188
+
181
189
  return this
182
190
  }
183
191
 
@@ -755,6 +755,67 @@ export default class Molecule {
755
755
  return this
756
756
  }
757
757
 
758
+ /**
759
+ * Replenishes non-finite token supplies.
760
+ * Matches JS SDK Molecule.replenishToken (Molecule.js:521-566) exactly.
761
+ *
762
+ * Two orderings here are load-bearing for the molecular hash and must not be reordered:
763
+ * the remainder balance is computed BEFORE the source balance is overwritten, and the
764
+ * source V-atom is added BEFORE the remainder V-atom.
765
+ */
766
+ replenishToken({
767
+ amount,
768
+ units = []
769
+ }: {
770
+ amount: number
771
+ units?: Array<[string, string, Record<string, any>?]>
772
+ }): Molecule {
773
+ if (amount < 0) {
774
+ throw new NegativeAmountException('Molecule::replenishToken() - Amount to replenish must be positive!')
775
+ }
776
+
777
+ if (!this.sourceWallet || !this.remainderWallet) {
778
+ throw new Error('Source and remainder wallets required for token replenishment')
779
+ }
780
+
781
+ if (units.length) {
782
+ // Prepare token units to formatted style
783
+ const formatted = Wallet.getTokenUnits(units)
784
+
785
+ // Merge token units with source wallet & new items
786
+ this.remainderWallet.tokenUnits = this.sourceWallet.tokenUnits
787
+ for (const unit of formatted) {
788
+ this.remainderWallet.tokenUnits.push(unit)
789
+ }
790
+ this.remainderWallet.balance = String(this.remainderWallet.tokenUnits.length)
791
+
792
+ // Override first atom's token units to replenish values
793
+ this.sourceWallet.tokenUnits = formatted
794
+ this.sourceWallet.balance = String(this.sourceWallet.tokenUnits.length)
795
+ } else {
796
+ // Update wallet's balances
797
+ this.remainderWallet.balance = String(Number(this.sourceWallet.balance) + amount)
798
+ this.sourceWallet.balance = String(amount)
799
+ }
800
+
801
+ // Initializing a new Atom to remove tokens from source
802
+ this.addAtom(Atom.create({
803
+ isotope: 'V',
804
+ wallet: this.sourceWallet,
805
+ value: Number(this.sourceWallet.balance)
806
+ }))
807
+
808
+ this.addAtom(Atom.create({
809
+ isotope: 'V',
810
+ wallet: this.remainderWallet,
811
+ value: Number(this.remainderWallet.balance),
812
+ metaType: 'walletBundle',
813
+ metaId: this.remainderWallet.bundle!
814
+ }))
815
+
816
+ return this
817
+ }
818
+
758
819
  /**
759
820
  * Initialize authorization request
760
821
  * Creates U-isotope (authorization) atom for requesting auth token
@@ -51,6 +51,7 @@ import { randomString, chunkSubstr, isHex } from '@/libraries/strings'
51
51
  import { generateBundleHash, generateSecret, shake256, generateBatchId } from '@/libraries/crypto'
52
52
  import WalletCredentialException from '@/exception/WalletCredentialException'
53
53
  import { isBundleHash } from '@/types'
54
+ import TokenUnit from '@/core/TokenUnit'
54
55
  // Post-quantum cryptography for ML-KEM768 key encapsulation
55
56
  import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'
56
57
 
@@ -271,12 +272,13 @@ export default class Wallet {
271
272
  }
272
273
 
273
274
  /**
274
- * Get formatted token units from raw data
275
- * Stub implementation for now
275
+ * Map raw token-unit tuples to TokenUnit instances.
276
+ * Matches JS SDK Wallet.getTokenUnits (Wallet.js:190-196). The serialised shape reaches hashed
277
+ * atom meta via AtomMeta.setAtomWallet -> JSON.stringify(getTokenUnitsData()), so returning raw
278
+ * tuples here would diverge from every other SDK.
276
279
  */
277
- static getTokenUnits(unitsData: any[]): any[] {
278
- // TODO: Implement TokenUnit class if needed
279
- return unitsData
280
+ static getTokenUnits(unitsData: Array<[string, string, Record<string, any>?]>): TokenUnit[] {
281
+ return unitsData.map(unitData => TokenUnit.createFromDB(unitData))
280
282
  }
281
283
 
282
284
  /**
@@ -0,0 +1,97 @@
1
+ /*
2
+ (
3
+ (/(
4
+ (//(
5
+ (///(
6
+ (/////(
7
+ (//////( )
8
+ (////////( (/)
9
+ (////////( (///)
10
+ (//////////( (////)
11
+ (//////////( (//////)
12
+ (////////////( (///////)
13
+ (/////////////( (/////////)
14
+ (//////////////( (///////////)
15
+ (///////////////( (/////////////)
16
+ (////////////////( (//////////////)
17
+ ((((((((((((((((((( (((((((((((((((
18
+ ((((((((((((((((((( ((((((((((((((
19
+ ((((((((((((((((((( ((((((((((((((
20
+ (((((((((((((((((((( (((((((((((((
21
+ (((((((((((((((((((( ((((((((((((
22
+ ((((((((((((((((((( ((((((((((((
23
+ ((((((((((((((((((( ((((((((((
24
+ ((((((((((((((((((/ (((((((((
25
+ (((((((((((((((((( ((((((((
26
+ ((((((((((((((((( (((((((
27
+ (((((((((((((((((( (((((
28
+ ################# ##
29
+ ################ #
30
+ ################# ##
31
+ %################ ###
32
+ ###############( ####
33
+ ############### ####
34
+ ############### ######
35
+ %#############( (#######
36
+ %############# #########
37
+ ############( ##########
38
+ ########### #############
39
+ ######### ##############
40
+ %######
41
+
42
+ Powered by Knish.IO: Connecting a Decentralized World
43
+
44
+ Please visit https://github.com/WishKnish/KnishIO-Client-TS for information.
45
+
46
+ License: https://github.com/WishKnish/KnishIO-Client-TS/blob/master/LICENSE
47
+ */
48
+
49
+ import BaseException, { type BaseExceptionOptions } from './BaseException'
50
+
51
+ /**
52
+ * Exception thrown when a secret storage or hardware envelope encryption operation fails
53
+ */
54
+ export default class SecretStorageException extends BaseException {
55
+ constructor(message = 'Secret storage operation failed', options: BaseExceptionOptions = {}) {
56
+ super('WALLET_CREDENTIAL_ERROR', message, {
57
+ code: 'SECRET_STORAGE_ERROR',
58
+ ...options
59
+ })
60
+ }
61
+
62
+ /**
63
+ * Secret not found for the requested bundle hash
64
+ */
65
+ static notFound(bundleHash: string): SecretStorageException {
66
+ return new SecretStorageException(`Secret not found for bundle: ${bundleHash}`, {
67
+ code: 'SECRET_NOT_FOUND',
68
+ details: { bundleHash }
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Decryption failed (wrong passphrase or corrupted payload)
74
+ */
75
+ static decryptionFailed(reason?: string): SecretStorageException {
76
+ return new SecretStorageException(
77
+ `Failed to decrypt master secret: ${reason || 'Invalid passphrase or corrupted ciphertext'}`,
78
+ {
79
+ code: 'DECRYPTION_FAILED',
80
+ details: { reason }
81
+ }
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Provider is unavailable in current platform
87
+ */
88
+ static unavailable(provider: string, reason?: string): SecretStorageException {
89
+ return new SecretStorageException(
90
+ `Secret storage provider '${provider}' is unavailable: ${reason || 'Hardware or API not accessible'}`,
91
+ {
92
+ code: 'STORAGE_UNAVAILABLE',
93
+ details: { provider, reason }
94
+ }
95
+ )
96
+ }
97
+ }
@@ -62,6 +62,7 @@ export { default as MolecularHashMismatchException } from './MolecularHashMismat
62
62
  export { default as SignatureMismatchException } from './SignatureMismatchException'
63
63
  export { default as TransferBalanceException } from './TransferBalanceException'
64
64
  export { default as WalletCredentialException } from './WalletCredentialException'
65
+ export { default as SecretStorageException } from './SecretStorageException'
65
66
  export { default as InvalidResponseException } from './InvalidResponseException'
66
67
 
67
68
  // Additional exceptions (now implemented)
package/src/index.ts CHANGED
@@ -261,12 +261,39 @@ export {
261
261
  SignatureMismatchException,
262
262
  TransferBalanceException,
263
263
  WalletCredentialException,
264
+ SecretStorageException,
264
265
  InvalidResponseException,
265
266
  ExceptionFactory,
266
267
  EXCEPTION_TYPES,
267
268
  EXCEPTION_CODES
268
269
  } from './exception'
269
270
 
271
+ // =============================================================================
272
+ // SECRET STORAGE & HARDWARE ENVELOPE ENCRYPTION
273
+ // =============================================================================
274
+
275
+ export {
276
+ MemorySecretStorageProvider,
277
+ WebCryptoSecretStorageProvider,
278
+ MemoryStorageBackend,
279
+ createDefaultSecretStorage,
280
+ type IStorageBackend,
281
+ type CreateSecretStorageOptions
282
+ } from './storage'
283
+
284
+ export {
285
+ zeroizeBytes,
286
+ withSecureBytes,
287
+ withSecureString,
288
+ constantTimeCompare
289
+ } from './libraries/secureMemory'
290
+
291
+ export type {
292
+ SecretStorageMetadata,
293
+ EncryptedSecretPayload,
294
+ ISecretStorageProvider
295
+ } from './types/storage'
296
+
270
297
  // =============================================================================
271
298
  // TYPE DEFINITIONS
272
299
  // =============================================================================
@@ -362,7 +389,7 @@ export {
362
389
  // MUST equal package.json's "version". The CI `version consistency` job enforces this via
363
390
  // .github/scripts/check-version.sh, which parses this exact line — keep the literal form
364
391
  // `export const SDK_VERSION = '<semver>'` intact so the gate can read it.
365
- export const SDK_VERSION = '0.9.5'
392
+ export const SDK_VERSION = '0.9.7'
366
393
  export const SDK_NAME = 'KnishIO-Client-TS'
367
394
  export const COMPATIBLE_SERVER_VERSIONS = [4, 5]
368
395
 
@@ -141,16 +141,30 @@ export default class GraphQLClient implements IGraphQLClient {
141
141
  return createClient({
142
142
  url: serverUri,
143
143
  exchanges,
144
- // PQ-transport Phase E: when encryption is on, route fetch through the CipherHash wrapper
145
- // (encrypt the request body to the validator's ML-KEM pubkey, decrypt the response).
146
- // Omitted → urql uses the global fetch (plaintext).
147
- ...(this.cipherLink ? { fetch: ((input: any, init: any) => this.cipherFetch(input, init)) as typeof fetch } : {}),
144
+ // urql 5 had no default and always POSTed; urql 6 defaults to 'within-url-limit', which
145
+ // URL-encodes short queries and sends NO body. That would silently disable the CipherHash
146
+ // envelope below — cipherFetch's `typeof init.body === 'string'` guard fails on a GET, so
147
+ // the query would leave as plaintext URL parameters with no error. Pin POST explicitly.
148
+ preferGetMethod: false,
149
+ // Always route through our own fetch. Two reasons: (1) PQ-transport Phase E — when
150
+ // encryption is on, cipherFetch wraps the request body in the CipherHash envelope and
151
+ // decrypts the response; (2) the 60s timeout. urql's makeFetchSource unconditionally
152
+ // overwrites init.signal with its own AbortController, so a signal returned from
153
+ // fetchOptions() is discarded and never fired. Combining it here is the only place it
154
+ // survives.
155
+ fetch: ((input: RequestInfo | URL, init?: RequestInit) => {
156
+ const timeoutSignal = AbortSignal.timeout(60000)
157
+ // Keep urql's own signal so teardown still aborts; AbortSignal.any needs Node >= 18.17.
158
+ const signal = init?.signal && typeof AbortSignal.any === 'function'
159
+ ? AbortSignal.any([init.signal, timeoutSignal])
160
+ : (init?.signal ?? timeoutSignal)
161
+ const timedInit: RequestInit = { ...init, signal }
162
+ return this.cipherLink ? this.cipherFetch(input, timedInit) : fetch(input, timedInit)
163
+ }) as typeof fetch,
148
164
  fetchOptions: () => ({
149
165
  headers: {
150
166
  'X-Auth-Token': this.$__authToken
151
- },
152
- // Add 60 second timeout
153
- signal: AbortSignal.timeout(60000)
167
+ }
154
168
  })
155
169
  })
156
170
  }
@@ -155,12 +155,17 @@ class UrqlClientWrapper {
155
155
  return createClient({
156
156
  url: serverUri,
157
157
  exchanges,
158
+ // urql 6 defaults preferGetMethod to 'within-url-limit', sending short queries as GET with
159
+ // no body. Keep the urql-5 behaviour (always POST) so this client matches GraphQLClient and
160
+ // cannot silently change the wire format if it is ever put back into use.
161
+ preferGetMethod: false,
162
+ // No `signal` here: urql's makeFetchSource overwrites init.signal with its own
163
+ // AbortController, so a timeout passed through fetchOptions() is silently discarded and
164
+ // never fires. GraphQLClient applies its 60s timeout inside a wrapping `fetch` instead.
158
165
  fetchOptions: () => ({
159
166
  headers: {
160
167
  'X-Auth-Token': this.$__authToken
161
- },
162
- // Add 60 second timeout for debugging
163
- signal: AbortSignal.timeout(60000)
168
+ }
164
169
  })
165
170
  })
166
171
  }
@@ -659,13 +659,11 @@ export function verifyOTSSignature(
659
659
  const enumerated = enumerateMolecularHash(base17Hash)
660
660
  const normalized = normalizeMolecularHash(enumerated)
661
661
 
662
- // Step 2: Process OTS fragments (handle compression if needed)
663
- let ots = otsFragments
662
+ // Step 2: Fragments must be the uncompressed 2048-character form. Base64-compressed
663
+ // signatures are not supported and are rejected here rather than expanded.
664
+ const ots = otsFragments
664
665
  if (ots.length !== 2048) {
665
- // If not 2048 chars, it might be base64 compressed - for now, assume uncompressed
666
- if (ots.length !== 2048) {
667
- return false // Invalid signature length
668
- }
666
+ return false
669
667
  }
670
668
 
671
669
  // Step 3: Subdivide OTS into 16 segments of 128 characters each