auth-vir 3.0.1 → 3.1.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/README.md +7 -8
- package/dist/auth-client/backend-auth.client.d.ts +23 -0
- package/dist/auth-client/backend-auth.client.js +110 -17
- package/dist/auth-client/frontend-auth.client.d.ts +4 -2
- package/dist/auth-client/frontend-auth.client.js +14 -22
- package/dist/auth.d.ts +8 -7
- package/dist/auth.js +6 -10
- package/dist/cookie.js +3 -1
- package/dist/csrf-token-store.d.ts +21 -0
- package/dist/csrf-token-store.js +35 -0
- package/dist/csrf-token.d.ts +16 -15
- package/dist/csrf-token.js +13 -10
- package/dist/generated/internal/class.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/jwt/jwt.js +16 -5
- package/dist/jwt/user-jwt.js +3 -1
- package/dist/mock-csrf-token-store.d.ts +64 -0
- package/dist/mock-csrf-token-store.js +107 -0
- package/package.json +8 -7
- package/src/auth-client/backend-auth.client.ts +169 -26
- package/src/auth-client/frontend-auth.client.ts +15 -25
- package/src/auth.ts +9 -15
- package/src/cookie.ts +3 -1
- package/src/csrf-token-store.ts +54 -0
- package/src/csrf-token.ts +25 -25
- package/src/generated/internal/class.ts +2 -2
- package/src/index.ts +2 -1
- package/src/jwt/jwt.ts +16 -5
- package/src/jwt/user-jwt.ts +3 -1
- package/src/mock-csrf-token-store.ts +141 -0
- package/dist/mock-local-storage.d.ts +0 -33
- package/dist/mock-local-storage.js +0 -56
- package/src/mock-local-storage.ts +0 -72
package/src/csrf-token.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from 'date-vir';
|
|
14
14
|
import {defineShape, parseJsonWithShape} from 'object-shape-tester';
|
|
15
15
|
import {type RequireExactlyOne} from 'type-fest';
|
|
16
|
+
import {getDefaultCsrfTokenStore, type CsrfTokenStore} from './csrf-token-store.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Shape definition for {@link CsrfToken}.
|
|
@@ -38,7 +39,9 @@ export type CsrfToken = typeof csrfTokenShape.runtimeType;
|
|
|
38
39
|
* @category Internal
|
|
39
40
|
* @default {minutes: 5}
|
|
40
41
|
*/
|
|
41
|
-
export const defaultAllowedClockSkew: Readonly<AnyDuration> = {
|
|
42
|
+
export const defaultAllowedClockSkew: Readonly<AnyDuration> = {
|
|
43
|
+
minutes: 5,
|
|
44
|
+
};
|
|
42
45
|
|
|
43
46
|
/**
|
|
44
47
|
* Generates a random, cryptographically secure CSRF token.
|
|
@@ -135,24 +138,23 @@ export function extractCsrfTokenHeader(
|
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
/**
|
|
138
|
-
* Stores the given CSRF token into
|
|
141
|
+
* Stores the given CSRF token into IndexedDB.
|
|
139
142
|
*
|
|
140
143
|
* @category Auth : Client
|
|
141
144
|
*/
|
|
142
|
-
export function storeCsrfToken(
|
|
145
|
+
export async function storeCsrfToken(
|
|
143
146
|
csrfToken: Readonly<CsrfToken>,
|
|
144
147
|
options: Readonly<CsrfHeaderNameOption> &
|
|
145
148
|
PartialWithUndefined<{
|
|
146
149
|
/**
|
|
147
|
-
* Allows mocking or overriding the
|
|
150
|
+
* Allows mocking or overriding the default CSRF token store.
|
|
148
151
|
*
|
|
149
|
-
* @default
|
|
152
|
+
* @default getDefaultCsrfTokenStore()
|
|
150
153
|
*/
|
|
151
|
-
|
|
154
|
+
csrfTokenStore: CsrfTokenStore;
|
|
152
155
|
}>,
|
|
153
|
-
) {
|
|
154
|
-
(options.
|
|
155
|
-
resolveCsrfHeaderName(options),
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).setCsrfToken(
|
|
156
158
|
JSON.stringify(csrfToken),
|
|
157
159
|
);
|
|
158
160
|
}
|
|
@@ -224,15 +226,15 @@ export function parseCsrfToken(
|
|
|
224
226
|
*
|
|
225
227
|
* @category Auth : Client
|
|
226
228
|
*/
|
|
227
|
-
export function getCurrentCsrfToken(
|
|
229
|
+
export async function getCurrentCsrfToken(
|
|
228
230
|
options: Readonly<CsrfHeaderNameOption> &
|
|
229
231
|
PartialWithUndefined<{
|
|
230
232
|
/**
|
|
231
|
-
* Allows mocking or overriding the
|
|
233
|
+
* Allows mocking or overriding the default CSRF token store.
|
|
232
234
|
*
|
|
233
|
-
* @default
|
|
235
|
+
* @default getDefaultCsrfTokenStore()
|
|
234
236
|
*/
|
|
235
|
-
|
|
237
|
+
csrfTokenStore: CsrfTokenStore;
|
|
236
238
|
/**
|
|
237
239
|
* Allowed clock skew tolerance for CSRF token expiration checks.
|
|
238
240
|
*
|
|
@@ -240,32 +242,30 @@ export function getCurrentCsrfToken(
|
|
|
240
242
|
*/
|
|
241
243
|
allowedClockSkew: Readonly<AnyDuration>;
|
|
242
244
|
}>,
|
|
243
|
-
): Readonly<GetCsrfTokenResult
|
|
245
|
+
): Promise<Readonly<GetCsrfTokenResult>> {
|
|
244
246
|
const rawCsrfToken: string | undefined =
|
|
245
|
-
(options.
|
|
247
|
+
(await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).getCsrfToken()) ||
|
|
246
248
|
undefined;
|
|
247
249
|
|
|
248
250
|
return parseCsrfToken(rawCsrfToken, options);
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
/**
|
|
252
|
-
* Wipes the current stored CSRF token. This should be used by client (frontend) code to
|
|
253
|
-
*
|
|
254
|
+
* Wipes the current stored CSRF token. This should be used by client (frontend) code to react to a
|
|
255
|
+
* session timeout.
|
|
254
256
|
*
|
|
255
257
|
* @category Auth : Client
|
|
256
258
|
*/
|
|
257
|
-
export function wipeCurrentCsrfToken(
|
|
259
|
+
export async function wipeCurrentCsrfToken(
|
|
258
260
|
options: Readonly<CsrfHeaderNameOption> &
|
|
259
261
|
PartialWithUndefined<{
|
|
260
262
|
/**
|
|
261
|
-
* Allows mocking or overriding the
|
|
263
|
+
* Allows mocking or overriding the default CSRF token store.
|
|
262
264
|
*
|
|
263
|
-
* @default
|
|
265
|
+
* @default getDefaultCsrfTokenStore()
|
|
264
266
|
*/
|
|
265
|
-
|
|
267
|
+
csrfTokenStore: CsrfTokenStore;
|
|
266
268
|
}>,
|
|
267
|
-
) {
|
|
268
|
-
|
|
269
|
-
resolveCsrfHeaderName(options),
|
|
270
|
-
);
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).deleteCsrfToken();
|
|
271
271
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,10 +3,11 @@ export * from './auth-client/frontend-auth.client.js';
|
|
|
3
3
|
export * from './auth-client/is-session-refresh-ready.js';
|
|
4
4
|
export * from './auth.js';
|
|
5
5
|
export * from './cookie.js';
|
|
6
|
+
export * from './csrf-token-store.js';
|
|
6
7
|
export * from './csrf-token.js';
|
|
7
8
|
export * from './hash.js';
|
|
8
9
|
export * from './headers.js';
|
|
9
10
|
export * from './jwt/jwt-keys.js';
|
|
10
11
|
export * from './jwt/jwt.js';
|
|
11
12
|
export * from './jwt/user-jwt.js';
|
|
12
|
-
export * from './mock-
|
|
13
|
+
export * from './mock-csrf-token-store.js';
|
package/src/jwt/jwt.ts
CHANGED
|
@@ -16,8 +16,13 @@ import {EncryptJWT, jwtDecrypt, jwtVerify, SignJWT} from 'jose';
|
|
|
16
16
|
import {defaultAllowedClockSkew} from '../csrf-token.js';
|
|
17
17
|
import {type JwtKeys} from './jwt-keys.js';
|
|
18
18
|
|
|
19
|
-
const encryptionProtectedHeader = {
|
|
20
|
-
|
|
19
|
+
const encryptionProtectedHeader = {
|
|
20
|
+
alg: 'dir',
|
|
21
|
+
enc: 'A256GCM',
|
|
22
|
+
};
|
|
23
|
+
const signingProtectedHeader = {
|
|
24
|
+
alg: 'HS512',
|
|
25
|
+
};
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Params for {@link createJwt}.
|
|
@@ -110,7 +115,9 @@ export async function createJwt<JwtData extends AnyObject = AnyObject>(
|
|
|
110
115
|
data: JwtData,
|
|
111
116
|
params: Readonly<CreateJwtParams>,
|
|
112
117
|
): Promise<string> {
|
|
113
|
-
const rawJwt = new SignJWT({
|
|
118
|
+
const rawJwt = new SignJWT({
|
|
119
|
+
data,
|
|
120
|
+
})
|
|
114
121
|
.setProtectedHeader(signingProtectedHeader)
|
|
115
122
|
.setIssuedAt(
|
|
116
123
|
params.issuedAt
|
|
@@ -129,7 +136,9 @@ export async function createJwt<JwtData extends AnyObject = AnyObject>(
|
|
|
129
136
|
|
|
130
137
|
const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
|
|
131
138
|
|
|
132
|
-
return await new EncryptJWT({
|
|
139
|
+
return await new EncryptJWT({
|
|
140
|
+
jwt: signedJwt,
|
|
141
|
+
})
|
|
133
142
|
.setProtectedHeader(encryptionProtectedHeader)
|
|
134
143
|
.encrypt(params.jwtKeys.encryptionKey);
|
|
135
144
|
}
|
|
@@ -182,7 +191,9 @@ export async function parseJwt<JwtData extends AnyObject = AnyObject>(
|
|
|
182
191
|
|
|
183
192
|
const clockToleranceSeconds = convertDuration(
|
|
184
193
|
params.allowedClockSkew || defaultAllowedClockSkew,
|
|
185
|
-
{
|
|
194
|
+
{
|
|
195
|
+
seconds: true,
|
|
196
|
+
},
|
|
186
197
|
).seconds;
|
|
187
198
|
|
|
188
199
|
const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
|
package/src/jwt/user-jwt.ts
CHANGED
|
@@ -27,7 +27,9 @@ export const userJwtDataShape = defineShape({
|
|
|
27
27
|
* enforce the max session duration. If not present, the session is considered to have started
|
|
28
28
|
* when the JWT was issued.
|
|
29
29
|
*/
|
|
30
|
-
sessionStartedAt: optionalShape(0, {
|
|
30
|
+
sessionStartedAt: optionalShape(0, {
|
|
31
|
+
alsoUndefined: true,
|
|
32
|
+
}),
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {type CsrfTokenStore} from './csrf-token-store.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `accessRecord` type for {@link createMockLocalStorage}'s output.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export type MockLocalStorageAccessRecord = {
|
|
9
|
+
getItem: string[];
|
|
10
|
+
removeItem: string[];
|
|
11
|
+
setItem: {key: string; value: string}[];
|
|
12
|
+
key: number[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
17
|
+
* {@link createMockLocalStorage}.
|
|
18
|
+
*
|
|
19
|
+
* @category Mock
|
|
20
|
+
*/
|
|
21
|
+
export function createEmptyMockLocalStorageAccessRecord(): MockLocalStorageAccessRecord {
|
|
22
|
+
return {
|
|
23
|
+
getItem: [],
|
|
24
|
+
removeItem: [],
|
|
25
|
+
setItem: [],
|
|
26
|
+
key: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a LocalStorage mock.
|
|
32
|
+
*
|
|
33
|
+
* @category Mock
|
|
34
|
+
*/
|
|
35
|
+
export function createMockLocalStorage(
|
|
36
|
+
/** Set values in here to initialize the mocked localStorage data store contents. */
|
|
37
|
+
init: Record<string, string> = {},
|
|
38
|
+
) {
|
|
39
|
+
const store: Record<string, string> = init;
|
|
40
|
+
const accessRecord = createEmptyMockLocalStorageAccessRecord();
|
|
41
|
+
|
|
42
|
+
const mockLocalStorage: Storage = {
|
|
43
|
+
clear() {
|
|
44
|
+
Object.keys(store).forEach((key) => {
|
|
45
|
+
delete store[key];
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
getItem(key) {
|
|
49
|
+
accessRecord.getItem.push(key);
|
|
50
|
+
return store[key] ?? null;
|
|
51
|
+
},
|
|
52
|
+
get length() {
|
|
53
|
+
return Object.keys(store).length;
|
|
54
|
+
},
|
|
55
|
+
key(index) {
|
|
56
|
+
accessRecord.key.push(index);
|
|
57
|
+
return Object.keys(store)[index] ?? null;
|
|
58
|
+
},
|
|
59
|
+
removeItem(key) {
|
|
60
|
+
accessRecord.removeItem.push(key);
|
|
61
|
+
delete store[key];
|
|
62
|
+
},
|
|
63
|
+
setItem(key, value) {
|
|
64
|
+
accessRecord.setItem.push({
|
|
65
|
+
key,
|
|
66
|
+
value,
|
|
67
|
+
});
|
|
68
|
+
store[key] = value;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
localStorage: mockLocalStorage,
|
|
74
|
+
store,
|
|
75
|
+
accessRecord,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `accessRecord` type for {@link createMockCsrfTokenStore}'s output.
|
|
81
|
+
*
|
|
82
|
+
* @category Internal
|
|
83
|
+
*/
|
|
84
|
+
export type MockCsrfTokenStoreAccessRecord = {
|
|
85
|
+
getCsrfToken: number;
|
|
86
|
+
setCsrfToken: string[];
|
|
87
|
+
deleteCsrfToken: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
92
|
+
* {@link createMockCsrfTokenStore}.
|
|
93
|
+
*
|
|
94
|
+
* @category Mock
|
|
95
|
+
*/
|
|
96
|
+
export function createEmptyMockCsrfTokenStoreAccessRecord(): MockCsrfTokenStoreAccessRecord {
|
|
97
|
+
return {
|
|
98
|
+
getCsrfToken: 0,
|
|
99
|
+
setCsrfToken: [],
|
|
100
|
+
deleteCsrfToken: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a mock {@link CsrfTokenStore} backed by a simple in-memory object, for use in tests.
|
|
106
|
+
*
|
|
107
|
+
* @category Mock
|
|
108
|
+
*/
|
|
109
|
+
export function createMockCsrfTokenStore(
|
|
110
|
+
/** Set an initial value to initialize the mocked store contents. */
|
|
111
|
+
init?: string | undefined,
|
|
112
|
+
) {
|
|
113
|
+
let storedValue: string | undefined = init;
|
|
114
|
+
const accessRecord = createEmptyMockCsrfTokenStoreAccessRecord();
|
|
115
|
+
|
|
116
|
+
const csrfTokenStore: CsrfTokenStore = {
|
|
117
|
+
getCsrfToken() {
|
|
118
|
+
accessRecord.getCsrfToken++;
|
|
119
|
+
return Promise.resolve(storedValue);
|
|
120
|
+
},
|
|
121
|
+
setCsrfToken(value: string) {
|
|
122
|
+
accessRecord.setCsrfToken.push(value);
|
|
123
|
+
storedValue = value;
|
|
124
|
+
return Promise.resolve();
|
|
125
|
+
},
|
|
126
|
+
deleteCsrfToken() {
|
|
127
|
+
accessRecord.deleteCsrfToken++;
|
|
128
|
+
storedValue = undefined;
|
|
129
|
+
return Promise.resolve();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
csrfTokenStore,
|
|
135
|
+
/** The current value held in the mock store. */
|
|
136
|
+
get storedValue() {
|
|
137
|
+
return storedValue;
|
|
138
|
+
},
|
|
139
|
+
accessRecord,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `accessRecord` type for {@link createMockLocalStorage}'s output.
|
|
3
|
-
*
|
|
4
|
-
* @category Internal
|
|
5
|
-
*/
|
|
6
|
-
export type MockLocalStorageAccessRecord = {
|
|
7
|
-
getItem: string[];
|
|
8
|
-
removeItem: string[];
|
|
9
|
-
setItem: {
|
|
10
|
-
key: string;
|
|
11
|
-
value: string;
|
|
12
|
-
}[];
|
|
13
|
-
key: number[];
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
17
|
-
* {@link createMockLocalStorage}.
|
|
18
|
-
*
|
|
19
|
-
* @category Mock
|
|
20
|
-
*/
|
|
21
|
-
export declare function createEmptyMockLocalStorageAccessRecord(): MockLocalStorageAccessRecord;
|
|
22
|
-
/**
|
|
23
|
-
* Create a LocalStorage mock.
|
|
24
|
-
*
|
|
25
|
-
* @category Mock
|
|
26
|
-
*/
|
|
27
|
-
export declare function createMockLocalStorage(
|
|
28
|
-
/** Set values in here to initialize the mocked localStorage data store contents. */
|
|
29
|
-
init?: Record<string, string>): {
|
|
30
|
-
localStorage: Storage;
|
|
31
|
-
store: Record<string, string>;
|
|
32
|
-
accessRecord: MockLocalStorageAccessRecord;
|
|
33
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
3
|
-
* {@link createMockLocalStorage}.
|
|
4
|
-
*
|
|
5
|
-
* @category Mock
|
|
6
|
-
*/
|
|
7
|
-
export function createEmptyMockLocalStorageAccessRecord() {
|
|
8
|
-
return {
|
|
9
|
-
getItem: [],
|
|
10
|
-
removeItem: [],
|
|
11
|
-
setItem: [],
|
|
12
|
-
key: [],
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Create a LocalStorage mock.
|
|
17
|
-
*
|
|
18
|
-
* @category Mock
|
|
19
|
-
*/
|
|
20
|
-
export function createMockLocalStorage(
|
|
21
|
-
/** Set values in here to initialize the mocked localStorage data store contents. */
|
|
22
|
-
init = {}) {
|
|
23
|
-
const store = init;
|
|
24
|
-
const accessRecord = createEmptyMockLocalStorageAccessRecord();
|
|
25
|
-
const mockLocalStorage = {
|
|
26
|
-
clear() {
|
|
27
|
-
Object.keys(store).forEach((key) => {
|
|
28
|
-
delete store[key];
|
|
29
|
-
});
|
|
30
|
-
},
|
|
31
|
-
getItem(key) {
|
|
32
|
-
accessRecord.getItem.push(key);
|
|
33
|
-
return store[key] ?? null;
|
|
34
|
-
},
|
|
35
|
-
get length() {
|
|
36
|
-
return Object.keys(store).length;
|
|
37
|
-
},
|
|
38
|
-
key(index) {
|
|
39
|
-
accessRecord.key.push(index);
|
|
40
|
-
return Object.keys(store)[index] ?? null;
|
|
41
|
-
},
|
|
42
|
-
removeItem(key) {
|
|
43
|
-
accessRecord.removeItem.push(key);
|
|
44
|
-
delete store[key];
|
|
45
|
-
},
|
|
46
|
-
setItem(key, value) {
|
|
47
|
-
accessRecord.setItem.push({ key, value });
|
|
48
|
-
store[key] = value;
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
localStorage: mockLocalStorage,
|
|
53
|
-
store,
|
|
54
|
-
accessRecord,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `accessRecord` type for {@link createMockLocalStorage}'s output.
|
|
3
|
-
*
|
|
4
|
-
* @category Internal
|
|
5
|
-
*/
|
|
6
|
-
export type MockLocalStorageAccessRecord = {
|
|
7
|
-
getItem: string[];
|
|
8
|
-
removeItem: string[];
|
|
9
|
-
setItem: {key: string; value: string}[];
|
|
10
|
-
key: number[];
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
15
|
-
* {@link createMockLocalStorage}.
|
|
16
|
-
*
|
|
17
|
-
* @category Mock
|
|
18
|
-
*/
|
|
19
|
-
export function createEmptyMockLocalStorageAccessRecord(): MockLocalStorageAccessRecord {
|
|
20
|
-
return {
|
|
21
|
-
getItem: [],
|
|
22
|
-
removeItem: [],
|
|
23
|
-
setItem: [],
|
|
24
|
-
key: [],
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Create a LocalStorage mock.
|
|
30
|
-
*
|
|
31
|
-
* @category Mock
|
|
32
|
-
*/
|
|
33
|
-
export function createMockLocalStorage(
|
|
34
|
-
/** Set values in here to initialize the mocked localStorage data store contents. */
|
|
35
|
-
init: Record<string, string> = {},
|
|
36
|
-
) {
|
|
37
|
-
const store: Record<string, string> = init;
|
|
38
|
-
const accessRecord = createEmptyMockLocalStorageAccessRecord();
|
|
39
|
-
|
|
40
|
-
const mockLocalStorage: Storage = {
|
|
41
|
-
clear() {
|
|
42
|
-
Object.keys(store).forEach((key) => {
|
|
43
|
-
delete store[key];
|
|
44
|
-
});
|
|
45
|
-
},
|
|
46
|
-
getItem(key) {
|
|
47
|
-
accessRecord.getItem.push(key);
|
|
48
|
-
return store[key] ?? null;
|
|
49
|
-
},
|
|
50
|
-
get length() {
|
|
51
|
-
return Object.keys(store).length;
|
|
52
|
-
},
|
|
53
|
-
key(index) {
|
|
54
|
-
accessRecord.key.push(index);
|
|
55
|
-
return Object.keys(store)[index] ?? null;
|
|
56
|
-
},
|
|
57
|
-
removeItem(key) {
|
|
58
|
-
accessRecord.removeItem.push(key);
|
|
59
|
-
delete store[key];
|
|
60
|
-
},
|
|
61
|
-
setItem(key, value) {
|
|
62
|
-
accessRecord.setItem.push({key, value});
|
|
63
|
-
store[key] = value;
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
localStorage: mockLocalStorage,
|
|
69
|
-
store,
|
|
70
|
-
accessRecord,
|
|
71
|
-
};
|
|
72
|
-
}
|