@fgv/ts-extras 5.0.2 → 5.1.0-1
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/index.browser.js +6 -2
- package/dist/index.js +5 -1
- package/dist/packlets/ai-assist/apiClient.js +484 -0
- package/dist/packlets/ai-assist/converters.js +121 -0
- package/dist/packlets/ai-assist/index.js +10 -0
- package/dist/packlets/ai-assist/model.js +90 -0
- package/dist/packlets/ai-assist/registry.js +145 -0
- package/dist/packlets/ai-assist/toolFormats.js +160 -0
- package/dist/packlets/crypto-utils/constants.js +48 -0
- package/dist/packlets/crypto-utils/converters.js +155 -0
- package/dist/packlets/crypto-utils/directEncryptionProvider.js +86 -0
- package/dist/packlets/crypto-utils/encryptedFile.js +161 -0
- package/dist/packlets/crypto-utils/index.browser.js +41 -0
- package/dist/packlets/crypto-utils/index.js +41 -0
- package/dist/packlets/crypto-utils/keystore/converters.js +84 -0
- package/dist/packlets/crypto-utils/keystore/index.js +31 -0
- package/dist/packlets/crypto-utils/keystore/keyStore.js +758 -0
- package/dist/packlets/crypto-utils/keystore/model.js +64 -0
- package/dist/packlets/crypto-utils/model.js +39 -0
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +159 -0
- package/dist/packlets/experimental/formatter.js +1 -1
- package/dist/packlets/mustache/index.js +23 -0
- package/dist/packlets/mustache/interfaces.js +25 -0
- package/dist/packlets/mustache/mustacheTemplate.js +242 -0
- package/dist/packlets/record-jar/recordJarHelpers.js +1 -1
- package/dist/packlets/yaml/converters.js +46 -0
- package/dist/packlets/yaml/index.js +23 -0
- package/dist/packlets/zip-file-tree/index.js +1 -0
- package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +6 -2
- package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +40 -0
- package/dist/ts-extras.d.ts +1964 -112
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/index.browser.d.ts +3 -1
- package/lib/index.browser.js +6 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.js +9 -1
- package/lib/packlets/ai-assist/apiClient.d.ts +60 -0
- package/lib/packlets/ai-assist/apiClient.js +488 -0
- package/lib/packlets/ai-assist/converters.d.ts +55 -0
- package/lib/packlets/ai-assist/converters.js +124 -0
- package/lib/packlets/ai-assist/index.d.ts +10 -0
- package/lib/packlets/ai-assist/index.js +33 -0
- package/lib/packlets/ai-assist/model.d.ts +222 -0
- package/lib/packlets/ai-assist/model.js +95 -0
- package/lib/packlets/ai-assist/registry.d.ts +25 -0
- package/lib/packlets/ai-assist/registry.js +150 -0
- package/lib/packlets/ai-assist/toolFormats.d.ts +44 -0
- package/lib/packlets/ai-assist/toolFormats.js +166 -0
- package/lib/packlets/crypto-utils/constants.d.ts +26 -0
- package/lib/packlets/crypto-utils/constants.js +51 -0
- package/lib/packlets/crypto-utils/converters.d.ts +58 -0
- package/lib/packlets/crypto-utils/converters.js +192 -0
- package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts +69 -0
- package/lib/packlets/crypto-utils/directEncryptionProvider.js +90 -0
- package/lib/packlets/crypto-utils/encryptedFile.d.ts +88 -0
- package/lib/packlets/crypto-utils/encryptedFile.js +201 -0
- package/lib/packlets/crypto-utils/index.browser.d.ts +14 -0
- package/lib/packlets/crypto-utils/index.browser.js +91 -0
- package/lib/packlets/crypto-utils/index.d.ts +15 -0
- package/lib/packlets/crypto-utils/index.js +88 -0
- package/lib/packlets/crypto-utils/keystore/converters.d.ts +29 -0
- package/lib/packlets/crypto-utils/keystore/converters.js +87 -0
- package/lib/packlets/crypto-utils/keystore/index.d.ts +9 -0
- package/lib/packlets/crypto-utils/keystore/index.js +71 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +239 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.js +795 -0
- package/lib/packlets/crypto-utils/keystore/model.d.ts +245 -0
- package/lib/packlets/crypto-utils/keystore/model.js +68 -0
- package/lib/packlets/crypto-utils/model.d.ts +236 -0
- package/lib/packlets/crypto-utils/model.js +76 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +62 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +196 -0
- package/lib/packlets/experimental/formatter.d.ts +1 -1
- package/lib/packlets/experimental/formatter.js +1 -1
- package/lib/packlets/mustache/index.d.ts +3 -0
- package/lib/packlets/mustache/index.js +27 -0
- package/lib/packlets/mustache/interfaces.d.ts +97 -0
- package/lib/packlets/mustache/interfaces.js +26 -0
- package/lib/packlets/mustache/mustacheTemplate.d.ts +76 -0
- package/lib/packlets/mustache/mustacheTemplate.js +249 -0
- package/lib/packlets/record-jar/recordJarHelpers.js +1 -1
- package/lib/packlets/yaml/converters.d.ts +9 -0
- package/lib/packlets/yaml/converters.js +82 -0
- package/lib/packlets/yaml/index.d.ts +2 -0
- package/lib/packlets/yaml/index.js +39 -0
- package/lib/packlets/zip-file-tree/index.d.ts +1 -0
- package/lib/packlets/zip-file-tree/index.js +15 -0
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +5 -1
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +6 -2
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +27 -0
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +43 -0
- package/package.json +50 -31
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
// Copyright (c) 2026 Erik Fortune
|
|
2
|
+
//
|
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
// furnished to do so, subject to the following conditions:
|
|
9
|
+
//
|
|
10
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
// copies or substantial portions of the Software.
|
|
12
|
+
//
|
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
// SOFTWARE.
|
|
20
|
+
import { captureResult, fail, succeed } from '@fgv/ts-utils';
|
|
21
|
+
import * as Constants from '../constants';
|
|
22
|
+
import { createEncryptedFile } from '../encryptedFile';
|
|
23
|
+
import { DEFAULT_KEYSTORE_ITERATIONS, DEFAULT_SECRET_ITERATIONS, KEYSTORE_FORMAT, MIN_SALT_LENGTH } from './model';
|
|
24
|
+
import { keystoreFile, keystoreVaultContents } from './converters';
|
|
25
|
+
/**
|
|
26
|
+
* Gets the current ISO timestamp.
|
|
27
|
+
*/
|
|
28
|
+
function getCurrentTimestamp() {
|
|
29
|
+
return new Date().toISOString();
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// KeyStore Class
|
|
33
|
+
// ============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Password-protected key store for managing encryption secrets.
|
|
36
|
+
*
|
|
37
|
+
* The KeyStore provides a secure vault for storing named encryption keys.
|
|
38
|
+
* The vault is encrypted at rest using a master password via PBKDF2 key derivation.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* // Create new key store
|
|
43
|
+
* const keystore = KeyStore.create({ cryptoProvider: nodeCryptoProvider }).orThrow();
|
|
44
|
+
* await keystore.initialize('master-password');
|
|
45
|
+
*
|
|
46
|
+
* // Add secrets
|
|
47
|
+
* await keystore.addSecret('my-key', { description: 'Production key' });
|
|
48
|
+
*
|
|
49
|
+
* // Save to file
|
|
50
|
+
* const fileContent = await keystore.save();
|
|
51
|
+
*
|
|
52
|
+
* // Later: Open existing key store
|
|
53
|
+
* const keystore2 = KeyStore.open({
|
|
54
|
+
* cryptoProvider: nodeCryptoProvider,
|
|
55
|
+
* keystoreFile: fileContent.value
|
|
56
|
+
* }).orThrow();
|
|
57
|
+
* await keystore2.unlock('master-password');
|
|
58
|
+
*
|
|
59
|
+
* // Use as secret provider for encrypted file loading
|
|
60
|
+
* const encryptionConfig = keystore2.getEncryptionConfig().orThrow();
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export class KeyStore {
|
|
66
|
+
constructor(cryptoProvider, iterations, keystoreFile, isNew = true) {
|
|
67
|
+
this._cryptoProvider = cryptoProvider;
|
|
68
|
+
this._iterations = iterations;
|
|
69
|
+
this._keystoreFile = keystoreFile;
|
|
70
|
+
this._state = 'locked';
|
|
71
|
+
this._dirty = false;
|
|
72
|
+
this._isNew = isNew;
|
|
73
|
+
}
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Factory Methods
|
|
76
|
+
// ============================================================================
|
|
77
|
+
/**
|
|
78
|
+
* Creates a new, empty key store.
|
|
79
|
+
* Call `initialize(password)` to set the master password.
|
|
80
|
+
* @param params - Creation parameters
|
|
81
|
+
* @returns Success with new KeyStore instance, or Failure if parameters invalid
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
static create(params) {
|
|
85
|
+
var _a;
|
|
86
|
+
const iterations = (_a = params.iterations) !== null && _a !== void 0 ? _a : DEFAULT_KEYSTORE_ITERATIONS;
|
|
87
|
+
if (iterations < 1) {
|
|
88
|
+
return fail('Iterations must be at least 1');
|
|
89
|
+
}
|
|
90
|
+
return succeed(new KeyStore(params.cryptoProvider, iterations, undefined, true));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Opens an existing encrypted key store.
|
|
94
|
+
* Call `unlock(password)` to decrypt and access secrets.
|
|
95
|
+
* @param params - Open parameters including the encrypted file
|
|
96
|
+
* @returns Success with KeyStore instance, or Failure if file format invalid
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
static open(params) {
|
|
100
|
+
// Validate the file format
|
|
101
|
+
const fileResult = keystoreFile.convert(params.keystoreFile);
|
|
102
|
+
if (fileResult.isFailure()) {
|
|
103
|
+
return fail(`Invalid key store file: ${fileResult.message}`);
|
|
104
|
+
}
|
|
105
|
+
const iterations = fileResult.value.keyDerivation.iterations;
|
|
106
|
+
return succeed(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
|
|
107
|
+
}
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Lifecycle Methods
|
|
110
|
+
// ============================================================================
|
|
111
|
+
/**
|
|
112
|
+
* Initializes a new key store with the master password.
|
|
113
|
+
* Generates a random salt for key derivation.
|
|
114
|
+
* Only valid for newly created (not opened) key stores.
|
|
115
|
+
* @param password - The master password
|
|
116
|
+
* @returns Success with this instance when initialized, Failure if already initialized or opened
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
119
|
+
async initialize(password) {
|
|
120
|
+
if (!this._isNew) {
|
|
121
|
+
return fail('Cannot initialize an opened key store - use unlock() instead');
|
|
122
|
+
}
|
|
123
|
+
if (this._state === 'unlocked') {
|
|
124
|
+
return fail('Key store is already initialized');
|
|
125
|
+
}
|
|
126
|
+
if (!password || password.length === 0) {
|
|
127
|
+
return fail('Password cannot be empty');
|
|
128
|
+
}
|
|
129
|
+
// Generate salt for this key store using crypto provider
|
|
130
|
+
const saltResult = this._cryptoProvider.generateRandomBytes(MIN_SALT_LENGTH);
|
|
131
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
132
|
+
if (saltResult.isFailure()) {
|
|
133
|
+
return fail(`Failed to generate salt: ${saltResult.message}`);
|
|
134
|
+
}
|
|
135
|
+
this._salt = saltResult.value;
|
|
136
|
+
this._secrets = new Map();
|
|
137
|
+
this._state = 'unlocked';
|
|
138
|
+
this._dirty = true; // New store needs to be saved
|
|
139
|
+
return succeed(this);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Unlocks an existing key store with the master password.
|
|
143
|
+
* Decrypts the vault and loads secrets into memory.
|
|
144
|
+
* @param password - The master password
|
|
145
|
+
* @returns Success with this instance when unlocked, Failure if password incorrect
|
|
146
|
+
* @public
|
|
147
|
+
*/
|
|
148
|
+
async unlock(password) {
|
|
149
|
+
var _a;
|
|
150
|
+
if (this._isNew) {
|
|
151
|
+
return fail('Cannot unlock a new key store - use initialize() instead');
|
|
152
|
+
}
|
|
153
|
+
/* c8 ignore next 6 - error paths tested but coverage intermittently missed */
|
|
154
|
+
if (this._state === 'unlocked') {
|
|
155
|
+
return fail('Key store is already unlocked');
|
|
156
|
+
}
|
|
157
|
+
if (!this._keystoreFile) {
|
|
158
|
+
return fail('No key store file to unlock');
|
|
159
|
+
}
|
|
160
|
+
const keyDerivation = this._keystoreFile.keyDerivation;
|
|
161
|
+
const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
|
|
162
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
163
|
+
if (saltResult.isFailure()) {
|
|
164
|
+
return fail(`Invalid salt in key store file: ${saltResult.message}`);
|
|
165
|
+
}
|
|
166
|
+
const salt = saltResult.value;
|
|
167
|
+
// Derive the key from password
|
|
168
|
+
const keyResult = await this._cryptoProvider.deriveKey(password, salt, keyDerivation.iterations);
|
|
169
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
170
|
+
if (keyResult.isFailure()) {
|
|
171
|
+
return fail(`Key derivation failed: ${keyResult.message}`);
|
|
172
|
+
}
|
|
173
|
+
// Decrypt the vault
|
|
174
|
+
const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
|
|
175
|
+
const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
|
|
176
|
+
const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
|
|
177
|
+
/* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
|
|
178
|
+
if (ivResult.isFailure()) {
|
|
179
|
+
return fail(`Invalid IV in key store file: ${ivResult.message}`);
|
|
180
|
+
}
|
|
181
|
+
if (authTagResult.isFailure()) {
|
|
182
|
+
return fail(`Invalid auth tag in key store file: ${authTagResult.message}`);
|
|
183
|
+
}
|
|
184
|
+
if (encryptedDataResult.isFailure()) {
|
|
185
|
+
return fail(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
|
|
186
|
+
}
|
|
187
|
+
const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
|
|
188
|
+
if (decryptResult.isFailure()) {
|
|
189
|
+
return fail('Incorrect password or corrupted key store');
|
|
190
|
+
}
|
|
191
|
+
// Parse the vault contents
|
|
192
|
+
const parseResult = captureResult(() => JSON.parse(decryptResult.value));
|
|
193
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
194
|
+
if (parseResult.isFailure()) {
|
|
195
|
+
return fail(`Failed to parse vault contents: ${parseResult.message}`);
|
|
196
|
+
}
|
|
197
|
+
const vaultResult = keystoreVaultContents.convert(parseResult.value);
|
|
198
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
199
|
+
if (vaultResult.isFailure()) {
|
|
200
|
+
return fail(`Invalid vault format: ${vaultResult.message}`);
|
|
201
|
+
}
|
|
202
|
+
// Load secrets into memory
|
|
203
|
+
this._salt = salt;
|
|
204
|
+
this._secrets = new Map();
|
|
205
|
+
for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
|
|
206
|
+
const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
|
|
207
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
208
|
+
if (keyBytesResult.isFailure()) {
|
|
209
|
+
return fail(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
|
|
210
|
+
}
|
|
211
|
+
const entry = {
|
|
212
|
+
name,
|
|
213
|
+
/* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
|
|
214
|
+
type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
|
|
215
|
+
key: keyBytesResult.value,
|
|
216
|
+
description: jsonEntry.description,
|
|
217
|
+
createdAt: jsonEntry.createdAt
|
|
218
|
+
};
|
|
219
|
+
this._secrets.set(name, entry);
|
|
220
|
+
}
|
|
221
|
+
this._state = 'unlocked';
|
|
222
|
+
this._dirty = false;
|
|
223
|
+
return succeed(this);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Locks the key store, clearing all secrets from memory.
|
|
227
|
+
* @param force - If true, discards unsaved changes
|
|
228
|
+
* @returns Success when locked, Failure if unsaved changes and !force
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
231
|
+
lock(force) {
|
|
232
|
+
if (this._state === 'locked') {
|
|
233
|
+
return succeed(this);
|
|
234
|
+
}
|
|
235
|
+
if (this._dirty && !force) {
|
|
236
|
+
return fail('Unsaved changes - use force=true to discard or save() first');
|
|
237
|
+
}
|
|
238
|
+
// Clear secrets from memory (overwrite for security)
|
|
239
|
+
if (this._secrets) {
|
|
240
|
+
for (const entry of this._secrets.values()) {
|
|
241
|
+
entry.key.fill(0);
|
|
242
|
+
}
|
|
243
|
+
this._secrets.clear();
|
|
244
|
+
this._secrets = undefined;
|
|
245
|
+
}
|
|
246
|
+
this._state = 'locked';
|
|
247
|
+
this._dirty = false;
|
|
248
|
+
return succeed(this);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Checks if the key store is unlocked.
|
|
252
|
+
* @public
|
|
253
|
+
*/
|
|
254
|
+
get isUnlocked() {
|
|
255
|
+
return this._state === 'unlocked';
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Checks if there are unsaved changes.
|
|
259
|
+
* @public
|
|
260
|
+
*/
|
|
261
|
+
get isDirty() {
|
|
262
|
+
return this._dirty;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Whether this is a newly created key store (not opened from a file).
|
|
266
|
+
* A new key store must be initialized with a password before use.
|
|
267
|
+
* An opened key store must be unlocked with the existing password.
|
|
268
|
+
* @public
|
|
269
|
+
*/
|
|
270
|
+
get isNew() {
|
|
271
|
+
return this._isNew;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Gets the current lock state.
|
|
275
|
+
* @public
|
|
276
|
+
*/
|
|
277
|
+
get state() {
|
|
278
|
+
return this._state;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Gets the crypto provider used by this key store.
|
|
282
|
+
* Available regardless of lock state.
|
|
283
|
+
* @public
|
|
284
|
+
*/
|
|
285
|
+
get cryptoProvider() {
|
|
286
|
+
return this._cryptoProvider;
|
|
287
|
+
}
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Secret Management
|
|
290
|
+
// ============================================================================
|
|
291
|
+
/**
|
|
292
|
+
* Lists all secret names in the key store.
|
|
293
|
+
* @returns Success with array of secret names, Failure if locked
|
|
294
|
+
* @public
|
|
295
|
+
*/
|
|
296
|
+
listSecrets() {
|
|
297
|
+
if (!this._secrets) {
|
|
298
|
+
return fail('Key store is locked');
|
|
299
|
+
}
|
|
300
|
+
return succeed(Array.from(this._secrets.keys()));
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Gets a secret by name.
|
|
304
|
+
* @param name - Name of the secret
|
|
305
|
+
* @returns Success with secret entry, Failure if not found or locked
|
|
306
|
+
* @public
|
|
307
|
+
*/
|
|
308
|
+
getSecret(name) {
|
|
309
|
+
if (!this._secrets) {
|
|
310
|
+
return fail('Key store is locked');
|
|
311
|
+
}
|
|
312
|
+
const entry = this._secrets.get(name);
|
|
313
|
+
if (!entry) {
|
|
314
|
+
return fail(`Secret '${name}' not found`);
|
|
315
|
+
}
|
|
316
|
+
return succeed(entry);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Checks if a secret exists.
|
|
320
|
+
* @param name - Name of the secret
|
|
321
|
+
* @returns Success with boolean, Failure if locked
|
|
322
|
+
* @public
|
|
323
|
+
*/
|
|
324
|
+
hasSecret(name) {
|
|
325
|
+
if (!this._secrets) {
|
|
326
|
+
return fail('Key store is locked');
|
|
327
|
+
}
|
|
328
|
+
return succeed(this._secrets.has(name));
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Adds a new secret with a randomly generated key.
|
|
332
|
+
* @param name - Unique name for the secret
|
|
333
|
+
* @param options - Optional description
|
|
334
|
+
* @returns Success with the generated entry, Failure if locked or name invalid
|
|
335
|
+
* @public
|
|
336
|
+
*/
|
|
337
|
+
async addSecret(name, options) {
|
|
338
|
+
if (!this._secrets) {
|
|
339
|
+
return fail('Key store is locked');
|
|
340
|
+
}
|
|
341
|
+
if (!name || name.length === 0) {
|
|
342
|
+
return fail('Secret name cannot be empty');
|
|
343
|
+
}
|
|
344
|
+
const replaced = this._secrets.has(name);
|
|
345
|
+
// Generate a new random key
|
|
346
|
+
const keyResult = await this._cryptoProvider.generateKey();
|
|
347
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
348
|
+
if (keyResult.isFailure()) {
|
|
349
|
+
return fail(`Failed to generate key: ${keyResult.message}`);
|
|
350
|
+
}
|
|
351
|
+
const entry = {
|
|
352
|
+
name,
|
|
353
|
+
type: 'encryption-key',
|
|
354
|
+
key: keyResult.value,
|
|
355
|
+
description: options === null || options === void 0 ? void 0 : options.description,
|
|
356
|
+
createdAt: getCurrentTimestamp()
|
|
357
|
+
};
|
|
358
|
+
this._secrets.set(name, entry);
|
|
359
|
+
this._dirty = true;
|
|
360
|
+
return succeed({ entry, replaced });
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Imports an existing secret key.
|
|
364
|
+
* @param name - Unique name for the secret
|
|
365
|
+
* @param key - The 32-byte AES-256 key
|
|
366
|
+
* @param options - Optional description, whether to replace existing
|
|
367
|
+
* @returns Success with entry, Failure if locked, key invalid, or exists and !replace
|
|
368
|
+
* @public
|
|
369
|
+
*/
|
|
370
|
+
importSecret(name, key, options) {
|
|
371
|
+
if (!this._secrets) {
|
|
372
|
+
return fail('Key store is locked');
|
|
373
|
+
}
|
|
374
|
+
if (!name || name.length === 0) {
|
|
375
|
+
return fail('Secret name cannot be empty');
|
|
376
|
+
}
|
|
377
|
+
if (key.length !== Constants.AES_256_KEY_SIZE) {
|
|
378
|
+
return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
|
|
379
|
+
}
|
|
380
|
+
const exists = this._secrets.has(name);
|
|
381
|
+
if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
|
|
382
|
+
return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
|
|
383
|
+
}
|
|
384
|
+
const entry = {
|
|
385
|
+
name,
|
|
386
|
+
type: 'encryption-key',
|
|
387
|
+
key: new Uint8Array(key), // Copy to prevent external modification
|
|
388
|
+
description: options === null || options === void 0 ? void 0 : options.description,
|
|
389
|
+
createdAt: getCurrentTimestamp()
|
|
390
|
+
};
|
|
391
|
+
this._secrets.set(name, entry);
|
|
392
|
+
this._dirty = true;
|
|
393
|
+
return succeed({ entry, replaced: exists });
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Adds a secret derived from a password using PBKDF2.
|
|
397
|
+
*
|
|
398
|
+
* Generates a random salt, derives a 32-byte AES-256 key from the password,
|
|
399
|
+
* and stores it in the vault. Returns the key derivation parameters so they
|
|
400
|
+
* can be stored alongside encrypted files, enabling decryption with just the
|
|
401
|
+
* password (without unlocking the keystore).
|
|
402
|
+
*
|
|
403
|
+
* @param name - Unique name for the secret
|
|
404
|
+
* @param password - Password to derive the key from
|
|
405
|
+
* @param options - Optional description, iterations, replace flag
|
|
406
|
+
* @returns Success with entry and keyDerivation params, Failure if locked or invalid
|
|
407
|
+
* @public
|
|
408
|
+
*/
|
|
409
|
+
async addSecretFromPassword(name, password, options) {
|
|
410
|
+
var _a;
|
|
411
|
+
if (!this._secrets) {
|
|
412
|
+
return fail('Key store is locked');
|
|
413
|
+
}
|
|
414
|
+
if (!name || name.length === 0) {
|
|
415
|
+
return fail('Secret name cannot be empty');
|
|
416
|
+
}
|
|
417
|
+
if (!password || password.length === 0) {
|
|
418
|
+
return fail('Password cannot be empty');
|
|
419
|
+
}
|
|
420
|
+
const exists = this._secrets.has(name);
|
|
421
|
+
if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
|
|
422
|
+
return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
|
|
423
|
+
}
|
|
424
|
+
const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : DEFAULT_SECRET_ITERATIONS;
|
|
425
|
+
// Generate a random salt for this secret's key derivation
|
|
426
|
+
const saltResult = this._cryptoProvider.generateRandomBytes(MIN_SALT_LENGTH);
|
|
427
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
428
|
+
if (saltResult.isFailure()) {
|
|
429
|
+
return fail(`Failed to generate salt: ${saltResult.message}`);
|
|
430
|
+
}
|
|
431
|
+
// Derive the key from password
|
|
432
|
+
const keyResult = await this._cryptoProvider.deriveKey(password, saltResult.value, iterations);
|
|
433
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
434
|
+
if (keyResult.isFailure()) {
|
|
435
|
+
return fail(`Key derivation failed: ${keyResult.message}`);
|
|
436
|
+
}
|
|
437
|
+
const entry = {
|
|
438
|
+
name,
|
|
439
|
+
type: 'encryption-key',
|
|
440
|
+
key: keyResult.value,
|
|
441
|
+
description: options === null || options === void 0 ? void 0 : options.description,
|
|
442
|
+
createdAt: getCurrentTimestamp()
|
|
443
|
+
};
|
|
444
|
+
this._secrets.set(name, entry);
|
|
445
|
+
this._dirty = true;
|
|
446
|
+
return succeed({
|
|
447
|
+
entry,
|
|
448
|
+
replaced: exists,
|
|
449
|
+
keyDerivation: {
|
|
450
|
+
kdf: 'pbkdf2',
|
|
451
|
+
salt: this._cryptoProvider.toBase64(saltResult.value),
|
|
452
|
+
iterations
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Removes a secret by name.
|
|
458
|
+
* @param name - Name of the secret to remove
|
|
459
|
+
* @returns Success with removed entry, Failure if not found or locked
|
|
460
|
+
* @public
|
|
461
|
+
*/
|
|
462
|
+
removeSecret(name) {
|
|
463
|
+
if (!this._secrets) {
|
|
464
|
+
return fail('Key store is locked');
|
|
465
|
+
}
|
|
466
|
+
const entry = this._secrets.get(name);
|
|
467
|
+
if (!entry) {
|
|
468
|
+
return fail(`Secret '${name}' not found`);
|
|
469
|
+
}
|
|
470
|
+
// Clear the key before removing (security)
|
|
471
|
+
entry.key.fill(0);
|
|
472
|
+
this._secrets.delete(name);
|
|
473
|
+
this._dirty = true;
|
|
474
|
+
return succeed(entry);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Imports an API key string into the vault.
|
|
478
|
+
* The string is UTF-8 encoded and stored with type `'api-key'`.
|
|
479
|
+
* @param name - Unique name for the secret
|
|
480
|
+
* @param apiKey - The API key string
|
|
481
|
+
* @param options - Optional description, whether to replace existing
|
|
482
|
+
* @returns Success with entry, Failure if locked, empty, or exists and !replace
|
|
483
|
+
* @public
|
|
484
|
+
*/
|
|
485
|
+
importApiKey(name, apiKey, options) {
|
|
486
|
+
if (!this._secrets) {
|
|
487
|
+
return fail('Key store is locked');
|
|
488
|
+
}
|
|
489
|
+
if (!name || name.length === 0) {
|
|
490
|
+
return fail('Secret name cannot be empty');
|
|
491
|
+
}
|
|
492
|
+
if (!apiKey || apiKey.length === 0) {
|
|
493
|
+
return fail('API key cannot be empty');
|
|
494
|
+
}
|
|
495
|
+
const exists = this._secrets.has(name);
|
|
496
|
+
if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
|
|
497
|
+
return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
|
|
498
|
+
}
|
|
499
|
+
const encoder = new TextEncoder();
|
|
500
|
+
const entry = {
|
|
501
|
+
name,
|
|
502
|
+
type: 'api-key',
|
|
503
|
+
key: encoder.encode(apiKey),
|
|
504
|
+
description: options === null || options === void 0 ? void 0 : options.description,
|
|
505
|
+
createdAt: getCurrentTimestamp()
|
|
506
|
+
};
|
|
507
|
+
this._secrets.set(name, entry);
|
|
508
|
+
this._dirty = true;
|
|
509
|
+
return succeed({ entry, replaced: exists });
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Retrieves an API key string by name.
|
|
513
|
+
* Only works for secrets with type `'api-key'`.
|
|
514
|
+
* @param name - Name of the secret
|
|
515
|
+
* @returns Success with the API key string, Failure if not found, locked, or wrong type
|
|
516
|
+
* @public
|
|
517
|
+
*/
|
|
518
|
+
getApiKey(name) {
|
|
519
|
+
if (!this._secrets) {
|
|
520
|
+
return fail('Key store is locked');
|
|
521
|
+
}
|
|
522
|
+
const entry = this._secrets.get(name);
|
|
523
|
+
if (!entry) {
|
|
524
|
+
return fail(`Secret '${name}' not found`);
|
|
525
|
+
}
|
|
526
|
+
if (entry.type !== 'api-key') {
|
|
527
|
+
return fail(`Secret '${name}' is not an API key (type: ${entry.type})`);
|
|
528
|
+
}
|
|
529
|
+
const decoder = new TextDecoder();
|
|
530
|
+
return succeed(decoder.decode(entry.key));
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Lists secret names filtered by type.
|
|
534
|
+
* @param type - The secret type to filter by
|
|
535
|
+
* @returns Success with array of matching secret names, Failure if locked
|
|
536
|
+
* @public
|
|
537
|
+
*/
|
|
538
|
+
listSecretsByType(type) {
|
|
539
|
+
if (!this._secrets) {
|
|
540
|
+
return fail('Key store is locked');
|
|
541
|
+
}
|
|
542
|
+
const names = [];
|
|
543
|
+
for (const [name, entry] of this._secrets) {
|
|
544
|
+
if (entry.type === type) {
|
|
545
|
+
names.push(name);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return succeed(names);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Renames a secret.
|
|
552
|
+
* @param oldName - Current name
|
|
553
|
+
* @param newName - New name
|
|
554
|
+
* @returns Success with updated entry, Failure if source not found, target exists, or locked
|
|
555
|
+
* @public
|
|
556
|
+
*/
|
|
557
|
+
renameSecret(oldName, newName) {
|
|
558
|
+
if (!this._secrets) {
|
|
559
|
+
return fail('Key store is locked');
|
|
560
|
+
}
|
|
561
|
+
if (!newName || newName.length === 0) {
|
|
562
|
+
return fail('New name cannot be empty');
|
|
563
|
+
}
|
|
564
|
+
const entry = this._secrets.get(oldName);
|
|
565
|
+
if (!entry) {
|
|
566
|
+
return fail(`Secret '${oldName}' not found`);
|
|
567
|
+
}
|
|
568
|
+
if (oldName !== newName && this._secrets.has(newName)) {
|
|
569
|
+
return fail(`Secret '${newName}' already exists`);
|
|
570
|
+
}
|
|
571
|
+
// Create new entry with new name (preserve type)
|
|
572
|
+
const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
|
|
573
|
+
this._secrets.delete(oldName);
|
|
574
|
+
this._secrets.set(newName, newEntry);
|
|
575
|
+
this._dirty = true;
|
|
576
|
+
return succeed(newEntry);
|
|
577
|
+
}
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// Persistence
|
|
580
|
+
// ============================================================================
|
|
581
|
+
/**
|
|
582
|
+
* Saves the key store, returning the encrypted file content.
|
|
583
|
+
* Requires the master password to encrypt.
|
|
584
|
+
* @param password - The master password
|
|
585
|
+
* @returns Success with IKeyStoreFile, Failure if locked
|
|
586
|
+
* @public
|
|
587
|
+
*/
|
|
588
|
+
async save(password) {
|
|
589
|
+
if (!this._secrets || !this._salt) {
|
|
590
|
+
return fail('Key store is locked');
|
|
591
|
+
}
|
|
592
|
+
if (!password || password.length === 0) {
|
|
593
|
+
return fail('Password cannot be empty');
|
|
594
|
+
}
|
|
595
|
+
// Derive the encryption key
|
|
596
|
+
const keyResult = await this._cryptoProvider.deriveKey(password, this._salt, this._iterations);
|
|
597
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
598
|
+
if (keyResult.isFailure()) {
|
|
599
|
+
return fail(`Key derivation failed: ${keyResult.message}`);
|
|
600
|
+
}
|
|
601
|
+
// Build vault contents
|
|
602
|
+
const secrets = {};
|
|
603
|
+
for (const [name, entry] of this._secrets) {
|
|
604
|
+
secrets[name] = {
|
|
605
|
+
name: entry.name,
|
|
606
|
+
type: entry.type,
|
|
607
|
+
key: this._cryptoProvider.toBase64(entry.key),
|
|
608
|
+
description: entry.description,
|
|
609
|
+
createdAt: entry.createdAt
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const vaultContents = {
|
|
613
|
+
version: KEYSTORE_FORMAT,
|
|
614
|
+
secrets
|
|
615
|
+
};
|
|
616
|
+
// Serialize and encrypt
|
|
617
|
+
const jsonResult = captureResult(() => JSON.stringify(vaultContents));
|
|
618
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
619
|
+
if (jsonResult.isFailure()) {
|
|
620
|
+
return fail(`Failed to serialize vault: ${jsonResult.message}`);
|
|
621
|
+
}
|
|
622
|
+
const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, keyResult.value);
|
|
623
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
624
|
+
if (encryptResult.isFailure()) {
|
|
625
|
+
return fail(`Encryption failed: ${encryptResult.message}`);
|
|
626
|
+
}
|
|
627
|
+
const { iv, authTag, encryptedData } = encryptResult.value;
|
|
628
|
+
const keystoreFileData = {
|
|
629
|
+
format: KEYSTORE_FORMAT,
|
|
630
|
+
algorithm: Constants.DEFAULT_ALGORITHM,
|
|
631
|
+
iv: this._cryptoProvider.toBase64(iv),
|
|
632
|
+
authTag: this._cryptoProvider.toBase64(authTag),
|
|
633
|
+
encryptedData: this._cryptoProvider.toBase64(encryptedData),
|
|
634
|
+
keyDerivation: {
|
|
635
|
+
kdf: 'pbkdf2',
|
|
636
|
+
salt: this._cryptoProvider.toBase64(this._salt),
|
|
637
|
+
iterations: this._iterations
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
this._keystoreFile = keystoreFileData;
|
|
641
|
+
this._dirty = false;
|
|
642
|
+
this._isNew = false;
|
|
643
|
+
return succeed(keystoreFileData);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Changes the master password.
|
|
647
|
+
* Re-encrypts the vault with the new password-derived key.
|
|
648
|
+
* @param currentPassword - Current master password (for verification)
|
|
649
|
+
* @param newPassword - New master password
|
|
650
|
+
* @returns Success when password changed, Failure if locked or current password incorrect
|
|
651
|
+
* @public
|
|
652
|
+
*/
|
|
653
|
+
async changePassword(currentPassword, newPassword) {
|
|
654
|
+
if (!this._secrets || !this._salt) {
|
|
655
|
+
return fail('Key store is locked');
|
|
656
|
+
}
|
|
657
|
+
if (!newPassword || newPassword.length === 0) {
|
|
658
|
+
return fail('New password cannot be empty');
|
|
659
|
+
}
|
|
660
|
+
// Verify current password by trying to derive and re-encrypt
|
|
661
|
+
// (For opened stores, we'd need to verify against the stored file)
|
|
662
|
+
if (this._keystoreFile) {
|
|
663
|
+
const saltResult = this._cryptoProvider.fromBase64(this._keystoreFile.keyDerivation.salt);
|
|
664
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
665
|
+
if (saltResult.isFailure()) {
|
|
666
|
+
return fail(`Invalid salt in key store file: ${saltResult.message}`);
|
|
667
|
+
}
|
|
668
|
+
const keyResult = await this._cryptoProvider.deriveKey(currentPassword, saltResult.value, this._keystoreFile.keyDerivation.iterations);
|
|
669
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
670
|
+
if (keyResult.isFailure()) {
|
|
671
|
+
return fail(`Key derivation failed: ${keyResult.message}`);
|
|
672
|
+
}
|
|
673
|
+
// Try to decrypt to verify password
|
|
674
|
+
const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
|
|
675
|
+
const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
|
|
676
|
+
const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
|
|
677
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
678
|
+
if (ivResult.isFailure() || authTagResult.isFailure() || encryptedDataResult.isFailure()) {
|
|
679
|
+
return fail('Invalid key store file format');
|
|
680
|
+
}
|
|
681
|
+
const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
|
|
682
|
+
if (decryptResult.isFailure()) {
|
|
683
|
+
return fail('Current password is incorrect');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Generate new salt for the new password using crypto provider
|
|
687
|
+
const saltResult = this._cryptoProvider.generateRandomBytes(MIN_SALT_LENGTH);
|
|
688
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
689
|
+
if (saltResult.isFailure()) {
|
|
690
|
+
return fail(`Failed to generate salt: ${saltResult.message}`);
|
|
691
|
+
}
|
|
692
|
+
this._salt = saltResult.value;
|
|
693
|
+
this._dirty = true;
|
|
694
|
+
// Save with new password
|
|
695
|
+
const saveResult = await this.save(newPassword);
|
|
696
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
697
|
+
if (saveResult.isFailure()) {
|
|
698
|
+
return fail(saveResult.message);
|
|
699
|
+
}
|
|
700
|
+
return succeed(this);
|
|
701
|
+
}
|
|
702
|
+
// ============================================================================
|
|
703
|
+
// IEncryptionProvider
|
|
704
|
+
// ============================================================================
|
|
705
|
+
/** {@inheritDoc IEncryptionProvider.encryptByName} */
|
|
706
|
+
async encryptByName(secretName, content, metadata) {
|
|
707
|
+
const secretResult = this.getSecret(secretName);
|
|
708
|
+
if (secretResult.isFailure()) {
|
|
709
|
+
return fail(`encryptByName: ${secretResult.message}`);
|
|
710
|
+
}
|
|
711
|
+
return createEncryptedFile({
|
|
712
|
+
content,
|
|
713
|
+
secretName,
|
|
714
|
+
key: secretResult.value.key,
|
|
715
|
+
cryptoProvider: this._cryptoProvider,
|
|
716
|
+
metadata
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// Integration with IEncryptionConfig
|
|
721
|
+
// ============================================================================
|
|
722
|
+
/**
|
|
723
|
+
* Creates a SecretProvider function for use with IEncryptionConfig.
|
|
724
|
+
* The returned function looks up secrets from this key store.
|
|
725
|
+
* @returns Success with SecretProvider, Failure if locked
|
|
726
|
+
* @public
|
|
727
|
+
*/
|
|
728
|
+
getSecretProvider() {
|
|
729
|
+
if (!this._secrets) {
|
|
730
|
+
return fail('Key store is locked');
|
|
731
|
+
}
|
|
732
|
+
const secrets = this._secrets;
|
|
733
|
+
const provider = async (secretName) => {
|
|
734
|
+
const entry = secrets.get(secretName);
|
|
735
|
+
if (!entry) {
|
|
736
|
+
return fail(`Secret '${secretName}' not found in key store`);
|
|
737
|
+
}
|
|
738
|
+
return succeed(entry.key);
|
|
739
|
+
};
|
|
740
|
+
return succeed(provider);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Creates a partial IEncryptionConfig using this key store as the secret source.
|
|
744
|
+
* @returns Partial config that can be spread into a full IEncryptionConfig
|
|
745
|
+
* @public
|
|
746
|
+
*/
|
|
747
|
+
getEncryptionConfig() {
|
|
748
|
+
const providerResult = this.getSecretProvider();
|
|
749
|
+
if (providerResult.isFailure()) {
|
|
750
|
+
return fail(providerResult.message);
|
|
751
|
+
}
|
|
752
|
+
return succeed({
|
|
753
|
+
secretProvider: providerResult.value,
|
|
754
|
+
cryptoProvider: this._cryptoProvider
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
//# sourceMappingURL=keyStore.js.map
|