joshlei-cookies 1.0.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.
Files changed (2) hide show
  1. package/index.js +346 -0
  2. package/package.json +38 -0
package/index.js ADDED
@@ -0,0 +1,346 @@
1
+ const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
2
+
3
+ let cryptoAPI;
4
+ const isCryptoAvailable = typeof crypto !== 'undefined' && crypto.subtle;
5
+
6
+ const initCrypto = async () => {
7
+ if (cryptoAPI) return cryptoAPI;
8
+
9
+ if (isCryptoAvailable) {
10
+ cryptoAPI = crypto;
11
+ } else {
12
+ try {
13
+ if (typeof window === 'undefined') {
14
+ const nodeCrypto = await import('crypto');
15
+ cryptoAPI = {
16
+ subtle: {
17
+ importKey: async (format, keyData, algorithm, extractable, keyUsages) => {
18
+ return { keyData, algorithm };
19
+ },
20
+ encrypt: async (algorithm, key, data) => {
21
+ const cipher = nodeCrypto.createCipher('aes-256-cbc', Buffer.from(key.keyData));
22
+ let encrypted = cipher.update(data, 'utf8', 'hex');
23
+ encrypted += cipher.final('hex');
24
+ return Buffer.from(encrypted, 'hex');
25
+ },
26
+ decrypt: async (algorithm, key, encryptedData) => {
27
+ const decipher = nodeCrypto.createDecipher('aes-256-cbc', Buffer.from(key.keyData));
28
+ let decrypted = decipher.update(Buffer.from(encryptedData), 'hex', 'utf8');
29
+ decrypted += decipher.final('utf8');
30
+ return Buffer.from(decrypted, 'utf8');
31
+ },
32
+ digest: async (algorithm, data) => {
33
+ const hash = nodeCrypto.createHash('sha256');
34
+ hash.update(data);
35
+ return hash.digest();
36
+ }
37
+ },
38
+ getRandomValues: (array) => {
39
+ const randomBytes = nodeCrypto.randomBytes(array.length);
40
+ array.set(randomBytes);
41
+ return array;
42
+ }
43
+ };
44
+ } else {
45
+ throw new Error('Node.js crypto not available in browser');
46
+ }
47
+ } catch (nodeError) {
48
+ cryptoAPI = {
49
+ subtle: {
50
+ importKey: async (format, keyData, algorithm, extractable, keyUsages) => {
51
+ return { keyData, algorithm };
52
+ },
53
+ encrypt: async (algorithm, key, data) => {
54
+ const keyBytes = new Uint8Array(key.keyData);
55
+ const dataBytes = new Uint8Array(data);
56
+ const encrypted = new Uint8Array(dataBytes.length);
57
+
58
+ for (let i = 0; i < dataBytes.length; i++) {
59
+ encrypted[i] = dataBytes[i] ^ keyBytes[i % keyBytes.length];
60
+ }
61
+ return encrypted.buffer;
62
+ },
63
+ decrypt: async (algorithm, key, encryptedData) => {
64
+ const keyBytes = new Uint8Array(key.keyData);
65
+ const encryptedBytes = new Uint8Array(encryptedData);
66
+ const decrypted = new Uint8Array(encryptedBytes.length);
67
+
68
+ for (let i = 0; i < encryptedBytes.length; i++) {
69
+ decrypted[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length];
70
+ }
71
+ return decrypted.buffer;
72
+ },
73
+ digest: async (algorithm, data) => {
74
+ const dataBytes = new Uint8Array(data);
75
+ const hash = new Uint8Array(32);
76
+ let hashValue = 0;
77
+
78
+ for (let i = 0; i < dataBytes.length; i++) {
79
+ hashValue = ((hashValue << 5) - hashValue + dataBytes[i]) & 0xffffffff;
80
+ }
81
+
82
+ for (let i = 0; i < 32; i++) {
83
+ hash[i] = (hashValue >> (i % 32)) & 0xff;
84
+ }
85
+
86
+ return hash.buffer;
87
+ }
88
+ },
89
+ getRandomValues: (array) => {
90
+ for (let i = 0; i < array.length; i++) {
91
+ array[i] = Math.floor(Math.random() * 256);
92
+ }
93
+ return array;
94
+ }
95
+ };
96
+
97
+ console.warn('⚠️ Using fallback crypto implementation. For production use, ensure Web Crypto API or Node.js crypto is available.');
98
+ }
99
+ }
100
+
101
+ return cryptoAPI;
102
+ };
103
+
104
+ const CookieManager = {
105
+ set: (name, value, options = {}) => {
106
+ if (!isBrowser) {
107
+ throw new Error('Cookies can only be set in a browser environment');
108
+ }
109
+
110
+ let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
111
+
112
+ if (options.expires) {
113
+ const date = new Date();
114
+ let milliseconds = 0;
115
+
116
+ if (typeof options.expires === 'string') {
117
+ const match = options.expires.match(/^(\d+)([dmw])$/i);
118
+ if (!match) {
119
+ throw new Error('Invalid expires format. Use number (days) or string like "1d", "2w", "3m"');
120
+ }
121
+
122
+ const value = parseInt(match[1]);
123
+ const unit = match[2].toLowerCase();
124
+
125
+ switch (unit) {
126
+ case 'd':
127
+ milliseconds = value * 24 * 60 * 60 * 1000;
128
+ break;
129
+ case 'w':
130
+ milliseconds = value * 7 * 24 * 60 * 60 * 1000;
131
+ break;
132
+ case 'm':
133
+ milliseconds = value * 30 * 24 * 60 * 60 * 1000;
134
+ break;
135
+ }
136
+ } else if (typeof options.expires === 'number') {
137
+ milliseconds = options.expires * 24 * 60 * 60 * 1000;
138
+ } else {
139
+ throw new Error('Expires must be a number (days) or string like "1d", "2w", "3m"');
140
+ }
141
+
142
+ date.setTime(date.getTime() + milliseconds);
143
+ cookieString += `; expires=${date.toUTCString()}`;
144
+ }
145
+
146
+ if (options.path) {
147
+ cookieString += `; path=${options.path}`;
148
+ } else {
149
+ cookieString += '; path=/';
150
+ }
151
+
152
+ if (options.domain) {
153
+ cookieString += `; domain=${options.domain}`;
154
+ }
155
+
156
+ if (options.secure) {
157
+ cookieString += '; secure';
158
+ }
159
+
160
+ if (options.sameSite) {
161
+ cookieString += `; samesite=${options.sameSite}`;
162
+ }
163
+
164
+ document.cookie = cookieString;
165
+ },
166
+
167
+ get: (name) => {
168
+ if (!isBrowser) {
169
+ throw new Error('Cookies can only be accessed in a browser environment');
170
+ }
171
+
172
+ if (!document.cookie) return null;
173
+
174
+ const cookies = document.cookie.split(';');
175
+ for (let cookie of cookies) {
176
+ cookie = cookie.trim();
177
+ if (cookie.indexOf('=') === -1) continue;
178
+
179
+ const [key, ...valueParts] = cookie.split('=');
180
+ const value = valueParts.join('=');
181
+
182
+ if (decodeURIComponent(key) === name) {
183
+ return decodeURIComponent(value);
184
+ }
185
+ }
186
+ return null;
187
+ },
188
+
189
+ remove: (name, options = {}) => {
190
+ if (!isBrowser) {
191
+ throw new Error('Cookies can only be removed in a browser environment');
192
+ }
193
+
194
+ let cookieString = `${encodeURIComponent(name)}=; expires=Thu, 01 Jan 1970 00:00:00 UTC`;
195
+
196
+ if (options.path) {
197
+ cookieString += `; path=${options.path}`;
198
+ } else {
199
+ cookieString += '; path=/';
200
+ }
201
+
202
+ if (options.domain) {
203
+ cookieString += `; domain=${options.domain}`;
204
+ }
205
+
206
+ document.cookie = cookieString;
207
+ },
208
+ };
209
+
210
+ const importKey = async (rawKey) => {
211
+ try {
212
+ const crypto = await initCrypto();
213
+ return await crypto.subtle.importKey(
214
+ 'raw',
215
+ rawKey,
216
+ { name: 'AES-CBC' },
217
+ false,
218
+ ['encrypt', 'decrypt']
219
+ );
220
+ } catch (error) {
221
+ throw new Error(`Failed to import encryption key: ${error.message}`);
222
+ }
223
+ };
224
+
225
+ const CryptoManager = {
226
+ encrypt: async (data, rawKey) => {
227
+ try {
228
+ const crypto = await initCrypto();
229
+ const key = await importKey(rawKey);
230
+ const encoder = new TextEncoder();
231
+ const iv = crypto.getRandomValues(new Uint8Array(16));
232
+ const encrypted = await crypto.subtle.encrypt(
233
+ { name: 'AES-CBC', iv },
234
+ key,
235
+ encoder.encode(data)
236
+ );
237
+ return JSON.stringify({
238
+ iv: Array.from(iv),
239
+ data: Array.from(new Uint8Array(encrypted)),
240
+ });
241
+ } catch (error) {
242
+ throw new Error(`Encryption failed: ${error.message}`);
243
+ }
244
+ },
245
+
246
+ decrypt: async (encryptedData, rawKey) => {
247
+ try {
248
+ const crypto = await initCrypto();
249
+ const key = await importKey(rawKey);
250
+ const { iv, data } = JSON.parse(encryptedData);
251
+
252
+ if (!iv || !data) {
253
+ throw new Error('Invalid encrypted data format');
254
+ }
255
+
256
+ const decrypted = await crypto.subtle.decrypt(
257
+ { name: 'AES-CBC', iv: new Uint8Array(iv) },
258
+ key,
259
+ new Uint8Array(data)
260
+ );
261
+ return new TextDecoder().decode(decrypted);
262
+ } catch (error) {
263
+ throw new Error(`Decryption failed: ${error.message}`);
264
+ }
265
+ },
266
+ };
267
+
268
+ let hashedKey = null;
269
+
270
+ const hashKey = async (key) => {
271
+ const crypto = await initCrypto();
272
+ const encoder = new TextEncoder();
273
+ const data = encoder.encode(key);
274
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
275
+ return new Uint8Array(hashBuffer);
276
+ };
277
+
278
+ export const jstudio = {
279
+ setKey: async (key) => {
280
+ if (typeof key !== 'string') {
281
+ throw new Error('Secret key must be a string.');
282
+ }
283
+ if (key.length !== 64) {
284
+ throw new Error('Secret key must be exactly 64 characters long.');
285
+ }
286
+ try {
287
+ hashedKey = await hashKey(key);
288
+ } catch (error) {
289
+ throw new Error(`Failed to set secret key: ${error.message}`);
290
+ }
291
+ },
292
+ };
293
+
294
+ export const SetJSCookie = async (name, data, options = {}) => {
295
+ if (!hashedKey) {
296
+ throw new Error('Secret key is not set. Use jstudio.setKey() to set it.');
297
+ }
298
+ if (typeof name !== 'string' || name.trim() === '') {
299
+ throw new Error('Cookie name must be a non-empty string.');
300
+ }
301
+ if (data === undefined || data === null) {
302
+ throw new Error('Cookie data cannot be undefined or null.');
303
+ }
304
+
305
+ try {
306
+ const encryptedData = await CryptoManager.encrypt(JSON.stringify(data), hashedKey);
307
+ CookieManager.set(name, encryptedData, options);
308
+ } catch (error) {
309
+ throw new Error(`Failed to set cookie: ${error.message}`);
310
+ }
311
+ };
312
+
313
+ export const GetJSCookie = async (name) => {
314
+ if (!hashedKey) {
315
+ throw new Error('Secret key is not set. Use jstudio.setKey() to set it.');
316
+ }
317
+ if (typeof name !== 'string' || name.trim() === '') {
318
+ throw new Error('Cookie name must be a non-empty string.');
319
+ }
320
+
321
+ try {
322
+ const encryptedData = CookieManager.get(name);
323
+ if (!encryptedData) return null;
324
+
325
+ const decryptedData = await CryptoManager.decrypt(encryptedData, hashedKey);
326
+ return JSON.parse(decryptedData);
327
+ } catch (error) {
328
+ try {
329
+ CookieManager.remove(name);
330
+ } catch (removeError) {
331
+ }
332
+ return null;
333
+ }
334
+ };
335
+
336
+ export const RemoveJSCookie = (name, options = {}) => {
337
+ if (typeof name !== 'string' || name.trim() === '') {
338
+ throw new Error('Cookie name must be a non-empty string.');
339
+ }
340
+
341
+ try {
342
+ CookieManager.remove(name, options);
343
+ } catch (error) {
344
+ throw new Error(`Failed to remove cookie: ${error.message}`);
345
+ }
346
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "joshlei-cookies",
3
+ "version": "1.0.0",
4
+ "description": "A secure cookie management library with built-in AES encryption for browser environments",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "keywords": [
8
+ "cookies",
9
+ "encryption",
10
+ "security",
11
+ "aes",
12
+ "browser",
13
+ "jstudio",
14
+ "cookie-management"
15
+ ],
16
+ "author": "JStudio",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://joshlei.com"
21
+ },
22
+ "bugs": {
23
+ "url": "https://joshlei.com"
24
+ },
25
+ "homepage": "https://joshlei.com",
26
+ "engines": {
27
+ "node": ">=16.0.0"
28
+ },
29
+ "files": [
30
+ "index.js"
31
+ ],
32
+ "exports": {
33
+ ".": {
34
+ "import": "./index.js",
35
+ "types": "./index.d.ts"
36
+ }
37
+ }
38
+ }