@wishknish/knishio-client-ts 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-ts",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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",
@@ -94,4 +94,13 @@ export default class SecretStorageException extends BaseException {
94
94
  }
95
95
  )
96
96
  }
97
+
98
+ /**
99
+ * Validation error for storage options or parameters
100
+ */
101
+ static validationError(message: string): SecretStorageException {
102
+ return new SecretStorageException(message, {
103
+ code: 'VALIDATION_ERROR'
104
+ })
105
+ }
97
106
  }
package/src/index.ts CHANGED
@@ -275,10 +275,23 @@ export {
275
275
  export {
276
276
  MemorySecretStorageProvider,
277
277
  WebCryptoSecretStorageProvider,
278
+ WebAuthnPrfSecretStorageProvider,
279
+ NonExtractableKeySecretStorageProvider,
280
+ IndexedDbKeyStore,
281
+ MemoryKeyStore,
278
282
  MemoryStorageBackend,
283
+ FileStorageBackend,
284
+ WebStorageBackend,
279
285
  createDefaultSecretStorage,
286
+ sealEnvelope,
287
+ openEnvelope,
288
+ SECRET_KEY_PREFIX,
289
+ RECOVERY_KEY_PREFIX,
280
290
  type IStorageBackend,
281
- type CreateSecretStorageOptions
291
+ type IKeyStore,
292
+ type CreateSecretStorageOptions,
293
+ type WebAuthnPrfSecretStorageOptions,
294
+ type NonExtractableKeyStorageOptions
282
295
  } from './storage'
283
296
 
284
297
  export {
@@ -291,6 +304,7 @@ export {
291
304
  export type {
292
305
  SecretStorageMetadata,
293
306
  EncryptedSecretPayload,
307
+ StorageOptions,
294
308
  ISecretStorageProvider
295
309
  } from './types/storage'
296
310
 
@@ -389,7 +403,7 @@ export {
389
403
  // MUST equal package.json's "version". The CI `version consistency` job enforces this via
390
404
  // .github/scripts/check-version.sh, which parses this exact line — keep the literal form
391
405
  // `export const SDK_VERSION = '<semver>'` intact so the gate can read it.
392
- export const SDK_VERSION = '1.0.0'
406
+ export const SDK_VERSION = '1.1.0'
393
407
  export const SDK_NAME = 'KnishIO-Client-TS'
394
408
  export const COMPATIBLE_SERVER_VERSIONS = [4, 5]
395
409
 
@@ -0,0 +1,176 @@
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 type { IStorageBackend } from './WebCryptoSecretStorageProvider'
50
+ import SecretStorageException from '@/exception/SecretStorageException'
51
+
52
+ /**
53
+ * Node-only persistent file storage backend.
54
+ * Stores key-value entries in a JSON file with restrictive permissions (0o600).
55
+ * Uses temporary file writing followed by atomic rename to prevent corruption.
56
+ */
57
+ export default class FileStorageBackend implements IStorageBackend {
58
+ readonly filePath: string
59
+ private store: Map<string, string> = new Map()
60
+ private loaded = false
61
+
62
+ constructor(filePath: string) {
63
+ if (!filePath) {
64
+ throw new SecretStorageException('Storage file path cannot be empty')
65
+ }
66
+ this.filePath = filePath
67
+ }
68
+
69
+ private async getFs() {
70
+ try {
71
+ // Platform-specific: node:fs and node:path do not exist in browser runtimes
72
+ // and cannot be statically imported without breaking browser bundles.
73
+ const fs = await import('node:fs/promises')
74
+ const path = await import('node:path')
75
+ return { fs, path }
76
+ } catch {
77
+ throw SecretStorageException.unavailable(
78
+ 'file-storage',
79
+ 'FileStorageBackend is only supported in Node.js environments with node:fs access'
80
+ )
81
+ }
82
+ }
83
+
84
+ private async ensureLoaded(): Promise<Map<string, string>> {
85
+ if (this.loaded) {
86
+ return this.store
87
+ }
88
+
89
+ const { fs } = await this.getFs()
90
+
91
+ try {
92
+ const content = await fs.readFile(this.filePath, 'utf8')
93
+ let parsed: Record<string, unknown>
94
+ try {
95
+ parsed = JSON.parse(content)
96
+ } catch {
97
+ throw SecretStorageException.decryptionFailed('Corrupted storage file format')
98
+ }
99
+
100
+ if (parsed && typeof parsed === 'object') {
101
+ this.store = new Map(Object.entries(parsed).map(([k, v]) => [k, String(v)]))
102
+ }
103
+ } catch (err: unknown) {
104
+ if (err instanceof SecretStorageException) {
105
+ throw err
106
+ }
107
+ // If file does not exist (ENOENT), treat as empty map
108
+ const nodeErr = err as { code?: string }
109
+ if (nodeErr?.code !== 'ENOENT') {
110
+ const msg = err instanceof Error ? err.message : String(err)
111
+ throw new SecretStorageException(`Failed to read storage file: ${msg}`)
112
+ }
113
+ this.store = new Map()
114
+ }
115
+
116
+ this.loaded = true
117
+ return this.store
118
+ }
119
+
120
+ private async persist(): Promise<void> {
121
+ const { fs, path } = await this.getFs()
122
+
123
+ const dir = path.dirname(this.filePath)
124
+ if (dir && dir !== '.') {
125
+ await fs.mkdir(dir, { recursive: true })
126
+ }
127
+
128
+ const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`
129
+ const data = JSON.stringify(Object.fromEntries(this.store), null, 2)
130
+
131
+ try {
132
+ await fs.writeFile(tmpPath, data, { mode: 0o600, encoding: 'utf8' })
133
+ if (typeof process !== 'undefined' && process.platform !== 'win32') {
134
+ try {
135
+ await fs.chmod(tmpPath, 0o600)
136
+ } catch {
137
+ // Ignore chmod errors if file system does not support it
138
+ }
139
+ }
140
+ await fs.rename(tmpPath, this.filePath)
141
+ } catch (err: unknown) {
142
+ try {
143
+ await fs.unlink(tmpPath)
144
+ } catch {
145
+ // Ignore unlink cleanup error
146
+ }
147
+ const msg = err instanceof Error ? err.message : String(err)
148
+ throw new SecretStorageException(`Failed to persist storage file: ${msg}`)
149
+ }
150
+ }
151
+
152
+ async getItem(key: string): Promise<string | null> {
153
+ await this.ensureLoaded()
154
+ return this.store.get(key) ?? null
155
+ }
156
+
157
+ async setItem(key: string, value: string): Promise<void> {
158
+ await this.ensureLoaded()
159
+ this.store.set(key, value)
160
+ await this.persist()
161
+ }
162
+
163
+ async removeItem(key: string): Promise<boolean> {
164
+ await this.ensureLoaded()
165
+ const existed = this.store.delete(key)
166
+ if (existed) {
167
+ await this.persist()
168
+ }
169
+ return existed
170
+ }
171
+
172
+ async keys(): Promise<string[]> {
173
+ await this.ensureLoaded()
174
+ return Array.from(this.store.keys())
175
+ }
176
+ }
@@ -46,9 +46,15 @@ Please visit https://github.com/WishKnish/KnishIO-Client-TS for information.
46
46
  License: https://github.com/WishKnish/KnishIO-Client-TS/blob/master/LICENSE
47
47
  */
48
48
 
49
- import type { ISecretStorageProvider, SecretStorageMetadata } from '@/types/storage'
49
+ import type {
50
+ ISecretStorageProvider,
51
+ SecretStorageMetadata,
52
+ EncryptedSecretPayload,
53
+ StorageOptions
54
+ } from '@/types/storage'
50
55
  import SecretStorageException from '@/exception/SecretStorageException'
51
- import { withSecureString } from '@/libraries/secureMemory'
56
+ import { withSecureString, zeroizeBytes } from '@/libraries/secureMemory'
57
+ import { sealEnvelope, openEnvelope } from './secretEnvelope'
52
58
 
53
59
  interface MemorySecretEntry {
54
60
  secret: string
@@ -62,6 +68,7 @@ interface MemorySecretEntry {
62
68
  export default class MemorySecretStorageProvider implements ISecretStorageProvider {
63
69
  public readonly providerType = 'memory'
64
70
  private secrets: Map<string, MemorySecretEntry> = new Map()
71
+ private recoverySecrets: Map<string, string> = new Map()
65
72
 
66
73
  /**
67
74
  * Memory storage is not hardware backed
@@ -83,7 +90,7 @@ export default class MemorySecretStorageProvider implements ISecretStorageProvid
83
90
  async storeSecret(
84
91
  bundleHash: string,
85
92
  secret: string,
86
- options?: { label?: string; passphrase?: string }
93
+ options?: StorageOptions
87
94
  ): Promise<void> {
88
95
  if (!bundleHash) {
89
96
  throw new SecretStorageException('Bundle hash cannot be empty')
@@ -101,6 +108,18 @@ export default class MemorySecretStorageProvider implements ISecretStorageProvid
101
108
  }
102
109
 
103
110
  this.secrets.set(bundleHash, { secret, metadata })
111
+
112
+ if (options?.recoveryPassphrase) {
113
+ const recoveryMetadata: SecretStorageMetadata = {
114
+ bundleHash,
115
+ label: options?.label,
116
+ createdAt: Date.now(),
117
+ hardwareBacked: false,
118
+ providerType: 'webcrypto-aes-gcm'
119
+ }
120
+ const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata)
121
+ this.recoverySecrets.set(bundleHash, JSON.stringify(recoveryPayload))
122
+ }
104
123
  }
105
124
 
106
125
  /**
@@ -115,6 +134,7 @@ export default class MemorySecretStorageProvider implements ISecretStorageProvid
115
134
  * Delete a stored secret
116
135
  */
117
136
  async deleteSecret(bundleHash: string): Promise<boolean> {
137
+ this.recoverySecrets.delete(bundleHash)
118
138
  return this.secrets.delete(bundleHash)
119
139
  }
120
140
 
@@ -152,5 +172,57 @@ export default class MemorySecretStorageProvider implements ISecretStorageProvid
152
172
  */
153
173
  clear(): void {
154
174
  this.secrets.clear()
175
+ this.recoverySecrets.clear()
176
+ }
177
+
178
+ /**
179
+ * Recover a secret using its recovery envelope and restore it
180
+ */
181
+ async recoverSecret(
182
+ bundleHash: string,
183
+ recoveryPassphrase: string,
184
+ options?: { label?: string }
185
+ ): Promise<void> {
186
+ if (!bundleHash) {
187
+ throw new SecretStorageException('Bundle hash cannot be empty')
188
+ }
189
+ if (!recoveryPassphrase) {
190
+ throw new SecretStorageException('Recovery passphrase cannot be empty')
191
+ }
192
+
193
+ const raw = this.recoverySecrets.get(bundleHash)
194
+ if (!raw) {
195
+ throw SecretStorageException.notFound(bundleHash)
196
+ }
197
+
198
+ let payload: EncryptedSecretPayload
199
+ try {
200
+ payload = JSON.parse(raw)
201
+ } catch {
202
+ throw SecretStorageException.decryptionFailed('Corrupted recovery payload format')
203
+ }
204
+
205
+ let decryptedBytes: Uint8Array
206
+ try {
207
+ decryptedBytes = await openEnvelope(payload, recoveryPassphrase)
208
+ } catch (err: unknown) {
209
+ if (err instanceof SecretStorageException) {
210
+ throw err
211
+ }
212
+ const msg = err instanceof Error ? err.message : String(err)
213
+ throw SecretStorageException.decryptionFailed(msg)
214
+ }
215
+
216
+ let secretStr: string
217
+ try {
218
+ secretStr = new TextDecoder().decode(decryptedBytes)
219
+ } finally {
220
+ zeroizeBytes(decryptedBytes)
221
+ }
222
+
223
+ await this.storeSecret(bundleHash, secretStr, {
224
+ ...options,
225
+ recoveryPassphrase
226
+ })
155
227
  }
156
228
  }