@finueva/auth 0.6.0 → 0.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/dist/server.js CHANGED
@@ -173,6 +173,39 @@ function stringifyParsedURL(parsed) {
173
173
  return (parsed.protocol || parsed[protocolRelative] ? (parsed.protocol || "") + "//" : "") + auth + host + pathname + search + hash;
174
174
  }
175
175
  //#endregion
176
+ //#region src/errors.ts
177
+ const messages = {
178
+ invalid_credentials: "The credentials are invalid.",
179
+ invalid_auth_input: "The authentication input is invalid.",
180
+ account_conflict: "The account is not available.",
181
+ password_reset_invalid: "The password reset request is invalid.",
182
+ authentication_required: "Authentication is required.",
183
+ request_origin_denied: "The request origin is not allowed.",
184
+ invalid_invitation: "The invitation is invalid.",
185
+ invitation_unavailable: "The invitation is not available.",
186
+ invitation_recipient_mismatch: "The invitation cannot be accepted by this account.",
187
+ email_verification_required: "Email verification is required.",
188
+ invalid_workspace_selection: "The workspace selection is invalid.",
189
+ organization_context_too_large: "The organization context is too large.",
190
+ credential_rate_limited: "Credential requests are temporarily limited.",
191
+ auth_request_rate_limited: "Authentication requests are temporarily limited.",
192
+ state_conflict: "The authentication state conflicts with the request.",
193
+ access_denied: "Access is denied.",
194
+ identity_authority_unavailable: "Authentication is temporarily unavailable."
195
+ };
196
+ var AuthError = class extends Error {
197
+ code;
198
+ status;
199
+ retryAfterSeconds;
200
+ constructor(code, status, retryAfterSeconds) {
201
+ super(messages[code]);
202
+ this.name = "AuthError";
203
+ this.code = code;
204
+ this.status = status;
205
+ this.retryAfterSeconds = retryAfterSeconds;
206
+ }
207
+ };
208
+ //#endregion
176
209
  //#region ../../node_modules/.pnpm/destr@2.0.5/node_modules/destr/dist/index.mjs
177
210
  const suspectProtoRx = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/;
178
211
  const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
@@ -436,39 +469,32 @@ function createFetch(globalOptions = {}) {
436
469
  return $fetch;
437
470
  }
438
471
  //#endregion
439
- //#region src/errors.ts
440
- const messages = {
441
- invalid_credentials: "The credentials are invalid.",
442
- authentication_required: "Authentication is required.",
443
- request_origin_denied: "The request origin is not allowed.",
444
- invalid_invitation: "The invitation is invalid.",
445
- invitation_unavailable: "The invitation is not available.",
446
- invitation_recipient_mismatch: "The invitation cannot be accepted by this account.",
447
- email_verification_required: "Email verification is required.",
448
- invalid_workspace_selection: "The workspace selection is invalid.",
449
- organization_context_too_large: "The organization context is too large.",
450
- credential_rate_limited: "Credential requests are temporarily limited.",
451
- state_conflict: "The authentication state conflicts with the request.",
452
- access_denied: "Access is denied.",
453
- identity_authority_unavailable: "Authentication is temporarily unavailable."
454
- };
455
- var AuthError = class extends Error {
456
- code;
457
- status;
458
- retryAfterSeconds;
459
- constructor(code, status, retryAfterSeconds) {
460
- super(messages[code]);
461
- this.name = "AuthError";
462
- this.code = code;
463
- this.status = status;
464
- this.retryAfterSeconds = retryAfterSeconds;
465
- }
466
- };
467
- //#endregion
468
472
  //#region src/shared.ts
469
473
  const DRIVE_CREDENTIAL_LIFETIME_SECONDS = 840;
470
474
  const id = /^[A-Za-z0-9_-]{1,128}$/;
471
475
  const email = /^[^\s@]+@[^\s@]+$/;
476
+ const MAX_SOCIAL_ID_TOKEN_BYTES = 16 * 1024;
477
+ function normalizeSocialIdTokenInput(value) {
478
+ if (!isObject(value)) throw new TypeError("social ID token input is invalid");
479
+ const keys = Object.keys(value);
480
+ if (keys.length !== 3 && keys.length !== 4 || keys.some((key) => key !== "provider" && key !== "idToken" && key !== "nonce" && key !== "signUp") || value.provider !== "google" && value.provider !== "apple" || typeof value.signUp !== "boolean" || typeof value.idToken !== "string" || value.idToken.length < 1 || new TextEncoder().encode(value.idToken).byteLength > MAX_SOCIAL_ID_TOKEN_BYTES || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value.idToken) || value.nonce !== void 0 && (typeof value.nonce !== "string" || value.nonce.length < 1 || value.nonce.length > 256 || !/^[\x20-\x7e]+$/.test(value.nonce))) throw new TypeError("social ID token input is invalid");
481
+ return {
482
+ provider: value.provider,
483
+ idToken: value.idToken,
484
+ ...value.nonce === void 0 ? {} : { nonce: value.nonce },
485
+ signUp: value.signUp
486
+ };
487
+ }
488
+ function socialIdTokenRequest(value) {
489
+ return {
490
+ provider: value.provider,
491
+ idToken: {
492
+ token: value.idToken,
493
+ ...value.nonce === void 0 ? {} : { nonce: value.nonce }
494
+ },
495
+ requestSignUp: value.signUp
496
+ };
497
+ }
472
498
  function normalizeOrigin(value, name) {
473
499
  if (value.length === 0 || value.length > 2048 || /[^\x20-\x7e]/.test(value)) throw new TypeError(`${name} must be an ASCII HTTP origin`);
474
500
  let url;
@@ -505,7 +531,7 @@ function normalizeSession(value) {
505
531
  };
506
532
  }
507
533
  function normalizeDriveCredential(value, now) {
508
- if (!isObject(value) || value.tokenType !== "Bearer" || value.expiresIn !== DRIVE_CREDENTIAL_LIFETIME_SECONDS || typeof value.token !== "string" || value.token.length === 0 || value.token.length + 7 > 16 * 1024) throw authorityUnavailable();
534
+ if (!isObject(value) || value.tokenType !== "Bearer" || value.expiresIn !== DRIVE_CREDENTIAL_LIFETIME_SECONDS || typeof value.token !== "string" || value.token.length === 0 || new TextEncoder().encode(`Bearer ${value.token}`).byteLength > 16 * 1024) throw authorityUnavailable();
509
535
  return {
510
536
  tokenType: "Bearer",
511
537
  token: value.token,
@@ -547,6 +573,26 @@ function fromAuthorityError(value, fallbackStatus, retryAfter) {
547
573
  const mapped = mapError(isObject(value) && typeof value.code === "string" ? value.code : void 0, fallbackStatus);
548
574
  return new AuthError(mapped.code, mapped.status, parseRetryAfter(retryAfter));
549
575
  }
576
+ function fromAccountAuthorityError(value, status, retryAfter) {
577
+ const code = isObject(value) && typeof value.code === "string" ? value.code : void 0;
578
+ if (status === 403 && (code === "INVALID_ORIGIN" || code === "MISSING_OR_NULL_ORIGIN")) return new AuthError("request_origin_denied", 403);
579
+ if (code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") return new AuthError("account_conflict", 409);
580
+ if (code === "INVALID_TOKEN") return new AuthError("password_reset_invalid", 422);
581
+ if (code === "INVALID_PASSWORD" || code === "INVALID_EMAIL_OR_PASSWORD") return new AuthError("invalid_credentials", 401);
582
+ if (code === "INVALID_EMAIL" || code === "PASSWORD_TOO_SHORT" || code === "PASSWORD_TOO_LONG" || code === "BODY_MUST_BE_AN_OBJECT" || code === "EMAIL_CAN_NOT_BE_UPDATED") return new AuthError("invalid_auth_input", 422);
583
+ if (status === 401) return new AuthError("authentication_required", 401);
584
+ if (status === 403) return new AuthError("access_denied", 403);
585
+ if (status === 409) return new AuthError("state_conflict", 409);
586
+ if (status === 429) return new AuthError("auth_request_rate_limited", 429, parseRetryAfter(retryAfter));
587
+ if (status === 400 || status === 422) return new AuthError("invalid_auth_input", 422);
588
+ return authorityUnavailable();
589
+ }
590
+ function fromSocialIdTokenAuthorityError(value, status, retryAfter) {
591
+ const code = isObject(value) && typeof value.code === "string" ? value.code : void 0;
592
+ if (code === "INVALID_TOKEN" || code === "FAILED_TO_GET_USER_INFO" || code === "USER_EMAIL_NOT_FOUND") return new AuthError("invalid_credentials", 401);
593
+ if (code === "OAUTH_LINK_ERROR") return new AuthError("account_conflict", 409);
594
+ return fromAccountAuthorityError(value, status, retryAfter);
595
+ }
550
596
  function authorityUnavailable() {
551
597
  return new AuthError("identity_authority_unavailable", 503);
552
598
  }
@@ -645,7 +691,7 @@ async function requestJsonResponse(url, options) {
645
691
  method: options.method,
646
692
  headers: options.headers,
647
693
  body: options.body,
648
- credentials: "include",
694
+ credentials: options.credentials ?? "include",
649
695
  retry: 0,
650
696
  timeout: options.timeoutMs,
651
697
  redirect: "manual",
@@ -705,6 +751,7 @@ async function readJson(response) {
705
751
  }
706
752
  //#endregion
707
753
  //#region src/server.ts
754
+ const MAX_AUTHORIZATION_BYTES = 16 * 1024;
708
755
  function createServerAuth(options) {
709
756
  const authOrigin = normalizeOrigin(options.authOrigin, "authOrigin");
710
757
  const appOrigin = normalizeOrigin(options.appOrigin, "appOrigin");
@@ -712,10 +759,12 @@ function createServerAuth(options) {
712
759
  if (typeof fetchImpl !== "function") throw new TypeError("fetch is required");
713
760
  const now = options.now ?? Date.now;
714
761
  const timeoutMs = normalizeTimeout(options.timeoutMs);
762
+ const authorization = authAuthorizationHeader(options.request.headers.get("authorization"));
715
763
  const cookie = authCookieHeader(options.request.headers.get("cookie"));
716
- const authHeaders = () => ({
717
- cookie,
718
- origin: appOrigin
764
+ const credentials = authorization ? "omit" : "include";
765
+ const authHeaders = (includeOrigin = true) => ({
766
+ ...authorization ? { authorization } : { cookie },
767
+ ...includeOrigin ? { origin: appOrigin } : {}
719
768
  });
720
769
  return {
721
770
  async acceptInvitation(input) {
@@ -725,6 +774,7 @@ function createServerAuth(options) {
725
774
  fetch: fetchImpl,
726
775
  timeoutMs,
727
776
  method: "POST",
777
+ credentials,
728
778
  headers: {
729
779
  ...authHeaders(),
730
780
  "content-type": "application/json; charset=utf-8"
@@ -749,7 +799,8 @@ function createServerAuth(options) {
749
799
  fetch: fetchImpl,
750
800
  timeoutMs,
751
801
  method: "GET",
752
- headers: { cookie }
802
+ credentials,
803
+ headers: authHeaders(false)
753
804
  }));
754
805
  },
755
806
  async listOrganizations() {
@@ -758,18 +809,52 @@ function createServerAuth(options) {
758
809
  fetch: fetchImpl,
759
810
  timeoutMs,
760
811
  method: "GET",
812
+ credentials,
761
813
  headers: authHeaders()
762
814
  }));
763
815
  } catch (error) {
764
816
  throw fromOrganizationListError(error);
765
817
  }
766
818
  },
819
+ async signInWithSocialIdToken(input) {
820
+ if (authorization) throw new AuthError("state_conflict", 409);
821
+ const body = socialIdTokenRequest(normalizeSocialIdTokenInput(input));
822
+ const cookies = responseCookies((await requestJsonResponse(joinURL(authOrigin, "api/v1/auth/sign-in/social"), {
823
+ fetch: fetchImpl,
824
+ timeoutMs,
825
+ method: "POST",
826
+ credentials,
827
+ headers: {
828
+ ...authHeaders(),
829
+ "content-type": "application/json; charset=utf-8"
830
+ },
831
+ body,
832
+ mapError: fromSocialIdTokenAuthorityError
833
+ })).headers);
834
+ const cookieHeader = authCookieHeader(cookies.map((value) => value.split(";", 1)[0]).join("; "));
835
+ if (!cookieHeader) throw authorityUnavailable();
836
+ const session = normalizeSession(await requestJson(joinURL(authOrigin, "api/v1/auth/get-session"), {
837
+ fetch: fetchImpl,
838
+ timeoutMs,
839
+ method: "GET",
840
+ credentials: "include",
841
+ headers: { cookie: cookieHeader }
842
+ }));
843
+ if (!session) throw new AuthError("authentication_required", 401);
844
+ return {
845
+ ...session,
846
+ applyCookies(headers) {
847
+ applyCookies(cookies, headers);
848
+ }
849
+ };
850
+ },
767
851
  async getDriveCredential(input) {
768
852
  const workspace = normalizeWorkspaceSelection(input?.workspace);
769
853
  return normalizeDriveCredential(await requestJson(joinURL(authOrigin, "api/v1/auth/drive-credential"), {
770
854
  fetch: fetchImpl,
771
855
  timeoutMs,
772
856
  method: "POST",
857
+ credentials,
773
858
  headers: {
774
859
  ...authHeaders(),
775
860
  "content-type": "application/json; charset=utf-8"
@@ -782,6 +867,7 @@ function createServerAuth(options) {
782
867
  try {
783
868
  response = await fetchImpl(joinURL(authOrigin, "api/v1/auth/sign-out"), {
784
869
  method: "POST",
870
+ credentials,
785
871
  headers: authHeaders(),
786
872
  redirect: "manual",
787
873
  signal: AbortSignal.timeout(timeoutMs)
@@ -817,5 +903,10 @@ function authCookieHeader(value) {
817
903
  return name.startsWith("better-auth.") || name.startsWith("__Secure-better-auth.");
818
904
  }).join("; ");
819
905
  }
906
+ function authAuthorizationHeader(value) {
907
+ if (value === null) return void 0;
908
+ if (new TextEncoder().encode(value).byteLength > MAX_AUTHORIZATION_BYTES || /[^\x20-\x7e]/.test(value) || !/^Bearer [^\s,]+$/i.test(value)) throw new AuthError("authentication_required", 401);
909
+ return value;
910
+ }
820
911
  //#endregion
821
912
  export { AuthError, createServerAuth };
@@ -1,3 +1,13 @@
1
+ //#region src/errors.d.ts
2
+ type AuthErrorCode = "invalid_credentials" | "invalid_auth_input" | "account_conflict" | "password_reset_invalid" | "authentication_required" | "request_origin_denied" | "invalid_invitation" | "invitation_unavailable" | "invitation_recipient_mismatch" | "email_verification_required" | "invalid_workspace_selection" | "organization_context_too_large" | "credential_rate_limited" | "auth_request_rate_limited" | "state_conflict" | "access_denied" | "identity_authority_unavailable";
3
+ type AuthErrorStatus = 401 | 403 | 409 | 422 | 429 | 503;
4
+ declare class AuthError extends Error {
5
+ readonly code: AuthErrorCode;
6
+ readonly status: AuthErrorStatus;
7
+ readonly retryAfterSeconds?: number;
8
+ constructor(code: AuthErrorCode, status: AuthErrorStatus, retryAfterSeconds?: number);
9
+ }
10
+ //#endregion
1
11
  //#region src/types.d.ts
2
12
  interface AuthUser {
3
13
  id: string;
@@ -64,6 +74,17 @@ interface SignInInput {
64
74
  email: string;
65
75
  password: string;
66
76
  }
77
+ type SocialIdTokenProvider = "google" | "apple";
78
+ interface SocialIdTokenInput {
79
+ provider: SocialIdTokenProvider;
80
+ idToken: string;
81
+ nonce?: string;
82
+ signUp: boolean;
83
+ }
84
+ interface UpdateProfileInput {
85
+ name?: string;
86
+ image?: string | null;
87
+ }
67
88
  interface SendVerificationEmailInput {
68
89
  email: string;
69
90
  callbackURL?: string;
@@ -85,11 +106,17 @@ interface FinuevaAuthClient {
85
106
  getSession(): Promise<AuthSession | null>;
86
107
  listOrganizations(): Promise<AuthOrganization[]>;
87
108
  sendVerificationEmail(input: SendVerificationEmailInput): Promise<void>;
109
+ signUp(email: string, password: string, name: string): Promise<AuthSession>;
88
110
  signIn(input: SignInInput): Promise<AuthSession>;
111
+ signInWithSocialIdToken(input: SocialIdTokenInput): Promise<AuthSession>;
112
+ updateProfile(input: UpdateProfileInput): Promise<AuthSession>;
113
+ changePassword(currentPassword: string, newPassword: string, revokeOtherSessions?: boolean): Promise<AuthSession>;
114
+ requestPasswordReset(email: string, redirectTo: string): Promise<void>;
115
+ resetPassword(token: string, newPassword: string): Promise<void>;
89
116
  signOut(): Promise<void>;
90
117
  getDriveCredential(input: DriveCredentialInput): Promise<DriveCredential>;
91
118
  onAuthStateChange(listener: (state: AuthState) => void): () => void;
92
119
  onSessionChange(listener: (session: AuthSession | null) => void): () => void;
93
120
  }
94
121
  //#endregion
95
- export { WorkspaceSelection as _, AuthPersistenceDriver as a, AuthUser as c, FinuevaAuthClient as d, InvitationAcceptance as f, SignInInput as g, SendVerificationEmailInput as h, AuthPersistence as i, DriveCredential as l, PersonalWorkspaceSelection as m, AuthClientOptions as n, AuthSession as o, OrganizationWorkspaceSelection as p, AuthOrganization as r, AuthState as s, AcceptInvitationInput as t, DriveCredentialInput as u };
122
+ export { AuthErrorStatus as C, AuthErrorCode as S, SocialIdTokenInput as _, AuthPersistenceDriver as a, WorkspaceSelection as b, AuthUser as c, FinuevaAuthClient as d, InvitationAcceptance as f, SignInInput as g, SendVerificationEmailInput as h, AuthPersistence as i, DriveCredential as l, PersonalWorkspaceSelection as m, AuthClientOptions as n, AuthSession as o, OrganizationWorkspaceSelection as p, AuthOrganization as r, AuthState as s, AcceptInvitationInput as t, DriveCredentialInput as u, SocialIdTokenProvider as v, AuthError as x, UpdateProfileInput as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finueva/auth",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Public browser and server SDK for Finueva Auth",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -41,6 +41,7 @@
41
41
  "build": "rolldown -c",
42
42
  "coverage": "vitest run --coverage",
43
43
  "lint": "oxlint . --deny-warnings",
44
+ "prepack": "pnpm build",
44
45
  "test": "pnpm test:unit && pnpm build && pnpm test:package",
45
46
  "test:composition": "node scripts/test-composition.mjs",
46
47
  "test:package": "node scripts/test-package.mjs",
@@ -1,11 +0,0 @@
1
- //#region src/errors.d.ts
2
- type AuthErrorCode = "invalid_credentials" | "authentication_required" | "request_origin_denied" | "invalid_invitation" | "invitation_unavailable" | "invitation_recipient_mismatch" | "email_verification_required" | "invalid_workspace_selection" | "organization_context_too_large" | "credential_rate_limited" | "state_conflict" | "access_denied" | "identity_authority_unavailable";
3
- type AuthErrorStatus = 401 | 403 | 409 | 422 | 429 | 503;
4
- declare class AuthError extends Error {
5
- readonly code: AuthErrorCode;
6
- readonly status: AuthErrorStatus;
7
- readonly retryAfterSeconds?: number;
8
- constructor(code: AuthErrorCode, status: AuthErrorStatus, retryAfterSeconds?: number);
9
- }
10
- //#endregion
11
- export { AuthErrorCode as n, AuthErrorStatus as r, AuthError as t };