authvital-sdk 0.1.1-dev.3.cefb119.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 +657 -0
- package/dist/chunk-ETJ5ICJ7.mjs +412 -0
- package/dist/chunk-FXVD4Y5G.js +412 -0
- package/dist/chunk-JNEJMHGA.mjs +235 -0
- package/dist/chunk-JPODZIZT.mjs +95 -0
- package/dist/chunk-QPYBK2J4.js +235 -0
- package/dist/chunk-R4OHZZQP.js +95 -0
- package/dist/index.d.mts +615 -0
- package/dist/index.d.ts +615 -0
- package/dist/index.js +1299 -0
- package/dist/index.mjs +1299 -0
- package/dist/invitations-EFJA5C6L.mjs +22 -0
- package/dist/invitations-LZHJ3AZY.js +22 -0
- package/dist/oauth-K7E7OCWI.js +34 -0
- package/dist/oauth-UAFXEKZ7.mjs +34 -0
- package/dist/server.d.mts +2656 -0
- package/dist/server.d.ts +2656 -0
- package/dist/server.js +2915 -0
- package/dist/server.mjs +2915 -0
- package/dist/webhook-router-DdfXLtHa.d.mts +461 -0
- package/dist/webhook-router-DdfXLtHa.d.ts +461 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# @authvital/sdk
|
|
2
|
+
|
|
3
|
+
Official SDK for integrating with AuthVital Identity Provider.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @authvital/sdk
|
|
9
|
+
# or
|
|
10
|
+
yarn add @authvital/sdk
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @authvital/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Server-Side (Node.js/Backend)
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createAuthVital } from '@authvital/sdk/server';
|
|
21
|
+
|
|
22
|
+
const authvital = createAuthVital({
|
|
23
|
+
authVitalHost: process.env.AUTHVITAL_HOST!,
|
|
24
|
+
clientId: process.env.AUTHVITAL_CLIENT_ID!,
|
|
25
|
+
clientSecret: process.env.AUTHVITAL_CLIENT_SECRET!,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Validate JWT from incoming request
|
|
29
|
+
app.get('/api/protected', async (req, res) => {
|
|
30
|
+
const { authenticated, user } = await authvital.getCurrentUser(req);
|
|
31
|
+
|
|
32
|
+
if (!authenticated) {
|
|
33
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
res.json({ message: `Hello ${user.email}!` });
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Client-Side (React)
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { AuthVitalProvider, useAuth } from '@authvital/sdk/client';
|
|
44
|
+
|
|
45
|
+
function App() {
|
|
46
|
+
return (
|
|
47
|
+
<AuthVitalProvider
|
|
48
|
+
authVitalHost={process.env.REACT_APP_AUTHVITAL_HOST!}
|
|
49
|
+
clientId={process.env.REACT_APP_CLIENT_ID!}
|
|
50
|
+
>
|
|
51
|
+
<MyApp />
|
|
52
|
+
</AuthVitalProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function MyApp() {
|
|
57
|
+
const { user, isAuthenticated, login, logout } = useAuth();
|
|
58
|
+
|
|
59
|
+
if (!isAuthenticated) {
|
|
60
|
+
return <button onClick={login}>Login</button>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
<p>Welcome, {user.email}!</p>
|
|
66
|
+
<button onClick={logout}>Logout</button>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Server SDK Features
|
|
75
|
+
|
|
76
|
+
### JWT Validation
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { createAuthVital } from '@authvital/sdk/server';
|
|
80
|
+
|
|
81
|
+
const authvital = createAuthVital({
|
|
82
|
+
authVitalHost: 'https://auth.yourapp.com',
|
|
83
|
+
clientId: 'your-client-id',
|
|
84
|
+
clientSecret: 'your-client-secret',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Get current user from request
|
|
88
|
+
const { authenticated, user } = await authvital.getCurrentUser(req);
|
|
89
|
+
|
|
90
|
+
// User object includes:
|
|
91
|
+
// - sub: string (user ID)
|
|
92
|
+
// - email: string
|
|
93
|
+
// - given_name: string
|
|
94
|
+
// - family_name: string
|
|
95
|
+
// - picture: string (profile pic URL)
|
|
96
|
+
// - tenant_id: string (if scoped to tenant)
|
|
97
|
+
// - app_roles: string[] (role slugs)
|
|
98
|
+
// - app_permissions: string[] (permission keys)
|
|
99
|
+
// - license: { type, name, features }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### JWT Permission Helpers
|
|
103
|
+
|
|
104
|
+
Extract and check permissions directly from validated JWT claims:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import {
|
|
108
|
+
getPermissionsFromClaims,
|
|
109
|
+
hasPermission,
|
|
110
|
+
hasAnyPermission,
|
|
111
|
+
hasAllPermissions,
|
|
112
|
+
getRolesFromClaims,
|
|
113
|
+
hasRole,
|
|
114
|
+
getTenantIdFromClaims,
|
|
115
|
+
} from '@authvital/sdk/server';
|
|
116
|
+
|
|
117
|
+
// After validating JWT, you have claims:
|
|
118
|
+
const { authenticated, user: claims } = await authvital.getCurrentUser(req);
|
|
119
|
+
|
|
120
|
+
if (authenticated) {
|
|
121
|
+
// Get all permissions from the token
|
|
122
|
+
const permissions = getPermissionsFromClaims(claims);
|
|
123
|
+
// ['documents:read', 'documents:write', 'settings:view']
|
|
124
|
+
|
|
125
|
+
// Check single permission
|
|
126
|
+
if (hasPermission(claims, 'documents:write')) {
|
|
127
|
+
// User can write documents
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if user has ANY of these permissions
|
|
131
|
+
if (hasAnyPermission(claims, ['admin:access', 'documents:delete'])) {
|
|
132
|
+
// Show delete button
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if user has ALL permissions
|
|
136
|
+
if (hasAllPermissions(claims, ['billing:read', 'billing:write'])) {
|
|
137
|
+
// Allow billing management
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Get roles
|
|
141
|
+
const roles = getRolesFromClaims(claims); // ['admin', 'editor']
|
|
142
|
+
if (hasRole(claims, 'admin')) {
|
|
143
|
+
// Admin-only logic
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get tenant context
|
|
147
|
+
const tenantId = getTenantIdFromClaims(claims);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Namespaces
|
|
152
|
+
|
|
153
|
+
The SDK provides namespaced methods for different operations:
|
|
154
|
+
|
|
155
|
+
#### Invitations
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Send an invitation
|
|
159
|
+
await authvital.invitations.send({
|
|
160
|
+
email: 'newuser@example.com',
|
|
161
|
+
tenantId: 'tenant-123',
|
|
162
|
+
roleId: 'role-member',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// List pending invitations
|
|
166
|
+
const pending = await authvital.invitations.listPending('tenant-123');
|
|
167
|
+
|
|
168
|
+
// Revoke an invitation
|
|
169
|
+
await authvital.invitations.revoke('invitation-id');
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Memberships
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// List tenant members
|
|
176
|
+
const members = await authvital.memberships.listForTenant('tenant-123');
|
|
177
|
+
|
|
178
|
+
// Get user's tenants
|
|
179
|
+
const tenants = await authvital.memberships.listUserTenants(req);
|
|
180
|
+
|
|
181
|
+
// Set member role
|
|
182
|
+
await authvital.memberships.setTenantRole({
|
|
183
|
+
membershipId: 'membership-123',
|
|
184
|
+
roleSlug: 'admin',
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### Permissions
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Check a single permission
|
|
192
|
+
const { allowed } = await authvital.permissions.check(req, {
|
|
193
|
+
permission: 'documents:write',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Check multiple permissions
|
|
197
|
+
const results = await authvital.permissions.checkMany(req, {
|
|
198
|
+
permissions: ['documents:read', 'documents:write', 'admin:access'],
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Licenses
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Check if user has a license
|
|
206
|
+
const { hasLicense, licenseType } = await authvital.licenses.check(req, {
|
|
207
|
+
applicationId: 'app-123',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Check specific feature
|
|
211
|
+
const { hasFeature } = await authvital.licenses.hasFeature(req, {
|
|
212
|
+
applicationId: 'app-123',
|
|
213
|
+
feature: 'advanced-analytics',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Grant a license (admin)
|
|
217
|
+
await authvital.licenses.grant(req, {
|
|
218
|
+
userId: 'user-123',
|
|
219
|
+
applicationId: 'app-123',
|
|
220
|
+
licenseTypeId: 'license-pro',
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Sessions
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// List user's active sessions
|
|
228
|
+
const { sessions } = await authvital.sessions.list(req);
|
|
229
|
+
|
|
230
|
+
// Revoke a specific session
|
|
231
|
+
await authvital.sessions.revoke(req, 'session-id');
|
|
232
|
+
|
|
233
|
+
// Logout from all devices
|
|
234
|
+
await authvital.sessions.revokeAll(req);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Entitlements
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// Check user's entitlements for a resource
|
|
241
|
+
const entitlements = await authvital.entitlements.check(req, {
|
|
242
|
+
resourceType: 'api-calls',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Returns: { limit, used, remaining, unlimited }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## OAuth Flow Utilities
|
|
251
|
+
|
|
252
|
+
### OAuthFlow Class (Recommended)
|
|
253
|
+
|
|
254
|
+
For server-side OAuth with PKCE:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { OAuthFlow } from '@authvital/sdk/server';
|
|
258
|
+
|
|
259
|
+
const oauth = new OAuthFlow({
|
|
260
|
+
authVitalHost: process.env.AV_HOST!,
|
|
261
|
+
clientId: process.env.AV_CLIENT_ID!,
|
|
262
|
+
clientSecret: process.env.AV_CLIENT_SECRET!,
|
|
263
|
+
redirectUri: 'https://myapp.com/api/auth/callback',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// GET /api/auth/login
|
|
267
|
+
app.get('/api/auth/login', (req, res) => {
|
|
268
|
+
const { authorizeUrl, state, codeVerifier } = oauth.startFlow({
|
|
269
|
+
appState: req.query.returnTo, // Optional - gets passed through OAuth
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Store for callback verification
|
|
273
|
+
req.session.oauthState = state;
|
|
274
|
+
req.session.codeVerifier = codeVerifier;
|
|
275
|
+
|
|
276
|
+
res.redirect(authorizeUrl);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// GET /api/auth/callback
|
|
280
|
+
app.get('/api/auth/callback', async (req, res) => {
|
|
281
|
+
const tokens = await oauth.handleCallback(
|
|
282
|
+
req.query.code,
|
|
283
|
+
req.query.state,
|
|
284
|
+
req.session.oauthState,
|
|
285
|
+
req.session.codeVerifier
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// tokens includes: access_token, refresh_token, id_token, appState
|
|
289
|
+
// Set cookies, redirect to appState or dashboard
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### State Encoding
|
|
294
|
+
|
|
295
|
+
For custom flows that need CSRF + app state:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { encodeState, decodeState, type StatePayload } from '@authvital/sdk/server';
|
|
299
|
+
|
|
300
|
+
// Encode CSRF + app state into OAuth state param
|
|
301
|
+
const state = encodeState(csrfNonce, '/dashboard?tab=settings');
|
|
302
|
+
|
|
303
|
+
// Later, decode it
|
|
304
|
+
const payload: StatePayload = decodeState(state);
|
|
305
|
+
// { csrf: 'abc123', appState: '/dashboard?tab=settings' }
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### URL Builders
|
|
309
|
+
|
|
310
|
+
For landing pages, emails, or simple redirects (no PKCE ceremony):
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import {
|
|
314
|
+
getLoginUrl,
|
|
315
|
+
getSignupUrl,
|
|
316
|
+
getLogoutUrl,
|
|
317
|
+
getInviteAcceptUrl,
|
|
318
|
+
} from '@authvital/sdk/server';
|
|
319
|
+
|
|
320
|
+
// Simple login link
|
|
321
|
+
const loginUrl = getLoginUrl({
|
|
322
|
+
authVitalHost: 'https://auth.myapp.com',
|
|
323
|
+
clientId: 'my-app',
|
|
324
|
+
redirectUri: 'https://app.myapp.com/dashboard',
|
|
325
|
+
tenantHint: 'acme-corp', // Optional
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Signup with pre-filled email
|
|
329
|
+
const signupUrl = getSignupUrl({
|
|
330
|
+
authVitalHost: 'https://auth.myapp.com',
|
|
331
|
+
clientId: 'my-app',
|
|
332
|
+
redirectUri: 'https://app.myapp.com/onboarding',
|
|
333
|
+
email: 'user@example.com', // Optional
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Logout URL
|
|
337
|
+
const logoutUrl = getLogoutUrl({
|
|
338
|
+
authVitalHost: 'https://auth.myapp.com',
|
|
339
|
+
postLogoutRedirectUri: 'https://myapp.com',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Invitation acceptance link (for emails)
|
|
343
|
+
const inviteUrl = getInviteAcceptUrl({
|
|
344
|
+
authVitalHost: 'https://auth.myapp.com',
|
|
345
|
+
clientId: 'my-app',
|
|
346
|
+
inviteToken: 'abc123xyz',
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Low-Level PKCE Utilities
|
|
351
|
+
|
|
352
|
+
For custom OAuth implementations:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import {
|
|
356
|
+
generatePKCE,
|
|
357
|
+
buildAuthorizeUrl,
|
|
358
|
+
exchangeCodeForTokens,
|
|
359
|
+
} from '@authvital/sdk/server';
|
|
360
|
+
|
|
361
|
+
// Generate PKCE challenge
|
|
362
|
+
const { codeVerifier, codeChallenge } = await generatePKCE();
|
|
363
|
+
|
|
364
|
+
// Build authorization URL
|
|
365
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
366
|
+
authVitalHost: 'https://auth.yourapp.com',
|
|
367
|
+
clientId: 'your-client-id',
|
|
368
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
369
|
+
codeChallenge,
|
|
370
|
+
state: 'random-state',
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Exchange code for tokens
|
|
374
|
+
const tokens = await exchangeCodeForTokens({
|
|
375
|
+
authVitalHost: 'https://auth.yourapp.com',
|
|
376
|
+
clientId: 'your-client-id',
|
|
377
|
+
clientSecret: 'your-client-secret',
|
|
378
|
+
code: 'authorization-code',
|
|
379
|
+
codeVerifier,
|
|
380
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Identity Sync (Local Database Mirroring)
|
|
387
|
+
|
|
388
|
+
The SDK provides tools for syncing AuthVital identities to your local database, reducing API calls and enabling offline queries.
|
|
389
|
+
|
|
390
|
+
### Step 1: Add Prisma Schema
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { printSchema } from '@authvital/sdk/server';
|
|
394
|
+
|
|
395
|
+
// Print schema to console, then copy to your schema.prisma
|
|
396
|
+
printSchema();
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
This outputs:
|
|
400
|
+
|
|
401
|
+
```prisma
|
|
402
|
+
model Identity {
|
|
403
|
+
id String @id // AuthVital subject ID
|
|
404
|
+
email String? @unique
|
|
405
|
+
givenName String? @map("given_name")
|
|
406
|
+
familyName String? @map("family_name")
|
|
407
|
+
pictureUrl String? @map("picture_url") // Profile picture
|
|
408
|
+
thumbnailUrl String? @map("thumbnail_url") // Small avatar
|
|
409
|
+
tenantId String? @map("tenant_id")
|
|
410
|
+
appRole String? @map("app_role")
|
|
411
|
+
isActive Boolean @default(true) @map("is_active")
|
|
412
|
+
syncedAt DateTime @default(now()) @map("synced_at")
|
|
413
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
414
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
415
|
+
|
|
416
|
+
sessions IdentitySession[]
|
|
417
|
+
|
|
418
|
+
// Add your app-specific relations below
|
|
419
|
+
// posts Post[]
|
|
420
|
+
// preferences Json @default("{}")
|
|
421
|
+
|
|
422
|
+
@@map("identities")
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
model IdentitySession {
|
|
426
|
+
id String @id @default(cuid())
|
|
427
|
+
identityId String @map("identity_id")
|
|
428
|
+
identity Identity @relation(fields: [identityId], references: [id], onDelete: Cascade)
|
|
429
|
+
authSessionId String? @unique @map("auth_session_id") // AuthVital session ID
|
|
430
|
+
deviceInfo String? @map("device_info")
|
|
431
|
+
ipAddress String? @map("ip_address")
|
|
432
|
+
userAgent String? @map("user_agent")
|
|
433
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
434
|
+
lastActiveAt DateTime @default(now()) @map("last_active_at")
|
|
435
|
+
expiresAt DateTime @map("expires_at")
|
|
436
|
+
revokedAt DateTime? @map("revoked_at")
|
|
437
|
+
|
|
438
|
+
@@index([identityId])
|
|
439
|
+
@@map("identity_sessions")
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Step 2: Set Up Webhook Handler
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { IdentitySyncHandler, WebhookRouter } from '@authvital/sdk/server';
|
|
447
|
+
import { prisma } from './prisma';
|
|
448
|
+
|
|
449
|
+
// Create the sync handler with your Prisma client
|
|
450
|
+
const syncHandler = new IdentitySyncHandler(prisma);
|
|
451
|
+
|
|
452
|
+
// Create the webhook router (uses JWKS for verification)
|
|
453
|
+
const router = new WebhookRouter({
|
|
454
|
+
authVitalHost: process.env.AUTHVITAL_HOST!,
|
|
455
|
+
handler: syncHandler,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Mount in your Express app
|
|
459
|
+
app.post('/webhooks/authvital', router.expressHandler());
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Step 3: Configure Webhook in AuthVital
|
|
463
|
+
|
|
464
|
+
In your AuthVital dashboard, add a webhook endpoint pointing to your `/webhooks/authvital` URL. Enable these events:
|
|
465
|
+
|
|
466
|
+
- `subject.created` - New user registered
|
|
467
|
+
- `subject.updated` - User profile changed
|
|
468
|
+
- `subject.deleted` - User deleted
|
|
469
|
+
- `subject.deactivated` - User deactivated
|
|
470
|
+
- `member.joined` - User joined tenant
|
|
471
|
+
- `member.left` - User left tenant
|
|
472
|
+
- `member.role_changed` - User's role changed
|
|
473
|
+
- `app_access.granted` - User granted app access
|
|
474
|
+
- `app_access.revoked` - User's app access revoked
|
|
475
|
+
- `app_access.role_changed` - User's app role changed
|
|
476
|
+
|
|
477
|
+
### Step 4: Optional Session Cleanup
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { cleanupSessions } from '@authvital/sdk/server';
|
|
481
|
+
|
|
482
|
+
// Run daily via cron
|
|
483
|
+
cron.schedule('0 3 * * *', async () => {
|
|
484
|
+
const result = await cleanupSessions(prisma, {
|
|
485
|
+
expiredOlderThanDays: 30, // Delete sessions expired 30+ days ago
|
|
486
|
+
deleteRevoked: false, // Keep revoked for audit trail
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
console.log(`Cleaned up ${result.deletedCount} sessions`);
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Or use raw SQL with pg_cron:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import { getCleanupSQL } from '@authvital/sdk/server';
|
|
497
|
+
|
|
498
|
+
console.log(getCleanupSQL({ expiredOlderThanDays: 30 }));
|
|
499
|
+
// DELETE FROM identity_sessions WHERE expires_at < NOW() - INTERVAL '30 days';
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Webhooks
|
|
505
|
+
|
|
506
|
+
Webhooks are verified using JWKS (JSON Web Key Set) from your AuthVital instance - no shared secrets needed!
|
|
507
|
+
|
|
508
|
+
### Using WebhookRouter
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import { WebhookRouter, IdentitySyncHandler } from '@authvital/sdk/server';
|
|
512
|
+
|
|
513
|
+
// Option 1: Use built-in IdentitySyncHandler for database mirroring
|
|
514
|
+
const router = new WebhookRouter({
|
|
515
|
+
authVitalHost: process.env.AUTHVITAL_HOST!,
|
|
516
|
+
handler: new IdentitySyncHandler(prisma),
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
app.post('/webhooks/authvital', router.expressHandler());
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Custom Event Handler
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
import { AuthVitalEventHandler, WebhookRouter } from '@authvital/sdk/server';
|
|
526
|
+
|
|
527
|
+
class MyEventHandler extends AuthVitalEventHandler {
|
|
528
|
+
async onSubjectCreated(event) {
|
|
529
|
+
// User registered
|
|
530
|
+
await sendWelcomeEmail(event.data.email);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async onMemberJoined(event) {
|
|
534
|
+
// User joined a tenant
|
|
535
|
+
await notifyTeam(event.data.tenant_id, `${event.data.email} joined!`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async onLicenseAssigned(event) {
|
|
539
|
+
// User got a license
|
|
540
|
+
await provisionResources(event.data.sub, event.data.license_type_name);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async onInviteAccepted(event) {
|
|
544
|
+
// Invitation was accepted
|
|
545
|
+
await notifyInviter(event.data.invited_by, event.data.email);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const router = new WebhookRouter({
|
|
550
|
+
authVitalHost: process.env.AUTHVITAL_HOST!,
|
|
551
|
+
handler: new MyEventHandler(),
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
app.post('/webhooks', router.expressHandler());
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Available Events
|
|
558
|
+
|
|
559
|
+
| Event | Method | Description |
|
|
560
|
+
|-------|--------|-------------|
|
|
561
|
+
| `invite.created` | `onInviteCreated` | Invitation sent |
|
|
562
|
+
| `invite.accepted` | `onInviteAccepted` | Invitation accepted |
|
|
563
|
+
| `invite.deleted` | `onInviteDeleted` | Invitation revoked |
|
|
564
|
+
| `invite.expired` | `onInviteExpired` | Invitation expired |
|
|
565
|
+
| `subject.created` | `onSubjectCreated` | User/service account created |
|
|
566
|
+
| `subject.updated` | `onSubjectUpdated` | User profile updated |
|
|
567
|
+
| `subject.deleted` | `onSubjectDeleted` | User deleted |
|
|
568
|
+
| `subject.deactivated` | `onSubjectDeactivated` | User deactivated |
|
|
569
|
+
| `member.joined` | `onMemberJoined` | User joined tenant |
|
|
570
|
+
| `member.left` | `onMemberLeft` | User left tenant |
|
|
571
|
+
| `member.role_changed` | `onMemberRoleChanged` | Member role changed |
|
|
572
|
+
| `member.suspended` | `onMemberSuspended` | Member suspended |
|
|
573
|
+
| `member.activated` | `onMemberActivated` | Member reactivated |
|
|
574
|
+
| `app_access.granted` | `onAppAccessGranted` | App access granted |
|
|
575
|
+
| `app_access.revoked` | `onAppAccessRevoked` | App access revoked |
|
|
576
|
+
| `app_access.role_changed` | `onAppAccessRoleChanged` | App role changed |
|
|
577
|
+
| `license.assigned` | `onLicenseAssigned` | License assigned |
|
|
578
|
+
| `license.revoked` | `onLicenseRevoked` | License revoked |
|
|
579
|
+
| `license.changed` | `onLicenseChanged` | License type changed |
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## TypeScript Types
|
|
584
|
+
|
|
585
|
+
All types are exported for type-safe development:
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
import type {
|
|
589
|
+
// JWT & Auth
|
|
590
|
+
EnhancedJwtPayload,
|
|
591
|
+
ValidatedClaims,
|
|
592
|
+
TokenResponse,
|
|
593
|
+
|
|
594
|
+
// Identities (for sync)
|
|
595
|
+
Identity,
|
|
596
|
+
IdentitySession,
|
|
597
|
+
IdentityCreateInput,
|
|
598
|
+
IdentityUpdateInput,
|
|
599
|
+
|
|
600
|
+
// OAuth Flow
|
|
601
|
+
OAuthFlowConfig,
|
|
602
|
+
StartFlowResult,
|
|
603
|
+
StatePayload,
|
|
604
|
+
|
|
605
|
+
// Invitations
|
|
606
|
+
InvitationResponse,
|
|
607
|
+
PendingInvitation,
|
|
608
|
+
|
|
609
|
+
// Memberships
|
|
610
|
+
TenantMembership,
|
|
611
|
+
MembershipUser,
|
|
612
|
+
|
|
613
|
+
// Licenses
|
|
614
|
+
LicenseCheckResponse,
|
|
615
|
+
LicenseGrantResponse,
|
|
616
|
+
|
|
617
|
+
// Webhooks
|
|
618
|
+
SyncEvent,
|
|
619
|
+
SubjectCreatedEvent,
|
|
620
|
+
MemberJoinedEvent,
|
|
621
|
+
WebhookPayload,
|
|
622
|
+
} from '@authvital/sdk/server';
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Environment Variables
|
|
628
|
+
|
|
629
|
+
```bash
|
|
630
|
+
# Required
|
|
631
|
+
AUTHVITAL_HOST=https://auth.yourapp.com
|
|
632
|
+
AUTHVITAL_CLIENT_ID=your-client-id
|
|
633
|
+
AUTHVITAL_CLIENT_SECRET=your-client-secret
|
|
634
|
+
|
|
635
|
+
# Optional (for OAuth flow)
|
|
636
|
+
AUTHVITAL_REDIRECT_URI=https://yourapp.com/callback
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Documentation
|
|
642
|
+
|
|
643
|
+
For detailed documentation, see:
|
|
644
|
+
|
|
645
|
+
- **[JWT Validation Guide](./docs/jwt-validation.md)** - Deep dive into token verification
|
|
646
|
+
- **[Identity Sync Guide](./docs/identity-sync.md)** - Complete database mirroring setup
|
|
647
|
+
- **[Webhook Reference](./docs/webhooks.md)** - All events and payload schemas
|
|
648
|
+
- **[OAuth Flow Guide](./docs/oauth-flow.md)** - Server-side OAuth implementation
|
|
649
|
+
- **[API Reference](./docs/api-reference.md)** - Full SDK API documentation
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
## License
|
|
654
|
+
|
|
655
|
+
See [LICENSE](../../LICENSE) in the repository root for terms.
|
|
656
|
+
|
|
657
|
+
**TL;DR:** Free to use in your own projects. Modifications must be open-sourced. Commercial SaaS use requires written permission.
|