@meistrari/auth-nuxt 3.18.1 → 3.20.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/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
3
  "configKey": "telaAuth",
4
- "version": "3.18.1",
4
+ "version": "3.20.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,8 +1,9 @@
1
- import type { AdminApplicationRecord, AdminApplicationEntitlementRecord, AdminOrganization, AdminOrganizationTeamRecord, CreateAdminApplicationEntitlementOptions, CreateAdminApplicationOptions, CreateAdminOrganizationOptions, CreateAdminOrganizationTeamOptions, GetAdminOrganizationOptions, ListAdminApplicationsOptions, ListAdminApplicationsResult, ListAdminOrganizationMembersOptions, ListAdminOrganizationMembersResult, ListAdminOrganizationsOptions, ListAdminOrganizationsResult, ListAdminOrganizationTeamsOptions, ListAdminOrganizationTeamsResult, ListAdminUsersOptions, ListAdminUsersResult, UpdateAdminOrganizationOptions } from '@meistrari/auth-core';
1
+ import type { AdminApplicationRecord, AdminApplicationEntitlementRecord, AdminOrganization, AdminOrganizationTeamRecord, AdminUserAccess, CreateAdminApplicationEntitlementOptions, CreateAdminApplicationOptions, CreateAdminOrganizationOptions, CreateAdminOrganizationTeamOptions, GetAdminOrganizationOptions, InviteAdminOrganizationUsersOptions, InviteAdminOrganizationUsersResult, ListAdminApplicationsOptions, ListAdminApplicationsResult, ListAdminOrganizationMembersOptions, ListAdminOrganizationMembersResult, ListAdminOrganizationsOptions, ListAdminOrganizationsResult, ListAdminOrganizationTeamsOptions, ListAdminOrganizationTeamsResult, ListAdminUsersOptions, ListAdminUsersResult, AdminMember, RemoveAdminMemberOptions, ResendAdminInviteOptions, SetAdminUserApplicationRoleOptions, SetAdminUserTeamOptions, UpdateAdminMemberRoleOptions, UpdateAdminOrganizationOptions } from '@meistrari/auth-core';
2
2
  export interface UseTelaAdminReturn {
3
3
  /**
4
- * Lists users from the Better Auth `user` table as a global admin.
5
- * @param options - Pagination, search, filter, and ordering options
4
+ * Lists users as a global admin, optionally with organization previews
5
+ * and totals when a preview size is requested.
6
+ * @param options - Pagination, ordering, search, and preview options
6
7
  * @returns The paginated user listing
7
8
  */
8
9
  listUsers: (options?: ListAdminUsersOptions) => Promise<ListAdminUsersResult>;
@@ -64,6 +65,14 @@ export interface UseTelaAdminReturn {
64
65
  * @returns The created organization team
65
66
  */
66
67
  createOrganizationTeam: (options: CreateAdminOrganizationTeamOptions) => Promise<AdminOrganizationTeamRecord>;
68
+ /**
69
+ * Fetches the per-organization access report for a single user as a global
70
+ * admin, including organization roles, teams, and entitled applications with
71
+ * the user's effective application role.
72
+ * @param userId - ID of the user to fetch access for
73
+ * @returns The user access report
74
+ */
75
+ getUserAccess: (userId: string) => Promise<AdminUserAccess>;
67
76
  /**
68
77
  * Creates an organization as a global admin without joining it.
69
78
  * @param options - Organization name and optional slug, logo, metadata, and settings
@@ -76,6 +85,62 @@ export interface UseTelaAdminReturn {
76
85
  * @returns The updated organization
77
86
  */
78
87
  updateOrganization: (options: UpdateAdminOrganizationOptions) => Promise<AdminOrganization>;
88
+ /**
89
+ * Updates an organization member's role as a global admin.
90
+ * @param options - Membership ID and the new role
91
+ * @returns The updated membership
92
+ */
93
+ updateMemberRole: (options: UpdateAdminMemberRoleOptions) => Promise<AdminMember>;
94
+ /**
95
+ * Removes an organization member as a global admin.
96
+ * @param options - Membership ID to remove
97
+ * @returns The removed membership
98
+ */
99
+ removeMember: (options: RemoveAdminMemberOptions) => Promise<AdminMember>;
100
+ /**
101
+ * Sets a user's effective application role within an organization as a
102
+ * global admin. A non-null role grants access, a null role denies access
103
+ * (leave application), and clear removes the user-specific override.
104
+ * @param options - User, organization, application, and the role or clear flag
105
+ */
106
+ setUserApplicationRole: (options: SetAdminUserApplicationRoleOptions) => Promise<void>;
107
+ /**
108
+ * Reconciles a user's team memberships within an organization to a given set
109
+ * as a global admin.
110
+ * @param options - User, organization, and the desired teams (empty to clear)
111
+ */
112
+ setUserTeam: (options: SetAdminUserTeamOptions) => Promise<void>;
113
+ /**
114
+ * Bans a user from the auth system and invalidates all their sessions.
115
+ * @param userId - ID of the user to ban
116
+ */
117
+ banUser: (userId: string) => Promise<void>;
118
+ /**
119
+ * Lifts a user's ban.
120
+ * @param userId - ID of the user to unban
121
+ */
122
+ unbanUser: (userId: string) => Promise<void>;
123
+ /**
124
+ * Permanently removes a user from the auth system.
125
+ * @param userId - ID of the user to remove
126
+ */
127
+ removeUser: (userId: string) => Promise<void>;
128
+ /**
129
+ * Cancels a pending invitation as a global admin.
130
+ * @param invitationId - ID of the invitation to cancel
131
+ */
132
+ cancelInvite: (invitationId: string) => Promise<void>;
133
+ /**
134
+ * Re-sends an organization invitation email as a global admin.
135
+ * @param options - Organization, email, role, and optional application
136
+ */
137
+ resendInvite: (options: ResendAdminInviteOptions) => Promise<void>;
138
+ /**
139
+ * Invites several users to an organization in one call, isolating failures
140
+ * per email in the results payload.
141
+ * @param options - Organization, emails, role, and optional teams/application
142
+ */
143
+ inviteOrganizationUsers: (options: InviteAdminOrganizationUsersOptions) => Promise<InviteAdminOrganizationUsersResult>;
79
144
  }
80
145
  /**
81
146
  * Composable for global admin operations.
@@ -136,12 +136,45 @@ export function useTelaAdmin() {
136
136
  async function createOrganizationTeam(options) {
137
137
  return await authClient.admin.createOrganizationTeam(options);
138
138
  }
139
+ async function getUserAccess(userId) {
140
+ return await authClient.admin.getUserAccess(userId);
141
+ }
139
142
  async function createOrganization(options) {
140
143
  return await authClient.admin.createOrganization(options);
141
144
  }
142
145
  async function updateOrganization(options) {
143
146
  return await authClient.admin.updateOrganization(options);
144
147
  }
148
+ async function updateMemberRole(options) {
149
+ return await authClient.admin.updateMemberRole(options);
150
+ }
151
+ async function removeMember(options) {
152
+ return await authClient.admin.removeMember(options);
153
+ }
154
+ async function setUserApplicationRole(options) {
155
+ return await authClient.admin.setUserApplicationRole(options);
156
+ }
157
+ async function setUserTeam(options) {
158
+ return await authClient.admin.setUserTeam(options);
159
+ }
160
+ async function banUser(userId) {
161
+ return await authClient.admin.banUser(userId);
162
+ }
163
+ async function unbanUser(userId) {
164
+ return await authClient.admin.unbanUser(userId);
165
+ }
166
+ async function removeUser(userId) {
167
+ return await authClient.admin.removeUser(userId);
168
+ }
169
+ async function cancelInvite(invitationId) {
170
+ return await authClient.admin.cancelInvite(invitationId);
171
+ }
172
+ async function resendInvite(options) {
173
+ return await authClient.admin.resendInvite(options);
174
+ }
175
+ async function inviteOrganizationUsers(options) {
176
+ return await authClient.admin.inviteOrganizationUsers(options);
177
+ }
145
178
  return {
146
179
  listUsers,
147
180
  listApplications,
@@ -153,7 +186,18 @@ export function useTelaAdmin() {
153
186
  listOrganizationMembers,
154
187
  listOrganizationTeams,
155
188
  createOrganizationTeam,
189
+ getUserAccess,
156
190
  createOrganization,
157
- updateOrganization
191
+ updateOrganization,
192
+ updateMemberRole,
193
+ removeMember,
194
+ setUserApplicationRole,
195
+ setUserTeam,
196
+ banUser,
197
+ unbanUser,
198
+ removeUser,
199
+ cancelInvite,
200
+ resendInvite,
201
+ inviteOrganizationUsers
158
202
  };
159
203
  }
@@ -1,4 +1,4 @@
1
- import type { CreateTeamPayload, FullOrganization, Invitation, InviteUserToOrganizationOptions, ListMembersOptions, Member, RemoveUserFromOrganizationOptions, Team, TeamMember, UpdateMemberRoleOptions, UpdateTeamPayload } from '@meistrari/auth-core';
1
+ import type { AcceptInvitationResult, CreateTeamPayload, FullOrganization, Invitation, InviteUserToOrganizationOptions, ListMembersOptions, Member, RemoveUserFromOrganizationOptions, Team, TeamMember, UpdateMemberRoleOptions, UpdateTeamPayload } from '@meistrari/auth-core';
2
2
  import type { Ref } from 'vue';
3
3
  export interface UseTelaApplicationOrganizationReturn {
4
4
  /** Reactive reference to the active organization with members, invitations, and teams. */
@@ -84,11 +84,9 @@ export interface UseTelaApplicationOrganizationReturn {
84
84
  * afterwards.
85
85
  *
86
86
  * @param id - The invitation id to accept.
87
- * @returns An object with `homeUrl`: the application's home URL, or `null`.
87
+ * @returns The accepted member and the application's home URL, or `null`.
88
88
  */
89
- acceptInvitation: (id: string) => Promise<{
90
- homeUrl: string | null;
91
- }>;
89
+ acceptInvitation: (id: string) => Promise<AcceptInvitationResult>;
92
90
  /**
93
91
  * Cancels a pending invitation before it is accepted, revoking it so the link
94
92
  * can no longer be used, and removes it from `activeOrganization.invitations`
@@ -49,7 +49,7 @@ export function useTelaOrganization() {
49
49
  return invitation;
50
50
  }
51
51
  async function acceptInvitation(id) {
52
- const { homeUrl } = await authClient.organization.acceptInvitation(id);
52
+ const result = await authClient.organization.acceptInvitation(id);
53
53
  const members = await authClient.organization.listMembers();
54
54
  if (activeOrganization.value) {
55
55
  activeOrganization.value = {
@@ -57,7 +57,7 @@ export function useTelaOrganization() {
57
57
  members
58
58
  };
59
59
  }
60
- return { homeUrl };
60
+ return result;
61
61
  }
62
62
  async function cancelInvitation(id) {
63
63
  await authClient.organization.cancelInvitation(id);
@@ -1,4 +1,4 @@
1
- import type { CreateTeamPayload, FullOrganization, Invitation, InviteUserToOrganizationOptions, ListMembersOptions, Member, RemoveUserFromOrganizationOptions, Team, TeamMember, UpdateMemberRoleOptions, UpdateOrganizationPayload, UpdateTeamPayload } from '@meistrari/auth-core';
1
+ import type { AcceptInvitationResult, CreateTeamPayload, FullOrganization, Invitation, InviteUserToOrganizationOptions, ListMembersOptions, Member, RemoveUserFromOrganizationOptions, Team, TeamMember, UpdateMemberRoleOptions, UpdateOrganizationPayload, UpdateTeamPayload } from '@meistrari/auth-core';
2
2
  import type { Ref } from 'vue';
3
3
  export interface UseTelaOrganizationReturn {
4
4
  /** Reactive reference to the active organization with members, invitations, and teams. */
@@ -46,12 +46,10 @@ export interface UseTelaOrganizationReturn {
46
46
  /**
47
47
  * Accepts an organization invitation and updates the active organization state.
48
48
  * @param id - The invitation ID to accept
49
- * @returns Object containing the application's `homeUrl` when the invitation is
50
- * scoped to an application that defines one; otherwise `null`.
49
+ * @returns The accepted member and the application's `homeUrl` when the invitation
50
+ * is scoped to an application that defines one; otherwise `null`.
51
51
  */
52
- acceptInvitation: (id: string) => Promise<{
53
- homeUrl: string | null;
54
- }>;
52
+ acceptInvitation: (id: string) => Promise<AcceptInvitationResult>;
55
53
  /**
56
54
  * Cancels a pending organization invitation and updates the active organization state.
57
55
  * @param id - The invitation ID to cancel
@@ -58,7 +58,7 @@ export function useTelaOrganization() {
58
58
  return invitation;
59
59
  }
60
60
  async function acceptInvitation(id) {
61
- const { homeUrl } = await authClient.organization.acceptInvitation(id);
61
+ const result = await authClient.organization.acceptInvitation(id);
62
62
  const members = await authClient.organization.listMembers();
63
63
  if (activeOrganization.value) {
64
64
  activeOrganization.value = {
@@ -66,7 +66,7 @@ export function useTelaOrganization() {
66
66
  members
67
67
  };
68
68
  }
69
- return { homeUrl };
69
+ return result;
70
70
  }
71
71
  async function cancelInvitation(id) {
72
72
  await authClient.organization.cancelInvitation(id);
@@ -2,7 +2,15 @@ import { defineNuxtPlugin, useCookie, useRequestURL, useRuntimeConfig } from "#a
2
2
  import { navigateTo, useRoute } from "#imports";
3
3
  import { watch } from "vue";
4
4
  import { useTelaSession } from "../composables/session.js";
5
+ import { useOrganizationState } from "../composables/state.js";
6
+ import { readClientCookie } from "../helpers/client-cookies.js";
7
+ import { createRefreshOrchestrator } from "../helpers/refresh-orchestrator.js";
8
+ import { DEFAULT_REFRESH_POLICY } from "../helpers/refresh-policy.js";
9
+ import { createRefreshScheduler } from "../helpers/refresh-scheduler.js";
10
+ import { parseTokenExpiry } from "../helpers/token.js";
11
+ import { createWorkerTimer } from "../helpers/worker-timer.js";
5
12
  import { createNuxtAuthClient } from "../shared.js";
13
+ const TOKEN_FETCH_TIMEOUT_MS = 15e3;
6
14
  export default defineNuxtPlugin({
7
15
  name: "tela-auth-handshake",
8
16
  enforce: "pre",
@@ -11,13 +19,23 @@ export default defineNuxtPlugin({
11
19
  },
12
20
  async setup() {
13
21
  const authConfig = useRuntimeConfig().public.telaAuth;
14
- const { session, user, getToken } = useTelaSession();
22
+ const { session, user, signOut } = useTelaSession();
15
23
  const tokenCookie = useCookie(authConfig.jwtCookieName);
16
24
  tokenCookie.value = null;
17
25
  const authClient = createNuxtAuthClient(authConfig.apiUrl, () => tokenCookie.value ?? null);
18
26
  const query = useRoute().query;
19
27
  const nonce = query.nonce;
20
28
  const origin = useRequestURL();
29
+ async function initializeSession() {
30
+ if (session.value && user.value)
31
+ return;
32
+ const sessionData = await authClient.session.getSession();
33
+ if (!sessionData) {
34
+ return;
35
+ }
36
+ user.value = sessionData.user;
37
+ session.value = sessionData.session;
38
+ }
21
39
  if (import.meta.server) {
22
40
  if (nonce && typeof nonce === "string") {
23
41
  if (nonce !== "null") {
@@ -39,43 +57,97 @@ export default defineNuxtPlugin({
39
57
  });
40
58
  return;
41
59
  }
42
- } else if (import.meta.client && !session.value) {
43
- const token = await getToken();
44
- if (token) {
45
- tokenCookie.value = token;
60
+ if (tokenCookie.value) {
61
+ await initializeSession();
46
62
  }
47
- }
48
- async function initializeSession() {
49
- if (session.value && user.value)
50
- return;
51
- const sessionData = await authClient.session.getSession();
52
- if (!sessionData) {
53
- return;
54
- }
55
- user.value = sessionData.user;
56
- session.value = sessionData.session;
57
- }
58
- if (tokenCookie.value) {
59
- await initializeSession();
63
+ return;
60
64
  }
61
65
  if (import.meta.client) {
62
- let tokenRefreshInterval = null;
63
- watch(session, async (newVal) => {
64
- if (tokenRefreshInterval) {
65
- clearTimeout(tokenRefreshInterval);
66
- tokenRefreshInterval = null;
66
+ const readAccessToken = () => readClientCookie(authConfig.jwtCookieName) ?? tokenCookie.value ?? null;
67
+ async function signOutSafely() {
68
+ try {
69
+ await signOut();
70
+ } catch {
71
+ const { activeOrganization, activeMember } = useOrganizationState();
72
+ tokenCookie.value = null;
73
+ user.value = null;
74
+ session.value = null;
75
+ activeOrganization.value = null;
76
+ activeMember.value = null;
77
+ }
78
+ }
79
+ async function fetchToken() {
80
+ let timeoutId;
81
+ const timeout = new Promise((_, reject) => {
82
+ timeoutId = setTimeout(() => reject(new Error("Token fetch timed out")), TOKEN_FETCH_TIMEOUT_MS);
83
+ });
84
+ try {
85
+ const { token } = await Promise.race([authClient.session.getToken(), timeout]);
86
+ return token;
87
+ } finally {
88
+ clearTimeout(timeoutId);
89
+ }
90
+ }
91
+ const orchestrator = createRefreshOrchestrator({
92
+ performRefresh: async () => {
93
+ tokenCookie.value = await fetchToken();
94
+ },
95
+ readAccessToken,
96
+ parseTokenExpiry,
97
+ policy: DEFAULT_REFRESH_POLICY,
98
+ lockName: "tela-auth-handshake-token-refresh"
99
+ });
100
+ let startupRefreshFailed = false;
101
+ if (!session.value) {
102
+ const outcome = await orchestrator.ensureFreshToken({ force: true });
103
+ if (outcome.status === "auth-failed") {
104
+ await signOutSafely();
105
+ }
106
+ startupRefreshFailed = outcome.status === "transient-failed" || outcome.status === "skipped-offline";
107
+ }
108
+ if (tokenCookie.value) {
109
+ await initializeSession();
110
+ }
111
+ const scheduler = createRefreshScheduler({
112
+ ensureFreshToken: orchestrator.ensureFreshToken,
113
+ readAccessToken,
114
+ parseTokenExpiry,
115
+ policy: DEFAULT_REFRESH_POLICY,
116
+ timer: createWorkerTimer(),
117
+ // Reachable only from a definitive 401/404 — every other
118
+ // failure keeps the session and reschedules. Middleware
119
+ // handles the redirect to the login page after sign-out.
120
+ onAuthFailure: async () => {
121
+ await signOutSafely();
67
122
  }
123
+ });
124
+ watch(session, (newVal) => {
68
125
  if (newVal) {
69
- async function createTokenRefreshInterval() {
70
- const token = await getToken();
71
- if (token) {
72
- tokenCookie.value = token;
73
- }
74
- tokenRefreshInterval = window.setTimeout(() => void createTokenRefreshInterval(), 6e4);
75
- }
76
- await createTokenRefreshInterval();
126
+ scheduler.start();
127
+ } else {
128
+ scheduler.stop();
77
129
  }
78
130
  }, { immediate: true });
131
+ if (startupRefreshFailed) {
132
+ const stopTokenWatch = watch(tokenCookie, async (token) => {
133
+ if (!token || session.value) {
134
+ return;
135
+ }
136
+ try {
137
+ await initializeSession();
138
+ stopTokenWatch();
139
+ } catch {
140
+ }
141
+ });
142
+ scheduler.start();
143
+ }
144
+ document.addEventListener("visibilitychange", () => {
145
+ if (document.visibilityState === "visible") {
146
+ scheduler.kick();
147
+ }
148
+ });
149
+ window.addEventListener("online", () => scheduler.kick());
150
+ window.addEventListener("focus", () => scheduler.kick());
79
151
  }
80
152
  },
81
153
  hooks: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
- "version": "3.18.1",
3
+ "version": "3.20.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -36,7 +36,7 @@
36
36
  "docs": "nuxt-module-build prepare && typedoc"
37
37
  },
38
38
  "dependencies": {
39
- "@meistrari/auth-core": "1.28.1",
39
+ "@meistrari/auth-core": "1.30.0",
40
40
  "jose": "6.1.3"
41
41
  },
42
42
  "peerDependencies": {