@finueva/auth 0.1.0 → 0.4.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.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { a as AuthSession, c as DriveCredentialInput, n as AuthErrorCode, o as AuthUser, r as AuthErrorStatus, s as DriveCredential, t as AuthError, u as PersonalWorkspaceSelection } from "./errors-C-cHFWC7.js";
1
+ import { c as AuthUser, f as InvitationAcceptance, g as WorkspaceSelection, l as DriveCredential, m as PersonalWorkspaceSelection, o as AuthSession, p as OrganizationWorkspaceSelection, r as AuthOrganization, t as AcceptInvitationInput, u as DriveCredentialInput } from "./types-zEqO1_r2.js";
2
+ import { n as AuthErrorCode, r as AuthErrorStatus, t as AuthError } from "./errors-Crd93Rah.js";
2
3
  //#region src/server.d.ts
3
4
  interface ServerAuthOptions {
4
5
  authOrigin: string;
@@ -11,11 +12,16 @@ interface ServerAuthOptions {
11
12
  interface ServerSignOutResult {
12
13
  applyCookies(headers: Headers): void;
13
14
  }
15
+ interface ServerInvitationAcceptance extends InvitationAcceptance {
16
+ applyCookies(headers: Headers): void;
17
+ }
14
18
  interface FinuevaServerAuth {
19
+ acceptInvitation(input: AcceptInvitationInput): Promise<ServerInvitationAcceptance>;
15
20
  getSession(): Promise<AuthSession | null>;
21
+ listOrganizations(): Promise<AuthOrganization[]>;
16
22
  getDriveCredential(input: DriveCredentialInput): Promise<DriveCredential>;
17
23
  signOut(): Promise<ServerSignOutResult>;
18
24
  }
19
25
  declare function createServerAuth(options: ServerAuthOptions): FinuevaServerAuth;
20
26
  //#endregion
21
- export { AuthError, type AuthErrorCode, type AuthErrorStatus, type AuthSession, type AuthUser, type DriveCredential, type DriveCredentialInput, FinuevaServerAuth, type PersonalWorkspaceSelection, ServerAuthOptions, ServerSignOutResult, createServerAuth };
27
+ export { type AcceptInvitationInput, AuthError, type AuthErrorCode, type AuthErrorStatus, type AuthOrganization, type AuthSession, type AuthUser, type DriveCredential, type DriveCredentialInput, FinuevaServerAuth, type InvitationAcceptance, type OrganizationWorkspaceSelection, type PersonalWorkspaceSelection, ServerAuthOptions, ServerInvitationAcceptance, ServerSignOutResult, type WorkspaceSelection, createServerAuth };
package/dist/server.js CHANGED
@@ -1,26 +1,3 @@
1
- //#region src/errors.ts
2
- const messages = {
3
- invalid_credentials: "The credentials are invalid.",
4
- authentication_required: "Authentication is required.",
5
- request_origin_denied: "The request origin is not allowed.",
6
- invalid_workspace_selection: "The workspace selection is invalid.",
7
- credential_rate_limited: "Credential requests are temporarily limited.",
8
- state_conflict: "The authentication state conflicts with the request.",
9
- access_denied: "Access is denied.",
10
- identity_authority_unavailable: "Authentication is temporarily unavailable."
11
- };
12
- var AuthError = class extends Error {
13
- code;
14
- status;
15
- retryAfterSeconds;
16
- constructor(code, status, retryAfterSeconds) {
17
- super(messages[code]);
18
- this.name = "AuthError";
19
- this.code = code;
20
- this.status = status;
21
- this.retryAfterSeconds = retryAfterSeconds;
22
- }
23
- };
24
1
  String.fromCharCode;
25
2
  const HASH_RE = /#/g;
26
3
  const AMPERSAND_RE = /&/g;
@@ -459,6 +436,35 @@ function createFetch(globalOptions = {}) {
459
436
  return $fetch;
460
437
  }
461
438
  //#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
462
468
  //#region src/shared.ts
463
469
  const id = /^[A-Za-z0-9_-]{1,128}$/;
464
470
  const email = /^[^\s@]+@[^\s@]+$/;
@@ -505,6 +511,37 @@ function normalizeDriveCredential(value, now) {
505
511
  expiresAt: new Date(now + value.expiresIn * 1e3)
506
512
  };
507
513
  }
514
+ function normalizeOrganizations(value) {
515
+ if (!Array.isArray(value) || value.length >= 100) throw authorityUnavailable();
516
+ const seen = new Set();
517
+ return value.map((organization) => {
518
+ if (!isObject(organization) || !isId(organization.id) || seen.has(organization.id) || typeof organization.name !== "string" || organization.name.length < 1 || typeof organization.slug !== "string" || organization.slug.length < 1) throw authorityUnavailable();
519
+ seen.add(organization.id);
520
+ return {
521
+ id: organization.id,
522
+ name: organization.name,
523
+ slug: organization.slug
524
+ };
525
+ });
526
+ }
527
+ function normalizeInvitationInput(value) {
528
+ if (!isObject(value) || Object.keys(value).length !== 1 || !isId(value.invitationId)) throw new AuthError("invalid_invitation", 422);
529
+ return { invitationId: value.invitationId };
530
+ }
531
+ function normalizeInvitationAcceptance(value, invitationId) {
532
+ if (!isObject(value) || !isObject(value.invitation) || !isObject(value.member) || value.invitation.id !== invitationId || value.invitation.status !== "accepted" || !isId(value.invitation.organizationId) || value.member.organizationId !== value.invitation.organizationId) throw authorityUnavailable();
533
+ return { organizationId: value.invitation.organizationId };
534
+ }
535
+ function normalizeWorkspaceSelection(value) {
536
+ if (!isObject(value)) throw new AuthError("invalid_workspace_selection", 422);
537
+ const keys = Object.keys(value);
538
+ if (value.type === "personal" && keys.length === 1 && keys[0] === "type") return { type: "personal" };
539
+ if (value.type === "organization" && keys.length === 2 && keys.includes("type") && keys.includes("organizationId") && typeof value.organizationId === "string" && id.test(value.organizationId)) return {
540
+ type: "organization",
541
+ organizationId: value.organizationId
542
+ };
543
+ throw new AuthError("invalid_workspace_selection", 422);
544
+ }
508
545
  function fromAuthorityError(value, fallbackStatus, retryAfter) {
509
546
  const mapped = mapError(isObject(value) && typeof value.code === "string" ? value.code : void 0, fallbackStatus);
510
547
  return new AuthError(mapped.code, mapped.status, parseRetryAfter(retryAfter));
@@ -512,6 +549,23 @@ function fromAuthorityError(value, fallbackStatus, retryAfter) {
512
549
  function authorityUnavailable() {
513
550
  return new AuthError("identity_authority_unavailable", 503);
514
551
  }
552
+ function fromOrganizationListError(error) {
553
+ if (error instanceof AuthError && error.status === 401) return new AuthError("authentication_required", 401);
554
+ if (error instanceof AuthError) return error;
555
+ return authorityUnavailable();
556
+ }
557
+ function fromInvitationAuthorityError(value, status) {
558
+ const code = isObject(value) && typeof value.code === "string" ? value.code : void 0;
559
+ if (status === 401 && code === "UNAUTHORIZED") return new AuthError("authentication_required", 401);
560
+ if (status === 403 && (code === "INVALID_ORIGIN" || code === "MISSING_OR_NULL_ORIGIN")) return new AuthError("request_origin_denied", 403);
561
+ if (status === 403 && code === "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION") return new AuthError("invitation_recipient_mismatch", 403);
562
+ if (status === 403 && code === "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION") return new AuthError("email_verification_required", 403);
563
+ if (status === 400 && (code === "INVITATION_NOT_FOUND" || code === "ORGANIZATION_NOT_FOUND" || code === "TEAM_NOT_FOUND")) return new AuthError("invitation_unavailable", 409);
564
+ return authorityUnavailable();
565
+ }
566
+ function fromInvitationError(error) {
567
+ return error instanceof AuthError ? error : authorityUnavailable();
568
+ }
515
569
  function mapError(code, status) {
516
570
  if (code === "authentication_required") return {
517
571
  code,
@@ -525,6 +579,10 @@ function mapError(code, status) {
525
579
  code,
526
580
  status: 422
527
581
  };
582
+ if (code === "organization_context_too_large") return {
583
+ code,
584
+ status: 422
585
+ };
528
586
  if (code === "credential_rate_limited") return {
529
587
  code,
530
588
  status: 429
@@ -577,6 +635,9 @@ function isObject(value) {
577
635
  //#region src/http.ts
578
636
  const MAX_RESPONSE_BYTES = 64 * 1024;
579
637
  async function requestJson(url, options) {
638
+ return (await requestJsonResponse(url, options)).value;
639
+ }
640
+ async function requestJsonResponse(url, options) {
580
641
  let response;
581
642
  try {
582
643
  response = await createFetch({ fetch: options.fetch }).raw(url, {
@@ -587,6 +648,7 @@ async function requestJson(url, options) {
587
648
  retry: 0,
588
649
  timeout: options.timeoutMs,
589
650
  redirect: "manual",
651
+ signal: options.signal,
590
652
  responseType: "stream",
591
653
  ignoreResponseError: true
592
654
  });
@@ -597,9 +659,12 @@ async function requestJson(url, options) {
597
659
  if (response.status >= 300 || response.status < 200 || contentType !== "application/json") {
598
660
  const body = contentType === "application/json" ? await readJson(response) : void 0;
599
661
  await response.body?.cancel();
600
- throw fromAuthorityError(body, response.status, response.headers.get("retry-after"));
662
+ throw (options.mapError ?? fromAuthorityError)(body, response.status, response.headers.get("retry-after"));
601
663
  }
602
- return readJson(response);
664
+ return {
665
+ value: await readJson(response),
666
+ headers: response.headers
667
+ };
603
668
  }
604
669
  async function readJson(response) {
605
670
  const length = response.headers.get("content-length");
@@ -652,6 +717,32 @@ function createServerAuth(options) {
652
717
  origin: appOrigin
653
718
  });
654
719
  return {
720
+ async acceptInvitation(input) {
721
+ const invitation = normalizeInvitationInput(input);
722
+ try {
723
+ const result = await requestJsonResponse(joinURL(authOrigin, "api/v1/auth/organization/accept-invitation"), {
724
+ fetch: fetchImpl,
725
+ timeoutMs,
726
+ method: "POST",
727
+ headers: {
728
+ ...authHeaders(),
729
+ "content-type": "application/json; charset=utf-8"
730
+ },
731
+ body: invitation,
732
+ mapError: fromInvitationAuthorityError
733
+ });
734
+ const acceptance = normalizeInvitationAcceptance(result.value, invitation.invitationId);
735
+ const cookies = responseCookies(result.headers);
736
+ return {
737
+ ...acceptance,
738
+ applyCookies(headers) {
739
+ applyCookies(cookies, headers);
740
+ }
741
+ };
742
+ } catch (error) {
743
+ throw fromInvitationError(error);
744
+ }
745
+ },
655
746
  async getSession() {
656
747
  return normalizeSession(await requestJson(joinURL(authOrigin, "api/v1/auth/get-session"), {
657
748
  fetch: fetchImpl,
@@ -660,8 +751,20 @@ function createServerAuth(options) {
660
751
  headers: { cookie }
661
752
  }));
662
753
  },
754
+ async listOrganizations() {
755
+ try {
756
+ return normalizeOrganizations(await requestJson(joinURL(authOrigin, "api/v1/auth/organization/list"), {
757
+ fetch: fetchImpl,
758
+ timeoutMs,
759
+ method: "GET",
760
+ headers: authHeaders()
761
+ }));
762
+ } catch (error) {
763
+ throw fromOrganizationListError(error);
764
+ }
765
+ },
663
766
  async getDriveCredential(input) {
664
- if (input.workspace.type !== "personal") throw new AuthError("invalid_workspace_selection", 422);
767
+ const workspace = normalizeWorkspaceSelection(input?.workspace);
665
768
  return normalizeDriveCredential(await requestJson(joinURL(authOrigin, "api/v1/auth/drive-credential"), {
666
769
  fetch: fetchImpl,
667
770
  timeoutMs,
@@ -670,7 +773,7 @@ function createServerAuth(options) {
670
773
  ...authHeaders(),
671
774
  "content-type": "application/json; charset=utf-8"
672
775
  },
673
- body: { workspace: { type: "personal" } }
776
+ body: { workspace }
674
777
  }), now());
675
778
  },
676
779
  async signOut() {
@@ -689,15 +792,21 @@ function createServerAuth(options) {
689
792
  await response.body?.cancel();
690
793
  throw authorityUnavailable();
691
794
  }
692
- const getSetCookie = response.headers.getSetCookie;
693
- const cookies = getSetCookie ? getSetCookie.call(response.headers) : [response.headers.get("set-cookie")].filter(Boolean);
795
+ const cookies = responseCookies(response.headers);
694
796
  await response.body?.cancel();
695
797
  return { applyCookies(headers) {
696
- for (const value of cookies) headers.append("set-cookie", value);
798
+ applyCookies(cookies, headers);
697
799
  } };
698
800
  }
699
801
  };
700
802
  }
803
+ function responseCookies(headers) {
804
+ const getSetCookie = headers.getSetCookie;
805
+ return getSetCookie ? getSetCookie.call(headers) : [headers.get("set-cookie")].filter((value) => !!value);
806
+ }
807
+ function applyCookies(cookies, headers) {
808
+ for (const value of cookies) headers.append("set-cookie", value);
809
+ }
701
810
  function authCookieHeader(value) {
702
811
  if (!value) return "";
703
812
  return value.split(";").map((cookie) => cookie.trim()).filter((cookie) => {
@@ -0,0 +1,90 @@
1
+ //#region src/types.d.ts
2
+ interface AuthUser {
3
+ id: string;
4
+ name: string;
5
+ email: string;
6
+ emailVerified: boolean;
7
+ image: string | null;
8
+ }
9
+ interface AuthSession {
10
+ id: string;
11
+ expiresAt: Date;
12
+ user: AuthUser;
13
+ }
14
+ interface AuthOrganization {
15
+ id: string;
16
+ name: string;
17
+ slug: string;
18
+ }
19
+ interface AcceptInvitationInput {
20
+ invitationId: string;
21
+ }
22
+ interface InvitationAcceptance {
23
+ organizationId: string;
24
+ }
25
+ type AuthState = {
26
+ status: "loading";
27
+ } | {
28
+ status: "cached";
29
+ session: AuthSession;
30
+ authoritative: false;
31
+ } | {
32
+ status: "authenticated";
33
+ session: AuthSession;
34
+ authoritative: true;
35
+ } | {
36
+ status: "unauthenticated";
37
+ session: null;
38
+ authoritative: true;
39
+ } | {
40
+ status: "unavailable";
41
+ session: AuthSession | null;
42
+ authoritative: false;
43
+ };
44
+ interface AuthPersistenceDriver {
45
+ getItem(key: string): string | null | Promise<string | null>;
46
+ setItem(key: string, value: string): void | Promise<void>;
47
+ removeItem(key: string): void | Promise<void>;
48
+ }
49
+ type AuthPersistence = "local" | "session" | "memory" | AuthPersistenceDriver;
50
+ interface PersonalWorkspaceSelection {
51
+ type: "personal";
52
+ }
53
+ interface OrganizationWorkspaceSelection {
54
+ type: "organization";
55
+ organizationId: string;
56
+ }
57
+ type WorkspaceSelection = PersonalWorkspaceSelection | OrganizationWorkspaceSelection;
58
+ interface DriveCredential {
59
+ tokenType: "Bearer";
60
+ token: string;
61
+ expiresAt: Date;
62
+ }
63
+ interface SignInInput {
64
+ email: string;
65
+ password: string;
66
+ }
67
+ interface DriveCredentialInput {
68
+ workspace: WorkspaceSelection;
69
+ forceRefresh?: boolean;
70
+ }
71
+ interface AuthClientOptions {
72
+ authOrigin: string;
73
+ appOrigin: string;
74
+ fetch?: typeof globalThis.fetch;
75
+ now?: () => number;
76
+ persistence?: AuthPersistence;
77
+ timeoutMs?: number;
78
+ }
79
+ interface FinuevaAuthClient {
80
+ acceptInvitation(input: AcceptInvitationInput): Promise<InvitationAcceptance>;
81
+ getSession(): Promise<AuthSession | null>;
82
+ listOrganizations(): Promise<AuthOrganization[]>;
83
+ signIn(input: SignInInput): Promise<AuthSession>;
84
+ signOut(): Promise<void>;
85
+ getDriveCredential(input: DriveCredentialInput): Promise<DriveCredential>;
86
+ onAuthStateChange(listener: (state: AuthState) => void): () => void;
87
+ onSessionChange(listener: (session: AuthSession | null) => void): () => void;
88
+ }
89
+ //#endregion
90
+ export { AuthPersistenceDriver as a, AuthUser as c, FinuevaAuthClient as d, InvitationAcceptance as f, WorkspaceSelection as g, SignInInput 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finueva/auth",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "description": "Public browser and server SDK for Finueva Auth",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -18,6 +18,10 @@
18
18
  "./server": {
19
19
  "types": "./dist/server.d.ts",
20
20
  "import": "./dist/server.js"
21
+ },
22
+ "./device": {
23
+ "types": "./dist/device.d.ts",
24
+ "import": "./dist/device.js"
21
25
  }
22
26
  },
23
27
  "files": [
@@ -38,20 +42,24 @@
38
42
  "coverage": "vitest run --coverage",
39
43
  "lint": "eslint .",
40
44
  "test": "pnpm test:unit && pnpm build && pnpm test:package",
45
+ "test:composition": "node scripts/test-composition.mjs",
41
46
  "test:package": "node scripts/test-package.mjs",
42
47
  "test:unit": "vitest run",
43
48
  "typecheck": "tsc --noEmit"
44
49
  },
45
50
  "devDependencies": {
51
+ "@chiballc/utils": "2.2.22",
46
52
  "@types/node": "24.13.3",
47
53
  "@vitest/coverage-v8": "4.1.10",
48
- "@chiballc/utils": "2.2.22",
49
54
  "better-auth": "1.6.22",
55
+ "miniflare": "4.20260722.0",
50
56
  "ofetch": "1.5.1",
57
+ "playwright": "1.62.0",
51
58
  "rolldown": "1.2.0",
52
59
  "rolldown-plugin-dts": "0.27.14",
53
60
  "typescript": "5.9.2",
54
61
  "ufo": "1.6.4",
62
+ "unstorage": "1.17.5",
55
63
  "vitest": "4.1.10"
56
64
  }
57
65
  }
@@ -1,55 +0,0 @@
1
- //#region src/types.d.ts
2
- interface AuthUser {
3
- id: string;
4
- name: string;
5
- email: string;
6
- emailVerified: boolean;
7
- image: string | null;
8
- }
9
- interface AuthSession {
10
- id: string;
11
- expiresAt: Date;
12
- user: AuthUser;
13
- }
14
- interface PersonalWorkspaceSelection {
15
- type: "personal";
16
- }
17
- interface DriveCredential {
18
- tokenType: "Bearer";
19
- token: string;
20
- expiresAt: Date;
21
- }
22
- interface SignInInput {
23
- email: string;
24
- password: string;
25
- }
26
- interface DriveCredentialInput {
27
- workspace: PersonalWorkspaceSelection;
28
- forceRefresh?: boolean;
29
- }
30
- interface AuthClientOptions {
31
- authOrigin: string;
32
- appOrigin: string;
33
- fetch?: typeof globalThis.fetch;
34
- now?: () => number;
35
- timeoutMs?: number;
36
- }
37
- interface FinuevaAuthClient {
38
- getSession(): Promise<AuthSession | null>;
39
- signIn(input: SignInInput): Promise<AuthSession>;
40
- signOut(): Promise<void>;
41
- getDriveCredential(input: DriveCredentialInput): Promise<DriveCredential>;
42
- onSessionChange(listener: (session: AuthSession | null) => void): () => void;
43
- }
44
- //#endregion
45
- //#region src/errors.d.ts
46
- type AuthErrorCode = "invalid_credentials" | "authentication_required" | "request_origin_denied" | "invalid_workspace_selection" | "credential_rate_limited" | "state_conflict" | "access_denied" | "identity_authority_unavailable";
47
- type AuthErrorStatus = 401 | 403 | 409 | 422 | 429 | 503;
48
- declare class AuthError extends Error {
49
- readonly code: AuthErrorCode;
50
- readonly status: AuthErrorStatus;
51
- readonly retryAfterSeconds?: number;
52
- constructor(code: AuthErrorCode, status: AuthErrorStatus, retryAfterSeconds?: number);
53
- }
54
- //#endregion
55
- export { AuthSession as a, DriveCredentialInput as c, SignInInput as d, AuthClientOptions as i, FinuevaAuthClient as l, AuthErrorCode as n, AuthUser as o, AuthErrorStatus as r, DriveCredential as s, AuthError as t, PersonalWorkspaceSelection as u };