@wemush/wols 0.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/LICENSE +201 -0
- package/README.md +294 -0
- package/dist/cjs/compact-url.js +272 -0
- package/dist/cjs/compact-url.js.map +1 -0
- package/dist/cjs/crypto/decrypt.js +125 -0
- package/dist/cjs/crypto/decrypt.js.map +1 -0
- package/dist/cjs/crypto/encrypt.js +151 -0
- package/dist/cjs/crypto/encrypt.js.map +1 -0
- package/dist/cjs/crypto/index.js +16 -0
- package/dist/cjs/crypto/index.js.map +1 -0
- package/dist/cjs/errors.js +188 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.js +81 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/parser.js +148 -0
- package/dist/cjs/parser.js.map +1 -0
- package/dist/cjs/qr/generate.js +275 -0
- package/dist/cjs/qr/generate.js.map +1 -0
- package/dist/cjs/qr/index.js +18 -0
- package/dist/cjs/qr/index.js.map +1 -0
- package/dist/cjs/specimen.js +163 -0
- package/dist/cjs/specimen.js.map +1 -0
- package/dist/cjs/types.js +78 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils/cuid.js +150 -0
- package/dist/cjs/utils/cuid.js.map +1 -0
- package/dist/cjs/utils/iso8601.js +131 -0
- package/dist/cjs/utils/iso8601.js.map +1 -0
- package/dist/cjs/validator.js +307 -0
- package/dist/cjs/validator.js.map +1 -0
- package/dist/esm/compact-url.js +264 -0
- package/dist/esm/compact-url.js.map +1 -0
- package/dist/esm/crypto/decrypt.js +122 -0
- package/dist/esm/crypto/decrypt.js.map +1 -0
- package/dist/esm/crypto/encrypt.js +147 -0
- package/dist/esm/crypto/encrypt.js.map +1 -0
- package/dist/esm/crypto/index.js +10 -0
- package/dist/esm/crypto/index.js.map +1 -0
- package/dist/esm/errors.js +178 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.js +47 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/parser.js +145 -0
- package/dist/esm/parser.js.map +1 -0
- package/dist/esm/qr/generate.js +235 -0
- package/dist/esm/qr/generate.js.map +1 -0
- package/dist/esm/qr/index.js +10 -0
- package/dist/esm/qr/index.js.map +1 -0
- package/dist/esm/specimen.js +159 -0
- package/dist/esm/specimen.js.map +1 -0
- package/dist/esm/types.js +71 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils/cuid.js +145 -0
- package/dist/esm/utils/cuid.js.map +1 -0
- package/dist/esm/utils/iso8601.js +122 -0
- package/dist/esm/utils/iso8601.js.map +1 -0
- package/dist/esm/validator.js +304 -0
- package/dist/esm/validator.js.map +1 -0
- package/dist/types/compact-url.d.ts +104 -0
- package/dist/types/compact-url.d.ts.map +1 -0
- package/dist/types/crypto/decrypt.d.ts +33 -0
- package/dist/types/crypto/decrypt.d.ts.map +1 -0
- package/dist/types/crypto/encrypt.d.ts +32 -0
- package/dist/types/crypto/encrypt.d.ts.map +1 -0
- package/dist/types/crypto/index.d.ts +10 -0
- package/dist/types/crypto/index.d.ts.map +1 -0
- package/dist/types/errors.d.ts +103 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/parser.d.ts +30 -0
- package/dist/types/parser.d.ts.map +1 -0
- package/dist/types/qr/generate.d.ts +62 -0
- package/dist/types/qr/generate.d.ts.map +1 -0
- package/dist/types/qr/index.d.ts +10 -0
- package/dist/types/qr/index.d.ts.map +1 -0
- package/dist/types/specimen.d.ts +71 -0
- package/dist/types/specimen.d.ts.map +1 -0
- package/dist/types/types.d.ts +269 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils/cuid.d.ts +44 -0
- package/dist/types/utils/cuid.d.ts.map +1 -0
- package/dist/types/utils/iso8601.d.ts +79 -0
- package/dist/types/utils/iso8601.d.ts.map +1 -0
- package/dist/types/validator.d.ts +25 -0
- package/dist/types/validator.d.ts.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Compact URL Format Functions
|
|
4
|
+
*
|
|
5
|
+
* This module provides functions to generate and parse compact URL format
|
|
6
|
+
* for WOLS specimens. The compact format is designed for small QR codes
|
|
7
|
+
* (petri dishes, culture tubes) where space is limited.
|
|
8
|
+
*
|
|
9
|
+
* Format: wemush://v1/{id}?s={species_code}&st={stage}&t={timestamp}
|
|
10
|
+
*
|
|
11
|
+
* @module compact-url
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SPECIES_CODES = void 0;
|
|
15
|
+
exports.getSpeciesFromCode = getSpeciesFromCode;
|
|
16
|
+
exports.getCodeFromSpecies = getCodeFromSpecies;
|
|
17
|
+
exports.registerSpeciesCode = registerSpeciesCode;
|
|
18
|
+
exports.toCompactUrl = toCompactUrl;
|
|
19
|
+
exports.parseCompactUrl = parseCompactUrl;
|
|
20
|
+
const errors_js_1 = require("./errors.js");
|
|
21
|
+
/**
|
|
22
|
+
* Standard species codes from the WOLS specification
|
|
23
|
+
*
|
|
24
|
+
* @see https://github.com/wemush/open-standard for additional codes
|
|
25
|
+
*/
|
|
26
|
+
exports.SPECIES_CODES = {
|
|
27
|
+
POSTR: 'Pleurotus ostreatus',
|
|
28
|
+
PCITRI: 'Pleurotus citrinopileatus',
|
|
29
|
+
PDJAR: 'Pleurotus djamor',
|
|
30
|
+
PERYN: 'Pleurotus eryngii',
|
|
31
|
+
HERIN: 'Hericium erinaceus',
|
|
32
|
+
GLUCI: 'Ganoderma lucidum',
|
|
33
|
+
LPUDE: 'Laetiporus pudens',
|
|
34
|
+
LLENC: 'Lentinula edodes',
|
|
35
|
+
APOLY: 'Agrocybe polyphylla',
|
|
36
|
+
STRUG: 'Stropharia rugosoannulata',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Reverse lookup map: species name → code
|
|
40
|
+
*/
|
|
41
|
+
const SPECIES_TO_CODE = {};
|
|
42
|
+
// Initialize reverse lookup
|
|
43
|
+
for (const [code, species] of Object.entries(exports.SPECIES_CODES)) {
|
|
44
|
+
SPECIES_TO_CODE[species] = code;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the full species name from a species code
|
|
48
|
+
*
|
|
49
|
+
* @param code - Species code (e.g., "POSTR")
|
|
50
|
+
* @returns Species name or undefined if code is unknown
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* getSpeciesFromCode('POSTR') // Returns "Pleurotus ostreatus"
|
|
55
|
+
* getSpeciesFromCode('XXXXX') // Returns undefined
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
function getSpeciesFromCode(code) {
|
|
59
|
+
return exports.SPECIES_CODES[code];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the species code from a species name
|
|
63
|
+
*
|
|
64
|
+
* @param species - Full species name (e.g., "Pleurotus ostreatus")
|
|
65
|
+
* @returns Species code or undefined if species is not registered
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* getCodeFromSpecies('Pleurotus ostreatus') // Returns "POSTR"
|
|
70
|
+
* getCodeFromSpecies('Unknown species') // Returns undefined
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
function getCodeFromSpecies(species) {
|
|
74
|
+
return SPECIES_TO_CODE[species];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Register a custom species code
|
|
78
|
+
*
|
|
79
|
+
* Use this to add species codes that are not in the standard WOLS list.
|
|
80
|
+
*
|
|
81
|
+
* @param code - Species code (uppercase, 5 characters recommended)
|
|
82
|
+
* @param species - Full species name
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* registerSpeciesCode('CCOMA', 'Coprinus comatus');
|
|
87
|
+
* getSpeciesFromCode('CCOMA') // Returns "Coprinus comatus"
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
function registerSpeciesCode(code, species) {
|
|
91
|
+
// Remove old reverse lookup if code is being overwritten
|
|
92
|
+
const oldSpecies = exports.SPECIES_CODES[code];
|
|
93
|
+
if (oldSpecies !== undefined) {
|
|
94
|
+
delete SPECIES_TO_CODE[oldSpecies];
|
|
95
|
+
}
|
|
96
|
+
exports.SPECIES_CODES[code] = species;
|
|
97
|
+
SPECIES_TO_CODE[species] = code;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generate a compact URL from a specimen
|
|
101
|
+
*
|
|
102
|
+
* The compact URL format is designed for small QR codes:
|
|
103
|
+
* ```
|
|
104
|
+
* wemush://v1/{id}?s={species_code}&st={stage}&t={timestamp}
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @param specimen - The specimen to encode
|
|
108
|
+
* @returns Compact URL string
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const specimen = createSpecimen({
|
|
113
|
+
* type: 'CULTURE',
|
|
114
|
+
* species: 'Pleurotus ostreatus',
|
|
115
|
+
* stage: 'COLONIZATION'
|
|
116
|
+
* });
|
|
117
|
+
*
|
|
118
|
+
* const url = toCompactUrl(specimen);
|
|
119
|
+
* // wemush://v1/clx1a2b3c4?s=POSTR&st=COLONIZATION&ty=CULTURE&t=1734307200
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
function toCompactUrl(specimen) {
|
|
123
|
+
// Extract ID without prefix
|
|
124
|
+
const idWithoutPrefix = specimen.id.replace(/^wemush:/, '');
|
|
125
|
+
// Start building URL
|
|
126
|
+
let url = `wemush://v1/${idWithoutPrefix}`;
|
|
127
|
+
// Build query parameters
|
|
128
|
+
const params = new URLSearchParams();
|
|
129
|
+
// Species: use code if known, otherwise full name
|
|
130
|
+
const speciesCode = getCodeFromSpecies(specimen.species);
|
|
131
|
+
if (speciesCode !== undefined) {
|
|
132
|
+
params.set('s', speciesCode);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
params.set('s', specimen.species);
|
|
136
|
+
}
|
|
137
|
+
// Type
|
|
138
|
+
params.set('ty', specimen.type);
|
|
139
|
+
// Stage (if present)
|
|
140
|
+
if (specimen.stage !== undefined) {
|
|
141
|
+
params.set('st', specimen.stage);
|
|
142
|
+
}
|
|
143
|
+
// Timestamp: convert ISO date to Unix timestamp
|
|
144
|
+
if (specimen.created !== undefined) {
|
|
145
|
+
const timestamp = Math.floor(new Date(specimen.created).getTime() / 1000);
|
|
146
|
+
params.set('t', timestamp.toString());
|
|
147
|
+
}
|
|
148
|
+
// Optional: Batch
|
|
149
|
+
if (specimen.batch !== undefined) {
|
|
150
|
+
params.set('b', specimen.batch);
|
|
151
|
+
}
|
|
152
|
+
// Optional: Strain name
|
|
153
|
+
if (specimen.strain?.name !== undefined) {
|
|
154
|
+
params.set('sn', specimen.strain.name);
|
|
155
|
+
}
|
|
156
|
+
// Optional: Strain generation
|
|
157
|
+
if (specimen.strain?.generation !== undefined) {
|
|
158
|
+
params.set('sg', specimen.strain.generation);
|
|
159
|
+
}
|
|
160
|
+
// Append query string
|
|
161
|
+
const queryString = params.toString();
|
|
162
|
+
if (queryString) {
|
|
163
|
+
url += `?${queryString}`;
|
|
164
|
+
}
|
|
165
|
+
return url;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse a compact URL into a SpecimenRef
|
|
169
|
+
*
|
|
170
|
+
* Returns a reference object containing the minimal information
|
|
171
|
+
* encoded in the compact URL. Use the id to fetch full specimen
|
|
172
|
+
* data from the API if needed.
|
|
173
|
+
*
|
|
174
|
+
* @param url - Compact URL to parse (wemush://v1/...)
|
|
175
|
+
* @returns ParseResult with SpecimenRef on success, error on failure
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* const result = parseCompactUrl('wemush://v1/clx1a2b3c4?s=POSTR&st=COLONIZATION');
|
|
180
|
+
*
|
|
181
|
+
* if (result.success) {
|
|
182
|
+
* console.log(result.data.species); // "Pleurotus ostreatus"
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
function parseCompactUrl(url) {
|
|
187
|
+
// Validate URL scheme
|
|
188
|
+
if (!url.startsWith('wemush://')) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
error: new errors_js_1.WolsParseError(errors_js_1.WolsErrorCode.INVALID_URL, 'URL must start with wemush://', {
|
|
192
|
+
url,
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Parse URL parts
|
|
197
|
+
const withoutScheme = url.slice('wemush://'.length);
|
|
198
|
+
const [pathPart, queryPart] = withoutScheme.split('?');
|
|
199
|
+
if (!pathPart) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: new errors_js_1.WolsParseError(errors_js_1.WolsErrorCode.INVALID_URL, 'Invalid compact URL format: missing path', { url }),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Parse path: v1/{id}
|
|
206
|
+
const pathSegments = pathPart.split('/');
|
|
207
|
+
if (pathSegments.length < 2 || !pathSegments[1]) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: new errors_js_1.WolsParseError(errors_js_1.WolsErrorCode.INVALID_URL, 'Invalid compact URL format: missing specimen ID', { url }),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const version = pathSegments[0] === 'v1' ? '1.1.0' : '1.0.0';
|
|
214
|
+
const idWithoutPrefix = pathSegments[1];
|
|
215
|
+
// Parse query parameters
|
|
216
|
+
const params = new URLSearchParams(queryPart ?? '');
|
|
217
|
+
// Extract species (resolve code if known)
|
|
218
|
+
const speciesParam = params.get('s');
|
|
219
|
+
let species;
|
|
220
|
+
if (speciesParam !== null) {
|
|
221
|
+
const resolvedSpecies = getSpeciesFromCode(speciesParam);
|
|
222
|
+
species = resolvedSpecies ?? speciesParam;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
error: new errors_js_1.WolsParseError(errors_js_1.WolsErrorCode.INVALID_URL, 'Invalid compact URL format: missing species parameter', { url }),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Build result object
|
|
231
|
+
const result = {
|
|
232
|
+
id: `wemush:${idWithoutPrefix}`,
|
|
233
|
+
species,
|
|
234
|
+
version,
|
|
235
|
+
};
|
|
236
|
+
// Extract type
|
|
237
|
+
const typeParam = params.get('ty');
|
|
238
|
+
if (typeParam !== null) {
|
|
239
|
+
result.type = typeParam;
|
|
240
|
+
}
|
|
241
|
+
// Extract stage
|
|
242
|
+
const stageParam = params.get('st');
|
|
243
|
+
if (stageParam !== null) {
|
|
244
|
+
result.stage = stageParam;
|
|
245
|
+
}
|
|
246
|
+
// Extract timestamp
|
|
247
|
+
const timestampParam = params.get('t');
|
|
248
|
+
if (timestampParam !== null) {
|
|
249
|
+
result.timestamp = parseInt(timestampParam, 10);
|
|
250
|
+
}
|
|
251
|
+
// Extract batch
|
|
252
|
+
const batchParam = params.get('b');
|
|
253
|
+
if (batchParam !== null) {
|
|
254
|
+
result.batch = batchParam;
|
|
255
|
+
}
|
|
256
|
+
// Extract strain info
|
|
257
|
+
const strainNameParam = params.get('sn');
|
|
258
|
+
const strainGenParam = params.get('sg');
|
|
259
|
+
if (strainNameParam !== null) {
|
|
260
|
+
result.strain = {
|
|
261
|
+
name: strainNameParam,
|
|
262
|
+
};
|
|
263
|
+
if (strainGenParam !== null) {
|
|
264
|
+
result.strain.generation = strainGenParam;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
data: result,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=compact-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-url.js","sourceRoot":"","sources":["../../src/compact-url.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WOLS Decryption Module
|
|
4
|
+
*
|
|
5
|
+
* Provides AES-256-GCM decryption for encrypted specimens using Web Crypto API.
|
|
6
|
+
* Supports both password-based (PBKDF2) and raw CryptoKey decryption.
|
|
7
|
+
*
|
|
8
|
+
* @module crypto/decrypt
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.decryptSpecimen = decryptSpecimen;
|
|
12
|
+
const errors_js_1 = require("../errors.js");
|
|
13
|
+
const parser_js_1 = require("../parser.js");
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// CONSTANTS
|
|
16
|
+
// =============================================================================
|
|
17
|
+
/** Salt for PBKDF2 key derivation (must match encrypt.ts) */
|
|
18
|
+
const PBKDF2_SALT = new TextEncoder().encode('@wemush/wols/crypto/v1');
|
|
19
|
+
/** Number of PBKDF2 iterations (must match encrypt.ts) */
|
|
20
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// UTILITY FUNCTIONS
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Convert base64url string to ArrayBuffer
|
|
26
|
+
*/
|
|
27
|
+
function fromBase64Url(base64url) {
|
|
28
|
+
// Convert base64url to base64
|
|
29
|
+
const base64 = base64url
|
|
30
|
+
.replace(/-/g, '+')
|
|
31
|
+
.replace(/_/g, '/')
|
|
32
|
+
.padEnd(base64url.length + ((4 - (base64url.length % 4)) % 4), '=');
|
|
33
|
+
// Decode base64 to binary string
|
|
34
|
+
const binaryString = atob(base64);
|
|
35
|
+
// Convert to ArrayBuffer
|
|
36
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
37
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
38
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
39
|
+
}
|
|
40
|
+
return bytes.buffer;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Derive a CryptoKey from a password using PBKDF2
|
|
44
|
+
*/
|
|
45
|
+
async function deriveKey(password) {
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
const passwordBytes = encoder.encode(password);
|
|
48
|
+
// Import password as key material
|
|
49
|
+
const keyMaterial = await crypto.subtle.importKey('raw', passwordBytes, 'PBKDF2', false, [
|
|
50
|
+
'deriveKey',
|
|
51
|
+
]);
|
|
52
|
+
// Derive AES-256-GCM key
|
|
53
|
+
return crypto.subtle.deriveKey({
|
|
54
|
+
name: 'PBKDF2',
|
|
55
|
+
salt: PBKDF2_SALT,
|
|
56
|
+
iterations: PBKDF2_ITERATIONS,
|
|
57
|
+
hash: 'SHA-256',
|
|
58
|
+
}, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
|
|
59
|
+
}
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// DECRYPTION FUNCTIONS
|
|
62
|
+
// =============================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Decrypt an encrypted specimen using AES-256-GCM
|
|
65
|
+
*
|
|
66
|
+
* @param encrypted - The encrypted specimen wrapper
|
|
67
|
+
* @param options - Decryption options (key required)
|
|
68
|
+
* @returns Promise resolving to ParseResult with decrypted specimen or error
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { decryptSpecimen } from '@wemush/wols/crypto';
|
|
73
|
+
*
|
|
74
|
+
* const result = await decryptSpecimen(encrypted, {
|
|
75
|
+
* key: 'my-secret-password',
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* if (result.success) {
|
|
79
|
+
* console.log(result.data);
|
|
80
|
+
* } else {
|
|
81
|
+
* console.error(result.error);
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
async function decryptSpecimen(encrypted, options) {
|
|
86
|
+
const { key } = options;
|
|
87
|
+
// Validate encrypted specimen structure
|
|
88
|
+
if (!encrypted.payload) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
error: new errors_js_1.WolsEncryptionError(errors_js_1.WolsErrorCode.DECRYPTION_ERROR, 'Missing encrypted payload', {
|
|
92
|
+
path: ['payload'],
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (!encrypted.iv) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error: new errors_js_1.WolsEncryptionError(errors_js_1.WolsErrorCode.DECRYPTION_ERROR, 'Missing initialization vector (IV)', { path: ['iv'] }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
// Get or derive the decryption key
|
|
104
|
+
const cryptoKey = typeof key === 'string' ? await deriveKey(key) : key;
|
|
105
|
+
// Decode payload and IV from base64url
|
|
106
|
+
const ciphertext = fromBase64Url(encrypted.payload);
|
|
107
|
+
const ivBuffer = fromBase64Url(encrypted.iv);
|
|
108
|
+
const iv = new Uint8Array(ivBuffer);
|
|
109
|
+
// Decrypt
|
|
110
|
+
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, ciphertext);
|
|
111
|
+
// Decode plaintext to string
|
|
112
|
+
const json = new TextDecoder().decode(plaintext);
|
|
113
|
+
// Parse the decrypted JSON back into a specimen
|
|
114
|
+
return (0, parser_js_1.parseSpecimen)(json);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Handle decryption errors (wrong key, tampered data, etc.)
|
|
118
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown decryption error';
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
error: new errors_js_1.WolsEncryptionError(errors_js_1.WolsErrorCode.DECRYPTION_ERROR, `Decryption failed: ${errorMessage}`),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=decrypt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decrypt.js","sourceRoot":"","sources":["../../../src/crypto/decrypt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WOLS Encryption Module
|
|
4
|
+
*
|
|
5
|
+
* Provides AES-256-GCM encryption for specimens using Web Crypto API.
|
|
6
|
+
* Supports both password-based (PBKDF2) and raw CryptoKey encryption.
|
|
7
|
+
*
|
|
8
|
+
* @module crypto/encrypt
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.encryptSpecimen = encryptSpecimen;
|
|
12
|
+
exports.isEncrypted = isEncrypted;
|
|
13
|
+
const specimen_js_1 = require("../specimen.js");
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// CONSTANTS
|
|
16
|
+
// =============================================================================
|
|
17
|
+
/** Salt for PBKDF2 key derivation (fixed for deterministic key from same password) */
|
|
18
|
+
const PBKDF2_SALT = new TextEncoder().encode('@wemush/wols/crypto/v1');
|
|
19
|
+
/** Number of PBKDF2 iterations */
|
|
20
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
21
|
+
/** IV length in bytes (96 bits = 12 bytes recommended for GCM) */
|
|
22
|
+
const IV_LENGTH = 12;
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// UTILITY FUNCTIONS
|
|
25
|
+
// =============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Convert ArrayBuffer to base64url string
|
|
28
|
+
*/
|
|
29
|
+
function toBase64Url(buffer) {
|
|
30
|
+
const bytes = new Uint8Array(buffer);
|
|
31
|
+
let binary = '';
|
|
32
|
+
for (const byte of bytes) {
|
|
33
|
+
binary += String.fromCharCode(byte);
|
|
34
|
+
}
|
|
35
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Derive a CryptoKey from a password using PBKDF2
|
|
39
|
+
*/
|
|
40
|
+
async function deriveKey(password) {
|
|
41
|
+
const encoder = new TextEncoder();
|
|
42
|
+
const passwordBytes = encoder.encode(password);
|
|
43
|
+
// Import password as key material
|
|
44
|
+
const keyMaterial = await crypto.subtle.importKey('raw', passwordBytes, 'PBKDF2', false, [
|
|
45
|
+
'deriveKey',
|
|
46
|
+
]);
|
|
47
|
+
// Derive AES-256-GCM key
|
|
48
|
+
return crypto.subtle.deriveKey({
|
|
49
|
+
name: 'PBKDF2',
|
|
50
|
+
salt: PBKDF2_SALT,
|
|
51
|
+
iterations: PBKDF2_ITERATIONS,
|
|
52
|
+
hash: 'SHA-256',
|
|
53
|
+
}, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate a random initialization vector
|
|
57
|
+
*/
|
|
58
|
+
function generateIV() {
|
|
59
|
+
const buffer = new ArrayBuffer(IV_LENGTH);
|
|
60
|
+
const iv = new Uint8Array(buffer);
|
|
61
|
+
crypto.getRandomValues(iv);
|
|
62
|
+
return { iv, ivBuffer: buffer };
|
|
63
|
+
}
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// ENCRYPTION FUNCTIONS
|
|
66
|
+
// =============================================================================
|
|
67
|
+
/**
|
|
68
|
+
* Encrypt a specimen using AES-256-GCM
|
|
69
|
+
*
|
|
70
|
+
* @param specimen - The specimen to encrypt
|
|
71
|
+
* @param options - Encryption options (key required)
|
|
72
|
+
* @returns Promise resolving to encrypted specimen wrapper
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* import { encryptSpecimen } from '@wemush/wols/crypto';
|
|
77
|
+
*
|
|
78
|
+
* const encrypted = await encryptSpecimen(specimen, {
|
|
79
|
+
* key: 'my-secret-password',
|
|
80
|
+
* });
|
|
81
|
+
* // Returns: { encrypted: true, payload: "...", iv: "...", algorithm: "AES-256-GCM" }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
async function encryptSpecimen(specimen, options) {
|
|
85
|
+
const { key, fields } = options;
|
|
86
|
+
// If partial encryption requested, handle separately
|
|
87
|
+
if (fields !== undefined && fields.length > 0) {
|
|
88
|
+
return encryptPartial(specimen, options);
|
|
89
|
+
}
|
|
90
|
+
// Get or derive the encryption key
|
|
91
|
+
const cryptoKey = typeof key === 'string' ? await deriveKey(key) : key;
|
|
92
|
+
// Serialize the specimen to JSON
|
|
93
|
+
const plaintext = (0, specimen_js_1.serializeSpecimen)(specimen);
|
|
94
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
95
|
+
// Generate random IV
|
|
96
|
+
const { iv, ivBuffer } = generateIV();
|
|
97
|
+
// Encrypt
|
|
98
|
+
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, plaintextBytes);
|
|
99
|
+
// Return encrypted specimen
|
|
100
|
+
return {
|
|
101
|
+
encrypted: true,
|
|
102
|
+
payload: toBase64Url(ciphertext),
|
|
103
|
+
iv: toBase64Url(ivBuffer),
|
|
104
|
+
algorithm: 'AES-256-GCM',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Encrypt only specific fields of a specimen
|
|
109
|
+
*
|
|
110
|
+
* Returns a specimen-like object with specified fields encrypted.
|
|
111
|
+
*/
|
|
112
|
+
async function encryptPartial(specimen, options) {
|
|
113
|
+
const { key, fields = [] } = options;
|
|
114
|
+
// Get or derive the encryption key
|
|
115
|
+
const cryptoKey = typeof key === 'string' ? await deriveKey(key) : key;
|
|
116
|
+
// Clone the specimen
|
|
117
|
+
const result = { ...specimen };
|
|
118
|
+
// Encrypt each specified field
|
|
119
|
+
for (const field of fields) {
|
|
120
|
+
const value = result[field];
|
|
121
|
+
if (value !== undefined) {
|
|
122
|
+
const plaintext = JSON.stringify(value);
|
|
123
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
124
|
+
const { iv, ivBuffer } = generateIV();
|
|
125
|
+
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, plaintextBytes);
|
|
126
|
+
result[field] = {
|
|
127
|
+
encrypted: true,
|
|
128
|
+
payload: toBase64Url(ciphertext),
|
|
129
|
+
iv: toBase64Url(ivBuffer),
|
|
130
|
+
algorithm: 'AES-256-GCM',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Cast to EncryptedSpecimen for return type consistency
|
|
135
|
+
// In partial mode, this is actually a hybrid object
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Type guard to check if an object is an encrypted specimen
|
|
140
|
+
*/
|
|
141
|
+
function isEncrypted(value) {
|
|
142
|
+
if (value === null || value === undefined) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (typeof value !== 'object') {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const obj = value;
|
|
149
|
+
return obj['encrypted'] === true;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=encrypt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encrypt.js","sourceRoot":"","sources":["../../../src/crypto/encrypt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WOLS Crypto Module
|
|
4
|
+
*
|
|
5
|
+
* Provides AES-256-GCM encryption and decryption for specimens.
|
|
6
|
+
*
|
|
7
|
+
* @module crypto
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.decryptSpecimen = exports.isEncrypted = exports.encryptSpecimen = void 0;
|
|
11
|
+
var encrypt_js_1 = require("./encrypt.js");
|
|
12
|
+
Object.defineProperty(exports, "encryptSpecimen", { enumerable: true, get: function () { return encrypt_js_1.encryptSpecimen; } });
|
|
13
|
+
Object.defineProperty(exports, "isEncrypted", { enumerable: true, get: function () { return encrypt_js_1.isEncrypted; } });
|
|
14
|
+
var decrypt_js_1 = require("./decrypt.js");
|
|
15
|
+
Object.defineProperty(exports, "decryptSpecimen", { enumerable: true, get: function () { return decrypt_js_1.decryptSpecimen; } });
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/crypto/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,2CAA4D;AAAnD,6GAAA,eAAe,OAAA;AAAE,yGAAA,WAAW,OAAA;AACrC,2CAA+C;AAAtC,6GAAA,eAAe,OAAA"}
|