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/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> = {minutes: 5};
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 local storage.
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 global `localStorage`.
150
+ * Allows mocking or overriding the default CSRF token store.
148
151
  *
149
- * @default globalThis.localStorage
152
+ * @default getDefaultCsrfTokenStore()
150
153
  */
151
- localStorage: Pick<Storage, 'setItem' | 'removeItem'>;
154
+ csrfTokenStore: CsrfTokenStore;
152
155
  }>,
153
- ) {
154
- (options.localStorage || globalThis.localStorage).setItem(
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 global `localStorage`.
233
+ * Allows mocking or overriding the default CSRF token store.
232
234
  *
233
- * @default globalThis.localStorage
235
+ * @default getDefaultCsrfTokenStore()
234
236
  */
235
- localStorage: Pick<Storage, 'getItem'>;
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.localStorage || globalThis.localStorage).getItem(resolveCsrfHeaderName(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 logout a
253
- * user or react to a session timeout.
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 global `localStorage`.
263
+ * Allows mocking or overriding the default CSRF token store.
262
264
  *
263
- * @default globalThis.localStorage
265
+ * @default getDefaultCsrfTokenStore()
264
266
  */
265
- localStorage: Pick<Storage, 'removeItem'>;
267
+ csrfTokenStore: CsrfTokenStore;
266
268
  }>,
267
- ) {
268
- return (options.localStorage || globalThis.localStorage).removeItem(
269
- resolveCsrfHeaderName(options),
270
- );
269
+ ): Promise<void> {
270
+ await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).deleteCsrfToken();
271
271
  }
@@ -27,8 +27,8 @@ const config: runtime.GetPrismaClientConfig = {
27
27
  "fromEnvVar": null
28
28
  },
29
29
  "config": {
30
- "engineType": "client",
31
- "moduleFormat": "esm"
30
+ "moduleFormat": "esm",
31
+ "engineType": "client"
32
32
  },
33
33
  "binaryTargets": [
34
34
  {
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-local-storage.js';
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 = {alg: 'dir', enc: 'A256GCM'};
20
- const signingProtectedHeader = {alg: 'HS512'};
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({data})
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({jwt: signedJwt})
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
- {seconds: true},
194
+ {
195
+ seconds: true,
196
+ },
186
197
  ).seconds;
187
198
 
188
199
  const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
@@ -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, {alsoUndefined: true}),
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
- }