@absolutejs/auth 0.4.0 → 0.5.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.
@@ -0,0 +1,245 @@
1
+ // eslint.config.mjs
2
+ import { dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+
6
+ import pluginJs from '@eslint/js';
7
+ import stylisticTs from '@stylistic/eslint-plugin-ts';
8
+ import tsParser from '@typescript-eslint/parser';
9
+ import absolutePlugin from 'eslint-plugin-absolute';
10
+ import importPlugin from 'eslint-plugin-import';
11
+ import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
12
+ import promisePlugin from 'eslint-plugin-promise';
13
+ import reactPlugin from 'eslint-plugin-react';
14
+ import reactCompilerPlugin from 'eslint-plugin-react-compiler';
15
+ import reactHooksPlugin from 'eslint-plugin-react-hooks';
16
+ import securityPlugin from 'eslint-plugin-security';
17
+ import globals from 'globals';
18
+ import tseslint from 'typescript-eslint';
19
+
20
+ export default [
21
+ {
22
+ ignores: ['example/build/**']
23
+ },
24
+ pluginJs.configs.recommended,
25
+
26
+ ...tseslint.configs.recommended,
27
+
28
+ {
29
+ files: ['**/*.{ts,tsx}'],
30
+ languageOptions: {
31
+ globals: globals.browser,
32
+ parser: tsParser,
33
+ parserOptions: {
34
+ createDefaultProgram: true,
35
+ project: './tsconfig.json',
36
+ tsconfigRootDir: __dirname
37
+ }
38
+ }
39
+ },
40
+
41
+ {
42
+ files: ['**/*.{ts,tsx}'],
43
+ plugins: { '@stylistic/ts': stylisticTs },
44
+ rules: {
45
+ '@stylistic/ts/padding-line-between-statements': [
46
+ 'error',
47
+ { blankLine: 'always', next: 'return', prev: '*' }
48
+ ]
49
+ }
50
+ },
51
+
52
+ {
53
+ files: ['**/*.{js,mjs,cjs,ts,tsx,jsx}'],
54
+ ignores: ['example/build/**'],
55
+ plugins: {
56
+ absolute: absolutePlugin,
57
+ import: importPlugin,
58
+ promise: promisePlugin,
59
+ security: securityPlugin
60
+ },
61
+ rules: {
62
+ 'absolute/explicit-object-types': 'error',
63
+ 'absolute/localize-react-props': 'error',
64
+ 'absolute/max-depth-extended': ['error', 1],
65
+ 'absolute/max-jsxnesting': ['error', 5],
66
+ 'absolute/min-var-length': [
67
+ 'error',
68
+ { allowedVars: ['_', 'id', 'db', 'OK'], minLength: 3 }
69
+ ],
70
+ 'absolute/no-button-navigation': 'error',
71
+ 'absolute/no-explicit-return-type': 'error',
72
+ 'absolute/no-inline-prop-types': 'error',
73
+ 'absolute/no-multi-style-objects': 'error',
74
+ 'absolute/no-nested-jsx-return': 'error',
75
+ 'absolute/no-or-none-component': 'error',
76
+ 'absolute/no-transition-cssproperties': 'error',
77
+ 'absolute/no-type-cast': 'error',
78
+ 'absolute/no-unnecessary-div': 'error',
79
+ 'absolute/no-unnecessary-key': 'error',
80
+ 'absolute/no-useless-function': 'error',
81
+ 'absolute/seperate-style-files': 'error',
82
+ 'absolute/sort-exports': [
83
+ 'error',
84
+ {
85
+ caseSensitive: true,
86
+ natural: true,
87
+ order: 'asc',
88
+ variablesBeforeFunctions: true
89
+ }
90
+ ],
91
+ 'absolute/sort-keys-fixable': [
92
+ 'error',
93
+ {
94
+ caseSensitive: true,
95
+ natural: true,
96
+ order: 'asc',
97
+ variablesBeforeFunctions: true
98
+ }
99
+ ],
100
+ 'arrow-body-style': ['error', 'as-needed'],
101
+ 'consistent-return': 'error',
102
+ eqeqeq: 'error',
103
+ 'func-style': [
104
+ 'error',
105
+ 'expression',
106
+ { allowArrowFunctions: true }
107
+ ],
108
+ 'import/no-cycle': 'error',
109
+ 'import/no-default-export': 'error',
110
+ 'import/no-relative-packages': 'error',
111
+ 'import/no-unused-modules': ['error', { missingExports: true }],
112
+ 'import/order': ['error', { alphabetize: { order: 'asc' } }],
113
+ 'no-await-in-loop': 'error',
114
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
115
+ 'no-debugger': 'error',
116
+ 'no-duplicate-case': 'error',
117
+ 'no-duplicate-imports': 'error',
118
+ 'no-else-return': 'error',
119
+ 'no-empty-function': 'error',
120
+ 'no-empty-pattern': 'error',
121
+ 'no-empty-static-block': 'error',
122
+ 'no-fallthrough': 'error',
123
+ 'no-floating-decimal': 'error',
124
+ 'no-global-assign': 'error',
125
+ 'no-implicit-coercion': 'error',
126
+ 'no-implicit-globals': 'error',
127
+ 'no-loop-func': 'error',
128
+ 'no-magic-numbers': [
129
+ 'warn',
130
+ { detectObjects: false, enforceConst: true, ignore: [0, 1] }
131
+ ],
132
+ 'no-misleading-character-class': 'error',
133
+ 'no-nested-ternary': 'error',
134
+ 'no-new-native-nonconstructor': 'error',
135
+ 'no-new-wrappers': 'error',
136
+ 'no-param-reassign': 'error',
137
+ 'no-restricted-imports': [
138
+ 'error',
139
+ {
140
+ paths: [
141
+ {
142
+ importNames: ['default'],
143
+ message:
144
+ 'Import only named React exports for tree-shaking.',
145
+ name: 'react'
146
+ },
147
+ {
148
+ importNames: ['default'],
149
+ message: 'Import only the required Bun exports.',
150
+ name: 'bun'
151
+ }
152
+ ]
153
+ }
154
+ ],
155
+ 'no-return-await': 'error',
156
+ 'no-shadow': 'error',
157
+ 'no-undef': 'error',
158
+ 'no-unneeded-ternary': 'error',
159
+ 'no-unreachable': 'error',
160
+ 'no-useless-assignment': 'error',
161
+ 'no-useless-concat': 'error',
162
+ 'no-useless-return': 'error',
163
+ 'no-var': 'error',
164
+ 'prefer-arrow-callback': 'error',
165
+ 'prefer-const': 'error',
166
+ 'prefer-destructuring': [
167
+ 'error',
168
+ { array: true, object: true },
169
+ { enforceForRenamedProperties: false }
170
+ ],
171
+ 'prefer-template': 'error',
172
+ 'promise/always-return': 'warn',
173
+ 'promise/avoid-new': 'warn',
174
+ 'promise/catch-or-return': 'error',
175
+ 'promise/no-callback-in-promise': 'warn',
176
+ 'promise/no-nesting': 'warn',
177
+ 'promise/no-promise-in-callback': 'warn',
178
+ 'promise/no-return-wrap': 'error',
179
+ 'promise/param-names': 'error'
180
+ }
181
+ },
182
+ {
183
+ files: ['example/**/*.{js,jsx,ts,tsx}'],
184
+ plugins: {
185
+ 'jsx-a11y': jsxA11yPlugin,
186
+ react: reactPlugin,
187
+ 'react-compiler': reactCompilerPlugin,
188
+ 'react-hooks': reactHooksPlugin
189
+ },
190
+ rules: {
191
+ 'jsx-a11y/prefer-tag-over-role': 'error',
192
+ 'react-compiler/react-compiler': 'error',
193
+ 'react-hooks/exhaustive-deps': 'warn',
194
+ 'react-hooks/rules-of-hooks': 'error',
195
+ 'react/checked-requires-onchange-or-readonly': 'error',
196
+ 'react/destructuring-assignment': ['error', 'always'],
197
+ 'react/jsx-filename-extension': ['error', { extensions: ['.tsx'] }],
198
+ 'react/jsx-no-leaked-render': 'error',
199
+ 'react/jsx-no-target-blank': 'error',
200
+ 'react/jsx-no-useless-fragment': 'error',
201
+ 'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
202
+ 'react/no-multi-comp': 'error',
203
+ 'react/no-unknown-property': 'off',
204
+ 'react/react-in-jsx-scope': 'off',
205
+ 'react/self-closing-comp': 'error'
206
+ },
207
+ settings: {
208
+ react: { version: 'detect' }
209
+ }
210
+ },
211
+ {
212
+ files: [
213
+ 'example/server.ts',
214
+ 'example/indexes/*.tsx',
215
+ 'example/db/migrate.ts'
216
+ ],
217
+ rules: {
218
+ 'import/no-unused-modules': 'off'
219
+ }
220
+ },
221
+ {
222
+ files: ['example/db/migrate.ts','example/server.ts'],
223
+ rules: {
224
+ 'no-console': 'off'
225
+ }
226
+ },
227
+ {
228
+ files: ['eslint.config.mjs'],
229
+ rules: {
230
+ 'no-magic-numbers': 'off'
231
+ }
232
+ },
233
+ {
234
+ files: ['eslint.config.mjs'],
235
+ rules: {
236
+ 'import/no-default-export': 'off'
237
+ }
238
+ },
239
+ {
240
+ files: ['example/db/schema.ts'],
241
+ rules: {
242
+ 'absolute/explicit-object-types': 'off'
243
+ }
244
+ }
245
+ ];
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.0",
2
+ "version": "0.5.0",
3
3
  "name": "@absolutejs/auth",
4
4
  "description": "An authorization library for absolutejs",
5
5
  "repository": {
@@ -10,13 +10,15 @@
10
10
  "license": "CC BY-NC 4.0",
11
11
  "author": "Alex Kahn",
12
12
  "scripts": {
13
- "build": "rm -rf dist && bun build src/index.ts --outdir dist --minify --splitting --target=bun --external arctic --external elysia && tsc --emitDeclarationOnly --project tsconfig.json",
13
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --target=bun --external elysia && tsc --emitDeclarationOnly --project tsconfig.json",
14
14
  "test": "echo \"Error: no test specified\" && exit 1",
15
15
  "format": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,json}\"",
16
16
  "dev": "bun run --watch example/server.ts",
17
17
  "db:generate": "drizzle-kit generate --schema ./example/db/schema.ts --dialect postgresql",
18
18
  "db:migrate": "bun run ./example/db/migrate.ts",
19
19
  "db:studio": "drizzle-kit studio",
20
+ "lint": "eslint ./src ./example ./eslint.config.mjs",
21
+ "typecheck": "bun run tsc --noEmit",
20
22
  "release": "bun run format && bun run build && bun publish"
21
23
  },
22
24
  "keywords": [
@@ -28,23 +30,36 @@
28
30
  ],
29
31
  "types": "dist/src/index.d.ts",
30
32
  "peerDependencies": {
31
- "arctic": ">= 3.6.0",
32
33
  "elysia": ">= 1.2.0"
33
34
  },
34
35
  "devDependencies": {
35
- "arctic": "3.6.0",
36
+ "@absolutejs/absolute": "0.4.0",
36
37
  "@elysiajs/static": "1.2.0",
37
38
  "@neondatabase/serverless": "1.0.0",
39
+ "@react-spring/web": "10.0.0-beta.0",
40
+ "@stylistic/eslint-plugin-ts": "4.2.0",
38
41
  "@types/bun": "1.2.9",
39
42
  "@types/react": "19.1.1",
40
43
  "@types/react-dom": "19.1.2",
44
+ "citra": "0.15.1",
41
45
  "drizzle-kit": "0.30.6",
42
46
  "drizzle-orm": "0.41.0",
47
+ "elysia": "1.2.25",
48
+ "eslint": "9.26.0",
49
+ "eslint-plugin-absolute": "0.0.3",
50
+ "eslint-plugin-import": "2.31.0",
51
+ "eslint-plugin-jsx-a11y": "6.10.2",
52
+ "eslint-plugin-promise": "7.2.1",
53
+ "eslint-plugin-react": "7.37.5",
54
+ "eslint-plugin-react-compiler": "19.1.0-rc.1",
55
+ "eslint-plugin-react-hooks": "5.2.0",
56
+ "eslint-plugin-security": "3.0.1",
43
57
  "prettier": "3.5.3",
44
58
  "react": "19.1.0",
45
59
  "react-dom": "19.1.0",
46
- "elysia": "1.2.25",
47
- "typescript": "5.8.3"
60
+ "react-icons": "5.5.0",
61
+ "typescript": "5.8.3",
62
+ "typescript-eslint": "8.32.0"
48
63
  },
49
64
  "module": "dist/index.js"
50
65
  }
@@ -1,10 +0,0 @@
1
- export type ProfileRequest = {
2
- endpoint: string;
3
- method?: 'GET' | 'POST';
4
- authIn: 'header' | 'query';
5
- tokenParam?: string;
6
- headers?: Record<string, string>;
7
- body?: any;
8
- };
9
- export declare function fetchUserProfile(provider: string, accessToken: string): Promise<any>;
10
- export declare const profileConfigs: Record<string, ProfileRequest>;
@@ -1,51 +0,0 @@
1
- import { AmazonCognito, AniList, Apple, Atlassian, Auth0, Authentik, Bitbucket, Box, Coinbase, Discord, Dribbble, Dropbox, Facebook, Figma, Intuit, GitHub, GitLab, Google, Kakao, KeyCloak, Lichess, Line, Linear, LinkedIn, MicrosoftEntraId, MyAnimeList, Notion, Okta, Osu, Patreon, Reddit, Roblox, Salesforce, Shikimori, Slack, Spotify, Strava, Tiltify, Tumblr, Twitch, Twitter, VK, WorkOS, Yahoo, Yandex, Zoom, FortyTwo } from 'arctic';
2
- export declare const providers: {
3
- AmazonCognito: typeof AmazonCognito;
4
- AniList: typeof AniList;
5
- Apple: typeof Apple;
6
- Atlassian: typeof Atlassian;
7
- Auth0: typeof Auth0;
8
- Authentik: typeof Authentik;
9
- Bitbucket: typeof Bitbucket;
10
- Box: typeof Box;
11
- Coinbase: typeof Coinbase;
12
- Discord: typeof Discord;
13
- Dribbble: typeof Dribbble;
14
- Dropbox: typeof Dropbox;
15
- Facebook: typeof Facebook;
16
- Figma: typeof Figma;
17
- FortyTwo: typeof FortyTwo;
18
- GitHub: typeof GitHub;
19
- GitLab: typeof GitLab;
20
- Google: typeof Google;
21
- Intuit: typeof Intuit;
22
- Kakao: typeof Kakao;
23
- KeyCloak: typeof KeyCloak;
24
- Lichess: typeof Lichess;
25
- Line: typeof Line;
26
- Linear: typeof Linear;
27
- LinkedIn: typeof LinkedIn;
28
- MicrosoftEntraId: typeof MicrosoftEntraId;
29
- MyAnimeList: typeof MyAnimeList;
30
- Notion: typeof Notion;
31
- Okta: typeof Okta;
32
- Osu: typeof Osu;
33
- Patreon: typeof Patreon;
34
- Reddit: typeof Reddit;
35
- Roblox: typeof Roblox;
36
- Salesforce: typeof Salesforce;
37
- Shikimori: typeof Shikimori;
38
- Slack: typeof Slack;
39
- Spotify: typeof Spotify;
40
- Strava: typeof Strava;
41
- Tiltify: typeof Tiltify;
42
- Tumblr: typeof Tumblr;
43
- Twitch: typeof Twitch;
44
- Twitter: typeof Twitter;
45
- VK: typeof VK;
46
- WorkOS: typeof WorkOS;
47
- Yahoo: typeof Yahoo;
48
- Yandex: typeof Yandex;
49
- Zoom: typeof Zoom;
50
- };
51
- export declare const normalizedProviderKeys: Record<string, string>;
package/src/authorize.ts DELETED
@@ -1,119 +0,0 @@
1
- import { generateState, generateCodeVerifier } from 'arctic';
2
- import { Elysia } from 'elysia';
3
- import { isValidProviderKey } from './typeGuards';
4
- import { ClientProviders } from './types';
5
- import { COOKIE_DURATION } from './constants';
6
-
7
- type AuthorizeProps = {
8
- clientProviders: ClientProviders;
9
- authorizeRoute?: string;
10
- onAuthorize?: () => void;
11
- };
12
-
13
- export const authorize = ({
14
- clientProviders,
15
- authorizeRoute = 'authorize',
16
- onAuthorize
17
- }: AuthorizeProps) =>
18
- new Elysia().get(
19
- `/${authorizeRoute}/:provider`,
20
- ({
21
- error,
22
- redirect,
23
- cookie: { state, code_verifier, auth_provider, redirect_url },
24
- params: { provider },
25
- headers
26
- }) => {
27
- if (provider === undefined)
28
- return error('Bad Request', 'Provider is required');
29
-
30
- if (!isValidProviderKey(provider)) {
31
- return error('Bad Request', 'Invalid provider');
32
- }
33
-
34
- try {
35
- const normalizedProvider = provider.toLowerCase();
36
- const { providerInstance, scopes, searchParams } =
37
- clientProviders[normalizedProvider];
38
-
39
- const redirectUrl = headers['referer'] ?? '/';
40
-
41
- redirect_url.set({
42
- httpOnly: true,
43
- maxAge: COOKIE_DURATION,
44
- path: '/',
45
- sameSite: 'lax',
46
- secure: true,
47
- value: redirectUrl
48
- });
49
-
50
- auth_provider.set({
51
- httpOnly: true,
52
- maxAge: COOKIE_DURATION,
53
- path: '/',
54
- sameSite: 'lax',
55
- secure: true,
56
- value: provider
57
- });
58
-
59
- const currentState = generateState();
60
-
61
- state.set({
62
- httpOnly: true,
63
- maxAge: COOKIE_DURATION,
64
- path: '/',
65
- sameSite: 'lax',
66
- secure: true,
67
- value: currentState
68
- });
69
-
70
- const usesCodeVerifier = providerInstance.createAuthorizationURL
71
- .toString()
72
- .includes('codeVerifier');
73
- const verifier = usesCodeVerifier
74
- ? generateCodeVerifier()
75
- : undefined;
76
- void (
77
- usesCodeVerifier &&
78
- code_verifier.set({
79
- httpOnly: true,
80
- maxAge: COOKIE_DURATION,
81
- path: '/',
82
- sameSite: 'lax',
83
- secure: true,
84
- value: verifier ?? ''
85
- })
86
- );
87
-
88
- const authorizationURL = usesCodeVerifier
89
- ? providerInstance.createAuthorizationURL(
90
- currentState,
91
- // @ts-expect-error - This is a dynamic check
92
- verifier,
93
- scopes
94
- )
95
- : // @ts-expect-error - This is a dynamic check
96
- providerInstance.createAuthorizationURL(
97
- currentState,
98
- scopes
99
- );
100
-
101
- searchParams.forEach(([key, value]) => {
102
- authorizationURL.searchParams.set(key, value);
103
- });
104
-
105
- onAuthorize?.();
106
-
107
- return redirect(authorizationURL.toString());
108
- } catch (err) {
109
- if (err instanceof Error) {
110
- return error(
111
- 'Internal Server Error',
112
- `${err.message} - ${err.stack ?? ''}`
113
- );
114
- }
115
-
116
- return error('Internal Server Error', `Unknown error: ${err}`);
117
- }
118
- }
119
- );
package/src/callback.ts DELETED
@@ -1,132 +0,0 @@
1
- import { decodeIdToken } from 'arctic';
2
- import { Elysia } from 'elysia';
3
- import { sessionStore } from './sessionStore';
4
- import { isNonEmptyString, isValidProviderKey } from './typeGuards';
5
- import { ClientProviders, OnCallback } from './types';
6
- import { fetchUserProfile } from './profiles';
7
-
8
- type CallbackProps<UserType> = {
9
- clientProviders: ClientProviders;
10
- callbackRoute?: string;
11
- onCallback?: OnCallback<UserType>;
12
- };
13
-
14
- export const callback = <UserType>({
15
- clientProviders,
16
- callbackRoute = 'authorize/callback',
17
- onCallback
18
- }: CallbackProps<UserType>) =>
19
- new Elysia()
20
- .use(sessionStore<UserType>())
21
- .get(
22
- `/${callbackRoute}`,
23
- async ({
24
- error,
25
- redirect,
26
- store: { session },
27
- cookie: {
28
- state: stored_state,
29
- code_verifier,
30
- redirect_url,
31
- user_session_id,
32
- auth_provider
33
- },
34
- query: { code, state: callback_state }
35
- }) => {
36
- if (
37
- !isNonEmptyString(code) ||
38
- stored_state.value === undefined
39
- ) {
40
- return error('Bad Request', 'Invalid callback request');
41
- }
42
-
43
- if (callback_state !== stored_state.value) {
44
- return error('Bad Request', 'Invalid state mismatch');
45
- }
46
-
47
- if (auth_provider.value === undefined) {
48
- return error('Unauthorized', 'No auth provider found');
49
- }
50
-
51
- if (!isValidProviderKey(auth_provider.value)) {
52
- return error('Unauthorized', 'Invalid provider');
53
- }
54
-
55
- const normalizedProvider = auth_provider.value.toLowerCase();
56
- const { providerInstance } =
57
- clientProviders[normalizedProvider];
58
-
59
- try {
60
- // Clear the stored state so the next request doesn't use it
61
- stored_state.remove();
62
-
63
- const hasCodeVerifier =
64
- providerInstance.validateAuthorizationCode
65
- .toString()
66
- .includes('codeVerifier');
67
- const verifier = code_verifier.value;
68
- if (hasCodeVerifier && verifier === undefined)
69
- return error(
70
- 'Bad Request',
71
- 'Code verifier not found and is required'
72
- );
73
-
74
- const safeVerifier = verifier ?? '';
75
- const tokens = await (hasCodeVerifier
76
- ? providerInstance.validateAuthorizationCode(
77
- code,
78
- safeVerifier
79
- )
80
- : // @ts-expect-error - This is a dynamic check
81
- providerInstance.validateAuthorizationCode(code));
82
-
83
- if (hasCodeVerifier) code_verifier.remove();
84
-
85
- let userProfile;
86
- const authProvider = auth_provider.value;
87
-
88
- // TODO : Change if arctic ever updates
89
- try {
90
- const decodedIdToken = Object.fromEntries(
91
- Object.entries(decodeIdToken(tokens.idToken())).map(
92
- ([key, value]) => [
93
- key,
94
- typeof value === 'string'
95
- ? value
96
- : undefined
97
- ]
98
- )
99
- );
100
- userProfile = decodedIdToken;
101
- } catch (err) {
102
- // TODO : This has type any see if it can be fixed
103
- userProfile = await fetchUserProfile(
104
- authProvider,
105
- tokens.accessToken()
106
- );
107
- }
108
-
109
- await onCallback?.({
110
- authProvider,
111
- userProfile,
112
- session,
113
- user_session_id
114
- });
115
-
116
- const redirectUrl = redirect_url.value ?? '/';
117
-
118
- return redirect(redirectUrl);
119
- } catch (err) {
120
- if (err instanceof Error)
121
- return error(
122
- 'Internal Server Error',
123
- `${err.message} - ${err.stack ?? ''}`
124
- );
125
-
126
- return error(
127
- 'Internal Server Error',
128
- `Failed to validate authorization code: Unknown error: ${err}`
129
- );
130
- }
131
- }
132
- );
package/src/constants.ts DELETED
@@ -1,13 +0,0 @@
1
- export const SECONDS_IN_A_MINUTE = 60;
2
- export const MILLISECONDS_IN_A_SECOND = 1000;
3
- export const MILLISECONDS_IN_A_MINUTE =
4
- MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE;
5
- export const MINUTES_IN_AN_HOUR = 60;
6
- export const HOURS_IN_A_DAY = 24;
7
- export const MILLISECONDS_IN_A_DAY =
8
- MILLISECONDS_IN_A_SECOND *
9
- SECONDS_IN_A_MINUTE *
10
- MINUTES_IN_AN_HOUR *
11
- HOURS_IN_A_DAY;
12
- export const COOKIE_MINUTES = 30; // TODO : See if i can remove
13
- export const COOKIE_DURATION = SECONDS_IN_A_MINUTE * COOKIE_MINUTES;