@wishknish/knishio-client-js 1.0.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishknish/knishio-client-js",
3
- "version": "1.0.0",
3
+ "version": "1.1.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.",
@@ -92,4 +92,14 @@ export default class SecretStorageException extends BaseException {
92
92
  static unavailable (provider, reason = 'Hardware or API not accessible') {
93
93
  return new SecretStorageException(`Secret storage provider '${provider}' is unavailable: ${reason}`)
94
94
  }
95
+
96
+ /**
97
+ * Factory method: validation error
98
+ *
99
+ * @param {string} message
100
+ * @returns {SecretStorageException}
101
+ */
102
+ static validationError (message) {
103
+ return new SecretStorageException(message)
104
+ }
95
105
  }
package/src/index.js CHANGED
@@ -330,8 +330,18 @@ export {
330
330
  export {
331
331
  MemorySecretStorageProvider,
332
332
  WebCryptoSecretStorageProvider,
333
+ WebAuthnPrfSecretStorageProvider,
334
+ NonExtractableKeySecretStorageProvider,
335
+ IndexedDbKeyStore,
336
+ MemoryKeyStore,
333
337
  MemoryStorageBackend,
334
- createDefaultSecretStorage
338
+ FileStorageBackend,
339
+ WebStorageBackend,
340
+ createDefaultSecretStorage,
341
+ sealEnvelope,
342
+ openEnvelope,
343
+ SECRET_KEY_PREFIX,
344
+ RECOVERY_KEY_PREFIX
335
345
  } from './storage/index.js'
336
346
 
337
347
  export {
@@ -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
  }