@hanzo/iam 0.6.1 → 0.6.2
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/dist/auth.d.ts +1 -1
- package/dist/auth.js +3 -3
- package/dist/auth.js.map +1 -1
- package/dist/betterauth.d.ts +67 -0
- package/dist/betterauth.d.ts.map +1 -0
- package/dist/betterauth.js +64 -0
- package/dist/betterauth.js.map +1 -0
- package/dist/browser.d.ts +84 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +224 -3
- package/dist/browser.js.map +1 -1
- package/dist/client.d.ts +11 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +39 -96
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/nextauth.d.ts +12 -10
- package/dist/nextauth.d.ts.map +1 -1
- package/dist/nextauth.js +12 -11
- package/dist/nextauth.js.map +1 -1
- package/dist/passport.d.ts +44 -0
- package/dist/passport.d.ts.map +1 -0
- package/dist/passport.js +67 -0
- package/dist/passport.js.map +1 -0
- package/dist/pkce.d.ts +1 -1
- package/dist/pkce.js +1 -1
- package/dist/react.d.ts +1 -87
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +20 -451
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +1 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +16 -7
- package/src/auth.ts +3 -3
- package/src/betterauth.ts +91 -0
- package/src/browser.ts +247 -3
- package/src/client.ts +47 -154
- package/src/index.ts +1 -2
- package/src/nextauth.ts +15 -13
- package/src/passport.ts +97 -0
- package/src/pkce.ts +1 -1
- package/src/react.ts +20 -627
- package/src/types.ts +1 -21
package/src/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core HTTP client for Hanzo IAM
|
|
2
|
+
* Core HTTP client for Hanzo IAM API.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type {
|
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
IamApiResponse,
|
|
8
8
|
IamUser,
|
|
9
9
|
IamOrganization,
|
|
10
|
-
IamInvitation,
|
|
11
10
|
IamProject,
|
|
12
11
|
OidcDiscovery,
|
|
13
12
|
TokenResponse,
|
|
@@ -184,6 +183,46 @@ export class IamClient {
|
|
|
184
183
|
}
|
|
185
184
|
}
|
|
186
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Resource Owner Password Credentials grant.
|
|
188
|
+
* Used for service-to-service auth, CLI login, and e2e tests.
|
|
189
|
+
*/
|
|
190
|
+
async passwordGrant(params: {
|
|
191
|
+
username: string;
|
|
192
|
+
password: string;
|
|
193
|
+
scope?: string;
|
|
194
|
+
}): Promise<TokenResponse> {
|
|
195
|
+
const discovery = await this.getDiscovery();
|
|
196
|
+
const body = new URLSearchParams({
|
|
197
|
+
grant_type: "password",
|
|
198
|
+
client_id: this.clientId,
|
|
199
|
+
username: params.username,
|
|
200
|
+
password: params.password,
|
|
201
|
+
scope: params.scope ?? "openid profile email phone",
|
|
202
|
+
});
|
|
203
|
+
if (this.clientSecret) {
|
|
204
|
+
body.set("client_secret", this.clientSecret);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(discovery.token_endpoint, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
213
|
+
body: body.toString(),
|
|
214
|
+
signal: controller.signal,
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
const text = await res.text().catch(() => "");
|
|
218
|
+
throw new IamApiError(res.status, `Password grant failed: ${text}`);
|
|
219
|
+
}
|
|
220
|
+
return (await res.json()) as TokenResponse;
|
|
221
|
+
} finally {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
187
226
|
/** Refresh an access token. */
|
|
188
227
|
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
|
189
228
|
const discovery = await this.getDiscovery();
|
|
@@ -253,76 +292,12 @@ export class IamClient {
|
|
|
253
292
|
|
|
254
293
|
/** List organizations (for the configured owner). */
|
|
255
294
|
async getOrganizations(token?: string): Promise<IamOrganization[]> {
|
|
256
|
-
// Build the user's org list from JWT claims.
|
|
257
|
-
// The user's "owner" field is the signup org (used for auth only).
|
|
258
|
-
// Their personal org (name == username) is their actual workspace.
|
|
259
|
-
// Additional orgs come from invitations (future: membership table).
|
|
260
|
-
const orgs: IamOrganization[] = [];
|
|
261
|
-
const orgNames = new Set<string>();
|
|
262
|
-
|
|
263
|
-
let signupOrg = "";
|
|
264
|
-
|
|
265
|
-
if (token) {
|
|
266
|
-
try {
|
|
267
|
-
let b64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
268
|
-
while (b64.length % 4) b64 += "=";
|
|
269
|
-
const payload = JSON.parse(atob(b64));
|
|
270
|
-
const userOwner = payload.owner as string;
|
|
271
|
-
const userName = payload.name as string;
|
|
272
|
-
|
|
273
|
-
signupOrg = userOwner;
|
|
274
|
-
const isAdmin = !!payload.isAdmin;
|
|
275
|
-
|
|
276
|
-
// Personal org (name == username) is the user's primary workspace.
|
|
277
|
-
if (userName && userName !== userOwner) {
|
|
278
|
-
orgs.push({
|
|
279
|
-
owner: "admin",
|
|
280
|
-
name: userName,
|
|
281
|
-
displayName: userName,
|
|
282
|
-
});
|
|
283
|
-
orgNames.add(userName);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Admin users also see their signup org (they manage it).
|
|
287
|
-
// Non-admin users only see their personal org.
|
|
288
|
-
if (isAdmin && userOwner && !orgNames.has(userOwner)) {
|
|
289
|
-
orgs.push({ owner: "admin", name: userOwner, displayName: userOwner });
|
|
290
|
-
orgNames.add(userOwner);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// If no personal org (username == owner), show the signup org as workspace
|
|
294
|
-
if (!orgNames.size && userOwner) {
|
|
295
|
-
orgs.push({ owner: "admin", name: userOwner, displayName: userOwner });
|
|
296
|
-
orgNames.add(userOwner);
|
|
297
|
-
}
|
|
298
|
-
} catch {
|
|
299
|
-
// JWT parse failed
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Try the API to get additional orgs the user was invited to.
|
|
304
|
-
// Exclude the signup org (it's just for auth, not a workspace).
|
|
305
295
|
const owner = this.orgName ?? "admin";
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (resp.data) {
|
|
312
|
-
for (const org of resp.data) {
|
|
313
|
-
// Skip the signup org — it's not a user workspace
|
|
314
|
-
if (org.name === signupOrg && orgNames.size > 0) continue;
|
|
315
|
-
if (!orgNames.has(org.name)) {
|
|
316
|
-
orgs.push(org);
|
|
317
|
-
orgNames.add(org.name);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
} catch {
|
|
322
|
-
// API failed — JWT-derived orgs are sufficient
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return orgs;
|
|
296
|
+
const resp = await this.request<IamApiResponse<IamOrganization[]>>(
|
|
297
|
+
"/api/get-organizations",
|
|
298
|
+
{ params: { owner }, token },
|
|
299
|
+
);
|
|
300
|
+
return resp.data ?? [];
|
|
326
301
|
}
|
|
327
302
|
|
|
328
303
|
/** Get a specific organization. */
|
|
@@ -342,7 +317,7 @@ export class IamClient {
|
|
|
342
317
|
userId: string,
|
|
343
318
|
token?: string,
|
|
344
319
|
): Promise<IamOrganization[]> {
|
|
345
|
-
//
|
|
320
|
+
// IAM returns orgs the user is a member of via the user's properties.
|
|
346
321
|
// We can also query via get-user and read their signupApplication/org.
|
|
347
322
|
const user = await this.getUser(userId, token);
|
|
348
323
|
if (!user) return [];
|
|
@@ -354,88 +329,6 @@ export class IamClient {
|
|
|
354
329
|
return org ? [org] : [];
|
|
355
330
|
}
|
|
356
331
|
|
|
357
|
-
/** Create a new organization. */
|
|
358
|
-
async createOrganization(
|
|
359
|
-
org: Partial<IamOrganization>,
|
|
360
|
-
token?: string,
|
|
361
|
-
): Promise<IamApiResponse<IamOrganization>> {
|
|
362
|
-
return this.request<IamApiResponse<IamOrganization>>(
|
|
363
|
-
"/api/add-organization",
|
|
364
|
-
{ method: "POST", body: org, token },
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Update an existing organization. */
|
|
369
|
-
async updateOrganization(
|
|
370
|
-
org: Partial<IamOrganization>,
|
|
371
|
-
token?: string,
|
|
372
|
-
): Promise<IamApiResponse<IamOrganization>> {
|
|
373
|
-
return this.request<IamApiResponse<IamOrganization>>(
|
|
374
|
-
"/api/update-organization",
|
|
375
|
-
{ method: "POST", body: org, token },
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** Delete an organization by owner and name. */
|
|
380
|
-
async deleteOrganization(
|
|
381
|
-
org: { owner: string; name: string },
|
|
382
|
-
token?: string,
|
|
383
|
-
): Promise<IamApiResponse<IamOrganization>> {
|
|
384
|
-
return this.request<IamApiResponse<IamOrganization>>(
|
|
385
|
-
"/api/delete-organization",
|
|
386
|
-
{ method: "POST", body: org, token },
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// -----------------------------------------------------------------------
|
|
391
|
-
// Invitation
|
|
392
|
-
// -----------------------------------------------------------------------
|
|
393
|
-
|
|
394
|
-
/** List invitations for an owner (organization). */
|
|
395
|
-
async getInvitations(
|
|
396
|
-
owner: string,
|
|
397
|
-
token?: string,
|
|
398
|
-
): Promise<IamInvitation[]> {
|
|
399
|
-
const resp = await this.request<IamApiResponse<IamInvitation[]>>(
|
|
400
|
-
"/api/get-invitations",
|
|
401
|
-
{ params: { owner }, token },
|
|
402
|
-
);
|
|
403
|
-
return resp.data ?? [];
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/** Create a new invitation. */
|
|
407
|
-
async createInvitation(
|
|
408
|
-
invitation: Partial<IamInvitation>,
|
|
409
|
-
token?: string,
|
|
410
|
-
): Promise<IamApiResponse<IamInvitation>> {
|
|
411
|
-
return this.request<IamApiResponse<IamInvitation>>(
|
|
412
|
-
"/api/add-invitation",
|
|
413
|
-
{ method: "POST", body: invitation, token },
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** Send an invitation by owner and name. */
|
|
418
|
-
async sendInvitation(
|
|
419
|
-
invitation: { owner: string; name: string },
|
|
420
|
-
token?: string,
|
|
421
|
-
): Promise<IamApiResponse<IamInvitation>> {
|
|
422
|
-
return this.request<IamApiResponse<IamInvitation>>(
|
|
423
|
-
"/api/send-invitation",
|
|
424
|
-
{ method: "POST", body: invitation, token },
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/** Verify an invitation code. */
|
|
429
|
-
async verifyInvitation(
|
|
430
|
-
code: string,
|
|
431
|
-
token?: string,
|
|
432
|
-
): Promise<IamApiResponse<IamInvitation>> {
|
|
433
|
-
return this.request<IamApiResponse<IamInvitation>>(
|
|
434
|
-
"/api/verify-invitation",
|
|
435
|
-
{ params: { code }, token },
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
332
|
// -----------------------------------------------------------------------
|
|
440
333
|
// Project
|
|
441
334
|
// -----------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* });
|
|
15
15
|
*
|
|
16
16
|
* const billing = new BillingClient({
|
|
17
|
-
* commerceUrl: "https://commerce
|
|
17
|
+
* commerceUrl: "https://commerce.hanzo.ai",
|
|
18
18
|
* });
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
@@ -43,7 +43,6 @@ export type {
|
|
|
43
43
|
IamJwtClaims,
|
|
44
44
|
IamUser,
|
|
45
45
|
IamOrganization,
|
|
46
|
-
IamInvitation,
|
|
47
46
|
IamProject,
|
|
48
47
|
Subscription,
|
|
49
48
|
Plan,
|
package/src/nextauth.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NextAuth.js provider for
|
|
2
|
+
* NextAuth.js / Auth.js provider for IAM (OIDC-based).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* so all Next.js apps can share one
|
|
4
|
+
* Provides a canonical NextAuth/Auth.js provider configuration
|
|
5
|
+
* so all Next.js apps can share one implementation.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
9
|
* // next-auth config
|
|
10
|
-
* import {
|
|
10
|
+
* import { IamProvider } from "@hanzo/iam/nextauth";
|
|
11
11
|
*
|
|
12
12
|
* export default NextAuth({
|
|
13
13
|
* providers: [
|
|
14
|
-
*
|
|
14
|
+
* IamProvider({
|
|
15
15
|
* serverUrl: process.env.IAM_SERVER_URL!,
|
|
16
16
|
* clientId: process.env.IAM_CLIENT_ID!,
|
|
17
17
|
* clientSecret: process.env.IAM_CLIENT_SECRET!,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @packageDocumentation
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
interface
|
|
26
|
+
export interface IamProfile extends Record<string, unknown> {
|
|
27
27
|
sub: string;
|
|
28
28
|
name: string;
|
|
29
29
|
email: string;
|
|
@@ -35,7 +35,7 @@ interface HanzoIamProfile extends Record<string, unknown> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* NextAuth.js / Auth.js compatible OAuth provider for
|
|
38
|
+
* NextAuth.js / Auth.js compatible OAuth provider for IAM.
|
|
39
39
|
*
|
|
40
40
|
* Uses standard OIDC well-known endpoint for automatic configuration.
|
|
41
41
|
* JWT id_token validation (issuer, audience, signature) is handled by
|
|
@@ -43,7 +43,7 @@ interface HanzoIamProfile extends Record<string, unknown> {
|
|
|
43
43
|
*
|
|
44
44
|
* Pass `checks: ["state", "pkce"]` in options for PKCE alignment.
|
|
45
45
|
*/
|
|
46
|
-
export function
|
|
46
|
+
export function IamProvider<P extends IamProfile>(
|
|
47
47
|
options: {
|
|
48
48
|
serverUrl: string;
|
|
49
49
|
clientId: string;
|
|
@@ -59,8 +59,8 @@ export function HanzoIamProvider<P extends HanzoIamProfile>(
|
|
|
59
59
|
const checks = options.checks ?? ["state"];
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
|
-
id: "
|
|
63
|
-
name: "
|
|
62
|
+
id: "iam",
|
|
63
|
+
name: "IAM",
|
|
64
64
|
type: "oauth",
|
|
65
65
|
wellKnown: `${issuer}/.well-known/openid-configuration`,
|
|
66
66
|
idToken: true,
|
|
@@ -88,6 +88,8 @@ export function HanzoIamProvider<P extends HanzoIamProfile>(
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
export
|
|
91
|
+
// Backwards-compatible aliases
|
|
92
|
+
/** @deprecated Use IamProvider instead */
|
|
93
|
+
export { IamProvider as HanzoIamProvider };
|
|
94
|
+
/** @deprecated Use IamProfile instead */
|
|
95
|
+
export type { IamProfile as HanzoIamProfile };
|
package/src/passport.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passport.js OAuth2 strategy factory for Hanzo IAM.
|
|
3
|
+
*
|
|
4
|
+
* Creates a pre-configured passport-oauth2 strategy that authenticates
|
|
5
|
+
* against hanzo.id with PKCE and fetches user info on callback.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import passport from "passport";
|
|
10
|
+
* import { createIamPassportStrategy } from "@hanzo/iam/passport";
|
|
11
|
+
*
|
|
12
|
+
* passport.use("iam", createIamPassportStrategy({
|
|
13
|
+
* serverUrl: "https://hanzo.id",
|
|
14
|
+
* clientId: "hanzo-kms-client-id",
|
|
15
|
+
* clientSecret: process.env.IAM_CLIENT_SECRET!,
|
|
16
|
+
* callbackUrl: "https://kms.hanzo.ai/api/v1/sso/oidc/callback",
|
|
17
|
+
* }));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { IamConfig } from "./types.js";
|
|
24
|
+
|
|
25
|
+
export interface IamPassportConfig extends IamConfig {
|
|
26
|
+
/** Full callback URL for OAuth2 redirect. */
|
|
27
|
+
callbackUrl: string;
|
|
28
|
+
/** OAuth2 scopes. Default: "openid profile email". */
|
|
29
|
+
scope?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IamPassportUser {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
refreshToken?: string;
|
|
35
|
+
userinfo: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a Passport OAuth2 strategy for Hanzo IAM.
|
|
40
|
+
*
|
|
41
|
+
* Requires `passport-oauth2` as a peer dependency.
|
|
42
|
+
* Returns an OAuth2Strategy instance ready to pass to `passport.use()`.
|
|
43
|
+
*
|
|
44
|
+
* The verify callback fetches userinfo from the IAM server and passes
|
|
45
|
+
* `{ accessToken, refreshToken, userinfo }` as the user object.
|
|
46
|
+
*/
|
|
47
|
+
export function createIamPassportStrategy(
|
|
48
|
+
config: IamPassportConfig,
|
|
49
|
+
): unknown {
|
|
50
|
+
// Dynamic import to keep passport-oauth2 as optional peer dep.
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const { Strategy: OAuth2Strategy } = require("passport-oauth2") as {
|
|
53
|
+
Strategy: new (
|
|
54
|
+
options: Record<string, unknown>,
|
|
55
|
+
verify: (...args: unknown[]) => void,
|
|
56
|
+
) => unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const baseUrl = config.serverUrl.replace(/\/+$/, "");
|
|
60
|
+
|
|
61
|
+
const verify = async (
|
|
62
|
+
...args: unknown[]
|
|
63
|
+
): Promise<void> => {
|
|
64
|
+
// passReqToCallback=true: (req, accessToken, refreshToken, profile, done)
|
|
65
|
+
const accessToken = args[1] as string;
|
|
66
|
+
const refreshToken = args[2] as string | undefined;
|
|
67
|
+
const done = args[4] as (err: Error | null, user?: IamPassportUser) => void;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${baseUrl}/oauth/userinfo`, {
|
|
71
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
return done(new Error(`IAM userinfo failed: ${res.status}`));
|
|
75
|
+
}
|
|
76
|
+
const userinfo = (await res.json()) as Record<string, unknown>;
|
|
77
|
+
done(null, { accessToken, refreshToken, userinfo });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return new OAuth2Strategy(
|
|
84
|
+
{
|
|
85
|
+
authorizationURL: `${baseUrl}/oauth/authorize`,
|
|
86
|
+
tokenURL: `${baseUrl}/oauth/token`,
|
|
87
|
+
clientID: config.clientId,
|
|
88
|
+
clientSecret: config.clientSecret ?? "",
|
|
89
|
+
callbackURL: config.callbackUrl,
|
|
90
|
+
scope: config.scope ?? "openid profile email",
|
|
91
|
+
state: true,
|
|
92
|
+
pkce: true,
|
|
93
|
+
passReqToCallback: true,
|
|
94
|
+
},
|
|
95
|
+
verify,
|
|
96
|
+
);
|
|
97
|
+
}
|
package/src/pkce.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PKCE (Proof Key for Code Exchange) utilities for browser-side OAuth2 flows.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* PKCE utilities for OAuth2 flows, using native Web Crypto API.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
function generateRandomString(length: number): string {
|