@wishknish/knishio-client-js 0.9.4 → 1.1.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.
@@ -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
 
@@ -0,0 +1,199 @@
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-JS for information.
45
+
46
+ License: https://github.com/WishKnish/KnishIO-Client-JS/blob/master/LICENSE
47
+ */
48
+
49
+ import SecretStorageException from '../exception/SecretStorageException.js'
50
+
51
+ /**
52
+ * Node-only persistent file storage backend.
53
+ * Stores key-value entries in a JSON file with restrictive permissions (0o600).
54
+ * Uses temporary file writing followed by atomic rename to prevent corruption.
55
+ */
56
+ export default class FileStorageBackend {
57
+ /**
58
+ * @param {string} filePath
59
+ */
60
+ constructor (filePath) {
61
+ if (!filePath) {
62
+ throw new SecretStorageException('Storage file path cannot be empty')
63
+ }
64
+ this.filePath = filePath
65
+ this.store = new Map()
66
+ this.loaded = false
67
+ }
68
+
69
+ /**
70
+ * @private
71
+ */
72
+ async getFs () {
73
+ try {
74
+ // Platform-specific: node:fs and node:path do not exist in browser runtimes
75
+ // and cannot be statically imported without breaking browser bundles.
76
+ const fs = await import('node:fs/promises')
77
+ const path = await import('node:path')
78
+ return { fs, path }
79
+ } catch {
80
+ throw SecretStorageException.unavailable(
81
+ 'file-storage',
82
+ 'FileStorageBackend is only supported in Node.js environments with node:fs access'
83
+ )
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @private
89
+ */
90
+ async ensureLoaded () {
91
+ if (this.loaded) {
92
+ return this.store
93
+ }
94
+
95
+ const { fs } = await this.getFs()
96
+
97
+ try {
98
+ const content = await fs.readFile(this.filePath, 'utf8')
99
+ let parsed
100
+ try {
101
+ parsed = JSON.parse(content)
102
+ } catch {
103
+ throw SecretStorageException.decryptionFailed('Corrupted storage file format')
104
+ }
105
+
106
+ if (parsed && typeof parsed === 'object') {
107
+ this.store = new Map(Object.entries(parsed).map(([k, v]) => [k, String(v)]))
108
+ }
109
+ } catch (err) {
110
+ if (err instanceof SecretStorageException) {
111
+ throw err
112
+ }
113
+ if (err?.code !== 'ENOENT') {
114
+ const msg = err instanceof Error ? err.message : String(err)
115
+ throw new SecretStorageException(`Failed to read storage file: ${msg}`)
116
+ }
117
+ this.store = new Map()
118
+ }
119
+
120
+ this.loaded = true
121
+ return this.store
122
+ }
123
+
124
+ /**
125
+ * @private
126
+ */
127
+ async persist () {
128
+ const { fs, path } = await this.getFs()
129
+
130
+ const dir = path.dirname(this.filePath)
131
+ if (dir && dir !== '.') {
132
+ await fs.mkdir(dir, { recursive: true })
133
+ }
134
+
135
+ const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`
136
+ const data = JSON.stringify(Object.fromEntries(this.store), null, 2)
137
+
138
+ try {
139
+ await fs.writeFile(tmpPath, data, { mode: 0o600, encoding: 'utf8' })
140
+ if (typeof process !== 'undefined' && process.platform !== 'win32') {
141
+ try {
142
+ await fs.chmod(tmpPath, 0o600)
143
+ } catch {
144
+ // Ignore chmod error if file system does not support it
145
+ }
146
+ }
147
+ await fs.rename(tmpPath, this.filePath)
148
+ } catch (err) {
149
+ try {
150
+ await fs.unlink(tmpPath)
151
+ } catch {
152
+ // Ignore cleanup error
153
+ }
154
+ const msg = err instanceof Error ? err.message : String(err)
155
+ throw new SecretStorageException(`Failed to persist storage file: ${msg}`)
156
+ }
157
+ }
158
+
159
+ /**
160
+ * @param {string} key
161
+ * @returns {Promise<string|null>}
162
+ */
163
+ async getItem (key) {
164
+ await this.ensureLoaded()
165
+ return this.store.get(key) ?? null
166
+ }
167
+
168
+ /**
169
+ * @param {string} key
170
+ * @param {string} value
171
+ * @returns {Promise<void>}
172
+ */
173
+ async setItem (key, value) {
174
+ await this.ensureLoaded()
175
+ this.store.set(key, value)
176
+ await this.persist()
177
+ }
178
+
179
+ /**
180
+ * @param {string} key
181
+ * @returns {Promise<boolean>}
182
+ */
183
+ async removeItem (key) {
184
+ await this.ensureLoaded()
185
+ const existed = this.store.delete(key)
186
+ if (existed) {
187
+ await this.persist()
188
+ }
189
+ return existed
190
+ }
191
+
192
+ /**
193
+ * @returns {Promise<string[]>}
194
+ */
195
+ async keys () {
196
+ await this.ensureLoaded()
197
+ return Array.from(this.store.keys())
198
+ }
199
+ }
@@ -47,7 +47,8 @@ License: https://github.com/WishKnish/KnishIO-Client-JS/blob/master/LICENSE
47
47
  */
48
48
 
49
49
  import SecretStorageException from '../exception/SecretStorageException.js'
50
- import { withSecureString } from '../libraries/secureMemory.js'
50
+ import { withSecureString, zeroizeBytes } from '../libraries/secureMemory.js'
51
+ import { sealEnvelope, openEnvelope } from './secretEnvelope.js'
51
52
 
52
53
  /**
53
54
  * In-memory secret storage provider
@@ -57,6 +58,7 @@ export default class MemorySecretStorageProvider {
57
58
  constructor () {
58
59
  this.providerType = 'memory'
59
60
  this.secrets = new Map()
61
+ this.recoverySecrets = new Map()
60
62
  }
61
63
 
62
64
  /**
@@ -102,6 +104,18 @@ export default class MemorySecretStorageProvider {
102
104
  }
103
105
 
104
106
  this.secrets.set(bundleHash, { secret, metadata })
107
+
108
+ if (options.recoveryPassphrase) {
109
+ const recoveryMetadata = {
110
+ bundleHash,
111
+ label: options.label,
112
+ createdAt: Date.now(),
113
+ hardwareBacked: false,
114
+ providerType: 'webcrypto-aes-gcm'
115
+ }
116
+ const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata)
117
+ this.recoverySecrets.set(bundleHash, JSON.stringify(recoveryPayload))
118
+ }
105
119
  }
106
120
 
107
121
  /**
@@ -122,6 +136,7 @@ export default class MemorySecretStorageProvider {
122
136
  * @returns {Promise<boolean>}
123
137
  */
124
138
  async deleteSecret (bundleHash) {
139
+ this.recoverySecrets.delete(bundleHash)
125
140
  return this.secrets.delete(bundleHash)
126
141
  }
127
142
 
@@ -166,5 +181,58 @@ export default class MemorySecretStorageProvider {
166
181
  */
167
182
  clear () {
168
183
  this.secrets.clear()
184
+ this.recoverySecrets.clear()
185
+ }
186
+
187
+ /**
188
+ * Recover a secret using its recovery envelope and restore it
189
+ *
190
+ * @param {string} bundleHash
191
+ * @param {string} recoveryPassphrase
192
+ * @param {{ label?: string }} [options]
193
+ * @returns {Promise<void>}
194
+ */
195
+ async recoverSecret (bundleHash, recoveryPassphrase, options = {}) {
196
+ if (!bundleHash) {
197
+ throw new SecretStorageException('Bundle hash cannot be empty')
198
+ }
199
+ if (!recoveryPassphrase) {
200
+ throw new SecretStorageException('Recovery passphrase cannot be empty')
201
+ }
202
+
203
+ const raw = this.recoverySecrets.get(bundleHash)
204
+ if (!raw) {
205
+ throw SecretStorageException.notFound(bundleHash)
206
+ }
207
+
208
+ let payload
209
+ try {
210
+ payload = JSON.parse(raw)
211
+ } catch {
212
+ throw SecretStorageException.decryptionFailed('Corrupted recovery payload format')
213
+ }
214
+
215
+ let decryptedBytes
216
+ try {
217
+ decryptedBytes = await openEnvelope(payload, recoveryPassphrase)
218
+ } catch (err) {
219
+ if (err instanceof SecretStorageException) {
220
+ throw err
221
+ }
222
+ const msg = err instanceof Error ? err.message : String(err)
223
+ throw SecretStorageException.decryptionFailed(msg)
224
+ }
225
+
226
+ let secretStr
227
+ try {
228
+ secretStr = new TextDecoder().decode(decryptedBytes)
229
+ } finally {
230
+ zeroizeBytes(decryptedBytes)
231
+ }
232
+
233
+ await this.storeSecret(bundleHash, secretStr, {
234
+ ...options,
235
+ recoveryPassphrase
236
+ })
169
237
  }
170
238
  }