codeforlife 2.6.17 → 2.7.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.
- package/.github/workflows/main.yml +13 -1
- package/.prettierignore +1 -5
- package/CHANGELOG.md +7 -0
- package/package.json +2 -2
- package/src/api/endpoints/session.ts +7 -0
- package/src/hooks/auth.tsx +279 -2
- package/src/utils/auth.ts +61 -0
- package/src/utils/general.ts +16 -0
- package/.prettierrc.json +0 -4
- package/.vscode/launch.json +0 -22
- package/.vscode/settings.json +0 -30
|
@@ -7,13 +7,20 @@ on:
|
|
|
7
7
|
- '**/*.code-*'
|
|
8
8
|
- '.vscode/**'
|
|
9
9
|
- '.devcontainer.json'
|
|
10
|
+
pull_request:
|
|
11
|
+
issues:
|
|
12
|
+
issue_comment:
|
|
10
13
|
workflow_dispatch:
|
|
11
14
|
|
|
12
15
|
jobs:
|
|
16
|
+
issue:
|
|
17
|
+
uses: ocadotechnology/codeforlife-workspace/.github/workflows/issues.yaml@main
|
|
18
|
+
secrets: inherit
|
|
19
|
+
|
|
13
20
|
contributor:
|
|
14
21
|
uses: ocadotechnology/codeforlife-contributor-backend/.github/workflows/check-pr-authors-signed-latest-agreement.yaml@main
|
|
15
22
|
|
|
16
|
-
test:
|
|
23
|
+
test:
|
|
17
24
|
uses: ocadotechnology/codeforlife-workspace/.github/workflows/test-javascript-code.yaml@main
|
|
18
25
|
secrets: inherit
|
|
19
26
|
with:
|
|
@@ -44,3 +51,8 @@ jobs:
|
|
|
44
51
|
needs: [release]
|
|
45
52
|
with:
|
|
46
53
|
node-version: 22
|
|
54
|
+
|
|
55
|
+
submodule:
|
|
56
|
+
uses: ocadotechnology/codeforlife-workspace/.github/workflows/gitmodules.yaml@main
|
|
57
|
+
secrets: inherit
|
|
58
|
+
needs: [release]
|
package/.prettierignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [2.7.0](https://github.com/ocadotechnology/codeforlife-package-javascript/compare/v2.6.17...v2.7.0) (2025-08-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* oauth2 support ([#87](https://github.com/ocadotechnology/codeforlife-package-javascript/issues/87)) ([6332e5c](https://github.com/ocadotechnology/codeforlife-package-javascript/commit/6332e5c9583a4936066e4144a2436af59fdaa46c))
|
|
7
|
+
|
|
1
8
|
## [2.6.17](https://github.com/ocadotechnology/codeforlife-package-javascript/compare/v2.6.16...v2.6.17) (2025-06-25)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"name": "codeforlife",
|
|
3
3
|
"description": "Common frontend code",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.7.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"cli": "VITE_CONFIG=./vite.config.ts ../scripts/frontend
|
|
8
|
+
"cli": "VITE_CONFIG=./vite.config.ts ../scripts/frontend $@"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -2,6 +2,13 @@ import { type EndpointBuilder, type Api } from "@reduxjs/toolkit/query/react"
|
|
|
2
2
|
|
|
3
3
|
import { login, logout } from "../../slices/session"
|
|
4
4
|
|
|
5
|
+
export type ExchangeOAuth2CodeResult = null
|
|
6
|
+
export type ExchangeOAuth2CodeArg = {
|
|
7
|
+
code: string
|
|
8
|
+
code_verifier: string
|
|
9
|
+
redirect_uri: string
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export function buildLoginEndpoint<ResultType, QueryArg>(
|
|
6
13
|
build: EndpointBuilder<any, any, any>,
|
|
7
14
|
url: string = "session/login/",
|
package/src/hooks/auth.tsx
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
+
import * as yup from "yup"
|
|
1
2
|
import Cookies from "js-cookie"
|
|
2
|
-
import { useEffect, type ReactNode } from "react"
|
|
3
|
-
import { createSearchParams
|
|
3
|
+
import { useEffect, useState, useCallback, type ReactNode } from "react"
|
|
4
|
+
import { createSearchParams } from "react-router-dom"
|
|
5
|
+
import type { TypedUseMutation } from "@reduxjs/toolkit/query/react"
|
|
4
6
|
import { useSelector } from "react-redux"
|
|
5
7
|
|
|
6
8
|
import { type AuthFactor, type User } from "../api"
|
|
9
|
+
import { generateSecureRandomString } from "../utils/general"
|
|
10
|
+
import {
|
|
11
|
+
makeOAuth2StorageKey,
|
|
12
|
+
generateOAuth2CodeChallenge,
|
|
13
|
+
type OAuth2CodeChallengeLengths,
|
|
14
|
+
type OAuth2CodeChallenge,
|
|
15
|
+
type OAuth2RequestCodeUrlSearchParams,
|
|
16
|
+
type OAuth2ReceiveCodeUrlSearchParams,
|
|
17
|
+
} from "../utils/auth"
|
|
18
|
+
import {
|
|
19
|
+
type ExchangeOAuth2CodeResult,
|
|
20
|
+
type ExchangeOAuth2CodeArg,
|
|
21
|
+
} from "../api/endpoints/session"
|
|
22
|
+
import { useSearchParams, useLocation, useNavigate } from "./router"
|
|
7
23
|
import { SESSION_METADATA_COOKIE_NAME } from "../settings"
|
|
8
24
|
import { selectIsLoggedIn } from "../slices/session"
|
|
9
25
|
|
|
26
|
+
// -----------------------------------------------------------------------------
|
|
27
|
+
// Session
|
|
28
|
+
// -----------------------------------------------------------------------------
|
|
29
|
+
|
|
10
30
|
export interface SessionMetadata {
|
|
11
31
|
user_id: User["id"]
|
|
12
32
|
user_type: "teacher" | "student" | "indy"
|
|
@@ -85,3 +105,260 @@ export function useSession<
|
|
|
85
105
|
|
|
86
106
|
return children
|
|
87
107
|
}
|
|
108
|
+
|
|
109
|
+
// -----------------------------------------------------------------------------
|
|
110
|
+
// OAuth2
|
|
111
|
+
// -----------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
export function useOAuth2State(
|
|
114
|
+
provider: string,
|
|
115
|
+
length: number = 32,
|
|
116
|
+
storageKey: string = "state",
|
|
117
|
+
): [string | undefined, () => void] {
|
|
118
|
+
const oAuth2StorageKey = makeOAuth2StorageKey(provider, storageKey)
|
|
119
|
+
const storageValue = sessionStorage.getItem(oAuth2StorageKey)
|
|
120
|
+
|
|
121
|
+
const [_state, _setState] = useState<string>()
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
let state: string
|
|
125
|
+
if (storageValue && storageValue.length === length) {
|
|
126
|
+
state = storageValue
|
|
127
|
+
} else {
|
|
128
|
+
state = generateSecureRandomString(length)
|
|
129
|
+
sessionStorage.setItem(oAuth2StorageKey, state)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_setState(state)
|
|
133
|
+
}, [oAuth2StorageKey, storageValue, length])
|
|
134
|
+
|
|
135
|
+
const resetState = useCallback(() => {
|
|
136
|
+
sessionStorage.removeItem(oAuth2StorageKey)
|
|
137
|
+
_setState(undefined)
|
|
138
|
+
}, [oAuth2StorageKey])
|
|
139
|
+
|
|
140
|
+
return [_state, resetState]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function useOAuth2CodeChallenge(
|
|
144
|
+
provider: string,
|
|
145
|
+
length: OAuth2CodeChallengeLengths = 128,
|
|
146
|
+
storageKey: string = "codeChallenge",
|
|
147
|
+
): [OAuth2CodeChallenge | undefined, () => void] {
|
|
148
|
+
const oAuth2StorageKey = makeOAuth2StorageKey(provider, storageKey)
|
|
149
|
+
const storageValue = sessionStorage.getItem(oAuth2StorageKey)
|
|
150
|
+
|
|
151
|
+
const [_codeChallenge, _setCodeChallenge] = useState<OAuth2CodeChallenge>()
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
let codeChallenge: OAuth2CodeChallenge | undefined
|
|
155
|
+
if (storageValue) {
|
|
156
|
+
const storageJsonValue: unknown = JSON.parse(storageValue)
|
|
157
|
+
if (
|
|
158
|
+
typeof storageJsonValue === "object" &&
|
|
159
|
+
storageJsonValue &&
|
|
160
|
+
"verifier" in storageJsonValue &&
|
|
161
|
+
typeof storageJsonValue.verifier == "string" &&
|
|
162
|
+
storageJsonValue.verifier.length === length &&
|
|
163
|
+
"challenge" in storageJsonValue &&
|
|
164
|
+
typeof storageJsonValue.challenge === "string" &&
|
|
165
|
+
"method" in storageJsonValue &&
|
|
166
|
+
storageJsonValue.method === "S256"
|
|
167
|
+
) {
|
|
168
|
+
codeChallenge = {
|
|
169
|
+
verifier: storageJsonValue.verifier,
|
|
170
|
+
challenge: storageJsonValue.challenge,
|
|
171
|
+
method: storageJsonValue.method,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (codeChallenge) _setCodeChallenge(codeChallenge)
|
|
177
|
+
else {
|
|
178
|
+
generateOAuth2CodeChallenge(length)
|
|
179
|
+
.then(codeChallenge => {
|
|
180
|
+
sessionStorage.setItem(
|
|
181
|
+
oAuth2StorageKey,
|
|
182
|
+
JSON.stringify(codeChallenge),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
_setCodeChallenge(codeChallenge)
|
|
186
|
+
})
|
|
187
|
+
.catch(error => {
|
|
188
|
+
if (error) console.error(error)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}, [oAuth2StorageKey, storageValue, length])
|
|
192
|
+
|
|
193
|
+
const resetCodeChallenge = useCallback(() => {
|
|
194
|
+
sessionStorage.removeItem(oAuth2StorageKey)
|
|
195
|
+
_setCodeChallenge(undefined)
|
|
196
|
+
}, [oAuth2StorageKey])
|
|
197
|
+
|
|
198
|
+
return [_codeChallenge, resetCodeChallenge]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface UseOAuth2KwArgs<ResultType = ExchangeOAuth2CodeResult> {
|
|
202
|
+
provider: string
|
|
203
|
+
authUri: string
|
|
204
|
+
clientId: string
|
|
205
|
+
redirectUri: string
|
|
206
|
+
scope: string
|
|
207
|
+
responseType?: "code"
|
|
208
|
+
accessType?: "offline"
|
|
209
|
+
prompt?: string
|
|
210
|
+
useLoginMutation: TypedUseMutation<ResultType, ExchangeOAuth2CodeArg, any>
|
|
211
|
+
onCreateSession: (result: ResultType) => void
|
|
212
|
+
onRetrieveSession: (metadata: SessionMetadata) => void
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type OAuth2 = [string, OAuth2RequestCodeUrlSearchParams] | []
|
|
216
|
+
|
|
217
|
+
// https://datatracker.ietf.org/doc/html/rfc7636
|
|
218
|
+
export function useOAuth2<ResultType = ExchangeOAuth2CodeResult>({
|
|
219
|
+
provider,
|
|
220
|
+
authUri,
|
|
221
|
+
clientId,
|
|
222
|
+
redirectUri,
|
|
223
|
+
scope,
|
|
224
|
+
responseType = "code",
|
|
225
|
+
accessType = "offline",
|
|
226
|
+
prompt,
|
|
227
|
+
useLoginMutation,
|
|
228
|
+
onCreateSession,
|
|
229
|
+
onRetrieveSession,
|
|
230
|
+
}: UseOAuth2KwArgs<ResultType>): OAuth2 {
|
|
231
|
+
const [state, resetState] = useOAuth2State(provider)
|
|
232
|
+
const [
|
|
233
|
+
{
|
|
234
|
+
verifier: codeVerifier,
|
|
235
|
+
challenge: codeChallenge,
|
|
236
|
+
method: codeChallengeMethod,
|
|
237
|
+
} = {},
|
|
238
|
+
resetCodeChallenge,
|
|
239
|
+
] = useOAuth2CodeChallenge(provider)
|
|
240
|
+
const [
|
|
241
|
+
login,
|
|
242
|
+
{
|
|
243
|
+
originalArgs: loginArgs = {} as ExchangeOAuth2CodeArg,
|
|
244
|
+
isLoading: loginIsLoading,
|
|
245
|
+
isError: loginIsError,
|
|
246
|
+
},
|
|
247
|
+
] = useLoginMutation()
|
|
248
|
+
const sessionMetadata = useSessionMetadata()
|
|
249
|
+
const navigate = useNavigate()
|
|
250
|
+
const searchParams =
|
|
251
|
+
useSearchParams({ code: yup.string(), state: yup.string() }) || {}
|
|
252
|
+
const location = useLocation<OAuth2ReceiveCodeUrlSearchParams>()
|
|
253
|
+
|
|
254
|
+
const locationState = location.state || {}
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
// If the the auth provider has redirected back to our site with the
|
|
258
|
+
// expected search params, we redirect to the current page to remove them.
|
|
259
|
+
if (searchParams.code && searchParams.state) {
|
|
260
|
+
navigate<OAuth2ReceiveCodeUrlSearchParams>(".", {
|
|
261
|
+
// Removes the URL containing the search params from the history stack.
|
|
262
|
+
replace: true,
|
|
263
|
+
// Ensure we don't break the auth flow by navigating to another page.
|
|
264
|
+
next: false,
|
|
265
|
+
// Store the search params in the page's state instead.
|
|
266
|
+
state: { code: searchParams.code, state: searchParams.state },
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
}, [searchParams.code, searchParams.state, navigate])
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
// If we're already logged in, no need to log in again.
|
|
273
|
+
if (sessionMetadata) onRetrieveSession(sessionMetadata)
|
|
274
|
+
else if (
|
|
275
|
+
// If the state and code verifier have been generated...
|
|
276
|
+
state &&
|
|
277
|
+
codeVerifier &&
|
|
278
|
+
// ...and the page's state contains a code...
|
|
279
|
+
locationState.code &&
|
|
280
|
+
// ...and the page's state contains the stored state...
|
|
281
|
+
locationState.state === state &&
|
|
282
|
+
// ...and the login endpoint was not called with the current values or has
|
|
283
|
+
// not returned and error...
|
|
284
|
+
(loginArgs.code !== locationState.code ||
|
|
285
|
+
loginArgs.code_verifier !== codeVerifier ||
|
|
286
|
+
loginArgs.redirect_uri !== redirectUri ||
|
|
287
|
+
!loginIsError) &&
|
|
288
|
+
// ...and the login endpoint is not currently being called...
|
|
289
|
+
!loginIsLoading
|
|
290
|
+
) {
|
|
291
|
+
// ...call the login endpoint.
|
|
292
|
+
login({
|
|
293
|
+
code: locationState.code,
|
|
294
|
+
code_verifier: codeVerifier,
|
|
295
|
+
redirect_uri: redirectUri,
|
|
296
|
+
})
|
|
297
|
+
.unwrap()
|
|
298
|
+
.then(onCreateSession)
|
|
299
|
+
.catch(() => {
|
|
300
|
+
navigate(".", {
|
|
301
|
+
replace: true,
|
|
302
|
+
state: {
|
|
303
|
+
notifications: [
|
|
304
|
+
{
|
|
305
|
+
props: {
|
|
306
|
+
error: true,
|
|
307
|
+
children: "Failed to login. Please try again.",
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
.finally(() => {
|
|
315
|
+
resetState()
|
|
316
|
+
resetCodeChallenge()
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}, [
|
|
320
|
+
navigate,
|
|
321
|
+
redirectUri,
|
|
322
|
+
// State
|
|
323
|
+
state,
|
|
324
|
+
locationState.state,
|
|
325
|
+
resetState,
|
|
326
|
+
// Code
|
|
327
|
+
codeVerifier,
|
|
328
|
+
locationState.code,
|
|
329
|
+
resetCodeChallenge,
|
|
330
|
+
// Login
|
|
331
|
+
login,
|
|
332
|
+
loginIsLoading,
|
|
333
|
+
loginIsError,
|
|
334
|
+
loginArgs.code,
|
|
335
|
+
loginArgs.code_verifier,
|
|
336
|
+
loginArgs.redirect_uri,
|
|
337
|
+
// Session
|
|
338
|
+
sessionMetadata,
|
|
339
|
+
onCreateSession,
|
|
340
|
+
onRetrieveSession,
|
|
341
|
+
])
|
|
342
|
+
|
|
343
|
+
if (state && codeChallenge && codeChallengeMethod) {
|
|
344
|
+
const urlSearchParams: OAuth2RequestCodeUrlSearchParams = {
|
|
345
|
+
client_id: clientId,
|
|
346
|
+
redirect_uri: redirectUri,
|
|
347
|
+
scope,
|
|
348
|
+
response_type: responseType,
|
|
349
|
+
access_type: accessType,
|
|
350
|
+
state,
|
|
351
|
+
code_challenge: codeChallenge,
|
|
352
|
+
code_challenge_method: codeChallengeMethod,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (prompt) urlSearchParams["prompt"] = prompt
|
|
356
|
+
|
|
357
|
+
return [
|
|
358
|
+
authUri + "?" + new URLSearchParams(urlSearchParams).toString(),
|
|
359
|
+
urlSearchParams,
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return []
|
|
364
|
+
}
|
package/src/utils/auth.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
SESSION_METADATA_COOKIE_NAME,
|
|
6
6
|
CSRF_COOKIE_NAME,
|
|
7
7
|
} from "../settings"
|
|
8
|
+
import { generateSecureRandomString } from "./general"
|
|
8
9
|
|
|
9
10
|
export function logout() {
|
|
10
11
|
Cookies.remove(SESSION_COOKIE_NAME)
|
|
@@ -15,3 +16,63 @@ export function logout() {
|
|
|
15
16
|
export function getCsrfCookie() {
|
|
16
17
|
return Cookies.get(CSRF_COOKIE_NAME)
|
|
17
18
|
}
|
|
19
|
+
|
|
20
|
+
export function makeOAuth2StorageKey(provider: string, key: string) {
|
|
21
|
+
return `oauth2.${provider}.${key}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const OAUTH2_CODE_CHALLENGE_METHODS = ["S256"] as const
|
|
25
|
+
|
|
26
|
+
export type OAuth2CodeChallengeMethods =
|
|
27
|
+
(typeof OAUTH2_CODE_CHALLENGE_METHODS)[number]
|
|
28
|
+
|
|
29
|
+
export const OAUTH2_CODE_CHALLENGE_LENGTHS = [
|
|
30
|
+
43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
|
|
31
|
+
62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
|
|
32
|
+
81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
|
|
33
|
+
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114,
|
|
34
|
+
115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128,
|
|
35
|
+
] as const
|
|
36
|
+
|
|
37
|
+
export type OAuth2CodeChallengeLengths =
|
|
38
|
+
(typeof OAUTH2_CODE_CHALLENGE_LENGTHS)[number]
|
|
39
|
+
|
|
40
|
+
export type OAuth2RequestCodeUrlSearchParams = {
|
|
41
|
+
client_id: string
|
|
42
|
+
redirect_uri: string
|
|
43
|
+
scope: string
|
|
44
|
+
response_type: string
|
|
45
|
+
access_type: string
|
|
46
|
+
prompt?: string
|
|
47
|
+
state: string
|
|
48
|
+
code_challenge: string
|
|
49
|
+
code_challenge_method: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type OAuth2ReceiveCodeUrlSearchParams = {
|
|
53
|
+
code: string
|
|
54
|
+
state: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type OAuth2CodeChallenge = {
|
|
58
|
+
verifier: string
|
|
59
|
+
challenge: string
|
|
60
|
+
method: OAuth2CodeChallengeMethods
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function generateOAuth2CodeChallenge(
|
|
64
|
+
length: OAuth2CodeChallengeLengths,
|
|
65
|
+
): Promise<OAuth2CodeChallenge> {
|
|
66
|
+
const verifier = generateSecureRandomString(length)
|
|
67
|
+
const data = new TextEncoder().encode(verifier)
|
|
68
|
+
const digest = await window.crypto.subtle.digest("SHA-256", data)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
verifier,
|
|
72
|
+
challenge: btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
73
|
+
.replace(/\+/g, "-")
|
|
74
|
+
.replace(/\//g, "_")
|
|
75
|
+
.replace(/=+$/, ""),
|
|
76
|
+
method: "S256",
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/utils/general.ts
CHANGED
|
@@ -1025,3 +1025,19 @@ export function excludeKeyPaths(
|
|
|
1025
1025
|
|
|
1026
1026
|
return exclude.length ? _excludeKeyPaths(obj, []) : obj
|
|
1027
1027
|
}
|
|
1028
|
+
|
|
1029
|
+
export function generateSecureRandomString(
|
|
1030
|
+
length: number,
|
|
1031
|
+
charSet: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
1032
|
+
) {
|
|
1033
|
+
// Create an array of 32-bit unsigned integers
|
|
1034
|
+
const randomValues = window.crypto.getRandomValues(new Uint8Array(length))
|
|
1035
|
+
|
|
1036
|
+
// Map the random values to characters from our string
|
|
1037
|
+
let result = ""
|
|
1038
|
+
for (let i = 0; i < length; i++) {
|
|
1039
|
+
result += charSet.charAt(randomValues[i] % charSet.length)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return result
|
|
1043
|
+
}
|
package/.prettierrc.json
DELETED
package/.vscode/launch.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"configurations": [
|
|
3
|
-
{
|
|
4
|
-
"args": [
|
|
5
|
-
"run",
|
|
6
|
-
"${relativeFile}"
|
|
7
|
-
],
|
|
8
|
-
"autoAttachChildProcesses": true,
|
|
9
|
-
"console": "integratedTerminal",
|
|
10
|
-
"name": "Vitest: Current File",
|
|
11
|
-
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
|
|
12
|
-
"request": "launch",
|
|
13
|
-
"skipFiles": [
|
|
14
|
-
"<node_internals>/**",
|
|
15
|
-
"**/node_modules/**"
|
|
16
|
-
],
|
|
17
|
-
"smartStep": true,
|
|
18
|
-
"type": "node"
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"version": "0.2.0"
|
|
22
|
-
}
|
package/.vscode/settings.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"[md]": {
|
|
3
|
-
"editor.tabSize": 4
|
|
4
|
-
},
|
|
5
|
-
"cSpell.words": [
|
|
6
|
-
"codeforlife",
|
|
7
|
-
"klass",
|
|
8
|
-
"ocado",
|
|
9
|
-
"kurono",
|
|
10
|
-
"pipenv"
|
|
11
|
-
],
|
|
12
|
-
"editor.codeActionsOnSave": {
|
|
13
|
-
"source.fixAll.eslint": "always",
|
|
14
|
-
"source.organizeImports": "never"
|
|
15
|
-
},
|
|
16
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
17
|
-
"editor.formatOnSave": true,
|
|
18
|
-
"editor.rulers": [
|
|
19
|
-
80
|
|
20
|
-
],
|
|
21
|
-
"editor.tabSize": 2,
|
|
22
|
-
"files.exclude": {
|
|
23
|
-
"**/*.tsbuildinfo": true
|
|
24
|
-
},
|
|
25
|
-
"javascript.format.semicolons": "remove",
|
|
26
|
-
"javascript.preferences.quoteStyle": "double",
|
|
27
|
-
"prettier.configPath": ".prettierrc.json",
|
|
28
|
-
"typescript.format.semicolons": "remove",
|
|
29
|
-
"typescript.preferences.quoteStyle": "double"
|
|
30
|
-
}
|