@finueva/auth 0.5.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,38 +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
473
+ const DRIVE_CREDENTIAL_LIFETIME_SECONDS = 840;
469
474
  const id = /^[A-Za-z0-9_-]{1,128}$/;
470
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
+ }
471
498
  function normalizeOrigin(value, name) {
472
499
  if (value.length === 0 || value.length > 2048 || /[^\x20-\x7e]/.test(value)) throw new TypeError(`${name} must be an ASCII HTTP origin`);
473
500
  let url;
@@ -504,7 +531,7 @@ function normalizeSession(value) {
504
531
  };
505
532
  }
506
533
  function normalizeDriveCredential(value, now) {
507
- if (!isObject(value) || value.tokenType !== "Bearer" || value.expiresIn !== 900 || 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();
508
535
  return {
509
536
  tokenType: "Bearer",
510
537
  token: value.token,
@@ -546,6 +573,26 @@ function fromAuthorityError(value, fallbackStatus, retryAfter) {
546
573
  const mapped = mapError(isObject(value) && typeof value.code === "string" ? value.code : void 0, fallbackStatus);
547
574
  return new AuthError(mapped.code, mapped.status, parseRetryAfter(retryAfter));
548
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
+ }
549
596
  function authorityUnavailable() {
550
597
  return new AuthError("identity_authority_unavailable", 503);
551
598
  }
@@ -644,7 +691,7 @@ async function requestJsonResponse(url, options) {
644
691
  method: options.method,
645
692
  headers: options.headers,
646
693
  body: options.body,
647
- credentials: "include",
694
+ credentials: options.credentials ?? "include",
648
695
  retry: 0,
649
696
  timeout: options.timeoutMs,
650
697
  redirect: "manual",
@@ -704,6 +751,7 @@ async function readJson(response) {
704
751
  }
705
752
  //#endregion
706
753
  //#region src/server.ts
754
+ const MAX_AUTHORIZATION_BYTES = 16 * 1024;
707
755
  function createServerAuth(options) {
708
756
  const authOrigin = normalizeOrigin(options.authOrigin, "authOrigin");
709
757
  const appOrigin = normalizeOrigin(options.appOrigin, "appOrigin");
@@ -711,10 +759,12 @@ function createServerAuth(options) {
711
759
  if (typeof fetchImpl !== "function") throw new TypeError("fetch is required");
712
760
  const now = options.now ?? Date.now;
713
761
  const timeoutMs = normalizeTimeout(options.timeoutMs);
762
+ const authorization = authAuthorizationHeader(options.request.headers.get("authorization"));
714
763
  const cookie = authCookieHeader(options.request.headers.get("cookie"));
715
- const authHeaders = () => ({
716
- cookie,
717
- origin: appOrigin
764
+ const credentials = authorization ? "omit" : "include";
765
+ const authHeaders = (includeOrigin = true) => ({
766
+ ...authorization ? { authorization } : { cookie },
767
+ ...includeOrigin ? { origin: appOrigin } : {}
718
768
  });
719
769
  return {
720
770
  async acceptInvitation(input) {
@@ -724,6 +774,7 @@ function createServerAuth(options) {
724
774
  fetch: fetchImpl,
725
775
  timeoutMs,
726
776
  method: "POST",
777
+ credentials,
727
778
  headers: {
728
779
  ...authHeaders(),
729
780
  "content-type": "application/json; charset=utf-8"
@@ -748,7 +799,8 @@ function createServerAuth(options) {
748
799
  fetch: fetchImpl,
749
800
  timeoutMs,
750
801
  method: "GET",
751
- headers: { cookie }
802
+ credentials,
803
+ headers: authHeaders(false)
752
804
  }));
753
805
  },
754
806
  async listOrganizations() {
@@ -757,18 +809,52 @@ function createServerAuth(options) {
757
809
  fetch: fetchImpl,
758
810
  timeoutMs,
759
811
  method: "GET",
812
+ credentials,
760
813
  headers: authHeaders()
761
814
  }));
762
815
  } catch (error) {
763
816
  throw fromOrganizationListError(error);
764
817
  }
765
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
+ },
766
851
  async getDriveCredential(input) {
767
852
  const workspace = normalizeWorkspaceSelection(input?.workspace);
768
853
  return normalizeDriveCredential(await requestJson(joinURL(authOrigin, "api/v1/auth/drive-credential"), {
769
854
  fetch: fetchImpl,
770
855
  timeoutMs,
771
856
  method: "POST",
857
+ credentials,
772
858
  headers: {
773
859
  ...authHeaders(),
774
860
  "content-type": "application/json; charset=utf-8"
@@ -781,6 +867,7 @@ function createServerAuth(options) {
781
867
  try {
782
868
  response = await fetchImpl(joinURL(authOrigin, "api/v1/auth/sign-out"), {
783
869
  method: "POST",
870
+ credentials,
784
871
  headers: authHeaders(),
785
872
  redirect: "manual",
786
873
  signal: AbortSignal.timeout(timeoutMs)
@@ -816,5 +903,10 @@ function authCookieHeader(value) {
816
903
  return name.startsWith("better-auth.") || name.startsWith("__Secure-better-auth.");
817
904
  }).join("; ");
818
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
+ }
819
911
  //#endregion
820
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.5.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",
@@ -48,7 +49,7 @@
48
49
  "typecheck": "tsc --noEmit"
49
50
  },
50
51
  "devDependencies": {
51
- "@chiballc/utils": "2.2.22",
52
+ "@chiballc/utils": "2.5.0",
52
53
  "@types/node": "24.13.3",
53
54
  "@typescript/native": "npm:typescript@7.0.2",
54
55
  "@vitest/coverage-v8": "4.1.10",
@@ -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 };