@schemavaults/auth-client-sdk 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.
Files changed (65) hide show
  1. package/README.md +14 -0
  2. package/dist/auth-client.d.ts +128 -0
  3. package/dist/auth-client.js +1190 -0
  4. package/dist/auth-client.js.map +1 -0
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/lib/acquire-access-token.d.ts +11 -0
  9. package/dist/lib/acquire-access-token.js +126 -0
  10. package/dist/lib/acquire-access-token.js.map +1 -0
  11. package/dist/lib/auth-client-events.d.ts +7 -0
  12. package/dist/lib/auth-client-events.js +4 -0
  13. package/dist/lib/auth-client-events.js.map +1 -0
  14. package/dist/lib/authenticate-url-encoder.d.ts +19 -0
  15. package/dist/lib/authenticate-url-encoder.js +41 -0
  16. package/dist/lib/authenticate-url-encoder.js.map +1 -0
  17. package/dist/lib/authentication-outcome-type.d.ts +3 -0
  18. package/dist/lib/authentication-outcome-type.js +15 -0
  19. package/dist/lib/authentication-outcome-type.js.map +1 -0
  20. package/dist/lib/credentials-schema/credentials-schema.d.ts +28 -0
  21. package/dist/lib/credentials-schema/credentials-schema.js +14 -0
  22. package/dist/lib/credentials-schema/credentials-schema.js.map +1 -0
  23. package/dist/lib/credentials-schema/index.d.ts +1 -0
  24. package/dist/lib/credentials-schema/index.js +2 -0
  25. package/dist/lib/credentials-schema/index.js.map +1 -0
  26. package/dist/lib/debugPrintTokensAsTable.d.ts +2 -0
  27. package/dist/lib/debugPrintTokensAsTable.js +14 -0
  28. package/dist/lib/debugPrintTokensAsTable.js.map +1 -0
  29. package/dist/lib/debugPrintUserDataAsTable.d.ts +2 -0
  30. package/dist/lib/debugPrintUserDataAsTable.js +12 -0
  31. package/dist/lib/debugPrintUserDataAsTable.js.map +1 -0
  32. package/dist/lib/is-private-beta.d.ts +3 -0
  33. package/dist/lib/is-private-beta.js +24 -0
  34. package/dist/lib/is-private-beta.js.map +1 -0
  35. package/dist/lib/isValidRefreshToken.d.ts +2 -0
  36. package/dist/lib/isValidRefreshToken.js +13 -0
  37. package/dist/lib/isValidRefreshToken.js.map +1 -0
  38. package/dist/lib/send-authenticate-request.d.ts +2 -0
  39. package/dist/lib/send-authenticate-request.js +133 -0
  40. package/dist/lib/send-authenticate-request.js.map +1 -0
  41. package/dist/types/IAuthClientConstructorOptions.d.ts +14 -0
  42. package/dist/types/IAuthClientConstructorOptions.js +2 -0
  43. package/dist/types/IAuthClientConstructorOptions.js.map +1 -0
  44. package/dist/types/ISchemaVaultsAuthClient.d.ts +59 -0
  45. package/dist/types/ISchemaVaultsAuthClient.js +2 -0
  46. package/dist/types/ISchemaVaultsAuthClient.js.map +1 -0
  47. package/dist/types/ISchemaVaultsAuthClientAdapter.d.ts +62 -0
  48. package/dist/types/ISchemaVaultsAuthClientAdapter.js +2 -0
  49. package/dist/types/ISchemaVaultsAuthClientAdapter.js.map +1 -0
  50. package/dist/types/ISendAuthenticateRequestOptions.d.ts +13 -0
  51. package/dist/types/ISendAuthenticateRequestOptions.js +2 -0
  52. package/dist/types/ISendAuthenticateRequestOptions.js.map +1 -0
  53. package/dist/types/UserData.d.ts +1 -0
  54. package/dist/types/UserData.js +2 -0
  55. package/dist/types/UserData.js.map +1 -0
  56. package/dist/types/acquire-access-token-options.d.ts +8 -0
  57. package/dist/types/acquire-access-token-options.js +2 -0
  58. package/dist/types/acquire-access-token-options.js.map +1 -0
  59. package/dist/types/credentials.d.ts +3 -0
  60. package/dist/types/credentials.js +2 -0
  61. package/dist/types/credentials.js.map +1 -0
  62. package/dist/types/framework-adapter-interface.d.ts +36 -0
  63. package/dist/types/framework-adapter-interface.js +2 -0
  64. package/dist/types/framework-adapter-interface.js.map +1 -0
  65. package/package.json +42 -0
@@ -0,0 +1,1190 @@
1
+ import AuthenticateURLEncoder from "./lib/authenticate-url-encoder";
2
+ import { PKCE_ProofKeyManager, requestTokensResultSchema, authorizationCodePOSTbody, refreshTokenPOSTbody, audienceSchema, } from "@schemavaults/auth-common";
3
+ import { sendAuthenticateRequest } from "./lib/send-authenticate-request";
4
+ import { appIdSchema, SCHEMAVAULTS_AUTH_APP_DEFINITION, schemaVaultsAppEnvironmentSchema, } from "@schemavaults/app-definitions";
5
+ import isPrivateBeta from "./lib/is-private-beta";
6
+ import debugPrintTokensAsTable from "./lib/debugPrintTokensAsTable";
7
+ import debugPrintUserDataAsTable from "./lib/debugPrintUserDataAsTable";
8
+ /**
9
+ * The SchemaVaultsAuthClient is a client SDK for the SchemaVaults Auth Server
10
+ * It is used to authenticate users, store tokens, and manage user data
11
+ * @name SchemaVaultsAuthClient
12
+ * @alias AuthClient
13
+ * @author jalexw
14
+ * @implements ISchemaVaultsAuthClient
15
+ */
16
+ export class SchemaVaultsAuthClient extends EventTarget {
17
+ _adapter;
18
+ environment;
19
+ // URL of @schemavaults/auth-server
20
+ _authServerUri;
21
+ // Where to send user once they are logged in & have acquired a refresh token
22
+ _successful_authentication_redirect_uri;
23
+ // Where to send the user after they log out
24
+ _successful_logout_redirect_uri;
25
+ // Where to send the user to trade an authorization code + code verifier for a refresh token
26
+ _authorize_uri;
27
+ _app_id; // Undefined on the auth-server, set from 3rd party client
28
+ listeners = new Map();
29
+ DEBUG;
30
+ get debug() {
31
+ return this.DEBUG;
32
+ }
33
+ _default_audiences;
34
+ // Initialize the auth client
35
+ constructor(opts) {
36
+ // Set up event emitter
37
+ super();
38
+ // First, parse the environment
39
+ const parsed_app_env = schemaVaultsAppEnvironmentSchema.safeParse(opts.app_env);
40
+ if (!parsed_app_env.success) {
41
+ console.error(parsed_app_env.error);
42
+ throw new Error("Invalid app environment option (`app_env`) for auth client to run in!");
43
+ }
44
+ else {
45
+ this.environment = parsed_app_env.data;
46
+ }
47
+ if (typeof opts.debug === "boolean") {
48
+ this.DEBUG = opts.debug;
49
+ }
50
+ else {
51
+ // debug state was not explicitly supplied!
52
+ if (this.environment === "development" ||
53
+ this.environment === "test" ||
54
+ this.environment === "staging") {
55
+ this.DEBUG = true;
56
+ }
57
+ else if (isPrivateBeta()) {
58
+ this.DEBUG = true;
59
+ }
60
+ else {
61
+ this.DEBUG = false;
62
+ }
63
+ }
64
+ if (this.DEBUG) {
65
+ console.log("[SchemaVaultsAuthClient] Initializing with DEBUG = true!");
66
+ }
67
+ // Set up framework adapter
68
+ this._adapter = opts.adapter;
69
+ // Auth Server URI is a required prop
70
+ if (typeof opts.auth_server_uri !== "string" || !opts.auth_server_uri) {
71
+ throw new Error("`auth_server_uri` is a required option to initalize the SchemaVaultsAuthClient");
72
+ }
73
+ // Set up auth client options
74
+ this._authServerUri = opts.auth_server_uri;
75
+ if (this.environment === "production" || this.environment === "staging") {
76
+ // Ensure HTTPS
77
+ if (!opts.auth_server_uri.startsWith("https://")) {
78
+ throw new Error("Auth Server URI must use HTTPS in production!");
79
+ }
80
+ }
81
+ if (this.DEBUG) {
82
+ console.log(`[SchemaVaultsAuthClient] auth_server_uri: '${this._authServerUri}'`);
83
+ }
84
+ if (this.DEBUG) {
85
+ let currentUrl = undefined;
86
+ try {
87
+ // @ts-expect-error Window is not defined in Node.js, this may not be in a browser environment
88
+ if (typeof window === "object" && !!window) {
89
+ // @ts-expect-error Window is not defined in Node.js, this may not be in a browser environment
90
+ currentUrl = window.location.href;
91
+ }
92
+ }
93
+ catch (e) {
94
+ void e; /** no-op */
95
+ }
96
+ if (typeof currentUrl === "string") {
97
+ // Successfully loaded current url
98
+ console.log(`[SchemaVaultsAuthClient] Initializing auth client${typeof currentUrl === "string"
99
+ ? ` (from current url: "${currentUrl}")`
100
+ : ""} for auth server: `, this._authServerUri);
101
+ }
102
+ }
103
+ // Get frontend client app ID
104
+ const parsed_app_id = appIdSchema.safeParse(opts.app_id);
105
+ if (!parsed_app_id.success) {
106
+ throw new Error("Invalid app ID received for @schemavaults/auth-client-sdk initialization");
107
+ }
108
+ this._app_id = parsed_app_id.data;
109
+ console.assert(typeof this._app_id === "string", "App ID that this auth client is running for should be a string after being parsed!");
110
+ // Get redirect URLS (optional, non-web clients will not be redirected if not provided)
111
+ this._successful_authentication_redirect_uri =
112
+ opts.successful_authentication_redirect_uri;
113
+ if (!this._successful_authentication_redirect_uri) {
114
+ throw new Error(`'successful_authentication_redirect_uri' is required for PKCE login flow`);
115
+ }
116
+ this._successful_logout_redirect_uri = opts.successful_logout_redirect_uri;
117
+ if (typeof opts.authorize_uri !== "string" &&
118
+ typeof opts.authorize_uri !== "undefined") {
119
+ throw new Error(`Expected 'authorize_uri' to be a string or undefined, received type '${typeof opts.authorize_uri}'`);
120
+ }
121
+ this._authorize_uri = opts.authorize_uri;
122
+ // Get default audiences
123
+ // E.g. the web app has https://api.schemavaults.com('s app ID) as an audience
124
+ this._default_audiences = opts.default_audiences ?? [];
125
+ // Set up auth state change listener
126
+ this.addEventListener("authStateChanged", this.handleAuthStateChange.bind(this));
127
+ if (this.DEBUG) {
128
+ console.log(`[SchemaVaultsAuthClient] Successfully initialized! End of constructor()`);
129
+ }
130
+ }
131
+ /**
132
+ * @name handleAuthStateChange()
133
+ * @description Loops over attached listeners and calls each one
134
+ * @see this.listeners
135
+ */
136
+ handleAuthStateChange() {
137
+ if (this.DEBUG) {
138
+ console.log("[handleAuthStateChange] Triggering listeners...");
139
+ }
140
+ for (const [listener_id, listener_ref] of this.listeners.entries()) {
141
+ if (listener_id !== listener_ref.id) {
142
+ throw new Error("[handleAuthStateChange] Listener ID mismatch");
143
+ }
144
+ try {
145
+ if (this.debug) {
146
+ console.log(`[handleAuthStateChange] Triggering listener with ID "${listener_id}"...`);
147
+ }
148
+ listener_ref.listener();
149
+ }
150
+ catch (e) {
151
+ console.error(`Error thrown from onAuthStateChange listener with ID "${listener_id}":`, e);
152
+ throw new Error(`[handleAuthStateChange] Error thrown from onAuthStateChange listener with ID "${listener_id}"`);
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * @name adapter
158
+ * @description Returns the adapter instance used by the auth client.
159
+ * @type ISchemaVaultsAuthClientAdapter
160
+ */
161
+ get adapter() {
162
+ return this._adapter;
163
+ }
164
+ get app_id() {
165
+ if (!this._app_id || typeof this._app_id !== "string") {
166
+ throw new Error("Frontend client application ID not set for auth client!");
167
+ }
168
+ return this._app_id;
169
+ }
170
+ // PKCE: Proof Key for Code Exchange
171
+ // https://datatracker.ietf.org/doc/html/rfc7636
172
+ // The client generates a code verifier and a code challenge
173
+ // The code verifier is stored securely within the client
174
+ // The code challenge is sent to the auth server
175
+ // The auth server will hash the code challenge and compare it to the code verifier
176
+ storeCodeVerifier(code_verifier, challenge_time) {
177
+ // Store the code verifier in a secure location
178
+ // This should be kept securely within the SDK
179
+ // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
180
+ const now = Date.now();
181
+ if (!challenge_time || typeof challenge_time !== "number") {
182
+ throw new Error("Invalid challenge_time; not a number");
183
+ }
184
+ else if (challenge_time > now) {
185
+ throw new Error("Invalid challenge_time; in the future");
186
+ }
187
+ // Store within a cookie that can only be accessed by the SDK
188
+ this.adapter.storeCodeVerifier(code_verifier, challenge_time);
189
+ return;
190
+ }
191
+ // Load the code verifier from a secure location
192
+ loadCodeVerifier(challenge_time) {
193
+ const now = Date.now();
194
+ if (!challenge_time || typeof challenge_time !== "number") {
195
+ throw new Error("Invalid challenge_time; not a number");
196
+ }
197
+ else if (challenge_time > now) {
198
+ throw new Error("Invalid challenge_time; in the future");
199
+ }
200
+ else if (now - challenge_time > PKCE_ProofKeyManager.max_age) {
201
+ throw new Error("Code verifier has expired");
202
+ }
203
+ if (this.environment === "development") {
204
+ console.log("[SchemaVaultsAuthClient::loadCodeVerifier()] Loading code verifier...");
205
+ }
206
+ const code_verifier = this.adapter.loadCodeVerifier(challenge_time);
207
+ if (typeof code_verifier === "string" &&
208
+ code_verifier.startsWith("deleted-at-")) {
209
+ throw new Error("A code verifier with that challenge time has already been used & deleted!");
210
+ }
211
+ if (this.environment === "development") {
212
+ if (code_verifier) {
213
+ console.log("[SchemaVaultsAuthClient::loadCodeVerifier()] Loaded code verifier:", code_verifier);
214
+ }
215
+ else {
216
+ console.warn("[SchemaVaultsAuthClient::loadCodeVerifier()] Failed to load code verifier!");
217
+ }
218
+ }
219
+ if (!code_verifier)
220
+ return null;
221
+ return code_verifier;
222
+ }
223
+ async generateCodeChallenge(challenge_time = Date.now()) {
224
+ const code_verifier = PKCE_ProofKeyManager.createCodeVerifier(challenge_time);
225
+ const pkce = new PKCE_ProofKeyManager(code_verifier);
226
+ const code_challenge = await pkce.getCodeChallenge();
227
+ if (typeof code_challenge.challenge_time !== "number") {
228
+ throw new Error("Expected challenge_time to be set (from input code_verifier");
229
+ }
230
+ else if (typeof code_challenge.code_challenge_method !== "string" ||
231
+ code_challenge.code_challenge_method !== "S256") {
232
+ throw new Error("Expected code_challenge_method to be set to S256");
233
+ }
234
+ code_challenge.code_challenge_method;
235
+ this.storeCodeVerifier(code_verifier.code_verifier, code_challenge.challenge_time);
236
+ return code_challenge;
237
+ }
238
+ async authenticateWithRedirect(type) {
239
+ if (this.DEBUG) {
240
+ console.log(`[SchemaVaultsAuthClient] Authenticating with redirect (type "${type}")...`);
241
+ }
242
+ // This should be kept securely within the SDK
243
+ let code_verifier;
244
+ try {
245
+ code_verifier = PKCE_ProofKeyManager.createCodeVerifier();
246
+ }
247
+ catch (e) {
248
+ console.error("Failed to create code verifier to initialize PKCE challenge process: ", e);
249
+ throw new Error("Failed to create code verifier to initialize PKCE challenge process");
250
+ }
251
+ // Do some validation
252
+ if (typeof code_verifier !== "object" || !code_verifier) {
253
+ throw new TypeError("Expected generated 'code_verifier' to be an object!");
254
+ }
255
+ else if (typeof code_verifier.challenge_time !== "number") {
256
+ throw new TypeError("Expected generated 'code_verifier.challenge_time' to be a number!");
257
+ }
258
+ else if (typeof code_verifier.code_verifier !== "string") {
259
+ throw new TypeError("Expected generated 'code_verifier.code_verifier' to be a string!");
260
+ }
261
+ // This is sent to the auth server-- it's a hash of the code verifier
262
+ // https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
263
+ let code_challenge;
264
+ try {
265
+ const new_code_challenge = await PKCE_ProofKeyManager.createCodeChallenge(code_verifier);
266
+ code_challenge = new_code_challenge;
267
+ }
268
+ catch (e) {
269
+ console.error("Auth client failed to create code challenge from code verifier object: ", e);
270
+ const errMsg = e instanceof Error && typeof e.message === "string"
271
+ ? e.message
272
+ : "Auth client encountered an unknown error while creating code challenge";
273
+ throw new Error(`Auth client failed to create code challenge from code verifier object (error: ${errMsg})`);
274
+ }
275
+ // Validate code challenge a bit
276
+ if (typeof code_challenge.challenge_time !== "number") {
277
+ throw new Error("Expected challenge_time to be set (from input code_verifier)");
278
+ }
279
+ else if (typeof code_challenge.code_challenge_method !== "string" ||
280
+ code_challenge.code_challenge_method !== "S256") {
281
+ throw new Error("Expected code_challenge_method to be set to S256");
282
+ }
283
+ else if (typeof code_challenge.code_challenge !== "string") {
284
+ throw new Error("Expected code_challenge to be set");
285
+ }
286
+ // Store the code verifier in a secure location
287
+ try {
288
+ this.storeCodeVerifier(code_verifier.code_verifier, code_challenge.challenge_time);
289
+ }
290
+ catch (e) {
291
+ console.error("Failed to store code verifier: ", e);
292
+ throw new Error("Failed to store code verifier!");
293
+ }
294
+ // If the authentication is successful, the auth server will redirect the user back to the client
295
+ // and the code_verifier will be used to prove that the client initiating the flow is the same as the client that the authorization server issued the code to
296
+ const app_id = this.app_id;
297
+ if (!app_id) {
298
+ console.error("App ID not set, but required for PKCE flow");
299
+ throw new Error("App ID not set, but required for PKCE flow");
300
+ }
301
+ // The user is about to be redirected to auth server. Where should they be redirected back to this app? (for PKCE flow)
302
+ const redirect_uri = this.authorize_uri;
303
+ if (typeof redirect_uri !== "string") {
304
+ throw new Error("A URL to redirect to when authentication is successful was not provided. Required for PKCE flow.");
305
+ }
306
+ if (this.DEBUG) {
307
+ console.log("[SchemaVaultsAuthClient] Attempting to build URL to open from client in order to start OAuth 2.0 PKCE flow with SchemaVaults Auth Server...");
308
+ }
309
+ // Redirect the user to the auth server
310
+ let authenticate_url;
311
+ try {
312
+ authenticate_url = AuthenticateURLEncoder.encode({
313
+ type,
314
+ code_challenge: code_challenge,
315
+ redirect_uri,
316
+ app_id,
317
+ auth_server_uri: this._authServerUri,
318
+ app_env: this.environment,
319
+ });
320
+ }
321
+ catch (e) {
322
+ console.error("Failed to build authenticate URL: ", e);
323
+ throw new Error("Failed to build authenticate URL (i.e. where to login/register url not found)");
324
+ }
325
+ if (this.DEBUG) {
326
+ console.log("[SchemaVaultsAuthClient] Redirecting to authenticate URL: ", authenticate_url);
327
+ }
328
+ try {
329
+ await this.adapter.redirect(authenticate_url);
330
+ return;
331
+ }
332
+ catch (e) {
333
+ console.error("Failed to redirect to authentication server using client adapter: ", e);
334
+ throw new Error("Failed to redirect to authentication server");
335
+ }
336
+ }
337
+ async login() {
338
+ if (this.DEBUG) {
339
+ console.log("[SchemaVaultsAuthClient] Attempting to sign in with redirect...");
340
+ }
341
+ try {
342
+ return await this.authenticateWithRedirect("login");
343
+ }
344
+ catch (e) {
345
+ console.error("Failed to authenticate with redirect to login: ", e);
346
+ if (e instanceof Error)
347
+ throw e;
348
+ throw new Error("Failed to authenticate with redirect to login!");
349
+ }
350
+ } // login()
351
+ async register() {
352
+ if (this.DEBUG) {
353
+ console.log("[SchemaVaultsAuthClient] Attempting to register with redirect...");
354
+ }
355
+ try {
356
+ return await this.authenticateWithRedirect("register");
357
+ }
358
+ catch (e) {
359
+ console.error("Failed to authenticate with redirect to register: ", e);
360
+ if (e instanceof Error)
361
+ throw e;
362
+ throw new Error("Failed to authenticate with redirect to register!");
363
+ }
364
+ } // register()
365
+ triggerAuthStateChanged() {
366
+ const eventType = "authStateChanged";
367
+ const changeEvent = new Event(eventType);
368
+ if (this.DEBUG) {
369
+ console.log(`[SchemaVaultsAuthClient] triggerAuthStateChanged() - Dispatching event of type ${eventType}`);
370
+ }
371
+ this.dispatchEvent(changeEvent);
372
+ } // triggerAuthStateChanged()
373
+ storeMultipleAccessTokens(access_tokens) {
374
+ const newAccessTokenAudiences = Object.keys(access_tokens);
375
+ newAccessTokenAudiences.forEach((audience) => {
376
+ // store each access token
377
+ const accessToken = access_tokens[audience];
378
+ if (!accessToken) {
379
+ throw new TypeError(`Missing access token for audience "${audience}"`);
380
+ }
381
+ if (typeof accessToken === "object" && accessToken.type === "access") {
382
+ if (audience !== accessToken.aud) {
383
+ throw new Error("Record key does not match access token audience field");
384
+ }
385
+ this.storeAccessToken(audience, accessToken);
386
+ return;
387
+ }
388
+ else if (typeof accessToken === "string" &&
389
+ accessToken === "AS_HTTP_ONLY_COOKIE") {
390
+ throw new Error("Unimplemented; handling for HTTP-only Cookie Access Tokens is not implemented yet!");
391
+ }
392
+ else {
393
+ throw new TypeError(`Invalid access token type for audience "${audience}"`);
394
+ }
395
+ });
396
+ }
397
+ /**
398
+ * @name isClientForAuthServer
399
+ * @description Determines whether this client is running on the frontend of the authentication server app
400
+ * (only the auth server can acquire access tokens for the auth server apis, as a security feature)
401
+ */
402
+ get isClientForAuthServer() {
403
+ if (this.app_id === SCHEMAVAULTS_AUTH_APP_DEFINITION.app_id) {
404
+ return true;
405
+ }
406
+ return false;
407
+ }
408
+ get defaultTokenAudiences() {
409
+ if (this.DEBUG) {
410
+ console.log("[SchemaVaultsAuthClient] Loading default token audiences...");
411
+ }
412
+ let defaults = [];
413
+ if (this.isClientForAuthServer) {
414
+ defaults.push(SCHEMAVAULTS_AUTH_APP_DEFINITION.app_id);
415
+ }
416
+ defaults.push(...this._default_audiences);
417
+ if (defaults.length === 0) {
418
+ console.warn("[SchemaVaultsAuthClient] No default token audiences set for auth client!");
419
+ return [];
420
+ }
421
+ const parsed = audienceSchema.safeParse(defaults.length === 1 ? defaults[0] : defaults);
422
+ if (!parsed.success) {
423
+ console.error("Failed to validate list of token audiences: ", parsed.error);
424
+ throw new Error("Failed to acquire list of default token audiences. You want to acquire access tokens without sending them anywhere? Get out of here goofball");
425
+ }
426
+ const output = parsed.data;
427
+ if (this.DEBUG) {
428
+ console.log("[SchemaVaultsAuthClient] Loaded default token audiences:", output);
429
+ }
430
+ return output;
431
+ }
432
+ async loadSavedAuthorizationCodeVerifiers() {
433
+ const codeVerifiers = this.adapter.loadCodeVerifiers();
434
+ return codeVerifiers;
435
+ } // loadSavedAuthorizationCodeVerifiers()
436
+ async handleSuccessfulAuthentication(authorization_code, challenge_time, code_verifier) {
437
+ const debug = this.DEBUG;
438
+ if (debug) {
439
+ console.log("[SchemaVaultsAuthClient::handleSuccessfulAuthentication]" +
440
+ " " +
441
+ "Handling successful authentication...");
442
+ }
443
+ if (!authorization_code) {
444
+ throw new Error("Missing authorization code");
445
+ }
446
+ else if (typeof authorization_code !== "string" ||
447
+ authorization_code.length === 0) {
448
+ throw new TypeError("Expected 'authorization_code' to be a non-empty string!");
449
+ }
450
+ if (!challenge_time || typeof challenge_time !== "number") {
451
+ throw new Error("Invalid challenge_time");
452
+ }
453
+ const time_elapsed_since_challenge_time = Date.now() - challenge_time;
454
+ if (time_elapsed_since_challenge_time <= 0) {
455
+ throw new Error("Expected challenge time to be in the past");
456
+ }
457
+ if (time_elapsed_since_challenge_time > PKCE_ProofKeyManager.max_age) {
458
+ console.error("[SchemaVaultsAuthClient::handleSuccessfulAuthentication] Code verifier has expired based on challenge time");
459
+ if (this.debug) {
460
+ try {
461
+ console.table({
462
+ challenge_time,
463
+ current_time: Date.now(),
464
+ time_elapsed: time_elapsed_since_challenge_time,
465
+ max_age: PKCE_ProofKeyManager.max_age,
466
+ });
467
+ }
468
+ catch (e) {
469
+ void e; /** no-op */
470
+ }
471
+ }
472
+ throw new Error("Code verifier has expired");
473
+ }
474
+ // The auth server will redirect the user back to the client
475
+ // The client will have a code in the query parameters
476
+ // The client will use the code to get an access token
477
+ // PKCE: The client will use the code_verifier to prove that it is the same client
478
+ if (debug) {
479
+ console.log("[SchemaVaultsAuthClient] " +
480
+ "Attempting to load code verifier to prove authorization code validity...");
481
+ }
482
+ const cached_code_verifier = code_verifier ?? this.loadCodeVerifier(challenge_time);
483
+ if (!cached_code_verifier) {
484
+ const errorMessage = `[SchemaVaultsAuthClient] Failed to load code_verifier at challenge_time=${challenge_time}`;
485
+ console.error(errorMessage);
486
+ throw new Error(errorMessage);
487
+ }
488
+ cached_code_verifier;
489
+ const shouldClearCodeVerifierAfterLoad = this.environment !== "development";
490
+ if (shouldClearCodeVerifierAfterLoad) {
491
+ // Clear the code verifier from storage
492
+ try {
493
+ if (debug) {
494
+ console.log("[SchemaVaultsAuthClient] " +
495
+ "Code verifier was retrieved from storage, now clearing code verifier at challenge time: ", challenge_time);
496
+ }
497
+ this.adapter.clearCodeVerifier(challenge_time);
498
+ if (debug) {
499
+ console.log("[SchemaVaultsAuthClient] Cleared code verifiers from storage");
500
+ }
501
+ }
502
+ catch (e) {
503
+ console.error("[SchemaVaultsAuthClient] Failed to clear code verifiers: ", e);
504
+ if (debug) {
505
+ throw new Error("Failed to clear code verifiers");
506
+ }
507
+ }
508
+ }
509
+ else {
510
+ if (debug) {
511
+ console.log("[SchemaVaultsAuthClient] Not attempting to clear code verifiers in this app environment...");
512
+ }
513
+ }
514
+ // Get the endpoint to exchange the authorization code for an access token
515
+ // https://datatracker.ietf.org/doc/html/rfc7636#section-4.5
516
+ const token_endpoint = `${this.auth_server_uri}/api/auth/token/authorization_code`;
517
+ if (debug) {
518
+ console.log("[SchemaVaultsAuthClient::handleSuccessfulAuthentication()] Token Endpoint: ", token_endpoint);
519
+ }
520
+ const client_app_id = this.app_id;
521
+ if (debug) {
522
+ console.log("[SchemaVaultsAuthClient::handleSuccessfulAuthentication()] Client App ID: ", client_app_id);
523
+ }
524
+ let audience = this.defaultTokenAudiences;
525
+ if (debug) {
526
+ console.log("[SchemaVaultsAuthClient::handleSuccessfulAuthentication()] Initial access token audience(s): ", audience);
527
+ }
528
+ if (!audience || (Array.isArray(audience) && audience.length === 0)) {
529
+ console.warn("[SchemaVaultsAuthClient::handleSuccessfulAuthentication()] No access token audience(s) set");
530
+ audience = [];
531
+ }
532
+ // Exchange the authorization code for an access token
533
+ let request_body;
534
+ try {
535
+ const parsed = await authorizationCodePOSTbody.safeParseAsync({
536
+ grant_type: "authorization_code",
537
+ code: authorization_code,
538
+ code_verifier: cached_code_verifier,
539
+ client_app_id,
540
+ audience,
541
+ challenge_time,
542
+ });
543
+ if (!parsed.success)
544
+ throw parsed.error;
545
+ request_body = parsed.data;
546
+ }
547
+ catch (e) {
548
+ console.error(e);
549
+ throw new Error("Failed to prepare request body for authorization grant request");
550
+ }
551
+ // Send the request to the auth server
552
+ // The auth server will hash the code_verifier and compare it to the code_challenge
553
+ let response;
554
+ try {
555
+ if (this.debug) {
556
+ console.log(`[SchemaVaultsAuthClient] Exchanging authorization code for access token; sending req body to token endpoint: "${token_endpoint}"`, request_body);
557
+ }
558
+ response = await this.adapter.sendPOSTRequest(token_endpoint, request_body, {});
559
+ if (this.debug) {
560
+ console.log("[SchemaVaultsAuthClient] Received response in attempt to exchange authorization code for access token: ", response);
561
+ }
562
+ }
563
+ catch (e) {
564
+ console.error("Failed to exchange authorization code for access token:", e);
565
+ throw new Error("Failed to exchange authorization code for access token");
566
+ }
567
+ if (!response || !response.ok || response.status !== 200) {
568
+ const errorMsg = "Failed to exchange authorization code for access token";
569
+ console.error(errorMsg);
570
+ throw new Error(errorMsg);
571
+ }
572
+ if (this.DEBUG) {
573
+ console.log("[SchemaVaultsAuthClient::handleSuccessfulAuthentication()] " +
574
+ "Successfully exchanged authorization code for token(s)");
575
+ }
576
+ let access_tokens;
577
+ let refresh_token;
578
+ let user;
579
+ try {
580
+ const tokens_data = await requestTokensResultSchema.safeParseAsync(response.data);
581
+ if (!tokens_data.success) {
582
+ console.error("[SchemaVaultsAuthClient::handleSuccessfulAuthentication()] " +
583
+ "Failed to parse tokens from auth server response:", tokens_data.error);
584
+ throw new Error("Failed to parse tokens from auth server response");
585
+ }
586
+ else if (!tokens_data.data.success) {
587
+ throw new Error(tokens_data.data.message);
588
+ }
589
+ if (this.DEBUG) {
590
+ console.log("[SchemaVaultsAuthClient::handleSuccessfulAuthentication()] Success response data: ", tokens_data.data);
591
+ }
592
+ const { tokens, userData } = tokens_data.data;
593
+ if (!tokens) {
594
+ console.error("Did not receive tokens in response from auth server");
595
+ throw new Error("Did not receive tokens in response from auth server");
596
+ }
597
+ if (!tokens.access) {
598
+ console.error("Did not receive any access tokens in response from auth server");
599
+ throw new Error("Did not receive any access tokens in response from auth server");
600
+ }
601
+ if (!tokens.refresh ||
602
+ (typeof tokens.refresh !== "object" &&
603
+ typeof tokens.refresh !== "string")) {
604
+ console.error("Did not receive (valid) refresh token in response from auth server.", `Type: ${typeof tokens.refresh}`, tokens.refresh);
605
+ throw new Error("Did not receive refresh token in response from auth server");
606
+ }
607
+ if (this.debug) {
608
+ debugPrintTokensAsTable(tokens);
609
+ }
610
+ access_tokens = tokens.access;
611
+ refresh_token = tokens.refresh;
612
+ if (!userData) {
613
+ console.error("Did not receive user data in response from auth server");
614
+ throw new Error("Did not receive user data in response from auth server");
615
+ }
616
+ else {
617
+ if (this.debug) {
618
+ debugPrintUserDataAsTable(userData);
619
+ }
620
+ }
621
+ user = userData;
622
+ }
623
+ catch (e) {
624
+ let errorMessage = "Unknown error";
625
+ if (e instanceof Error) {
626
+ errorMessage = e.message;
627
+ }
628
+ console.error("Failed to parse tokens response: ", errorMessage);
629
+ throw new Error(`Failed to parse tokens response: ${errorMessage}`);
630
+ }
631
+ // Store refresh token
632
+ const doStoreReceivedRefreshToken = () => {
633
+ if (typeof refresh_token === "object" &&
634
+ refresh_token.type === "refresh") {
635
+ try {
636
+ if (debug) {
637
+ console.log("[SchemaVaultsAuthClient] Storing refresh token...");
638
+ }
639
+ this.storeRefreshToken(refresh_token);
640
+ if (debug) {
641
+ console.log("[SchemaVaultsAuthClient] Stored refresh token!");
642
+ }
643
+ }
644
+ catch (e) {
645
+ console.error(e);
646
+ throw new Error("Failed to store refresh token");
647
+ }
648
+ }
649
+ else if (typeof refresh_token === "string" &&
650
+ refresh_token === "AS_HTTP_ONLY_COOKIE") {
651
+ this.assertHttpOnlyRefreshTokenCookieHasAccompanyingMarkerCookie();
652
+ if (debug) {
653
+ console.log("[SchemaVaultsAuthClient] Detected HTTP-only cookie refresh token (accompanying-cookie).");
654
+ }
655
+ return;
656
+ }
657
+ else {
658
+ throw new TypeError("Invalid type for refresh token!");
659
+ }
660
+ };
661
+ doStoreReceivedRefreshToken();
662
+ // Store access tokens
663
+ this.storeMultipleAccessTokens(access_tokens);
664
+ try {
665
+ if (debug) {
666
+ console.log("[SchemaVaultsAuthClient] Storing user data...");
667
+ }
668
+ this.storeUserData(user);
669
+ if (debug) {
670
+ console.log("[SchemaVaultsAuthClient] Stored user data.");
671
+ }
672
+ }
673
+ catch (e) {
674
+ console.error("Failed to store user data: ", e);
675
+ throw new Error("Failed to store user data");
676
+ }
677
+ if (debug) {
678
+ console.log("[SchemaVaultsAuthClient] Triggering auth state changed!");
679
+ }
680
+ this.triggerAuthStateChanged();
681
+ if (debug) {
682
+ console.log("[SchemaVaultsAuthClient] Finished triggering auth state change.");
683
+ }
684
+ if (debug) {
685
+ console.log("[SchemaVaultsAuthClient] handleSuccessfulAuthentication success!");
686
+ }
687
+ return;
688
+ } // handleSuccessfulAuthentication()
689
+ async logout() {
690
+ if (this.debug) {
691
+ console.log("[SchemaVaultsAuthClient] logout()");
692
+ }
693
+ try {
694
+ this.adapter.clearCodeVerifiers();
695
+ (await this.adapter.clearAuthTokens());
696
+ this.adapter.clearUserData();
697
+ }
698
+ catch (e) {
699
+ console.error(e);
700
+ throw new Error("Failed to clear local data for logout");
701
+ }
702
+ if (this.DEBUG) {
703
+ console.log("[SchemaVaultsAuthClient] logout() cleared local state, triggering auth state change event...");
704
+ }
705
+ this.triggerAuthStateChanged();
706
+ return;
707
+ } // logout()
708
+ hasHttpOnlyRefreshToken() {
709
+ if (typeof this.adapter.doesSupportHttpOnlyRefreshToken !== "function") {
710
+ return false;
711
+ }
712
+ if (!this.adapter.doesSupportHttpOnlyRefreshToken()) {
713
+ return false;
714
+ }
715
+ if (typeof this.adapter.hasHttpOnlyRefreshToken !== "function") {
716
+ return false;
717
+ }
718
+ if (this.adapter.hasHttpOnlyRefreshToken()) {
719
+ return true;
720
+ }
721
+ return false;
722
+ } // hasHttpOnlyRefreshToken()
723
+ get auth_server_uri() {
724
+ const host = this._authServerUri;
725
+ if (this.secure && !host.startsWith("https://")) {
726
+ throw new Error("Auth server host must use HTTPS");
727
+ }
728
+ return host;
729
+ }
730
+ get secure() {
731
+ const env = this.environment;
732
+ if (env === "development" || env === "test") {
733
+ return false;
734
+ }
735
+ else {
736
+ // staging + prod use https
737
+ return true;
738
+ }
739
+ }
740
+ /**
741
+ * @name successful_authentication_redirect_uri
742
+ * @description Where to send the user after successful authentication
743
+ * @example "For example, maybe send them to their account dashboard: `/account`."
744
+ */
745
+ get successful_authentication_redirect_uri() {
746
+ const uri = this._successful_authentication_redirect_uri;
747
+ if (typeof uri !== "string" && typeof uri !== "undefined") {
748
+ throw new Error("Unexpected data type for redirect uri");
749
+ }
750
+ if (!uri) {
751
+ throw new Error("No successful authentication redirect URI set");
752
+ }
753
+ const app_env = this.environment;
754
+ if (app_env !== "development" && app_env !== "test") {
755
+ if (!uri.startsWith("https://")) {
756
+ console.error(`Redirect URI must use HTTPS in production, received: "${uri}"`);
757
+ throw new Error("Redirect URI must use HTTPS in production environments!");
758
+ }
759
+ }
760
+ return uri;
761
+ }
762
+ get authorize_uri() {
763
+ return this._authorize_uri;
764
+ }
765
+ /**
766
+ * @name storeRefreshToken(refresh_token)
767
+ * @param refresh_token A 'RefreshToken' object to be stored
768
+ * @returns nothing, after storing the refresh token via the adapter
769
+ */
770
+ storeRefreshToken(refresh_token) {
771
+ if (typeof refresh_token !== "object" || refresh_token.type !== "refresh") {
772
+ throw new TypeError("Expected 'refresh_token' to be an object with 'type' set to 'refresh'");
773
+ }
774
+ if (this.DEBUG) {
775
+ console.log(`[SchemaVaultsAuthClient] storeRefreshToken(${JSON.stringify(refresh_token)})`);
776
+ }
777
+ this.adapter.storeRefreshToken(refresh_token);
778
+ return;
779
+ }
780
+ /**
781
+ * @name storeAccessToken(access_token)
782
+ * @param token_id The ID of the access token to be stored
783
+ * @param access_token An 'AccessToken' object to be stored
784
+ * @returns nothing, after storing the access token via the adapter
785
+ */
786
+ storeAccessToken(token_id, access_token) {
787
+ if (typeof token_id !== "string" || token_id.length === 0) {
788
+ throw new TypeError("Expected 'token_id' to be a non-empty string");
789
+ }
790
+ else if (typeof access_token !== "object" ||
791
+ access_token.type !== "access") {
792
+ throw new TypeError("Expected 'access_token' to be an object with 'type' set to 'access'");
793
+ }
794
+ if (this.DEBUG) {
795
+ console.log(`[SchemaVaultsAuthClient] storeAccessToken("${token_id}", ${JSON.stringify(access_token)})`);
796
+ }
797
+ this.adapter.storeAccessToken(token_id, access_token);
798
+ return;
799
+ }
800
+ getAccessTokenFromCache(token_id) {
801
+ if (this.DEBUG) {
802
+ console.log(`[SchemaVaultsAuthClient] Getting access token with ID "${token_id}" via adapter...`);
803
+ }
804
+ const token = this.adapter.getAccessToken(token_id);
805
+ if (this.DEBUG && !token) {
806
+ console.warn(`[SchemaVaultsAuthClient] Cache lookup failed for access token with ID "${token_id}" via adapter...`);
807
+ }
808
+ return token;
809
+ }
810
+ getRefreshTokenFromCache() {
811
+ if (this.DEBUG) {
812
+ console.log(`[SchemaVaultsAuthClient] Getting access token from cache via adapter...`);
813
+ }
814
+ if (typeof this.adapter.doesSupportHttpOnlyRefreshToken === "function" &&
815
+ this.adapter.doesSupportHttpOnlyRefreshToken()) {
816
+ throw new Error("Cannot get refresh token from cache when using HTTP-only cookie storage");
817
+ }
818
+ const token = this.adapter.getRefreshToken();
819
+ if (this.DEBUG && !token) {
820
+ if (!token) {
821
+ console.warn("[SchemaVaultsAuthClient] Cache lookup failed for refresh token!");
822
+ }
823
+ else {
824
+ console.log("[SchemaVaultsAuthClient] Cache lookup success for refresh token: ", token);
825
+ }
826
+ }
827
+ return token ?? null;
828
+ }
829
+ /**
830
+ * @name acquireAccessToken
831
+ * @description Attempt to acquire an access token in order to communicate with a SchemaVaults resource server.
832
+ * This will attempt to load a locally-saved refresh token in order to exchange it for an access token.
833
+ * @see this.exchangeAuthTokens()
834
+ */
835
+ async acquireAccessToken(opts) {
836
+ let tradeRefreshTokenForAccessToken;
837
+ try {
838
+ tradeRefreshTokenForAccessToken = await import("./lib/acquire-access-token").then((mod) => mod.default);
839
+ if (typeof tradeRefreshTokenForAccessToken !== "function") {
840
+ throw new Error("Expected default export from 'acquire-access-token' module to be a function!");
841
+ }
842
+ }
843
+ catch (error) {
844
+ console.error("[SchemaVaultsAuthClient] Failed to import acquire-access-token module:", error);
845
+ throw new Error("Failed to import 'acquire-access-token' module");
846
+ }
847
+ const adapter = this.adapter;
848
+ return await tradeRefreshTokenForAccessToken({
849
+ opts,
850
+ adapter,
851
+ logout: this.logout.bind(this),
852
+ exchangeAuthTokens: this.exchangeAuthTokens.bind(this),
853
+ debug: this.debug,
854
+ });
855
+ }
856
+ storeUserData(userData) {
857
+ if (this.DEBUG) {
858
+ console.log("[SchemaVaultsAuthClient] Attempting to cache user data VIA the platform adapter...");
859
+ }
860
+ this.adapter.storeUserData(userData);
861
+ if (this.DEBUG) {
862
+ console.log("[SchemaVaultsAuthClient] Cached user data VIA the platform adapter.");
863
+ }
864
+ }
865
+ getUserData() {
866
+ if (this.DEBUG)
867
+ console.log("[SchemaVaultsAuthClient] Attempting to load cached user data VIA the platform adapter...");
868
+ let userData;
869
+ try {
870
+ userData = this.adapter.getUserData();
871
+ }
872
+ catch (e) {
873
+ console.error("Failed to use @schemavaults/auth-client-sdk platform adapter to load user data: ", e);
874
+ throw new Error("Failed to load user data using @schemavaults/auth-client-sdk platform adapter");
875
+ }
876
+ // note that user data can still be null/undefined here-- platform might be tryna say that there's no user data but no error
877
+ if (this.DEBUG) {
878
+ if (typeof userData === "object" && !!userData) {
879
+ console.log("[SchemaVaultsAuthClient] Successfully loaded user data VIA the platform adapter:", userData);
880
+ }
881
+ else {
882
+ // no user data, but not an error
883
+ console.warn("[SchemaVaultsAuthClient] Platform adapter returned no user data without indicating a failure-- user probably not logged in!");
884
+ }
885
+ }
886
+ return userData;
887
+ }
888
+ getCurrentTimestamp() {
889
+ return Date.now();
890
+ }
891
+ /**
892
+ * @name isAuthenticated
893
+ * @description Getter that returns true/false based on whether a user is currently signed into their account
894
+ */
895
+ get isAuthenticated() {
896
+ if (this.DEBUG) {
897
+ console.log("[SchemaVaultsAuthClient::isAuthenticated] Checking whether auth client has a refresh token to see if authenticated...");
898
+ }
899
+ const refreshToken = this.getRefreshTokenFromCache();
900
+ if (refreshToken) {
901
+ if (this.DEBUG) {
902
+ console.log("[SchemaVaultsAuthClient::isAuthenticated] There is a refresh token stored, checking if it is expired...");
903
+ }
904
+ const refreshTokenExpiryTime = refreshToken.exp;
905
+ const now = this.getCurrentTimestamp();
906
+ if (now < refreshTokenExpiryTime) {
907
+ return true;
908
+ }
909
+ else {
910
+ if (this.DEBUG) {
911
+ console.warn("[SchemaVaultsAuthClient::isAuthenticated] There is a refresh token stored, but it appears to be expired!");
912
+ }
913
+ }
914
+ }
915
+ else {
916
+ if (this.DEBUG) {
917
+ console.warn("[SchemaVaultsAuthClient::isAuthenticated] No refresh token found from cache!");
918
+ }
919
+ }
920
+ return false;
921
+ }
922
+ /**
923
+ * @name sendAuthenticateRequest
924
+ * @description Send credentials to acquire an authorization code
925
+ * @param authentication_type 'login' | 'register' | 'reset-password'
926
+ * @param credentials Username/email/password/invite code
927
+ * @param code_challenge A code challenge for Oauth2 PKCE flow. Allows ensuring that trading authorization code for refresh token is done by the client that initialized the attempt to acquire the authorization code!
928
+ * @returns A 'string' authorization code, that can be exchanged for refresh/access JWTs (in combination with the code verifier-- which was used to generate the code challenge!)
929
+ */
930
+ async sendAuthenticateRequest(authentication_type, credentials, code_challenge) {
931
+ if (this.DEBUG)
932
+ console.log("[SchemaVaultsAuthClient] Attempting to send authenticate request...");
933
+ return await sendAuthenticateRequest({
934
+ adapter: this._adapter,
935
+ authentication_type,
936
+ credentials,
937
+ code_challenge,
938
+ app_environment: this.environment,
939
+ });
940
+ }
941
+ /**
942
+ * @name currentUser
943
+ * @description If a user is signed in to this auth client and their user data is stored locally, return it. Else, returns null.
944
+ * @returns `UserData` | `null`
945
+ */
946
+ get currentUser() {
947
+ const userData = this.getUserData();
948
+ return userData;
949
+ }
950
+ assertHttpOnlyRefreshTokenCookieHasAccompanyingMarkerCookie() {
951
+ if (typeof this.adapter.hasHttpOnlyRefreshToken === "function" &&
952
+ !this.adapter.hasHttpOnlyRefreshToken()) {
953
+ throw new Error("Adapter does not indicate having an HTTP-only refresh token after exchange," +
954
+ " " +
955
+ "despite response of AS_HTTP_ONLY_COOKIE!");
956
+ }
957
+ }
958
+ async handleSuccessfulExchangeAuthTokensResponse(tokens_response) {
959
+ const parsed_tokens_data = await requestTokensResultSchema.safeParseAsync(tokens_response);
960
+ if (!parsed_tokens_data.success) {
961
+ if (this.DEBUG) {
962
+ console.error("[SchemaVaultsAuthClient::handleSuccessfulExchangeAuthTokensResponse()] " +
963
+ "Failed to parse successful exchange auth tokens result: ", parsed_tokens_data.error.errors);
964
+ }
965
+ throw new Error("Failed to parse successful exchange auth tokens result!");
966
+ }
967
+ const request_tokens_result = parsed_tokens_data.data;
968
+ if (!request_tokens_result.success) {
969
+ throw new Error("Request tokens response has success === false");
970
+ }
971
+ const tokens = request_tokens_result.tokens;
972
+ if (!tokens) {
973
+ throw new Error("Response did not include any tokens");
974
+ }
975
+ if (!tokens.access) {
976
+ throw new Error("No access token was included in the tokens response");
977
+ }
978
+ if (this.DEBUG) {
979
+ console.log("[SchemaVaultsAuthClient] Successfully exchanged refresh token for new authentication token(s)");
980
+ }
981
+ if (tokens.access) {
982
+ this.storeMultipleAccessTokens(tokens.access);
983
+ }
984
+ if (tokens.refresh) {
985
+ if (typeof tokens.refresh === "object" &&
986
+ tokens.refresh.type === "refresh") {
987
+ this.storeRefreshToken(tokens.refresh);
988
+ }
989
+ else if (typeof tokens.refresh === "string" &&
990
+ tokens.refresh === "AS_HTTP_ONLY_COOKIE") {
991
+ this.assertHttpOnlyRefreshTokenCookieHasAccompanyingMarkerCookie();
992
+ if (this.debug) {
993
+ console.log("[SchemaVaultsAuthClient] Detected HTTP-only cookie refresh token from exchange response.");
994
+ }
995
+ }
996
+ else {
997
+ throw new TypeError("Invalid refresh token type");
998
+ }
999
+ }
1000
+ return tokens;
1001
+ }
1002
+ async exchangeAuthTokens(refreshToken, audience, replaceRefreshToo) {
1003
+ if (this.DEBUG) {
1004
+ console.log("[SchemaVaultsAuthClient] Attempting to send request to exchange refresh token for access token...");
1005
+ }
1006
+ const token_endpoint = `${this.auth_server_uri}/api/auth/token/refresh_token`;
1007
+ const client_app_id = this.app_id;
1008
+ if (!audience && !replaceRefreshToo) {
1009
+ throw new Error("Type of token to acquire not specified");
1010
+ }
1011
+ // Exchange the authorization code for an access token
1012
+ let request_body;
1013
+ try {
1014
+ const parsed = await refreshTokenPOSTbody.safeParseAsync({
1015
+ grant_type: "refresh_token",
1016
+ client_app_id,
1017
+ audience: audience ?? this.defaultTokenAudiences,
1018
+ replaceRefreshToo: replaceRefreshToo ?? false,
1019
+ });
1020
+ if (!parsed.success) {
1021
+ console.error(parsed.error);
1022
+ throw new Error("Failed to parse tokens from exchange auth tokens POST request!");
1023
+ }
1024
+ request_body = parsed.data;
1025
+ }
1026
+ catch (e) {
1027
+ if (this.DEBUG) {
1028
+ console.error("Failed to prepare request body for authorization grant request: ", e);
1029
+ }
1030
+ throw new Error("Failed to prepare request body for authorization grant request");
1031
+ }
1032
+ const exchangeAuthTokensReqHeaders = {};
1033
+ if (!refreshToken) {
1034
+ throw new Error("Did not receive a refresh token to exchange for access token!");
1035
+ }
1036
+ if (typeof refreshToken === "object" && refreshToken.type === "refresh") {
1037
+ if (typeof refreshToken.token !== "string" ||
1038
+ refreshToken.token.length === 0) {
1039
+ throw new TypeError("Expected 'token' to be a non-empty string!");
1040
+ }
1041
+ exchangeAuthTokensReqHeaders["Authorization"] =
1042
+ `Bearer ${refreshToken.token}`;
1043
+ }
1044
+ else if (typeof refreshToken === "string" &&
1045
+ refreshToken === "AS_HTTP_ONLY_COOKIE") {
1046
+ const doesSupportHttpOnlyRefreshToken = this.adapter.doesSupportHttpOnlyRefreshToken;
1047
+ if (typeof doesSupportHttpOnlyRefreshToken !== "function" ||
1048
+ !doesSupportHttpOnlyRefreshToken()) {
1049
+ throw new Error("Adapter does not support HTTP-only refresh tokens!");
1050
+ }
1051
+ }
1052
+ else {
1053
+ throw new Error("Did not receive a valid refresh token (or valid method of acquiring refresh token)");
1054
+ }
1055
+ if (this.DEBUG) {
1056
+ console.log("[SchemaVaultsAuthClient::exchangeAuthTokens()] " +
1057
+ `Sending POST request to "${token_endpoint}" with body & headers:`, request_body, exchangeAuthTokensReqHeaders);
1058
+ }
1059
+ let tokens_response_data;
1060
+ try {
1061
+ if (this.DEBUG) {
1062
+ console.log(`POST => ${token_endpoint}`);
1063
+ }
1064
+ const response = await this.adapter.sendPOSTRequest(token_endpoint,
1065
+ // body
1066
+ request_body,
1067
+ // headers
1068
+ exchangeAuthTokensReqHeaders);
1069
+ if (!response ||
1070
+ typeof response !== "object" ||
1071
+ response.status !== 200) {
1072
+ if (response.status === 403 || response.status === 401) {
1073
+ console.error("401/403 error response from exchange token attempt, client is not logged in!");
1074
+ await this.logout();
1075
+ }
1076
+ throw new Error("HTTP request failed to exchange refresh token for access token(s) object");
1077
+ }
1078
+ const parsed_failed_tokens_result = await requestTokensResultSchema.safeParseAsync(response.data);
1079
+ if (!parsed_failed_tokens_result.success) {
1080
+ console.error("Failed to parse tokens response from server: ", parsed_failed_tokens_result.error);
1081
+ throw new Error("Failed to parse tokens response from server!");
1082
+ }
1083
+ tokens_response_data = parsed_failed_tokens_result.data;
1084
+ }
1085
+ catch (e) {
1086
+ if (this.DEBUG) {
1087
+ console.error("[this.adapter.sendPOSTRequest] FAILED: ", e);
1088
+ throw new Error("Failed to ");
1089
+ }
1090
+ if (e instanceof Error) {
1091
+ const errMsg = e.message;
1092
+ const eMsg = errMsg.toLowerCase();
1093
+ if (eMsg.includes("expired") ||
1094
+ eMsg.includes("jwtexpired") ||
1095
+ eMsg.includes("err_jwt_expired")) {
1096
+ console.error("Refresh token appears to have expired!");
1097
+ throw new Error("Refresh token has expired!");
1098
+ }
1099
+ }
1100
+ if (this.DEBUG) {
1101
+ console.error("Failed to exchange refresh token for access token: ", e);
1102
+ }
1103
+ throw new Error("Failed to exchange refresh token for access token");
1104
+ } // end of this.adapter.sendPOSTRequest catch block
1105
+ try {
1106
+ // Parse tokens from response JSON body
1107
+ return await this.handleSuccessfulExchangeAuthTokensResponse(tokens_response_data);
1108
+ }
1109
+ catch (e) {
1110
+ if (this.DEBUG) {
1111
+ console.log("typeof e === ", typeof e);
1112
+ if (e instanceof Error) {
1113
+ console.error("Parse tokens error message: ", e.message);
1114
+ }
1115
+ console.error("Failed to parse authentication tokens from exchange tokens POST request: ", e);
1116
+ }
1117
+ throw new Error("Failed to parse authentication tokens from exchange tokens POST request!");
1118
+ }
1119
+ } // exchangeAuthTokens()
1120
+ uuid() {
1121
+ let id;
1122
+ try {
1123
+ id = this.adapter.uuid();
1124
+ }
1125
+ catch (e) {
1126
+ console.error("Failed to generate UUID using SchemaVaultsAuthClient platform adapter: ", e);
1127
+ throw new Error("Failed to generate UUID using SchemaVaultsAuthClient platform adapter!");
1128
+ }
1129
+ if (typeof id !== "string" || id.length === 0) {
1130
+ throw new TypeError("Invalid UUID generated!");
1131
+ }
1132
+ return id;
1133
+ }
1134
+ onAuthStateChanged(listener, listener_id) {
1135
+ let id;
1136
+ if (!!listener_id && typeof listener_id === "string") {
1137
+ id = listener_id;
1138
+ }
1139
+ else {
1140
+ id = this.uuid();
1141
+ }
1142
+ if (typeof id !== "string") {
1143
+ throw new Error("Failed to generate listener ID to initialize onAuthStateChanged listener with!");
1144
+ }
1145
+ if (this.listeners.has(id)) {
1146
+ throw new Error(`An auth state listener callback already exists with ID: "${id}"`);
1147
+ }
1148
+ this.listeners.set(id, {
1149
+ id,
1150
+ listener,
1151
+ });
1152
+ if (this.DEBUG) {
1153
+ console.log(`[SchemaVaultsAuthClient] onAuthStateChanged(listener, listener_id="${id}") -> Listener created!`);
1154
+ }
1155
+ return id;
1156
+ }
1157
+ removeAuthStateChangeListener(listener_id) {
1158
+ if (!this.listeners.has(listener_id))
1159
+ throw new Error(`No auth state change listener with ID "${listener_id}"`);
1160
+ const removed_successfully = this.listeners.delete(listener_id);
1161
+ if (!removed_successfully) {
1162
+ console.error(`[SchemaVaultsAuthClient] removeAuthStateChangeListener(listener_id="${listener_id}") -> Failed to remove listener from map-- wrong ID or does not exist?.`);
1163
+ throw new Error(`Failed to remove auth state change listener with ID "${listener_id}"`);
1164
+ }
1165
+ if (this.DEBUG) {
1166
+ console.log(`[SchemaVaultsAuthClient] removeAuthStateChangeListener(listener_id="${listener_id}") -> Listener removed successfully!`);
1167
+ }
1168
+ return;
1169
+ }
1170
+ /**
1171
+ * @name successful_logout_redirect_uri
1172
+ * @description Where to redirect after /auth/logout effect succeeds
1173
+ * @example "Perhaps the user should be sent back to the homepage: `/`"
1174
+ */
1175
+ get successful_logout_redirect_uri() {
1176
+ const redirect_uri = this._successful_logout_redirect_uri ?? undefined;
1177
+ if (this.DEBUG) {
1178
+ console.log("[SchemaVaultsAuthClient] successful_logout_redirect_uri: ", redirect_uri);
1179
+ }
1180
+ return redirect_uri;
1181
+ }
1182
+ supports(feature_name) {
1183
+ if (feature_name === "http-only-refresh-token") {
1184
+ return (typeof this.adapter.doesSupportHttpOnlyRefreshToken === "function" &&
1185
+ this.adapter.doesSupportHttpOnlyRefreshToken());
1186
+ }
1187
+ return false;
1188
+ }
1189
+ }
1190
+ //# sourceMappingURL=auth-client.js.map