@meistrari/auth-nuxt 2.4.0 → 4.0.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/README.md +8 -420
- package/dist/module.json +1 -1
- package/dist/runtime/composables/application-auth.d.ts +2 -31
- package/dist/runtime/composables/application-auth.js +5 -0
- package/dist/runtime/composables/state.d.ts +6 -147
- package/dist/runtime/plugins/application-token-refresh.js +7 -6
- package/dist/runtime/plugins/auth-guard.js +19 -16
- package/dist/runtime/server/routes/auth/callback.js +7 -0
- package/dist/runtime/server/routes/auth/organizations.d.ts +1 -1
- package/dist/runtime/server/routes/auth/refresh.d.ts +2 -16
- package/dist/runtime/server/routes/auth/switch-organization.d.ts +2 -16
- package/dist/runtime/server/routes/auth/whoami.d.ts +2 -16
- package/dist/runtime/server/utils/require-auth.js +0 -4
- package/dist/runtime/shared.d.ts +1 -2
- package/dist/runtime/types/page-meta.d.ts +1 -4
- package/package.json +51 -51
package/README.md
CHANGED
|
@@ -1,428 +1,16 @@
|
|
|
1
1
|
# @meistrari/auth-nuxt
|
|
2
2
|
|
|
3
|
-
A Nuxt module
|
|
3
|
+
A Nuxt module for authentication, organization management, and API key capabilities.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @meistrari/auth-nuxt
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Prerequisites
|
|
12
|
-
|
|
13
|
-
Before setting up the SDK, make sure the following are configured in the Auth API:
|
|
14
|
-
|
|
15
|
-
1. **Allowed Origins**: Your application URL (e.g., `https://your-app.com`) must be added to the allowed origins list in the Auth API. This ensures the Auth API accepts requests from your application.
|
|
16
|
-
2. **Allowed Email Domains**: The email domains of users who will access the application must be added to the allowed email domains list in the Auth API. For example, if your users sign in with `@company.com` emails, that domain must be whitelisted.
|
|
17
|
-
|
|
18
|
-
## Setup
|
|
19
|
-
|
|
20
|
-
Add the module to your `nuxt.config.ts`:
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
export default defineNuxtConfig({
|
|
24
|
-
modules: ['@meistrari/auth-nuxt'],
|
|
25
|
-
telaAuth: {
|
|
26
|
-
apiUrl: 'https://your-auth-api.com',
|
|
27
|
-
jwtCookieName: 'tela-jwt', // Optional: custom JWT cookie name
|
|
28
|
-
skipServerMiddleware: false, // Optional: skip automatic server middleware
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Configuration Options
|
|
34
|
-
|
|
35
|
-
| Option | Type | Default | Description |
|
|
36
|
-
|--------|------|---------|-------------|
|
|
37
|
-
| `apiUrl` | `string` | **Required** | Base URL of your authentication API |
|
|
38
|
-
| `jwtCookieName` | `string` | `'tela-jwt'` | Name of the JWT cookie |
|
|
39
|
-
| `skipServerMiddleware` | `boolean` | `false` | Skip automatic server-side auth context setup |
|
|
40
|
-
|
|
41
|
-
## Composables
|
|
42
|
-
|
|
43
|
-
The SDK provides three main composables for different aspects of authentication and organization management:
|
|
44
|
-
|
|
45
|
-
### useTelaSession
|
|
46
|
-
|
|
47
|
-
Manages user sessions, authentication, and sign-in/sign-out operations.
|
|
48
|
-
|
|
49
|
-
```vue
|
|
50
|
-
<script setup>
|
|
51
|
-
const { user, session, signOut, getToken } = useTelaSession()
|
|
52
|
-
|
|
53
|
-
// Access current user
|
|
54
|
-
console.log(user.value) // User object or null
|
|
55
|
-
|
|
56
|
-
// Access current session
|
|
57
|
-
console.log(session.value) // Session object or null
|
|
58
|
-
|
|
59
|
-
// Get a valid JWT token
|
|
60
|
-
const token = await getToken()
|
|
61
|
-
|
|
62
|
-
// Sign out
|
|
63
|
-
await signOut(() => {
|
|
64
|
-
console.log('User signed out')
|
|
65
|
-
})
|
|
66
|
-
</script>
|
|
67
|
-
|
|
68
|
-
<template>
|
|
69
|
-
<div v-if="user">
|
|
70
|
-
Welcome, {{ user.name }}!
|
|
71
|
-
<button @click="signOut">
|
|
72
|
-
Sign Out
|
|
73
|
-
</button>
|
|
74
|
-
</div>
|
|
75
|
-
<div v-else>
|
|
76
|
-
Please sign in
|
|
77
|
-
</div>
|
|
78
|
-
</template>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
#### Available Methods
|
|
82
|
-
|
|
83
|
-
**Session Management:**
|
|
84
|
-
- `getSession()` - Retrieves the current user session
|
|
85
|
-
- `getToken()` - Retrieves a valid JWT token (refreshes if needed)
|
|
86
|
-
- `signOut(callback?)` - Signs out the user
|
|
5
|
+
## Auth Types
|
|
87
6
|
|
|
88
|
-
|
|
89
|
-
- `signInWithEmailAndPassword(options)` - Email/password authentication
|
|
90
|
-
- `signInWithSocialProvider(options)` - Social authentication (Google, Microsoft)
|
|
91
|
-
- `signInWithSaml(options)` - SAML-based SSO authentication
|
|
7
|
+
This SDK supports two authentication models:
|
|
92
8
|
|
|
93
|
-
**
|
|
94
|
-
-
|
|
95
|
-
- `resetPassword(token, password)` - Completes password reset
|
|
9
|
+
- **Application Authentication (recommended)**: Use this for product applications that need end-user authentication and authorization scoped to a specific application. See [Application Authentication Guide](./docs/APPLICATION_AUTH.md).
|
|
10
|
+
- **First-Party Authentication**: Use this only for trusted first-party tools that interact directly with the Auth API in an administrative way, such as the Auth Dashboard and Backoffice. See [First-Party Authentication Guide](./docs/FIRST_PARTY_AUTH.md).
|
|
96
11
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
**Email and Password:**
|
|
100
|
-
```typescript
|
|
101
|
-
await signInWithEmailAndPassword({
|
|
102
|
-
email: 'user@example.com',
|
|
103
|
-
password: 'secure-password',
|
|
104
|
-
})
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
**Social Authentication:**
|
|
108
|
-
```typescript
|
|
109
|
-
await signInWithSocialProvider({
|
|
110
|
-
provider: 'google', // or 'microsoft'
|
|
111
|
-
callbackURL: '/',
|
|
112
|
-
errorCallbackURL: '/login?error=true'
|
|
113
|
-
})
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
**SAML SSO:**
|
|
117
|
-
```typescript
|
|
118
|
-
await signInWithSaml({
|
|
119
|
-
email: 'user@example.com',
|
|
120
|
-
callbackURL: '/',
|
|
121
|
-
errorCallbackURL: '/login?error=true'
|
|
122
|
-
})
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### useTelaOrganization
|
|
126
|
-
|
|
127
|
-
Manages organizations, members, invitations, and teams.
|
|
128
|
-
|
|
129
|
-
```vue
|
|
130
|
-
<script setup>
|
|
131
|
-
const {
|
|
132
|
-
activeOrganization,
|
|
133
|
-
activeMember,
|
|
134
|
-
getActiveOrganization,
|
|
135
|
-
setActiveOrganization,
|
|
136
|
-
inviteUserToOrganization
|
|
137
|
-
} = useTelaOrganization()
|
|
138
|
-
|
|
139
|
-
// Get the active organization
|
|
140
|
-
await getActiveOrganization()
|
|
141
|
-
|
|
142
|
-
// Switch organizations
|
|
143
|
-
await setActiveOrganization('org-id')
|
|
144
|
-
|
|
145
|
-
// Invite a user
|
|
146
|
-
await inviteUserToOrganization({
|
|
147
|
-
userEmail: 'user@example.com',
|
|
148
|
-
role: 'member'
|
|
149
|
-
})
|
|
150
|
-
</script>
|
|
151
|
-
|
|
152
|
-
<template>
|
|
153
|
-
<div v-if="activeOrganization">
|
|
154
|
-
<h2>{{ activeOrganization.name }}</h2>
|
|
155
|
-
<p>{{ activeOrganization.members.length }} members</p>
|
|
156
|
-
</div>
|
|
157
|
-
</template>
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
#### Available Methods
|
|
161
|
-
|
|
162
|
-
**Organization Management:**
|
|
163
|
-
- `getActiveOrganization()` - Gets the current active organization with members, invitations, and teams
|
|
164
|
-
- `listOrganizations()` - Lists all organizations for the user
|
|
165
|
-
- `setActiveOrganization(id)` - Sets the active organization
|
|
166
|
-
- `updateOrganization(payload)` - Updates organization details (name, logo, settings)
|
|
167
|
-
|
|
168
|
-
**Member Management:**
|
|
169
|
-
- `listMembers(options?)` - Lists organization members with pagination
|
|
170
|
-
- `getActiveMember()` - Gets the current user's member record
|
|
171
|
-
- `inviteUserToOrganization(options)` - Invites a user to the organization
|
|
172
|
-
- `removeUserFromOrganization(options)` - Removes a user from the organization
|
|
173
|
-
- `updateMemberRole(options)` - Updates a member's role
|
|
174
|
-
- `acceptInvitation(id)` - Accepts an organization invitation
|
|
175
|
-
- `cancelInvitation(id)` - Cancels a pending invitation
|
|
176
|
-
|
|
177
|
-
**Team Management:**
|
|
178
|
-
- `createTeam(payload)` - Creates a new team
|
|
179
|
-
- `updateTeam(id, payload)` - Updates team details
|
|
180
|
-
- `deleteTeam(id)` - Deletes a team
|
|
181
|
-
- `listTeams()` - Lists all teams
|
|
182
|
-
- `listTeamMembers(id)` - Lists members of a specific team
|
|
183
|
-
- `addTeamMember(teamId, userId)` - Adds a user to a team
|
|
184
|
-
- `removeTeamMember(teamId, userId)` - Removes a user from a team
|
|
185
|
-
|
|
186
|
-
#### Organization Examples
|
|
187
|
-
|
|
188
|
-
**Update Organization:**
|
|
189
|
-
```typescript
|
|
190
|
-
await updateOrganization({
|
|
191
|
-
name: 'New Organization Name',
|
|
192
|
-
logo: 'https://example.com/logo.png',
|
|
193
|
-
settings: { /* custom settings */ }
|
|
194
|
-
})
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Invite Member:**
|
|
198
|
-
```typescript
|
|
199
|
-
await inviteUserToOrganization({
|
|
200
|
-
userEmail: 'user@example.com',
|
|
201
|
-
role: 'admin', // or 'member', 'reviewer'
|
|
202
|
-
teamId: 'team-id', // optional
|
|
203
|
-
resend: false // optional: resend if invitation exists
|
|
204
|
-
})
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
**Create and Manage Teams:**
|
|
208
|
-
```typescript
|
|
209
|
-
// Create team
|
|
210
|
-
const team = await createTeam({
|
|
211
|
-
name: 'Development Team'
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
// Add member to team
|
|
215
|
-
await addTeamMember(team.id, 'user-id')
|
|
216
|
-
|
|
217
|
-
// List team members
|
|
218
|
-
const members = await listTeamMembers(team.id)
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### useTelaApiKey
|
|
222
|
-
|
|
223
|
-
Manages API keys for programmatic access.
|
|
224
|
-
|
|
225
|
-
```vue
|
|
226
|
-
<script setup>
|
|
227
|
-
const {
|
|
228
|
-
listApiKeys,
|
|
229
|
-
createApiKey,
|
|
230
|
-
deleteApiKey
|
|
231
|
-
} = useTelaApiKey()
|
|
232
|
-
|
|
233
|
-
// List all API keys
|
|
234
|
-
const apiKeys = await listApiKeys()
|
|
235
|
-
|
|
236
|
-
// Create a new API key
|
|
237
|
-
const newKey = await createApiKey({
|
|
238
|
-
name: 'Production API Key',
|
|
239
|
-
expiresIn: '90d',
|
|
240
|
-
prefix: 'prod',
|
|
241
|
-
metadata: { environment: 'production' }
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
// Delete an API key
|
|
245
|
-
await deleteApiKey('key-id')
|
|
246
|
-
</script>
|
|
247
|
-
|
|
248
|
-
<template>
|
|
249
|
-
<div v-for="key in apiKeys" :key="key.id">
|
|
250
|
-
<span>{{ key.name }}</span>
|
|
251
|
-
<button @click="deleteApiKey(key.id)">
|
|
252
|
-
Delete
|
|
253
|
-
</button>
|
|
254
|
-
</div>
|
|
255
|
-
</template>
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
#### Available Methods
|
|
259
|
-
|
|
260
|
-
- `listApiKeys()` - Lists all API keys for the current user
|
|
261
|
-
- `getApiKey(id)` - Retrieves a specific API key
|
|
262
|
-
- `createApiKey(payload)` - Creates a new API key
|
|
263
|
-
- `updateApiKey(payload)` - Updates an API key (name)
|
|
264
|
-
- `deleteApiKey(id)` - Deletes an API key
|
|
265
|
-
|
|
266
|
-
## Server-Side Usage
|
|
267
|
-
|
|
268
|
-
The SDK automatically sets up server-side authentication context for API routes.
|
|
269
|
-
|
|
270
|
-
### Using Authentication Context
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
// server/api/protected.ts
|
|
274
|
-
export default defineEventHandler(async (event) => {
|
|
275
|
-
// Authentication context is automatically available
|
|
276
|
-
const { user, workspace } = event.context.auth
|
|
277
|
-
|
|
278
|
-
if (!user) {
|
|
279
|
-
throw createError({
|
|
280
|
-
statusCode: 401,
|
|
281
|
-
statusMessage: 'Unauthorized'
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
message: `Hello, ${user.email}!`,
|
|
287
|
-
userId: user.id
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Custom Middleware
|
|
293
|
-
|
|
294
|
-
Create custom server middleware using the provided helper:
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
// server/middleware/custom.ts
|
|
298
|
-
import { meistrariAuthMiddleware } from '@meistrari/auth-nuxt/server/middleware/auth'
|
|
299
|
-
|
|
300
|
-
export default meistrariAuthMiddleware(async (event) => {
|
|
301
|
-
// event.context.auth contains user and workspace
|
|
302
|
-
const { user, workspace } = event.context.auth
|
|
303
|
-
|
|
304
|
-
// Your custom logic here
|
|
305
|
-
if (user) {
|
|
306
|
-
console.log(`Authenticated user: ${user.email}`)
|
|
307
|
-
}
|
|
308
|
-
})
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
## Types
|
|
312
|
-
|
|
313
|
-
The SDK exports comprehensive TypeScript types from the core package:
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
import type {
|
|
317
|
-
User,
|
|
318
|
-
Session,
|
|
319
|
-
Organization,
|
|
320
|
-
FullOrganization,
|
|
321
|
-
Member,
|
|
322
|
-
Invitation,
|
|
323
|
-
Team,
|
|
324
|
-
TeamMember,
|
|
325
|
-
ApiKey,
|
|
326
|
-
CreateApiKeyPayload,
|
|
327
|
-
UpdateApiKeyPayload,
|
|
328
|
-
CreateTeamPayload,
|
|
329
|
-
UpdateTeamPayload,
|
|
330
|
-
InviteUserToOrganizationOptions,
|
|
331
|
-
RemoveUserFromOrganizationOptions,
|
|
332
|
-
UpdateMemberRoleOptions,
|
|
333
|
-
UpdateOrganizationPayload
|
|
334
|
-
} from '@meistrari/auth-nuxt/core'
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Key Types
|
|
338
|
-
|
|
339
|
-
- **User**: User account information including email, name, and verification status
|
|
340
|
-
- **Session**: Active session data with expiration and organization info
|
|
341
|
-
- **Organization**: Basic organization entity
|
|
342
|
-
- **FullOrganization**: Organization with members, invitations, and teams
|
|
343
|
-
- **Member**: Organization member with role and team assignments
|
|
344
|
-
- **Team**: Team entity within an organization
|
|
345
|
-
- **Invitation**: Pending organization invitations
|
|
346
|
-
- **ApiKey**: API key entity with metadata
|
|
347
|
-
|
|
348
|
-
## JWT Token Management
|
|
349
|
-
|
|
350
|
-
The SDK automatically manages JWT tokens:
|
|
351
|
-
|
|
352
|
-
- Stores JWT tokens in cookies (configurable name)
|
|
353
|
-
- Refreshes tokens before expiration (every 60 seconds)
|
|
354
|
-
- Validates tokens client-side and server-side
|
|
355
|
-
- Provides `getToken()` method for accessing valid tokens
|
|
356
|
-
|
|
357
|
-
The handshake flow ensures secure authentication:
|
|
358
|
-
|
|
359
|
-
1. On initial visit without a token, redirects to auth API for handshake
|
|
360
|
-
2. Auth API validates the session and redirects back with a nonce
|
|
361
|
-
3. SDK exchanges the nonce for a JWT token
|
|
362
|
-
4. Token is stored in a cookie and used for subsequent requests
|
|
363
|
-
|
|
364
|
-
## Utilities
|
|
365
|
-
|
|
366
|
-
### Token Validation
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
import { isTokenExpired, validateToken, extractTokenPayload } from '@meistrari/auth-nuxt/core'
|
|
370
|
-
|
|
371
|
-
// Check if token is expired
|
|
372
|
-
if (isTokenExpired(jwtToken)) {
|
|
373
|
-
// Token needs refresh
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Validate token against JWKS endpoint
|
|
377
|
-
const isValid = await validateToken(jwtToken, 'https://your-api.com')
|
|
378
|
-
|
|
379
|
-
// Extract token payload (without validation)
|
|
380
|
-
const payload = extractTokenPayload(jwtToken)
|
|
381
|
-
console.log(payload.user, payload.workspace)
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
## Security Features
|
|
385
|
-
|
|
386
|
-
- **JWT Validation**: Cryptographic validation using JWKS
|
|
387
|
-
- **Secure Cookies**: HTTP-only, secure cookies in production
|
|
388
|
-
- **Token Refresh**: Automatic token rotation every 60 seconds
|
|
389
|
-
- **Session Management**: Secure session handling with handshake flow
|
|
390
|
-
- **Server-Side Validation**: Middleware validates tokens on every API request
|
|
391
|
-
|
|
392
|
-
## Error Handling
|
|
393
|
-
|
|
394
|
-
The SDK handles common authentication errors automatically:
|
|
395
|
-
|
|
396
|
-
- Expired tokens trigger sign-out when refresh fails
|
|
397
|
-
- Invalid tokens result in `null` user/session values
|
|
398
|
-
- Network errors are handled gracefully
|
|
399
|
-
- API errors can be caught and handled in your code
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
import { APIError } from '@meistrari/auth-nuxt/core'
|
|
12
|
+
## Installation
|
|
403
13
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
email: 'user@example.com',
|
|
407
|
-
password: 'wrong-password',
|
|
408
|
-
})
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
if (error instanceof APIError) {
|
|
412
|
-
console.error('Authentication failed:', error.message, error.status)
|
|
413
|
-
}
|
|
414
|
-
}
|
|
14
|
+
```bash
|
|
15
|
+
npm install @meistrari/auth-nuxt
|
|
415
16
|
```
|
|
416
|
-
|
|
417
|
-
## Migration
|
|
418
|
-
|
|
419
|
-
If upgrading from a previous version:
|
|
420
|
-
|
|
421
|
-
1. Update package name to `@meistrari/auth-nuxt`
|
|
422
|
-
2. Change config key from `authSdk` to `telaAuth`
|
|
423
|
-
3. Replace `useMeistrariAuth()` with appropriate composable:
|
|
424
|
-
- `useTelaSession()` for authentication
|
|
425
|
-
- `useTelaOrganization()` for organization management
|
|
426
|
-
- `useTelaApiKey()` for API key management
|
|
427
|
-
4. Update cookie name if using custom value (default is now `tela-jwt`)
|
|
428
|
-
5. Remove `useJwt` and `isDevelopment` options (no longer needed)
|
package/dist/module.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { FullOrganization } from '@meistrari/auth-core';
|
|
2
1
|
/**
|
|
3
2
|
* Composable for managing Tela application authentication with OAuth 2.0 PKCE flow
|
|
4
3
|
*
|
|
@@ -41,34 +40,6 @@ export declare function useTelaApplicationAuth(): {
|
|
|
41
40
|
switchOrganization: (organizationId: string) => Promise<void>;
|
|
42
41
|
refreshToken: () => Promise<void>;
|
|
43
42
|
getToken: () => Promise<string | null | undefined>;
|
|
44
|
-
user: import("vue").Ref<
|
|
45
|
-
|
|
46
|
-
createdAt: Date;
|
|
47
|
-
updatedAt: Date;
|
|
48
|
-
email: string;
|
|
49
|
-
emailVerified: boolean;
|
|
50
|
-
name: string;
|
|
51
|
-
image?: string | null | undefined;
|
|
52
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
53
|
-
banned: boolean | null | undefined;
|
|
54
|
-
role?: string | null | undefined;
|
|
55
|
-
banReason?: string | null | undefined;
|
|
56
|
-
banExpires?: Date | null | undefined;
|
|
57
|
-
lastActiveAt?: Date | null | undefined;
|
|
58
|
-
} | null, {
|
|
59
|
-
id: string;
|
|
60
|
-
createdAt: Date;
|
|
61
|
-
updatedAt: Date;
|
|
62
|
-
email: string;
|
|
63
|
-
emailVerified: boolean;
|
|
64
|
-
name: string;
|
|
65
|
-
image?: string | null | undefined;
|
|
66
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
67
|
-
banned: boolean | null | undefined;
|
|
68
|
-
role?: string | null | undefined;
|
|
69
|
-
banReason?: string | null | undefined;
|
|
70
|
-
banExpires?: Date | null | undefined;
|
|
71
|
-
lastActiveAt?: Date | null | undefined;
|
|
72
|
-
} | null>;
|
|
73
|
-
activeOrganization: import("vue").Ref<FullOrganization | null, FullOrganization | null>;
|
|
43
|
+
user: import("vue").Ref<any, any>;
|
|
44
|
+
activeOrganization: import("vue").Ref<any, any>;
|
|
74
45
|
};
|
|
@@ -33,6 +33,11 @@ export function useTelaApplicationAuth() {
|
|
|
33
33
|
throw new AuthorizationFlowError("The login function can only be called on the client side.");
|
|
34
34
|
}
|
|
35
35
|
const { state: stateKey, challenge: codeChallenge } = await $fetch("/auth/login", { method: "POST" });
|
|
36
|
+
const returnTo = new URL(window.location.href).searchParams.get("returnTo");
|
|
37
|
+
if (returnTo && returnTo.startsWith("/") && !returnTo.startsWith("//")) {
|
|
38
|
+
const returnUrlCookie = useCookie("tela-return-url", { sameSite: "lax", path: "/" });
|
|
39
|
+
returnUrlCookie.value = returnTo;
|
|
40
|
+
}
|
|
36
41
|
const url = new URL("/applications/login", appConfig.application?.dashboardUrl);
|
|
37
42
|
url.searchParams.set("application_id", applicationId);
|
|
38
43
|
url.searchParams.set("code_challenge", codeChallenge);
|
|
@@ -1,161 +1,20 @@
|
|
|
1
|
-
import type { FullOrganization } from '@meistrari/auth-core';
|
|
2
1
|
/**
|
|
3
2
|
* Shared state for session management.
|
|
4
3
|
* This module provides access to session-related state without creating circular dependencies.
|
|
5
4
|
*/
|
|
6
5
|
export declare function useSessionState(): {
|
|
7
|
-
user: import("vue").Ref<
|
|
8
|
-
|
|
9
|
-
createdAt: Date;
|
|
10
|
-
updatedAt: Date;
|
|
11
|
-
email: string;
|
|
12
|
-
emailVerified: boolean;
|
|
13
|
-
name: string;
|
|
14
|
-
image?: string | null | undefined;
|
|
15
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
16
|
-
banned: boolean | null | undefined;
|
|
17
|
-
role?: string | null | undefined;
|
|
18
|
-
banReason?: string | null | undefined;
|
|
19
|
-
banExpires?: Date | null | undefined;
|
|
20
|
-
lastActiveAt?: Date | null | undefined;
|
|
21
|
-
} | null, {
|
|
22
|
-
id: string;
|
|
23
|
-
createdAt: Date;
|
|
24
|
-
updatedAt: Date;
|
|
25
|
-
email: string;
|
|
26
|
-
emailVerified: boolean;
|
|
27
|
-
name: string;
|
|
28
|
-
image?: string | null | undefined;
|
|
29
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
30
|
-
banned: boolean | null | undefined;
|
|
31
|
-
role?: string | null | undefined;
|
|
32
|
-
banReason?: string | null | undefined;
|
|
33
|
-
banExpires?: Date | null | undefined;
|
|
34
|
-
lastActiveAt?: Date | null | undefined;
|
|
35
|
-
} | null>;
|
|
36
|
-
session: import("vue").Ref<{
|
|
37
|
-
user: {
|
|
38
|
-
id: string;
|
|
39
|
-
createdAt: Date;
|
|
40
|
-
updatedAt: Date;
|
|
41
|
-
email: string;
|
|
42
|
-
emailVerified: boolean;
|
|
43
|
-
name: string;
|
|
44
|
-
image?: string | null | undefined;
|
|
45
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
46
|
-
banned: boolean | null | undefined;
|
|
47
|
-
role?: string | null | undefined;
|
|
48
|
-
banReason?: string | null | undefined;
|
|
49
|
-
banExpires?: Date | null | undefined;
|
|
50
|
-
lastActiveAt?: Date | null | undefined;
|
|
51
|
-
};
|
|
52
|
-
session: {
|
|
53
|
-
id: string;
|
|
54
|
-
createdAt: Date;
|
|
55
|
-
updatedAt: Date;
|
|
56
|
-
userId: string;
|
|
57
|
-
expiresAt: Date;
|
|
58
|
-
token: string;
|
|
59
|
-
ipAddress?: string | null | undefined;
|
|
60
|
-
userAgent?: string | null | undefined;
|
|
61
|
-
activeOrganizationId?: string | null | undefined;
|
|
62
|
-
activeTeamId?: string | null | undefined;
|
|
63
|
-
impersonatedBy?: string | null | undefined;
|
|
64
|
-
};
|
|
65
|
-
} | null, {
|
|
66
|
-
user: {
|
|
67
|
-
id: string;
|
|
68
|
-
createdAt: Date;
|
|
69
|
-
updatedAt: Date;
|
|
70
|
-
email: string;
|
|
71
|
-
emailVerified: boolean;
|
|
72
|
-
name: string;
|
|
73
|
-
image?: string | null | undefined;
|
|
74
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
75
|
-
banned: boolean | null | undefined;
|
|
76
|
-
role?: string | null | undefined;
|
|
77
|
-
banReason?: string | null | undefined;
|
|
78
|
-
banExpires?: Date | null | undefined;
|
|
79
|
-
lastActiveAt?: Date | null | undefined;
|
|
80
|
-
};
|
|
81
|
-
session: {
|
|
82
|
-
id: string;
|
|
83
|
-
createdAt: Date;
|
|
84
|
-
updatedAt: Date;
|
|
85
|
-
userId: string;
|
|
86
|
-
expiresAt: Date;
|
|
87
|
-
token: string;
|
|
88
|
-
ipAddress?: string | null | undefined;
|
|
89
|
-
userAgent?: string | null | undefined;
|
|
90
|
-
activeOrganizationId?: string | null | undefined;
|
|
91
|
-
activeTeamId?: string | null | undefined;
|
|
92
|
-
impersonatedBy?: string | null | undefined;
|
|
93
|
-
};
|
|
94
|
-
} | null>;
|
|
6
|
+
user: import("vue").Ref<any, any>;
|
|
7
|
+
session: import("vue").Ref<any, any>;
|
|
95
8
|
};
|
|
96
9
|
/**
|
|
97
10
|
* Shared state for organization management.
|
|
98
11
|
* This module provides access to organization-related state without creating circular dependencies.
|
|
99
12
|
*/
|
|
100
13
|
export declare function useOrganizationState(): {
|
|
101
|
-
activeOrganization: import("vue").Ref<
|
|
102
|
-
activeMember: import("vue").Ref<
|
|
103
|
-
id: string;
|
|
104
|
-
organizationId: string;
|
|
105
|
-
role: "org:admin" | "org:member" | "org:reviewer";
|
|
106
|
-
createdAt: Date;
|
|
107
|
-
userId: string;
|
|
108
|
-
teamId?: string | undefined | undefined | undefined;
|
|
109
|
-
user: {
|
|
110
|
-
id: string;
|
|
111
|
-
email: string;
|
|
112
|
-
name: string;
|
|
113
|
-
image?: string | undefined;
|
|
114
|
-
};
|
|
115
|
-
} | null, {
|
|
116
|
-
id: string;
|
|
117
|
-
organizationId: string;
|
|
118
|
-
role: "org:admin" | "org:member" | "org:reviewer";
|
|
119
|
-
createdAt: Date;
|
|
120
|
-
userId: string;
|
|
121
|
-
teamId?: string | undefined | undefined | undefined;
|
|
122
|
-
user: {
|
|
123
|
-
id: string;
|
|
124
|
-
email: string;
|
|
125
|
-
name: string;
|
|
126
|
-
image?: string | undefined;
|
|
127
|
-
};
|
|
128
|
-
} | null>;
|
|
14
|
+
activeOrganization: import("vue").Ref<any, any>;
|
|
15
|
+
activeMember: import("vue").Ref<any, any>;
|
|
129
16
|
};
|
|
130
17
|
export declare function useApplicationSessionState(): {
|
|
131
|
-
user: import("vue").Ref<
|
|
132
|
-
|
|
133
|
-
createdAt: Date;
|
|
134
|
-
updatedAt: Date;
|
|
135
|
-
email: string;
|
|
136
|
-
emailVerified: boolean;
|
|
137
|
-
name: string;
|
|
138
|
-
image?: string | null | undefined;
|
|
139
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
140
|
-
banned: boolean | null | undefined;
|
|
141
|
-
role?: string | null | undefined;
|
|
142
|
-
banReason?: string | null | undefined;
|
|
143
|
-
banExpires?: Date | null | undefined;
|
|
144
|
-
lastActiveAt?: Date | null | undefined;
|
|
145
|
-
} | null, {
|
|
146
|
-
id: string;
|
|
147
|
-
createdAt: Date;
|
|
148
|
-
updatedAt: Date;
|
|
149
|
-
email: string;
|
|
150
|
-
emailVerified: boolean;
|
|
151
|
-
name: string;
|
|
152
|
-
image?: string | null | undefined;
|
|
153
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
154
|
-
banned: boolean | null | undefined;
|
|
155
|
-
role?: string | null | undefined;
|
|
156
|
-
banReason?: string | null | undefined;
|
|
157
|
-
banExpires?: Date | null | undefined;
|
|
158
|
-
lastActiveAt?: Date | null | undefined;
|
|
159
|
-
} | null>;
|
|
160
|
-
activeOrganization: import("vue").Ref<FullOrganization | null, FullOrganization | null>;
|
|
18
|
+
user: import("vue").Ref<any, any>;
|
|
19
|
+
activeOrganization: import("vue").Ref<any, any>;
|
|
161
20
|
};
|
|
@@ -43,13 +43,14 @@ export default defineNuxtPlugin({
|
|
|
43
43
|
refreshTokenCookie.value = refreshToken2;
|
|
44
44
|
state.user.value = user2;
|
|
45
45
|
state.activeOrganization.value = organization2;
|
|
46
|
-
return;
|
|
46
|
+
return true;
|
|
47
47
|
}
|
|
48
48
|
const { user, organization } = await $fetch("/auth/refresh", {
|
|
49
49
|
method: "POST"
|
|
50
50
|
});
|
|
51
51
|
state.user.value = user;
|
|
52
52
|
state.activeOrganization.value = organization;
|
|
53
|
+
return true;
|
|
53
54
|
} catch {
|
|
54
55
|
await sdkLogout();
|
|
55
56
|
if (import.meta.client) {
|
|
@@ -69,11 +70,11 @@ export default defineNuxtPlugin({
|
|
|
69
70
|
clearTimeout(tokenRefreshInterval);
|
|
70
71
|
tokenRefreshInterval = null;
|
|
71
72
|
}
|
|
72
|
-
if (!accessTokenCookie.value) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
if (!accessTokenCookie.value || isTokenExpired(accessTokenCookie.value, TWO_MINUTES)) {
|
|
74
|
+
const result = await refreshToken();
|
|
75
|
+
if (!result) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
77
78
|
}
|
|
78
79
|
const expiry = parseTokenExpiry(accessTokenCookie.value);
|
|
79
80
|
if (!expiry) {
|
|
@@ -5,35 +5,38 @@ export default defineNuxtPlugin(() => {
|
|
|
5
5
|
const authConfig = config.public.telaAuth;
|
|
6
6
|
const loginPath = authConfig.application?.loginPath ?? "/login";
|
|
7
7
|
const unauthorizedPath = authConfig.application?.unauthorizedPath ?? "/unauthorized";
|
|
8
|
+
const exemptPaths = [loginPath, unauthorizedPath];
|
|
8
9
|
addRouteMiddleware("auth-guard", async (to) => {
|
|
9
10
|
const authMeta = to.meta?.auth;
|
|
10
|
-
|
|
11
|
+
const path = decodeURI(to.path);
|
|
12
|
+
if (authMeta === false || exemptPaths.includes(path)) {
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
const token = useCookie("tela-access-token");
|
|
14
16
|
if (!token.value) {
|
|
15
17
|
return await navigateTo({
|
|
16
18
|
path: loginPath,
|
|
17
|
-
query: {
|
|
19
|
+
query: { returnTo: to.fullPath }
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
|
-
if (authMeta
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
query: { redirect: to.fullPath }
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error("Failed to decode token:", error);
|
|
22
|
+
if (!authMeta?.roles?.length) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const payload = decodeJwt(token.value);
|
|
27
|
+
const userRole = payload.user?.role;
|
|
28
|
+
if (!userRole || !authMeta.roles.includes(userRole)) {
|
|
32
29
|
return await navigateTo({
|
|
33
|
-
path:
|
|
34
|
-
query: {
|
|
30
|
+
path: unauthorizedPath,
|
|
31
|
+
query: { returnTo: to.fullPath }
|
|
35
32
|
});
|
|
36
33
|
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Failed to decode token:", error);
|
|
36
|
+
return await navigateTo({
|
|
37
|
+
path: loginPath,
|
|
38
|
+
query: { returnTo: to.fullPath }
|
|
39
|
+
});
|
|
37
40
|
}
|
|
38
41
|
}, { global: true });
|
|
39
42
|
});
|
|
@@ -40,6 +40,13 @@ export default defineEventHandler(async (event) => {
|
|
|
40
40
|
path: "/"
|
|
41
41
|
});
|
|
42
42
|
deleteCookie(event, `tela-verifier-${state}`, { path: "/" });
|
|
43
|
+
const returnUrlCookie = getCookie(event, "tela-return-url");
|
|
44
|
+
if (returnUrlCookie) {
|
|
45
|
+
deleteCookie(event, "tela-return-url", { path: "/" });
|
|
46
|
+
if (returnUrlCookie.startsWith("/") && !returnUrlCookie.startsWith("//")) {
|
|
47
|
+
return await sendRedirect(event, returnUrlCookie);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
43
50
|
return await sendRedirect(event, "/");
|
|
44
51
|
} catch (error) {
|
|
45
52
|
console.error("[Auth Callback] OAuth flow error:", error);
|
|
@@ -11,21 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
13
13
|
success: boolean;
|
|
14
|
-
user:
|
|
15
|
-
|
|
16
|
-
createdAt: Date;
|
|
17
|
-
updatedAt: Date;
|
|
18
|
-
email: string;
|
|
19
|
-
emailVerified: boolean;
|
|
20
|
-
name: string;
|
|
21
|
-
image?: string | null | undefined;
|
|
22
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
23
|
-
banned: boolean | null | undefined;
|
|
24
|
-
role?: string | null | undefined;
|
|
25
|
-
banReason?: string | null | undefined;
|
|
26
|
-
banExpires?: Date | null | undefined;
|
|
27
|
-
lastActiveAt?: Date | null | undefined;
|
|
28
|
-
};
|
|
29
|
-
organization: import("@meistrari/auth-core").FullOrganization;
|
|
14
|
+
user: any;
|
|
15
|
+
organization: any;
|
|
30
16
|
}>>;
|
|
31
17
|
export default _default;
|
|
@@ -12,21 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
14
14
|
success: boolean;
|
|
15
|
-
user:
|
|
16
|
-
|
|
17
|
-
createdAt: Date;
|
|
18
|
-
updatedAt: Date;
|
|
19
|
-
email: string;
|
|
20
|
-
emailVerified: boolean;
|
|
21
|
-
name: string;
|
|
22
|
-
image?: string | null | undefined;
|
|
23
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
24
|
-
banned: boolean | null | undefined;
|
|
25
|
-
role?: string | null | undefined;
|
|
26
|
-
banReason?: string | null | undefined;
|
|
27
|
-
banExpires?: Date | null | undefined;
|
|
28
|
-
lastActiveAt?: Date | null | undefined;
|
|
29
|
-
};
|
|
30
|
-
organization: import("@meistrari/auth-core").FullOrganization;
|
|
15
|
+
user: any;
|
|
16
|
+
organization: any;
|
|
31
17
|
}>>;
|
|
32
18
|
export default _default;
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
2
2
|
success: boolean;
|
|
3
|
-
user:
|
|
4
|
-
|
|
5
|
-
createdAt: Date;
|
|
6
|
-
updatedAt: Date;
|
|
7
|
-
email: string;
|
|
8
|
-
emailVerified: boolean;
|
|
9
|
-
name: string;
|
|
10
|
-
image?: string | null | undefined;
|
|
11
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
12
|
-
banned: boolean | null | undefined;
|
|
13
|
-
role?: string | null | undefined;
|
|
14
|
-
banReason?: string | null | undefined;
|
|
15
|
-
banExpires?: Date | null | undefined;
|
|
16
|
-
lastActiveAt?: Date | null | undefined;
|
|
17
|
-
};
|
|
18
|
-
organization: import("@meistrari/auth-core").FullOrganization;
|
|
3
|
+
user: any;
|
|
4
|
+
organization: any;
|
|
19
5
|
}>>;
|
|
20
6
|
export default _default;
|
|
@@ -3,10 +3,6 @@ import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
|
3
3
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
4
4
|
export function requireAuth(handler, options) {
|
|
5
5
|
return defineEventHandler(async (event) => {
|
|
6
|
-
const moduleOptions = useRuntimeConfig(event).public.telaAuth;
|
|
7
|
-
if (!moduleOptions.skipServerMiddleware && !options?.roles && import.meta.dev) {
|
|
8
|
-
console.warn("You have enabled the global server middleware, meaning you only need to use requireAuth() on routes that require specific roles.", `Triggered at ${event.path}`);
|
|
9
|
-
}
|
|
10
6
|
if (event.context.auth?.user && event.context.auth?.token) {
|
|
11
7
|
if (import.meta.dev) {
|
|
12
8
|
console.debug("Using existing auth context from global server middleware");
|
package/dist/runtime/shared.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null, getRefreshToken?: () => string | null): AuthClient;
|
|
1
|
+
export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null, getRefreshToken?: () => string | null): any;
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"name": "@meistrari/auth-nuxt",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/types.d.mts",
|
|
8
|
+
"import": "./dist/module.mjs"
|
|
9
|
+
},
|
|
10
|
+
"./server/middleware/auth": {
|
|
11
|
+
"types": "./dist/runtime/server/middleware/auth.d.ts",
|
|
12
|
+
"import": "./dist/runtime/server/middleware/auth.js"
|
|
13
|
+
},
|
|
14
|
+
"./core": {
|
|
15
|
+
"types": "./dist/core.d.mts",
|
|
16
|
+
"import": "./dist/core.mjs"
|
|
17
|
+
}
|
|
9
18
|
},
|
|
10
|
-
"./
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
"main": "./dist/module.mjs",
|
|
20
|
+
"typesVersions": {
|
|
21
|
+
"*": {
|
|
22
|
+
".": [
|
|
23
|
+
"./dist/types.d.mts"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
13
26
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@meistrari/auth-core": "1.11.3",
|
|
35
|
+
"jose": "6.1.3"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"nuxt": "^3.0.0 || ^4.0.0",
|
|
39
|
+
"vue": "^3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@nuxt/devtools": "2.6.3",
|
|
43
|
+
"@nuxt/eslint-config": "1.9.0",
|
|
44
|
+
"@nuxt/kit": "4.0.3",
|
|
45
|
+
"@nuxt/module-builder": "1.0.2",
|
|
46
|
+
"@nuxt/schema": "4.0.3",
|
|
47
|
+
"@nuxt/test-utils": "3.19.2",
|
|
48
|
+
"@types/node": "latest",
|
|
49
|
+
"changelogen": "0.6.2",
|
|
50
|
+
"nuxt": "4.0.3",
|
|
51
|
+
"typescript": "5.9.2",
|
|
52
|
+
"unbuild": "3.6.1",
|
|
53
|
+
"vitest": "3.2.4",
|
|
54
|
+
"vue-tsc": "3.0.6"
|
|
25
55
|
}
|
|
26
|
-
},
|
|
27
|
-
"files": [
|
|
28
|
-
"dist"
|
|
29
|
-
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@meistrari/auth-core": "1.11.2",
|
|
35
|
-
"jose": "6.1.3"
|
|
36
|
-
},
|
|
37
|
-
"peerDependencies": {
|
|
38
|
-
"nuxt": "^3.0.0 || ^4.0.0",
|
|
39
|
-
"vue": "^3.0.0"
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"@nuxt/devtools": "2.6.3",
|
|
43
|
-
"@nuxt/eslint-config": "1.9.0",
|
|
44
|
-
"@nuxt/kit": "4.0.3",
|
|
45
|
-
"@nuxt/module-builder": "1.0.2",
|
|
46
|
-
"@nuxt/schema": "4.0.3",
|
|
47
|
-
"@nuxt/test-utils": "3.19.2",
|
|
48
|
-
"@types/node": "latest",
|
|
49
|
-
"changelogen": "0.6.2",
|
|
50
|
-
"nuxt": "4.0.3",
|
|
51
|
-
"typescript": "5.9.2",
|
|
52
|
-
"unbuild": "3.6.1",
|
|
53
|
-
"vitest": "3.2.4",
|
|
54
|
-
"vue-tsc": "3.0.6"
|
|
55
|
-
}
|
|
56
56
|
}
|