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.
@@ -1,6 +1,7 @@
1
1
  import { type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
2
  import { type AnyDuration } from 'date-vir';
3
3
  import { type RequireExactlyOne } from 'type-fest';
4
+ import { type CsrfTokenStore } from './csrf-token-store.js';
4
5
  /**
5
6
  * Shape definition for {@link CsrfToken}.
6
7
  *
@@ -100,18 +101,18 @@ export declare function extractCsrfTokenHeader(response: Readonly<PartialWithUnd
100
101
  allowedClockSkew: Readonly<AnyDuration>;
101
102
  }>): Readonly<GetCsrfTokenResult>;
102
103
  /**
103
- * Stores the given CSRF token into local storage.
104
+ * Stores the given CSRF token into IndexedDB.
104
105
  *
105
106
  * @category Auth : Client
106
107
  */
107
108
  export declare function storeCsrfToken(csrfToken: Readonly<CsrfToken>, options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
108
109
  /**
109
- * Allows mocking or overriding the global `localStorage`.
110
+ * Allows mocking or overriding the default CSRF token store.
110
111
  *
111
- * @default globalThis.localStorage
112
+ * @default getDefaultCsrfTokenStore()
112
113
  */
113
- localStorage: Pick<Storage, 'setItem' | 'removeItem'>;
114
- }>): void;
114
+ csrfTokenStore: CsrfTokenStore;
115
+ }>): Promise<void>;
115
116
  /**
116
117
  * Parse a raw CSRF token JSON string.
117
118
  *
@@ -134,29 +135,29 @@ export declare function parseCsrfToken(value: string | undefined | null, options
134
135
  */
135
136
  export declare function getCurrentCsrfToken(options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
136
137
  /**
137
- * Allows mocking or overriding the global `localStorage`.
138
+ * Allows mocking or overriding the default CSRF token store.
138
139
  *
139
- * @default globalThis.localStorage
140
+ * @default getDefaultCsrfTokenStore()
140
141
  */
141
- localStorage: Pick<Storage, 'getItem'>;
142
+ csrfTokenStore: CsrfTokenStore;
142
143
  /**
143
144
  * Allowed clock skew tolerance for CSRF token expiration checks.
144
145
  *
145
146
  * @default {minutes: 5}
146
147
  */
147
148
  allowedClockSkew: Readonly<AnyDuration>;
148
- }>): Readonly<GetCsrfTokenResult>;
149
+ }>): Promise<Readonly<GetCsrfTokenResult>>;
149
150
  /**
150
- * Wipes the current stored CSRF token. This should be used by client (frontend) code to logout a
151
- * user or react to a session timeout.
151
+ * Wipes the current stored CSRF token. This should be used by client (frontend) code to react to a
152
+ * session timeout.
152
153
  *
153
154
  * @category Auth : Client
154
155
  */
155
156
  export declare function wipeCurrentCsrfToken(options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
156
157
  /**
157
- * Allows mocking or overriding the global `localStorage`.
158
+ * Allows mocking or overriding the default CSRF token store.
158
159
  *
159
- * @default globalThis.localStorage
160
+ * @default getDefaultCsrfTokenStore()
160
161
  */
161
- localStorage: Pick<Storage, 'removeItem'>;
162
- }>): void;
162
+ csrfTokenStore: CsrfTokenStore;
163
+ }>): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { randomString, wrapInTry, } from '@augment-vir/common';
2
2
  import { calculateRelativeDate, fullDateShape, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
3
3
  import { defineShape, parseJsonWithShape } from 'object-shape-tester';
4
+ import { getDefaultCsrfTokenStore } from './csrf-token-store.js';
4
5
  /**
5
6
  * Shape definition for {@link CsrfToken}.
6
7
  *
@@ -17,7 +18,9 @@ export const csrfTokenShape = defineShape({
17
18
  * @category Internal
18
19
  * @default {minutes: 5}
19
20
  */
20
- export const defaultAllowedClockSkew = { minutes: 5 };
21
+ export const defaultAllowedClockSkew = {
22
+ minutes: 5,
23
+ };
21
24
  /**
22
25
  * Generates a random, cryptographically secure CSRF token.
23
26
  *
@@ -74,12 +77,12 @@ export function extractCsrfTokenHeader(response, csrfHeaderNameOption, options)
74
77
  return parseCsrfToken(rawCsrfToken, options);
75
78
  }
76
79
  /**
77
- * Stores the given CSRF token into local storage.
80
+ * Stores the given CSRF token into IndexedDB.
78
81
  *
79
82
  * @category Auth : Client
80
83
  */
81
- export function storeCsrfToken(csrfToken, options) {
82
- (options.localStorage || globalThis.localStorage).setItem(resolveCsrfHeaderName(options), JSON.stringify(csrfToken));
84
+ export async function storeCsrfToken(csrfToken, options) {
85
+ await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).setCsrfToken(JSON.stringify(csrfToken));
83
86
  }
84
87
  /**
85
88
  * Parse a raw CSRF token JSON string.
@@ -122,17 +125,17 @@ export function parseCsrfToken(value, options) {
122
125
  *
123
126
  * @category Auth : Client
124
127
  */
125
- export function getCurrentCsrfToken(options) {
126
- const rawCsrfToken = (options.localStorage || globalThis.localStorage).getItem(resolveCsrfHeaderName(options)) ||
128
+ export async function getCurrentCsrfToken(options) {
129
+ const rawCsrfToken = (await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).getCsrfToken()) ||
127
130
  undefined;
128
131
  return parseCsrfToken(rawCsrfToken, options);
129
132
  }
130
133
  /**
131
- * Wipes the current stored CSRF token. This should be used by client (frontend) code to logout a
132
- * user or react to a session timeout.
134
+ * Wipes the current stored CSRF token. This should be used by client (frontend) code to react to a
135
+ * session timeout.
133
136
  *
134
137
  * @category Auth : Client
135
138
  */
136
- export function wipeCurrentCsrfToken(options) {
137
- return (options.localStorage || globalThis.localStorage).removeItem(resolveCsrfHeaderName(options));
139
+ export async function wipeCurrentCsrfToken(options) {
140
+ await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).deleteCsrfToken();
138
141
  }
@@ -22,8 +22,8 @@ const config = {
22
22
  "fromEnvVar": null
23
23
  },
24
24
  "config": {
25
- "engineType": "client",
26
- "moduleFormat": "esm"
25
+ "moduleFormat": "esm",
26
+ "engineType": "client"
27
27
  },
28
28
  "binaryTargets": [
29
29
  {
package/dist/index.d.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/dist/index.js 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/dist/jwt/jwt.js CHANGED
@@ -2,8 +2,13 @@ import { assertWrap, check } from '@augment-vir/assert';
2
2
  import { calculateRelativeDate, convertDuration, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
3
3
  import { EncryptJWT, jwtDecrypt, jwtVerify, SignJWT } from 'jose';
4
4
  import { defaultAllowedClockSkew } from '../csrf-token.js';
5
- const encryptionProtectedHeader = { alg: 'dir', enc: 'A256GCM' };
6
- const signingProtectedHeader = { alg: 'HS512' };
5
+ const encryptionProtectedHeader = {
6
+ alg: 'dir',
7
+ enc: 'A256GCM',
8
+ };
9
+ const signingProtectedHeader = {
10
+ alg: 'HS512',
11
+ };
7
12
  /**
8
13
  * JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
9
14
  *
@@ -28,7 +33,9 @@ export function parseJwtTimestamp(seconds) {
28
33
  export async function createJwt(
29
34
  /** The data to be included in the JWT. */
30
35
  data, params) {
31
- const rawJwt = new SignJWT({ data })
36
+ const rawJwt = new SignJWT({
37
+ data,
38
+ })
32
39
  .setProtectedHeader(signingProtectedHeader)
33
40
  .setIssuedAt(params.issuedAt
34
41
  ? toJwtTimestamp(createFullDateInUserTimezone(params.issuedAt))
@@ -40,7 +47,9 @@ data, params) {
40
47
  rawJwt.setNotBefore(toJwtTimestamp(createFullDateInUserTimezone(params.notValidUntil)));
41
48
  }
42
49
  const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
43
- return await new EncryptJWT({ jwt: signedJwt })
50
+ return await new EncryptJWT({
51
+ jwt: signedJwt,
52
+ })
44
53
  .setProtectedHeader(encryptionProtectedHeader)
45
54
  .encrypt(params.jwtKeys.encryptionKey);
46
55
  }
@@ -58,7 +67,9 @@ export async function parseJwt(encryptedJwt, params) {
58
67
  else if (!check.isString(decryptedJwt.payload.jwt)) {
59
68
  throw new TypeError('Decrypted jwt is not a string.');
60
69
  }
61
- const clockToleranceSeconds = convertDuration(params.allowedClockSkew || defaultAllowedClockSkew, { seconds: true }).seconds;
70
+ const clockToleranceSeconds = convertDuration(params.allowedClockSkew || defaultAllowedClockSkew, {
71
+ seconds: true,
72
+ }).seconds;
62
73
  const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
63
74
  issuer: params.issuer,
64
75
  audience: params.audience,
@@ -19,7 +19,9 @@ export const userJwtDataShape = defineShape({
19
19
  * enforce the max session duration. If not present, the session is considered to have started
20
20
  * when the JWT was issued.
21
21
  */
22
- sessionStartedAt: optionalShape(0, { alsoUndefined: true }),
22
+ sessionStartedAt: optionalShape(0, {
23
+ alsoUndefined: true,
24
+ }),
23
25
  });
24
26
  /**
25
27
  * Creates a new signed and encrypted {@link JwtUserData} when a client (frontend) successfully
@@ -0,0 +1,64 @@
1
+ import { type CsrfTokenStore } from './csrf-token-store.js';
2
+ /**
3
+ * `accessRecord` type for {@link createMockLocalStorage}'s output.
4
+ *
5
+ * @category Internal
6
+ */
7
+ export type MockLocalStorageAccessRecord = {
8
+ getItem: string[];
9
+ removeItem: string[];
10
+ setItem: {
11
+ key: string;
12
+ value: string;
13
+ }[];
14
+ key: number[];
15
+ };
16
+ /**
17
+ * Create an empty `accessRecord` object, this is to be used in conjunction with
18
+ * {@link createMockLocalStorage}.
19
+ *
20
+ * @category Mock
21
+ */
22
+ export declare function createEmptyMockLocalStorageAccessRecord(): MockLocalStorageAccessRecord;
23
+ /**
24
+ * Create a LocalStorage mock.
25
+ *
26
+ * @category Mock
27
+ */
28
+ export declare function createMockLocalStorage(
29
+ /** Set values in here to initialize the mocked localStorage data store contents. */
30
+ init?: Record<string, string>): {
31
+ localStorage: Storage;
32
+ store: Record<string, string>;
33
+ accessRecord: MockLocalStorageAccessRecord;
34
+ };
35
+ /**
36
+ * `accessRecord` type for {@link createMockCsrfTokenStore}'s output.
37
+ *
38
+ * @category Internal
39
+ */
40
+ export type MockCsrfTokenStoreAccessRecord = {
41
+ getCsrfToken: number;
42
+ setCsrfToken: string[];
43
+ deleteCsrfToken: number;
44
+ };
45
+ /**
46
+ * Create an empty `accessRecord` object, this is to be used in conjunction with
47
+ * {@link createMockCsrfTokenStore}.
48
+ *
49
+ * @category Mock
50
+ */
51
+ export declare function createEmptyMockCsrfTokenStoreAccessRecord(): MockCsrfTokenStoreAccessRecord;
52
+ /**
53
+ * Create a mock {@link CsrfTokenStore} backed by a simple in-memory object, for use in tests.
54
+ *
55
+ * @category Mock
56
+ */
57
+ export declare function createMockCsrfTokenStore(
58
+ /** Set an initial value to initialize the mocked store contents. */
59
+ init?: string | undefined): {
60
+ csrfTokenStore: CsrfTokenStore;
61
+ /** The current value held in the mock store. */
62
+ readonly storedValue: string | undefined;
63
+ accessRecord: MockCsrfTokenStoreAccessRecord;
64
+ };
@@ -0,0 +1,107 @@
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({
48
+ key,
49
+ value,
50
+ });
51
+ store[key] = value;
52
+ },
53
+ };
54
+ return {
55
+ localStorage: mockLocalStorage,
56
+ store,
57
+ accessRecord,
58
+ };
59
+ }
60
+ /**
61
+ * Create an empty `accessRecord` object, this is to be used in conjunction with
62
+ * {@link createMockCsrfTokenStore}.
63
+ *
64
+ * @category Mock
65
+ */
66
+ export function createEmptyMockCsrfTokenStoreAccessRecord() {
67
+ return {
68
+ getCsrfToken: 0,
69
+ setCsrfToken: [],
70
+ deleteCsrfToken: 0,
71
+ };
72
+ }
73
+ /**
74
+ * Create a mock {@link CsrfTokenStore} backed by a simple in-memory object, for use in tests.
75
+ *
76
+ * @category Mock
77
+ */
78
+ export function createMockCsrfTokenStore(
79
+ /** Set an initial value to initialize the mocked store contents. */
80
+ init) {
81
+ let storedValue = init;
82
+ const accessRecord = createEmptyMockCsrfTokenStoreAccessRecord();
83
+ const csrfTokenStore = {
84
+ getCsrfToken() {
85
+ accessRecord.getCsrfToken++;
86
+ return Promise.resolve(storedValue);
87
+ },
88
+ setCsrfToken(value) {
89
+ accessRecord.setCsrfToken.push(value);
90
+ storedValue = value;
91
+ return Promise.resolve();
92
+ },
93
+ deleteCsrfToken() {
94
+ accessRecord.deleteCsrfToken++;
95
+ storedValue = undefined;
96
+ return Promise.resolve();
97
+ },
98
+ };
99
+ return {
100
+ csrfTokenStore,
101
+ /** The current value held in the mock store. */
102
+ get storedValue() {
103
+ return storedValue;
104
+ },
105
+ accessRecord,
106
+ };
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth-vir",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
5
5
  "keywords": [
6
6
  "auth",
@@ -41,20 +41,21 @@
41
41
  "test:web": "virmator test web"
42
42
  },
43
43
  "dependencies": {
44
- "@augment-vir/assert": "^31.65.0",
45
- "@augment-vir/common": "^31.65.0",
46
- "date-vir": "^8.1.1",
44
+ "@augment-vir/assert": "^31.68.1",
45
+ "@augment-vir/common": "^31.68.1",
46
+ "date-vir": "^8.2.1",
47
47
  "detect-activity": "^1.0.0",
48
48
  "hash-wasm": "^4.12.0",
49
- "jose": "^6.1.3",
49
+ "jose": "^6.2.1",
50
+ "local-db-client": "^1.0.0",
50
51
  "object-shape-tester": "^6.11.0",
51
52
  "type-fest": "^5.4.4",
52
53
  "url-vir": "^2.1.7"
53
54
  },
54
55
  "devDependencies": {
55
- "@augment-vir/test": "^31.65.0",
56
+ "@augment-vir/test": "^31.68.1",
56
57
  "@prisma/client": "^6.19.2",
57
- "@types/node": "^25.3.0",
58
+ "@types/node": "^25.3.3",
58
59
  "@web/dev-server-esbuild": "^1.0.5",
59
60
  "@web/test-runner": "^0.20.2",
60
61
  "@web/test-runner-commands": "^0.9.0",