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 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.